diff --git a/.github/workflows/security-monthly.yml b/.github/workflows/security-monthly.yml new file mode 100644 index 000000000..216834c7b --- /dev/null +++ b/.github/workflows/security-monthly.yml @@ -0,0 +1,130 @@ +name: Security — monthly SBOM & VEX report + +# Runs on GitHub's servers (not on anyone's laptop). Every month it regenerates +# the SBOM, scans dependencies, renders the +# report from the versioned template, drops everything into security//, +# and opens a PR for the team to review. Nothing is merged automatically. +# + +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 + +concurrency: + group: security-monthly + cancel-in-progress: false + +jobs: + report: + runs-on: ubuntu-latest + 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: | + OSV_VER=v2.4.0 # pinned; verified against the release SHA256SUMS + curl -sSfL "https://github.com/google/osv-scanner/releases/download/${OSV_VER}/osv-scanner_linux_amd64" -o /tmp/osv-scanner_linux_amd64 + curl -sSfL "https://github.com/google/osv-scanner/releases/download/${OSV_VER}/osv-scanner_SHA256SUMS" -o /tmp/osv_SHA256SUMS + ( cd /tmp && grep " osv-scanner_linux_amd64$" osv_SHA256SUMS | sha256sum -c - ) + install -m 0755 /tmp/osv-scanner_linux_amd64 /usr/local/bin/osv-scanner + - name: Scan + run: | + CFG=""; [ -f osv-scanner.toml ] && CFG="--config=osv-scanner.toml" + set +e + osv-scanner scan $CFG --recursive --format=json --output=/tmp/osv.json . + rc=$? + set -e + # osv-scanner: 0 = no vulns, 1 = vulns found. Any other code is a scanner + # failure — do NOT let scan-vulns turn it into an empty (clean) register. + if [ "$rc" != "0" ] && [ "$rc" != "1" ]; then echo "::error::osv-scanner failed (exit $rc)"; exit 1; fi + [ -s /tmp/osv.json ] || echo '{"results":[]}' > /tmp/osv.json + node scripts/scan-vulns.mjs /tmp/osv.json sbom/vulnerabilities.csv + # also an UNfiltered scan, so the report's headline numbers are derived + # live (raw total vs. suppressed vs. surfacing) instead of hard-coded. + set +e + osv-scanner scan --recursive --format=json --output=/tmp/osv-raw.json . + rc2=$? + set -e + if [ "$rc2" != "0" ] && [ "$rc2" != "1" ]; then echo "::error::osv-scanner (raw) failed (exit $rc2)"; exit 1; fi + [ -s /tmp/osv-raw.json ] || echo '{"results":[]}' > /tmp/osv-raw.json + + - 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" \ + --osv-raw /tmp/osv-raw.json \ + --osv-delta /tmp/osv.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. + # --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-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 + + # ---- delivery: open the PR for review ---- + - name: Open Pull Request + uses: peter-evans/create-pull-request@v6 + with: + # A PR opened with the default GITHUB_TOKEN does NOT trigger other + # workflows (so the gate/lint/tests would never run on the monthly PR). + # Set a SECURITY_BOT_TOKEN secret (a GitHub App installation token or a + # fine-grained PAT with contents+PR write) to make checks run; it falls + # back to GITHUB_TOKEN if unset (PR still opens, just without checks). + token: ${{ secrets.SECURITY_BOT_TOKEN || github.token }} + base: development + 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) surface in `vulnerabilities.csv` for human review (no AI triage on this public repo). + + Nothing is merged automatically. Approve to archive this month's snapshot. diff --git a/.github/workflows/security-pr-archive.yml b/.github/workflows/security-pr-archive.yml new file mode 100644 index 000000000..03118ad9b --- /dev/null +++ b/.github/workflows/security-pr-archive.yml @@ -0,0 +1,88 @@ +name: Security — archive SBOM on merge + +# Runs when a commit lands on the default branch (a PR merged, or a direct push). +# It regenerates the SBOM + raw scan for that exact state and stores them on a +# dedicated ORPHAN branch `security-archive` under -/, giving a +# permanent per-merge supply-chain trail WITHOUT bloating the code branch's +# history or slowing clones. +# +# Why `push` (not pull_request): the push event's GITHUB_TOKEN is always writable +# — so this also works for merged fork PRs — and it only fires for the default +# branch. We archive the SBOM + raw scan output only (no rendered "report"), so +# there is never a stale/false attestation committed anywhere. + +on: + push: + branches: [development] + +permissions: + contents: write + +concurrency: + group: security-archive + cancel-in-progress: false + +jobs: + archive: + runs-on: ubuntu-latest + steps: + - name: Checkout (post-merge state) + uses: actions/checkout@v4 + with: + fetch-depth: 2 # enough to read the merge/commit subject + 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: | + OSV_VER=v2.4.0 # pinned; verified against the release SHA256SUMS + curl -sSfL "https://github.com/google/osv-scanner/releases/download/${OSV_VER}/osv-scanner_linux_amd64" -o /tmp/osv-scanner_linux_amd64 + curl -sSfL "https://github.com/google/osv-scanner/releases/download/${OSV_VER}/osv-scanner_SHA256SUMS" -o /tmp/osv_SHA256SUMS + ( cd /tmp && grep " osv-scanner_linux_amd64$" osv_SHA256SUMS | sha256sum -c - ) + install -m 0755 /tmp/osv-scanner_linux_amd64 /usr/local/bin/osv-scanner + + - name: Generate SBOM + scan (SBOM & raw scan only — no rendered report) + run: | + bash scripts/generate-sbom.sh + NAME=$(basename "$(ls sbom/*.cdx.json | head -1)" .cdx.json) + echo "NAME=$NAME" >> "$GITHUB_ENV" + CFG=""; [ -f osv-scanner.toml ] && CFG="--config=osv-scanner.toml" + set +e + osv-scanner scan $CFG --recursive --format=json --output=/tmp/osv.json . + rc=$? + set -e + if [ "$rc" != "0" ] && [ "$rc" != "1" ]; then echo "::error::osv-scanner failed (exit $rc)"; exit 1; fi + [ -s /tmp/osv.json ] || echo '{"results":[]}' > /tmp/osv.json + node scripts/scan-vulns.mjs /tmp/osv.json sbom/vulnerabilities.csv + + - name: Publish snapshot to the security-archive orphan branch + env: + GH_TOKEN: ${{ github.token }} + run: | + MSG=$(git log -1 --format=%s) + PR=$(printf '%s' "$MSG" | grep -oE '#[0-9]+' | head -1 | tr -d '#') + if [ -n "$PR" ]; then ID="pr-$PR"; else ID="commit-$(git rev-parse --short HEAD)"; fi + DIR="${ID}-$(date -u +%Y-%m-%d)" + URL="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + if git clone --depth 1 --branch security-archive "$URL" /tmp/arch 2>/dev/null; then + : + else + git clone --depth 1 "$URL" /tmp/arch + git -C /tmp/arch checkout --orphan security-archive + git -C /tmp/arch rm -rf . >/dev/null 2>&1 || true + fi + mkdir -p "/tmp/arch/$DIR" + cp "sbom/$NAME.cdx.json" "sbom/$NAME.spdx.json" "sbom/$NAME.components.csv" sbom/vulnerabilities.csv "/tmp/arch/$DIR/" + git -C /tmp/arch -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" add "$DIR" + if git -C /tmp/arch diff --cached --quiet; then + echo "No SBOM changes to archive for $DIR." + else + git -C /tmp/arch -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" commit -m "chore(security): SBOM snapshot ${DIR}" + git -C /tmp/arch push "$URL" HEAD:security-archive + fi diff --git a/.github/workflows/security-pr-gate.yml b/.github/workflows/security-pr-gate.yml new file mode 100644 index 000000000..0bf02be21 --- /dev/null +++ b/.github/workflows/security-pr-gate.yml @@ -0,0 +1,114 @@ +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: Unit tests (security scripts) + run: node --test scripts/__tests__/*.test.mjs + + - name: Install osv-scanner + run: | + OSV_VER=v2.4.0 # pinned; verified against the release SHA256SUMS + curl -sSfL "https://github.com/google/osv-scanner/releases/download/${OSV_VER}/osv-scanner_linux_amd64" -o /tmp/osv-scanner_linux_amd64 + curl -sSfL "https://github.com/google/osv-scanner/releases/download/${OSV_VER}/osv-scanner_SHA256SUMS" -o /tmp/osv_SHA256SUMS + ( cd /tmp && grep " osv-scanner_linux_amd64$" osv_SHA256SUMS | sha256sum -c - ) + install -m 0755 /tmp/osv-scanner_linux_amd64 /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" + 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 }}" + git worktree add -f /tmp/base "${{ github.event.pull_request.base.sha }}" + CFG=""; [ -f /tmp/head-config.toml ] && CFG="--config=/tmp/head-config.toml" + 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. + - 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/.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/__tests__/security-scripts.test.mjs b/scripts/__tests__/security-scripts.test.mjs new file mode 100644 index 000000000..ffa861dc3 --- /dev/null +++ b/scripts/__tests__/security-scripts.test.mjs @@ -0,0 +1,64 @@ +// Unit tests for the merge-deciding / compliance scripts. Zero-dependency +// (node:test), run with: node --test scripts/__tests__/ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { writeFileSync, mkdtempSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const SCRIPTS = join(dirname(fileURLToPath(import.meta.url)), '..'); +const dir = mkdtempSync(join(tmpdir(), 'sec-test-')); +const w = (name, obj) => { const p = join(dir, name); writeFileSync(p, JSON.stringify(obj)); return p; }; +const adv = (id, sev, extra = {}) => ({ id, ...(sev ? { database_specific: { severity: sev } } : {}), ...extra }); +const scan = (...pkgs) => ({ results: [{ packages: pkgs.map(([name, vulns]) => ({ package: { name, version: '1' }, vulnerabilities: vulns })) }] }); + +function gate(base, head, threshold = 'HIGH') { + const env = { ...process.env, GATE_COMMENT_FILE: join(dir, 'c.md'), GATE_STATUS_FILE: join(dir, 's') }; + return spawnSync('node', [join(SCRIPTS, 'pr-gate-diff.mjs'), base, head, threshold], { env, encoding: 'utf8' }); +} + +test('gate: MEDIUM does not block (normalized to MODERATE)', () => { + const r = gate(w('b.json', { results: [] }), w('h.json', scan(['m', [adv('CVE-MED', 'MEDIUM')]]))); + assert.equal(r.status, 0, r.stderr); +}); +test('gate: HIGH blocks', () => { + const r = gate(w('b.json', { results: [] }), w('h.json', scan(['h', [adv('CVE-HI', 'HIGH')]]))); + assert.equal(r.status, 1); +}); +test('gate: same CVE on a NEW package is introduced (blocks); on the same package it is not', () => { + const base = w('b.json', scan(['shared', [adv('CVE-X', 'HIGH')]])); + const headNew = w('hn.json', scan(['shared', [adv('CVE-X', 'HIGH')]], ['newpkg', [adv('CVE-X', 'HIGH')]])); + assert.equal(gate(base, headNew).status, 1, 'new package must block'); + const headSame = w('hs.json', scan(['shared', [adv('CVE-X', 'HIGH')]])); + assert.equal(gate(base, headSame).status, 0, 'pre-existing must not block'); +}); +test('gate: fails CLOSED (exit 2) on missing or shapeless scan input', () => { + assert.equal(gate(join(dir, 'nope.json'), w('h.json', { results: [] })).status, 2, 'missing base'); + assert.equal(gate(w('bad.json', { garbage: true }), w('h.json', { results: [] })).status, 2, 'no results array'); +}); +test('gate: CVSS 3.1 vector (9.8) scored as blocking', () => { + const head = w('h.json', scan(['c', [adv('CVE-CVSS', null, { severity: [{ type: 'CVSS_V3', score: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H' }] })]])); + assert.equal(gate(w('b.json', { results: [] }), head).status, 1); +}); + +function spdx(cdx) { + const out = join(dir, 'o.spdx.json'); + const r = spawnSync('node', [join(SCRIPTS, 'cdx-to-spdx.mjs'), w('in.cdx.json', cdx), out], { encoding: 'utf8' }); + assert.equal(r.status, 0, r.stderr); + return JSON.parse(readFileSync(out, 'utf8')); +} +test('cdx-to-spdx: no duplicate SPDXID when a component appears twice', () => { + const doc = spdx({ + metadata: { component: { 'bom-ref': 'root@1', name: 'root', components: [{ 'bom-ref': 'dup@1', name: 'dup', version: '1' }] } }, + components: [{ 'bom-ref': 'dup@1', name: 'dup', version: '1' }, { 'bom-ref': 'solo@2', name: 'solo', version: '2' }], + }); + const ids = doc.packages.map((p) => p.SPDXID); + assert.equal(new Set(ids).size, ids.length, 'SPDXIDs must be unique'); +}); +test('cdx-to-spdx: multiple licenses join with OR (dual-licensed), not AND', () => { + const doc = spdx({ components: [{ 'bom-ref': 'x@1', name: 'x', version: '1', licenses: [{ license: { id: 'MIT' } }, { license: { id: 'GPL-3.0-only' } }] }] }); + const x = doc.packages.find((p) => p.name === 'x'); + assert.equal(x.licenseDeclared, 'MIT OR GPL-3.0-only'); +}); diff --git a/scripts/build-report.mjs b/scripts/build-report.mjs new file mode 100644 index 000000000..0054ad0ee --- /dev/null +++ b/scripts/build-report.mjs @@ -0,0 +1,230 @@ +#!/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); + +// --- live advisory metrics from the actual scan(s), when provided ----------- +// Keeps the headline honest: the numbers reflect THIS run's scan, not a static +// value in the config. --osv-raw = unfiltered scan; --osv-delta = scan WITH the +// osv-scanner.toml VEX baseline applied. Falls back to the config numbers if no +// scan is passed (e.g. an ad-hoc local render). +const loadScan = (p) => { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; } }; +function tally(scan) { + const bySev = { CRITICAL: 0, HIGH: 0, MODERATE: 0, LOW: 0 }; + const ids = new Set(); + for (const r of (scan?.results || [])) for (const p of (r.packages || [])) for (const v of (p.vulnerabilities || [])) { + if (!v.id || ids.has(v.id)) continue; ids.add(v.id); + let s = (v.database_specific?.severity || '').toUpperCase(); + if (s === 'MEDIUM') s = 'MODERATE'; + if (!(s in bySev)) s = 'MODERATE'; + bySev[s]++; + } + return { total: ids.size, bySev }; +} +const sevStr = (b) => `${b.CRITICAL} critical · ${b.HIGH} high · ${b.MODERATE} moderate · ${b.LOW} low`; +const rawScan = args['osv-raw'] ? loadScan(args['osv-raw']) : null; +const deltaScan = args['osv-delta'] ? loadScan(args['osv-delta']) : null; +let advisoryRows; +if (rawScan && deltaScan) { + const raw = tally(rawScan), delta = tally(deltaScan); + const suppressed = Math.max(0, raw.total - delta.total); + const pct = raw.total ? Math.round(100 * suppressed / raw.total) : 0; + advisoryRows = [ + { label: `Raw advisories detected (this scan · ${date})`, value: `${raw.total} (${sevStr(raw.bySev)})` }, + { label: 'Not applicable — suppressed by the VEX baseline', value: `${suppressed} (${pct}%)`, badge: 'b-green' }, + { label: 'Surfacing after suppression — review', value: `${delta.total} (${sevStr(delta.bySev)})`, badge: delta.total ? 'b-red' : 'b-green' }, + ]; +} else { + advisoryRows = [ + { label: 'Raw advisories detected', value: `${cfg.advisories.total} (${cfg.advisories.critical} critical · ${cfg.advisories.high} high · ${cfg.advisories.moderate} moderate · ${cfg.advisories.low} low)` }, + { label: 'AFFECTED — action required', value: `${cfg.counts.affected.n} (${cfg.counts.affected.sev})`, badge: 'b-red' }, + { label: 'AFFECTED — mitigating controls in place', value: `${cfg.counts.mitigated.n} (${cfg.counts.mitigated.sev})`, badge: 'b-amber' }, + { label: 'NOT AFFECTED', value: `${cfg.counts.notAffected.n} (${cfg.counts.notAffected.pct})`, badge: 'b-green' }, + ]; +} + +const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(//g, '>'); +// 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