Promote dev to main: v0.3.1 - #205
Merged
Merged
Conversation
…147) * backend: bump fastapi to 0.141.1 and uvicorn to 0.52.0 Applies the runtime bumps proposed by the post-v0.3.0 Dependabot group PR (#144) and regenerates the hash-pinned lockfile, which Dependabot does not do — its branch edits pyproject.toml only and so fails CI's drift gate. FastAPI 0.141.x adds an app.frontend() dev-server convenience that Scrye does not use; no routing, dependency-injection or response-model behaviour changes. uvicorn 0.52.0 adds an opt-in experimental --http zttp parser that upstream marks not-for-production; docker/entrypoint.sh passes no --http flag, so the default parser selection is unchanged. starlette holds at 1.3.1 — the pin carried for CVE-2025-62727 / CVE-2026-48818 / CVE-2026-54283 — confirming FastAPI 0.141.1 still accepts it. ruff moves 0.16.0 -> 0.16.1 in the dev extra. Lock regenerated with the pinned uv 0.8.17 command from CONTRIBUTING.md § Backend dependency lock; the diff is those two packages and their hashes. * ci: re-pin codeql-action to the commit behind v4.37.4 Dependabot (#146) proposed moving github/codeql-action/init and .../analyze from ea14db8 to f205ea1 while leaving the trailing comment at v4.37.4. That is not a mislabelled bump — resolving the tag upstream shows both SHAs are the same release: ea14db8afdef5d462e69d78c4ca45002d4522418 refs/tags/v4.37.4 f205ea1c3313d32999d8d6a48b4f6530d4437b38 refs/tags/v4.37.4^{} codeql-action publishes annotated tags, so the old pin named the tag OBJECT and the new one names the commit it dereferences to. The advanced-setup migration pinned the tag object; a SHA-pin is meant to name an immutable commit, which is what every other action pinned here already does — the rest publish lightweight tags, where refs/tags/vX is the commit, which is why this is the only ref affected. No functional change: same release, same CodeQL bundle (2.26.2), same security-extended suite. The old pin worked — Actions dereferences a tag object — so this is pin correctness, not a fix for a broken workflow. * frontend: apply the routine bumps from the post-v0.3.0 dependency group Takes 8 of the 20 updates Dependabot proposed in #145 and narrows a 9th. The Mantine/React ignores held — the PR proposed no locked major, and @mantine/* moved 7.15.2 -> 7.17.8, a minor inside v7. Applied: @mantine/core/form/hooks 7.17.8, @tabler/icons-react 3.46.0, @testing-library/react 16.3.2, @testing-library/jest-dom 7.0.0, postcss-preset-mantine 1.18.0, globals 17.8.0, prettier 3.9.6. The two majors were checked rather than waved through. jest-dom 7 requires @testing-library/dom as a peer (already a direct devDependency at 10.4.1) and Node >= 22 (CI is on 24); it removes no matchers. globals 17 feeds eslint.config.js's globals.browser, where a shrunken set would leave lint passing while losing coverage — the set was inspected: 1191 entries, with window/document/fetch present. @types/node is narrowed from the proposed 26.1.2 to 24.13.3. Its major tracks Node's, and the SPA builds on Node 24; tsconfig.node.json sets types: [node], so types ahead of the pinned runtime describe APIs the build does not have and feed them into the type-aware ESLint gate — the same failure mode that put @types/react* in the ignore list. Prettier 3.9 collapses short union types onto one line instead of the leading-| form, which is the whitespace-only change to three source files. Held back: typescript 7, eslint 10, @eslint/js 10, typescript-eslint 8.65, vite 8, @vitejs/plugin-react 6, vitest 4, jsdom 30, eslint-plugin-react-hooks 7 and eslint-plugin-react-refresh 0.5 — the deferred #86 toolchain sweep, which lands on the type-aware ESLint gate and is its own scoped work. * ci: decline @types/node majors against the pinned Node 24 @types/node's major tracks Node's, and this repo builds and runs on Node 24 (the Dockerfile builder stage and ci.yml's node-version), with Node majors already declined for the docker ecosystem on a support-lifecycle argument. tsconfig.node.json sets types: [node], so a @types/node ahead of the pinned runtime feeds APIs the build does not have into the type-aware ESLint gate — the same reason @types/react* are majors-locked. Without this, #145's @types/node 26 returns inside the grouped PR every week. The comment also records what is deliberately NOT ignored: the frontend toolchain majors are wanted work tracked in docs/ROADMAP.md and should keep being surfaced. * docs: log the post-v0.3.0 Dependabot triage Adds the dated ARCHIVE.md §14 entry covering all three grouped PRs (#144, #145, #146) — the base-branch check, the changelog findings, the annotated- tag SHA-pin discovery, the applied/narrowed/held split on the frontend, and the verification evidence. Refreshes the ROADMAP #86 sweep list to the versions #145 surfaced and adds the four members that were not on the original list. Records the bumps under CHANGELOG's Unreleased § Changed.
…#156) CVE-2026-69247 (HIGH) is a Bleichenbacher-style oracle in the PKCS7 decryption helpers: pkcs7_decrypt_der and its variants exposed distinguishable errors and timing while unwrapping an encrypted key. Upstream substitutes a random key on failure, per RFC 3218. Verified against pyca/cryptography's CHANGELOG.rst rather than the scanner's FIXED IN column, per CLAUDE.md § Interpreter CVEs: the fix is recorded under 50.0.0 (2026-07-31), and 49.0.0 (2026-06-12) predates it. Scrye never reached the vulnerable path — it uses four symbols from the library, all in backend/app/core/crypto.py (AESGCM, HKDF, hashes, InvalidTag), and never calls PKCS7. The bump is taken because the finding is fixable, which is what the dogfood gate keys on; unreachability is a reason not to panic, not a reason to waive. 50.0.0 is a major whose breaking changes miss that surface: FFDH deprecated, X.509 verification APIs stabilised, SCT/X.509 validation tightened. The ChaCha20 counter change landed in 49.0.0 and was already absorbed. All four APIs were exercised against 50.0.0 first, including an AES-GCM round trip under a row-bound AAD and the InvalidTag raise on AAD mismatch that secret_store.py's row_aad() binding depends on. requirements.lock regenerated with the pinned uv 0.8.17 command from CONTRIBUTING.md § Backend dependency lock; the diff is that one package and its hashes, with no transitive churn. Surfaced by the dogfood self-scan going red on an unrelated docs-only PR (#155) — see docs/ARCHIVE.md § Deviations for why that matters now that the gate is a required check.
The post-v0.3.0 Dependabot triage entry recorded uvicorn 0.52.0's WebSocket header fix as applying to websockets 13.0. Upstream's release notes say 17.0 — the fix prepares for websockets 17.0 encoding headers as ISO-8859-1. 13.0 came from a misread of the changelog while writing the entry. Nothing about the triage decision changes: the fix is unrelated to how Scrye runs uvicorn either way, and the lockfile pins websockets 16.1.
…A + Track B) (#155) * docs(roadmap): replace ROADMAP.md with the two-track revision (Track A + Track B) Replaces docs/ROADMAP.md wholesale with an externally-drafted revision. The previous document becomes Track A — Engineering & hardening, carried across verbatim; Track B — Features is new, covering six phases plus a Deferred list, an out-of-scope policy list, a licensing & bundling policy table, and the guiding principles. Known limitations & accepted trade-offs is unchanged. Track A's only substantive edits are three † prerequisite markers, on the items that gate a Track B feature: content-addressed SBOM identity (Phase 2 fix-watch), cancel a running scan (endpoint-scanning target class), and the generated API client (Phase 4 MCP server). Track A and Known limitations were diffed line by line against the outgoing file before replacing, and every §14 pointer was re-checked against the entry it names rather than against its presence in the old file. One regression was caught: the draft's "Frontend tooling majors from Dependabot #86" paragraph predated 004d2b5 (#147), so taking it as written would have reverted the #145 shopping-list refresh, dropped four toolchain members, reverted jsdom 26 -> 30 to 26 -> 29, and removed the sentence that mirrors dependabot.yml's "Deliberately NOT ignored" comment. The current paragraph is spliced in verbatim. See docs/ARCHIVE.md § Deviations for the full verification record. * docs(archive): link the ROADMAP replacement deviation entry to its PR
…eue audit (#159) uvicorn[standard] 0.52.0 -> 0.52.1 and alembic 1.18.5 -> 1.19.1 in backend/pyproject.toml, with backend/requirements.lock regenerated by the pinned uv 0.8.17 command. Dependabot edits pyproject.toml only and leaves the lock stale, so #157's branch fails CI's lock-drift gate; its content is reapplied here instead. - uvicorn 0.52.1: four WebSocket-only fixes; Scrye serves no WebSocket route, so none of it is reachable — pure currency. - alembic 1.19.1, not the 1.19.0 #157 proposes: 1.19.1 (published hours after #157 opened) fixes a defect in the named-CHECK-constraint autogenerate feature 1.19.0 added. Both confined to migration authoring. Also adds the docs/ARCHIVE.md §14 entry for the full open-queue audit (#149, #150, #151, #153, #154, #157) and a CHANGELOG [Unreleased] entry.
…ker-images group (#151) Routine digest refresh of the pinned debian:bookworm-slim base image for the scanners and runtime stages, proposed by Dependabot's docker ecosystem.
github/codeql-action init/analyze moved to v4.37.6, pinned to the commit the
annotated tag dereferences to (5595ccaf = refs/tags/v4.37.6^{}, verified with
git ls-remote against upstream) — the correct pin form for this repo's
annotated tags, consistent with the #146 correction.
….0 (#160) The build ARGs are invisible to every Dependabot ecosystem (they are not FROM images), so these are moved by hand. Verified at the source: each version is the current upstream release per git ls-remote, no release crossed documents a breaking, deprecation, or CLI change, and syft's JSON schema moves only at patch level (16.1.5 -> 16.1.10), so JSON parsing and persisted SBOMs are unaffected. Moved in lockstep, per the pins' own documentation: ci.yml and rescan.yml scan-image pins (documented as pinned to the bundled versions), with digests resolved from the Docker Hub registry; THIRD_PARTY_LICENSES/ re-verified at the new tags (every LICENSE and Trivy's NOTICE byte-identical upstream, Grype/Syft still ship no NOTICE), so only its version table moves; README's Integrations versions; a CHANGELOG Unreleased entry. test_scanner_symlink_containment.py re-run against the real downloaded syft 1.50.0 and grype 0.116.1 binaries on CPython 3.14.6 (7 passed), and repeated by CI's image job against the binaries the image ships.
…decision, and the governance re-verification (#161) docs/ARCHIVE.md gains the dated §14 entry for the 2026-08-09 round: the merge/close outcome for every queue member, the #158 -> #159 branch replacement and its mechanics corrections, the DECLINED profile-display-name decision, the verified-unchanged alembic timing claim, the re-verified ignore-list-on-main finding with its options and costs, the live governance/ruleset re-verification, the sandbox-interpreter resolution, and the environment's footer-append behaviour on GitHub API writes. CLAUDE.md § Git & PR conventions and docs/ROADMAP.md now record the display name as declined-not-deferred so future sessions do not re-raise it. The ROADMAP's CodeQL item is reduced to alert disposition (both settings edits verified done via live ruleset reads), the 2026-08-02 governance list is re-dated with what was re-verifiable, and the #86 sweep entry names #153 as its open, deliberately unactioned Dependabot expression.
…ument dependabot ignore-list promotion lag (#162) - CLAUDE.md: remove the instruction to re-check and strip PR-body attribution footers (verified not to work; a PATCH stripping the footer gets it re-appended server-side), replace with a short Attribution section that says never write one and never try to remove one that appears after the fact. - .gitignore: ignore .pr-body.md, a prior-session scratch artifact. - CONTRIBUTING.md: document that .github/dependabot.yml's ignore list is read from main (not target-branch), same root cause as security updates targeting main; confirmed Dependabot security updates are enabled. - docs/ARCHIVE.md: dated entry recording the maintainer's manual Settings verification (Actions secrets, Dependabot toggles, workflow permissions, code scanning, Actions SHA-pin requirement), Secret Protection enabled, the SHA-pinning audit (all uses: lines already pinned to resolved commit SHAs), the attribution-stripping ban, and the declined dependabot.yml main-promotion option. - docs/ROADMAP.md: close the CodeQL alert-disposition item (0 open / 6 closed on both branches) and update the governance-checklist paragraph with the 2026-08-09 manual verification. No code, schema, or workflow-trigger changes.
…nce (#163) Adds docs/upgrades/frontend-toolchain-86.md, turning docs/ROADMAP.md § Track A's "Frontend tooling majors from Dependabot #86" — one PR (#153) carrying eleven majors behind a single red check — into eight independently verifiable steps. Every constraint is cited to a published peerDependencies range or an upstream migration guide; thirteen package tarballs were unpacked to compare shipped config objects directly. - TypeScript 7 is out, as of 2026-08-09: typescript@7.0.2 is the Go-native compiler and no longer exposes the classic compiler API that @typescript-eslint/typescript-estree uses (114 distinct ts.* symbols). Written as a dated ceiling with a one-command re-check, not a permanent blocker. - #153's red check is an unsatisfiable dependency graph, not lint churn: npm ci fails on ERESOLVE and ESLint never runs. Corrected here and on the PR. - typescript-eslint is the only unblocking move (8.19.0 caps TypeScript at <5.8.0; 8.56.0 admits ESLint 10; 8.58.0 admits TypeScript 6), and its recommendedTypeChecked rule set is unchanged across the span. - ESLint 10 peer-forces eslint-plugin-react-hooks 7, whose recommended config adds the React Compiler rules; that adoption is split into its own step. - Vitest 4 needs no Vite major; @vitejs/plugin-react 6 does require Vite 8. - No sweep member declares a react/react-dom/@Mantine peer, so no locked decision is at risk. react-router 8 does, and is called out separately. Resolved at source: the pinned node digest ships Node 24.18.1 (satisfies jsdom 30); TypeScript 6.0's breaking changes, audited option by option against both tsconfigs; jsdom 27/28/29's changelog; and GHSA-qwww-vcr4-c8h2 confirmed as an upstream re-cut rather than a registry quirk. Scoping only: no dependency version, lockfile, or config file changed. docs/ROADMAP.md deliberately left unedited. See docs/ARCHIVE.md § Deviations for the dated entry.
…edit to its step (#164) Follow-up to #163 on the same document. Sequence membership is unchanged; only estimates, ordering rationale, and the placement of one edit moved. The jsdom step is re-costed against the real test code now that the 27/28/29 changelogs are readable. Two channels are live: the 27.0.0 selector-engine swap (nwsapi -> @asamuzakjp/dom-selector) sits under 116 Testing Library query call sites, and the re-derived UA stylesheet reaches the suite through exactly one path, verified in the installed @testing-library/dom 10.4.1 — isSubtreeInaccessible() reads getComputedStyle(el).display, defaultHidden is false, and the repo never calls configure(), so all 34 getByRole sites run that filter. Four channels are provably inert, including the two that sound worst: the CSSOM rewrite has no author CSS to act on (Vitest defaults to css: false), and click() -> PointerEvent is unreachable since there is no .click() anywhere in src/. Net: effort widens S-M to S-L, risk holds at medium, because detection is complete — 79 assertions in ~15s, with no failure mode that survives a green run. jsdom and Vite 8 swap, making jsdom step 6 and Vite step 7: steps 4-7 have no mutual dependency, so their order is a judgement about verification cost, and this spends the cheap total oracle before the expensive partial one. The original order is recorded as not wrong. Adds an explicit risk ranking to section 8. jsdom is riskier than typescript-eslint and already was in the first draft, but typescript-eslint was never the top of the ranking — step 3 and the Vite step outrank both, and jsdom now sits third. Every other estimate was re-checked and confirmed unchanged. The "types": [] recommendation now appears as a checklist item under step 4 rather than only as prose in the section 7.2 audit. Scoping only: no dependency version, lockfile, or config file changed. docs/ROADMAP.md deliberately left unedited. See docs/ARCHIVE.md for the dated entry.
…fy it (#165) The jsdom step's medium rating rested on "detection is complete — no failure mode that survives a green run". That was asserted rather than argued, and it is too strong: a green suite catches a query that finds nothing or too much, but not one that resolves to a different element while downstream assertions still pass. Section 8 now carries a four-part audit against the suite instead. - getBy* throws on zero matches and on more than one, so silent one-to-one drift needs the engine to stop matching A and start matching exactly one B in the same pass; any widening raises "found multiple elements" instead. - 62 of the 116 query sites are discriminated by accessible name or label text computed in JS, so B would need an identical accessible name to A — which is the >1-match throw condition. - 85 of the 116 are assertion subjects, categorised rather than estimated. - The suite's only set-valued query feeds a helper asserted with toEqual on the full ordered array, which also pins its only raw selector use. The residual it cannot argue away — roughly 17 interaction targets — becomes a checklist item on step 6: a resolved-element snapshot diffed across the bump, turning a judgement into a two-run measurement. Two corrections fell out of the same check: "79 assertions" was the test count, not the assertion count (the suite runs 151 expect calls), and "every query bottoms out in querySelectorAll" overstated the selector engine's reach, since getByText and getByLabelText use '*'-class candidate selectors. Rating unchanged: jsdom stays medium and stays third. No estimate, ordering, or sequence membership changed. Scoping only: no dependency version, lockfile, or config file changed. docs/ROADMAP.md deliberately left unedited. See docs/ARCHIVE.md for the dated entry.
…and GHSA-2v37-7h3g-55p8 (#168) Two HIGH advisories in the frontend build/dev toolchain, both transitive and both fixed inside the ranges their requiring packages already declare, so this is a package-lock.json change only — package.json is untouched. - js-yaml 4.3.0 -> 4.3.1 (GHSA-5p4m-2wfm-xmqj, CVSS 7.5, CWE-407): quadratic CPU consumption resolving a !!omap. Reached via eslint 9.39.4 -> @eslint/eslintrc 3.3.5, which declares js-yaml ^4.1.1. - nanoid 3.3.16 -> 3.3.18 (GHSA-2v37-7h3g-55p8, CVSS 5.9, CWE-835): a custom generator loops indefinitely when size is zero. Reached via postcss 8.5.25, which declares nanoid ^3.3.16. The advisory's fix is 3.3.17; npm update resolves to 3.3.18, the highest release in range, which extends the same zero-size guard to the async native entry point. Both fixes were verified against the published tarballs rather than taken from the advisory metadata. Neither package ships in the image: both are dev-only, neither appears in the built bundle, and the runtime stage copies frontend/dist with no node_modules. Refreshed with the targeted `npm update <pkg>` documented in CONTRIBUTING.md. The lockfile diff is exactly those two entries, six lines each way, with no transitive requirement moved. npm audit afterwards reports 0 at every severity. ESLint clean, Prettier clean, 79 tests across 21 files passing, and the build unchanged at 7,035 modules -> 645.18 kB JS / 201.38 kB CSS. Deliberately separate from the #86 frontend toolchain sweep: this is none of its eight steps, takes no major, and leaves #153 untouched. See docs/ARCHIVE.md § Deviations for the full record.
Step 1 of the eight-step frontend toolchain sweep in docs/upgrades/frontend-toolchain-86.md, taken alone because it is the only unblocking move in that sequence. The pinned 8.19.0 capped typescript at <5.8.0 and eslint at ^9; 8.66.0 raises those to <6.1.0 and ^10.0.0, which is what steps 2 (ESLint 10) and 4 (TypeScript 6.0.3) are gated behind. Only typescript-eslint moves. No ESLint or TypeScript config was edited, typescript stays at 5.7.2 and eslint at 9.39.4, and no later sweep step is touched. Two of the scoping document's predictions did not hold: - An existing rule's implementation got stricter. The rule set is unchanged as predicted (recommended-type-checked.js is identical across the span, 50 entries either side), but no-unnecessary-type-assertion now catches what it previously missed and reported two redundant `'' as string` assertions in NewScanPage.tsx. Removed by hand at those two sites only; tsc -b confirms the inferred useForm type is unchanged and the emitted bundle keeps the same content hashes. "No new rules" does not mean "no new findings." - The effective rule set did change. The document's byte-for-byte claim diffed one of the three files recommendedTypeChecked composes; the layer it did not diff gains `no-with: 'off'`, dropping that rule from error to off in the resolved config. Accepted rather than restored: `with` is a hard compile error under this repo's tsconfigs, verified at the compiler (TS1101 plus TS2410) rather than argued from the strict flag. Lockfile movement is confined to the typescript-eslint subtree, 374 -> 362 packages, with each of the 17 removals checked to have no surviving requirer. Written with npm 11.19.0 to match CI's Node 24. Suites unchanged against a re-measured pre-bump baseline: lint clean, Prettier clean, 79 tests across 21 files, 7035 modules to 645.18 kB JS / 201.38 kB CSS, npm audit 0 at every severity. See docs/ARCHIVE.md section 14 for the full record.
§0.3 claimed the shipped recommendedTypeChecked config was byte-for-byte identical across the typescript-eslint bump. That was only checked for one of its three composite layers (base + eslint-recommended + recommended-type-checked); the undiffed eslint-recommended-raw.js layer went 22 to 23 entries, adding no-with: 'off', which does change the resolved config this repo lints with. Rewrites §0.3 to state what was verified vs. not, reworks the headline so it cannot be read as predicting zero findings, adds a method note binding on every remaining step (re-verify against the installed tree, every composite layer, via eslint --print-config before/after), updates Step 1's and Step 2's rows accordingly, and records the correction in docs/ARCHIVE.md §14. Sequence membership and ordering are unchanged. No dependency, lockfile, or config file touched; no sweep step executed. Also closes #170, Dependabot's regenerated frontend-dependencies group, which still proposes typescript 7.0.2 against typescript-eslint's <6.1.0 peer cap (verified against the PR's actual diff) -- the same unsatisfiable graph #153 carried. docs/upgrades/frontend-toolchain-86.md remains the tracking surface.
…les inert (#174) Step 2 of the frontend toolchain sweep in docs/upgrades/frontend-toolchain-86.md: eslint 9.39.4 -> 10.8.1 @eslint/js 9.39.4 -> 10.0.1 eslint-plugin-react-hooks 5.1.0 -> 7.1.1 (peer-forced) eslint-plugin-react-refresh 0.4.16 -> 0.5.3 Unblocked by step 1: typescript-eslint@8.66.0 is the first release peering eslint ^10.0.0. Lint-only — no source file changed, the suites are unchanged, and the emitted bundle carries the same content hashes as before. eslint-plugin-react-hooks 7.x folds the React Compiler rule set into configs.recommended, taking it from 2 rules to 16 (13 error, 3 warn). Since eslint.config.js spread that object, the spread is replaced by the two rules 5.1.0 contributed, written out. Adopting the compiler set is step 3 and is a separate decision; eslint --print-config confirms exactly those two react-hooks rules resolve, at their original severities, for all three file classes. The resolved rule set moves 118 -> 121 across app .tsx, library .ts and the test override alike: @eslint/js 10's recommended adds no-unassigned-vars, no-useless-assignment and preserve-caught-error at error (61 -> 64 entries, nothing removed or re-severitied), and no-shadow-restricted-names now reports globalThis by default. Lint is clean regardless. ESLint 10 drops @eslint/eslintrc, so eslintrc and js-yaml leave the tree entirely. None of v10's removals apply here: no .eslintrc*, no eslint-env comments, no custom rules or SourceCode API use, and the new engine floor is satisfied by CI's Node 24 and the pinned node:24 digest. See docs/ARCHIVE.md § Deviations for which of the scoping document's predictions held and which did not.
…ect back (#177) Step 3 of the frontend toolchain sweep in docs/upgrades/frontend-toolchain-86.md. No dependency version moves: package.json and package-lock.json are byte-identical. Step 2's holding edit is replaced by the ...reactHooks.configs.recommended.rules spread it was standing in for, taking the resolved config 121 -> 135 rules across all three file classes. Twelve of the fourteen added rules report nothing against the current tree, including immutability, purity and preserve-manual-memoization. The cost is two rules and 24 findings. react-hooks/refs (6 findings) is fixed by hand: all six are one idiom in ScansPage, a useRef whose .current was read during render to seed six useState initializers. A lazy useState initializer runs viewFromParams once on first render exactly as the ref did, without the render-time ref read. react-hooks/set-state-in-effect (18 findings) is left off, with the reason in the config and the work tracked in #176. Only 6 of the 18 are the synchronous setState the rule's rationale describes, and two of those are deliberate effects that each closed a real bug (L17/P2-2 and P3-2), so #176 names them so they are not "fixed" blind. The other 12 are the fetch-on-mount idiom where every setState runs after an await; a probe showed the report tracks what the compiler can see rather than a behavioural difference, so they are reported rather than worked around and are deliberately outside #176's scope. See docs/ARCHIVE.md § Deviations for changes made in this step.
The reset effect in ScanDetailPage has guarded against two scans' state mixing since 2026-07-13, but nothing tested it. The three existing ScanDetailPage suites — findingsTable, latestwins, poller — never navigate between two :scanId values, so the effect could have been deleted with the whole suite staying green. The gap surfaced while auditing #176, which lists that effect as one of six sites a future change may touch and warns against "fixing" it blind; that warning was leaning on a test that did not exist. Added as its own file rather than folded into an existing suite: findingsTable never mounts the page, and latestwins and poller are each scoped to one concern with deliberately minimal fixtures, while this needs two scans differing in id, target, tags, findings and artifacts. One concern per file, named for it, is the existing convention. The test renders /scans/1, edits the tag draft away from the server value, navigates to /scans/2 with the second fetch held open, and asserts none of scan 1's header, findings, artifacts or tag draft survives — then that the stale draft does not reappear once scan 2 lands. Verified to catch the regression rather than merely to pass: with the reset effect temporarily deleted the test fails on the first in-flight assertion, with scan 1's target still rendered. The effect was then restored and the file confirmed byte-identical to dev. No production code changed. The effect was not refactored toward the key-prop remount #176 names, and none of the other five findings it tracks was touched. See docs/ARCHIVE.md § Deviations for changes made in this step.
Step 4 of the frontend toolchain sweep in docs/upgrades/frontend-toolchain-86.md. Only `typescript` moves: the lockfile diff is one entry, 346 packages either side, nothing added, removed, or bumped transitively. 6.0.3 is the ceiling, not the latest. Re-checked at the registry before editing anything: typescript-eslint 8.66.0 (latest) and its 8.66.1-alpha.10 canary both peer `typescript: ">=4.8.4 <6.1.0"`, as do @typescript-eslint/parser and /typescript-estree, so no published typescript-eslint accepts the Go-native TypeScript 7. 6.0.3 is also still the head of the 6.x line — 6.0.2 and 6.0.3 are the only stable releases inside `<6.1.0`. tsconfig.app.json pins `"types": []` rather than inheriting TypeScript 6.0's new default. Verified a no-op for src/ by audit (no process/Buffer/__dirname, no NodeJS. namespace, all 22 test files import their globals from 'vitest', timers go through window.setTimeout) and then empirically, by applying the edit under 5.7.2 first — where the old enumerate-everything default was still live — and confirming `tsc -b --force` and lint stayed clean. tsconfig.node.json already set `"types": ["node"]` and is unchanged. The `this`-less function inference change surfaced with no error and no deprecation diagnostic, and was found by diffing emitted declarations across both compilers: one line in 79 .d.ts files, where React.memo's overload for FindingsTable resolves to MemoExoticComponent instead of NamedExoticComponent. A type probe confirms both compilers accept the exact prop object and reject an extra and a missing prop identically, and the repo emits no declarations, so nothing consumes the printed form. Reported, not worked around. `eslint --print-config` is byte-identical before and after on an app .tsx, a library .ts and the test override (135 rules each). Suites from a fresh `rm -rf node_modules && npm ci`: lint clean, format:check clean, 80 tests across 22 files, npm audit 0, and the build emits the same content hashes as the baseline.
Step 5 of the frontend toolchain sweep in docs/upgrades/frontend-toolchain-86.md. `vitest` is the only package bumped; jsdom stays at 26.1.0 (step 6) and vite at 6.4.3 (step 7). vitest@4.1.10 peers `vite: "^6.0.0 || ^7.0.0 || ^8.0.0"` as a required peer, re-read at the published manifest, so Vitest 4 needs no Vite major and the 5/6/7 ordering holds. 4.1.10 is still `dist-tags.latest`. vite.config.ts needed no edit, confirmed against the shipped 4.1.10 tarball: `declare module "vite"` still augments UserConfig with `test`, `extends?: string | true` survives, and `projects` is already the current spelling. Nothing else in v4's breaking surface has a consumer here. One type error had to be fixed, in test code. Vitest 4 widened `vi.fn`'s type-parameter constraint to `Procedure | Constructable`, so the alias `ReturnType<typeof vi.fn>` — which instantiates at the constraint, not the default — no longer satisfies a plain call signature, and `tsc -b` failed where such a mock is passed to `.mockImplementation()` on a `History.replaceState` spy. Typed against the real method signature at that one site; no autofix. `vi.restoreAllMocks()` also changed meaning (v4 restores only `vi.spyOn` spies), which reaches OidcLinkCard.test.tsx. Measured on both versions and instrumented in the real suite: the carryover is real and inert, and no test passes for a different reason than it did on 3.2.7. Suites from a fresh `rm -rf node_modules && npm ci`: lint clean, format:check clean, 80 tests across 22 files — identical to the baseline per test name and status — audit 0, and a build whose emitted assets carry the same content hashes as the baseline. See docs/ARCHIVE.md § Deviations for the full record of this step.
…ping ban (#182) Adds a docs/ARCHIVE.md §14 entry documenting that PR #169 reversed the attribution-stripping ban a few hours after it was recorded, and that the reversal itself was never logged per this repo's own deviation rule. Does not relitigate which policy is correct — CLAUDE.md's current strip-and-reverify text stands as-is. Adds one line to CLAUDE.md § Attribution: future attribution-policy changes must include their §14 entry in the same PR that changes the policy, not as a follow-up.
* chore(deps): bump jsdom 26.1.0 -> 30.0.1 (#86 sweep step 6) Step 6 of the eight-step frontend toolchain sweep in docs/upgrades/frontend-toolchain-86.md. jsdom is the only package bumped; vite stays at 6.4.3 (step 7) and no config, source or test file changed. jsdom is a devDependency reached only through Vitest's environment: 'jsdom', so the emitted bundle is unchanged down to its content hashes. jsdom 27.0.0 swapped the CSS selector engine (nwsapi -> @asamuzakjp/dom-selector), and a Testing Library query can resolve to a different element after such a swap while every downstream assertion still passes. Per the document's Step 6 checklist, a throwaway setupFiles shim logged each resolved element across the whole suite on both jsdom versions and the two logs were diffed: 176 query resolutions across 46 tests in 17 files, diff empty. The shim was deleted before this commit. Written as the checklist specifies -- outerHTML.slice(0, 120) -- the shim is not deterministic: Mantine's useId mints fresh ids per run, and one button was caught mid-loading-state. Two same-version runs diffed non-empty before the version was touched. The shim now logs the element's DOM index path plus a normalised outerHTML, and its determinism was verified on both versions. The Node floor rises: jsdom@30.0.1 requires ^22.22.2 || ^24.15.0 || >=26.0.0. CI's node-version "24" resolves to 24.19.0 and the pinned node:24-bookworm-slim digest ships 24.18.1, so ci.yml and the Dockerfile need no change; README.md and CONTRIBUTING.md said "Node 22+", under which Node 22.13 installs and then runs npm test unsupported, and are raised to "Node 22.22.2+ or 24.15+". Lockfile 338 -> 340 packages, every movement attributed to jsdom or its transitive closure. Suites unchanged: lint clean, format:check clean, 80 tests across 22 files (compared per test name and status, not per total), build identical, npm audit 0. See docs/ARCHIVE.md section 14 for the full record. * docs: record the drift-shim specification defect as a standing note The section 8 shim spec in docs/upgrades/frontend-toolchain-86.md -- log element.outerHTML.slice(0, 120) keyed by test name and call index -- is methodologically broken, not just in this step's execution. Any app whose component library mints identifiers per render puts a fresh random id inside the first 120 characters of most elements, so a diff of two logs measures the id generator rather than the DOM. Mantine's useId does this here; React's own useId, Emotion, Radix, Headless UI, Chakra and MUI all do the same thing. outerHTML also captures contents, so an element caught mid-transition differs between runs while being the same element. The failure points the wrong way rather than merely adding noise: a spec-conformant shim yields a large non-empty diff on a bump that changed nothing, which is exactly the signal the check exists to raise, and the likely response is to discredit the measurement and fall back on the green suite -- the state the checklist item was written to escape. Records the replacement as a template for future drift checks: log the DOM index path as the element's identity with an id-normalised outerHTML slice as a readable label, and calibrate the noise floor by running the shim twice on the unchanged version before touching the dependency. The sweep document itself is left unedited for maintainer review.
…-> 6.0.5 (#185) Step 7 of the frontend toolchain sweep in docs/upgrades/frontend-toolchain-86.md. The two move in lockstep because @vitejs/plugin-react@6.0.5 declares vite: "^8.0.0" as a required, non-optional peer. These are the only two packages bumped; vite.config.ts, both tsconfigs, eslint.config.js and every source and test file are untouched. Vite 8 replaces Rollup + esbuild with Rolldown + Oxc and moves CSS minification to Lightning CSS, so this is the one step in the sweep that changes what ships. Output was compared against the baseline rather than assumed equivalent: modules 7,035 -> 7,018 (-17) JS 645.14 kB -> 630.29 kB (-2.30%), gzip 193.61 -> 187.36 kB CSS 201.38 kB -> 196.79 kB (-2.28%), gzip 29.30 -> 28.63 kB build 7.30 s -> 1.28 s The module delta is entirely CommonJS-interop scaffolding: a module census under both bundlers shows the 19 Vite-6-only ids are commonjsHelpers.js plus the ?commonjs-* proxy modules @rollup/plugin-commonjs mints for react, react-dom, scheduler, cookie, fast-deep-equal and set-cookie-parser. Rolldown handles CommonJS natively. No application or library module was added or removed. The CSS was diffed declaration by declaration, with selector lists split into individual selectors so rule merging and splitting cancel out: 1,171 selector keys on each side, none gained or lost, and zero declarations added or dropped. All 46 differing declarations are semantics-preserving minifier rewrites (29 vendor prefixes dropped where the unprefixed property is present, `ease` elided as the initial timing function, transparent -> #00000000, shorthand merges, and the six ::-webkit-* selectors split out of one comma list, which is a correctness improvement). Rendering was compared pixel by pixel across six routes in both light and dark mode. A first pass diffed non-empty; re-running the same build twice exposed a ~135-pixel noise floor from in-flight animations, so that pass measured nothing. With animations frozen the noise floor is exactly zero across all twelve views, and against it all twelve are pixel-identical. The one genuine behaviour change is the browser target: Vite 8 defaults to baseline-widely-available (chrome111/edge111/firefox114/safari16.4), up from esbuild's modules default. Every syntax Lightning CSS newly emits is inside that target. No project document states a browser floor. plugin-react 6.0.0 removed all Babel features. This repo passes no babel option and has no babel config anywhere, so nothing moved to @rolldown/plugin-babel and nothing was dropped. React Fast Refresh now runs on Oxc and was smoke-tested on the dev server. Lockfile 339 -> 305 packages, every movement attributed to a requirer. @babel/core and @babel/parser remain, now required only by eslint-plugin-react-hooks. Lint, format:check and audit clean; 80 tests across 22 files, compared per test name. See docs/ARCHIVE.md § Deviations for the full record, including a correction to the scoping document's §3.3.
sweep step 8) (#187) Step 8 — the final step — of the frontend toolchain sweep in docs/upgrades/frontend-toolchain-86.md. Three routine minors/patches with no config change and no coupling; all three re-confirmed as dist-tags.latest before the bump. The lockfile diff is +13/-13: 306 packages before and after, zero added, zero removed, exactly the three targets bumped. postcss@8.5.26 raises its own nanoid requirement to ^3.3.17, which installs nothing — nanoid@3.3.18 has been in the tree since the 2026-08-09 advisory refresh. globals.browser was inspected rather than trusted to a green lint run, per the row's inherited caution: 1191 -> 1196 keys, five added and none removed. eslint --print-config on one file of each class holds at 135 rules with nothing added, removed, or re-severitied. All three emitted assets are byte-identical to the baseline by SHA-256 — the check that matters because postcss runs in the build path via postcss.config.cjs. Suites on both sides from a clean npm ci: lint clean, format:check clean, 80 tests across 22 files (diffed per test name and status, empty), build 630.29 kB JS / 196.79 kB CSS, npm audit 0. Nothing was autofixed; there were no findings. See docs/ARCHIVE.md § Deviations for the full record, including the sweep-completion assessment.
…r 8 (#189) Three Track A edits in docs/ROADMAP.md, each gated on a live check made this session rather than on a prior session's claim: - Strike the "Frontend tooling majors from Dependabot #86" item as Done 2026-08-09, naming all eight sweep PRs (#171, #174, #177, #179, #180, #183, #185, #187) and both deliberately-excluded packages (TypeScript 7 and @types/node 26). All eight squash commits re-confirmed present on dev before writing the bullet. - Strike the GHSA-qwww-vcr4-c8h2 re-cut request: the advisory has already been re-cut upstream to >= 7.12.0, < 7.18.2 plus >= 8.0.0, < 8.3.0, with the 8.x range untouched. Verified against the record in github/advisory-database and independently against the npm registry's advisory endpoint, which returns nothing for 7.18.2. - Reword react-router 7 -> 8 as blocked on a React 19 decision rather than on a tooling bump, and move it out of the tooling-majors grouping into Longer-term / speculative. react-router@8.3.0 peers react and react-dom at >=19.2.7; this repo pins both at 18.3.1. See docs/ARCHIVE.md section 14 for the dated record of what was verified for each.
Takes the one package the #86 frontend toolchain sweep deliberately declined (docs/upgrades/frontend-toolchain-86.md §5), on the maintainer's instruction and on currency grounds — nothing in the toolchain requires it. The only @types/node constraints in the resolved tree are the optional peers of vite@8.2.1 and vitest@4.1.10, both already satisfied by 24.13.3. §5's argument against it was that tsconfig.node.json's `"types": ["node"]` feeds runtime-ahead types into the type-aware lint gate. Step 4 (#179) retired that premise by writing `"types": []` into tsconfig.app.json: `tsc --listFiles` now loads 1,063 files for the app project and zero of them are @types/node, against 82 for the node project. The whole surface of this bump is vite.config.ts, whose only Node API use is `process.env` — and `interface ProcessEnv extends Dict<string> {}` is character-identical across the two versions. Node 24 compatibility measured rather than inferred from the major number, in both directions. Every exported symbol of every node: module was enumerated from both packages with the installed TypeScript 6.0.3 compiler API: 26.2.0 adds two modules and 263 symbols and drops 72 (31 of them value exports). Checked against a real Node 24.19.0 — the version ci.yml's `node-version: "24"` resolves to — 26 of those 31 still exist on Node 24 (24 top-level zlib.Z_* aliases, assert.CallTracker, buffer.SlowBuffer), and 44 of the 65 added value exports do not (node:ffi and node:quic wholesale). Neither list is referenced anywhere in this repository, and neither is reachable from src/. Lockfile: 306 -> 306 packages, +8/-8, zero added, zero removed, two bumped — @types/node and its sole dependency undici-types 7.18.2 -> 8.3.0. Written with npm 11.19.0 in a scratch prefix to match CI's Node 24. eslint --print-config diffed before and after on the three standard file classes plus vite.config.ts, the one file this bump can reach: all four byte-identical at 135 rules. Lint, format:check, tsc -b --force and npm audit clean on both sides; 80 tests across 22 files, diffed per test name rather than by total; all three emitted assets byte-identical by SHA-256. No lint rule was disabled, downgraded or suppressed, and no other package moved — typescript stays at 6.0.3.
…192) @types/node 26.2.0 landed on dev in #190 (5948b73), so the ignore's premise is spent. The comment it carried argued that tsconfig.node.json's `"types": ["node"]` feeds a runtime-ahead @types/node straight into the type-aware ESLint gate — true of the tree it was written against, and no longer true of this one. Step 4 (#179) wrote an explicit empty `types` array into tsconfig.app.json, which narrows the package's reach to a single file: `tsc --listFiles` loads 1,063 files for the app project and zero of them are @types/node, against 82 for the node project. The rule and its comment are removed together rather than the comment being rewritten, since a comment whose premise has been retired reads as a live argument. Deliberately NOT done: no ignore rule was added for typescript. TypeScript 7 is wanted; what blocks it is upstream — typescript-eslint's typescript peer range, whose upper bound has never exceeded <6.1.0 across all 1,510 published versions (re-verified today from the full packument, not from `latest` alone). The regenerating Dependabot PR proposing typescript 7.x is the notification that tells us when upstream ships support, so an ignore rule would suppress the signal while changing nothing about the blocker. Dependabot's own automated reply on the PR closed today recommended exactly that ignore; it is correct on mechanics and wrong as advice here. Like every prior change to this file, it is inert on main until the next dev->main promotion — Dependabot reads its configuration, ignore list included, from the default branch. main's copy has never carried the @types/node stanza (added on dev by #147, never promoted), so this removal deletes a rule that was never live and brings the file closer to the effective configuration rather than further from it. No other key in dependabot.yml changed: the six ecosystems, every target-branch, schedule, groups and commit-message block, and the five remaining npm ignores enforcing locked decision §2 are untouched.
…a current reason (#193) Comment-only change to the npm /frontend entry. No ignore rule, group, schedule, target-branch or any other key is added, removed or modified. The paragraph's instruction — leave the frontend tooling majors unignored so Dependabot keeps surfacing them — is still correct. Its stated reason was not: it called them "the deferred #86 sweep" and said to surface them "until that PR is done". The sweep completed 2026-08-09 across #171, #174, #177, #179, #180, #183, #185 and #187, and docs/ROADMAP.md marks the item Done (verified before writing). So the comment had begun arguing against its own instruction — a reader who checked the roadmap would find the work finished and reasonably conclude the exemption had expired, whose obvious next move is to add the typescript ignore, which is the one thing that must not happen. Rewritten so the instruction rests on two current reasons. The tooling majors stay unignored as ordinary policy: we want to see them, evaluate them and land them, which is exactly what the sweep did (pointing at docs/upgrades/frontend-toolchain-86.md rather than restating it). typescript stays unignored for a sharper reason: TypeScript 7 is wanted, the blocker is entirely upstream, and the regenerating Dependabot PR proposing 7.x IS the notification that tells us when typescript-eslint ships support — an ignore would suppress the signal while changing nothing about the blocker. The re-check command is inlined so the next reader can test it without a session, along with the expected shape of support (a new typescript-eslint MAJOR built against TS 7's ./unstable/* API, not a point-release range widen). Verified by parsing rather than by reading: both versions load with yaml.safe_load to byte-identical canonical JSON (SHA-256 07e71c6c on each side), and every changed raw line matches ^[+-]\s*#.
…ivers kept, pending Grype-DB catch-up (#98, #116) (#195) * test(ci): lift the Group A-1/A-2 interpreter waivers to capture a pre-bump baseline Deliberately temporary, and expected to turn the Grype gate RED. There is no unit test for a base-image digest pin, so the fail-first evidence for the Python 3.14.7 bump has to come from the dogfood scan itself: this commit removes the six `- vulnerability:` entries that Group A-1 (#98) and Group A-2 (#116) waive, leaving the runtime still pinned to the 3.14.6 digest, so CI records exactly which findings those waivers were suppressing and which of them trip `--fail-on high`. Three of the six are HIGH (CVE-2026-15308, CVE-2026-11940, CVE-2026-11972), so the Grype gate fails on this commit by design. The next commit bumps both `FROM` digests to the 3.14.7 image and re-runs the same scan; the two runs are the before/after pair. Group B (CVE-2025-15367, poplib, issue #52) is untouched: its fix is still `main`-only — verified again at the v3.14.7 tag, where Lib/poplib.py is byte-identical to v3.14.6 — so no 3.14.x point release clears it. * fix(docker): bump the runtime base image to Python 3.14.7 (#98, #116) Both `FROM python:3.14-slim-bookworm@…` lines — the backend-builder stage and the runtime stage — move together from sha256:86f975aca15cf04a40b399eebede9aea7c82eae084d1f1a0a6ef6bcaae871a30 (3.14.6) to sha256:23c59390fc717bf09f9336908199a0ae75d9c4264bf296123f94ad772fea3b52 (3.14.7). Nothing else changes in this commit: the Group A-1/A-2 waivers stay lifted from the previous commit, so the dogfood scan re-runs with the digest as the only variable against that baseline. How the digest was established, without trusting a rendered page: - `HEAD /v2/library/python/manifests/3.14-slim-bookworm` on registry-1.docker.io returns docker-content-digest sha256:23c59390…, and the same request for the `3.14.7-slim-bookworm` tag returns the identical digest (`3.14.8-slim-bookworm` is 404 — 3.14.7 is the current 3.14.x). - That index is an OCI image index carrying both linux/amd64 and linux/arm64 children, which publish.yml's multi-arch build needs. - Its per-arch annotations name the build source docker-library/python@228f71e:3.14/slim-bookworm, created 2026-08-05; that Dockerfile declares `ENV PYTHON_VERSION 3.14.7`. The same lookup on the outgoing digest resolves to rev 7914d06 with `ENV PYTHON_VERSION 3.14.6`, confirming what is being replaced. The six fixes were verified in CPython itself, at the v3.14.7 tag versus v3.14.6, per CLAUDE.md § Dependency hygiene — not from Grype's FIXED IN column, which reports 3.15.x for all six and does not record the 3.14 backports. The per-file, per-CVE evidence is in docs/ARCHIVE.md §14 (2026-08-11). * fix(docker): strip pip from the runtime image, closing two vendored-dependency HIGHs The 3.14.7 base-image bump turned the Trivy dogfood gate red with two fixable HIGHs that are not Scrye dependencies: msgpack GHSA-6v7p-g79w-8964 HIGH 1.1.2 -> 1.2.1 setuptools CVE-2025-47273 HIGH 70.3.0 -> 78.1.1 Both versions are exactly pip's vendored pins (`pip/_vendor/vendor.txt`: `msgpack==1.1.2`, `setuptools==70.3.0`). `backend/requirements.lock` pins setuptools 83.0.0 and carries no msgpack at all, so neither is ours to bump — they are whatever the digest-pinned base image's bundled pip vendors. 3.14.6 bundled pip 26.1.2 and 3.14.7 bundles 26.2.1 (`Lib/ensurepip/__init__.py` `_PIP_VERSION`), which is what surfaced them. Rather than excuse them in ci/trivyignore, the runtime image stops shipping pip. Nothing there needs it: the entrypoint runs `alembic upgrade head` then `exec uvicorn`, and no application code imports pip, ensurepip, or pkg_resources. `backend-builder` keeps pip — it installs the hash-pinned lock with `pip --require-hashes` (SC-1) — so this is a runtime-stage change only. Both prefixes are cleaned (/opt/venv, seeded by `python -m venv`, and /usr/local, from the base image's `--with-ensurepip` build), along with `ensurepip` itself, whose entire payload is that same pip wheel. The step asserts `pip` is actually off PATH and that the venv still imports alembic, fastapi, sqlalchemy and uvicorn, so a version glob that stops matching after a future base bump fails the build instead of silently shipping pip again. Guarded by a new static regression test in backend/tests/test_dockerfile_supply_chain.py, verified to fail against the pre-strip Dockerfile (1 failed, 7 passed) and pass after (8 passed). No stage boundary, layer ordering, or build-cache scope changed — this only removes files from the final stage (docs/ARCHIVE.md § Build performance § Invariants). * docs: record the 3.14.7 outcome — fixes verified at the source, waivers kept The measurement the branch existed to produce: after the digest bump, with the Group A-1/A-2 waivers lifted, Grype reported the same six CVEs it reported on 3.14.6, now against `python 3.14.7`, and the gate exited 2 (CI run 31457878086). So no waiver is retired. That is the Grype-DB lag #98 and #116 each predicted in as many words, and the `FIXED IN 3.15.0*` column is the mechanism: Grype's data records these as fixed only in 3.15.x, has no entry for the 3.14 backports, and compares 3.14.7 < 3.15.0b4. What establishes the fix is the source diff, not the scanner. All six were read at the `v3.14.7` tag and diffed against `v3.14.6` — the second verification for each, and the first against a *released* tag rather than the `3.14` branch, which could only ever show a fix was queued. ci/grype.yaml therefore restores all seven waivers unchanged in membership and inverts their stated reason: from "unfixable at the pinned version, waiting on a release" to "fixed in the pinned interpreter, verified at the source, waived because the scanner's data lags." The new trigger is a Grype-DB refresh, at which point both Group A blocks are deleted outright rather than re-dated; the shared 2026-10-25 review date is kept deliberately, since a daily-cadence DB refresh is not a release months away. The observed scan output is quoted in the file so the next reader doesn't have to reconstruct why a waiver outlived its own trigger. Group B (CVE-2025-15367, poplib) is byte-identical to dev — `Lib/poplib.py` is byte-identical between the two tags, so 3.14.7 clears nothing there. Also here: - CLAUDE.md § Locked decisions #2: runtime floor 3.14.6 -> 3.14.7, keeping the incremental-GC rationale and adding the interpreter-CVE one alongside it. The two bind independently — the GC reason floors at 3.14.5, the CVE reason raises to 3.14.7 — so neither replaced the other, and a future reader resolving the CVEs cannot conclude the floor may drop back and reintroduce the GC leak. - CONTRIBUTING.md's prerequisite moves with it. - CHANGELOG [Unreleased] § Security: the bump, why the waivers stay anyway, and the pip strip. - docs/ARCHIVE.md: two dated §14 entries — one for the CVE/scan outcome, one for the locked-decision floor change, deliberately separate so the floor doesn't read as a consequence of a scan whose result was "all six still report" — plus their index lines. The index header count was stale at 157 against 158 entries before this change; it now reads 160 and matches. Both entries state the environment limitation explicitly: the image build, the dogfood scans and the interpreter version come from CI, which pulls and builds the real image (the `INSTALLED 3.14.7` column is the built image reporting itself), because this sandbox's egress policy denies the registry blob hosts and python.org. The backend suite has not run on 3.14.7 anywhere — CI's `Set up Python 3.14` resolves to 3.14.6 — and that is recorded as an open gap, not claimed as done. The minor-version pins in ci.yml and pyproject.toml are untouched, as are the historical 3.14.6 references in released CHANGELOG sections, CLAUDE.md's source-verification rule, and the AboutPanel mock payload.
…and #116 (#196) * docs(ci): make the Group A waiver blocks stand alone after closing #98/#116 Both issues tracked deferrals whose resolution trigger has fired: 3.14.7 ships all six fixes, verified at the source against the released tag in PR #195. What remains behind the waivers is Grype-DB lag, not an open risk acceptance, so tracking moves out of GitHub issues for this pair. ci/grype.yaml: replace the forward-looking "Tracked in issue #NN" pointers in the A-1 and A-2 blocks (and in the block->issue index) with the verified-fixed / waived-pending-DB-refresh statement, cited to PR #195, tracked by this file plus docs/ARCHIVE.md §14. Move the shared review date 2026-10-25 -> 2026-11-01 and label it advisory: nothing in the repo notices when the DB catches up, the waivers just go inert. Historical #98/#116 references are kept. Waiver membership, format, and gate behaviour are unchanged - the ignore: list still parses to the same ten entries. Adds one dated docs/ARCHIVE.md §14 entry recording the decision, and updates the §14 index (160 -> 161). * docs(ci): retarget the dogfood log's block->issue header too The "Waived by ci/grype.yaml (informational)" step hardcodes its own header line, so it printed "A-1 (issue #98) · A-2 (issue #116)" on every run - the same stale forward pointer just outside the file being edited. Bare echo string only: no jq filter, gate threshold, --exclude, --fail-on, step condition, or job structure changed.
…rule (#197) Two separate problems, two separate fixes. The footer on PR bodies and issue comments is a Claude Code harness feature configured through settings, not text a session composes. It was configured in the maintainer's user-scope ~/.claude/settings.json, which has never applied here: each Cloud session is a fresh VM with no home-directory provisioning, so that file does not exist in the environment the work runs in. Add a committed, project-scope .claude/settings.json instead — the repo is cloned into every VM, and project scope outranks user scope. It sets attribution.commit "", attribution.pr "", and attribution.sessionUrl false; the file parses and validates with zero errors against the published settings schema. includeGitInstructions is deliberately not used — it would strip all built-in git/PR guidance from the system prompt and is unnecessary once the attribution keys are set. The strip-and-reverify instruction is removed from CLAUDE.md and CONTRIBUTING.md alike. A strip PATCH runs under a bot identity: on PR #196 the edit history shows the comment created by tyler-rich followed by two "claude (Bot)" edits — the two strip attempts — with the footer re-appended anyway. It is the only write in this workflow that lands under a non-maintainer identity, it has never succeeded, and it leaves a permanent, public attribution leak worse than the footer it targets. The reason is stated inline in the rule so a future session cannot read the prohibition as an oversight to repair. Also: restate the repo-local git identity step as a hard gate (the VM image presets both repo-local and global identity to Claude <noreply@anthropic.com>, and settings.json has no git-identity key, so those two commands are the only thing producing correct authorship); ignore .claude/settings.local.json by specific path, not a blanket .claude/ rule; and exclude .claude/ from the denylist-style .dockerignore so it never enters the build context this project dogfoods a scan against. See docs/ARCHIVE.md § Deviations for the full record.
…skill (#198) The skill is outside every repository, so no commit can reach it and it will lag until the maintainer syncs it by hand. Say plainly in the §14 entry that CLAUDE.md and CONTRIBUTING.md govern where the two disagree on attribution, and that the skill is known-stale on the subject — on the strip instruction and on the declined profile-display-name point alike. A session weighing two sources that disagree and picking the wrong one is the #169 failure mode; writing the precedence down is what stops it being rediscovered.
) Issue #52's source-verification table named v3.14.6 as the pinned runtime, which went stale when the runtime moved to 3.14.7 in PR #195. Re-verified independently at the source (Lib/poplib.py at v3.14.7 vs main): the POP3._putcmd() control-character guard is still absent from v3.14.7 and present only on main. No backport has appeared on gh-143923 since 2026-07-26. The acceptance is unchanged. - ci/grype.yaml: Group B block now cites v3.14.7 and records the 2026-08-11 re-check. - #52: verification table, date, and its cross-references to the now closed #98/#116 updated to point at their current tracking location (ci/grype.yaml + docs/ARCHIVE.md §14) instead of the closed issues. - docs/ARCHIVE.md §14: dated entry recording the re-verification method, result, and cross-reference retargeting. Not changed: the waiver itself, #52's review date/cadence, the Group A-1/A-2 blocks and their review date, or anything scoping a 3.15 move. See docs/ARCHIVE.md §14 (2026-08-11) for the full record.
) - .claude/settings.json: add includeGitInstructions: false. Attribution block is untouched. Motivated by #199, where the attribution keys were confirmed live but the PR-creation path still appended a footer while a direct issue-body edit in the same session came back clean — the append is specific to the built-in PR workflow instructions this setting controls, not attribution.pr. Second attempt at the same problem, not a replacement for the attribution block. - CLAUDE.md: add a standing rule that a PR body describes the change, not the session's own process — no authorship-check narration, footer-status commentary, merge-permission statements, or scope/stop-condition meta-commentary. That content belongs in the session's chat summary instead. See docs/ARCHIVE.md §14 (2026-08-11) for the full record, including the accepted tradeoff (CLAUDE.md becomes the sole source of git conventions for sessions running with this setting) and confirmation that the settings file still validates against the published schema. Not changed: the never-PATCH rule, the attribution block, #199's branch, or CONTRIBUTING.md.
…'s six sites (#201) Refactors findings 1, 2, 3 and 5 from #176 so none of them calls setState synchronously from an effect body. Behaviour is preserved at every site. - LoginPage: the oidc_error message is seeded by a lazy useState initializer; the effect keeps only the history.replaceState that strips the parameter. - OidcLinkCard: same treatment for the oidc_link / oidc_link_error banners, over a shared linkParams() helper. - NewScanPage: the scanner clamp moves out of an effect into the target-type handler, the one place that can invalidate the pairing. Deliberately not a derive-during-render, which would resurrect a displaced scanner choice instead of overwriting it. - ScanDetailPage: findingsLoading is derived from a settled-request key rather than stored, which also removes a flash of "No findings match the current filters" over a request still in flight. The per-scanId reset effect keeps its shape; its one uncompilable line becomes setFindingsSettledKey(null). Each site gains a test verified to fail against the pre-refactor version of its own file. The react-hooks/set-state-in-effect override in eslint.config.js is unchanged: findings 4 and 6 and the rule flip are a follow-up, per #176. See docs/ARCHIVE.md § Deviations for the full account, including why ScanDetailPage's void loadFindings() still reports after the refactor.
…-attempt cap (#202) * docs: delete .claude/settings.json; reinstate footer strip with a one-attempt cap The committed project-scope settings file is removed in full — both the attribution block from #197 and includeGitInstructions from #200. Three sequential settings-layer attempts did not stop the attribution footer: #197 added the attribution keys, #200 added includeGitInstructions after #199 still showed a footer, and #201 ran with both merged and live and still got one on its PR body. The approach is abandoned, not paused. With no settings-level backstop left, the strip instruction returns to CLAUDE.md § Attribution and CONTRIBUTING.md § Pull request process — but capped at one PATCH per body, with a hard stop if the footer comes back. The cap prices #197's evidence rather than disputing it: each PATCH runs under a bot identity and leaves a permanent "claude (Bot)" entry in GitHub's public edit history, and #196 showed a second attempt only doubles that cost without changing the outcome. Commit-authorship verification is unchanged. The .gitignore comment that explained why the settings file was tracked is rewritten, and docs/ARCHIVE.md §14 records the deletion, the cap, and the fact that this is the fourth reversal of this policy. * docs(archive): record the strip's first outcome on #202's own body The §14 entry documented the policy change but not its result, since it was written before the PR was opened. Adds the outcome: the strip held on this PR's own body — footer found on creation, one PATCH, clean confirming re-read — which is the first success in this project's history. Bounds it on both sides rather than claiming more than it shows. The footer appeared on creation with #197's and #200's keys live and merged, the same result #201 got, so the settings demonstrably did not stop the append; whatever they may have contributed is confined to the re-append after the PATCH, and that is now an open and unfalsifiable question since the file is gone. The next PR is the harder test, running with no settings file at all. Also notes that #196's failures were on an issue comment while this success was on a PR body, so it does not refute #196.
…t-state-in-effect at 'warn' (#203) * refactor(frontend): replace #176 findings 4 and 6 with behaviour-preserving equivalents Finding 4 (L17/P2-2): ScanDetailPage's per-:scanId reset effect is replaced by a keyed remount — a ScanDetailRoute wrapper keys the page by the scan id, so React Router's element reuse across /scans/:id navigations can no longer leak the previous scan's state. The scanIdReset regression test now routes through the same wrapper the app uses. Finding 6 (P3-2): ScansPage's compare-selection reconcile effect moves into load(), the only place the visible rows ever change, so the selection is reconciled in the same commit as the rows instead of one commit later. The react-hooks/set-state-in-effect override in eslint.config.js is deliberately untouched pending a maintainer decision on the remaining fetch-on-mount findings. * refactor(frontend): enable react-hooks/set-state-in-effect at 'warn'; strengthen the P3-2 deleted-scan test With #176's six genuine sites all refactored, the override moves from 'off' to 'warn' (maintainer decision): the rule takes effect, its 13 remaining reports — all the fetch-on-mount idiom, which #176 puts out of scope with no honest fix — stay visible in lint output without failing lint or CI, and the config comment records why 'warn' was chosen over per-site disables. The P3-2 deleted-scan regression test gained the two assertions that actually discriminate ("Comparing 1/2 selected", Compare disabled): fail-first verification showed its existing assertions all held vacuously against the unprotected version. It now fails without the reconciliation and passes with it. docs/ARCHIVE.md §14 (2026-08-11, "#176 part 2") carries the full record; docs/upgrades/frontend-toolchain-86.md step 3 now marks its held-back rule enabled. * docs(archive): record #203 as the second consecutive PR-body strip success, the first with no settings file The strip-cap entry predicted the first post-deletion PR would be the harder test of the reinstated rule; #203 was that PR and its strip held. Two-for-two on PR bodies since the reinstatement — #202 with both key sets live, #203 with no settings layer at all — so the settings keys are not needed for a hold. Issue comments remain untested either way.
Runs the CONTRIBUTING.md § Releasing "Before you tag" checklist on dev, ahead of the promotion PR. - Version 0.3.0 -> 0.3.1 in sixteen places across nine files (frontend/package-lock.json written with `npm version`, never by hand). - CHANGELOG [Unreleased] cut to [0.3.1] - 2026-08-11, with a fresh empty [Unreleased] above it and the compare links added and re-pointed. - Adds a ### Fixed section for the #176 work landed in #201/#203, removes the test-only ### Added entry (§14 already carries it in full), and corrects the step-3 set-state-in-effect claims that #203 superseded. - THIRD_PARTY_LICENSES re-verified byte-for-byte against upstream at the pinned Trivy/Grype/Syft tags; backend/requirements.lock confirmed drift-free; the Dependabot queue is empty. Issue #194 is a stale published image rather than a defect: its only gate finding is CVE-2026-69247 (cryptography 49.0.0), already closed on dev at 50.0.0 and verified in upstream's source. See docs/ARCHIVE.md § Deviations for the full record.
…ts (#206) README § Requirements named only "Python 3.14", so it did not carry the patch-level floor that CONTRIBUTING.md § Prerequisites and CLAUDE.md § Locked decisions both set at 3.14.7. Anyone reading README alone for the native-development prerequisites could land on an interpreter below the floor. README now states the floor; CONTRIBUTING keeps the reasoning. The Node entry is unchanged — it already matched.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes
devtomainto cut v0.3.1 — a maintenance release with no new features, no schema change and no configuration change. 42 commits sincev0.3.0.Security
cryptography49.0.0 → 50.0.0, closing CVE-2026-69247 (HIGH) — a Bleichenbacher-style oracle in the PKCS7 decryption helpers, wherepkcs7_decrypt_derand its variants exposed distinguishable errors and timing while unwrapping an encrypted key. This is the finding that failed the dogfood gate against the published:latestimage ([image-rescan] Fixable HIGH/CRITICAL CVEs in ghcr.io/tyler-rich/scrye:latest #194). Scrye uses four symbols from the library and never calls PKCS7, so no deployed instance was exposed; the bump is taken because the finding is fixable.ci/grype.yamlwaives (CVE-2026-15308, CVE-2026-11940, CVE-2026-11972, CVE-2026-12003, CVE-2025-15366, CVE-2026-0864). All six were confirmed present by reading CPython at thev3.14.7tag and diffing againstv3.14.6. The waivers stay regardless: Grype's DB records the fixes only in 3.15.x, so it still reports all six against a 3.14.7 image. The locked runtime floor moves to 3.14.7 to match.pipno longer ships in the runtime image, closing GHSA-6v7p-g79w-8964 (msgpack1.1.2) and CVE-2025-47273 (setuptools70.3.0) — versions vendored inside the base image's pip and therefore not ours to bump. Nothing in the runtime used pip, so it is deleted rather than excused with a scanner exception.js-yaml) and GHSA-2v37-7h3g-55p8 (nanoid). Both are transitive devDependencies and neither ships in the image, which copies only built output.Fixed
Five state-carryover and rendering defects on the scans pages, surfaced while working through the
react-hooks/set-state-in-effectsites #176 enumerates — most importantly the findings list no longer flashing "No findings match the current filters" over a request that has not answered yet, and a delete-confirm dialog left open across a navigation now closing instead of re-arming against the newly-opened scan (which could delete a scan the operator was never asked to confirm).Changed
@types/node26 — with byte-identical build output throughout. The documented native-development floors move to Python 3.14.7+ and Node 22.22.2+ or 24.15+, stated in bothREADME.md§ Requirements andCONTRIBUTING.md§ Prerequisites.fastapi0.141.1,uvicorn[standard]0.52.1,alembic1.19.1, withbackend/requirements.lockregenerated.Upgrading
Pull and restart. No migration runs on first start —
0009_oidc_link_flowsis still head — and no environment variable was added, removed or renamed.Full detail in
CHANGELOG.md§ [0.3.1]; the release-prep record is indocs/ARCHIVE.md§14 (2026-08-11).