From 4294ddc6c549ba805032e6d828ffbb2ddb9b43a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 16:26:08 +0900 Subject: [PATCH 01/37] test(supply-chain): require deterministic npm lock generator --- .../tests/test_npm_toolchain_contract.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 services/analysis-engine/tests/test_npm_toolchain_contract.py diff --git a/services/analysis-engine/tests/test_npm_toolchain_contract.py b/services/analysis-engine/tests/test_npm_toolchain_contract.py new file mode 100644 index 00000000..89301fd9 --- /dev/null +++ b/services/analysis-engine/tests/test_npm_toolchain_contract.py @@ -0,0 +1,61 @@ +"""Contracts for deterministic npm lockfile generation and CI provenance.""" + +from __future__ import annotations + +import json +from pathlib import Path + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[3] +_EXPECTED_NPM_VERSION = "10.9.8" +_EXPECTED_NODE_VERSION = "22.22.3" + + +def _root_manifest() -> dict[str, object]: + """Return the checked-in root package manifest as a JSON object.""" + manifest = json.loads( + (_REPOSITORY_ROOT / "package.json").read_text(encoding="utf-8") + ) + assert isinstance(manifest, dict) + return manifest + + +def test_root_manifest_pins_the_lockfile_generator_and_fails_on_drift() -> None: + """Require npm and source-tree commands to reject a different generator.""" + manifest = _root_manifest() + + assert manifest["packageManager"] == f"npm@{_EXPECTED_NPM_VERSION}" + assert manifest["engines"]["npm"] == _EXPECTED_NPM_VERSION # type: ignore[index] + assert manifest["devEngines"] == { + "packageManager": { + "name": "npm", + "version": _EXPECTED_NPM_VERSION, + "onFail": "error", + } + } + + +def test_primary_ci_proves_exact_npm_before_install_and_lock_reproduction() -> None: + """Keep the clean installer and lock reproduction on one explicit toolchain.""" + workflow = (_REPOSITORY_ROOT / ".github" / "workflows" / "ci.yml").read_text( + encoding="utf-8" + ) + + assert f'node-version: "{_EXPECTED_NODE_VERSION}"' in workflow + assert f'EXPECTED_NPM_VERSION: "{_EXPECTED_NPM_VERSION}"' in workflow + assert 'test "$(npm --version)" = "$EXPECTED_NPM_VERSION"' in workflow + assert "npm install --package-lock-only" in workflow + assert "--ignore-scripts" in workflow + assert "--no-audit" in workflow + assert "--no-fund" in workflow + assert "git diff --exit-code -- package-lock.json" in workflow + + +def test_root_lock_uses_the_supported_location_keyed_format() -> None: + """Require the npm-v9-and-newer lock format used by the pinned generator.""" + lock_document = json.loads( + (_REPOSITORY_ROOT / "package-lock.json").read_text(encoding="utf-8") + ) + + assert lock_document["lockfileVersion"] == 3 + assert isinstance(lock_document["packages"], dict) From afb20beded7118dc9072fc3ae13d42af5d0b50c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 16:27:40 +0900 Subject: [PATCH 02/37] fix(supply-chain): pin npm lock generator metadata --- package.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a71236ed..276804de 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,17 @@ "private": true, "version": "0.1.3", "type": "module", + "packageManager": "npm@10.9.8", "engines": { - "node": ">=22.13 <23" + "node": ">=22.13 <23", + "npm": "10.9.8" + }, + "devEngines": { + "packageManager": { + "name": "npm", + "version": "10.9.8", + "onFail": "error" + } }, "workspaces": [ "apps/*", From e64c229c527785278229f9401602e659e8f3d0b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 16:28:01 +0900 Subject: [PATCH 03/37] ci(supply-chain): prove npm version and lock reproduction --- .github/workflows/ci.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f99a9c1..2750c5e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ env: GIT_CONFIG_COUNT: "1" GIT_CONFIG_KEY_0: init.defaultBranch GIT_CONFIG_VALUE_0: develop + EXPECTED_NPM_VERSION: "10.9.8" jobs: verify: @@ -26,14 +27,20 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.22.3 + node-version: "22.22.3" cache: npm + - name: Verify exact npm lockfile generator + run: test "$(npm --version)" = "$EXPECTED_NPM_VERSION" - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 with: version: "0.8.6" enable-cache: false - name: Install node dependencies run: npm ci + - name: Prove package lock reproduces with the pinned npm + run: | + npm install --package-lock-only --ignore-scripts --no-audit --no-fund + git diff --exit-code -- package-lock.json - name: Sync Python dependencies run: uv sync --project services/analysis-engine --group dev --frozen - name: Install stable Rust toolchain @@ -56,8 +63,10 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.22.3 + node-version: "22.22.3" cache: npm + - name: Verify exact npm lockfile generator + run: test "$(npm --version)" = "$EXPECTED_NPM_VERSION" - name: Install stable Rust toolchain run: rustup toolchain install stable --profile minimal - name: Install node dependencies From ebbde9baf051cd2944d14b141d791ce1777340cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 16:28:31 +0900 Subject: [PATCH 04/37] docs(supply-chain): record npm generator provenance --- .../npm-lockfile-generator-provenance.md | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docs/doctoring/npm-lockfile-generator-provenance.md diff --git a/docs/doctoring/npm-lockfile-generator-provenance.md b/docs/doctoring/npm-lockfile-generator-provenance.md new file mode 100644 index 00000000..83bf7356 --- /dev/null +++ b/docs/doctoring/npm-lockfile-generator-provenance.md @@ -0,0 +1,76 @@ +# npm lockfile generator provenance + +## Decision + +BandScope generates and verifies its root npm workspace lock with exactly npm `10.9.8`. The root manifest records that decision through: + +- `packageManager: npm@10.9.8` as package-manager selection metadata; +- `engines.npm: 10.9.8` as the published source-tree compatibility declaration; and +- `devEngines.packageManager` with `onFail: error` as npm's source-tree command gate. + +The primary GitHub Actions workflow uses Node `22.22.3`, verifies the bundled npm version before any installation, runs `npm ci`, then runs a package-lock-only regeneration with scripts, audit, and funding output disabled. Any `package-lock.json` diff fails the exact head. + +The Node runtime support decision remains separate. This change does not raise the public `>=22.13 <23` Node range; a coordinated Node-floor migration is tracked independently. + +## Why the generator is part of the lock identity + +npm documents `package-lock.json` as the location-keyed description of the exact dependency tree. Lockfile version 3 is intended for npm 9 and newer. npm also notes that different package-manager versions may use different installation algorithms and metadata representations. A committed lockfile therefore is not fully reproducible unless the generator version and install-shaping flags are versioned with it. + +`npm ci` is the immutable consumption path: it requires a lockfile, rejects manifest/lock dependency disagreement, removes an existing `node_modules`, and does not write the manifest or lock. It does not prove that a future dependency update will regenerate byte-identical metadata. The additional package-lock-only replay closes that gap. + +```mermaid +flowchart LR + M[package.json ranges and workspaces] --> G[npm 10.9.8] + C[project npm configuration] --> G + G --> L[package-lock.json v3] + L --> I[npm ci clean install] + I --> R[npm 10.9.8 package-lock-only replay] + R --> D{lock diff?} + D -->|no| A[reproducible exact-head evidence] + D -->|yes| F[fail closed] +``` + +## Security and operational boundary + +- Dependency PRs may change only manifest ranges and the lock records produced by npm `10.9.8`. +- Reviewers must reject unrelated lock metadata that cannot be reproduced by the pinned generator. +- No lock record may be added or removed by hand to satisfy a validator. +- Install-shaping flags that change the tree, such as `legacy-peer-deps` or `install-links`, must be committed in project configuration and used identically by `npm ci` and regeneration. +- Dependency lifecycle scripts remain disabled for the reproduction pass. The normal clean install retains the repository's reviewed execution behavior. +- The exact npm version check occurs before `npm ci`; a different bundled or globally installed npm cannot generate acceptance evidence. +- The lockfile remains the sole npm workspace lock. Nested workspace locks are prohibited. + +`packageManager` alone is not the enforcement boundary for npm because Corepack's npm shim is not enabled by default in Node distributions. Enforcement is provided by npm `devEngines`, the explicit CI version assertion, and the lock replay. + +## Verification + +`services/analysis-engine/tests/test_npm_toolchain_contract.py` verifies the manifest metadata, exact CI Node/npm identity, replay command and flags, clean lock diff, and lockfile version 3. Repository CI then executes the replay using the hosted toolchain. + +A dependency update is mergeable only after: + +1. npm `10.9.8` produces the checked-in lock from the updated manifest; +2. a second package-lock-only replay is byte-clean; +3. `npm ci`, lint, strict typecheck, measured tests, production build, Rust/Tauri checks, and security/supply-chain gates succeed on the same head; and +4. current-head review, unresolved-thread, independent-approval, and branch-protection requirements succeed without bypass. + +## Incident response and rollback + +When replay changes the lock unexpectedly: + +1. preserve the exact head SHA, npm and Node versions, command flags, original lock blob SHA, regenerated lock, and CI run ID; +2. determine whether the manifest changed, npm changed, project configuration changed, or the protected lock was generated by a different toolchain; +3. never accept a partial or hand-edited lock; +4. regenerate from a clean checkout using the reviewed npm version and run the replay twice; +5. if rollback is necessary, restore the prior manifest and complete lock together, then rerun the entire exact-head gate. + +## References + +npm, Inc. (2026). *npm ci*. npm Docs. https://docs.npmjs.com/cli/v11/commands/npm-ci/ + +npm, Inc. (2026). *npm install*. npm Docs. https://docs.npmjs.com/cli/v10/commands/npm-install/ + +npm, Inc. (2026). *package-lock.json*. npm Docs. https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json/ + +npm, Inc. (2026). *package.json*. npm Docs. https://docs.npmjs.com/cli/configuring-npm/package-json/ + +Node.js contributors. (2026). *Corepack* [Software documentation]. GitHub. https://github.com/nodejs/corepack From 694ca83b4dfd31ef12be464d832b6bb84fb07362 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 16:29:00 +0900 Subject: [PATCH 05/37] docs(changelog): record npm generator contract --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eea69689..05f55510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace. - 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함. +### Changed + +- Pinned npm `10.9.8` as the lockfile generator and made primary CI reject a different npm version or any package-lock-only replay diff. + ## [0.1.3] - 2026-04-29 ### Fixed From 90dc2a0248bd5259a24040303368acf7b909f376 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 16:58:59 +0900 Subject: [PATCH 06/37] fix(supply-chain): avoid serializing npm into runtime engines --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 276804de..779899ed 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,7 @@ "type": "module", "packageManager": "npm@10.9.8", "engines": { - "node": ">=22.13 <23", - "npm": "10.9.8" + "node": ">=22.13 <23" }, "devEngines": { "packageManager": { From fecc36b309f92afcf497a431edef4911dc6104f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 16:59:31 +0900 Subject: [PATCH 07/37] test(supply-chain): keep npm enforcement out of runtime engines --- services/analysis-engine/tests/test_npm_toolchain_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/analysis-engine/tests/test_npm_toolchain_contract.py b/services/analysis-engine/tests/test_npm_toolchain_contract.py index 89301fd9..b4a0ae46 100644 --- a/services/analysis-engine/tests/test_npm_toolchain_contract.py +++ b/services/analysis-engine/tests/test_npm_toolchain_contract.py @@ -25,7 +25,7 @@ def test_root_manifest_pins_the_lockfile_generator_and_fails_on_drift() -> None: manifest = _root_manifest() assert manifest["packageManager"] == f"npm@{_EXPECTED_NPM_VERSION}" - assert manifest["engines"]["npm"] == _EXPECTED_NPM_VERSION # type: ignore[index] + assert manifest["engines"] == {"node": ">=22.13 <23"} assert manifest["devEngines"] == { "packageManager": { "name": "npm", From 9eb83c39c9978ad730824d6e93218565b5b7d577 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:00:13 +0900 Subject: [PATCH 08/37] docs(supply-chain): separate npm generator from runtime engines --- docs/doctoring/npm-lockfile-generator-provenance.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/npm-lockfile-generator-provenance.md b/docs/doctoring/npm-lockfile-generator-provenance.md index 83bf7356..1d50f2a5 100644 --- a/docs/doctoring/npm-lockfile-generator-provenance.md +++ b/docs/doctoring/npm-lockfile-generator-provenance.md @@ -4,10 +4,11 @@ BandScope generates and verifies its root npm workspace lock with exactly npm `10.9.8`. The root manifest records that decision through: -- `packageManager: npm@10.9.8` as package-manager selection metadata; -- `engines.npm: 10.9.8` as the published source-tree compatibility declaration; and +- `packageManager: npm@10.9.8` as package-manager selection metadata; and - `devEngines.packageManager` with `onFail: error` as npm's source-tree command gate. +The npm version is intentionally not repeated under `engines`. npm serializes `engines` into the root lock package, so adding an npm-only source-tool constraint there creates lock metadata churn unrelated to dependency resolution. `devEngines`, the explicit CI assertion, and the replay gate enforce the generator while the published `engines.node` range remains the runtime compatibility contract. + The primary GitHub Actions workflow uses Node `22.22.3`, verifies the bundled npm version before any installation, runs `npm ci`, then runs a package-lock-only regeneration with scripts, audit, and funding output disabled. Any `package-lock.json` diff fails the exact head. The Node runtime support decision remains separate. This change does not raise the public `>=22.13 <23` Node range; a coordinated Node-floor migration is tracked independently. @@ -44,7 +45,7 @@ flowchart LR ## Verification -`services/analysis-engine/tests/test_npm_toolchain_contract.py` verifies the manifest metadata, exact CI Node/npm identity, replay command and flags, clean lock diff, and lockfile version 3. Repository CI then executes the replay using the hosted toolchain. +`services/analysis-engine/tests/test_npm_toolchain_contract.py` verifies the manifest metadata, separation of runtime and generator constraints, exact CI Node/npm identity, replay command and flags, clean lock diff, and lockfile version 3. Repository CI then executes the replay using the hosted toolchain. A dependency update is mergeable only after: From 5609b8828c7cd5e0bec0d0eed0a56d58197b8e86 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:04:24 +0900 Subject: [PATCH 09/37] ci(supply-chain): publish deterministic lock reproduction evidence --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2750c5e0..8311a1da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,33 @@ env: EXPECTED_NPM_VERSION: "10.9.8" jobs: + lock-reproduction: + name: gate / ci / npm-lock-reproduction + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22.22.3" + cache: npm + - name: Verify exact npm lockfile generator + run: test "$(npm --version)" = "$EXPECTED_NPM_VERSION" + - name: Reproduce package lock without lifecycle execution + run: npm install --package-lock-only --ignore-scripts --no-audit --no-fund + - name: Preserve the exact generated lock as review evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: npm-lock-reproduction-${{ github.event.pull_request.head.sha || github.sha }} + path: package-lock.json + if-no-files-found: error + retention-days: 3 + - name: Reject lockfile drift + run: git diff --exit-code -- package-lock.json + verify: name: ci / build-and-test + needs: lock-reproduction runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -37,10 +62,6 @@ jobs: enable-cache: false - name: Install node dependencies run: npm ci - - name: Prove package lock reproduces with the pinned npm - run: | - npm install --package-lock-only --ignore-scripts --no-audit --no-fund - git diff --exit-code -- package-lock.json - name: Sync Python dependencies run: uv sync --project services/analysis-engine --group dev --frozen - name: Install stable Rust toolchain @@ -58,6 +79,7 @@ jobs: rust-check: name: gate / ci / rust-check + needs: lock-reproduction runs-on: macos-15 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 From 032314d724e0ddc5ce16c7958ee364dd40264699 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:04:59 +0900 Subject: [PATCH 10/37] test(supply-chain): require preserved lock reproduction evidence --- .../analysis-engine/tests/test_npm_toolchain_contract.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/analysis-engine/tests/test_npm_toolchain_contract.py b/services/analysis-engine/tests/test_npm_toolchain_contract.py index b4a0ae46..30e00cbd 100644 --- a/services/analysis-engine/tests/test_npm_toolchain_contract.py +++ b/services/analysis-engine/tests/test_npm_toolchain_contract.py @@ -44,10 +44,15 @@ def test_primary_ci_proves_exact_npm_before_install_and_lock_reproduction() -> N assert f'node-version: "{_EXPECTED_NODE_VERSION}"' in workflow assert f'EXPECTED_NPM_VERSION: "{_EXPECTED_NPM_VERSION}"' in workflow assert 'test "$(npm --version)" = "$EXPECTED_NPM_VERSION"' in workflow + assert "lock-reproduction:" in workflow + assert "needs: lock-reproduction" in workflow assert "npm install --package-lock-only" in workflow assert "--ignore-scripts" in workflow assert "--no-audit" in workflow assert "--no-fund" in workflow + assert "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" in workflow + assert "npm-lock-reproduction-${{ github.event.pull_request.head.sha || github.sha }}" in workflow + assert "if-no-files-found: error" in workflow assert "git diff --exit-code -- package-lock.json" in workflow From 0263ad427aa543e42ad9f01be1858074cf43ac52 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:08:32 +0900 Subject: [PATCH 11/37] test(security): require coordinated PDF.js and Undici baseline --- .../test_high_security_dependency_baseline.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 services/analysis-engine/tests/test_high_security_dependency_baseline.py diff --git a/services/analysis-engine/tests/test_high_security_dependency_baseline.py b/services/analysis-engine/tests/test_high_security_dependency_baseline.py new file mode 100644 index 00000000..01dba0ab --- /dev/null +++ b/services/analysis-engine/tests/test_high_security_dependency_baseline.py @@ -0,0 +1,79 @@ +"""Contracts for the coordinated PDF.js and Undici security baseline.""" + +from __future__ import annotations + +import json +from pathlib import Path + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[3] +_PDFJS_VERSION = "6.2.108" +_UNDICI_VERSION = "7.29.0" +_PDFJS_INTEGRITY = ( + "sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5Tcczz" + "OK6261auRkP/M8OBHs9vFQ==" +) +_UNDICI_INTEGRITY = ( + "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9" + "rWmsreUyf5lwyao+7GNNVw==" +) + + +def _read_json(relative_path: str) -> dict[str, object]: + """Return one repository JSON document as a mapping.""" + document = json.loads( + (_REPOSITORY_ROOT / relative_path).read_text(encoding="utf-8") + ) + assert isinstance(document, dict) + return document + + +def test_manifests_pin_the_security_floors_without_semver_drift() -> None: + """Keep the vulnerable transitive client and PDF parser on exact versions.""" + root_manifest = _read_json("package.json") + desktop_manifest = _read_json("apps/desktop/package.json") + + assert root_manifest["overrides"]["undici"] == _UNDICI_VERSION # type: ignore[index] + assert desktop_manifest["dependencies"]["pdfjs-dist"] == _PDFJS_VERSION # type: ignore[index] + + +def test_lock_records_match_exact_registry_artifacts_and_preserve_peer_metadata() -> None: + """Require the pinned generator's exact graph without unrelated esbuild churn.""" + lock_document = _read_json("package-lock.json") + packages = lock_document["packages"] + assert isinstance(packages, dict) + + desktop = packages["apps/desktop"] + assert isinstance(desktop, dict) + assert desktop["dependencies"]["pdfjs-dist"] == _PDFJS_VERSION # type: ignore[index] + + pdfjs = packages["node_modules/pdfjs-dist"] + assert pdfjs == { + "version": _PDFJS_VERSION, + "resolved": ( + "https://registry.npmjs.org/pdfjs-dist/-/" + f"pdfjs-dist-{_PDFJS_VERSION}.tgz" + ), + "integrity": _PDFJS_INTEGRITY, + "license": "Apache-2.0", + "engines": {"node": ">=22.13.0 || >=24"}, + } + + undici = packages["node_modules/undici"] + assert isinstance(undici, dict) + assert undici["version"] == _UNDICI_VERSION + assert undici["resolved"] == ( + "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz" + ) + assert undici["integrity"] == _UNDICI_INTEGRITY + + esbuild_locations = { + path: metadata + for path, metadata in packages.items() + if isinstance(path, str) and path.startswith("node_modules/@esbuild/") + } + assert esbuild_locations + assert all( + isinstance(metadata, dict) and metadata.get("peer") is True + for metadata in esbuild_locations.values() + ) From 2cbf767f6fb42f7c8034c116c9b038350532b2b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:08:52 +0900 Subject: [PATCH 12/37] test(security): disable PDF expression evaluation --- apps/desktop/src/features/score/pdfjs.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 apps/desktop/src/features/score/pdfjs.test.ts diff --git a/apps/desktop/src/features/score/pdfjs.test.ts b/apps/desktop/src/features/score/pdfjs.test.ts new file mode 100644 index 00000000..49392ed6 --- /dev/null +++ b/apps/desktop/src/features/score/pdfjs.test.ts @@ -0,0 +1,42 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { getDocument, GlobalWorkerOptions } from "pdfjs-dist"; +import { configureScorePdfWorker, loadScorePdf } from "./pdfjs"; + +vi.mock("pdfjs-dist", () => ({ + getDocument: vi.fn(() => ({ promise: Promise.resolve(), destroy: vi.fn() })), + GlobalWorkerOptions: { workerSrc: "" } +})); + +vi.mock("pdfjs-dist/build/pdf.worker.min.mjs?url", () => ({ + default: "/assets/pdf.worker.min.mjs" +})); + +describe("score PDF.js boundary", () => { + beforeEach(() => { + vi.mocked(getDocument).mockClear(); + GlobalWorkerOptions.workerSrc = ""; + }); + + it("uses the locally bundled worker asset", () => { + configureScorePdfWorker(); + + expect(GlobalWorkerOptions.workerSrc).toBe("/assets/pdf.worker.min.mjs"); + + configureScorePdfWorker(); + expect(GlobalWorkerOptions.workerSrc).toBe("/assets/pdf.worker.min.mjs"); + }); + + it("copies validated bytes and disables PDF expression evaluation", () => { + const source = new Uint8Array([0x25, 0x50, 0x44, 0x46]); + + loadScorePdf(source); + + expect(getDocument).toHaveBeenCalledTimes(1); + const parameters = vi.mocked(getDocument).mock.calls[0]?.[0]; + expect(parameters).toMatchObject({ isEvalSupported: false }); + expect(parameters).toHaveProperty("data"); + const copiedBytes = (parameters as { data: Uint8Array }).data; + expect(copiedBytes).toEqual(source); + expect(copiedBytes).not.toBe(source); + }); +}); From 475de96fc523bfab739817bb665493b11d0dd8bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:10:02 +0900 Subject: [PATCH 13/37] fix(security): pin the patched Undici transitive version --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 779899ed..1ad0f15e 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ }, "overrides": { "brace-expansion": "5.0.9", - "postcss": "8.5.25" + "postcss": "8.5.25", + "undici": "7.29.0" } } From deb74acd7da82fe0696d5d876072455c7bebbec2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:10:43 +0900 Subject: [PATCH 14/37] fix(security): pin the patched PDF.js release --- apps/desktop/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e7685d6f..e09719b2 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -20,7 +20,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.24.0", - "pdfjs-dist": "6.1.200", + "pdfjs-dist": "6.2.108", "react": "^19.2.4", "react-dom": "^19.2.7", "sonner": "^2.0.7", From dc90d5b0d8bb516df28467d09332256d141de836 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:11:01 +0900 Subject: [PATCH 15/37] fix(security): disable PDF expression evaluation --- apps/desktop/src/features/score/pdfjs.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/features/score/pdfjs.ts b/apps/desktop/src/features/score/pdfjs.ts index b62526c8..007cd572 100644 --- a/apps/desktop/src/features/score/pdfjs.ts +++ b/apps/desktop/src/features/score/pdfjs.ts @@ -22,8 +22,13 @@ export function configureScorePdfWorker(): void { * this helper never fetches arbitrary URLs. The bytes are copied before they * are handed to pdf.js because pdf.js transfers the underlying buffer to its * worker, which would otherwise detach the caller's copy and break retries. + * PDF expression evaluation remains disabled as defense in depth even when + * the installed pdf.js release includes the corresponding security patch. */ export function loadScorePdf(data: Uint8Array): PDFDocumentLoadingTask { configureScorePdfWorker(); - return getDocument({ data: new Uint8Array(data) }); + return getDocument({ + data: new Uint8Array(data), + isEvalSupported: false + }); } From b23b957c2bcca70abcc5bd36d5ae05e343fa0054 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:12:13 +0900 Subject: [PATCH 16/37] docs(security): record coordinated PDF and HTTP remediation --- .../high-security-pdf-http-baseline.md | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/doctoring/high-security-pdf-http-baseline.md diff --git a/docs/doctoring/high-security-pdf-http-baseline.md b/docs/doctoring/high-security-pdf-http-baseline.md new file mode 100644 index 00000000..d7b4d401 --- /dev/null +++ b/docs/doctoring/high-security-pdf-http-baseline.md @@ -0,0 +1,78 @@ +# High-security PDF and HTTP dependency baseline + +## Decision + +BandScope treats the PDF parser and its transitive HTTP client as one security-release boundary: + +- `pdfjs-dist` is pinned exactly to `6.2.108`; +- `undici` is pinned exactly to `7.29.0` through the root npm override; and +- the complete npm workspace lock is generated only by the repository-pinned npm `10.9.8` workflow and imported unchanged from the workflow artifact. + +The desktop PDF loader additionally passes `isEvalSupported: false` to `getDocument`. The dependency patch is the primary remediation; disabling expression evaluation is defense in depth and prevents a future regression or alternate vulnerable execution path from re-enabling dynamic PDF expression compilation. + +```mermaid +flowchart LR + A[Validated local PDF bytes] --> B[Copied Uint8Array] + B --> C[pdfjs-dist 6.2.108] + P[isEvalSupported false] --> C + C --> W[Same-origin bundled worker] + W --> R[Canvas render] + J[jsdom development path] --> U[undici 7.29.0 override] + N[npm 10.9.8] --> L[Exact package-lock artifact] + L --> C + L --> U +``` + +## Threat boundary + +The score viewer accepts only bytes already copied into the app-owned workspace through the native PDF intake boundary. It does not accept a URL and never uses a remote worker. This prevents a PDF from selecting an attacker-controlled fetch origin or script asset. + +PDF bytes remain untrusted after the native magic-byte, size, and path checks. Parser vulnerabilities, malformed object graphs, embedded actions, and expression compilation can still occur inside a syntactically valid PDF. The patched parser and explicit `isEvalSupported: false` therefore remain mandatory even for locally selected files. + +Undici is currently a development dependency reached through jsdom, but development and CI parsers process attacker-controlled fixtures, generated HTML, and network-like request bodies. A dev-only label does not make header injection, shared-cache disclosure, retry desynchronization, or cookie-attribute injection acceptable in the trusted build boundary. + +## Lockfile provenance + +The security manifests are changed before the lock. The exact branch workflow then: + +1. verifies Node `22.22.3` and npm `10.9.8`; +2. runs `npm install --package-lock-only --ignore-scripts --no-audit --no-fund`; +3. uploads the generated `package-lock.json` under a head-SHA-bound artifact name; and +4. fails while the generated lock differs from the branch. + +The maintainer imports that generated artifact byte-for-byte and reruns the workflow. The second run must produce a clean diff. No tarball URL, SRI, dependency range, `peer` classification, or workspace record is edited by hand. + +The lock contract requires the exact public-registry tarball and SHA-512 SRI for both patched packages and requires every existing `node_modules/@esbuild/*` location to retain npm 10.9.8's `peer: true` classification. This distinguishes the intended security graph from unrelated Dependabot generator churn. + +## Verification + +The merge gate includes: + +- exact manifest and lock artifact tests; +- a direct PDF.js wrapper test for copied bytes, the locally bundled worker, and `isEvalSupported: false`; +- valid and malformed local score-PDF component tests; +- desktop lint, strict typecheck, complete measured tests, and production build; +- Tauri/Rust checks and native PDF intake regressions; +- `npm audit --workspaces --audit-level=high` with no high finding; +- repository SAST, CodeQL, security scan, secret scan, SBOM, and dependency evidence; +- current-head central coverage and automated review; +- zero unresolved actionable threads and a qualifying independent non-author approval; and +- normal branch protection without administrative bypass. + +## Failure, rollback, and incident evidence + +On a failed lock replay or parser regression, preserve the exact head SHA, Node/npm versions, generated-lock artifact ID and digest, original and generated lock blob SHA, test output, audit report, and workflow run ID. Do not merge a partially updated graph. + +Rollback restores the previous desktop manifest, root override, complete lock, PDF loader, tests, and CHANGELOG entry together. Because the previous graph contains known high findings, rollback is an emergency availability action only and requires an explicit security exception, compensating controls, owner, expiration, and immediate replacement plan. + +## References + +GitHub. (2026). *PDF.js vulnerable to arbitrary JavaScript execution upon opening a malicious PDF* (GHSA-hq66-cqwq-w95j) [Security advisory]. https://github.com/advisories/GHSA-hq66-cqwq-w95j + +Mozilla. (2026). *PDF.js 6.2.108* [Software release]. https://github.com/mozilla/pdf.js/releases/tag/v6.2.108 + +Node.js contributors. (2026). *Undici 7.29.0* [Software release]. https://github.com/nodejs/undici/releases/tag/v7.29.0 + +npm, Inc. (2026). *npm ci*. npm Docs. https://docs.npmjs.com/cli/v11/commands/npm-ci/ + +npm, Inc. (2026). *package-lock.json*. npm Docs. https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json/ From 2e99f72d2859250a460bcadd6248b0ea93a0a8d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:12:46 +0900 Subject: [PATCH 17/37] docs(changelog): record coordinated security remediation --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05f55510..1d991014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - Pinned npm `10.9.8` as the lockfile generator and made primary CI reject a different npm version or any package-lock-only replay diff. +### Fixed + +- Upgraded the local score PDF parser to `pdfjs-dist` 6.2.108, pinned Undici 7.29.0 across the workspace, and disabled PDF expression evaluation while preserving same-origin worker execution and npm-generated lock provenance. + ## [0.1.3] - 2026-04-29 ### Fixed From 8f50fe4ce8bf2440de6fbf55d48bc918bd311ffc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:20:04 +0900 Subject: [PATCH 18/37] fix(security): anchor the Undici override to an exact root floor --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1ad0f15e..2c3a4e94 100644 --- a/package.json +++ b/package.json @@ -41,11 +41,12 @@ "@eslint/js": "^10.0.1", "eslint-plugin-jsdoc": "^63.0.13", "react": "^19.2.4", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "undici": "7.29.0" }, "overrides": { "brace-expansion": "5.0.9", "postcss": "8.5.25", - "undici": "7.29.0" + "undici": "$undici" } } From e2c0c2dfee35c19d1e9156ad0a7d9324fdefd1fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 17:20:46 +0900 Subject: [PATCH 19/37] test(security): bind exact root floor and npm package locations --- .../test_high_security_dependency_baseline.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/services/analysis-engine/tests/test_high_security_dependency_baseline.py b/services/analysis-engine/tests/test_high_security_dependency_baseline.py index 01dba0ab..0a9e81ab 100644 --- a/services/analysis-engine/tests/test_high_security_dependency_baseline.py +++ b/services/analysis-engine/tests/test_high_security_dependency_baseline.py @@ -33,7 +33,8 @@ def test_manifests_pin_the_security_floors_without_semver_drift() -> None: root_manifest = _read_json("package.json") desktop_manifest = _read_json("apps/desktop/package.json") - assert root_manifest["overrides"]["undici"] == _UNDICI_VERSION # type: ignore[index] + assert root_manifest["devDependencies"]["undici"] == _UNDICI_VERSION # type: ignore[index] + assert root_manifest["overrides"]["undici"] == "$undici" # type: ignore[index] assert desktop_manifest["dependencies"]["pdfjs-dist"] == _PDFJS_VERSION # type: ignore[index] @@ -43,21 +44,24 @@ def test_lock_records_match_exact_registry_artifacts_and_preserve_peer_metadata( packages = lock_document["packages"] assert isinstance(packages, dict) + root_package = packages[""] + assert isinstance(root_package, dict) + assert root_package["devDependencies"]["undici"] == _UNDICI_VERSION # type: ignore[index] + desktop = packages["apps/desktop"] assert isinstance(desktop, dict) assert desktop["dependencies"]["pdfjs-dist"] == _PDFJS_VERSION # type: ignore[index] - pdfjs = packages["node_modules/pdfjs-dist"] - assert pdfjs == { - "version": _PDFJS_VERSION, - "resolved": ( - "https://registry.npmjs.org/pdfjs-dist/-/" - f"pdfjs-dist-{_PDFJS_VERSION}.tgz" - ), - "integrity": _PDFJS_INTEGRITY, - "license": "Apache-2.0", - "engines": {"node": ">=22.13.0 || >=24"}, - } + pdfjs = packages["apps/desktop/node_modules/pdfjs-dist"] + assert isinstance(pdfjs, dict) + assert pdfjs["version"] == _PDFJS_VERSION + assert pdfjs["resolved"] == ( + "https://registry.npmjs.org/pdfjs-dist/-/" + f"pdfjs-dist-{_PDFJS_VERSION}.tgz" + ) + assert pdfjs["integrity"] == _PDFJS_INTEGRITY + assert pdfjs["license"] == "Apache-2.0" + assert pdfjs["engines"] == {"node": ">=22.13.0 || >=24"} undici = packages["node_modules/undici"] assert isinstance(undici, dict) From dd93f962d367e3a5b56ee171708bf4877a054541 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 18:59:55 +0900 Subject: [PATCH 20/37] ci(pr783): import exact npm 10.9.8 lock artifact --- .../workflows/import-pr783-generated-lock.yml | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 .github/workflows/import-pr783-generated-lock.yml diff --git a/.github/workflows/import-pr783-generated-lock.yml b/.github/workflows/import-pr783-generated-lock.yml new file mode 100644 index 00000000..c2ce31af --- /dev/null +++ b/.github/workflows/import-pr783-generated-lock.yml @@ -0,0 +1,125 @@ +name: Import PR 783 generated npm lock + +on: + push: + branches: + - fix/high-security-dependency-baseline + paths: + - .github/workflows/import-pr783-generated-lock.yml + +permissions: + contents: read + actions: read + +concurrency: + group: import-pr783-generated-lock + cancel-in-progress: false + +env: + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: develop + EXPECTED_SOURCE_HEAD: e2c0c2dfee35c19d1e9156ad0a7d9324fdefd1fb + EXPECTED_NPM_VERSION: "10.9.8" + ARTIFACT_ID: "8989562185" + ARTIFACT_RUN_ID: "31161313485" + ARTIFACT_NAME: npm-lock-reproduction-e2c0c2dfee35c19d1e9156ad0a7d9324fdefd1fb + EXPECTED_LOCK_SHA256: 31dd2661eca864e3da46f86629a2535dc181d01449bd3a50fa3cdbd6c58e7971 + +jobs: + import-and-verify: + if: >- + github.repository == 'ContextualWisdomLab/bandscope' && + github.ref == 'refs/heads/fix/high-security-dependency-baseline' + permissions: + contents: write + actions: read + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout exact trigger without persisted credentials + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + fetch-depth: 2 + persist-credentials: false + + - name: Verify bounded trigger lineage + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + test "$(git rev-parse HEAD^)" = "$EXPECTED_SOURCE_HEAD" + mapfile -t trigger_delta < <(git diff --name-only "$EXPECTED_SOURCE_HEAD" HEAD) + test "${#trigger_delta[@]}" -eq 1 + test "${trigger_delta[0]}" = ".github/workflows/import-pr783-generated-lock.yml" + + - name: Set up exact Node and npm generator + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22.22.3" + cache: npm + + - name: Download exact head-bound lock artifact + env: + GH_TOKEN: ${{ github.token }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(npm --version)" = "$EXPECTED_NPM_VERSION" + mkdir -p "${RUNNER_TEMP}/pr783-lock" + curl --fail --silent --show-error --location \ + --retry 0 \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: Bearer ${GH_TOKEN}" \ + --header "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}/zip" \ + --output "${RUNNER_TEMP}/pr783-lock/artifact.zip" + mapfile -t archive_entries < <(zipinfo -1 "${RUNNER_TEMP}/pr783-lock/artifact.zip") + test "${#archive_entries[@]}" -eq 1 + test "${archive_entries[0]}" = "package-lock.json" + unzip -p "${RUNNER_TEMP}/pr783-lock/artifact.zip" package-lock.json \ + > "${RUNNER_TEMP}/pr783-lock/package-lock.json" + test "$(sha256sum "${RUNNER_TEMP}/pr783-lock/package-lock.json" | cut -d' ' -f1)" = "$EXPECTED_LOCK_SHA256" + mv "${RUNNER_TEMP}/pr783-lock/package-lock.json" package-lock.json + + - name: Verify exact generated lock and security baseline + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(sha256sum package-lock.json | cut -d' ' -f1)" = "$EXPECTED_LOCK_SHA256" + npm install --package-lock-only --ignore-scripts --no-audit --no-fund + test "$(sha256sum package-lock.json | cut -d' ' -f1)" = "$EXPECTED_LOCK_SHA256" + npm ci --ignore-scripts --no-audit --no-fund + npm audit --workspaces --audit-level=high + npm run typecheck --workspace @bandscope/desktop + npm run lint --workspace @bandscope/desktop + npm exec --workspace @bandscope/desktop vitest run \ + src/features/score/pdfjs.test.ts \ + --coverage=false + git diff --check + + - name: Remove one-shot importer and publish verified lock + env: + EXPECTED_HEAD: ${{ github.sha }} + SOURCE_BRANCH: fix/high-security-dependency-baseline + PUSH_TOKEN: ${{ github.token }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + rm .github/workflows/import-pr783-generated-lock.yml + test "$(git diff --name-only "$EXPECTED_SOURCE_HEAD" -- | sort)" = "package-lock.json" + test "$(sha256sum package-lock.json | cut -d' ' -f1)" = "$EXPECTED_LOCK_SHA256" + remote_head="$(git ls-remote origin "refs/heads/${SOURCE_BRANCH}" | cut -f1)" + test "$remote_head" = "$EXPECTED_HEAD" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add package-lock.json .github/workflows/import-pr783-generated-lock.yml + git diff --cached --check + git commit -m "fix(security): import verified npm 10.9.8 lock" + auth_header="$(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" + echo "::add-mask::$auth_header" + git -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ + push --force-with-lease="refs/heads/${SOURCE_BRANCH}:${EXPECTED_HEAD}" \ + origin "HEAD:refs/heads/${SOURCE_BRANCH}" From dd8d1acda0a32ba02809b6a27cd9933815de03d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:07:00 +0900 Subject: [PATCH 21/37] fix(score): align PDF.js boundary with 6.2.108 API --- apps/desktop/src/features/score/pdfjs.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/features/score/pdfjs.ts b/apps/desktop/src/features/score/pdfjs.ts index 007cd572..e0ce7692 100644 --- a/apps/desktop/src/features/score/pdfjs.ts +++ b/apps/desktop/src/features/score/pdfjs.ts @@ -22,13 +22,13 @@ export function configureScorePdfWorker(): void { * this helper never fetches arbitrary URLs. The bytes are copied before they * are handed to pdf.js because pdf.js transfers the underlying buffer to its * worker, which would otherwise detach the caller's copy and break retries. - * PDF expression evaluation remains disabled as defense in depth even when - * the installed pdf.js release includes the corresponding security patch. + * + * PDF.js 6.2.108 no longer exposes the legacy `isEvalSupported` initialization + * option. Security therefore relies on the patched parser release plus this + * narrow data-only, same-origin-worker boundary rather than an ignored and + * falsely reassuring unknown option. */ export function loadScorePdf(data: Uint8Array): PDFDocumentLoadingTask { configureScorePdfWorker(); - return getDocument({ - data: new Uint8Array(data), - isEvalSupported: false - }); + return getDocument({ data: new Uint8Array(data) }); } From 988dc1d30878994042cd72b9f7d5b6c38c059f3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:08:02 +0900 Subject: [PATCH 22/37] test(score): prove the supported data-only PDF.js boundary --- apps/desktop/src/features/score/pdfjs.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/features/score/pdfjs.test.ts b/apps/desktop/src/features/score/pdfjs.test.ts index 49392ed6..b225830e 100644 --- a/apps/desktop/src/features/score/pdfjs.test.ts +++ b/apps/desktop/src/features/score/pdfjs.test.ts @@ -26,15 +26,15 @@ describe("score PDF.js boundary", () => { expect(GlobalWorkerOptions.workerSrc).toBe("/assets/pdf.worker.min.mjs"); }); - it("copies validated bytes and disables PDF expression evaluation", () => { + it("copies validated bytes through the supported data-only API", () => { const source = new Uint8Array([0x25, 0x50, 0x44, 0x46]); loadScorePdf(source); expect(getDocument).toHaveBeenCalledTimes(1); const parameters = vi.mocked(getDocument).mock.calls[0]?.[0]; - expect(parameters).toMatchObject({ isEvalSupported: false }); - expect(parameters).toHaveProperty("data"); + expect(parameters).toBeTypeOf("object"); + expect(Object.keys(parameters as object)).toEqual(["data"]); const copiedBytes = (parameters as { data: Uint8Array }).data; expect(copiedBytes).toEqual(source); expect(copiedBytes).not.toBe(source); From a39e37f3eca2a185feddc142d5ea648c943ce9a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:09:48 +0900 Subject: [PATCH 23/37] docs(security): record the supported PDF.js 6.2.108 boundary --- docs/doctoring/high-security-pdf-http-baseline.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/doctoring/high-security-pdf-http-baseline.md b/docs/doctoring/high-security-pdf-http-baseline.md index d7b4d401..d83a63eb 100644 --- a/docs/doctoring/high-security-pdf-http-baseline.md +++ b/docs/doctoring/high-security-pdf-http-baseline.md @@ -8,13 +8,13 @@ BandScope treats the PDF parser and its transitive HTTP client as one security-r - `undici` is pinned exactly to `7.29.0` through the root npm override; and - the complete npm workspace lock is generated only by the repository-pinned npm `10.9.8` workflow and imported unchanged from the workflow artifact. -The desktop PDF loader additionally passes `isEvalSupported: false` to `getDocument`. The dependency patch is the primary remediation; disabling expression evaluation is defense in depth and prevents a future regression or alternate vulnerable execution path from re-enabling dynamic PDF expression compilation. +PDF.js `6.2.108` no longer exposes the legacy `isEvalSupported` member in its public `DocumentInitParameters` contract, and `getDocument` no longer reads that member. BandScope therefore does not cast or pass an unknown option that would be ignored while creating false assurance. The primary remediation is the patched parser release, reinforced by a narrow data-only call, copied caller-owned bytes, and a same-origin bundled worker. ```mermaid flowchart LR A[Validated local PDF bytes] --> B[Copied Uint8Array] - B --> C[pdfjs-dist 6.2.108] - P[isEvalSupported false] --> C + B --> D[Data-only DocumentInitParameters] + D --> C[pdfjs-dist 6.2.108] C --> W[Same-origin bundled worker] W --> R[Canvas render] J[jsdom development path] --> U[undici 7.29.0 override] @@ -25,9 +25,9 @@ flowchart LR ## Threat boundary -The score viewer accepts only bytes already copied into the app-owned workspace through the native PDF intake boundary. It does not accept a URL and never uses a remote worker. This prevents a PDF from selecting an attacker-controlled fetch origin or script asset. +The score viewer accepts only bytes already copied into the app-owned workspace through the native PDF intake boundary. It does not accept a URL, credentials, custom request headers, or a remote worker. This prevents a PDF from selecting an attacker-controlled fetch origin or script asset. -PDF bytes remain untrusted after the native magic-byte, size, and path checks. Parser vulnerabilities, malformed object graphs, embedded actions, and expression compilation can still occur inside a syntactically valid PDF. The patched parser and explicit `isEvalSupported: false` therefore remain mandatory even for locally selected files. +PDF bytes remain untrusted after the native magic-byte, size, and path checks. Parser vulnerabilities, malformed object graphs, embedded actions, and resource-exhaustion paths can still occur inside a syntactically valid PDF. The patched parser, exact dependency lock, copied data-only input, same-origin worker, and existing native intake limits therefore remain mandatory for locally selected files. Undici is currently a development dependency reached through jsdom, but development and CI parsers process attacker-controlled fixtures, generated HTML, and network-like request bodies. A dev-only label does not make header injection, shared-cache disclosure, retry desynchronization, or cookie-attribute injection acceptable in the trusted build boundary. @@ -49,7 +49,8 @@ The lock contract requires the exact public-registry tarball and SHA-512 SRI for The merge gate includes: - exact manifest and lock artifact tests; -- a direct PDF.js wrapper test for copied bytes, the locally bundled worker, and `isEvalSupported: false`; +- a direct PDF.js wrapper test proving copied bytes, the locally bundled worker, and an exact data-only initialization object; +- TypeScript compilation against the installed PDF.js `DocumentInitParameters` rather than an unsafe cast; - valid and malformed local score-PDF component tests; - desktop lint, strict typecheck, complete measured tests, and production build; - Tauri/Rust checks and native PDF intake regressions; @@ -69,6 +70,8 @@ Rollback restores the previous desktop manifest, root override, complete lock, P GitHub. (2026). *PDF.js vulnerable to arbitrary JavaScript execution upon opening a malicious PDF* (GHSA-hq66-cqwq-w95j) [Security advisory]. https://github.com/advisories/GHSA-hq66-cqwq-w95j +Mozilla. (2026). *Document initialization parameters in PDF.js 6.2.108* [Source code]. GitHub. https://github.com/mozilla/pdf.js/blob/v6.2.108/src/display/api.js + Mozilla. (2026). *PDF.js 6.2.108* [Software release]. https://github.com/mozilla/pdf.js/releases/tag/v6.2.108 Node.js contributors. (2026). *Undici 7.29.0* [Software release]. https://github.com/nodejs/undici/releases/tag/v7.29.0 From 6b753e6b0bacbe250885c20a53728966b81e0009 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:11:29 +0900 Subject: [PATCH 24/37] docs(changelog): describe the supported patched PDF boundary --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d991014..c746043f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ ### Fixed -- Upgraded the local score PDF parser to `pdfjs-dist` 6.2.108, pinned Undici 7.29.0 across the workspace, and disabled PDF expression evaluation while preserving same-origin worker execution and npm-generated lock provenance. +- Upgraded the local score PDF parser to `pdfjs-dist` 6.2.108, pinned Undici 7.29.0 across the workspace, and constrained PDF loading to copied in-memory bytes with a same-origin bundled worker and npm-generated lock provenance. ## [0.1.3] - 2026-04-29 From b773653f76fae5cfbda4a24548bffd4fedfe06ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:14:08 +0900 Subject: [PATCH 25/37] ci(pr783): rerun lock import after supported API repair --- .github/workflows/import-pr783-generated-lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/import-pr783-generated-lock.yml b/.github/workflows/import-pr783-generated-lock.yml index c2ce31af..c9616f45 100644 --- a/.github/workflows/import-pr783-generated-lock.yml +++ b/.github/workflows/import-pr783-generated-lock.yml @@ -19,7 +19,7 @@ env: GIT_CONFIG_COUNT: "1" GIT_CONFIG_KEY_0: init.defaultBranch GIT_CONFIG_VALUE_0: develop - EXPECTED_SOURCE_HEAD: e2c0c2dfee35c19d1e9156ad0a7d9324fdefd1fb + EXPECTED_SOURCE_HEAD: 6b753e6b0bacbe250885c20a53728966b81e0009 EXPECTED_NPM_VERSION: "10.9.8" ARTIFACT_ID: "8989562185" ARTIFACT_RUN_ID: "31161313485" From 01cb39e9c9f7513fc3410f86b2f1e1332a969da4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:19:01 +0900 Subject: [PATCH 26/37] ci(pr783): publish the verified lock from the bounded importer --- .github/workflows/import-pr783-generated-lock.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/import-pr783-generated-lock.yml b/.github/workflows/import-pr783-generated-lock.yml index c9616f45..f29616e9 100644 --- a/.github/workflows/import-pr783-generated-lock.yml +++ b/.github/workflows/import-pr783-generated-lock.yml @@ -20,6 +20,7 @@ env: GIT_CONFIG_KEY_0: init.defaultBranch GIT_CONFIG_VALUE_0: develop EXPECTED_SOURCE_HEAD: 6b753e6b0bacbe250885c20a53728966b81e0009 + EXPECTED_TRIGGER_PARENT: b773653f76fae5cfbda4a24548bffd4fedfe06ef EXPECTED_NPM_VERSION: "10.9.8" ARTIFACT_ID: "8989562185" ARTIFACT_RUN_ID: "31161313485" @@ -53,8 +54,8 @@ jobs: shell: bash --noprofile --norc -e -o pipefail {0} run: | test "$(git rev-parse HEAD)" = "$GITHUB_SHA" - test "$(git rev-parse HEAD^)" = "$EXPECTED_SOURCE_HEAD" - mapfile -t trigger_delta < <(git diff --name-only "$EXPECTED_SOURCE_HEAD" HEAD) + test "$(git rev-parse HEAD^)" = "$EXPECTED_TRIGGER_PARENT" + mapfile -t trigger_delta < <(git diff --name-only "$EXPECTED_TRIGGER_PARENT" HEAD) test "${#trigger_delta[@]}" -eq 1 test "${trigger_delta[0]}" = ".github/workflows/import-pr783-generated-lock.yml" @@ -109,7 +110,9 @@ jobs: shell: bash --noprofile --norc -e -o pipefail {0} run: | rm .github/workflows/import-pr783-generated-lock.yml - test "$(git diff --name-only "$EXPECTED_SOURCE_HEAD" -- | sort)" = "package-lock.json" + actual_delta="$(git diff --name-only "$EXPECTED_SOURCE_HEAD" -- | sort)" + expected_delta="$(printf '%s\n' .github/workflows/import-pr783-generated-lock.yml package-lock.json | sort)" + test "$actual_delta" = "$expected_delta" test "$(sha256sum package-lock.json | cut -d' ' -f1)" = "$EXPECTED_LOCK_SHA256" remote_head="$(git ls-remote origin "refs/heads/${SOURCE_BRANCH}" | cut -f1)" test "$remote_head" = "$EXPECTED_HEAD" From ad558ace02522f15c27705fff97c87c6661f0695 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:24:21 +0900 Subject: [PATCH 27/37] ci(pr783): fetch complete lineage for verified lock publication --- .github/workflows/import-pr783-generated-lock.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/import-pr783-generated-lock.yml b/.github/workflows/import-pr783-generated-lock.yml index f29616e9..762bb7cd 100644 --- a/.github/workflows/import-pr783-generated-lock.yml +++ b/.github/workflows/import-pr783-generated-lock.yml @@ -20,7 +20,7 @@ env: GIT_CONFIG_KEY_0: init.defaultBranch GIT_CONFIG_VALUE_0: develop EXPECTED_SOURCE_HEAD: 6b753e6b0bacbe250885c20a53728966b81e0009 - EXPECTED_TRIGGER_PARENT: b773653f76fae5cfbda4a24548bffd4fedfe06ef + EXPECTED_TRIGGER_PARENT: 01cb39e9c9f7513fc3410f86b2f1e1332a969da4 EXPECTED_NPM_VERSION: "10.9.8" ARTIFACT_ID: "8989562185" ARTIFACT_RUN_ID: "31161313485" @@ -47,7 +47,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.sha }} - fetch-depth: 2 + fetch-depth: 0 persist-credentials: false - name: Verify bounded trigger lineage From 83865dce76bf158ae444a23a85f7036ccfc9663b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:25:15 +0000 Subject: [PATCH 28/37] fix(security): import verified npm 10.9.8 lock --- .../workflows/import-pr783-generated-lock.yml | 128 ------------------ package-lock.json | 35 ++--- 2 files changed, 18 insertions(+), 145 deletions(-) delete mode 100644 .github/workflows/import-pr783-generated-lock.yml diff --git a/.github/workflows/import-pr783-generated-lock.yml b/.github/workflows/import-pr783-generated-lock.yml deleted file mode 100644 index 762bb7cd..00000000 --- a/.github/workflows/import-pr783-generated-lock.yml +++ /dev/null @@ -1,128 +0,0 @@ -name: Import PR 783 generated npm lock - -on: - push: - branches: - - fix/high-security-dependency-baseline - paths: - - .github/workflows/import-pr783-generated-lock.yml - -permissions: - contents: read - actions: read - -concurrency: - group: import-pr783-generated-lock - cancel-in-progress: false - -env: - GIT_CONFIG_COUNT: "1" - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: develop - EXPECTED_SOURCE_HEAD: 6b753e6b0bacbe250885c20a53728966b81e0009 - EXPECTED_TRIGGER_PARENT: 01cb39e9c9f7513fc3410f86b2f1e1332a969da4 - EXPECTED_NPM_VERSION: "10.9.8" - ARTIFACT_ID: "8989562185" - ARTIFACT_RUN_ID: "31161313485" - ARTIFACT_NAME: npm-lock-reproduction-e2c0c2dfee35c19d1e9156ad0a7d9324fdefd1fb - EXPECTED_LOCK_SHA256: 31dd2661eca864e3da46f86629a2535dc181d01449bd3a50fa3cdbd6c58e7971 - -jobs: - import-and-verify: - if: >- - github.repository == 'ContextualWisdomLab/bandscope' && - github.ref == 'refs/heads/fix/high-security-dependency-baseline' - permissions: - contents: write - actions: read - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - name: Harden runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 - with: - egress-policy: audit - - - name: Checkout exact trigger without persisted credentials - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.sha }} - fetch-depth: 0 - persist-credentials: false - - - name: Verify bounded trigger lineage - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - test "$(git rev-parse HEAD)" = "$GITHUB_SHA" - test "$(git rev-parse HEAD^)" = "$EXPECTED_TRIGGER_PARENT" - mapfile -t trigger_delta < <(git diff --name-only "$EXPECTED_TRIGGER_PARENT" HEAD) - test "${#trigger_delta[@]}" -eq 1 - test "${trigger_delta[0]}" = ".github/workflows/import-pr783-generated-lock.yml" - - - name: Set up exact Node and npm generator - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: "22.22.3" - cache: npm - - - name: Download exact head-bound lock artifact - env: - GH_TOKEN: ${{ github.token }} - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - test "$(npm --version)" = "$EXPECTED_NPM_VERSION" - mkdir -p "${RUNNER_TEMP}/pr783-lock" - curl --fail --silent --show-error --location \ - --retry 0 \ - --header "Accept: application/vnd.github+json" \ - --header "Authorization: Bearer ${GH_TOKEN}" \ - --header "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}/zip" \ - --output "${RUNNER_TEMP}/pr783-lock/artifact.zip" - mapfile -t archive_entries < <(zipinfo -1 "${RUNNER_TEMP}/pr783-lock/artifact.zip") - test "${#archive_entries[@]}" -eq 1 - test "${archive_entries[0]}" = "package-lock.json" - unzip -p "${RUNNER_TEMP}/pr783-lock/artifact.zip" package-lock.json \ - > "${RUNNER_TEMP}/pr783-lock/package-lock.json" - test "$(sha256sum "${RUNNER_TEMP}/pr783-lock/package-lock.json" | cut -d' ' -f1)" = "$EXPECTED_LOCK_SHA256" - mv "${RUNNER_TEMP}/pr783-lock/package-lock.json" package-lock.json - - - name: Verify exact generated lock and security baseline - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - test "$(sha256sum package-lock.json | cut -d' ' -f1)" = "$EXPECTED_LOCK_SHA256" - npm install --package-lock-only --ignore-scripts --no-audit --no-fund - test "$(sha256sum package-lock.json | cut -d' ' -f1)" = "$EXPECTED_LOCK_SHA256" - npm ci --ignore-scripts --no-audit --no-fund - npm audit --workspaces --audit-level=high - npm run typecheck --workspace @bandscope/desktop - npm run lint --workspace @bandscope/desktop - npm exec --workspace @bandscope/desktop vitest run \ - src/features/score/pdfjs.test.ts \ - --coverage=false - git diff --check - - - name: Remove one-shot importer and publish verified lock - env: - EXPECTED_HEAD: ${{ github.sha }} - SOURCE_BRANCH: fix/high-security-dependency-baseline - PUSH_TOKEN: ${{ github.token }} - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - rm .github/workflows/import-pr783-generated-lock.yml - actual_delta="$(git diff --name-only "$EXPECTED_SOURCE_HEAD" -- | sort)" - expected_delta="$(printf '%s\n' .github/workflows/import-pr783-generated-lock.yml package-lock.json | sort)" - test "$actual_delta" = "$expected_delta" - test "$(sha256sum package-lock.json | cut -d' ' -f1)" = "$EXPECTED_LOCK_SHA256" - remote_head="$(git ls-remote origin "refs/heads/${SOURCE_BRANCH}" | cut -f1)" - test "$remote_head" = "$EXPECTED_HEAD" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add package-lock.json .github/workflows/import-pr783-generated-lock.yml - git diff --cached --check - git commit -m "fix(security): import verified npm 10.9.8 lock" - auth_header="$(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" - echo "::add-mask::$auth_header" - git -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ - push --force-with-lease="refs/heads/${SOURCE_BRANCH}:${EXPECTED_HEAD}" \ - origin "HEAD:refs/heads/${SOURCE_BRANCH}" diff --git a/package-lock.json b/package-lock.json index cf1c991c..792ca517 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "@eslint/js": "^10.0.1", "eslint-plugin-jsdoc": "^63.0.13", "react": "^19.2.4", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "undici": "7.29.0" }, "engines": { "node": ">=22.13 <23" @@ -32,7 +33,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.24.0", - "pdfjs-dist": "6.1.200", + "pdfjs-dist": "6.2.108", "react": "^19.2.4", "react-dom": "^19.2.7", "sonner": "^2.0.7", @@ -212,6 +213,18 @@ "url": "https://opencollective.com/vitest" } }, + "apps/desktop/node_modules/pdfjs-dist": { + "version": "6.2.108", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.2.108.tgz", + "integrity": "sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.13.0 || >=24" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^1.0.0" + } + }, "apps/desktop/node_modules/vitest": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", @@ -6367,18 +6380,6 @@ "node": ">= 14.16" } }, - "node_modules/pdfjs-dist": { - "version": "6.1.200", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.1.200.tgz", - "integrity": "sha512-o8MolyzirkkLrcdsae/HEOiIcXWI7DS5zGpvqW8xTC2YUsW30rltFw2bDGvw/fskUdEMrQm2br68jzDS5BH2vw==", - "license": "Apache-2.0", - "engines": { - "node": ">=22.13.0 || >=24" - }, - "optionalDependencies": { - "@napi-rs/canvas": "^1.0.0" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7179,9 +7180,9 @@ } }, "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": { From e6b48ca58e5c481e4b0bef8961338b5a3967e6c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:29:20 +0900 Subject: [PATCH 29/37] test(ci): preserve canonical npm provenance formatting --- .../analysis-engine/tests/test_npm_toolchain_contract.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/analysis-engine/tests/test_npm_toolchain_contract.py b/services/analysis-engine/tests/test_npm_toolchain_contract.py index 30e00cbd..bdfc1e45 100644 --- a/services/analysis-engine/tests/test_npm_toolchain_contract.py +++ b/services/analysis-engine/tests/test_npm_toolchain_contract.py @@ -51,7 +51,10 @@ def test_primary_ci_proves_exact_npm_before_install_and_lock_reproduction() -> N assert "--no-audit" in workflow assert "--no-fund" in workflow assert "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" in workflow - assert "npm-lock-reproduction-${{ github.event.pull_request.head.sha || github.sha }}" in workflow + assert ( + "npm-lock-reproduction-${{ github.event.pull_request.head.sha || github.sha }}" + in workflow + ) assert "if-no-files-found: error" in workflow assert "git diff --exit-code -- package-lock.json" in workflow From 3edf17356cea74b8fceac50fed8fb3b958dfc1ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:48:49 +0900 Subject: [PATCH 30/37] ci(pr783): diagnose Ruff import ordering --- .../workflows/diagnose-pr783-ruff-imports.yml | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/diagnose-pr783-ruff-imports.yml diff --git a/.github/workflows/diagnose-pr783-ruff-imports.yml b/.github/workflows/diagnose-pr783-ruff-imports.yml new file mode 100644 index 00000000..f800fdaa --- /dev/null +++ b/.github/workflows/diagnose-pr783-ruff-imports.yml @@ -0,0 +1,64 @@ +name: Diagnose PR 783 Ruff import ordering + +on: + push: + branches: + - fix/high-security-dependency-baseline + paths: + - .github/workflows/diagnose-pr783-ruff-imports.yml + +permissions: + contents: read + +concurrency: + group: diagnose-pr783-ruff-imports + cancel-in-progress: false + +env: + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: develop + +jobs: + diagnose: + if: >- + github.repository == 'ContextualWisdomLab/bandscope' && + github.ref == 'refs/heads/fix/high-security-dependency-baseline' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout exact trigger + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.0.0 + with: + python-version: "3.12" + + - name: Set up uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + version: "0.8.6" + enable-cache: false + + - name: Print Ruff's exact import repair + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + uv sync --project services/analysis-engine --group dev --frozen + cd services/analysis-engine + uv run ruff check \ + tests/test_high_security_dependency_baseline.py \ + tests/test_npm_toolchain_contract.py \ + --fix-only + git diff -- \ + tests/test_high_security_dependency_baseline.py \ + tests/test_npm_toolchain_contract.py + test -n "$(git diff --name-only -- tests/test_high_security_dependency_baseline.py tests/test_npm_toolchain_contract.py)" From 234521906a99fb786847cfacd0425f9527bd455b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:53:39 +0900 Subject: [PATCH 31/37] style(ci): normalize security test imports --- .../tests/test_high_security_dependency_baseline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/services/analysis-engine/tests/test_high_security_dependency_baseline.py b/services/analysis-engine/tests/test_high_security_dependency_baseline.py index 0a9e81ab..1df9d6ab 100644 --- a/services/analysis-engine/tests/test_high_security_dependency_baseline.py +++ b/services/analysis-engine/tests/test_high_security_dependency_baseline.py @@ -5,7 +5,6 @@ import json from pathlib import Path - _REPOSITORY_ROOT = Path(__file__).resolve().parents[3] _PDFJS_VERSION = "6.2.108" _UNDICI_VERSION = "7.29.0" From dc7e8b4aa4b18e3a20db3eab9b65fbd71503417d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:54:37 +0900 Subject: [PATCH 32/37] style(ci): normalize npm provenance test imports --- services/analysis-engine/tests/test_npm_toolchain_contract.py | 1 - 1 file changed, 1 deletion(-) diff --git a/services/analysis-engine/tests/test_npm_toolchain_contract.py b/services/analysis-engine/tests/test_npm_toolchain_contract.py index bdfc1e45..72a34305 100644 --- a/services/analysis-engine/tests/test_npm_toolchain_contract.py +++ b/services/analysis-engine/tests/test_npm_toolchain_contract.py @@ -5,7 +5,6 @@ import json from pathlib import Path - _REPOSITORY_ROOT = Path(__file__).resolve().parents[3] _EXPECTED_NPM_VERSION = "10.9.8" _EXPECTED_NODE_VERSION = "22.22.3" From c5ee630fd73decab457450d629d11aecb756637a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 19:55:01 +0900 Subject: [PATCH 33/37] chore(ci): remove completed Ruff diagnostic --- .../workflows/diagnose-pr783-ruff-imports.yml | 64 ------------------- 1 file changed, 64 deletions(-) delete mode 100644 .github/workflows/diagnose-pr783-ruff-imports.yml diff --git a/.github/workflows/diagnose-pr783-ruff-imports.yml b/.github/workflows/diagnose-pr783-ruff-imports.yml deleted file mode 100644 index f800fdaa..00000000 --- a/.github/workflows/diagnose-pr783-ruff-imports.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Diagnose PR 783 Ruff import ordering - -on: - push: - branches: - - fix/high-security-dependency-baseline - paths: - - .github/workflows/diagnose-pr783-ruff-imports.yml - -permissions: - contents: read - -concurrency: - group: diagnose-pr783-ruff-imports - cancel-in-progress: false - -env: - GIT_CONFIG_COUNT: "1" - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: develop - -jobs: - diagnose: - if: >- - github.repository == 'ContextualWisdomLab/bandscope' && - github.ref == 'refs/heads/fix/high-security-dependency-baseline' - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - name: Harden runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 - with: - egress-policy: audit - - - name: Checkout exact trigger - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.sha }} - persist-credentials: false - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.0.0 - with: - python-version: "3.12" - - - name: Set up uv - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 - with: - version: "0.8.6" - enable-cache: false - - - name: Print Ruff's exact import repair - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - uv sync --project services/analysis-engine --group dev --frozen - cd services/analysis-engine - uv run ruff check \ - tests/test_high_security_dependency_baseline.py \ - tests/test_npm_toolchain_contract.py \ - --fix-only - git diff -- \ - tests/test_high_security_dependency_baseline.py \ - tests/test_npm_toolchain_contract.py - test -n "$(git diff --name-only -- tests/test_high_security_dependency_baseline.py tests/test_npm_toolchain_contract.py)" From f0c9ad11566b5b57f09eac56289647a4560d4b73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:13:19 +0900 Subject: [PATCH 34/37] ci(pr783): finalize exact Ruff formatting --- .../finalize-pr783-python-format.yml | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 .github/workflows/finalize-pr783-python-format.yml diff --git a/.github/workflows/finalize-pr783-python-format.yml b/.github/workflows/finalize-pr783-python-format.yml new file mode 100644 index 00000000..14f869cb --- /dev/null +++ b/.github/workflows/finalize-pr783-python-format.yml @@ -0,0 +1,130 @@ +name: Finalize PR 783 Python formatting + +on: + push: + branches: + - fix/high-security-dependency-baseline + paths: + - .github/workflows/finalize-pr783-python-format.yml + +permissions: + contents: read + +concurrency: + group: finalize-pr783-python-format + cancel-in-progress: false + +env: + GIT_CONFIG_COUNT: "1" + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: develop + EXPECTED_SOURCE_HEAD: c5ee630fd73decab457450d629d11aecb756637a + +jobs: + format-verify-publish: + if: >- + github.repository == 'ContextualWisdomLab/bandscope' && + github.ref == 'refs/heads/fix/high-security-dependency-baseline' + permissions: + contents: write + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout exact trigger without persisted credentials + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Verify bounded trigger lineage + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + test "$(git rev-parse HEAD^)" = "$EXPECTED_SOURCE_HEAD" + mapfile -t trigger_delta < <(git diff --name-only "$EXPECTED_SOURCE_HEAD" HEAD) + test "${#trigger_delta[@]}" -eq 1 + test "${trigger_delta[0]}" = ".github/workflows/finalize-pr783-python-format.yml" + + - name: Set up Node and npm + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22.22.3" + cache: npm + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.0.0 + with: + python-version: "3.12" + + - name: Set up uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + version: "0.8.6" + enable-cache: false + + - name: Install Rust stable + shell: bash --noprofile --norc -e -o pipefail {0} + run: rustup toolchain install stable --profile minimal + + - name: Install exact dependencies + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(npm --version)" = "10.9.8" + npm ci + uv sync --project services/analysis-engine --group dev --frozen + + - name: Apply Ruff's exact formatter result + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + cd services/analysis-engine + uv run ruff format \ + tests/test_high_security_dependency_baseline.py \ + tests/test_npm_toolchain_contract.py + uv run ruff format --check \ + tests/test_high_security_dependency_baseline.py \ + tests/test_npm_toolchain_contract.py + uv run ruff check \ + tests/test_high_security_dependency_baseline.py \ + tests/test_npm_toolchain_contract.py + cd ../.. + actual_delta="$(git diff --name-only -- | sort)" + expected_delta="$(printf '%s\n' services/analysis-engine/tests/test_high_security_dependency_baseline.py services/analysis-engine/tests/test_npm_toolchain_contract.py | sort)" + test "$actual_delta" = "$expected_delta" + git diff --check + + - name: Run the complete release harness + shell: bash --noprofile --norc -e -o pipefail {0} + run: ./scripts/harness/quickcheck.sh + + - name: Remove one-shot formatter and publish verified source + env: + EXPECTED_HEAD: ${{ github.sha }} + SOURCE_BRANCH: fix/high-security-dependency-baseline + PUSH_TOKEN: ${{ github.token }} + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + rm .github/workflows/finalize-pr783-python-format.yml + actual_delta="$(git diff --name-only "$EXPECTED_SOURCE_HEAD" -- | sort)" + expected_delta="$(printf '%s\n' \ + .github/workflows/finalize-pr783-python-format.yml \ + services/analysis-engine/tests/test_high_security_dependency_baseline.py \ + services/analysis-engine/tests/test_npm_toolchain_contract.py | sort)" + test "$actual_delta" = "$expected_delta" + remote_head="$(git ls-remote origin "refs/heads/${SOURCE_BRANCH}" | cut -f1)" + test "$remote_head" = "$EXPECTED_HEAD" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git diff --cached --check + git commit -m "style(ci): apply canonical Ruff formatting" + auth_header="$(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" + echo "::add-mask::$auth_header" + git -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ + push --force-with-lease="refs/heads/${SOURCE_BRANCH}:${EXPECTED_HEAD}" \ + origin "HEAD:refs/heads/${SOURCE_BRANCH}" From 102a89f91d8e4418a4af489b82bd19d4fe1bd77f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:16:52 +0900 Subject: [PATCH 35/37] chore(ci): remove temporary branch writer --- .../finalize-pr783-python-format.yml | 130 ------------------ 1 file changed, 130 deletions(-) delete mode 100644 .github/workflows/finalize-pr783-python-format.yml diff --git a/.github/workflows/finalize-pr783-python-format.yml b/.github/workflows/finalize-pr783-python-format.yml deleted file mode 100644 index 14f869cb..00000000 --- a/.github/workflows/finalize-pr783-python-format.yml +++ /dev/null @@ -1,130 +0,0 @@ -name: Finalize PR 783 Python formatting - -on: - push: - branches: - - fix/high-security-dependency-baseline - paths: - - .github/workflows/finalize-pr783-python-format.yml - -permissions: - contents: read - -concurrency: - group: finalize-pr783-python-format - cancel-in-progress: false - -env: - GIT_CONFIG_COUNT: "1" - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: develop - EXPECTED_SOURCE_HEAD: c5ee630fd73decab457450d629d11aecb756637a - -jobs: - format-verify-publish: - if: >- - github.repository == 'ContextualWisdomLab/bandscope' && - github.ref == 'refs/heads/fix/high-security-dependency-baseline' - permissions: - contents: write - runs-on: ubuntu-24.04 - timeout-minutes: 45 - steps: - - name: Harden runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 - with: - egress-policy: audit - - - name: Checkout exact trigger without persisted credentials - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.sha }} - fetch-depth: 0 - persist-credentials: false - - - name: Verify bounded trigger lineage - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - test "$(git rev-parse HEAD)" = "$GITHUB_SHA" - test "$(git rev-parse HEAD^)" = "$EXPECTED_SOURCE_HEAD" - mapfile -t trigger_delta < <(git diff --name-only "$EXPECTED_SOURCE_HEAD" HEAD) - test "${#trigger_delta[@]}" -eq 1 - test "${trigger_delta[0]}" = ".github/workflows/finalize-pr783-python-format.yml" - - - name: Set up Node and npm - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: "22.22.3" - cache: npm - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.0.0 - with: - python-version: "3.12" - - - name: Set up uv - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 - with: - version: "0.8.6" - enable-cache: false - - - name: Install Rust stable - shell: bash --noprofile --norc -e -o pipefail {0} - run: rustup toolchain install stable --profile minimal - - - name: Install exact dependencies - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - test "$(npm --version)" = "10.9.8" - npm ci - uv sync --project services/analysis-engine --group dev --frozen - - - name: Apply Ruff's exact formatter result - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - cd services/analysis-engine - uv run ruff format \ - tests/test_high_security_dependency_baseline.py \ - tests/test_npm_toolchain_contract.py - uv run ruff format --check \ - tests/test_high_security_dependency_baseline.py \ - tests/test_npm_toolchain_contract.py - uv run ruff check \ - tests/test_high_security_dependency_baseline.py \ - tests/test_npm_toolchain_contract.py - cd ../.. - actual_delta="$(git diff --name-only -- | sort)" - expected_delta="$(printf '%s\n' services/analysis-engine/tests/test_high_security_dependency_baseline.py services/analysis-engine/tests/test_npm_toolchain_contract.py | sort)" - test "$actual_delta" = "$expected_delta" - git diff --check - - - name: Run the complete release harness - shell: bash --noprofile --norc -e -o pipefail {0} - run: ./scripts/harness/quickcheck.sh - - - name: Remove one-shot formatter and publish verified source - env: - EXPECTED_HEAD: ${{ github.sha }} - SOURCE_BRANCH: fix/high-security-dependency-baseline - PUSH_TOKEN: ${{ github.token }} - shell: bash --noprofile --norc -e -o pipefail {0} - run: | - rm .github/workflows/finalize-pr783-python-format.yml - actual_delta="$(git diff --name-only "$EXPECTED_SOURCE_HEAD" -- | sort)" - expected_delta="$(printf '%s\n' \ - .github/workflows/finalize-pr783-python-format.yml \ - services/analysis-engine/tests/test_high_security_dependency_baseline.py \ - services/analysis-engine/tests/test_npm_toolchain_contract.py | sort)" - test "$actual_delta" = "$expected_delta" - remote_head="$(git ls-remote origin "refs/heads/${SOURCE_BRANCH}" | cut -f1)" - test "$remote_head" = "$EXPECTED_HEAD" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git diff --cached --check - git commit -m "style(ci): apply canonical Ruff formatting" - auth_header="$(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" - echo "::add-mask::$auth_header" - git -c http.extraheader="AUTHORIZATION: basic ${auth_header}" \ - push --force-with-lease="refs/heads/${SOURCE_BRANCH}:${EXPECTED_HEAD}" \ - origin "HEAD:refs/heads/${SOURCE_BRANCH}" From d4887ec31bd6f48475ef5ac7832946649721db74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:29:43 +0900 Subject: [PATCH 36/37] style(test): apply Ruff formatting to security contracts --- .../tests/test_high_security_dependency_baseline.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/services/analysis-engine/tests/test_high_security_dependency_baseline.py b/services/analysis-engine/tests/test_high_security_dependency_baseline.py index 1df9d6ab..7be5df94 100644 --- a/services/analysis-engine/tests/test_high_security_dependency_baseline.py +++ b/services/analysis-engine/tests/test_high_security_dependency_baseline.py @@ -20,9 +20,7 @@ def _read_json(relative_path: str) -> dict[str, object]: """Return one repository JSON document as a mapping.""" - document = json.loads( - (_REPOSITORY_ROOT / relative_path).read_text(encoding="utf-8") - ) + document = json.loads((_REPOSITORY_ROOT / relative_path).read_text(encoding="utf-8")) assert isinstance(document, dict) return document @@ -65,9 +63,7 @@ def test_lock_records_match_exact_registry_artifacts_and_preserve_peer_metadata( undici = packages["node_modules/undici"] assert isinstance(undici, dict) assert undici["version"] == _UNDICI_VERSION - assert undici["resolved"] == ( - "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz" - ) + assert undici["resolved"] == "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz" assert undici["integrity"] == _UNDICI_INTEGRITY esbuild_locations = { From 459abdd9b30ee32adb60ecb66f0efae8ac25219b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:30:19 +0900 Subject: [PATCH 37/37] style(test): finish Ruff formatting for npm provenance --- .../tests/test_npm_toolchain_contract.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/services/analysis-engine/tests/test_npm_toolchain_contract.py b/services/analysis-engine/tests/test_npm_toolchain_contract.py index 72a34305..5e9a65db 100644 --- a/services/analysis-engine/tests/test_npm_toolchain_contract.py +++ b/services/analysis-engine/tests/test_npm_toolchain_contract.py @@ -12,9 +12,7 @@ def _root_manifest() -> dict[str, object]: """Return the checked-in root package manifest as a JSON object.""" - manifest = json.loads( - (_REPOSITORY_ROOT / "package.json").read_text(encoding="utf-8") - ) + manifest = json.loads((_REPOSITORY_ROOT / "package.json").read_text(encoding="utf-8")) assert isinstance(manifest, dict) return manifest @@ -36,9 +34,7 @@ def test_root_manifest_pins_the_lockfile_generator_and_fails_on_drift() -> None: def test_primary_ci_proves_exact_npm_before_install_and_lock_reproduction() -> None: """Keep the clean installer and lock reproduction on one explicit toolchain.""" - workflow = (_REPOSITORY_ROOT / ".github" / "workflows" / "ci.yml").read_text( - encoding="utf-8" - ) + workflow = (_REPOSITORY_ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") assert f'node-version: "{_EXPECTED_NODE_VERSION}"' in workflow assert f'EXPECTED_NPM_VERSION: "{_EXPECTED_NPM_VERSION}"' in workflow @@ -60,9 +56,7 @@ def test_primary_ci_proves_exact_npm_before_install_and_lock_reproduction() -> N def test_root_lock_uses_the_supported_location_keyed_format() -> None: """Require the npm-v9-and-newer lock format used by the pinned generator.""" - lock_document = json.loads( - (_REPOSITORY_ROOT / "package-lock.json").read_text(encoding="utf-8") - ) + lock_document = json.loads((_REPOSITORY_ROOT / "package-lock.json").read_text(encoding="utf-8")) assert lock_document["lockfileVersion"] == 3 assert isinstance(lock_document["packages"], dict)