From be04d793e53fcc826f77ddfe84baf5018b55fb3f Mon Sep 17 00:00:00 2001 From: Konstantin Tursunov Date: Fri, 17 Jul 2026 20:23:18 +0300 Subject: [PATCH 1/5] ci(security): add report-only Semgrep SAST gate (#1797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standalone semgrep.yml running p/default (secrets excluded — TruffleHog owns those) in report-only mode: findings upload to Code Scanning (SARIF) and a job-summary count, but no --error so the check never blocks. PR + nightly + dispatch triggers, digest-pinned image, SHA-pinned actions. Adds .semgrepignore for build/dep artifacts and waives the known JWT ValidateLifetime finding inline (#346). Split from #1463; part of #1478. Signed-off-by: Konstantin Tursunov --- .github/workflows/semgrep.yml | 81 +++++++++++++++++++ .semgrepignore | 26 ++++++ .../src/Insight.Identity.Api/Program.cs | 2 +- 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/semgrep.yml create mode 100644 .semgrepignore diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 000000000..12c17c9f0 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,81 @@ +name: Semgrep SAST + +# Report-only SAST gate (constructorfabric/insight#1797, split from #1463; umbrella #1478). +# +# p/default findings 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + 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 \ + --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). Counts SARIF + # results with the stdlib json module (no jq dependency). + run: | + if [ -f semgrep.sarif ] && [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + count=$(python3 -c "import json; d=json.load(open('semgrep.sarif')); print(sum(len(r.get('results', [])) for r in d.get('runs', [])))" 2>/dev/null || echo "?") + { + echo "## Semgrep SAST (report-only)" + echo "" + echo "**${count}** finding(s) from \`p/default\` — see the **Security → Code scanning** tab for details." + echo "" + echo "_Report-only: this check never blocks. It becomes blocking when \`--error\` is added and the baseline is clean (#1797)._" + } >> "$GITHUB_STEP_SUMMARY" + fi + + - 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@fbcef3fba75224ed6476919775448de80b23cb15 # v3.32.0 + 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/ diff --git a/src/backend/services/identity/src/Insight.Identity.Api/Program.cs b/src/backend/services/identity/src/Insight.Identity.Api/Program.cs index 5c0450cbc..6da759f98 100644 --- a/src/backend/services/identity/src/Insight.Identity.Api/Program.cs +++ b/src/backend/services/identity/src/Insight.Identity.Api/Program.cs @@ -153,7 +153,7 @@ { ValidateIssuer = false, ValidateAudience = false, - ValidateLifetime = false, + ValidateLifetime = false, // nosemgrep: csharp.lang.security.ad.jwt-tokenvalidationparameters-no-expiry-validation.jwt-tokenvalidationparameters-no-expiry-validation -- known dev-auth skeleton; full validation tracked in #346 ValidateIssuerSigningKey = false, RequireSignedTokens = false, // Accept any token shape; do not enforce signature. Returning From bf86ba5a99cad5c79f51794882c9a60d22404f54 Mon Sep 17 00:00:00 2001 From: Konstantin Tursunov Date: Fri, 17 Jul 2026 20:52:51 +0300 Subject: [PATCH 2/5] ci(security): move Semgrep gate to Node-24 actions; robust JWT waiver - Bump actions/checkout v4.2.2 -> v5.0.1 and codeql-action/upload-sarif v3.32.0 -> v4.32.6 (SHA-pinned) to clear the Node-20 deprecation and the CodeQL Action v3 sunset warnings from the first run. - Switch the JWT #346 waiver from an inline rule-id nosemgrep to a bare // nosemgrep: under the full p/default ruleset the fully-namespaced rule id does not match, so no suppression was emitted and the alert stayed open. The bare form reliably emits SARIF suppressions[inSource], which Code Scanning honors as a dismissed alert. Signed-off-by: Konstantin Tursunov --- .github/workflows/semgrep.yml | 4 ++-- .../services/identity/src/Insight.Identity.Api/Program.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 12c17c9f0..d97a2c3ef 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -35,7 +35,7 @@ jobs: contents: read # checkout security-events: write # upload SARIF to Code Scanning steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false @@ -75,7 +75,7 @@ jobs: # 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@fbcef3fba75224ed6476919775448de80b23cb15 # v3.32.0 + uses: github/codeql-action/upload-sarif@fb0994ef1c058010acf1efccff928b0a83b1ed54 # v4.32.6 with: sarif_file: semgrep.sarif category: semgrep diff --git a/src/backend/services/identity/src/Insight.Identity.Api/Program.cs b/src/backend/services/identity/src/Insight.Identity.Api/Program.cs index 6da759f98..13d82237f 100644 --- a/src/backend/services/identity/src/Insight.Identity.Api/Program.cs +++ b/src/backend/services/identity/src/Insight.Identity.Api/Program.cs @@ -153,7 +153,7 @@ { ValidateIssuer = false, ValidateAudience = false, - ValidateLifetime = false, // nosemgrep: csharp.lang.security.ad.jwt-tokenvalidationparameters-no-expiry-validation.jwt-tokenvalidationparameters-no-expiry-validation -- known dev-auth skeleton; full validation tracked in #346 + ValidateLifetime = false, // nosemgrep -- JWT lifetime validation intentionally disabled in the dev-auth skeleton; tracked in #346 ValidateIssuerSigningKey = false, RequireSignedTokens = false, // Accept any token shape; do not enforce signature. Returning From 74356a399ad501ba5a9b3bd191158b072de71a13 Mon Sep 17 00:00:00 2001 From: Konstantin Tursunov Date: Fri, 17 Jul 2026 20:57:29 +0300 Subject: [PATCH 3/5] =?UTF-8?q?ci(security):=20revert=20JWT=20inline=20wai?= =?UTF-8?q?ver=20=E2=80=94=20keep=20#1797=20CI-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inline nosemgrep does not dismiss a GitHub Code Scanning alert (GitHub does not honor third-party SARIF suppressions[inSource]), so the marker touched product code for no effect. Restore Program.cs; the JWT #346 finding — like the rest of the report-only baseline — will be dismissed in the Code Scanning UI/API (state=dismissed, won't-fix, ref #346) during the Phase-6 triage, before the --error flip. Signed-off-by: Konstantin Tursunov --- .../services/identity/src/Insight.Identity.Api/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/services/identity/src/Insight.Identity.Api/Program.cs b/src/backend/services/identity/src/Insight.Identity.Api/Program.cs index 13d82237f..5c0450cbc 100644 --- a/src/backend/services/identity/src/Insight.Identity.Api/Program.cs +++ b/src/backend/services/identity/src/Insight.Identity.Api/Program.cs @@ -153,7 +153,7 @@ { ValidateIssuer = false, ValidateAudience = false, - ValidateLifetime = false, // nosemgrep -- JWT lifetime validation intentionally disabled in the dev-auth skeleton; tracked in #346 + ValidateLifetime = false, ValidateIssuerSigningKey = false, RequireSignedTokens = false, // Accept any token shape; do not enforce signature. Returning From 7c5f721b7ab7bf156a4fe0bdd0fb65ec7e0f69a9 Mon Sep 17 00:00:00 2001 From: Konstantin Tursunov Date: Fri, 17 Jul 2026 21:08:42 +0300 Subject: [PATCH 4/5] ci(security): richer Semgrep job-summary (severity + per-rule breakdown) Replace the bare finding count with a severity table plus a collapsible per-rule breakdown rendered from the SARIF (stdlib only, no jq). Signed-off-by: Konstantin Tursunov --- .github/workflows/semgrep.yml | 39 ++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index d97a2c3ef..0c0d4f98d 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -57,19 +57,34 @@ jobs: - name: Summarize findings in the job summary if: always() - # Null-guarded append (repo convention, cf. e2e-bronze-to-api.yml). Counts SARIF - # results with the stdlib json module (no jq dependency). + # 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: | - if [ -f semgrep.sarif ] && [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then - count=$(python3 -c "import json; d=json.load(open('semgrep.sarif')); print(sum(len(r.get('results', [])) for r in d.get('runs', [])))" 2>/dev/null || echo "?") - { - echo "## Semgrep SAST (report-only)" - echo "" - echo "**${count}** finding(s) from \`p/default\` — see the **Security → Code scanning** tab for details." - echo "" - echo "_Report-only: this check never blocks. It becomes blocking when \`--error\` is added and the baseline is clean (#1797)._" - } >> "$GITHUB_STEP_SUMMARY" - fi + [ -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` (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, From 0b3ce8256d83699f85cb9e91f07c733004484fa2 Mon Sep 17 00:00:00 2001 From: Konstantin Tursunov Date: Fri, 17 Jul 2026 21:15:32 +0300 Subject: [PATCH 5/5] ci(security): broaden Semgrep to p/rust, p/csharp, p/python p/default has thin Rust/C# security coverage (0 findings across 184 Rust files); add the dedicated language packs so the actual backend code is scanned, not just CI/YAML. Surfaces real Rust (unsafe-usage, temp-dir) and C# (unsigned-security-token) findings p/default missed. Still report-only. Signed-off-by: Konstantin Tursunov --- .github/workflows/semgrep.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 0c0d4f98d..84d09ab79 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -2,7 +2,8 @@ name: Semgrep SAST # Report-only SAST gate (constructorfabric/insight#1797, split from #1463; umbrella #1478). # -# p/default findings surface in the GitHub "Security -> Code scanning" tab (SARIF upload) +# 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. @@ -51,6 +52,9 @@ jobs: 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 @@ -72,7 +76,7 @@ jobs: 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` (secrets excluded — TruffleHog owns those). " + 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")