@bongos/core 1.19.256 → 1.19.257
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +28 -13
- package/docs/adr/0180-dependency-intake-and-drift-policy.md +36 -0
- package/docs/adr/README.md +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/audit-gate.js +121 -0
- package/src/bongos/module-scope-map.js +1 -1
- package/src/module-api.js +3 -1
- package/tests/audit_gate.mjs +89 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.257",
|
|
6
|
+
"core_contract": "1.19.257",
|
|
7
|
+
"source_commit": "89cf609c2e7581b6e48f1783d1f641e8c3993b54",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-19T02:
|
|
9
|
+
"built_at": "2026-08-19T02:39:24.112Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 316,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1350,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1688,
|
|
20
|
+
"tree_sha256": "78b0f8d12d8e15a2e9b640ce7aec3ea9cbee0ae53c0da8483946f7c261323434",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1379,10 +1379,15 @@
|
|
|
1379
1379
|
"mode": "0000644",
|
|
1380
1380
|
"sha256": "782900e7a2e49c14d7796d94e2b63493659e3c6c3ad8eb8bd721fa589307612b"
|
|
1381
1381
|
},
|
|
1382
|
+
{
|
|
1383
|
+
"path": "docs/adr/0180-dependency-intake-and-drift-policy.md",
|
|
1384
|
+
"mode": "0000644",
|
|
1385
|
+
"sha256": "62de8180208c3945c7a040b2b2799dceafd59b1c5a117aab7db56a57eb538e44"
|
|
1386
|
+
},
|
|
1382
1387
|
{
|
|
1383
1388
|
"path": "docs/adr/README.md",
|
|
1384
1389
|
"mode": "0000644",
|
|
1385
|
-
"sha256": "
|
|
1390
|
+
"sha256": "798471e95b27664e775d12dbf008fc1aa12edff0b115942a3632ecb0f2da24b5"
|
|
1386
1391
|
},
|
|
1387
1392
|
{
|
|
1388
1393
|
"path": "docs/api-reference.md",
|
|
@@ -4437,12 +4442,12 @@
|
|
|
4437
4442
|
{
|
|
4438
4443
|
"path": "package-lock.json",
|
|
4439
4444
|
"mode": "0000644",
|
|
4440
|
-
"sha256": "
|
|
4445
|
+
"sha256": "8810ea843995b1ca69fb9f14db1a394b00f3cf46b5460a167be62a2b634195b2"
|
|
4441
4446
|
},
|
|
4442
4447
|
{
|
|
4443
4448
|
"path": "package.json",
|
|
4444
4449
|
"mode": "0000644",
|
|
4445
|
-
"sha256": "
|
|
4450
|
+
"sha256": "78242b8e21dce2136eded28a2a19b70f2447bf6bdefac4e8cc22ec508569fff8"
|
|
4446
4451
|
},
|
|
4447
4452
|
{
|
|
4448
4453
|
"path": "public-docs/index.html",
|
|
@@ -4819,6 +4824,11 @@
|
|
|
4819
4824
|
"mode": "0000644",
|
|
4820
4825
|
"sha256": "7085afc3cc1f636027e1d1c7413cf6a346090de4717fd0fc2ae51d27ee766fd8"
|
|
4821
4826
|
},
|
|
4827
|
+
{
|
|
4828
|
+
"path": "scripts/gds/audit-gate.js",
|
|
4829
|
+
"mode": "0000644",
|
|
4830
|
+
"sha256": "df91d5e2514ea8c2e79d00a2386e878404ac572f111b2fb5c80ae6f2e53bd85a"
|
|
4831
|
+
},
|
|
4822
4832
|
{
|
|
4823
4833
|
"path": "scripts/gds/audit-replay.sh",
|
|
4824
4834
|
"mode": "0000644",
|
|
@@ -5892,7 +5902,7 @@
|
|
|
5892
5902
|
{
|
|
5893
5903
|
"path": "src/bongos/module-scope-map.js",
|
|
5894
5904
|
"mode": "0000644",
|
|
5895
|
-
"sha256": "
|
|
5905
|
+
"sha256": "e8a6050abfd191b0afd79c1b4990d4d0ad0a1836f8f83bf31823ec46a4ab14c8"
|
|
5896
5906
|
},
|
|
5897
5907
|
{
|
|
5898
5908
|
"path": "src/bongos/module-submissions.js",
|
|
@@ -6062,7 +6072,7 @@
|
|
|
6062
6072
|
{
|
|
6063
6073
|
"path": "src/module-api.js",
|
|
6064
6074
|
"mode": "0000644",
|
|
6065
|
-
"sha256": "
|
|
6075
|
+
"sha256": "c56a890ac70423925ba2b32d431db24ee9d88ff2f0dc484ff6057d5e18862849"
|
|
6066
6076
|
},
|
|
6067
6077
|
{
|
|
6068
6078
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6219,6 +6229,11 @@
|
|
|
6219
6229
|
"mode": "0000644",
|
|
6220
6230
|
"sha256": "917a10994a3c6b1f849953bf66fdbabf298a0fb4f07438dd51f513cff806a981"
|
|
6221
6231
|
},
|
|
6232
|
+
{
|
|
6233
|
+
"path": "tests/audit_gate.mjs",
|
|
6234
|
+
"mode": "0000644",
|
|
6235
|
+
"sha256": "af91c6018f572d9cb0a213b40ec7953f9f81cc20a33634fb5a66615a1649fee8"
|
|
6236
|
+
},
|
|
6222
6237
|
{
|
|
6223
6238
|
"path": "tests/audit_middleware.mjs",
|
|
6224
6239
|
"mode": "0000644",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# ADR 0180 — Dependency intake & drift policy
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-19
|
|
4
|
+
**Context:** Task 1003127 (goal 1000076, criterion `dependency-watch`). Grounded in the red-team baseline audit ([`docs/audits/ai-dev-redteam-baseline.md`](../audits/ai-dev-redteam-baseline.md) §3): 18 known advisories (2 high) sat unwatched in the 9 production dependencies, the `undici` override pinned a vulnerable range while also breaking Node 26 CLIs, and nothing gated what an agent may `npm install`. Threat backdrop: the 2025 npm wave (chalk/debug takeover, the Shai-Hulud self-replicating worm, the Nx postinstall stealer) ran on **install-lifecycle scripts** and **stale long-lived publish tokens**.
|
|
5
|
+
**Status:** Accepted.
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
### 1. CI audit gate with a shrink-only baseline
|
|
10
|
+
|
|
11
|
+
`scripts/gds/audit-gate.js` runs in the required `unit` workflow: `npm audit --omit=dev --json`, deduped to advisory×package findings, compared against the committed [`config/audit-baseline.json`](../../config/audit-baseline.json).
|
|
12
|
+
|
|
13
|
+
- A **new high/critical** advisory (not in the baseline) **fails the merge gate**.
|
|
14
|
+
- New low/moderate advisories warn; a baseline entry that stops reproducing prints a ratchet-down nudge.
|
|
15
|
+
- The baseline records **pre-gate debt only** (10 findings, 3 high+ at adoption). It shrinks via fix tasks; adding an entry to silence a *new* advisory defeats the gate and is out of policy.
|
|
16
|
+
- **Fail-open on infrastructure, fail-closed on findings:** a registry outage or unparseable audit output warns and exits 0 — the gate catches new advisories, it does not prove registry uptime.
|
|
17
|
+
- Blanket `npm audit fix` does not run against this tree (`workspace:` protocol in the dependency graph, `EUNSUPPORTEDPROTOCOL`) — one more reason fixes are explicit tasks, not autofix side effects.
|
|
18
|
+
|
|
19
|
+
### 2. Install-lifecycle scripts are off (`.npmrc ignore-scripts=true`)
|
|
20
|
+
|
|
21
|
+
The dominant live npm attack vector is the postinstall script. This tree needs none: **sharp** (the only native dep) installs prebuilt binaries via `optionalDependencies`, verified by a script-less `npm ci` followed by a sharp load (`vips 8.17.3`) and the full 430-test suite. `npm test`/`npm run` are unaffected (npm only suppresses install-time lifecycle hooks). A future dependency that genuinely needs an install script is an ADR-level exception with justification, not a reason to flip the flag.
|
|
22
|
+
|
|
23
|
+
### 3. Update cooldown: patrol-enforced now, npm-native later
|
|
24
|
+
|
|
25
|
+
The consensus post-Shai-Hulud control is a **release-age cooldown** (don't install versions younger than a few days; let the ecosystem catch registry malware first). npm supports this natively from 11.10 (`minimum-release-age`), but the CI toolchain (Node 22 → npm 10) would warn on the unknown key for every builder. So:
|
|
26
|
+
|
|
27
|
+
- **Now:** the weekly red-team patrol (task 1003128) flags any dependency bumped to a version **younger than 7 days** at install time, plus stale overrides (the `undici ^6.27.0` case — pinned inside a vulnerable range) and unpinned workflow actions.
|
|
28
|
+
- **Later:** when the toolchain floor reaches npm ≥ 11.10, move the cooldown into `.npmrc` and drop the patrol's version-age check.
|
|
29
|
+
|
|
30
|
+
### 4. Publish auth: OIDC trusted publishing, token as documented exception
|
|
31
|
+
|
|
32
|
+
The publish-on-merge lane (ADR 0161) authenticates with a long-lived `NPM_TOKEN` repository secret — exactly the credential class the Sept-2025 attacks monetized, and why GitHub revoked classic tokens and capped granular ones at 90 days. Decision: **adopt npm trusted publishing (OIDC)** — no long-lived secret, provenance for free. It needs a one-time owner action on npmjs.com (register the repo+workflow as a trusted publisher for `@bongos/core`), filed as a blocker. **Until then** the granular `NPM_TOKEN` remains a *documented exception*: 90-day expiry, publish-only scope, rotated on expiry.
|
|
33
|
+
|
|
34
|
+
## What this deliberately does not cover
|
|
35
|
+
|
|
36
|
+
New-dependency *intake vetting* (registry existence/age/download checks — the slopsquatting defense) lives in the weekly patrol (task 1003128), and one-off vulnerability *fixes* (sharp ≥ 0.35 for the libvips CVEs, the colyseus chain for nanoid) are ordinary tasks that shrink the baseline — filed in the idea inbox at adoption time.
|
package/docs/adr/README.md
CHANGED
|
@@ -253,5 +253,6 @@ This keeps the decision history honest and traceable.
|
|
|
253
253
|
| 0177 | [**A goal membership is a `membership_kind`, and the rename ships expand-then-contract** ([task 1003013](https://cloudbongos.com/builders#/task/1003013), goal 1000068). ADR 0174 reserved `role` for the three crafts and enforced it on the GOVERNMENT surface only — by design, since `builders.system_role` and Discord's own noun are legitimate. That left a **fourth** use uncatalogued: `goal_members.role` (`'lead' | 'member'`), the MANAGER predicate behind goal authorization. Renamed to **`membership_kind`** — values unchanged, because the noun was the problem; `rank` is wrong (not a ladder position) and `position` is worse (ADR 0174 uses it as the prose word for a rank, recreating the "rank-role" qualifier collision). A boolean `is_lead` was seriously considered and rejected: the text rename keeps the copied CHECK and stays a reversible change on a live authz column, where `ADD boolean; UPDATE; DROP text` is data-destructive. The migration is **expand-then-contract, never `RENAME COLUMN`** — auto-rollback reverts the code pin and NOT the DB, so `core_220` ADDs the column and keeps the pair in lockstep with a BEFORE trigger (direction decided by which side changed), and a later task DROPs the old column with the `-- forward-unsafe` pragma, a backup, and its own window; no `SET NOT NULL` in step 1, because a static NOT NULL on a column the previous release never heard of is what the forward-safety gate rightly flags. The API field renames with it and carries **no `role` alias** (ADR 0174's Lineage lesson: a time-boxed shim outlives its release), pinned by the test of its absence. The reservation gains a **second narrow wall** rather than a widened first one — line-scoped inside the 5k-line `db.js` to the lines that name `goal_members`, so no allowlist is needed. A **fifth** use (`project_memberships.role`) is catalogued and filed, with this as its pattern.](<redacted>.md) | naming / lifecycle |
|
|
254
254
|
| 0178 | [**The copy desk: a flag carries a judgement, and no column here could ever hold copy** ([task 1003113](https://cloudbongos.com/builders#/task/1003113), R02 of goal 1000074). [ADR 0081](<redacted>.md) made the repo the source of truth for design TOKENS; user-facing TEXT had no equivalent. R01 built the inventory (`copy-inventory.js` → `docs/copy-registry.json`), which answers *what does the product say* but cannot answer *what is wrong with it* — a judgement a person must record. This is that record plus the artist's queue. A new default-on module keyed **`copy-desk`, not `copy`**, for a concrete reason: the branding contract already owns a `copy` block, so the bare word reds the `core carries no module code` fitness check on the English word in `src/branding.js` — and the editorial "copy desk" is the more accurate name anyway. **The reason is required in the route AND in a CHECK constraint**: the route gives a person a usable error, the constraint means no future write path can create the reasonless flag the queue exists to not contain (a pointer with no judgement is *worse* than no flag — it still costs the artist a look). Closing requires a note by the same symmetry, since an unexplained close is indistinguishable from clearing a queue to move a number. The load-bearing decision is **NO LIVE CMS enforced by the schema's SHAPE**: `copy_desk_flags` has no column that could hold replacement text and must never gain one, because that non-goal does not erode via an "add a CMS" commit but via one plausible column, then one plausible read of it — `copy_no_cms.mjs` pins the write surface at exactly two verbs so R03's proposal path changes it in a reviewable diff. Rank is asymmetric: **filing is open to every rank** (the engineer who wrote the toast and the newcomer who could not read the button are the likeliest noticers, and neither is the artist — the reason floor is the quality gate, rank is not), closing is Metic+ **or the author** (withdrawing your own flag is a correction, not triage); nothing gates on craft, since an artist is a craft and not a rank. Duplicates are the only automatic signal (the one of stale/duplicated/off-voice needing no judgement layer) and the count **subtracts settled work**, or it never moves and teaches that the queue is decorative. A vanished target is **reported, never auto-closed** — the id is a hash over the text, so an edit routinely orphans a flag, and closing a human judgement because a hash moved is the family of error ADR 0162 rules out. Deliberately reports **no `generated_at`**: R01's contract is determinism, so a clock is the one field it cannot carry.](<redacted>.md) | design / artist loop |
|
|
255
255
|
| 0179 | [**`rank:metic+` means metic and above; a bare `rank:metic` still means exactly metic** ([task 1003094](https://cloudbongos.com/builders#/task/1003094), goal 1000069). [ADR 0175](<redacted>.md) gave the constitution ONE membership form, `rank:<key>`, resolved by an EXACT SQL match — so `rank:metic` seated metics ONLY (excluding Archons), `rank:xenos` seated NEWCOMERS ONLY rather than "everyone", and "Metic and above" was inexpressible. Everywhere else a rank is a LADDER and authority is cumulative (ADR 0034 inheritance, requireRank('metic','archon'), the floor-derived seed); board membership was the one place it meant "exactly", and nothing said so. Rehearsing R25 on a real Postgres, an Archon ratified `rank:metic` under monarchy — the apply worked perfectly — and the next vote came back 403 not_board_member: **the owner flipped the switch and fell off their own board**, with the board deadlocked at one member because an Archon can still PROPOSE but no longer VOTE. Owner decision: EXPLICIT GRAMMAR. `rank:metic+` (that rank and above), `rank:metic,archon` (union), and **a bare key still means EXACTLY that key** — rejecting the smaller diff (silently redefining the bare form) precisely because it would have changed the meaning of every already-ratified constitution, the monarchy default included, on upgrade: nothing in a government should change meaning without a vote. `+` is defined only on the STANDARD ladder — "and above" about a custom rank with no ladder position is a parse REFUSAL, not a guess, and a refusal seats nobody. The grammar had been written TWICE (a shape regex in the config sanitizer, a parse regex in the resolver, pinned by a property test) — sound for one literal form, unsound the moment a rule needs a LOOKUP, which is the same class of drift that produced the bug; both now call one pure `membership-predicate.js`. Adds the DISENFRANCHISEMENT GUARD: a proposal that would unseat its own proposer is refused before any write, naming the one-character fix, overridable with an explicit `acknowledge_self_removal` (a separate parameter, never a proposal field) because stepping back is a legitimate act. The two DB reads take a set via `= ANY($1)` + DISTINCT (a double-counted member silently raises the consent bar), the structural filters are re-proven against real Postgres (a deactivated builder and a machine principal still never govern), and the hall panel now describes every form in plain language — rendering `rank:metic+` as "the metic+ rank" would be the same bug one layer up. Unblocks task 1003095, which can now write each charter's TRUE predicate rather than wording around a limitation.](<redacted>.md) | government / the board |
|
|
256
|
+
| 0180 | [**Dependency intake & drift policy: baseline-ratchet audit gate, install scripts off, cooldown via patrol, OIDC publish** ([task 1003127](https://cloudbongos.com/builders#/task/1003127), goal 1000076). The red-team baseline audit found 18 advisories (2 high) sitting unwatched in 9 production deps, the `undici ^6.27.0` override pinned INSIDE a vulnerable range while also breaking Node 26 CLIs, and nothing gated what an agent may install — against a 2025 npm threat wave (chalk/debug, Shai-Hulud, Nx) that ran on postinstall scripts and long-lived publish tokens. Four decisions. (1) `scripts/gds/audit-gate.js` runs in the required `unit` check: a high/critical advisory NOT in the committed `config/audit-baseline.json` fails the merge gate; the baseline is **shrink-only pre-gate debt** (10 findings at adoption) — adding an entry to silence a NEW advisory is out of policy. Fail-OPEN on registry/infra errors, fail-CLOSED on findings: the gate catches new advisories, it does not prove registry uptime. Notably a blanket `npm audit fix` cannot even run here (`workspace:` protocol, EUNSUPPORTEDPROTOCOL) — fixes are explicit tasks. (2) `.npmrc ignore-scripts=true`: the tree needs no install-lifecycle scripts — sharp installs prebuilds via optionalDependencies, proven by a script-less `npm ci` + full 430-test suite; a future dep needing one is an ADR-level exception. (3) Update cooldown is PATROL-enforced (task 1003128 flags versions younger than 7 days + stale overrides + action-pin drift) rather than `.npmrc`-enforced, because CI's npm 10 warns on the npm-11 `minimum-release-age` key for every builder; move it into `.npmrc` when the toolchain floor reaches npm ≥ 11.10. (4) Publish auth moves to npm **trusted publishing (OIDC)** — needs a one-time owner registration on npmjs.com (blocker filed); until then the granular 90-day `NPM_TOKEN` is a documented exception.](<redacted>.md) | devsecops / supply chain |
|
|
256
257
|
|
|
257
258
|
> ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.257",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.257",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.257",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/audit-gate.js — the dependency vulnerability gate (task 1003127,
|
|
3
|
+
// goal 1000076; ADR 0180, docs/audits/ai-dev-redteam-baseline.md §3).
|
|
4
|
+
//
|
|
5
|
+
// Ratchet semantics, matching the goal's other budgets: a committed BASELINE
|
|
6
|
+
// (config/audit-baseline.json) records the advisories that predate the gate.
|
|
7
|
+
// A HIGH or CRITICAL advisory NOT in the baseline fails CI; anything below
|
|
8
|
+
// high warns; a baseline entry that no longer reproduces prints a shrink nudge
|
|
9
|
+
// (removing it is an ordinary PR). Fixes are explicit tasks — a blanket
|
|
10
|
+
// `npm audit fix` does not even run against this tree (workspace: protocol).
|
|
11
|
+
//
|
|
12
|
+
// Fail contract: CLOSED on findings, OPEN on infrastructure — a registry
|
|
13
|
+
// outage or unparseable audit output warns and exits 0 rather than bricking
|
|
14
|
+
// every PR (the gate's job is catching new advisories, not proving uptime).
|
|
15
|
+
//
|
|
16
|
+
// Run: node scripts/gds/audit-gate.js
|
|
17
|
+
// Refresh floor: node scripts/gds/audit-gate.js --update-baseline
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { execSync } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
26
|
+
const BASELINE_PATH = path.join(ROOT, 'config', 'audit-baseline.json');
|
|
27
|
+
const GATE_SEVERITIES = new Set(['high', 'critical']);
|
|
28
|
+
|
|
29
|
+
// npm audit JSON → flat advisory findings, one per (advisory, vulnerable pkg).
|
|
30
|
+
// Identity is the advisory's numeric `source` id + the package it hits — stable
|
|
31
|
+
// across runs, unlike the parent-package rollups npm prints.
|
|
32
|
+
function extractFindings(auditJson) {
|
|
33
|
+
const out = new Map();
|
|
34
|
+
const vulns = (auditJson && auditJson.vulnerabilities) || {};
|
|
35
|
+
for (const entry of Object.values(vulns)) {
|
|
36
|
+
for (const via of entry.via || []) {
|
|
37
|
+
if (typeof via !== 'object' || via === null || via.source == null) continue;
|
|
38
|
+
const key = `${via.source}:${via.name}`;
|
|
39
|
+
if (!out.has(key)) {
|
|
40
|
+
out.set(key, {
|
|
41
|
+
id: String(via.source),
|
|
42
|
+
name: via.name,
|
|
43
|
+
severity: String(via.severity || entry.severity || 'unknown').toLowerCase(),
|
|
44
|
+
title: via.title || '',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return [...out.values()].sort((a, b) => (a.name + a.id).localeCompare(b.name + b.id));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Pure gate decision — exported for the hermetic test.
|
|
53
|
+
function compare(currentFindings, baselineFindings) {
|
|
54
|
+
const baseKeys = new Set((baselineFindings || []).map((f) => `${f.id}:${f.name}`));
|
|
55
|
+
const curKeys = new Set(currentFindings.map((f) => `${f.id}:${f.name}`));
|
|
56
|
+
const fresh = currentFindings.filter((f) => !baseKeys.has(`${f.id}:${f.name}`));
|
|
57
|
+
return {
|
|
58
|
+
newGating: fresh.filter((f) => GATE_SEVERITIES.has(f.severity)),
|
|
59
|
+
newAdvisory: fresh.filter((f) => !GATE_SEVERITIES.has(f.severity)),
|
|
60
|
+
resolved: (baselineFindings || []).filter((f) => !curKeys.has(`${f.id}:${f.name}`)),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readBaseline() {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8')).findings || [];
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runAudit() {
|
|
73
|
+
try {
|
|
74
|
+
return execSync('npm audit --omit=dev --json', { cwd: ROOT, maxBuffer: 32 * 1024 * 1024 }).toString();
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// npm audit exits 1 when advisories exist — that IS the payload.
|
|
77
|
+
if (e && e.stdout) return e.stdout.toString();
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function main() {
|
|
83
|
+
const update = process.argv.includes('--update-baseline');
|
|
84
|
+
let findings;
|
|
85
|
+
try {
|
|
86
|
+
findings = extractFindings(JSON.parse(runAudit()));
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.warn(`audit-gate: WARN — npm audit did not produce parseable JSON (${e.message}); failing OPEN (infra, not findings).`);
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (update) {
|
|
93
|
+
fs.writeFileSync(
|
|
94
|
+
BASELINE_PATH,
|
|
95
|
+
JSON.stringify({ _comment: 'audit-gate baseline (ADR 0180): pre-existing advisories the gate tolerates. Shrink-only — remove entries as fix tasks land; never add by hand to silence a NEW advisory.', updated: null, findings }, null, 2) + '\n',
|
|
96
|
+
);
|
|
97
|
+
console.log(`audit-gate: baseline updated — ${findings.length} finding(s) recorded at ${path.relative(ROOT, BASELINE_PATH)}.`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const verdict = compare(findings, readBaseline());
|
|
102
|
+
for (const f of verdict.resolved) {
|
|
103
|
+
console.log(`audit-gate: ratchet-down available — baseline entry no longer reproduces: [${f.severity}] ${f.name} (${f.id}) ${f.title}`);
|
|
104
|
+
}
|
|
105
|
+
for (const f of verdict.newAdvisory) {
|
|
106
|
+
console.warn(`audit-gate: WARN new ${f.severity} advisory (below the gate): ${f.name} (${f.id}) ${f.title}`);
|
|
107
|
+
}
|
|
108
|
+
if (verdict.newGating.length) {
|
|
109
|
+
console.error('audit-gate: FAIL — new high/critical advisories not in the baseline:');
|
|
110
|
+
for (const f of verdict.newGating) {
|
|
111
|
+
console.error(` [${f.severity}] ${f.name} (advisory ${f.id}) ${f.title}`);
|
|
112
|
+
}
|
|
113
|
+
console.error('Fix or pin the dependency. The baseline only records PRE-GATE debt; do not add new entries to silence this.');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
console.log(`audit-gate: PASS — ${findings.length} known finding(s), 0 new at high/critical.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (require.main === module) main();
|
|
120
|
+
|
|
121
|
+
module.exports = { extractFindings, compare, GATE_SEVERITIES, BASELINE_PATH };
|
|
@@ -310,7 +310,7 @@ const MODULE_GLOBS = {
|
|
|
310
310
|
'.npmrc', '.gitleaks.toml',
|
|
311
311
|
'package.json', 'package-lock.json',
|
|
312
312
|
'scripts/gds/fitness.js', 'scripts/gds/docs-entropy.js',
|
|
313
|
-
'scripts/gds/redteam-patrol.js',
|
|
313
|
+
'scripts/gds/redteam-patrol.js', 'scripts/gds/audit-gate.js',
|
|
314
314
|
],
|
|
315
315
|
};
|
|
316
316
|
|
package/src/module-api.js
CHANGED
|
@@ -939,8 +939,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
939
939
|
// landed since 1.19.254 with no explicit bump. run 32207967928. (task 1002620)
|
|
940
940
|
// 1.19.256 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
941
941
|
// landed since 1.19.255 with no explicit bump. run 32208475455. (task 1002620)
|
|
942
|
+
// 1.19.257 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
943
|
+
// landed since 1.19.256 with no explicit bump. run 32209295358. (task 1002620)
|
|
942
944
|
// ---------------------------------------------------------------------------
|
|
943
|
-
const CORE_VERSION = '1.19.
|
|
945
|
+
const CORE_VERSION = '1.19.257'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
944
946
|
|
|
945
947
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
946
948
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// tests/audit_gate.mjs — the dependency audit gate's pure core (task 1003127,
|
|
2
|
+
// goal 1000076; ADR 0180). Hermetic: exercises extractFindings + compare on
|
|
3
|
+
// fixture JSON — the npm/network half is CI-only by design (fail-open there).
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
const gate = require(path.join(ROOT, 'scripts', 'gds', 'audit-gate.js'));
|
|
13
|
+
|
|
14
|
+
const AUDIT_FIXTURE = {
|
|
15
|
+
vulnerabilities: {
|
|
16
|
+
// parent rollup whose via is a STRING (another package) — contributes nothing
|
|
17
|
+
'discord.js': { severity: 'moderate', via: ['undici'] },
|
|
18
|
+
// real advisory objects — these are the findings
|
|
19
|
+
undici: {
|
|
20
|
+
severity: 'moderate',
|
|
21
|
+
via: [
|
|
22
|
+
{ source: 101, name: 'undici', severity: 'moderate', title: 'CRLF injection' },
|
|
23
|
+
{ source: 102, name: 'undici', severity: 'moderate', title: 'response desync' },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
sharp: {
|
|
27
|
+
severity: 'high',
|
|
28
|
+
via: [{ source: 200, name: 'sharp', severity: 'high', title: 'libvips CVEs' }],
|
|
29
|
+
},
|
|
30
|
+
nanoid: {
|
|
31
|
+
severity: 'high',
|
|
32
|
+
// duplicate of the same advisory via two chains — must dedupe
|
|
33
|
+
via: [
|
|
34
|
+
{ source: 300, name: 'nanoid', severity: 'high', title: 'predictable ids' },
|
|
35
|
+
{ source: 300, name: 'nanoid', severity: 'high', title: 'predictable ids' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
test('extractFindings flattens advisory objects, skips string vias, dedupes by advisory×package', () => {
|
|
42
|
+
const f = gate.extractFindings(AUDIT_FIXTURE);
|
|
43
|
+
assert.equal(f.length, 4, `expected 4 unique findings, got ${JSON.stringify(f)}`);
|
|
44
|
+
assert.ok(f.every((x) => typeof x.id === 'string' && x.name && x.severity));
|
|
45
|
+
assert.equal(f.filter((x) => x.name === 'nanoid').length, 1, 'duplicate advisory chains collapse');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('a baseline-covered tree passes: no newGating, no newAdvisory', () => {
|
|
49
|
+
const f = gate.extractFindings(AUDIT_FIXTURE);
|
|
50
|
+
const v = gate.compare(f, f);
|
|
51
|
+
assert.equal(v.newGating.length, 0);
|
|
52
|
+
assert.equal(v.newAdvisory.length, 0);
|
|
53
|
+
assert.equal(v.resolved.length, 0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('a NEW high advisory gates; a new moderate only advises', () => {
|
|
57
|
+
const baseline = gate.extractFindings(AUDIT_FIXTURE);
|
|
58
|
+
const current = [
|
|
59
|
+
...baseline,
|
|
60
|
+
{ id: '999', name: 'express', severity: 'high', title: 'fresh high' },
|
|
61
|
+
{ id: '998', name: 'pg', severity: 'moderate', title: 'fresh moderate' },
|
|
62
|
+
];
|
|
63
|
+
const v = gate.compare(current, baseline);
|
|
64
|
+
assert.deepEqual(v.newGating.map((f) => f.id), ['999']);
|
|
65
|
+
assert.deepEqual(v.newAdvisory.map((f) => f.id), ['998']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('a fixed advisory surfaces as a ratchet-down nudge, never a failure', () => {
|
|
69
|
+
const baseline = gate.extractFindings(AUDIT_FIXTURE);
|
|
70
|
+
const current = baseline.filter((f) => f.name !== 'sharp');
|
|
71
|
+
const v = gate.compare(current, baseline);
|
|
72
|
+
assert.equal(v.newGating.length, 0);
|
|
73
|
+
assert.deepEqual(v.resolved.map((f) => f.name), ['sharp']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('empty/missing baseline means every high gates (the fresh-repo case)', () => {
|
|
77
|
+
const v = gate.compare(gate.extractFindings(AUDIT_FIXTURE), []);
|
|
78
|
+
assert.equal(v.newGating.length, 2, 'sharp + nanoid gate');
|
|
79
|
+
assert.equal(v.newAdvisory.length, 2, 'the two undici moderates advise');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('the committed baseline file parses and only carries known severities', () => {
|
|
83
|
+
const b = require(path.join(ROOT, 'config', 'audit-baseline.json'));
|
|
84
|
+
assert.ok(Array.isArray(b.findings) && b.findings.length > 0);
|
|
85
|
+
for (const f of b.findings) {
|
|
86
|
+
assert.ok(['low', 'moderate', 'high', 'critical', 'info', 'unknown'].includes(f.severity), `odd severity ${f.severity}`);
|
|
87
|
+
assert.ok(f.id && f.name, 'baseline entries carry advisory id + package');
|
|
88
|
+
}
|
|
89
|
+
});
|