feat: reusable secret scanning, SAST, dependency and lint workflows

This commit is contained in:
James Johnson 2026-10-18 21:31:47 +01:00
parent c9ecafca2f
commit 1e0113c05f
No known key found for this signature in database
GPG key ID: 4DD5E522F8940E76
6 changed files with 257 additions and 11 deletions

View file

@ -0,0 +1,26 @@
# Known-vulnerable dependencies, via osv-scanner against the OSV database.
#
# Reads every lockfile and manifest it recognises (pnpm/npm/yarn, pip/uv/poetry,
# go.mod, NuGet, Cargo and more). Accepted vulnerabilities go in an
# osv-scanner.toml in the calling repo as [[IgnoredVulns]] entries with a
# reason and an ignoreUntil date, so an exception expires instead of
# silently living forever.
on:
workflow_call:
jobs:
osv-scanner:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: osv-scanner
run: |
osv-scanner --version | head -1
rc=0
osv-scanner scan source --recursive . || rc=$?
# 128 means no lockfile or manifest was found: nothing to check, not a failure.
if [ "$rc" -eq 128 ]; then
echo "No supported lockfiles or manifests found; nothing to scan."
exit 0
fi
exit "$rc"

103
.forgejo/workflows/lint.yml Normal file
View file

@ -0,0 +1,103 @@
# Lint gates, each switched on by the files present in the calling repo.
#
# shell scripts shellcheck (*.sh, *.bash)
# Python ruff check (repo's ruff/pyproject config if any)
# Dockerfiles hadolint (repo's .hadolint.yaml if any)
# YAML yamllint (repo's .yamllint if any, else relaxed)
# CI workflows actionlint (.forgejo/ and .github/ workflows)
# OpenTofu/HCL tofu fmt + tflint (repo's .tflint.hcl if any)
#
# Every linter runs even if an earlier one failed, so one push shows every
# problem; the job fails if any of them did. Turn one off with its input.
on:
workflow_call:
inputs:
shellcheck:
type: boolean
default: true
shellcheck-severity:
description: Lowest shellcheck severity that fails the job (error, warning, info, style).
type: string
default: warning
ruff:
type: boolean
default: true
ruff-format:
description: Also require `ruff format --check` to pass.
type: boolean
default: false
hadolint:
type: boolean
default: true
yamllint:
type: boolean
default: true
actionlint:
type: boolean
default: true
terraform:
type: boolean
default: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: shellcheck
if: ${{ !cancelled() && inputs.shellcheck }}
env:
SEVERITY: ${{ inputs.shellcheck-severity }}
run: |
if [ -z "$(git ls-files '*.sh' '*.bash')" ]; then echo "No shell scripts; skipping."; exit 0; fi
git ls-files -z '*.sh' '*.bash' | xargs -0 shellcheck --severity="$SEVERITY"
- name: ruff
if: ${{ !cancelled() && inputs.ruff }}
env:
FORMAT: ${{ inputs.ruff-format }}
run: |
if [ -z "$(git ls-files '*.py' '*.pyi')" ]; then echo "No Python; skipping."; exit 0; fi
ruff --version
ruff check --output-format=full .
if [ "$FORMAT" = "true" ]; then ruff format --check .; fi
- name: hadolint
if: ${{ !cancelled() && inputs.hadolint }}
run: |
set -- 'Dockerfile' '*/Dockerfile' 'Dockerfile.*' '*/Dockerfile.*' '*.Dockerfile' 'Containerfile' '*/Containerfile'
if [ -z "$(git ls-files "$@")" ]; then echo "No Dockerfiles; skipping."; exit 0; fi
git ls-files -z "$@" | xargs -0 hadolint --failure-threshold warning
- name: yamllint
if: ${{ !cancelled() && inputs.yamllint }}
run: |
if [ -z "$(git ls-files '*.yml' '*.yaml')" ]; then echo "No YAML; skipping."; exit 0; fi
cfg=""
for f in .yamllint .yamllint.yml .yamllint.yaml; do if [ -f "$f" ]; then cfg="$f"; fi; done
# Without a repo config: relaxed rules, and no line-length check,
# which mostly flags long URLs and Kubernetes annotations.
if [ -n "$cfg" ]; then set -- -c "$cfg"
else set -- -d '{extends: relaxed, rules: {line-length: disable}}'; fi
git ls-files -z '*.yml' '*.yaml' | xargs -0 yamllint -f parsable "$@"
- name: actionlint
if: ${{ !cancelled() && inputs.actionlint }}
run: |
set -- '.forgejo/workflows/*.yml' '.forgejo/workflows/*.yaml' '.github/workflows/*.yml' '.github/workflows/*.yaml'
if [ -z "$(git ls-files "$@")" ]; then echo "No workflow files; skipping."; exit 0; fi
# Forgejo needs runs-on on a job that calls a cross-repo reusable
# workflow (the runner expands it, not the server), which GitHub
# forbids. Ignore that one GitHub-only rule.
git ls-files -z "$@" | xargs -0 actionlint -shellcheck= -ignore '"runs-on" is not available'
- name: OpenTofu fmt and tflint
if: ${{ !cancelled() && inputs.terraform }}
run: |
if [ -z "$(git ls-files '*.tf')" ]; then echo "No .tf files; skipping."; exit 0; fi
rc=0
tofu fmt -check -recursive -diff || rc=1
if [ -f .tflint.hcl ]; then tflint --init; fi
tflint --recursive --format compact || rc=1
exit "$rc"

View file

@ -1,11 +0,0 @@
on:
workflow_call:
inputs:
msg:
type: string
default: hello
jobs:
probe:
runs-on: ubuntu-latest
steps:
- run: echo "reusable probe ran in ${GITHUB_REPOSITORY} msg=${{ inputs.msg }}"

View file

@ -0,0 +1,28 @@
# Static analysis with semgrep.
#
# Defaults to the curated p/default ruleset rather than `--config auto`, because
# auto requires sending metrics to semgrep.dev. Paths are excluded with a
# .semgrepignore in the calling repo; a single finding can be suppressed inline
# with a `nosemgrep: <rule-id>` comment.
on:
workflow_call:
inputs:
config:
description: Space-separated semgrep configs (registry packs or paths in the repo).
type: string
default: p/default
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: semgrep
env:
CONFIGS: ${{ inputs.config }}
SEMGREP_SEND_METRICS: "off"
run: |
semgrep --version
set -- scan --error --metrics=off --disable-version-check
for c in $CONFIGS; do set -- "$@" --config "$c"; done
semgrep "$@"

View file

@ -0,0 +1,34 @@
# Secret scanning with gitleaks over the repository's full git history.
#
# Full history rather than the pushed range: a push range is unreliable on
# force-pushes and first pushes, and these repos are small enough that a full
# scan takes seconds. A secret that was committed and later deleted is still
# exposed, so history is exactly what needs scanning.
#
# The calling repo controls what counts as a finding:
# .gitleaks.toml extra rules or allowlists, picked up automatically
# .gitleaksignore fingerprints of accepted findings, one per line, as
# printed in the "Fingerprint:" line of this job's output
on:
workflow_call:
inputs:
log-opts:
description: Extra `git log` options to narrow the scan, e.g. a commit range. Empty scans all history.
type: string
default: ""
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: gitleaks
env:
LOG_OPTS: ${{ inputs.log-opts }}
run: |
gitleaks version
set -- git --redact --verbose --no-banner --exit-code 1
if [ -n "$LOG_OPTS" ]; then set -- "$@" --log-opts "$LOG_OPTS"; fi
gitleaks "$@" .