Skip to content

Commit 2bc3c0e

Browse files
committed
feat(bin): release-report, a second opinion on what a deploy box served and for how long
bin/release-report.mjs reads commitwork.release-evidence/1 documents (--evidence, or --fetch over ssh from the private monitor/private/releases.json, kept under ~/.commitwork/releases), validates them fail-closed, re-judges every finding under the document's own policy with the local KEV catalogue, EPSS cache and advisory CVSS index, and writes a self-contained reports/releases/<product>/index.html plus report.json: serving release, rollback-eligible releases, blocked/refused/failed/removed history, serving timeline, exposure after the first blocked evidence and after the KEV date added, disagreements with the box, and what the check does not cover. Exit 0 ok, 1 serving release blocked or a disagreement, 2 input error, 4 evidence over 26 h old or KEV stale (no agreement is claimed). Adds schema/release-evidence.schema.json, schema/release-sources.schema.json (bound as a validator, so the applied-schema floor in schema-required goes 28 to 29), monitor/releases.example.json, fixtures and tests.
1 parent 4ea8543 commit 2bc3c0e

19 files changed

Lines changed: 2075 additions & 9 deletions

‎bin/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ Exposure view runs the same code.
256256
| `graph.mjs` | Builds the relation store (node:sqlite, `nodes` and `edges`) that joins findings to the lane that found them, their repository and area, the weakness classes each lane covers (`monitor/approach-taxonomy.json`), the lanes that ran on each repository (area rollups) and the opaque sessions that claimed them; `query neighbours` and `query path` answer from it, rebuilding first when an input changed. An edge kind whose input is absent is recorded as not built with the reason. The store holds fleet data, so it lives at `<reports root>/graph/graph.db` (`CW_GRAPH_DB`). Served as `GET /api/graph/neighbours` and the `graph_neighbours` MCP tool. |
257257
| `build-security-data.mjs` | `rollup.json` → machine-generated counts injected into report md — counts cannot drift from the scan of record. |
258258
| `render-report.mjs` | Zero-dep Markdown → self-contained themed HTML (prints cleanly to PDF). |
259+
| `release-report.mjs` | Reports what a deploy box served and whether it should have. Reads `commitwork.release-evidence/1` documents (`--evidence <file>`, or `--fetch` over ssh from the private `monitor/private/releases.json`, shape in `monitor/releases.example.json`; fetched documents are kept under `~/.commitwork/releases/<product>/`), refuses an invalid one without writing anything, re-judges every finding under the document's own policy with commitwork's KEV catalogue, EPSS cache and advisory CVSS index, and writes `reports/releases/<product>/index.html` and `report.json`: serving release and verdict, rollback-eligible releases, blocked/refused/failed/removed history, the serving timeline, exposure (serving time after the first blocked evidence and after the KEV date added), disagreements with the box, and what the check does not cover. Exit 0 ok, 1 the serving release is blocked or the two disagree, 2 input error, 4 evidence over 26 h old or KEV stale (no agreement is claimed). Schema at `schema/release-evidence.schema.json`. |
259260
| `md-view.mjs` | `md-view.mjs <file.md> [--out <file.html>] [--no-open]`: renders any Markdown file as one self-contained HTML page in the house palette and opens it. The page has a contents rail and a light/dark toggle, and draws the design values the document states. Colours become swatches. `--tokens` declared in a Dark/Light table resolve wherever they are named, and each contrast ratio gets its WCAG grade. Shadows, font stacks, type sizes, weights, tracking, radii, padding, spacing steps, durations, opacities, line heights and measures render as what they describe. A column or section heading decides what a bare number means. A ```lockup fence holds panel markup, which is sanitised against an allowlist and drawn twice, dark and light, with the panel's own stylesheets scoped under each container (`CW_MD_VIEW_ADMIN_DIR`). Fonts and local images are inlined; remote images are never fetched. Every drawn value is a generated class in one hashed `<style>`, so the page runs under a CSP with no `'unsafe-inline'`. Written to `reports/md-view/<path>.html` (`CW_REPORTS_DIR`). The parser is its own (`lib/md-view.mjs`): `lib/render-markdown.mjs` ends a list item at its first wrapped line and is held to docsite parity, so it was not widened. |
260261
| `parse-runtime.mjs` | One parser for DAST/BOLA reports feeding both the rollup and `runtime.html`. |
261262
| `mainline-data.mjs`, `slices.mjs` | Sitemap history harvesters (git-graph DAG; versioned state slices). |

‎bin/release-report.mjs‎

Lines changed: 548 additions & 0 deletions
Large diffs are not rendered by default.

‎bin/test/release-report.test.mjs‎

Lines changed: 327 additions & 0 deletions
Large diffs are not rendered by default.

‎bin/test/schema-required.test.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ test('every shipped example of a private record validates against its schema', (
290290

291291
test('the count of applied schemas may not fall', () => {
292292
// Raise only after a consumer is proven to reject an invalid document through the schema.
293-
const FLOOR = 28; // monitor/release-terms.example.json's schema: compileTerms in bin/lib/release-terms.mjs refuses an invalid list (bin/test/release-terms.test.mjs)
293+
const FLOOR = 29; // monitor/private/releases.json's schema: loadSources in bin/release-report.mjs refuses an invalid list (bin/test/release-report.test.mjs); before it, monitor/release-terms.example.json's: compileTerms in bin/lib/release-terms.mjs (bin/test/release-terms.test.mjs)
294294
const applied = inv().entries.filter((e) => e.binding === 'validator').length;
295295
assert.ok(applied >= FLOOR, `${applied} schemas are applied at runtime, floor is ${FLOOR}`);
296296
assert.equal(applied, FLOOR,

‎docs/EXIT-CODES.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ nothing about exit status; `prose` means it does, in a shape the extractor does
88
Neither is a claim about what the command returns. What may change between releases is in
99
[STABILITY.md](STABILITY.md).
1010

11-
344 commands: 147 declared, 12 prose, 185 undeclared. Of the declared, 116 use only codes 0–13, the range Node itself exits with (1 on an uncaught exception), and 31 declare a code of 20 or above.
11+
345 commands: 148 declared, 12 prose, 185 undeclared. Of the declared, 117 use only codes 0–13, the range Node itself exits with (1 on an uncaught exception), and 31 declare a code of 20 or above.
1212

1313
## `commitwork` subcommands
1414

@@ -715,6 +715,15 @@ Undeclared: `list`, `run`, `scan`, `brief`, `sarif`, `reindex`, `doctor`, `hook`
715715
| 1 | blocked |
716716
| 2 | incomplete (a gate could not run, or was skipped) |
717717

718+
### `bin/release-report.mjs`
719+
720+
| Exit | Meaning |
721+
|---:|---|
722+
| 0 | ok |
723+
| 1 | the serving release is blocked, or commitwork and the box disagree on a finding |
724+
| 2 | usage, unreadable or invalid input, or a failed fetch (invalid input writes nothing) |
725+
| 4 | evidence older than 26 h, or the KEV catalogue stale or undated (no agreement is claimed) |
726+
718727
### `bin/release-tag.mjs`
719728

720729
| Exit | Meaning |

‎docs/REFERENCE.md‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ verified against the code. Check definitions are indexed in [MANIFEST-MAP.md](MA
1010

1111
| Surface | Total | Documented | Undocumented |
1212
|---|---:|---:|---:|
13-
| Commands (a usage line in the header) | 331 | 269 | 62 |
14-
| Exit codes (declared in the header) | 344 | 147 | 197 |
15-
| `CW_*` environment variables (described in an `env` header) | 876 | 191 | 685 |
13+
| Commands (a usage line in the header) | 332 | 270 | 62 |
14+
| Exit codes (declared in the header) | 345 | 148 | 197 |
15+
| `CW_*` environment variables (described in an `env` header) | 878 | 191 | 687 |
1616
| Lane manifest check keys in use (described in the schema) | 44 | 42 | 2 |
1717

1818
Commands are the population of `bin/exit-codes.mjs`: top-level `bin/`, `monitor/` and `cra/`
@@ -1277,6 +1277,17 @@ release-names-reseed.mjs — lower the release-names HEAD floor as cleanups land
12771277
- Usage: `node bin/release-names-reseed.mjs [--write] (default is a dry run)`
12781278
- Exit: undeclared
12791279

1280+
### `bin/release-report.mjs`
1281+
1282+
bin/release-report.mjs — what a deploy box served, whether it should have, and for how long.
1283+
1284+
- Usage: `node bin/release-report.mjs --evidence <file> [--evidence <file> ...] [--fetch]`
1285+
- Option: `--evidence <file> a saved document; repeatable`
1286+
- Option: `--fetch pull each source of monitor/private/releases.json over ssh and keep the documents`
1287+
- Option: `--out <dir> base directory for the reports, default <reports>/releases; one <product>/ each`
1288+
- Option: `--json print the report JSON (an array when several products) to stdout`
1289+
- Exit: 0 ok · 1 the serving release is blocked, or commitwork and the box disagree on a finding · 2 usage, unreadable or invalid input, or a failed fetch (invalid input writes nothing) · 4 evidence older than 26 h, or the KEV catalogue stale or undated (no agreement is claimed)
1290+
12801291
### `bin/release-tag.mjs`
12811292

12821293
- Usage: `release-tag.mjs [--ref <rev>] [--tag] [--json]`
@@ -2638,8 +2649,8 @@ cra/watch.mjs — exploited-vulnerability watch with CRA Art.
26382649

26392650
## Environment variables
26402651

2641-
876 `CW_*` names read by tracked non-test sources: 756 by name, and
2642-
120 indirect (quoted in a file that reads the environment by a computed key, so the read
2652+
878 `CW_*` names read by tracked non-test sources: 760 by name, and
2653+
118 indirect (quoted in a file that reads the environment by a computed key, so the read
26432654
is likely but not shown). At most 6 readers are listed per name. A description is taken from
26442655
a reader's `env` header section when one has it, otherwise from another file's, which is named.
26452656

@@ -2948,7 +2959,7 @@ a reader's `env` header section when one has it, otherwise from another file's,
29482959
| `CW_EOL_API` | direct | `bin/eol-snapshot.mjs` | base URL |
29492960
| `CW_EOL_SNAPSHOT` | direct | `bin/eol-snapshot.mjs` | output path |
29502961
| `CW_EPHEMERAL_MIN` | direct | `monitor/listeners.mjs` | undocumented |
2951-
| `CW_EPSS` | indirect | `cra/lib.mjs` | undocumented |
2962+
| `CW_EPSS` | direct | `bin/release-report.mjs` | undocumented |
29522963
| `CW_EPSS_DETAIL` | indirect | `bin/test-run.mjs`, `cra/lib.mjs`, `monitor/store-paths.mjs` | undocumented |
29532964
| `CW_EPSS_PATH` | direct | `monitor/dep-findings.mjs` | undocumented |
29542965
| `CW_ESCALATE` | direct | `cra/watch.mjs` | undocumented |
@@ -3056,7 +3067,7 @@ a reader's `env` header section when one has it, otherwise from another file's,
30563067
| `CW_JVM_KEEP_SCRATCH` | direct | `bin/jvm-resolve.sh` | undocumented |
30573068
| `CW_JVM_TIMEOUT` | direct | `bin/jvm-resolve.sh` | undocumented |
30583069
| `CW_KEEP_REPORTS` | direct | `mcp/server.mjs` | undocumented |
3059-
| `CW_KEV` | indirect | `cra/lib.mjs` | undocumented |
3070+
| `CW_KEV` | direct | `bin/release-report.mjs` | undocumented |
30603071
| `CW_KEV_PATH` | direct | `monitor/dep-findings.mjs` | undocumented |
30613072
| `CW_KEV_STALE_DAYS` | direct | `cra/lib.mjs` | undocumented |
30623073
| `CW_LANES` | indirect | `bin/lane-staleness.mjs` | undocumented |
@@ -3266,6 +3277,7 @@ a reader's `env` header section when one has it, otherwise from another file's,
32663277
| `CW_REFUTATIONS` | direct | `monitor/store-paths.mjs` | undocumented |
32673278
| `CW_REGISTRY` | direct | `bin/hermetic-test.mjs`, `monitor/history-chain.mjs`, `monitor/registry.mjs`, `monitor/store-paths.mjs` | registry, and through it the reports root (`monitor/defect-signature.mjs`) |
32683279
| `CW_REGISTRY_REQUIRE_REAL` | indirect | `monitor/registry.mjs` | undocumented |
3280+
| `CW_RELEASES_DIR` | direct | `bin/release-report.mjs` | undocumented |
32693281
| `CW_RELEASES_OUT` | direct | `bin/releases-atom.mjs` | undocumented |
32703282
| `CW_RELEASES_PATTERN` | direct | `bin/releases-atom.mjs` | undocumented |
32713283
| `CW_RELEASES_SELF` | direct | `bin/releases-atom.mjs` | undocumented |
@@ -3277,6 +3289,7 @@ a reader's `env` header section when one has it, otherwise from another file's,
32773289
| `CW_RELEASE_REVIEWS_SCHEMA` | direct | `bin/lib/release-reviews.mjs` | undocumented |
32783290
| `CW_RELEASE_SCRATCH` | direct | `bin/release-candidate.mjs` | the directory the tests gate makes its scratch HOME in (default ~/.cache/commitwork/release-candidate), read at call time |
32793291
| `CW_RELEASE_SOURCE` | direct | `bin/release-candidate.mjs`, `bin/web-roadmap.mjs` | the repository --work reads commits from; default this checkout |
3292+
| `CW_RELEASE_SOURCES` | direct | `bin/release-report.mjs` | undocumented |
32803293
| `CW_RELEASE_TAG_PREFIX` | direct | `bin/release-tag.mjs` | the release tag prefix, default v (`bin/changelog.mjs`) |
32813294
| `CW_RELEASE_TERMS` | direct | `bin/lib/release-terms.mjs` | a list file; overrides the sidecar |
32823295
| `CW_REMEDIATION_INPUT` | indirect | `monitor/remediation-pr.mjs` | undocumented |
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
{
2+
"schema": "commitwork.release-evidence/1",
3+
"product": "audiencedirect-site",
4+
"generated_at": "2026-10-22T10:00:00Z",
5+
"source": "ad-site-deploy@0123456789ab",
6+
"policy": {
7+
"name": "ad-release-gate",
8+
"kev": true,
9+
"critical": true,
10+
"high_epss_min": 0.1,
11+
"runtime_only": true,
12+
"keep_days": 90,
13+
"keep_min": 3,
14+
"verdict_max_age_hours": 48
15+
},
16+
"serving": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
17+
"releases": [
18+
{
19+
"sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20+
"built_at": "2026-10-21T08:00:00Z",
21+
"state": "serving",
22+
"verdict": {
23+
"checked_at": "2026-10-22T09:00:00Z",
24+
"status": "clean",
25+
"blocking": [],
26+
"findings": [
27+
{
28+
"id": "GHSA-xxxx-0000-0001",
29+
"cves": [
30+
"CVE-2099-0001"
31+
],
32+
"package": "left-pad",
33+
"version": "1.0.0",
34+
"runtime": true,
35+
"severity": "medium",
36+
"cvss": 5.3,
37+
"epss": 0.01,
38+
"kev": false,
39+
"blocking": false,
40+
"reason": "no policy rule fires"
41+
}
42+
],
43+
"unchecked": {
44+
"jsr": 3,
45+
"remote_hosts": [
46+
"esm.sh"
47+
]
48+
},
49+
"kev_catalog": "2026.10.20"
50+
},
51+
"verdict_at": "2026-10-22T09:00:00Z",
52+
"inventory": {
53+
"npm": [
54+
{
55+
"name": "left-pad",
56+
"version": "1.0.0",
57+
"runtime": true,
58+
"source": "lock"
59+
}
60+
],
61+
"jsr": [
62+
"@std/assert"
63+
],
64+
"unchecked_remote": [
65+
"esm.sh"
66+
]
67+
}
68+
},
69+
{
70+
"sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
71+
"built_at": "2026-10-20T08:00:00Z",
72+
"state": "kept",
73+
"verdict": {
74+
"checked_at": "2026-10-22T09:00:00Z",
75+
"status": "clean",
76+
"blocking": [],
77+
"findings": [],
78+
"unchecked": {
79+
"jsr": 3,
80+
"remote_hosts": [
81+
"esm.sh"
82+
]
83+
},
84+
"kev_catalog": "2026.10.20"
85+
},
86+
"verdict_at": "2026-10-22T09:00:00Z",
87+
"inventory": {
88+
"npm": [
89+
{
90+
"name": "left-pad",
91+
"version": "1.0.0",
92+
"runtime": true,
93+
"source": "lock"
94+
}
95+
],
96+
"jsr": [
97+
"@std/assert"
98+
],
99+
"unchecked_remote": [
100+
"esm.sh"
101+
]
102+
}
103+
},
104+
{
105+
"sha": "cccccccccccccccccccccccccccccccccccccccc",
106+
"built_at": "2026-10-10T08:00:00Z",
107+
"state": "kept",
108+
"verdict": {
109+
"checked_at": "2026-10-10T09:00:00Z",
110+
"status": "clean",
111+
"blocking": [],
112+
"findings": [],
113+
"unchecked": {
114+
"jsr": 3,
115+
"remote_hosts": [
116+
"esm.sh"
117+
]
118+
},
119+
"kev_catalog": "2026.10.20"
120+
},
121+
"verdict_at": "2026-10-10T09:00:00Z",
122+
"inventory": {
123+
"npm": [
124+
{
125+
"name": "left-pad",
126+
"version": "1.0.0",
127+
"runtime": true,
128+
"source": "lock"
129+
}
130+
],
131+
"jsr": [
132+
"@std/assert"
133+
],
134+
"unchecked_remote": [
135+
"esm.sh"
136+
]
137+
}
138+
}
139+
],
140+
"events": [
141+
{
142+
"at": "2026-10-20T08:30:00Z",
143+
"event": "serving",
144+
"sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
145+
"detail": null
146+
},
147+
{
148+
"at": "2026-10-21T08:30:00Z",
149+
"event": "serving",
150+
"sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
151+
"detail": null
152+
}
153+
]
154+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"schema": "commitwork.release-evidence/1",
3+
"product": "audiencedirect-site",
4+
"generated_at": "2026-10-22T10:00:00Z",
5+
"source": "ad-site-deploy@0123456789ab",
6+
"policy": {
7+
"name": "ad-release-gate",
8+
"kev": true,
9+
"critical": true,
10+
"high_epss_min": 0.1,
11+
"runtime_only": true,
12+
"keep_days": 90,
13+
"keep_min": 3,
14+
"verdict_max_age_hours": 48
15+
},
16+
"serving": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
17+
"releases": [
18+
{
19+
"sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20+
"built_at": "2026-10-21T08:00:00Z",
21+
"state": "serving",
22+
"verdict": {
23+
"checked_at": "2026-10-22T09:00:00Z",
24+
"status": "clean",
25+
"blocking": [],
26+
"findings": [
27+
{
28+
"id": "GHSA-kev-0000-0002",
29+
"cves": [
30+
"CVE-2099-0002"
31+
],
32+
"package": "quiet-lib",
33+
"version": "1.0.0",
34+
"runtime": true,
35+
"severity": "medium",
36+
"cvss": 6.1,
37+
"epss": null,
38+
"kev": false,
39+
"blocking": false,
40+
"reason": "no policy rule fires"
41+
}
42+
],
43+
"unchecked": {
44+
"jsr": 3,
45+
"remote_hosts": [
46+
"esm.sh"
47+
]
48+
},
49+
"kev_catalog": "2026.10.20"
50+
},
51+
"verdict_at": "2026-10-22T09:00:00Z",
52+
"inventory": {
53+
"npm": [
54+
{
55+
"name": "left-pad",
56+
"version": "1.0.0",
57+
"runtime": true,
58+
"source": "lock"
59+
}
60+
],
61+
"jsr": [
62+
"@std/assert"
63+
],
64+
"unchecked_remote": [
65+
"esm.sh"
66+
]
67+
}
68+
}
69+
],
70+
"events": [
71+
{
72+
"at": "2026-10-21T08:30:00Z",
73+
"event": "serving",
74+
"sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
75+
"detail": null
76+
}
77+
]
78+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"CVE-2099-0100": 0.9
3+
}

0 commit comments

Comments
 (0)