@xenos1996/usa 2.0.1 → 2.3.1

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.
Files changed (81) hide show
  1. package/README.md +44 -33
  2. package/USA.md +9 -9
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +194 -7
  5. package/dist/cli.js.map +1 -1
  6. package/dist/config.d.ts +1 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +31 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/engine/audit.d.ts.map +1 -1
  11. package/dist/engine/audit.js +42 -4
  12. package/dist/engine/audit.js.map +1 -1
  13. package/dist/engine/automatability.d.ts +32 -0
  14. package/dist/engine/automatability.d.ts.map +1 -0
  15. package/dist/engine/automatability.js +81 -0
  16. package/dist/engine/automatability.js.map +1 -0
  17. package/dist/engine/catalogues.d.ts +43 -0
  18. package/dist/engine/catalogues.d.ts.map +1 -0
  19. package/dist/engine/catalogues.js +109 -0
  20. package/dist/engine/catalogues.js.map +1 -0
  21. package/dist/engine/diff.d.ts +14 -0
  22. package/dist/engine/diff.d.ts.map +1 -1
  23. package/dist/engine/diff.js +59 -36
  24. package/dist/engine/diff.js.map +1 -1
  25. package/dist/engine/evaluate.d.ts +2 -1
  26. package/dist/engine/evaluate.d.ts.map +1 -1
  27. package/dist/engine/evaluate.js +125 -3
  28. package/dist/engine/evaluate.js.map +1 -1
  29. package/dist/engine/gate.d.ts +33 -0
  30. package/dist/engine/gate.d.ts.map +1 -1
  31. package/dist/engine/gate.js +116 -0
  32. package/dist/engine/gate.js.map +1 -1
  33. package/dist/engine/loader.d.ts.map +1 -1
  34. package/dist/engine/loader.js +92 -4
  35. package/dist/engine/loader.js.map +1 -1
  36. package/dist/engine/review.d.ts +49 -0
  37. package/dist/engine/review.d.ts.map +1 -0
  38. package/dist/engine/review.js +128 -0
  39. package/dist/engine/review.js.map +1 -0
  40. package/dist/engine/suppression.d.ts +52 -0
  41. package/dist/engine/suppression.d.ts.map +1 -0
  42. package/dist/engine/suppression.js +91 -0
  43. package/dist/engine/suppression.js.map +1 -0
  44. package/dist/index.d.ts +6 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +6 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/report/json.d.ts +48 -0
  49. package/dist/report/json.d.ts.map +1 -0
  50. package/dist/report/json.js +60 -0
  51. package/dist/report/json.js.map +1 -0
  52. package/dist/report/markdown.d.ts.map +1 -1
  53. package/dist/report/markdown.js +110 -17
  54. package/dist/report/markdown.js.map +1 -1
  55. package/dist/report/sarif.d.ts +84 -0
  56. package/dist/report/sarif.d.ts.map +1 -0
  57. package/dist/report/sarif.js +129 -0
  58. package/dist/report/sarif.js.map +1 -0
  59. package/dist/report/signature.d.ts +76 -0
  60. package/dist/report/signature.d.ts.map +1 -0
  61. package/dist/report/signature.js +206 -0
  62. package/dist/report/signature.js.map +1 -0
  63. package/dist/types.d.ts +115 -0
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/types.js +2 -1
  66. package/dist/types.js.map +1 -1
  67. package/dist/util/project.d.ts.map +1 -1
  68. package/dist/util/project.js +23 -1
  69. package/dist/util/project.js.map +1 -1
  70. package/dist/util/site.d.ts +13 -0
  71. package/dist/util/site.d.ts.map +1 -0
  72. package/dist/util/site.js +24 -0
  73. package/dist/util/site.js.map +1 -0
  74. package/package.json +9 -1
  75. package/rules/catalogues.yaml +109 -0
  76. package/rules/core/provenance-attestation.yaml +65 -0
  77. package/rules/core/provenance-cosign.yaml +110 -0
  78. package/rules/index.yaml +2 -0
  79. package/rules/profiles/maturity.yaml +1 -1
  80. package/templates/AGENTS.audit.md +135 -0
  81. package/templates/AUDIT_REPORT.md +155 -0
package/README.md CHANGED
@@ -9,7 +9,8 @@
9
9
  [![CI](https://github.com/Er-Sajan-PLG/universal-software-auditor/actions/workflows/ci.yml/badge.svg)](https://github.com/Er-Sajan-PLG/universal-software-auditor/actions/workflows/ci.yml)
10
10
  [![npm](https://img.shields.io/npm/v/@xenos1996/usa)](https://www.npmjs.com/package/@xenos1996/usa)
11
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
12
- [![Rules](https://img.shields.io/badge/rules-200%2B-blue)](rules/)
12
+ [![Rules](https://img.shields.io/badge/rules-280%2B-blue)](rules/)
13
+ [![CII Best Practices](https://img.shields.io/badge/CII%20Best%20Practices-in%20progress-yellow)](https://www.bestpractices.dev/en)
13
14
 
14
15
  </div>
15
16
 
@@ -57,31 +58,32 @@ into a PR, or email to a client.
57
58
  ```markdown
58
59
  ## 📊 Executive Summary
59
60
 
60
- ### Overall Health Score: 71.4/100
61
+ ### Overall Health Score: **45.2/100**
61
62
 
62
- `█████████████████░░░░░░░`
63
+ `███████████░░░░░░░░░░░░░`
63
64
 
64
- Expected band for Beta / Growing: 60–85within the expected band 👍
65
+ Verified automatically: **65.8%** of applicable checks. 27 checks need a human see the judgement queue.
66
+
67
+ Expected band for **Prototype / Spike**: 30–65 — **within the expected band** 👍
65
68
 
66
69
  | Dimension | Score | Confidence |
67
70
  | ------------------------------------ | ------ | ---------- |
68
- | S1 · Repository & Project Structure | 8.4/10 | 72.7% |
69
- | S2 · Security | 6.2/10 | 100% |
70
- | S3 · Supply Chain & Build Provenance | 5.1/10 | 100% |
71
- | S7 · Testing & Quality Assurance | 7.5/10 | 80% |
71
+ | S1 · Repository & Project Structure | 7.2/10 | 83.3% |
72
+ | S2 · Security | 4.9/10 | 92.9% |
73
+ | S3 · Supply Chain & Build Provenance | 0/10 | 100% |
74
+ | S5 · Code Quality | 3.5/10 | 87.5% |
75
+ | S7 · Testing & Quality Assurance | 0/10 † | 33.3% |
72
76
 
73
77
  ## 🚨 Immediate Action Required
74
78
 
75
79
  1. 🔴 **No hardcoded credentials in source** · `SEC-001`
76
80
  - **Where:** `src/config.js:3`, `src/config.js:4`
77
- - **Why:** 2 occurrence(s): `api_key: 'example_key_not_real_12345',` at src/config.js:3 (+1 more)
78
- - **Fix:** Move to environment variables backed by a secret manager
79
-
80
- ## 🧠 Judgement Queue (agent / human review)
81
-
82
- | Rule | Section | Severity | What to look for | Evidence to record |
83
- | ----------------------------------------------- | ------- | -------- | ---------------- | -------------------------------- |
84
- | `SEC-015` Authorization is enforced per request | S2 | 🟠 HIGH | … | file:line of the ownership check |
81
+ - **Why:** 2 occurrence(s): `api_key: 'example_key_not_real_12345',` at src/config.js:3 (+1 more).
82
+ - **Fix:** Move to environment variables backed by a secret manager; rotate anything that was ever committed.
83
+ 2. 🔴 **SQL is not built by string concatenation** · `SEC-005`
84
+ - **Where:** `src/db.js:15`
85
+ - **Why:** 1 instance(s): `'SELECT * FROM orders WHERE id = ' + orderId,` at src/db.js:15.
86
+ - **Fix:** Use parameterised queries ($1, ?, :name) or the ORM query API.
85
87
  ```
86
88
 
87
89
  That excerpt is real output, generated by running USA against
@@ -100,7 +102,7 @@ See the full report: [`examples/sample-report.md`](examples/sample-report.md).
100
102
 
101
103
  ## 🔍 How it adapts
102
104
 
103
- **1 · Detect.** ~200 declarative signals in [`rules/detectors.yaml`](rules/detectors.yaml)
105
+ **1 · Detect.** <!-- usa:fact detectors-approx -->~230<!-- /usa:fact --> declarative signals in [`rules/detectors.yaml`](rules/detectors.yaml)
104
106
  produce facts about your project — language, framework, package manager, platform,
105
107
  database, auth, CI, AI stack, and more.
106
108
 
@@ -175,28 +177,32 @@ Override it: `usa audit . --profile production` — the "what would it take to s
175
177
 
176
178
  ## 📦 What it checks
177
179
 
178
- **16 sections**, S1–S16, in [`USA.md`](USA.md) — the human/agent-facing document.
180
+ **<!-- usa:fact sections -->16<!-- /usa:fact --> sections**, S1–S16, in [`USA.md`](USA.md) — the human/agent-facing document.
179
181
 
180
- **200+ rules** in [`rules/`](rules) — the machine-facing ones:
182
+ **<!-- usa:fact rules-floor -->280+<!-- /usa:fact --> rules** in [`rules/`](rules) — the machine-facing ones:
183
+
184
+ <!-- usa:begin rules-tree -->
181
185
 
182
186
  ```
183
187
  rules/
184
188
  ├── index.yaml pack registry
185
- ├── detectors.yaml ~200 detection signals → facts
186
- ├── sections.yaml section weights (override the defaults)
189
+ ├── detectors.yaml ~230 detection signals → facts
187
190
  ├── profiles/maturity.yaml the five lifecycle profiles
188
- ├── core/ 11 universal packs
191
+ ├── core/ 13 universal packs
189
192
  │ ├── repo.yaml ├── security.yaml ├── supply-chain.yaml
190
193
  │ ├── architecture.yaml ├── code-quality.yaml ├── testing.yaml
191
194
  │ ├── cicd.yaml ├── release.yaml ├── dependencies.yaml
192
195
  │ ├── documentation.yaml └── future-readiness.yaml
193
- └── stacks/ 16 conditional packs
196
+ └── stacks/ 17 conditional packs
194
197
  ├── node-typescript ├── python ├── go ├── rust
195
198
  ├── jvm ├── web-frontend├── mobile ├── containers
196
199
  ├── iac ├── solidity ├── ml-ai ├── cli
197
- ├── data ├── api-backend ├── compliance └── ai-era
200
+ ├── data ├── api-backend ├── compliance ├── ai-era
201
+ └── swift
198
202
  ```
199
203
 
204
+ <!-- usa:end rules-tree -->
205
+
200
206
  ### What USA adds beyond a conventional audit checklist
201
207
 
202
208
  | Addition | Why |
@@ -238,7 +244,7 @@ Ready-made packs:
238
244
 
239
245
  ## 🔧 Configuration
240
246
 
241
- `usa init` scaffolds `.usa.yaml`:
247
+ `usa init` scaffolds `.usa.yaml` (all entries commented — uncomment what you need):
242
248
 
243
249
  ```yaml
244
250
  version: 1
@@ -246,14 +252,14 @@ version: 1
246
252
  # include: [stacks/solidity] # force a pack on
247
253
  # exclude: [stacks/mobile] # force a pack off
248
254
  rules:
249
- DOC-003:
250
- severity: LOW
251
- reason: 'Docs live in Notion (decision: ADR-014)'
255
+ # DOC-003:
256
+ # severity: LOW
257
+ # reason: "Docs live in Notion, not the repo (decision: ADR-014)"
252
258
  suppressions:
253
- - rule: PERF-005
254
- reason: 'Known N+1 in the admin panel; 40 rows max.'
255
- until: '2026-12-31'
256
- ignore: [] # extra globs to exclude from indexing
259
+ # - rule: PERF-005
260
+ # reason: "Known N+1 in the admin panel; 40 rows max. Revisit Q4."
261
+ # until: "2026-12-31"
262
+ ignore: []
257
263
  facts: [] # assert facts detection missed, e.g. "has:database"
258
264
  ```
259
265
 
@@ -296,9 +302,14 @@ Add to any workflow ([`action.yml`](action.yml) is a composite action if you pre
296
302
  | [`docs/detectors.md`](docs/detectors.md) | The fact catalogue and how to add signals |
297
303
  | [`docs/maturity-profiles.md`](docs/maturity-profiles.md) | Lifecycle stages and dampening rules |
298
304
  | [`docs/agent-integration.md`](docs/agent-integration.md) | Driving USA from Claude/Cursor/Codex/Copilot |
299
- | [`docs/ci-integration.md`](docs/ci-integration.md) | GitHub Actions, GitLab CI, quality gates |
305
+ | [`docs/ci-integration.md`](docs/ci-integration.md) | GitHub Actions, GitLab CI, quality gates, SARIF/JSON output |
300
306
  | [`docs/standards-mapping.md`](docs/standards-mapping.md) | USA ↔ ASVS 5.0, SSDF, SLSA, Scorecard, ISO 5055, WCAG, CRA |
301
307
  | [`docs/configuration.md`](docs/configuration.md) | `.usa.yaml` reference |
308
+ | [`docs/EVOLUTION.md`](docs/EVOLUTION.md) | The deterministic self-extension loop |
309
+ | [`docs/reference/api.md`](docs/reference/api.md) | Programmatic API: `runAudit`, renderers, scoring, diffing, evolution |
310
+
311
+ The full index — including architecture, ADRs, and the CLI reference — is
312
+ [`docs/README.md`](docs/README.md).
302
313
 
303
314
  ---
304
315
 
package/USA.md CHANGED
@@ -18,13 +18,13 @@ usa audit . # 1. deterministic pass → AUDIT.md
18
18
 
19
19
  ## 0 · How the agent uses this template
20
20
 
21
- | Step | Action | Output |
22
- | ---- | --------------------------------------------------------- | ------------------------------ |
23
- | 1 | Run `usa detect <path>` (or read `rules/detectors.yaml`) | The project's **facts** |
24
- | 2 | Match facts against `applies_when` in `rules/` | The **applicable rule set** |
25
- | 3 | Run `usa audit <path>` | Deterministic findings + score |
26
- | 4 | Work the **Judgement Queue** in the generated report | Evidence-backed findings |
27
- | 5 | Emit the [Section 14 report](#14--report-output-template) | The deliverable |
21
+ | Step | Action | Output |
22
+ | ---- | ---------------------------------------------------------- | ------------------------------ |
23
+ | 1 | Run `usa detect <path>` (or read `rules/detectors.yaml`) | The project's **facts** |
24
+ | 2 | Match facts against `applies_when` in `rules/` | The **applicable rule set** |
25
+ | 3 | Run `usa audit <path>` | Deterministic findings + score |
26
+ | 4 | Work the **Judgement Queue** in the generated report | Evidence-backed findings |
27
+ | 5 | Emit the [report output template](#report-output-template) | The deliverable |
28
28
 
29
29
  **Ten behaviour rules.** They are not stylistic; each one prevents a specific failure mode.
30
30
 
@@ -121,7 +121,7 @@ Sections are activated by detection, not by the auditor's attention span. `S15
121
121
  | **S15** | Platform-Specific | per stack: Node, Python, Go, Rust, JVM, Web, Mobile, Containers, IaC, Solidity, ML, CLI, API | 1.0 |
122
122
  | **S16** | Future Readiness | always | 0.4 |
123
123
 
124
- The machine-readable version of every section lives in [`rules/core/`](rules/core), [`rules/stacks/`](rules/stacks), and [`rules/sections.yaml`](rules/sections.yaml).
124
+ The machine-readable rules live in [`rules/core/`](rules/core) and [`rules/stacks/`](rules/stacks). Section titles and weights are defined in [`src/engine/sections.ts`](src/engine/sections.ts); override them per-repo with an optional `sections.yaml` in your rules directory (`$rulesDir/sections.yaml`).
125
125
 
126
126
  ### What USA added to the classic 14-section audit
127
127
 
@@ -472,7 +472,7 @@ All 🔵 `FUTURE` — these are plans, not defects, and they never block a relea
472
472
 
473
473
  ---
474
474
 
475
- ## 14 · Report Output Template
475
+ ## Report output template
476
476
 
477
477
  > The `usa` CLI emits exactly this structure. If you are an agent writing it by hand, match it — a report that looks the same every time is a report you can diff.
478
478
 
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAmGA,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CA6B3C;AA6nBD,iBAAS,YAAY,CACnB,KAAK,GAAE,MAAM,GAAG,SAA2B,EAC3C,OAAO,GAAE,MAAwB,GAChC,OAAO,CAOT;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAgHA,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CA6B3C;AA+zBD,iBAAS,YAAY,CACnB,KAAK,GAAE,MAAM,GAAG,SAA2B,EAC3C,OAAO,GAAE,MAAwB,GAChC,OAAO,CAOT;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -8,14 +8,19 @@ import { loadDetectorFile } from './detect/index.js';
8
8
  import { Project } from './util/project.js';
9
9
  import { detect } from './detect/index.js';
10
10
  import { renderMarkdown, parseTrailer } from './report/markdown.js';
11
+ import { parseDetachedSignature, verifyDetachedSignature, } from './report/signature.js';
12
+ import { renderJson } from './report/json.js';
13
+ import { renderSarif } from './report/sarif.js';
11
14
  import { diffReports } from './engine/diff.js';
12
15
  import { bootstrapPacks, writeBootstrapPacks } from './bootstrap/index.js';
13
16
  import { EXAMPLE_CONFIG, loadConfig } from './config.js';
14
- import { evaluateGate } from './engine/gate.js';
17
+ import { evaluateGate, evaluateNewCodeGate, parseBaselineTrailer } from './engine/gate.js';
15
18
  import { learnFromReport, renderSuggestions } from './learn/index.js';
16
19
  import { Store } from './store/index.js';
17
20
  import { runEvolutionCycle } from './evolution/run.js';
18
21
  import { capabilityFromPack } from './evolution/capability.js';
22
+ import { ruleAutomatability } from './engine/automatability.js';
23
+ import { catalogueCoverage, catalogueOf, loadCatalogues } from './engine/catalogues.js';
19
24
  const HERE = path.dirname(fileURLToPath(import.meta.url));
20
25
  const DEFAULT_RULES_DIR = path.resolve(HERE, '..', 'rules');
21
26
  const VERSION = readVersion();
@@ -72,10 +77,12 @@ const COMMANDS = {
72
77
  rules: cmdRules,
73
78
  explain: cmdExplain,
74
79
  diff: cmdDiff,
80
+ 'verify-report': cmdVerifyReport,
75
81
  init: cmdInit,
76
82
  bootstrap: cmdBootstrap,
77
83
  learn: cmdLearn,
78
84
  evolve: cmdEvolve,
85
+ standards: cmdStandards,
79
86
  };
80
87
  export function main(argv) {
81
88
  const args = parseArgs(argv);
@@ -107,6 +114,18 @@ export function main(argv) {
107
114
  return 2;
108
115
  }
109
116
  }
117
+ const FORMATS = ['md', 'json', 'sarif'];
118
+ /** Resolves the report format from an explicit flag, else the --out suffix. */
119
+ function resolveFormat(flag, out) {
120
+ if (flag)
121
+ return flag.toLowerCase();
122
+ const ext = path.extname(out).toLowerCase();
123
+ if (ext === '.json')
124
+ return 'json';
125
+ if (ext === '.sarif')
126
+ return 'sarif';
127
+ return 'md';
128
+ }
110
129
  function parseCountFlag(v) {
111
130
  if (v === undefined)
112
131
  return undefined;
@@ -114,13 +133,16 @@ function parseCountFlag(v) {
114
133
  return Number.isFinite(n) ? n : NaN;
115
134
  }
116
135
  function readAuditOptions(args) {
136
+ const out = str(args, 'out', 'AUDIT.md') ?? 'AUDIT.md';
117
137
  return {
118
138
  target: args._[1] ?? '.',
119
139
  rulesDir: path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR),
120
140
  depth: str(args, 'depth', 'standard') ?? 'standard',
121
141
  profileArg: str(args, 'profile', 'auto') ?? 'auto',
122
- out: str(args, 'out', 'AUDIT.md') ?? 'AUDIT.md',
142
+ out,
143
+ format: resolveFormat(str(args, 'format'), out),
123
144
  failOn: (str(args, 'fail-on', 'none') ?? 'none').toLowerCase(),
145
+ baseline: str(args, 'baseline'),
124
146
  quiet: bool(args, 'quiet'),
125
147
  maxFiles: parseCountFlag(str(args, 'max-files')),
126
148
  maxBytes: parseCountFlag(str(args, 'max-bytes')),
@@ -132,6 +154,8 @@ function validateAuditOptions(o) {
132
154
  if (o.profileArg !== 'auto' && !MATURITIES.includes(o.profileArg)) {
133
155
  return `--profile must be auto or one of ${MATURITIES.join('|')}`;
134
156
  }
157
+ if (!FORMATS.includes(o.format))
158
+ return `--format must be one of ${FORMATS.join('|')}`;
135
159
  if (o.maxFiles !== undefined && !(o.maxFiles > 0))
136
160
  return '--max-files must be a positive number';
137
161
  if (o.maxBytes !== undefined && !(o.maxBytes > 0))
@@ -174,13 +198,61 @@ function cmdAudit(args) {
174
198
  });
175
199
  for (const w of warnings)
176
200
  console.error(`warning: ${w}`);
177
- const markdown = renderMarkdown(report, profile);
178
- fs.writeFileSync(o.out, markdown, 'utf8');
201
+ const rendered = renderReport(report, profile, o.format);
202
+ fs.writeFileSync(o.out, rendered, 'utf8');
179
203
  if (!o.quiet) {
180
204
  console.log(summaryLine(report));
181
205
  console.log(` report → ${o.out}`);
182
206
  }
183
- return evaluateGate(report, o.failOn, o.quiet);
207
+ // Without --baseline the absolute gate below is byte-identical to before:
208
+ // same function, same arguments, same exit codes.
209
+ if (!o.baseline)
210
+ return evaluateGate(report, o.failOn, o.quiet);
211
+ return runBaselineGate(report, o.baseline, o.failOn, o.quiet, args);
212
+ }
213
+ /**
214
+ * New-code gate: fail only on findings this change introduced or regressed,
215
+ * compared against a previous report's trailer. Every baseline failure mode
216
+ * exits 2 loudly — a gate that cannot read its baseline must never pass.
217
+ */
218
+ function runBaselineGate(report, baselinePath, failOn, quiet, args) {
219
+ let text;
220
+ try {
221
+ text = fs.readFileSync(baselinePath, 'utf8');
222
+ }
223
+ catch {
224
+ console.error(`--baseline file not found or unreadable: ${baselinePath}`);
225
+ return 2;
226
+ }
227
+ // Tolerate a raw YAML trailer file, mirroring `usa diff`.
228
+ const trailerYaml = parseTrailer(text) ?? text;
229
+ if (!trailerYaml.trim()) {
230
+ console.error(`--baseline has no report trailer and no YAML content: ${baselinePath}`);
231
+ return 2;
232
+ }
233
+ try {
234
+ const baseline = parseBaselineTrailer(trailerYaml);
235
+ // An explicit --fail-on (even none) wins; otherwise the new-code gate
236
+ // defaults to HIGH so legacy MEDIUM/LOW debt never blocks adoption.
237
+ const threshold = failOn === 'none' && args['baseline'] !== undefined && args['fail-on'] === undefined
238
+ ? 'high'
239
+ : failOn;
240
+ return evaluateNewCodeGate(report, baseline, threshold, quiet);
241
+ }
242
+ catch (err) {
243
+ console.error(`--baseline is malformed (${baselinePath}): ${err.message}`);
244
+ return 2;
245
+ }
246
+ }
247
+ function renderReport(report, profile, format) {
248
+ switch (format) {
249
+ case 'json':
250
+ return renderJson(report);
251
+ case 'sarif':
252
+ return renderSarif(report);
253
+ default:
254
+ return renderMarkdown(report, profile);
255
+ }
184
256
  }
185
257
  function summaryLine(report) {
186
258
  const s = report.score.severityCounts;
@@ -262,17 +334,36 @@ function cmdExplain(args) {
262
334
  }
263
335
  const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
264
336
  const { packs } = loadRulePacks(rulesDir);
337
+ const { catalogues } = loadCatalogues(rulesDir);
265
338
  for (const pack of packs) {
266
339
  const rule = pack.rules.find((r) => r.id.toLowerCase() === id.toLowerCase());
267
340
  if (!rule)
268
341
  continue;
269
- printRuleDetail(pack.id, rule);
342
+ printRuleDetail(pack.id, rule, catalogues);
270
343
  return 0;
271
344
  }
272
345
  console.error(`Rule not found: ${id}`);
273
346
  return 1;
274
347
  }
275
- function printRuleDetail(packId, rule) {
348
+ function cmdStandards(args) {
349
+ const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
350
+ const { packs } = loadRulePacks(rulesDir);
351
+ const { catalogues } = loadCatalogues(rulesDir);
352
+ const rows = catalogueCoverage(packs, catalogues);
353
+ const fmt = (str(args, 'format') ?? 'md').toLowerCase();
354
+ if (fmt === 'json') {
355
+ console.log(JSON.stringify(rows, null, 2));
356
+ return 0;
357
+ }
358
+ // Markdown table: catalogue → rules → how many the engine can fully decide.
359
+ console.log('| Catalogue | Rules | Fully automated | Assisted | Manual |');
360
+ console.log('| --------- | ----- | --------------- | -------- | ------ |');
361
+ for (const row of rows) {
362
+ console.log(`| ${row.name} (\`${row.catalogue}\`) | ${row.rules} | ${row.automatable.full} | ${row.automatable.assist} | ${row.automatable.manual} |`);
363
+ }
364
+ return 0;
365
+ }
366
+ function printRuleDetail(packId, rule, catalogues) {
276
367
  console.log(`# ${rule.id} — ${rule.title}`);
277
368
  console.log();
278
369
  console.log(`pack : ${packId}`);
@@ -281,6 +372,10 @@ function printRuleDetail(packId, rule) {
281
372
  console.log(`class : ${rule.ruleClass}`);
282
373
  console.log(`weight : ${rule.weight ?? 'default'}`);
283
374
  console.log(`depths : ${rule.depths?.join(', ') ?? 'all'}`);
375
+ console.log(`automatable: ${ruleAutomatability(rule)}`);
376
+ const catalogue = catalogueOf(rule, catalogues);
377
+ if (catalogue)
378
+ console.log(`catalogue : ${catalogue}`);
284
379
  console.log(`check : ${JSON.stringify(rule.check)}`);
285
380
  printRuleOptional(rule);
286
381
  }
@@ -321,6 +416,80 @@ function cmdDiff(args) {
321
416
  return 2;
322
417
  }
323
418
  }
419
+ /* -------------------------------------------------------- verify-report -- */
420
+ /** Reads a text input, or reports it loudly and returns null (exit 2). */
421
+ function readTextInput(file, label) {
422
+ try {
423
+ return fs.readFileSync(file, 'utf8');
424
+ }
425
+ catch {
426
+ console.error(`${label} not found or unreadable: ${file}`);
427
+ return null;
428
+ }
429
+ }
430
+ function cmdVerifyReport(args) {
431
+ const reportPath = args._[1];
432
+ const bundlePath = str(args, 'bundle');
433
+ if (!reportPath || !bundlePath) {
434
+ console.error('Usage: usa verify-report <AUDIT.md> --bundle <sidecar.sig.json> [--key <pubkey>] [--cosign-binary <path>] [--quiet]');
435
+ return 2;
436
+ }
437
+ const markdown = readTextInput(reportPath, 'Report file');
438
+ if (markdown === null)
439
+ return 2;
440
+ const sidecarRaw = readTextInput(bundlePath, 'Signature sidecar');
441
+ if (sidecarRaw === null)
442
+ return 2;
443
+ const trailerYaml = parseTrailer(markdown);
444
+ if (!trailerYaml || !trailerYaml.trim()) {
445
+ console.error(`Report has no machine-readable trailer: ${reportPath}`);
446
+ return 2;
447
+ }
448
+ let signature;
449
+ try {
450
+ signature = parseDetachedSignature(sidecarRaw);
451
+ }
452
+ catch (err) {
453
+ console.error(`Signature sidecar is malformed (${bundlePath}): ${err.message}`);
454
+ return 2;
455
+ }
456
+ // Payload bytes are exactly canonicalReportBytes(report) for the report
457
+ // that rendered this trailer: the trailer projection plus '\n'. The CLI
458
+ // only ever sees rendered bytes, so it reconstructs them from the parsed
459
+ // trailer rather than re-deriving a report object.
460
+ const payload = Buffer.from(`${trailerYaml}\n`, 'utf8');
461
+ return runSignatureVerify(args, reportPath, payload, signature);
462
+ }
463
+ /**
464
+ * Runs the cosign verification and maps the outcome onto the gate
465
+ * convention: 0 verified, 1 mismatch/failed, 2 environment refusal (an
466
+ * absent cosign binary throws — a loud error, never a fake pass).
467
+ */
468
+ function runSignatureVerify(args, reportPath, payload, signature) {
469
+ const quiet = bool(args, 'quiet');
470
+ const cosignBinary = str(args, 'cosign-binary');
471
+ let result;
472
+ try {
473
+ result = verifyDetachedSignature(payload, signature, {
474
+ publicKeyPath: str(args, 'key') ?? '',
475
+ ...(cosignBinary === undefined ? {} : { cosignBinary }),
476
+ });
477
+ }
478
+ catch (err) {
479
+ console.error(err.message);
480
+ return 2;
481
+ }
482
+ if (result.ok) {
483
+ if (!quiet)
484
+ console.log(`Signature verified: ${reportPath}`);
485
+ return 0;
486
+ }
487
+ // Gate convention: the mismatch detail goes to stderr, suppressed by
488
+ // --quiet; the exit code alone carries the verdict then.
489
+ if (!quiet)
490
+ console.error(`Signature verification failed for ${reportPath}: ${result.detail}`);
491
+ return 1;
492
+ }
324
493
  /* -------------------------------------------------------------- bootstrap -- */
325
494
  function cmdBootstrap(args) {
326
495
  const target = args._[1] ?? '.';
@@ -619,10 +788,12 @@ usa — Universal Software Auditor
619
788
  usa rules [--section S2] List all loaded rule packs and rules
620
789
  usa explain <RULE-ID> Show everything about one rule
621
790
  usa diff <before> <after> Compare two previously generated reports
791
+ usa verify-report <AUDIT.md> Verify a report's detached signature sidecar
622
792
  usa init [path] Scaffold .usa.yaml + a GitHub Actions workflow
623
793
  usa bootstrap [path] Propose rule packs for stacks USA cannot audit yet
624
794
  usa learn <report.md> Generate suggested rules from audit findings
625
795
  usa evolve [path] Run the audit → gap → candidate → release loop
796
+ usa standards Report catalogue coverage and automatability
626
797
 
627
798
  evolve options
628
799
  --store <dir> Persist audit runs/results (content-addressed store)
@@ -638,8 +809,13 @@ learn options
638
809
  --out <file> Output YAML file (default learn-suggestions.yaml)
639
810
  --min-severity <s> Minimum severity to consider (CRITICAL|HIGH|MEDIUM|LOW|FUTURE, default MEDIUM)
640
811
 
812
+ standards options
813
+ --format <fmt> md | json (default md)
814
+ --rules-dir <dir> Rule pack directory (default bundled rules/)
815
+
641
816
  audit options
642
817
  --out <file> Report path (default AUDIT.md)
818
+ --format <fmt> md | json | sarif (default: inferred from --out)
643
819
  --depth <level> quick | standard | deep (default standard)
644
820
  --profile <stage> auto | prototype | mvp | beta | production | legacy
645
821
  --rules-dir <dir> Rule pack directory (default bundled rules/)
@@ -649,6 +825,9 @@ audit options
649
825
  --fact <ns:value> Assert a fact detection missed, e.g. --fact has:database
650
826
  --allow-commands Run \`command:\` checks (shells out; off by default)
651
827
  --fail-on <sev> Exit 1 on findings >= sev: critical|high|medium|low|none
828
+ --baseline <file> New-code gate: only fail on findings new or regressed
829
+ vs this previous report (default threshold high;
830
+ explicit --fail-on overrides it)
652
831
  --quiet Only errors
653
832
  --max-files <n> Index at most n files (overrides config; default 60000)
654
833
  --max-bytes <n> Skip files larger than n bytes (overrides config; default 2 MiB)
@@ -656,12 +835,20 @@ audit options
656
835
  bootstrap options
657
836
  --out <file|dir> Write pack files instead of printing (default: print)
658
837
 
838
+ verify-report options
839
+ --bundle <file> Signature sidecar (required, e.g. AUDIT.md.sig.json)
840
+ --key <file> PEM public key the bundle is verified against
841
+ --cosign-binary <bin> Override the cosign binary (default cosign)
842
+ --quiet Only errors
843
+
659
844
  examples
660
845
  usa audit . --depth deep
661
846
  usa bootstrap ~/code/legacy-php-app --out /tmp/packs
662
847
  usa audit ../api --profile production --fail-on high
848
+ usa audit . --baseline reports/2026-08.md --fail-on high
663
849
  usa audit . --out reports/audit-$(date +%F).md
664
850
  usa learn AUDIT.md --out swift-suggestions.yaml
851
+ usa verify-report AUDIT.md --bundle AUDIT.md.sig.json --key cosign.pub
665
852
  `.trim();
666
853
  const WORKFLOW_TEMPLATE = `# USA — Universal Software Auditor
667
854
  # Runs on every PR and pushes a Markdown summary you can read in the Actions UI.