@xenos1996/usa 2.0.0 → 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.
- package/README.md +44 -33
- package/USA.md +9 -9
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +194 -7
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +31 -0
- package/dist/config.js.map +1 -1
- package/dist/engine/audit.d.ts.map +1 -1
- package/dist/engine/audit.js +42 -4
- package/dist/engine/audit.js.map +1 -1
- package/dist/engine/automatability.d.ts +32 -0
- package/dist/engine/automatability.d.ts.map +1 -0
- package/dist/engine/automatability.js +81 -0
- package/dist/engine/automatability.js.map +1 -0
- package/dist/engine/catalogues.d.ts +43 -0
- package/dist/engine/catalogues.d.ts.map +1 -0
- package/dist/engine/catalogues.js +109 -0
- package/dist/engine/catalogues.js.map +1 -0
- package/dist/engine/diff.d.ts +14 -0
- package/dist/engine/diff.d.ts.map +1 -1
- package/dist/engine/diff.js +59 -36
- package/dist/engine/diff.js.map +1 -1
- package/dist/engine/evaluate.d.ts +2 -1
- package/dist/engine/evaluate.d.ts.map +1 -1
- package/dist/engine/evaluate.js +125 -3
- package/dist/engine/evaluate.js.map +1 -1
- package/dist/engine/gate.d.ts +33 -0
- package/dist/engine/gate.d.ts.map +1 -1
- package/dist/engine/gate.js +116 -0
- package/dist/engine/gate.js.map +1 -1
- package/dist/engine/loader.d.ts.map +1 -1
- package/dist/engine/loader.js +92 -4
- package/dist/engine/loader.js.map +1 -1
- package/dist/engine/review.d.ts +49 -0
- package/dist/engine/review.d.ts.map +1 -0
- package/dist/engine/review.js +128 -0
- package/dist/engine/review.js.map +1 -0
- package/dist/engine/suppression.d.ts +52 -0
- package/dist/engine/suppression.d.ts.map +1 -0
- package/dist/engine/suppression.js +91 -0
- package/dist/engine/suppression.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/report/json.d.ts +48 -0
- package/dist/report/json.d.ts.map +1 -0
- package/dist/report/json.js +60 -0
- package/dist/report/json.js.map +1 -0
- package/dist/report/markdown.d.ts.map +1 -1
- package/dist/report/markdown.js +110 -17
- package/dist/report/markdown.js.map +1 -1
- package/dist/report/sarif.d.ts +84 -0
- package/dist/report/sarif.d.ts.map +1 -0
- package/dist/report/sarif.js +129 -0
- package/dist/report/sarif.js.map +1 -0
- package/dist/report/signature.d.ts +76 -0
- package/dist/report/signature.d.ts.map +1 -0
- package/dist/report/signature.js +206 -0
- package/dist/report/signature.js.map +1 -0
- package/dist/types.d.ts +115 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -1
- package/dist/types.js.map +1 -1
- package/dist/util/project.d.ts.map +1 -1
- package/dist/util/project.js +23 -1
- package/dist/util/project.js.map +1 -1
- package/dist/util/site.d.ts +13 -0
- package/dist/util/site.d.ts.map +1 -0
- package/dist/util/site.js +24 -0
- package/dist/util/site.js.map +1 -0
- package/package.json +9 -1
- package/rules/catalogues.yaml +109 -0
- package/rules/core/provenance-attestation.yaml +65 -0
- package/rules/core/provenance-cosign.yaml +110 -0
- package/rules/index.yaml +2 -0
- package/rules/profiles/maturity.yaml +1 -1
- package/templates/AGENTS.audit.md +135 -0
- package/templates/AUDIT_REPORT.md +155 -0
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
[](https://github.com/Er-Sajan-PLG/universal-software-auditor/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/@xenos1996/usa)
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[](rules/)
|
|
13
|
+
[](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:
|
|
61
|
+
### Overall Health Score: **45.2/100**
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
`███████████░░░░░░░░░░░░░`
|
|
63
64
|
|
|
64
|
-
|
|
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 |
|
|
69
|
-
| S2 · Security |
|
|
70
|
-
| S3 · Supply Chain & Build Provenance |
|
|
71
|
-
|
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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.**
|
|
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
|
-
|
|
180
|
+
**<!-- usa:fact sections -->16<!-- /usa:fact --> sections**, S1–S16, in [`USA.md`](USA.md) — the human/agent-facing document.
|
|
179
181
|
|
|
180
|
-
|
|
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 ~
|
|
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/
|
|
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/
|
|
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
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
ignore: []
|
|
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
|
|
22
|
-
| ---- |
|
|
23
|
-
| 1 | Run `usa detect <path>` (or read `rules/detectors.yaml`)
|
|
24
|
-
| 2 | Match facts against `applies_when` in `rules/`
|
|
25
|
-
| 3 | Run `usa audit <path>`
|
|
26
|
-
| 4 | Work the **Judgement Queue** in the generated report
|
|
27
|
-
| 5 | Emit the [
|
|
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
|
|
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
|
-
##
|
|
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":";
|
|
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
|
|
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
|
|
178
|
-
fs.writeFileSync(o.out,
|
|
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
|
-
|
|
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
|
|
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.
|