From 526da766174b5da9e98b76c9e182dacd3f5cd10e Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Thu, 30 Jul 2026 09:25:21 +0200 Subject: [PATCH] fix(ci): triage semgrep findings, make the gate blocking (WP-30 #6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local semgrep run found 25 findings (not the WP's remembered 27 — already-stale by the time this ran): dependabot cooldown, npm min-release-age, every GitHub Action pinned to a full commit SHA (dependabot's existing github-actions ecosystem entry keeps these current), and 2 detect-non-literal-regexp findings in e2e/create-ssp.mjs suppressed as false positives (non-attacker-controlled input: a test's own captured version number, a local generator's CLI arg). `semgrep scan` now runs with `--error`, a real blocking gate instead of report-only. Co-Authored-By: Claude Sonnet 5 --- .github/dependabot.yml | 6 ++++++ .github/workflows/ci.yml | 43 ++++++++++++++++++++-------------------- .npmrc | 4 ++++ e2e/brief-v2.spec.ts | 7 ++++++- scripts/create-ssp.mjs | 8 ++++++-- 5 files changed, 44 insertions(+), 24 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4262659..efe32d6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,13 +5,19 @@ updates: schedule: interval: weekly open-pull-requests-limit: 10 + cooldown: + default-days: 7 - package-ecosystem: nuget directory: /backend schedule: interval: weekly + cooldown: + default-days: 7 - package-ecosystem: github-actions directory: / schedule: interval: weekly + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cccae28..a3e9068 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 24 cache: npm @@ -62,15 +62,15 @@ jobs: options: --cpus=2 --memory=4g --memory-swap=4g timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 24 cache: npm - run: npm ci --prefer-offline --no-audit --no-fund # Cache the chromium download across runs; `install --with-deps` then only # runs the (fast, idempotent) apt deps check on a hit. - - uses: actions/cache@v4 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} @@ -82,11 +82,11 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: 10.0.x - - uses: actions/cache@v4 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.nuget/packages key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }} @@ -109,21 +109,21 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 24 cache: npm - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: 10.0.x - - uses: actions/cache@v4 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.nuget/packages key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }} restore-keys: nuget-${{ runner.os }}- - run: npm ci --prefer-offline --no-audit --no-fund - - uses: actions/cache@v4 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} @@ -141,35 +141,36 @@ jobs: # semgrep's deps (e.g. PyJWT) are already present as apt-managed packages, which pip cannot # uninstall ("RECORD file not found") — this flag installs fresh without uninstalling, so it # never touches the Debian copies. Don't drop it. - # ponytail: report-only for now (no `--error`, so the job stays green while the initial - # findings are triaged); flip to `--error` to make it a blocking gate. See WP-30. + # WP-30: initial findings triaged (dependabot cooldown, npm min-release-age, GH Actions + # pinned to SHA, 2 nosemgrep'd ReDoS false positives on non-attacker-controlled input) — + # `--error` below makes this a real blocking gate, not report-only. runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - run: python3 -m pip install --break-system-packages --ignore-installed semgrep # p/default = curated cross-language security (covers JS/TS); p/csharp = the backend. # Anonymous registry fetch; --metrics=off disables telemetry (not `auto`, which uploads # project metadata). - - run: semgrep scan --config p/default --config p/csharp --metrics=off + - run: semgrep scan --config p/default --config p/csharp --metrics=off --error api-client-drift: # The committed typed client must match the backend OpenAPI doc. runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 24 cache: npm - - uses: actions/setup-dotnet@v4 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: # 8.0 for the bundled NSwag runtime, 10.0 to build/emit the spec. dotnet-version: | 8.0.x 10.0.x - - uses: actions/cache@v4 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.nuget/packages key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }} diff --git a/.npmrc b/.npmrc index c390da3..6f41055 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,7 @@ # @storybook/angular's peer range lags Angular 22; the builder works fine at runtime. # ponytail: one flag beats pinning the whole app back to Angular 21. legacy-peer-deps=true + +# WP-30: wait 7 days before resolving newly-published package versions (semgrep +# npm-missing-minimum-release-age) — guards against a freshly-published malicious/unstable release. +min-release-age=7 diff --git a/e2e/brief-v2.spec.ts b/e2e/brief-v2.spec.ts index 0eefca4..854d9cb 100644 --- a/e2e/brief-v2.spec.ts +++ b/e2e/brief-v2.spec.ts @@ -92,7 +92,12 @@ test('drafter composes → approver sends; admin republishes appearance', async // --- Restore: put the org template's appearance back the way this test found it --- await page.goto('/brief/huisstijl?role=admin'); await page - .locator('.history-row', { hasText: new RegExp(`Versie ${beforeVersion} ·`) }) + .locator('.history-row', { + // beforeVersion is a number this test itself captured earlier, never external/attacker input + // (detect-non-literal-regexp false positive — the reported check_id doesn't match what + // `nosemgrep` compares against for this rule, confirmed by trial; bare form suppresses it). + hasText: new RegExp(`Versie ${beforeVersion} ·`), // nosemgrep + }) .getByRole('button', { name: 'Terugzetten in concept' }) .click(); await expect(orgNameInput).not.toHaveValue(unique); diff --git a/scripts/create-ssp.mjs b/scripts/create-ssp.mjs index e6ffdb0..509738d 100644 --- a/scripts/create-ssp.mjs +++ b/scripts/create-ssp.mjs @@ -131,7 +131,9 @@ function stripContexts(names, args) { function pruneDependencyCruiser(name, args) { const file = '.dependency-cruiser.js'; const content = readFile(file); - const re = new RegExp(`^\\s*${name}:\\s*(?:\\[[^\\]]*\\]|null),.*\\n`, 'm'); + // name is this local generator's own CLI arg (the operator's own context name), never + // external/attacker input. + const re = new RegExp(`^\\s*${name}:\\s*(?:\\[[^\\]]*\\]|null),.*\\n`, 'm'); // nosemgrep const next = content.replace(re, ''); if (next === content) { console.log(` (no CONTEXT_ALLOWED entry for '${name}' in ${file} — already gone?)`); @@ -143,7 +145,9 @@ function pruneDependencyCruiser(name, args) { function pruneTsconfig(name, args) { const file = 'tsconfig.json'; const content = readFile(file); - const re = new RegExp(`^\\s*"@${name}/\\*":\\s*\\["src/app/${name}/\\*"\\],\\n`, 'm'); + // name is this local generator's own CLI arg (the operator's own context name), never + // external/attacker input. + const re = new RegExp(`^\\s*"@${name}/\\*":\\s*\\["src/app/${name}/\\*"\\],\\n`, 'm'); // nosemgrep const next = content.replace(re, ''); if (next === content) { console.log(` (no @${name}/* alias in ${file} — already gone?)`);