diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 000000000..84d09ab79 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,100 @@ +name: Semgrep SAST + +# Report-only SAST gate (constructorfabric/insight#1797, split from #1463; umbrella #1478). +# +# Findings (p/default + the p/rust, p/csharp, p/python language packs) surface in the +# GitHub "Security -> Code scanning" tab (SARIF upload) +# AND as a count in the job summary, but they DO NOT block: `semgrep scan` runs WITHOUT +# `--error`. Flip to blocking once the baseline is triaged and clean (zero un-waived +# findings) by adding `--error` here and marking `sast` a required status check. +# +# Secret detection is intentionally excluded (`--exclude-rule generic.secrets...`); the +# separate TruffleHog gate owns secret scanning. + +on: + pull_request: + branches: [main] + schedule: + # Nightly full-tree baseline (independent of what any PR touched), 03:27 UTC. + - cron: "27 3 * * *" + workflow_dispatch: + +# A new push obsoletes any run still in flight for the same ref. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + sast: + name: sast + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read # checkout + security-events: write # upload SARIF to Code Scanning + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + persist-credentials: false + + - name: Semgrep scan (report-only — ratchet to blocking with --error once baseline is clean) + # Runs the digest-pinned Semgrep image via `docker run` (not a `container:` job) so the + # checkout and SARIF-upload JS actions keep the host runner's Node — the Semgrep image + # does not ship Node. No `--error`: findings are reported, never block. Secrets excluded + # (TruffleHog owns them). `.semgrepignore` in the repo root prunes build/dep artifacts. + run: | + docker run --rm \ + -v "${{ github.workspace }}:/src" \ + -w /src \ + semgrep/semgrep:1.131.0@sha256:6bd07d7b166b097e1384f41b94a62d8c8a26a4fff8713992c296e053310da01f \ + semgrep scan \ + --config p/default \ + --config p/rust \ + --config p/csharp \ + --config p/python \ + --exclude-rule generic.secrets.security.detected-generic-secret \ + --sarif --output semgrep.sarif \ + --metrics off + + - name: Summarize findings in the job summary + if: always() + # Null-guarded append (repo convention, cf. e2e-bronze-to-api.yml). Renders a + # severity + per-rule breakdown from the SARIF using the stdlib (no jq dependency). + run: | + [ -f semgrep.sarif ] || exit 0 + [ -n "${GITHUB_STEP_SUMMARY:-}" ] || exit 0 + python3 - semgrep.sarif "$GITHUB_STEP_SUMMARY" <<'PY' + import json, sys, collections + d = json.load(open(sys.argv[1])) + out = open(sys.argv[2], "a") + res = [x for r in d.get("runs", []) for x in r.get("results", [])] + total = len(res) + sev = collections.Counter((x.get("level") or "warning") for x in res) + rules = collections.Counter(x.get("ruleId", "?").split(".")[-1] for x in res) + w = out.write + w("## Semgrep SAST (report-only)\n\n") + w(f"**{total}** finding(s) from `p/default` + `p/rust`/`p/csharp`/`p/python` (secrets excluded — TruffleHog owns those). " + "This check does **not** block; full details in **Security -> Code scanning**.\n\n") + if total: + w("| Severity | Count |\n|---|---:|\n") + for s in ("error", "warning", "note"): + if sev.get(s): + w(f"| {s} | {sev[s]} |\n") + w("\n
Findings by rule\n\n| Count | Rule |\n|---:|---|\n") + for rid, n in rules.most_common(): + w(f"| {n} | `{rid}` |\n") + w("\n
\n") + w("\n_Report-only: becomes blocking when `--error` is added and the baseline is clean (#1797)._\n") + PY + + - name: Upload SARIF to GitHub Code Scanning + # Skip on fork PRs: they receive a read-only token and cannot upload to Code Scanning, + # which would otherwise red-X this report-only job. + if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + uses: github/codeql-action/upload-sarif@fb0994ef1c058010acf1efccff928b0a83b1ed54 # v4.32.6 + with: + sarif_file: semgrep.sarif + category: semgrep diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 000000000..140d90866 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,26 @@ +# Semgrep scan exclusions — build outputs and vendored dependencies only. +# Application AND test source stay in scope (constructorfabric/insight#1797). +# Syntax is .gitignore-style. Point/path/rule-class waivers live elsewhere: +# - per-finding: inline `// nosemgrep: ` / `# nosemgrep: ` + issue link +# - per-rule: `--exclude-rule` in .github/workflows/semgrep.yml (e.g. the TruffleHog boundary) + +# --- Rust build output --- +target/ + +# --- Python build/dep/caches --- +.venv/ +venv/ +__pycache__/ +*.pyc +*.egg-info/ +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ + +# --- Node / frontend build & deps --- +node_modules/ +dist/ +build/ + +# --- VCS / editor noise --- +.git/