From 645381cff967e927773fa882cec6c684d6859638 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Date: Mon, 27 Jul 2026 11:33:27 -0300 Subject: [PATCH 1/6] chore(security): automated monthly SBOM & VEX report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the validated supply-chain automation to openplc-editor (public repo — no AI triage; the Claude step is skipped gracefully with no token, deterministic SBOM + report + PR still run). Regenerates the SBOM (CycloneDX+SPDX), scans OSV honoring the VEX baseline, renders the report, opens a monthly PR. Baseline suppresses the 146 known not-affected/mitigated IDs; ws & socket.io-parser (debug-WebSocket DoS, P2) are left visible for action. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AejAiL4tfXCyMhwtjY4vFT --- .github/workflows/security-monthly.yml | 134 +++++ .gitignore | 3 + osv-scanner.toml | 740 +++++++++++++++++++++++++ scripts/build-report.mjs | 190 +++++++ scripts/cdx-to-spdx.mjs | 128 +++++ scripts/gen-osv-ignores.mjs | 59 ++ scripts/generate-sbom.sh | 63 +++ scripts/scan-vulns.mjs | 27 + security/README.md | 52 ++ security/report-config.json | 47 ++ 10 files changed, 1443 insertions(+) create mode 100644 .github/workflows/security-monthly.yml create mode 100644 osv-scanner.toml create mode 100644 scripts/build-report.mjs create mode 100644 scripts/cdx-to-spdx.mjs create mode 100644 scripts/gen-osv-ignores.mjs create mode 100755 scripts/generate-sbom.sh create mode 100644 scripts/scan-vulns.mjs create mode 100644 security/README.md create mode 100644 security/report-config.json diff --git a/.github/workflows/security-monthly.yml b/.github/workflows/security-monthly.yml new file mode 100644 index 000000000..905e45483 --- /dev/null +++ b/.github/workflows/security-monthly.yml @@ -0,0 +1,134 @@ +name: Security — monthly SBOM & VEX report + +# Runs on GitHub's servers (not on anyone's laptop). Every month it regenerates +# the SBOM, scans dependencies, has Claude triage any NEW advisory and write the +# report from the versioned template, drops everything into security//, +# and opens a PR for the team to review. Nothing is merged automatically. +# +# Auth: Claude runs on your Claude subscription via CLAUDE_CODE_OAUTH_TOKEN +# (from `claude setup-token`) — no pay-per-token API key. See security/README.md. + +on: + schedule: + - cron: "0 6 1 * *" # 06:00 UTC on the 1st of every month + workflow_dispatch: {} # manual "Run workflow" button + +permissions: + contents: write + pull-requests: write + id-token: write # required by claude-code-action (OIDC) + +concurrency: + group: security-monthly + cancel-in-progress: false + +jobs: + report: + runs-on: ubuntu-latest + # Mapped here because the `secrets` context is NOT allowed in a step-level + # `if:` — the Claude step gates on `env.CLAUDE_CODE_OAUTH_TOKEN` instead. + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + steps: + - name: Checkout (with submodules for vendored C libs) + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Month stamp + id: m + run: echo "month=$(date -u +%Y-%m)" >> "$GITHUB_OUTPUT" + + - uses: actions/setup-node@v4 + with: { node-version: "22" } + - name: Enable pnpm + run: corepack enable + - uses: actions/setup-python@v5 + with: { python-version: "3.12" } + + # ---- deterministic: SBOM (CycloneDX + SPDX + components.csv) ---- + - name: Generate SBOM + run: bash scripts/generate-sbom.sh + + # ---- deterministic: vulnerability scan (OSV, honoring the VEX baseline) ---- + - name: Install osv-scanner + run: | + curl -sSfL "https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64" -o /usr/local/bin/osv-scanner + chmod +x /usr/local/bin/osv-scanner + - name: Scan + run: | + CFG=""; [ -f osv-scanner.toml ] && CFG="--config=osv-scanner.toml" + osv-scanner scan $CFG --recursive --format=json --output=/tmp/osv.json . || true + node scripts/scan-vulns.mjs /tmp/osv.json sbom/vulnerabilities.csv + + # ---- judgment: Claude triages the delta + updates the report data ---- + # Runs on YOUR subscription (no API key). Skipped gracefully if the token + # isn't configured yet — the deterministic report still builds below. + - name: Claude — triage new advisories & update report data + if: ${{ env.CLAUDE_CODE_OAUTH_TOKEN != '' }} + continue-on-error: true + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: > + You are updating this repository's monthly security report DATA only. + Inputs: the fresh scan at /tmp/osv.json and the SBOM under sbom/. + For every advisory present in /tmp/osv.json that is NOT already listed + in osv-scanner.toml, read the source code and decide reachability + (is the vulnerable function called? is its input attacker-controlled?). + Then: + (a) if NOT exploitable, add an [[IgnoredVulns]] entry to osv-scanner.toml + with a CISA VEX reason and an ignoreUntil date ~3 months out; + (b) if exploitable, add/refresh the corresponding entry in + security/report-config.json (affected[]) with the fix version; + (c) keep the counts and the notAffected[]/mitigated[] arrays in + security/report-config.json consistent with the scan. + EDIT ONLY these two files: osv-scanner.toml and + security/report-config.json. Do not touch anything else. If there are + no new advisories, make no changes. + + # ---- deterministic: render the report from the (possibly updated) data ---- + - name: Build report (md + html) + run: | + NAME=$(node -p "require('./package.json').name") + node scripts/build-report.mjs \ + --config security/report-config.json \ + --cdx "sbom/$NAME.cdx.json" \ + --out "security/${{ steps.m.outputs.month }}" \ + --date "${{ steps.m.outputs.month }}" + + - name: Render PDF + uses: browser-actions/setup-chrome@v1 + id: chrome + - name: Assemble dated folder + run: | + NAME=$(node -p "require('./package.json').name") + MONTH="${{ steps.m.outputs.month }}" + DIR="security/$MONTH"; mkdir -p "$DIR/sbom" + cp "sbom/$NAME".cdx.json "sbom/$NAME".spdx.json "sbom/$NAME".components.csv sbom/vulnerabilities.csv "$DIR/sbom/" + REPORT="$DIR/$(ls "$DIR" | grep -E 'Security-Report\.html$')" + # --no-sandbox / --disable-dev-shm-usage: Chrome's zygote sandbox aborts + # (SIGABRT) on GitHub runners; required for headless Chrome in CI. + "${{ steps.chrome.outputs.chrome-path }}" --headless=new --no-sandbox --disable-dev-shm-usage \ + --disable-gpu --no-pdf-header-footer \ + --run-all-compositor-stages-before-draw --virtual-time-budget=5000 \ + --print-to-pdf="${REPORT%.html}.pdf" "file://$PWD/$REPORT" + ln -sfn "$MONTH" security/latest + + # ---- delivery: open the PR for review ---- + - name: Open Pull Request + uses: peter-evans/create-pull-request@v6 + with: + branch: chore/security-${{ steps.m.outputs.month }} + title: "chore(security): monthly SBOM & VEX report — ${{ steps.m.outputs.month }}" + labels: supply-chain, security + commit-message: "chore(security): SBOM & VEX report ${{ steps.m.outputs.month }}" + body: | + Automated monthly supply-chain snapshot for **${{ github.event.repository.name }}** — `security/${{ steps.m.outputs.month }}/`. + + - SBOM regenerated (CycloneDX + SPDX) from the current lockfile. + - Dependencies scanned against OSV (same source as Dependabot), honoring `osv-scanner.toml` (the VEX baseline). + - New advisories (if any) were triaged by Claude and reflected in `report-config.json` / `osv-scanner.toml` — **review those diffs**. + + Nothing is merged automatically. Approve to archive this month's snapshot. diff --git a/.gitignore b/.gitignore index ff41e9cd4..ee7ea1d0b 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ resources/strucpp/ playwright-report /blob-report/ /playwright/.cache/ + +# transient SBOM build output (canonical copy lives in security//sbom/) +/sbom/ diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 000000000..e6e79233c --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,740 @@ +# osv-scanner suppression baseline for openplc-editor = our VEX "not affected" +# (+ mitigated) decisions from the SBOM & Vulnerability Report. The advisories +# that REQUIRE action (ws, socket.io-parser — debug-WebSocket DoS via socket.io- +# client) are intentionally NOT listed here so they keep alerting until bumped. +# electron-builder prunes devDependencies from the shipped asar; the hardened +# Electron config and default-config DOMPurify constrain the reachable surface. +# +# Regenerated from a live osv-scanner scan of the CycloneDX SBOM. +# Each entry expires (ignoreUntil) so suppressions are re-reviewed quarterly. +# Full rationale: security//OpenPLC-Editor-Desktop-Security-Report.md. + +[[IgnoredVulns]] +id = "GHSA-2328-f5f3-gj25" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [node-forge] — see security report" + +[[IgnoredVulns]] +id = "GHSA-23c5-xmqv-rm74" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_cannot_be_controlled_by_adversary]: Node/build-only code path not exercised with adversary input in the running application [minimatch] — see security report" + +[[IgnoredVulns]] +id = "GHSA-23hp-3jrh-7fpw" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-25h7-pfq9-p65f" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [flatted] — see security report" + +[[IgnoredVulns]] +id = "GHSA-2g4f-4pwh-qvx6" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [ajv] — see security report" + +[[IgnoredVulns]] +id = "GHSA-2p49-hgcm-8545" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [svgo] — see security report" + +[[IgnoredVulns]] +id = "GHSA-34x7-hfp2-rc4v" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-35jp-ww65-95wh" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-37ch-88jc-xwx2" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_cannot_be_controlled_by_adversary]: Node/build-only code path not exercised with adversary input in the running application [path-to-regexp] — see security report" + +[[IgnoredVulns]] +id = "GHSA-38r7-794h-5758" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [webpack] — see security report" + +[[IgnoredVulns]] +id = "GHSA-395f-4hp3-45gv" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [shell-quote] — see security report" + +[[IgnoredVulns]] +id = "GHSA-39q2-94rc-95cp" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-3c8v-cfp5-9885" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-3g43-6gmg-66jw" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-3jxr-9vmj-r5cp" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [brace-expansion] — see security report" + +[[IgnoredVulns]] +id = "GHSA-3p68-rc4w-qgx5" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-3ppc-4f35-3m26" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_cannot_be_controlled_by_adversary]: Node/build-only code path not exercised with adversary input in the running application [minimatch] — see security report" + +[[IgnoredVulns]] +id = "GHSA-3v7f-55p6-f55p" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_cannot_be_controlled_by_adversary]: Node/build-only code path not exercised with adversary input in the running application [picomatch] — see security report" + +[[IgnoredVulns]] +id = "GHSA-3w6x-2g7m-8v23" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-42h9-826w-cgv3" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-445q-vr5w-6q77" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-4p4r-m79c-wq3v" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-4x5r-pxfx-6jf8" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [@babel/core] — see security report" + +[[IgnoredVulns]] +id = "GHSA-52cp-r559-cp3m" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [js-yaml] — see security report" + +[[IgnoredVulns]] +id = "GHSA-52f5-9888-hmc6" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tmp] — see security report" + +[[IgnoredVulns]] +id = "GHSA-532v-xpq5-8h95" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-554w-wpv2-vw27" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [node-forge] — see security report" + +[[IgnoredVulns]] +id = "GHSA-5c6j-r48x-rmvq" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [serialize-javascript] — see security report" + +[[IgnoredVulns]] +id = "GHSA-5c9x-8gcm-mpgx" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-5gfm-wpxj-wjgq" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [node-forge] — see security report" + +[[IgnoredVulns]] +id = "GHSA-5j98-mcp5-4vw2" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [glob] — see security report" + +[[IgnoredVulns]] +id = "GHSA-5m6q-g25r-mvwx" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [node-forge] — see security report" + +[[IgnoredVulns]] +id = "GHSA-5rqw-r77c-jp79" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-5wm8-gmm8-39j9" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [fast-xml-builder] — see security report" + +[[IgnoredVulns]] +id = "GHSA-62hf-57xw-28j9" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-64mm-vxmg-q3vj" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [http-proxy-middleware] — see security report" + +[[IgnoredVulns]] +id = "GHSA-65ch-62r8-g69g" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [node-forge] — see security report" + +[[IgnoredVulns]] +id = "GHSA-6chq-wfr3-2hj9" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-6g55-p6wh-862q" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [postcss] — see security report" + +[[IgnoredVulns]] +id = "GHSA-6rw7-vpxm-498p" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [qs] — see security report" + +[[IgnoredVulns]] +id = "GHSA-76mc-f452-cxcm" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-777c-7fjr-54vf" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-79cf-xcqc-c78w" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [webpack-dev-server] — see security report" + +[[IgnoredVulns]] +id = "GHSA-7g7r-gx96-252g" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [app-builder-lib] — see security report" + +[[IgnoredVulns]] +id = "GHSA-7h2j-956f-4vf2" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [@isaacs/brace-expansion] — see security report" + +[[IgnoredVulns]] +id = "GHSA-7mvr-c777-76hp" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [playwright] — see security report" + +[[IgnoredVulns]] +id = "GHSA-7q8q-rj6j-mhjq" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-7r86-cg39-jmmj" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_cannot_be_controlled_by_adversary]: Node/build-only code path not exercised with adversary input in the running application [minimatch] — see security report" + +[[IgnoredVulns]] +id = "GHSA-8337-3p73-46f4" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-83g3-92jg-28cx" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-848j-6mx2-7j84" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_cannot_be_controlled_by_adversary]: Node/build-only code path not exercised with adversary input in the running application [elliptic] — see security report" + +[[IgnoredVulns]] +id = "GHSA-898c-q2cr-xwhg" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-8fgc-7cc6-rx7x" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [webpack] — see security report" + +[[IgnoredVulns]] +id = "GHSA-8qq5-rm4j-mr97" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-8x5q-pvf5-64mp" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-8x88-c5mf-7j5w" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-9899-m83m-qhpj" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-9ppj-qmqm-q256" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-9w97-2464-8783" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-9wfr-w7mm-pc7f" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-c27g-q93r-2cwf" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [launch-editor] — see security report" + +[[IgnoredVulns]] +id = "GHSA-c2c7-rcm5-vvqj" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_cannot_be_controlled_by_adversary]: Node/build-only code path not exercised with adversary input in the running application [picomatch] — see security report" + +[[IgnoredVulns]] +id = "GHSA-c2j3-45gr-mqc4" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-cj63-jhhr-wcxv" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-cjmm-f4jc-qw8r" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-cmwh-pvxp-8882" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-crv5-9vww-q3g8" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-f23m-r3pf-42rh" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [lodash] — see security report" + +[[IgnoredVulns]] +id = "GHSA-f37v-82c4-4x64" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-f3pv-wv63-48x8" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-f5vj-f2hx-8m93" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [webpack-dev-server] — see security report" + +[[IgnoredVulns]] +id = "GHSA-f886-m6hf-6m8v" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [brace-expansion] — see security report" + +[[IgnoredVulns]] +id = "GHSA-fv7c-fp4j-7gwp" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [@babel/plugin-transform-modules-systemjs] — see security report" + +[[IgnoredVulns]] +id = "GHSA-fvcv-3m26-pcqx" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-gh4j-gqv2-49f6" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [fast-xml-parser] — see security report" + +[[IgnoredVulns]] +id = "GHSA-gvmj-g25r-r7wr" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-gvwx-54wh-qm9j" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-h67p-54hq-rp68" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [js-yaml] — see security report" + +[[IgnoredVulns]] +id = "GHSA-h7mw-gpvr-xq4m" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-h8r8-wccr-v5f2" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-hfxv-24rg-xrqf" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-hmw2-7cc7-3qxx" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_cannot_be_controlled_by_adversary]: Node/build-only code path not exercised with adversary input in the running application [form-data] — see security report" + +[[IgnoredVulns]] +id = "GHSA-hpcv-96wg-7vj8" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-j5f8-grm9-p9fc" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-jfqx-fxh3-c62j" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-jjp3-mq3x-295m" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-jqh4-m9w3-8hp9" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-m28w-2pqf-7qgj" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [webpack-dev-server] — see security report" + +[[IgnoredVulns]] +id = "GHSA-m7pr-hjqh-92cm" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-mh29-5h37-fv8m" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [js-yaml] — see security report" + +[[IgnoredVulns]] +id = "GHSA-mh99-v99m-4gvg" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [brace-expansion] — see security report" + +[[IgnoredVulns]] +id = "GHSA-mmx7-hfxf-jppx" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-mp7j-qc5w-4988" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [websocket-driver] — see security report" + +[[IgnoredVulns]] +id = "GHSA-mwf2-3pr3-8698" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-mwmh-mq4g-g6gr" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-mx8g-39q3-5c79" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [webpack-dev-server] — see security report" + +[[IgnoredVulns]] +id = "GHSA-p2f4-r6v6-j797" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [builder-util-runtime] — see security report" + +[[IgnoredVulns]] +id = "GHSA-p92q-9vqr-4j8v" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-pf86-5x62-jrwf" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-ph9p-34f9-6g65" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tmp] — see security report" + +[[IgnoredVulns]] +id = "GHSA-pmv8-rq9r-6j72" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-pmwg-cvhr-8vh7" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-ppp5-5v6c-4jwp" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [node-forge] — see security report" + +[[IgnoredVulns]] +id = "GHSA-q67f-28xg-22rw" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [node-forge] — see security report" + +[[IgnoredVulns]] +id = "GHSA-q8mj-m7cp-5q26" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [qs] — see security report" + +[[IgnoredVulns]] +id = "GHSA-q8qp-cvcw-x6jj" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-qffp-2rhf-9h96" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-qj8w-gfj5-8c6v" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [serialize-javascript] — see security report" + +[[IgnoredVulns]] +id = "GHSA-qx2v-qp2m-jg93" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [postcss] — see security report" + +[[IgnoredVulns]] +id = "GHSA-r28c-9q8g-f849" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [postcss] — see security report" + +[[IgnoredVulns]] +id = "GHSA-r292-9mhp-454m" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-r47g-fvhr-h676" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-r4q5-vmmm-2653" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_cannot_be_controlled_by_adversary]: Node/build-only code path not exercised with adversary input in the running application [follow-redirects] — see security report" + +[[IgnoredVulns]] +id = "GHSA-r5fr-rjxr-66jc" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [lodash] — see security report" + +[[IgnoredVulns]] +id = "GHSA-r5p7-gp4j-qhrx" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-r6q2-hw4h-h46w" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-rf6f-7fwh-wjgh" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [flatted] — see security report" + +[[IgnoredVulns]] +id = "GHSA-rp9w-3fw7-7cwq" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-v2v4-37r5-5v8g" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_cannot_be_controlled_by_adversary]: Node/build-only code path not exercised with adversary input in the running application [ip-address] — see security report" + +[[IgnoredVulns]] +id = "GHSA-v2wj-7wpq-c8vv" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-v422-hmwv-36x6" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_cannot_be_controlled_by_adversary]: Node/build-only code path not exercised with adversary input in the running application [body-parser] — see security report" + +[[IgnoredVulns]] +id = "GHSA-v56q-mh7h-f735" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [immutable] — see security report" + +[[IgnoredVulns]] +id = "GHSA-v6wh-96g9-6wx3" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [launch-editor] — see security report" + +[[IgnoredVulns]] +id = "GHSA-v8jm-5vwx-cfxm" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-v9jr-rg53-9pgp" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-vf2m-468p-8v99" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-vhxf-7vqr-mrjg" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-vmf3-w455-68vh" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-vmqv-hx8q-j7mg" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-vpq2-c234-7xj6" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [@tootallnate/once] — see security report" + +[[IgnoredVulns]] +id = "GHSA-vxr8-fq34-vvx9" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-w5hq-g745-h8pq" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [uuid] — see security report" + +[[IgnoredVulns]] +id = "GHSA-w7fw-mjwx-w883" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [qs] — see security report" + +[[IgnoredVulns]] +id = "GHSA-w7jw-789q-3m8p" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [shell-quote] — see security report" + +[[IgnoredVulns]] +id = "GHSA-w8wr-v893-vjvp" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [tar] — see security report" + +[[IgnoredVulns]] +id = "GHSA-w9j2-pvgh-6h63" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-wf6x-7x77-mvgw" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [immutable] — see security report" + +[[IgnoredVulns]] +id = "GHSA-x4vx-rjvf-j5p4" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: called with the default configuration over app-generated markup (search highlights); keep >= 3.4.12 [dompurify] — see security report" + +[[IgnoredVulns]] +id = "GHSA-xhjh-pmcv-23jw" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-xj5x-m3f3-5x3h" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-xpqw-6gx7-v673" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [svgo] — see security report" + +[[IgnoredVulns]] +id = "GHSA-xv26-6w52-cph6" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [websocket-driver] — see security report" + +[[IgnoredVulns]] +id = "GHSA-xvcm-6775-5m9r" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [component_not_present]: build/packaging/lint/test tooling pruned from the shipped asar by electron-builder [immutable] — see security report" + +[[IgnoredVulns]] +id = "GHSA-xwr5-m59h-vwqr" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX mitigated: hardened Electron config (sandbox:true, contextIsolation, nodeIntegration:false, local file:// only) neutralizes the renderer CVE class; update recommended as defense-in-depth [electron] — see security report" + +[[IgnoredVulns]] +id = "GHSA-xx6v-rp6x-q39c" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [axios] — see security report" + +[[IgnoredVulns]] +id = "GHSA-xxjr-mmjv-4gpg" +ignoreUntil = "2026-10-01T00:00:00Z" +reason = "VEX not_affected [vulnerable_code_not_in_execute_path]: package (or the specific vulnerable function) present but never executed with attacker-controlled input in the shipped app [lodash] — see security report" diff --git a/scripts/build-report.mjs b/scripts/build-report.mjs new file mode 100644 index 000000000..3a1603143 --- /dev/null +++ b/scripts/build-report.mjs @@ -0,0 +1,190 @@ +#!/usr/bin/env node +// build-report.mjs — render the Security Report (Markdown + HTML) deterministically +// from structured data, so the monthly output is IDENTICAL in shape every run and +// never drifts from the SBOM. +// +// node scripts/build-report.mjs \ +// --config security/report-config.json \ +// --cdx sbom/.cdx.json \ +// --out security/ \ +// --date 2026-08 +// +// Component count and license distribution are computed live from the CycloneDX +// SBOM; everything else (VEX triage, narrative) comes from the config, which is +// what Claude updates when a new advisory appears. + +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; + +const args = Object.fromEntries(process.argv.slice(2).reduce((a, v, i, arr) => { + if (v.startsWith('--')) a.push([v.slice(2), arr[i + 1]]); + return a; +}, [])); +const cfg = JSON.parse(readFileSync(args.config, 'utf8')); +const cdx = JSON.parse(readFileSync(args.cdx, 'utf8')); +const date = args.date || 'unknown'; +const outDir = args.out || '.'; +mkdirSync(outDir, { recursive: true }); + +// --- live metrics from the SBOM --- +const comps = cdx.components || []; +const componentCount = comps.length; +const licAgg = {}; +for (const c of comps) { + for (const l of (c.licenses || [])) { + const id = l.license?.id || l.expression || l.license?.name || 'Unlicensed'; + licAgg[id] = (licAgg[id] || 0) + 1; + } +} +const topLicenses = Object.entries(licAgg).sort((a, b) => b[1] - a[1]).slice(0, 8); + +const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(//g, '>'); +const rich = (s) => String(s ?? ''); // config strings may contain / — keep as-is in HTML +const stripTags = (s) => String(s ?? '').replace(/<[^>]+>/g, ''); + +// ========================= MARKDOWN ========================= +const md = []; +md.push(`# ${cfg.title} — Software Supply Chain Security Report (SBOM & VEX)\n`); +md.push('| | |'); +md.push('|---|---|'); +md.push(`| **Product** | ${cfg.title} (\`${cfg.product}\`) — ${cfg.subtitle} · v${cfg.version} |`); +md.push(`| **Report type** | Software Bill of Materials (SBOM) & Vulnerability Exploitability eXchange (VEX) |`); +md.push(`| **Assessment date** | ${date} · Report version 1.0 |`); +md.push(`| **Prepared by** | Autonomy Logic — Engineering / Product Security |`); +md.push(`| **Classification** | Confidential |`); +md.push(`| **Security contact** | ${cfg.securityContact} |`); +md.push('\n---\n'); +md.push('## Executive Summary\n'); +md.push(`This report documents the third-party software composition and known-vulnerability posture of **${cfg.title}**. It is aligned with U.S. Executive Order 14028, the NTIA *Minimum Elements for an SBOM*, and the CISA Vulnerability Exploitability eXchange (VEX) guidance.\n`); +md.push(`> **Headline posture.** ${stripTags(cfg.headline)}\n`); +md.push('### Key metrics\n'); +md.push('| Metric | Value |'); +md.push('|---|---|'); +md.push(`| Components inventoried (full transitive graph) | **${componentCount}** |`); +md.push(`| Raw advisories detected | ${cfg.advisories.total} (${cfg.advisories.critical} critical · ${cfg.advisories.high} high · ${cfg.advisories.moderate} moderate · ${cfg.advisories.low} low) |`); +md.push(`| **AFFECTED — action required** | **${cfg.counts.affected.n}** (${cfg.counts.affected.sev}) |`); +md.push(`| AFFECTED — mitigating controls in place | ${cfg.counts.mitigated.n} (${cfg.counts.mitigated.sev}) |`); +md.push(`| **NOT AFFECTED** | **${cfg.counts.notAffected.n}** (${cfg.counts.notAffected.pct}) |`); +md.push('\n## 1. Scope & System Description\n'); +md.push(stripTags(cfg.scope) + '\n'); +md.push(`> **Scope note.** ${stripTags(cfg.scopeNote)}\n`); +md.push('## 2. Methodology\n'); +md.push(stripTags(cfg.methodologyNote) + ' For each relevant package, source code was analyzed to determine whether the vulnerable code path is invoked and whether its input is attacker-controlled.\n'); +md.push('## 3. Software Bill of Materials Summary\n'); +md.push('| License | Components |'); +md.push('|---|---|'); +for (const [l, n] of topLicenses) md.push(`| ${l} | ${n} |`); +md.push(`\n**License finding:** ${stripTags(cfg.licenseNote)}\n`); +md.push('## 4. Findings Requiring Remediation (Affected)\n'); +if (cfg.affected.length) { + md.push('| Priority | Component | Installed | Fixed in | Severity | Reachability rationale |'); + md.push('|---|---|---|---|---|---|'); + for (const f of cfg.affected) md.push(`| ${f.priority} | \`${f.component}\` | ${f.installed} | ${f.fixedIn} | ${f.severity} | ${stripTags(f.rationale)} |`); +} else md.push('**None.**'); +md.push('\n## 5. Not Affected — VEX Justifications\n'); +md.push('| VEX justification (CISA) | Count | Representative components | Basis |'); +md.push('|---|---|---|---|'); +for (const n of cfg.notAffected) md.push(`| \`${n.justification}\` | ${n.count} | ${stripTags(n.components)} | ${stripTags(n.basis)} |`); +if (cfg.criticalNote) md.push(`\n**On critical severity.** ${stripTags(cfg.criticalNote)}\n`); +md.push('\n## 6. Mitigated Findings\n'); +md.push('| Component | Advisories | Existing control |'); +md.push('|---|---|---|'); +for (const m of cfg.mitigated) md.push(`| \`${m.component}\` | ${m.advisories} | ${stripTags(m.control)} |`); +md.push('\n## 7. Remediation Plan\n'); +for (const r of cfg.remediation) md.push(`- ${stripTags(r)}`); +md.push('\n## 8. Secure Development & Supply-Chain Practices\n'); +md.push('| Practice | Status |'); +md.push('|---|---|'); +for (const p of cfg.practices) md.push(`| ${p.practice} | ${p.status} |`); +md.push('\n## 9. Attached Artifacts\n'); +md.push('| Artifact | Format | Purpose |'); +md.push('|---|---|---|'); +md.push(`| \`sbom/${cfg.sbomBasename}.cdx.json\` | CycloneDX 1.6 | Canonical machine-readable SBOM |`); +md.push(`| \`sbom/${cfg.sbomBasename}.spdx.json\` | SPDX 2.3 (ISO/IEC 5962) | Procurement / compliance SBOM |`); +md.push(`| \`sbom/${cfg.sbomBasename}.components.csv\` | CSV | Human-readable component inventory (${componentCount} rows) |`); +md.push(`| \`sbom/vulnerabilities.csv\` | CSV | Full annotated advisory register (${cfg.advisories.total} rows) |`); +md.push(`\n---\n\n*Prepared by Autonomy Logic Engineering. Assessment date ${date}. Regenerate per release.*\n`); +const mdOut = md.join('\n'); + +// ========================= HTML ========================= +const sevBadge = (s) => s; // severity strings already human +const row = (cells) => `${cells.map((c) => `${c}`).join('')}`; +const th = (cells) => `${cells.map((c) => `${c}`).join('')}`; +const badge = (txt, cls) => `${txt}`; + +const html = ` + +${esc(cfg.title)} — Security Report (SBOM & VEX) + +

${esc(cfg.title)}

+

Software Supply Chain Security Report — SBOM & VEX

+ +${row(['Product', `${esc(cfg.title)} (${esc(cfg.product)}) — ${esc(cfg.subtitle)} · v${esc(cfg.version)}`])} +${row(['Report type', 'Software Bill of Materials (SBOM) & Vulnerability Exploitability eXchange (VEX)'])} +${row(['Assessment date', `${esc(date)} · Report version 1.0`])} +${row(['Prepared by', 'Autonomy Logic — Engineering / Product Security'])} +${row(['Classification', 'Confidential'])} +${row(['Security contact', esc(cfg.securityContact)])} +
+

Executive Summary

+

This report documents the third-party software composition and known-vulnerability posture of ${esc(cfg.title)}. It is aligned with U.S. Executive Order 14028, the NTIA Minimum Elements for an SBOM, and the CISA VEX guidance.

+
Headline posture. ${rich(cfg.headline)}
+

Key metrics

+${th(['Metric', 'Value'])} +${row(['Components inventoried (full transitive graph)', `${componentCount}`])} +${row(['Raw advisories detected', `${cfg.advisories.total} (${cfg.advisories.critical} critical · ${cfg.advisories.high} high · ${cfg.advisories.moderate} moderate · ${cfg.advisories.low} low)`])} +${row([badge('AFFECTED — action required', 'b-red'), `${cfg.counts.affected.n} (${cfg.counts.affected.sev})`])} +${row([badge('AFFECTED — mitigated', 'b-amber'), `${cfg.counts.mitigated.n} (${cfg.counts.mitigated.sev})`])} +${row([badge('NOT AFFECTED', 'b-green'), `${cfg.counts.notAffected.n} (${cfg.counts.notAffected.pct})`])} +
+

1. Scope & System Description

${rich(cfg.scope)}

+
Scope note. ${rich(cfg.scopeNote)}
+

2. Methodology

${rich(cfg.methodologyNote)} For each relevant package, source code was analyzed to determine whether the vulnerable code path is invoked and whether its input is attacker-controlled.

+

3. Software Bill of Materials Summary

+${th(['License', 'Components'])} +${topLicenses.map(([l, n]) => row([esc(l), String(n)])).join('\n')} +
+
License finding: ${rich(cfg.licenseNote)}
+

4. Findings Requiring Remediation (Affected)

+${cfg.affected.length ? `${th(['Priority', 'Component', 'Installed', 'Fixed in', 'Severity', 'Reachability rationale'])} +${cfg.affected.map((f) => row([f.priority, `${esc(f.component)}`, esc(f.installed), esc(f.fixedIn), f.severity, rich(f.rationale)])).join('\n')} +
` : '
None.
'} +

5. Not Affected — VEX Justifications

+${th(['VEX justification (CISA)', 'Count', 'Representative components', 'Basis'])} +${cfg.notAffected.map((n) => row([`${esc(n.justification)}`, n.count, rich(n.components), rich(n.basis)])).join('\n')} +
+${cfg.criticalNote ? `
On critical severity. ${rich(cfg.criticalNote)}
` : ''} +

6. Mitigated Findings

+${th(['Component', 'Advisories', 'Existing control'])} +${cfg.mitigated.map((m) => row([`${esc(m.component)}`, m.advisories, rich(m.control)])).join('\n')} +
+

7. Remediation Plan

    ${cfg.remediation.map((r) => `
  • ${rich(r)}
  • `).join('')}
+

8. Secure Development & Supply-Chain Practices

+${th(['Practice', 'Status'])} +${cfg.practices.map((p) => row([esc(p.practice), p.status === 'In place' ? badge('In place', 'b-green') : badge('Recommended', 'b-amber')])).join('\n')} +
+

9. Attached Artifacts

+${th(['Artifact', 'Format', 'Purpose'])} +${row([`sbom/${cfg.sbomBasename}.cdx.json`, 'CycloneDX 1.6', 'Canonical machine-readable SBOM'])} +${row([`sbom/${cfg.sbomBasename}.spdx.json`, 'SPDX 2.3 (ISO/IEC 5962)', 'Procurement / compliance SBOM'])} +${row([`sbom/${cfg.sbomBasename}.components.csv`, 'CSV', `Human-readable component inventory (${componentCount} rows)`])} +${row(['sbom/vulnerabilities.csv', 'CSV', `Full annotated advisory register (${cfg.advisories.total} rows)`])} +
+

Prepared by Autonomy Logic Engineering. Assessment date ${esc(date)}. Regenerate per release.

+`; + +const base = `${cfg.title.replace(/[^A-Za-z0-9]+/g, '-')}-Security-Report`; +writeFileSync(`${outDir}/${base}.md`, mdOut); +writeFileSync(`${outDir}/${base}.html`, html); +console.log(`Wrote ${outDir}/${base}.md and .html (${componentCount} components, ${topLicenses.length} license classes)`); diff --git a/scripts/cdx-to-spdx.mjs b/scripts/cdx-to-spdx.mjs new file mode 100644 index 000000000..926b252d2 --- /dev/null +++ b/scripts/cdx-to-spdx.mjs @@ -0,0 +1,128 @@ +#!/usr/bin/env node +// Convert a CycloneDX 1.6 BOM (produced by cdxgen) into a valid SPDX 2.3 JSON document. +// Keeps NTIA minimum elements: supplier, name, version, unique id (purl), relationships, author, timestamp. +import { readFileSync, writeFileSync } from 'node:fs'; + +const SRC = process.argv[2] || 'sbom/bom.cdx.json'; +const OUT = process.argv[3] || 'sbom/bom.spdx.json'; + +const cdx = JSON.parse(readFileSync(SRC, 'utf8')); +const created = cdx.metadata?.timestamp || new Date().toISOString(); + +// The actual SBOM generator(s), read from the CycloneDX metadata (cdxgen, +// cyclonedx-py, …) — never hardcoded. +const toolComps = cdx.metadata?.tools?.components + || (Array.isArray(cdx.metadata?.tools) ? cdx.metadata.tools : []); +const toolCreators = toolComps + .filter((t) => t && t.name) + .map((t) => `Tool: ${t.name}${t.version ? '-' + t.version : ''}`); + +// Flatten root + nested workspace components into the package list. +const comps = []; +const walk = (c) => { + if (!c) return; + comps.push(c); + (c.components || []).forEach(walk); +}; +walk(cdx.metadata?.component); +(cdx.components || []).forEach((c) => comps.push(c)); + +// Stable SPDXID per bom-ref/purl. +const idFor = new Map(); +let n = 0; +const spdxId = (ref) => { + if (idFor.has(ref)) return idFor.get(ref); + const safe = String(ref).replace(/[^a-zA-Z0-9.-]/g, '-').replace(/-+/g, '-'); + const id = `SPDXRef-Pkg-${++n}-${safe}`.slice(0, 200); + idFor.set(ref, id); + return id; +}; + +const licenseExpr = (c) => { + const ls = c.licenses || []; + if (!ls.length) return 'NOASSERTION'; + const parts = ls + .map((l) => l.license?.id || l.expression || null) + .filter(Boolean); + if (!parts.length) return 'NOASSERTION'; + const expr = parts.length === 1 ? parts[0] : parts.join(' AND '); + // Non-SPDX placeholders -> NOASSERTION + if (/SEE LICENSE|UNLICENSED|UNKNOWN/i.test(expr)) return 'NOASSERTION'; + return expr; +}; + +const packages = comps.map((c) => { + const ref = c['bom-ref'] || c.purl || `${c.group || ''}/${c.name}@${c.version || ''}`; + const pkg = { + name: (c.group ? `${c.group}/` : '') + c.name, + SPDXID: spdxId(ref), + versionInfo: c.version || 'NOASSERTION', + downloadLocation: 'NOASSERTION', + filesAnalyzed: false, + licenseConcluded: 'NOASSERTION', + licenseDeclared: licenseExpr(c), + copyrightText: 'NOASSERTION', + supplier: c.publisher ? `Organization: ${c.publisher}` : 'NOASSERTION', + }; + if (c.purl) { + pkg.externalRefs = [ + { + referenceCategory: 'PACKAGE-MANAGER', + referenceType: 'purl', + referenceLocator: c.purl, + }, + ]; + } + return pkg; +}); + +// Relationships: document DESCRIBES root; root/deps DEPENDS_ON edges from cdx.dependencies. +const rootRef = cdx.metadata?.component?.['bom-ref']; +const relationships = []; +if (rootRef && idFor.has(rootRef)) { + relationships.push({ + spdxElementId: 'SPDXRef-DOCUMENT', + relationshipType: 'DESCRIBES', + relatedSpdxElement: idFor.get(rootRef), + }); +} +for (const dep of cdx.dependencies || []) { + const from = idFor.get(dep.ref); + if (!from) continue; + for (const to of dep.dependsOn || []) { + const t = idFor.get(to); + if (!t) continue; + relationships.push({ + spdxElementId: from, + relationshipType: 'DEPENDS_ON', + relatedSpdxElement: t, + }); + } +} + +// Derive the document name/namespace from THIS SBOM's root component — never +// hardcode a product name (that mislabels every other product's SPDX). +const rootName = (cdx.metadata?.component?.name || 'unknown-project').replace(/[^a-zA-Z0-9._-]/g, '-'); +const serial = (cdx.serialNumber || `urn:uuid:${rootName}`).replace('urn:uuid:', ''); +const doc = { + spdxVersion: 'SPDX-2.3', + dataLicense: 'CC0-1.0', + SPDXID: 'SPDXRef-DOCUMENT', + name: `${rootName}-SBOM`, + documentNamespace: `https://autonomylogic.com/spdx/${rootName}/${serial}`, + creationInfo: { + created, + // Derive the generator tool(s) from the source CycloneDX metadata instead of + // hardcoding (which would mis-attribute e.g. cyclonedx-py SBOMs to cdxgen). + creators: [ + 'Organization: Autonomy Logic', + ...toolCreators, + 'Tool: cdx-to-spdx', + ], + }, + packages, + relationships, +}; + +writeFileSync(OUT, JSON.stringify(doc, null, 2)); +console.log(`Wrote ${OUT}: ${packages.length} packages, ${relationships.length} relationships`); diff --git a/scripts/gen-osv-ignores.mjs b/scripts/gen-osv-ignores.mjs new file mode 100644 index 000000000..27ab502b4 --- /dev/null +++ b/scripts/gen-osv-ignores.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node +// Generate osv-scanner.toml [[IgnoredVulns]] entries from a triaged +// vulnerabilities.csv. Rows classified "not affected" / "mitigated" (our VEX +// baseline) become suppressions so the monthly scan only surfaces NEW findings. +// Rows that require action (APLICA / affected) are intentionally NOT suppressed +// — they keep alerting until the dependency is bumped. +// +// Usage: +// node scripts/gen-osv-ignores.mjs sbom/vulnerabilities.csv 2026-10-01 >> osv-scanner.toml +// arg1 = CSV path +// arg2 = review-by date (YYYY-MM-DD) written as ignoreUntil +// +// The CSV must contain an advisory-id column (named cve / advisory / id) whose +// cells hold OSV ids (GHSA-*, CVE-*, PYSEC-*, possibly pipe-separated). If a +// verdict column is present (verdict / verdict_vex / reach), only not-affected +// rows are suppressed; otherwise every listed id is suppressed (review!). + +import { readFileSync } from 'node:fs'; + +const [csvPath, reviewDate = '1970-01-01'] = process.argv.slice(2); +if (!csvPath) { console.error('usage: gen-osv-ignores.mjs '); process.exit(1); } + +const rows = readFileSync(csvPath, 'utf8').trim().split('\n').map(parseCsvLine); +const header = rows.shift().map((h) => h.toLowerCase()); +const idCol = header.findIndex((h) => ['cve', 'advisory', 'id', 'advisory_id'].includes(h)); +const verdictCol = header.findIndex((h) => ['verdict', 'verdict_vex'].includes(h)); +const reasonCol = header.findIndex((h) => ['description', 'title', 'basis', 'verdict_vex', 'verdict'].includes(h)); +if (idCol < 0) { console.error('No advisory-id column (cve/advisory/id) found'); process.exit(1); } + +const isNotAffected = (v) => /not[_ ]?affected|mitig|\bn\/?a\b/i.test(v || ''); +const seen = new Set(); +let emitted = 0; + +for (const r of rows) { + const verdict = verdictCol >= 0 ? r[verdictCol] : ''; + if (verdictCol >= 0 && !isNotAffected(verdict)) continue; // keep actionable ones visible + const ids = String(r[idCol] || '').split('|').map((s) => s.trim()).filter((s) => /^(GHSA|CVE|PYSEC)-/i.test(s)); + const reason = (reasonCol >= 0 ? r[reasonCol] : verdict) || 'triaged not-affected'; + for (const id of ids) { + if (seen.has(id)) continue; + seen.add(id); + console.log(`\n[[IgnoredVulns]]`); + console.log(`id = "${id}"`); + console.log(`ignoreUntil = "${reviewDate}T00:00:00Z"`); + console.log(`reason = ${JSON.stringify('VEX not_affected: ' + reason.replace(/\s+/g, ' ').slice(0, 160))}`); + emitted++; + } +} +console.error(`Emitted ${emitted} ignore entries from ${rows.length} rows.`); + +function parseCsvLine(line) { + const out = []; let cur = '', q = false; + for (let i = 0; i < line.length; i++) { + const c = line[i]; + if (q) { if (c === '"' && line[i + 1] === '"') { cur += '"'; i++; } else if (c === '"') q = false; else cur += c; } + else { if (c === '"') q = true; else if (c === ',') { out.push(cur); cur = ''; } else cur += c; } + } + out.push(cur); return out; +} diff --git a/scripts/generate-sbom.sh b/scripts/generate-sbom.sh new file mode 100755 index 000000000..22d1a66fd --- /dev/null +++ b/scripts/generate-sbom.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# +# generate-sbom.sh — Reproducible Software Bill of Materials for this project. +# +# Portable: derives the project name and package-manager from the repo itself +# (no hardcoded product name). Produces three artifacts under ./sbom/: +# - .cdx.json CycloneDX 1.6 (canonical, security/VEX-oriented — OWASP) +# - .spdx.json SPDX 2.3 (ISO/IEC 5962 — procurement/compliance) +# - .components.csv flattened component list (human-readable) +# +# Standards satisfied: NTIA "Minimum Elements for an SBOM" and EO 14028. +# Source of truth: the committed lockfile (fully-resolved dependency graph). +# +# Usage: ./scripts/generate-sbom.sh +# CI: run on every release tag; commit/attach the artifacts to the release. + +set -euo pipefail +cd "$(dirname "$0")/.." + +NAME="$(node -p "require('./package.json').name" 2>/dev/null || basename "$PWD")" +VERSION="$(node -p "require('./package.json').version || ''" 2>/dev/null || echo "")" + +# Auto-detect the package manager / cdxgen project type from the lockfile. +if [ -f pnpm-lock.yaml ]; then TYPE=pnpm +elif [ -f package-lock.json ]; then TYPE=npm +elif [ -f yarn.lock ]; then TYPE=yarn +else TYPE=js; fi + +OUT="sbom" +mkdir -p "$OUT" + +echo "==> Generating CycloneDX 1.6 SBOM (cdxgen, -t $TYPE) for ${NAME}@${VERSION:-unversioned}" +# cdxgen can exceed Node's default ~2 GB heap on a large pnpm monorepo (SIGABRT / +# exit 134 in CI). Raise the old-space limit; harmless on machines with less RAM. +export NODE_OPTIONS="${NODE_OPTIONS:-} --max-old-space-size=6144" +FETCH_LICENSE=true npx --yes @cyclonedx/cdxgen@11 \ + -t "$TYPE" \ + --spec-version 1.6 \ + -o "$OUT/$NAME.cdx.json" \ + --project-name "$NAME" \ + ${VERSION:+--project-version "$VERSION"} \ + . 2>/dev/null + +echo "==> Converting to SPDX 2.3" +node scripts/cdx-to-spdx.mjs "$OUT/$NAME.cdx.json" "$OUT/$NAME.spdx.json" + +echo "==> Emitting flattened CSV" +NAME="$NAME" OUT="$OUT" node -e ' +const fs=require("fs"); +const name=process.env.NAME, out=process.env.OUT; +const b=require(`./${out}/${name}.cdx.json`); +const esc=s=>{s=String(s==null?"":s);return /[",\n]/.test(s)?"\""+s.replace(/"/g,"\"\"")+"\"":s;}; +const rows=[["name","version","type","purl","license"]]; +for(const c of (b.components||[])){ + const lic=(c.licenses||[]).map(l=>l.license?.id||l.expression||l.license?.name||"").join(" / "); + rows.push([(c.group?c.group+"/":"")+c.name,c.version||"",c.type||"",c.purl||"",lic]); +} +fs.writeFileSync(`${out}/${name}.components.csv`,rows.map(r=>r.map(esc).join(",")).join("\n")); +console.log(`Wrote ${out}/${name}.components.csv:`,rows.length-1,"components"); +' + +echo "==> Done. Artifacts in ./$OUT/" +ls -la "$OUT" diff --git a/scripts/scan-vulns.mjs b/scripts/scan-vulns.mjs new file mode 100644 index 000000000..ff5269e12 --- /dev/null +++ b/scripts/scan-vulns.mjs @@ -0,0 +1,27 @@ +#!/usr/bin/env node +// scan-vulns.mjs — flatten osv-scanner JSON into the annotated vulnerabilities.csv. +// node scripts/scan-vulns.mjs +// osv-scanner already honored osv-scanner.toml, so anything here that is NOT in the +// suppression baseline is, by definition, part of the monthly "delta". + +import { readFileSync, writeFileSync } from 'node:fs'; +const [src, out = 'sbom/vulnerabilities.csv'] = process.argv.slice(2); +let data = {}; +try { data = JSON.parse(readFileSync(src, 'utf8')); } catch { data = {}; } + +const esc = (s) => { s = String(s ?? ''); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }; +const sevOf = (v) => (v.database_specific?.severity) + || (Array.isArray(v.severity) && v.severity[0]?.score) || ''; +const rows = [['severity', 'package', 'version', 'advisory', 'fixed_in', 'summary', 'url']]; +for (const res of (data.results || [])) { + for (const p of (res.packages || [])) { + for (const v of (p.vulnerabilities || [])) { + const fixed = [...new Set((v.affected || []).flatMap((a) => (a.ranges || []) + .flatMap((r) => (r.events || []).filter((e) => e.fixed).map((e) => e.fixed))))].join(' | '); + rows.push([sevOf(v), p.package?.name || '', p.package?.version || '', v.id || '', + fixed, (v.summary || '').slice(0, 140), `https://osv.dev/${v.id}`]); + } + } +} +writeFileSync(out, rows.map((r) => r.map(esc).join(',')).join('\n')); +console.log(`Wrote ${out}: ${rows.length - 1} advisories (post-suppression delta).`); diff --git a/security/README.md b/security/README.md new file mode 100644 index 000000000..aed78dfcb --- /dev/null +++ b/security/README.md @@ -0,0 +1,52 @@ +# Security — SBOM & Vulnerability reports (automated, monthly) + +This folder holds this repository's supply-chain security deliverable: a **dated +snapshot per month** with the SBOM (CycloneDX + SPDX), the component inventory, +the annotated vulnerability register (VEX), and the human-readable report. + +``` +security/ +├── 2026-07/ ← a monthly snapshot (immutable once merged) +│ ├── 00-READ-ME-FIRST.md +│ ├── OpenPLC-Editor-Desktop-Security-Report.md / .html / .pdf +│ └── sbom/ → *.cdx.json · *.spdx.json · *.components.csv · vulnerabilities.csv +├── latest → 2026-07 ← pointer to the current month +└── report-config.json ← report DATA (VEX triage + narrative) +``` + +## How it runs + +`.github/workflows/security-monthly.yml` runs on the **1st of each month** (and on +the manual **Run workflow** button). Per run it: regenerates the SBOM, scans +dependencies with **osv-scanner** (OSV = the same advisory source as Dependabot, +honoring the suppression baseline in `../osv-scanner.toml`), has **Claude triage +any new advisory** and update the report data, renders the report from +`report-config.json`, writes `security//`, and **opens a PR** for review. + +The report is **generated from data** (`report-config.json` + the live SBOM), so +it never drifts from the actual dependency graph. + +## One-time setup — run Claude on your subscription (no API key) + +Claude runs inside the workflow on **your Claude subscription** (Pro/Max), not a +pay-per-token API key: + +1. On your machine: `claude setup-token` → complete the browser login → copy the + token it prints (`CLAUDE_CODE_OAUTH_TOKEN`). +2. In the repo: **Settings → Secrets and variables → Actions → New repository + secret** → name `CLAUDE_CODE_OAUTH_TOKEN`, value = the token. +3. Done. The monthly workflow will authenticate as you. (If the secret is absent, + the workflow still runs and builds the report deterministically — it just + skips the AI triage step.) + +> To move billing off a personal account later (e.g. an org API key), replace the +> secret with `ANTHROPIC_API_KEY` and swap the one input in the workflow — nothing +> else changes. + +## Reviewing the monthly PR + +- **No new advisory:** confirm the summary, approve, merge (the snapshot is archived). +- **New advisory, not exploitable:** check Claude's justification in the + `osv-scanner.toml` diff, then merge. +- **New advisory, exploitable:** bump the dependency (separate PR) and merge the + report that documents it. diff --git a/security/report-config.json b/security/report-config.json new file mode 100644 index 000000000..ffc5d53a8 --- /dev/null +++ b/security/report-config.json @@ -0,0 +1,47 @@ +{ + "product": "openplc-editor", + "title": "OpenPLC Editor Desktop", + "subtitle": "Electron desktop application — IEC 61131-3 PLC editor", + "version": "4.2.9", + "sbomBasename": "open-plc-editor", + "ecosystem": "npm", + "securityContact": "Thiago Alves — thiago.alves@autonomylogic.com", + "advisorySource": "GitHub Advisory Database via npm audit (equivalent to Dependabot)", + "advisories": { "total": 143, "critical": 3, "high": 54, "moderate": 69, "low": 17 }, + "counts": { + "affected": { "n": 3, "sev": "0 critical · 2 high · 1 moderate" }, + "mitigated": { "n": 37, "sev": "0 critical · 4 high · 25 moderate · 8 low" }, + "notAffected": { "n": 103, "pct": "72%" } + }, + "headline": "Of 143 raw advisories reported against the dependency tree, 3 warrant remediation — all lower-risk denial-of-service items exposed only through an optional live-debug feature — and none are critical or exploitable in normal use. 37 are reachable but constrained by existing controls (a hardened Electron configuration and default-configured DOMPurify); the remaining 103 are not applicable. All three critical-severity advisories are in build/test tooling that is never shipped in the packaged application.", + "scope": "The OpenPLC Editor desktop application is an Electron app: a Chromium renderer process running the React UI and a Node.js main process handling files, packaging, updates, and a language server. It is packaged with electron-builder (asar), and the Electron runtime (Chromium + Node) is bundled into the shipped application. The threat model includes a user opening a malicious project file and an optional live-debug connection to a user-configured PLC runtime.", + "scopeNote": "This SBOM covers the Node.js/npm dependency graph. The build additionally downloads a pinned native strucpp transpiler binary (see binary-versions.json); that binary is tracked separately and is outside the npm SBOM. The bundled Electron runtime (Chromium) is assessed under Mitigated Findings.", + "methodologyNote": "Advisory data was obtained from the GitHub Advisory Database via npm audit (equivalent to GitHub Dependabot alerts). The application is packaged with electron-builder, which prunes development dependencies; only code imported by src (webpack-bundled into dist) plus a minimal runtime module set is shipped in the asar. The bundled Electron runtime is in scope. Reachability was analyzed across both the Electron main (Node) and renderer (Chromium) processes.", + "licenseNote": "No strong copyleft (GPL/AGPL) license is present on the product runtime path. A couple of components declare a custom \"SEE LICENSE IN LICENSE\" field and were reviewed as permissive/first-party. Dual-licensed components (e.g. MPL-2.0 dual with Apache-2.0) are consumed under their permissive option.", + "affected": [ + { "priority": "P2", "component": "ws, socket.io-parser (via socket.io-client)", "installed": "via socket.io-client", "fixedIn": "ws ≥ 8.21.0 · socket.io-parser ≥ 4.2.6", "severity": "2× High, 1× Moderate", "rationale": "Parsed in the main process only when the user opens a live-debug WebSocket connection to a user-configured PLC runtime (with rejectUnauthorized:false). A malicious/MITM runtime could send forged frames causing memory-exhaustion DoS. Not reachable in normal editing; impact is DoS, not code execution." } + ], + "mitigated": [ + { "component": "electron", "advisories": "18 (4 high) — use-after-free, renderer switch injection", "control": "The application ships a hardened Electron configuration: sandbox: true on all windows, with contextIsolation, nodeIntegration:false, and webSecurity at their secure Electron-35 defaults; the preload uses contextBridge; and in production the app loads only local file:// content. These conditions neutralize the high-severity CVE class, which requires rendering hostile web content. Recommendation: update Electron, tighten the CSP (default-src 'none', drop 'unsafe-inline'), add a will-navigate guard." }, + { "component": "dompurify", "advisories": "19 (moderate/low) — XSS bypass class", "control": "Called with the default configuration (no RETURN_DOM/ADD_TAGS/SAFE_FOR_TEMPLATES), over app-generated markup (search highlights) — the exact intended mitigation. Recommendation: keep dompurify ≥ 3.4.12." } + ], + "notAffected": [ + { "justification": "component_not_present", "count": "~60", "components": "tar (11, incl. 1 Critical), node-forge (7), webpack-dev-server (4), shell-quote (1 Critical), websocket-driver (1 Critical), immutable (3), svgo, playwright, @babel/*", "basis": "Build, packaging, lint, and test tooling (webpack/electron-builder/sass/babel/playwright). electron-builder prunes development dependencies; these are not in the shipped asar. All three critical advisories fall here." }, + { "justification": "vulnerable_code_not_in_execute_path", "count": "~35", "components": "axios (28; a dead dependency — not imported anywhere), fast-xml-parser/fast-xml-builder (only the parser is used, the advisory is in the builder), lodash (only safe helpers), js-yaml/yaml (no YAML parsing), uuid (v4 only), qs, serialize-javascript, glob", "basis": "The package (or the specific vulnerable function) is present but never executed with attacker-controlled input." }, + { "justification": "vulnerable_code_cannot_be_controlled_by_adversary", "count": "~8", "components": "form-data, follow-redirects (via unused axios / build publish), minimatch/picomatch, elliptic, path-to-regexp", "basis": "Node/build-only code paths not exercised with adversary input in the running application." } + ], + "criticalNote": "All three critical advisories (shell-quote, tar, websocket-driver) are in development/build/packaging tooling and are pruned from the shipped application. None is present in the delivered product.", + "remediation": [ + "P2: bump socket.io-client so ws ≥ 8.21.0 and socket.io-parser ≥ 4.2.6 (removes the debug-WebSocket DoS surface).", + "Defense-in-depth: update Electron to a current release; keep dompurify ≥ 3.4.12; tighten the renderer CSP and add a navigation guard; validate schemes before shell.openExternal.", + "Hygiene: remove the unused axios dependency; configure electron-updater with a fixed signed feed before enabling auto-update; track the downloaded strucpp binary under a dedicated (non-npm) supply-chain inventory." + ], + "practices": [ + { "practice": "Deterministic builds from a committed lockfile (npm ci)", "status": "In place" }, + { "practice": "Hardened Electron config (sandbox, context isolation, local-only content)", "status": "In place" }, + { "practice": "devDependencies pruned from the packaged application (electron-builder)", "status": "In place" }, + { "practice": "Automated dependency updates (Dependabot / GitHub Advisory Database)", "status": "In place" }, + { "practice": "SBOM generated per release in CycloneDX and SPDX", "status": "In place" }, + { "practice": "Keep bundled Electron/Chromium current; tighten CSP; signed auto-update feed", "status": "Recommended" } + ] +} From 010b0015797b1154fbfeaff26d06fb97129a9234 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Date: Fri, 31 Jul 2026 08:38:35 -0300 Subject: [PATCH 2/6] ci(security): PR gate v2 (reads-only + sticky comment) + archive on merge Gate now only scans/reports (contents: read) and posts one actionable sticky PR comment; per-PR SBOM archived on MERGE by security-pr-archive.yml. Validated on autonomy-edge. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AejAiL4tfXCyMhwtjY4vFT --- .github/workflows/security-pr-archive.yml | 68 +++++++++++++ .github/workflows/security-pr-gate.yml | 92 +++++++++++++++++ scripts/pr-gate-diff.mjs | 115 ++++++++++++++++++++++ 3 files changed, 275 insertions(+) create mode 100644 .github/workflows/security-pr-archive.yml create mode 100644 .github/workflows/security-pr-gate.yml create mode 100644 scripts/pr-gate-diff.mjs diff --git a/.github/workflows/security-pr-archive.yml b/.github/workflows/security-pr-archive.yml new file mode 100644 index 000000000..adf0313fc --- /dev/null +++ b/.github/workflows/security-pr-archive.yml @@ -0,0 +1,68 @@ +name: Security — archive PR SBOM on merge + +# When a pull request is MERGED, generate the SBOM + report for the resulting +# state and commit it under security/pr--/ on the default branch, +# so the repo keeps a permanent, per-PR supply-chain history. Runs once per merge +# (not on synchronize), so it never loops and never touches the PR while it is +# under review. +# +# NOTE: this pushes directly to the default branch. If you later require PRs on +# the default branch in branch protection, switch this to open a PR instead. + +on: + pull_request: + types: [closed] + +permissions: + contents: write + +concurrency: + group: security-pr-archive + cancel-in-progress: false + +jobs: + archive: + if: ${{ github.event.pull_request.merged == true }} + runs-on: ubuntu-latest + steps: + - name: Checkout the default branch (post-merge state) + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 0 + submodules: recursive + + - uses: actions/setup-node@v4 + with: { node-version: "22" } + - name: Enable pnpm + run: corepack enable + - uses: actions/setup-python@v5 + with: { python-version: "3.12" } + + - name: Install osv-scanner + run: | + curl -sSfL "https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64" -o /usr/local/bin/osv-scanner + chmod +x /usr/local/bin/osv-scanner + + - name: Generate SBOM + report and archive under security/pr--/ + run: | + bash scripts/generate-sbom.sh + NAME=$(basename "$(ls sbom/*.cdx.json | head -1)" .cdx.json) + CFG=""; [ -f osv-scanner.toml ] && CFG="--config=osv-scanner.toml" + osv-scanner scan $CFG --recursive --format=json --output=/tmp/osv.json . || true + node scripts/scan-vulns.mjs /tmp/osv.json sbom/vulnerabilities.csv + DATE=$(date -u +%Y-%m-%d) + PR="${{ github.event.pull_request.number }}" + DIR="security/pr-${PR}-${DATE}" + node scripts/build-report.mjs --config security/report-config.json --cdx "sbom/$NAME.cdx.json" --out "$DIR" --date "$DATE" + mkdir -p "$DIR/sbom" + cp "sbom/$NAME".cdx.json "sbom/$NAME".spdx.json "sbom/$NAME".components.csv sbom/vulnerabilities.csv "$DIR/sbom/" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add "$DIR" + if git diff --cached --quiet; then + echo "No SBOM to archive." + else + git commit -m "chore(security): SBOM snapshot for merged PR #${PR} (${DATE})" + git push origin "HEAD:${{ github.event.pull_request.base.ref }}" + fi diff --git a/.github/workflows/security-pr-gate.yml b/.github/workflows/security-pr-gate.yml new file mode 100644 index 000000000..c65195c11 --- /dev/null +++ b/.github/workflows/security-pr-gate.yml @@ -0,0 +1,92 @@ +name: Security — PR gate + +# Runs on every pull request. Scans the BASE and the HEAD of the PR and blocks +# ONLY on security advisories the PR *introduces* (present in head, absent in +# base) at or above the severity threshold — pre-existing issues never block. +# Both scans honor osv-scanner.toml (the VEX baseline), so a justified new +# suppression in the PR clears the gate. +# +# The job never writes CODE to the repo (contents: read), so the check is present +# on every commit and is safe to require in branch protection. It DOES post a +# single sticky PR comment (pull-requests: write) with the actionable result, so +# the author sees what to fix without digging into the check log. The per-PR SBOM +# snapshot is archived on MERGE by security-pr-archive.yml. +# +# To ENFORCE the block, mark the "gate" job a required status check in branch +# protection for the default branch. + +on: + pull_request: + +permissions: + contents: read + pull-requests: write # post/update the result comment (never pushes code) + +concurrency: + group: security-pr-gate-${{ github.event.pull_request.number }} + cancel-in-progress: true + +env: + # Block when the PR introduces a NEW advisory at or above this severity. + GATE_THRESHOLD: HIGH + +jobs: + gate: + runs-on: ubuntu-latest + steps: + - name: Checkout PR head + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - uses: actions/setup-node@v4 + with: { node-version: "22" } + + - name: Install osv-scanner + run: | + curl -sSfL "https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64" -o /usr/local/bin/osv-scanner + chmod +x /usr/local/bin/osv-scanner + + # Both scans use the HEAD osv-scanner.toml so a justified suppression added + # in the PR is honored on both sides. + - name: Scan HEAD + run: | + cp osv-scanner.toml /tmp/head-config.toml 2>/dev/null || true + CFG=""; [ -f /tmp/head-config.toml ] && CFG="--config=/tmp/head-config.toml" + osv-scanner scan $CFG --recursive --format=json --output=/tmp/head.json . || true + + - name: Scan BASE + run: | + git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.sha }}" 2>/dev/null || true + git worktree add -f /tmp/base "${{ github.event.pull_request.base.sha }}" + CFG=""; [ -f /tmp/head-config.toml ] && CFG="--config=/tmp/head-config.toml" + osv-scanner scan $CFG --recursive --format=json --output=/tmp/base.json /tmp/base || true + + # THE GATE — non-zero exit here fails the check and (with branch protection) + # blocks the merge. It also writes /tmp/gate-comment.md and /tmp/gate-status. + - name: Evaluate — block on newly-introduced advisories + run: node scripts/pr-gate-diff.mjs /tmp/base.json /tmp/head.json "${GATE_THRESHOLD}" + + # Post/update ONE sticky comment on the PR with the actionable result. + # Runs even when the gate failed (always()); never flips the verdict + # (continue-on-error) — the pass/fail is decided by the step above. + - name: Comment result on the PR + if: ${{ always() && github.event.pull_request.head.repo.full_name == github.repository }} + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + STATUS=$(cat /tmp/gate-status 2>/dev/null || echo clean) + CID=$(gh api "repos/$REPO/issues/$PR/comments" --paginate \ + --jq '.[] | select(.body | contains("")) | .id' | head -1) + if [ "$STATUS" = "clean" ] && [ -z "$CID" ]; then + echo "Clean and no existing comment — nothing to post."; exit 0 + fi + if [ -n "$CID" ]; then + gh api -X PATCH "repos/$REPO/issues/comments/$CID" -F body=@/tmp/gate-comment.md >/dev/null && echo "Updated comment $CID" + else + gh pr comment "$PR" --repo "$REPO" --body-file /tmp/gate-comment.md && echo "Created comment" + fi diff --git a/scripts/pr-gate-diff.mjs b/scripts/pr-gate-diff.mjs new file mode 100644 index 000000000..8bec73c8f --- /dev/null +++ b/scripts/pr-gate-diff.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node +// pr-gate-diff.mjs — the PR security gate's decision. +// node scripts/pr-gate-diff.mjs [threshold] +// Both inputs are osv-scanner JSON outputs produced WITH --config=osv-scanner.toml +// (so the VEX baseline is already applied). We block ONLY on advisories that the +// PR INTRODUCES — present in head, absent in base — at or above +// (default HIGH). Pre-existing advisories never block. Exit 1 = block, 0 = pass. +// +// Side effect: writes a Markdown body to $GATE_COMMENT_FILE (default +// /tmp/gate-comment.md) and a one-word status (block|warn|clean) to +// $GATE_STATUS_FILE (default /tmp/gate-status), so the workflow can post a PR +// comment. The full detail is also printed to stdout (the check log). + +import { readFileSync, writeFileSync } from 'node:fs'; + +const [baseP, headP, thresholdArg] = process.argv.slice(2); +const THRESHOLD = (thresholdArg || 'HIGH').toUpperCase(); +const ORDER = ['LOW', 'MODERATE', 'HIGH', 'CRITICAL']; +const COMMENT_FILE = process.env.GATE_COMMENT_FILE || '/tmp/gate-comment.md'; +const STATUS_FILE = process.env.GATE_STATUS_FILE || '/tmp/gate-status'; +const MARKER = ''; + +const load = (p) => { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return {}; } }; + +// --- CVSS v3.x base-score computation (for entries that carry only a vector) --- +function cvss3Base(vector) { + const m = {}; + for (const kv of vector.split('/')) { const [k, v] = kv.split(':'); if (k && v) m[k] = v; } + const AV = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 }[m.AV]; + const AC = { L: 0.77, H: 0.44 }[m.AC]; + const UI = { N: 0.85, R: 0.62 }[m.UI]; + const scopeChanged = m.S === 'C'; + const PR = (scopeChanged ? { N: 0.85, L: 0.68, H: 0.5 } : { N: 0.85, L: 0.62, H: 0.27 })[m.PR]; + const imp = { N: 0, L: 0.22, H: 0.56 }; + const C = imp[m.C], I = imp[m.I], A = imp[m.A]; + if ([AV, AC, UI, PR, C, I, A].some((x) => x === undefined)) return null; + const iss = 1 - (1 - C) * (1 - I) * (1 - A); + const impact = scopeChanged ? 7.52 * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15) : 6.42 * iss; + if (impact <= 0) return 0; + const expl = 8.22 * AV * AC * PR * UI; + const roundup = (x) => Math.ceil(x * 10) / 10; + return roundup(Math.min((scopeChanged ? 1.08 : 1) * (impact + expl), 10)); +} +const bucketFromScore = (s) => (s >= 9 ? 'CRITICAL' : s >= 7 ? 'HIGH' : s >= 4 ? 'MODERATE' : s > 0 ? 'LOW' : 'LOW'); + +function severityOf(v) { + const word = (v.database_specific?.severity || '').toUpperCase(); + if (ORDER.includes(word)) return word === 'MEDIUM' ? 'MODERATE' : word; + for (const s of (v.severity || [])) { + if (typeof s.score === 'string' && s.score.startsWith('CVSS:3')) { + const sc = cvss3Base(s.score); + if (sc != null) return bucketFromScore(sc); + } + } + return 'HIGH'; // conservative: an unscored NEW advisory should surface, not slip through +} + +function fixedOf(v) { + const fixes = [...new Set((v.affected || []).flatMap((a) => (a.ranges || []) + .flatMap((r) => (r.events || []).filter((e) => e.fixed).map((e) => e.fixed))))]; + return fixes.join(', '); +} + +function index(data) { + const m = new Map(); + for (const r of (data.results || [])) for (const p of (r.packages || [])) for (const v of (p.vulnerabilities || [])) { + if (!v.id || m.has(v.id)) continue; + m.set(v.id, { sev: severityOf(v), pkg: p.package?.name || '?', ver: p.package?.version || '', fixed: fixedOf(v), summary: (v.summary || '').slice(0, 120) }); + } + return m; +} + +const base = index(load(baseP)); +const head = index(load(headP)); +const min = ORDER.indexOf(THRESHOLD); + +const introduced = [...head.entries()].filter(([id]) => !base.has(id)) + .map(([id, d]) => ({ id, ...d })) + .sort((a, b) => ORDER.indexOf(b.sev) - ORDER.indexOf(a.sev)); +const blocking = introduced.filter((a) => ORDER.indexOf(a.sev) >= min); + +// ---- stdout (the check log — full detail) ---- +if (introduced.length) { + console.log(`\nAdvisories introduced by this PR (${introduced.length}):`); + for (const a of introduced) console.log(` [${a.sev}] ${a.pkg}@${a.ver} ${a.id} ${a.summary}`); +} else { + console.log('No new advisories introduced by this PR.'); +} + +// ---- PR comment body ---- +const advLink = (id) => (id.startsWith('GHSA') ? `https://github.com/advisories/${id}` : `https://osv.dev/${id}`); +const rows = introduced.map((a) => `| ${a.sev} | \`${a.pkg}\` | ${a.ver} | [${a.id}](${advLink(a.id)}) | ${a.fixed || '—'} |`).join('\n'); +const table = introduced.length + ? `| Severity | Package | Version | Advisory | Fixed in |\n|---|---|---|---|---|\n${rows}` + : ''; +let status, body; +if (blocking.length) { + status = 'block'; + body = `${MARKER}\n## 🔴 Security gate — blocked\nThis PR introduces **${blocking.length}** new advisory(ies) at or above **${THRESHOLD}**, so the merge is blocked.\n\n${table}\n\n**How to unblock:**\n1. Upgrade the dependency to a fixed version (see *Fixed in*), or\n2. Remove the dependency, or\n3. If it is not exploitable in our usage, add a justified \`[[IgnoredVulns]]\` entry (CISA VEX reason) to \`osv-scanner.toml\`.\n\n_Only High/Critical block; Moderate/Low are shown for awareness. Full detail in the check log._`; +} else if (introduced.length) { + status = 'warn'; + body = `${MARKER}\n## 🟡 Security gate — passed (with notes)\nThis PR introduces new advisories, but none at or above **${THRESHOLD}**, so it does **not** block. Consider addressing them as hygiene.\n\n${table}\n\n_Full detail in the check log._`; +} else { + status = 'clean'; + body = `${MARKER}\n## ✅ Security gate — no new vulnerabilities\nThis PR does not introduce any new advisory versus the base branch.`; +} +writeFileSync(COMMENT_FILE, body); +writeFileSync(STATUS_FILE, status); + +// ---- verdict ---- +if (blocking.length) { + console.error(`\n❌ BLOCKED: this PR introduces ${blocking.length} new ${THRESHOLD}+ advisory(ies). Fix or remove the dependency, or add a justified suppression to osv-scanner.toml.`); + process.exit(1); +} +console.log(`\n✅ PASS: no new ${THRESHOLD}+ advisories introduced.`); From bbd5e96f8aff2339b1e116c3f6358c2862eb8f3a Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Date: Tue, 4 Aug 2026 08:51:46 -0300 Subject: [PATCH 3/6] =?UTF-8?q?fix(security):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20close=206=20gate/report=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate fails closed on scanner error; MEDIUM no longer blocks as HIGH; CVSS 4.0 handled conservatively; diff keyed by advisory id + package. Report HTML escaped (+ PDF rendered with JS disabled). cdx-to-spdx dedupes packages and uses OR for dual licenses. gen-osv-ignores requires a verdict column. Validated on autonomy-edge. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01AejAiL4tfXCyMhwtjY4vFT --- .github/workflows/security-monthly.yml | 5 +- .github/workflows/security-pr-gate.yml | 22 ++++++- scripts/build-report.mjs | 8 ++- scripts/cdx-to-spdx.mjs | 29 ++++++--- scripts/gen-osv-ignores.mjs | 22 +++++-- scripts/pr-gate-diff.mjs | 86 ++++++++++++++++++++------ 6 files changed, 134 insertions(+), 38 deletions(-) diff --git a/.github/workflows/security-monthly.yml b/.github/workflows/security-monthly.yml index 905e45483..f4fa92b86 100644 --- a/.github/workflows/security-monthly.yml +++ b/.github/workflows/security-monthly.yml @@ -110,8 +110,11 @@ jobs: REPORT="$DIR/$(ls "$DIR" | grep -E 'Security-Report\.html$')" # --no-sandbox / --disable-dev-shm-usage: Chrome's zygote sandbox aborts # (SIGABRT) on GitHub runners; required for headless Chrome in CI. + # --disable-javascript: the report HTML is a static document written from + # report-config.json (AI-authored) — no JS should ever run while rendering + # it with local file:// access. Defense-in-depth on top of the HTML escaping. "${{ steps.chrome.outputs.chrome-path }}" --headless=new --no-sandbox --disable-dev-shm-usage \ - --disable-gpu --no-pdf-header-footer \ + --disable-javascript --disable-gpu --no-pdf-header-footer \ --run-all-compositor-stages-before-draw --virtual-time-budget=5000 \ --print-to-pdf="${REPORT%.html}.pdf" "file://$PWD/$REPORT" ln -sfn "$MONTH" security/latest diff --git a/.github/workflows/security-pr-gate.yml b/.github/workflows/security-pr-gate.yml index c65195c11..c3aa5c9e0 100644 --- a/.github/workflows/security-pr-gate.yml +++ b/.github/workflows/security-pr-gate.yml @@ -54,14 +54,30 @@ jobs: run: | cp osv-scanner.toml /tmp/head-config.toml 2>/dev/null || true CFG=""; [ -f /tmp/head-config.toml ] && CFG="--config=/tmp/head-config.toml" - osv-scanner scan $CFG --recursive --format=json --output=/tmp/head.json . || true + set +e + osv-scanner scan $CFG --recursive --format=json --output=/tmp/head.json . + rc=$? + set -e + # osv-scanner: 0 = no vulns, 1 = vulns found. ANY other code is an + # operational failure — fail the gate CLOSED, never pass a broken scan. + if [ "$rc" != "0" ] && [ "$rc" != "1" ]; then + echo "::error::osv-scanner failed to scan HEAD (exit $rc)"; exit 1 + fi + [ -s /tmp/head.json ] || echo '{"results":[]}' > /tmp/head.json - name: Scan BASE run: | - git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.sha }}" 2>/dev/null || true + git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.sha }}" git worktree add -f /tmp/base "${{ github.event.pull_request.base.sha }}" CFG=""; [ -f /tmp/head-config.toml ] && CFG="--config=/tmp/head-config.toml" - osv-scanner scan $CFG --recursive --format=json --output=/tmp/base.json /tmp/base || true + set +e + osv-scanner scan $CFG --recursive --format=json --output=/tmp/base.json /tmp/base + rc=$? + set -e + if [ "$rc" != "0" ] && [ "$rc" != "1" ]; then + echo "::error::osv-scanner failed to scan BASE (exit $rc)"; exit 1 + fi + [ -s /tmp/base.json ] || echo '{"results":[]}' > /tmp/base.json # THE GATE — non-zero exit here fails the check and (with branch protection) # blocks the merge. It also writes /tmp/gate-comment.md and /tmp/gate-status. diff --git a/scripts/build-report.mjs b/scripts/build-report.mjs index 3a1603143..bddb45782 100644 --- a/scripts/build-report.mjs +++ b/scripts/build-report.mjs @@ -38,7 +38,13 @@ for (const c of comps) { const topLicenses = Object.entries(licAgg).sort((a, b) => b[1] - a[1]).slice(0, 8); const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(//g, '>'); -const rich = (s) => String(s ?? ''); // config strings may contain / — keep as-is in HTML +// report-config.json is written by the AI triage step, so its strings are +// UNTRUSTED. Escape everything, then re-enable only a small set of attribute-less +// formatting tags. This blocks