From a9d66440b18a13f97daa7448b3770c9a5ce369aa Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 27 Oct 2026 13:53:06 -0700 Subject: [PATCH] Verify the existing selector-parser security backport and CSS consumers --- ...dvisory-backports-with-locked-consumers.md | 34 +++++++++++ .agents/issues/INDEX.md | 1 + .agents/issues/security-audit-index.md | 1 + docs/quality/SECURITY_SCOPE_2026-10-23.md | 20 +++++++ .../lib/dependencies/selector-parser.test.mjs | 58 +++++++++++++++++++ 5 files changed, 114 insertions(+) create mode 100644 .agents/inbox/2026-10-27-reconcile-advisory-backports-with-locked-consumers.md create mode 100644 tests/lib/dependencies/selector-parser.test.mjs diff --git a/.agents/inbox/2026-10-27-reconcile-advisory-backports-with-locked-consumers.md b/.agents/inbox/2026-10-27-reconcile-advisory-backports-with-locked-consumers.md new file mode 100644 index 00000000..13f87d9c --- /dev/null +++ b/.agents/inbox/2026-10-27-reconcile-advisory-backports-with-locked-consumers.md @@ -0,0 +1,34 @@ +# Reconcile updated advisory ranges with the already locked consumer + +The October 23 inventory and #186 recorded three moderate CSS-chain entries. +On 2026-10-27, fresh-cache `npm audit --omit=dev --json` on dev +`8712389e24f567f5b3e6ed132c1ac5d4dafb21fa` reported five moderate entries: +Router/DOM and UUID/federation/subgraph, with no CSS-chain entry. + +The [official selector-parser advisory](https://github.com/advisories/GHSA-rj75-hqrm-r3gf) +was read directly and via `gh api /advisories/GHSA-rj75-hqrm-r3gf`. +Its `updated_at` is `2026-10-26T14:07:57Z`, `withdrawn_at` is null, and its +affected ranges are `<6.1.5` and `>=7.0.0 <7.1.6`, with fixes 6.1.5 and 7.1.6. +`npm ls postcss-selector-parser postcss-nested tailwindcss --all` resolves +Tailwind 3.4.19 and postcss-nested 6.2.0 to parser 6.1.5. This was already locked +in commit `36c02c020832cafd8f67b9fae85d126827e89a98`, dated 2026-10-23: +`git show 36c02c0:package-lock.json` contains the same parser version. + +The historical audit remains evidence of what that run reported. The current +official fixed ranges and actual consumer now establish that this locked CSS +path is patched. There is no dependency update or reclassification in this +follow-up. Do not force a Tailwind major migration or attribute the older audit +to an unproved cache/publication cause merely to make the historical count agree. + +The dedicated regression checks both maintained fixed branches and every locked +parser path, then resolves the actual Tailwind and postcss-nested consumers. +It verifies flat-selector round trips, nested selector output, and generated +base/responsive Tailwind utilities. The flat input is bounded at 2048 classes; +this is semantic coverage, not a claimed CPU-exhaustion benchmark. Fresh build, +integrated browser checks and exact-head results belong in the PR discussion. + +Limits: trusted build-time CSS was the inspected application surface. This is +clearance of the named advisory for the current resolution, not full CSS/npm/OS +security or deployed-site acceptance. Router, UUID and dev-tool work have their +own PRs. Preserve fresh dated audit payloads as metadata can change independently +of the lockfile. Local evidence: `/tmp/graphdone-resolution-20261027/css-*`. diff --git a/.agents/issues/INDEX.md b/.agents/issues/INDEX.md index 42ef9dbe..dcb80e6e 100644 --- a/.agents/issues/INDEX.md +++ b/.agents/issues/INDEX.md @@ -2,6 +2,7 @@ | Symptom | Canonical lesson and regression | | --- | --- | +| An old audit lists a dependency that the current advisory identifies as patched | [Reconcile advisory backports with the existing lock](../inbox/2026-10-27-reconcile-advisory-backports-with-locked-consumers.md); `selector-parser.test.mjs` | | Minimap zoom or overview inspection is overwritten while a newly loaded graph settles | [Explicit camera input must stop automatic framing](../inbox/2026-10-27-explicit-camera-input-must-stop-auto-fit.md); timer-controlled `semantic-lod.spec.ts` negative control | | Dense-graph browser case exhausts its timeout after slow API fixture creation; cleanup errors disappear | [Bound fixture setup separately and fail on cleanup errors](../inbox/2026-10-27-cold-lod-fixtures-need-separate-budgets.md); `tests/e2e/graph/semantic-lod.spec.ts` | | GitHub connector can read a repo but PR creation returns `Resource not accessible by integration` | [Client-specific write permissions](../inbox/2026-10-27-github-connector-write-permissions.md); verify existing authorized access without changing credentials or policy | diff --git a/.agents/issues/security-audit-index.md b/.agents/issues/security-audit-index.md index 039affee..12df38ea 100644 --- a/.agents/issues/security-audit-index.md +++ b/.agents/issues/security-audit-index.md @@ -2,6 +2,7 @@ | Symptom | Evidence and corrective rule | | --- | --- | +| Historical CSS audit findings disappear while the lock is unchanged | [Reconcile official backport ranges with actual consumers](../inbox/2026-10-27-reconcile-advisory-backports-with-locked-consumers.md); locked-path and Tailwind/parser regressions | | Fresh production audit flags Undici through optional SQLite build tooling without a direct application import | [Trace the native-build consumer and patch its actual resolved copy](../inbox/2026-10-24-undici-native-build-dependency.md); `tests/lib/dependencies/undici.test.mjs` rejects affected locked and installed consumer versions | | Green production audit hides development-tool findings or package counts are treated as exploit counts | [Audit scope lesson](../inbox/2026-10-23-dependency-audit-scope.md); [dated security scope](../../docs/quality/SECURITY_SCOPE_2026-10-23.md) | | Nested lint glob stalls the process | `tests/lib/dependencies/minimatch.test.mjs`; scoped parser matcher override and bounded child-process regression | diff --git a/docs/quality/SECURITY_SCOPE_2026-10-23.md b/docs/quality/SECURITY_SCOPE_2026-10-23.md index 07012d82..c2133265 100644 --- a/docs/quality/SECURITY_SCOPE_2026-10-23.md +++ b/docs/quality/SECURITY_SCOPE_2026-10-23.md @@ -45,6 +45,26 @@ Follow-up implementation on 2026-10-23: [PR #169](https://github.com/GraphDone/G Source locations are relative to `packages/server/src/` and anchored to the snapshot above. Local, secret-free evidence is under `/tmp/graphdone-residual-*-audit.json`, `/tmp/graphdone-minimatch-*.log`, and `/tmp/graphdone-residual-auth-probe.log`; the latter reports only `parallelMagicLinkSuccesses: 2` and `arbitraryCaptchaAccepted: true`. +## CSS advisory reconciliation on 2026-10-27 + +The historical three CSS entries above remain the result of the October 23 +inventory. On October 27, the [official advisory](https://github.com/advisories/GHSA-rj75-hqrm-r3gf) +lists parser 6.1.5 and 7.1.6 as patched; affected ranges are `<6.1.5` and +`>=7.0.0 <7.1.6`. Its metadata was updated on October 26 and it is not withdrawn. +Live dev `8712389` already locks parser 6.1.5 for Tailwind 3.4.19 and +postcss-nested 6.2.0, the same parser resolution present in `36c02c0` on October 23. +The fresh-cache production audit has no CSS-chain finding. This follow-up does +not alter any dependency declaration, lockfile or production/dev classification. + +The dedicated guard checks every locked parser path and both actual CSS +consumers. Semantic tests preserve flat and nested selectors and generated +responsive Tailwind utilities; they are not a timing benchmark. This resolves +the named advisory for the current locked chain, without forcing a Tailwind +migration. Keep earlier audit counts as dated observations and do not infer a +cache cause. The [canonical lesson](../../.agents/inbox/2026-10-27-reconcile-advisory-backports-with-locked-consumers.md) +records commands, sources and limits. Final build/browser/CI evidence belongs +to the separate CSS review. Router, UUID and toolchain work remain separate. + ## Coverage still needed The folder tests do not establish all generated Graph/WorkItem/Edge permissions, concurrent deletion integrity, MCP/agent grant revocation, share-link scopes, session/cookie policy, uploads, backups, email delivery, or deployed proxy controls. Those need isolated contracts and user-level tests with current roles, multiple accounts, races, and negative cases. Dependency counts, passing smoke tests, and absence of a discovered source sink are separate pieces of evidence; none is a full security assessment. diff --git a/tests/lib/dependencies/selector-parser.test.mjs b/tests/lib/dependencies/selector-parser.test.mjs new file mode 100644 index 00000000..d9be8cb1 --- /dev/null +++ b/tests/lib/dependencies/selector-parser.test.mjs @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import test from 'node:test'; + +const root = new URL('../../../', import.meta.url); +const require = createRequire(new URL('package.json', root)); +const webRequire = createRequire(new URL('packages/web/package.json', root)); +const tailwindRequire = createRequire(webRequire.resolve('tailwindcss')); +const nestedRequire = createRequire(tailwindRequire.resolve('postcss-nested')); +const semver = require('semver'); +const patched = version => semver.valid(version) !== null && semver.satisfies(version, '>=6.1.5 <7.0.0 || >=7.1.6'); + +test('selector advisory guard recognizes the fixed maintained branches', () => { + for (const version of ['6.1.4', '6.1.5-beta.0', '7.0.0', '7.1.5', '7.1.6-beta.0', 'invalid']) assert.equal(patched(version), false, version); + for (const version of ['6.1.5', '7.1.6', '7.1.7']) assert.equal(patched(version), true, version); +}); + +test('every locked and actual Tailwind selector parser contains the backport', async () => { + const lock = JSON.parse(await readFile(new URL('package-lock.json', root), 'utf8')); + const paths = Object.entries(lock.packages).filter(([path]) => /(^|\/)node_modules\/postcss-selector-parser$/.test(path)); + assert.ok(paths.length > 0); + for (const [path, entry] of paths) assert.ok(patched(entry.version), `${path}: ${entry.version}`); + for (const consumer of [tailwindRequire, nestedRequire]) assert.ok(patched(consumer('postcss-selector-parser/package.json').version)); +}); + +test('actual CSS consumers preserve flat and nested selector semantics', async () => { + const input = '.graph:hover > .node[data-kind="task"]:is(.selected, .pending)'; + for (const consumer of [tailwindRequire, nestedRequire]) { + const parser = consumer('postcss-selector-parser'); + assert.equal(parser().processSync(input), input); + const flat = '.node'.repeat(2048); + const ast = parser().astSync(flat); + let count = 0; + ast.walkClasses(() => count++); + assert.equal(count, 2048); + assert.equal(ast.toString(), flat); + } + const postcss = webRequire('postcss'); + const nested = tailwindRequire('postcss-nested'); + const output = await postcss([nested()]).process('.graph { & > .node { color: white; } }', { from: undefined }); + const selectors = []; + output.root.walkRules(rule => selectors.push(rule.selector)); + assert.deepEqual(selectors, ['.graph > .node']); +}); + +test('installed Tailwind builds base and responsive utilities through its parser', async () => { + const postcss = webRequire('postcss'); + const tailwind = webRequire('tailwindcss'); + const output = await postcss([tailwind({ content: [{ raw: '
', extension: 'html' }], corePlugins: { preflight: false } })]) + .process('@tailwind utilities;', { from: undefined }); + const selectors = []; + const media = []; + output.root.walkRules(rule => selectors.push(rule.selector)); + output.root.walkAtRules('media', rule => media.push(rule.params)); + for (const selector of ['.gap-2', '.text-sm', '.md\\:flex']) assert.ok(selectors.includes(selector), selector); + assert.ok(media.includes('(min-width: 768px)')); +});