@xenos1996/usat 1.0.0

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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +335 -0
  3. package/USAT.md +596 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +438 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +72 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/detect/index.d.ts +58 -0
  13. package/dist/detect/index.d.ts.map +1 -0
  14. package/dist/detect/index.js +329 -0
  15. package/dist/detect/index.js.map +1 -0
  16. package/dist/engine/audit.d.ts +21 -0
  17. package/dist/engine/audit.d.ts.map +1 -0
  18. package/dist/engine/audit.js +295 -0
  19. package/dist/engine/audit.js.map +1 -0
  20. package/dist/engine/diff.d.ts +24 -0
  21. package/dist/engine/diff.d.ts.map +1 -0
  22. package/dist/engine/diff.js +136 -0
  23. package/dist/engine/diff.js.map +1 -0
  24. package/dist/engine/evaluate.d.ts +19 -0
  25. package/dist/engine/evaluate.d.ts.map +1 -0
  26. package/dist/engine/evaluate.js +377 -0
  27. package/dist/engine/evaluate.js.map +1 -0
  28. package/dist/engine/gate.d.ts +19 -0
  29. package/dist/engine/gate.d.ts.map +1 -0
  30. package/dist/engine/gate.js +47 -0
  31. package/dist/engine/gate.js.map +1 -0
  32. package/dist/engine/loader.d.ts +22 -0
  33. package/dist/engine/loader.d.ts.map +1 -0
  34. package/dist/engine/loader.js +360 -0
  35. package/dist/engine/loader.js.map +1 -0
  36. package/dist/engine/maturity.d.ts +20 -0
  37. package/dist/engine/maturity.d.ts.map +1 -0
  38. package/dist/engine/maturity.js +157 -0
  39. package/dist/engine/maturity.js.map +1 -0
  40. package/dist/engine/score.d.ts +19 -0
  41. package/dist/engine/score.d.ts.map +1 -0
  42. package/dist/engine/score.js +141 -0
  43. package/dist/engine/score.js.map +1 -0
  44. package/dist/engine/sections.d.ts +14 -0
  45. package/dist/engine/sections.d.ts.map +1 -0
  46. package/dist/engine/sections.js +50 -0
  47. package/dist/engine/sections.js.map +1 -0
  48. package/dist/index.d.ts +23 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +17 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/report/markdown.d.ts +16 -0
  53. package/dist/report/markdown.d.ts.map +1 -0
  54. package/dist/report/markdown.js +488 -0
  55. package/dist/report/markdown.js.map +1 -0
  56. package/dist/types.d.ts +282 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/dist/types.js +16 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/util/glob.d.ts +11 -0
  61. package/dist/util/glob.d.ts.map +1 -0
  62. package/dist/util/glob.js +86 -0
  63. package/dist/util/glob.js.map +1 -0
  64. package/dist/util/project.d.ts +72 -0
  65. package/dist/util/project.d.ts.map +1 -0
  66. package/dist/util/project.js +586 -0
  67. package/dist/util/project.js.map +1 -0
  68. package/dist/util/yaml.d.ts +28 -0
  69. package/dist/util/yaml.d.ts.map +1 -0
  70. package/dist/util/yaml.js +48 -0
  71. package/dist/util/yaml.js.map +1 -0
  72. package/docs/ARCHITECTURE.md +119 -0
  73. package/docs/README.md +27 -0
  74. package/docs/adr/0001-record-architecture-decisions.md +35 -0
  75. package/docs/adr/0002-typescript-engine-over-python.md +49 -0
  76. package/docs/adr/0003-rules-are-data-not-code.md +50 -0
  77. package/docs/adr/0004-markdown-only-output.md +41 -0
  78. package/docs/adr/0005-maturity-dampens-severity.md +56 -0
  79. package/docs/adr/0006-severity-status-two-axes.md +48 -0
  80. package/docs/adr/0007-suppressions-are-visible-and-expiring.md +46 -0
  81. package/docs/adr/0008-report-trailer-and-diff.md +48 -0
  82. package/docs/adr/0009-fail-closed-on-malformed-input.md +56 -0
  83. package/docs/adr/0010-detector-design.md +54 -0
  84. package/docs/adr/0011-coexist-with-deep-scanners.md +52 -0
  85. package/docs/adr/README.md +32 -0
  86. package/docs/agent-integration.md +129 -0
  87. package/docs/ci-integration.md +163 -0
  88. package/docs/concepts.md +196 -0
  89. package/docs/configuration.md +105 -0
  90. package/docs/detectors.md +186 -0
  91. package/docs/getting-started.md +99 -0
  92. package/docs/maturity-profiles.md +156 -0
  93. package/docs/reference/api.md +193 -0
  94. package/docs/reference/cli.md +412 -0
  95. package/docs/rule-packs.md +278 -0
  96. package/docs/standards-mapping.md +171 -0
  97. package/package.json +96 -0
  98. package/rules/core/architecture.yaml +190 -0
  99. package/rules/core/cicd.yaml +212 -0
  100. package/rules/core/code-quality.yaml +261 -0
  101. package/rules/core/dependencies.yaml +132 -0
  102. package/rules/core/documentation.yaml +119 -0
  103. package/rules/core/future-readiness.yaml +91 -0
  104. package/rules/core/release.yaml +131 -0
  105. package/rules/core/repo.yaml +303 -0
  106. package/rules/core/security.yaml +494 -0
  107. package/rules/core/supply-chain.yaml +357 -0
  108. package/rules/core/testing.yaml +260 -0
  109. package/rules/detectors.yaml +2255 -0
  110. package/rules/index.yaml +41 -0
  111. package/rules/profiles/maturity.yaml +135 -0
  112. package/rules/stacks/ai-era.yaml +193 -0
  113. package/rules/stacks/api-backend.yaml +121 -0
  114. package/rules/stacks/cli.yaml +105 -0
  115. package/rules/stacks/compliance.yaml +157 -0
  116. package/rules/stacks/containers.yaml +124 -0
  117. package/rules/stacks/data.yaml +165 -0
  118. package/rules/stacks/go.yaml +134 -0
  119. package/rules/stacks/iac.yaml +128 -0
  120. package/rules/stacks/jvm.yaml +150 -0
  121. package/rules/stacks/ml-ai.yaml +140 -0
  122. package/rules/stacks/mobile.yaml +121 -0
  123. package/rules/stacks/node-typescript.yaml +166 -0
  124. package/rules/stacks/python.yaml +175 -0
  125. package/rules/stacks/rust.yaml +129 -0
  126. package/rules/stacks/solidity.yaml +139 -0
  127. package/rules/stacks/web-frontend.yaml +159 -0
  128. package/templates/AGENTS.audit.md +135 -0
  129. package/templates/AUDIT_REPORT.md +155 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Shape helpers for decoded YAML.
3
+ *
4
+ * `yaml.parse` hands back plain JS objects, which is exactly the *wrong* thing
5
+ * to sprinkle through the engine: every downstream field access would be `any`
6
+ * and a typo in a rule pack would surface as `undefined` three modules later.
7
+ * These helpers force the parse boundary to state up front what shape it
8
+ * expects, and make the "absent" case explicit rather than implicit.
9
+ */
10
+ export function isMap(v) {
11
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
12
+ }
13
+ export function asMap(v) {
14
+ return isMap(v) ? v : {};
15
+ }
16
+ /** Present and a string. */
17
+ export function str(v) {
18
+ return typeof v === 'string' ? v : undefined;
19
+ }
20
+ /** Present and a number. */
21
+ export function num(v) {
22
+ return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
23
+ }
24
+ /** Present and a boolean. */
25
+ export function bool(v) {
26
+ return typeof v === 'boolean' ? v : undefined;
27
+ }
28
+ /** Present and an array. */
29
+ export function list(v) {
30
+ return Array.isArray(v) ? v : undefined;
31
+ }
32
+ /** Present and an array — every element stringified. */
33
+ export function strList(v) {
34
+ return Array.isArray(v) ? v.map((x) => String(x)) : undefined;
35
+ }
36
+ /** A scalar suitable for `json_path.equals` comparisons. */
37
+ export function scalar(v) {
38
+ if (v === null)
39
+ return null;
40
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
41
+ return v;
42
+ return undefined;
43
+ }
44
+ /** One entry of an array, as a mapping. */
45
+ export function maps(v) {
46
+ return Array.isArray(v) ? v.filter(isMap) : [];
47
+ }
48
+ //# sourceMappingURL=yaml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yaml.js","sourceRoot":"","sources":["../../src/util/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,MAAM,UAAU,KAAK,CAAC,CAAU;IAC9B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,CAAU;IAC9B,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3B,CAAC;AAED,4BAA4B;AAC5B,MAAM,UAAU,GAAG,CAAC,CAAU;IAC5B,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/C,CAAC;AAED,4BAA4B;AAC5B,MAAM,UAAU,GAAG,CAAC,CAAU;IAC5B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACrE,CAAC;AAED,6BAA6B;AAC7B,MAAM,UAAU,IAAI,CAAC,CAAU;IAC7B,OAAO,OAAO,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAChD,CAAC;AAED,4BAA4B;AAC5B,MAAM,UAAU,IAAI,CAAC,CAAU;IAC7B,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAAe,CAAC,CAAC,CAAC,SAAS,CAAC;AACzD,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,OAAO,CAAC,CAAU;IAChC,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAChE,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,MAAM,CAAC,CAAU;IAC/B,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC5B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC;IACvF,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,IAAI,CAAC,CAAU;IAC7B,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACjD,CAAC"}
@@ -0,0 +1,119 @@
1
+ # Architecture
2
+
3
+ USAT is a **rule interpreter**, not a linter. Nothing about any particular
4
+ language, framework, or standard is hard-coded in TypeScript. The engine knows
5
+ how to walk a directory, match a glob, grep a file, and do arithmetic on
6
+ weights. Everything _opinionated_ lives in `rules/`, as YAML.
7
+
8
+ That split is the whole design. It is why a user can delete every pack we ship,
9
+ drop in their own, and still get a working audit — and why adding support for a
10
+ new framework is a five-line YAML diff rather than a release.
11
+
12
+ ## The five stages
13
+
14
+ ```
15
+ project tree
16
+
17
+
18
+ ┌───────────┐ rules/detectors.yaml ┌──────────┐
19
+ │ Project │ ─────────────────────────▶│ Facts │ lang, pm, fw, maturity…
20
+ │ (index) │ └────┬─────┘
21
+ └───────────┘ │
22
+ │ ▼
23
+ │ ┌──────────────────────────┐
24
+ │ │ Pack selection │
25
+ │ │ pack.applies_when ⊨ facts│
26
+ │ └────────────┬─────────────┘
27
+ │ ▼
28
+ │ ┌──────────────────────────┐
29
+ │ │ Rule evaluation │
30
+ │ │ 15 check kinds → status │
31
+ │ └────────────┬─────────────┘
32
+ │ ▼
33
+ │ ┌──────────────────────────┐
34
+ └───────────────────────▶│ Maturity dampening │
35
+ │ severity × profile │
36
+ └────────────┬─────────────┘
37
+
38
+ ┌──────────────────────────┐
39
+ │ Scoring → Markdown │
40
+ └──────────────────────────┘
41
+ ```
42
+
43
+ | Module | File | Responsibility |
44
+ | ------------- | ------------------------ | ------------------------------------------------------------------------------------------------- |
45
+ | Project index | `src/util/project.ts` | One pass over the tree. Honours `.gitignore` + user ignores. Caches reads. Never greps lockfiles. |
46
+ | Glob matcher | `src/util/glob.ts` | Dependency-free glob → RegExp. Handles `**`, `{a,b}`, `[abc]`, `?`. |
47
+ | YAML shape | `src/util/yaml.ts` | Makes the parse boundary explicit, so a typo in a pack is a warning, not `undefined`. |
48
+ | Detection | `src/detect/index.ts` | Evaluates 230 detector primitives into a fact set, then classifies maturity. |
49
+ | Pack loading | `src/engine/loader.ts` | Parses + validates packs, applies user overrides. Bad packs warn, never crash. |
50
+ | Evaluation | `src/engine/evaluate.ts` | Runs one check against the index; resolves `applies_when` predicates. |
51
+ | Maturity | `src/engine/maturity.ts` | Dampens severity by lifecycle stage. CRITICAL is never dampened. |
52
+ | Scoring | `src/engine/score.ts` | Weighted credit arithmetic. Returns `null` for unverified sections. |
53
+ | Report | `src/report/markdown.ts` | Deterministic Markdown + a machine-readable YAML trailer. |
54
+ | Diff | `src/engine/diff.ts` | Compares two reports via their trailers. |
55
+
56
+ ## Why the fact system
57
+
58
+ A rule that checks something needs to know when it is relevant. The naive
59
+ approach — `if (hasFile('package.json'))` — is a decision hard-coded in the
60
+ rule, invisible to the reader and impossible to override.
61
+
62
+ USAT separates the two. Detectors produce **facts** (`lang:typescript`,
63
+ `fw:next`, `maturity:mvp`, `has:ci`). Rules declare **predicates over facts**
64
+ (`applies_when`). The consequence:
65
+
66
+ - Detection is shared. Two hundred rules can key off `lang:typescript`
67
+ without re-deriving it.
68
+ - Users can inject facts (`--fact monorepo`, or `facts:` in `.usat.yaml`)
69
+ when detection guesses wrong.
70
+ - `usat detect` prints the fact set, so a wrong audit is _debuggable_ — you
71
+ can see exactly what the engine believes before you argue with it.
72
+
73
+ ## Why severity dampening exists
74
+
75
+ A prototype with no rate limiting and a payment service with no rate limiting
76
+ are not the same finding. Most scanners report both as HIGH, which teaches
77
+ teams to ignore the scanner.
78
+
79
+ USAT keeps the _rule's_ severity (the intrinsic badness) and computes a
80
+ _reported_ severity from the project's lifecycle stage. The rule never lies
81
+ about what it found; the report just stops shouting about the wrong things
82
+ right now. CRITICAL is exempt — a hardcoded credential is a hardcoded
83
+ credential on day one.
84
+
85
+ ## Why scores can be `null`
86
+
87
+ A section the engine could not verify is not a 10 and it is not a 0. It is
88
+ _unknown_, and it is excluded from both the numerator and the denominator.
89
+ Pretending otherwise is how dashboards end up at 94/100 while 30% of the
90
+ checklist sits in a queue nobody reads. Every section therefore reports a
91
+ **confidence** figure alongside its score.
92
+
93
+ ## Performance
94
+
95
+ The tree is walked once. File contents are cached lazily and never read twice.
96
+ Grep excludes lockfiles, minified bundles, source maps, and generated files,
97
+ which is where the bytes are. A repository of 10k files audits in a couple of
98
+ seconds; the cost is `O(patterns × matching files)`, not `O(patterns × repo)`.
99
+
100
+ ## What USAT deliberately does not do
101
+
102
+ - **No plugins.** Packs are data. Data can be forked, diffed, reviewed, and
103
+ vendored; a plugin ABI cannot.
104
+ - **No network calls.** Ever. An audit must be reproducible offline.
105
+ - **No auto-fixing.** USAT reports; humans decide. (It does print the exact
106
+ remediation text, which is the part that actually helps.)
107
+ - **No daemon, no database, no config server.** One process, one tree, one
108
+ Markdown file.
109
+
110
+ ## Extension points
111
+
112
+ | To add… | You touch… |
113
+ | -------------------------------- | --------------------------------------------------------------------------------- |
114
+ | A check | any `rules/**/*.yaml` |
115
+ | A technology USAT must recognise | `rules/detectors.yaml` |
116
+ | A lifecycle profile | `rules/profiles/maturity.yaml` |
117
+ | A new check _kind_ | `src/engine/evaluate.ts` + `src/types.ts` — the only change that needs TypeScript |
118
+
119
+ See [`adr/`](adr/) for the decisions behind each of these.
package/docs/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # USAT documentation
2
+
3
+ | Doc | What's in it |
4
+ | ----------------------------------------- | ---------------------------------------------------------------------------------------- |
5
+ | [`USAT.md`](../USAT.md) | **The template itself** — all 16 sections, the severity model, the agent behaviour rules |
6
+ | [Getting started](getting-started.md) | Install, first audit, reading the report |
7
+ | [Concepts](concepts.md) | Severity × status, scoring maths, confidence, applicability |
8
+ | [Configuration](configuration.md) | `.usat.yaml` reference |
9
+ | [Rule packs](rule-packs.md) | Every check kind, with examples — start here to contribute a pack |
10
+ | [Detectors](detectors.md) | The fact catalogue and how to add signals |
11
+ | [Maturity profiles](maturity-profiles.md) | Lifecycle stages, dampening, expected bands |
12
+ | [Agent integration](agent-integration.md) | Driving USAT from Claude, Cursor, Codex, Copilot |
13
+ | [CI integration](ci-integration.md) | GitHub Actions, GitLab CI, quality gates, drift detection |
14
+ | [Standards mapping](standards-mapping.md) | USAT ↔ ASVS 5.0, SSDF, SLSA, Scorecard, ISO 5055, WCAG, CRA, OWASP LLM/ASI |
15
+ | [Architecture](ARCHITECTURE.md) | How the engine is put together, and what it deliberately does not do |
16
+ | [API reference](reference/api.md) | Programmatic use: `runAudit`, `detect`, `Project`, scoring, diffing |
17
+ | [ADRs](adr/) | The five decisions behind the design, and what each one cost |
18
+
19
+ ## Reading order
20
+
21
+ - **Auditing a project?** → [Getting started](getting-started.md) → [Concepts](concepts.md)
22
+ - **Wiring up CI?** → [CI integration](ci-integration.md)
23
+ - **Using an agent?** → [Agent integration](agent-integration.md)
24
+ - **Contributing a rule pack?** → [Rule packs](rule-packs.md) → [Detectors](detectors.md)
25
+ - **Comparing to a standard?** → [Standards mapping](standards-mapping.md)
26
+ - **Embedding USAT in a tool?** → [API reference](reference/api.md)
27
+ - **Wondering why it is built this way?** → [Architecture](ARCHITECTURE.md) → [ADRs](adr/)
@@ -0,0 +1,35 @@
1
+ # 1. Record architectural decisions
2
+
3
+ - **Date:** 2026-09-08
4
+ - **Status:** Accepted
5
+
6
+ ## Context
7
+
8
+ USAT is a template that other people will fork and extend. Every non-obvious
9
+ choice in it will be questioned by a contributor who was not in the room: "why
10
+ is the engine TypeScript instead of Python?", "why is everything YAML instead of
11
+ code?", "why is the output only Markdown?"
12
+
13
+ Those questions get asked in issues, answered from memory, and then re-asked six
14
+ months later by the next person.
15
+
16
+ ## Decision
17
+
18
+ Architecture decisions are recorded as short numbered files in `docs/adr/`,
19
+ following the [Nygard
20
+ format](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions):
21
+ context, decision, consequences. They are immutable — a decision that changes
22
+ gets a new ADR that supersedes the old one, rather than an edit.
23
+
24
+ ## Consequences
25
+
26
+ **Good:** the reasoning survives the people; disagreeing with a decision becomes
27
+ a reviewable act rather than an archaeology project; USAT's own `ARCH-002` rule
28
+ (ADRs exist) now passes on itself, which is the only honest way to ship a rule.
29
+
30
+ **Bad:** ADRs rot. An ADR whose context no longer holds is worse than no ADR,
31
+ because it lends false authority to a stale choice. Mitigation: each file carries
32
+ a status, and superseded ones are kept, not deleted.
33
+
34
+ **Neutral:** three ADRs is a start, not a corpus. The convention matters more
35
+ than the count.
@@ -0,0 +1,49 @@
1
+ # 2. Build the engine in TypeScript, not Python
2
+
3
+ - **Date:** 2026-09-08
4
+ - **Status:** Accepted
5
+ - **Supersedes:** an earlier `src/auditor.py` prototype (deleted)
6
+
7
+ ## Context
8
+
9
+ USAT audits software projects. Most of those projects are JavaScript or
10
+ TypeScript, and the two natural candidate languages for the tool were:
11
+
12
+ - **Python**, the traditional home of linters and security tooling
13
+ (Bandit, Semgrep's original core, most scanners).
14
+ - **TypeScript**, which can run in the same process and on the same runtime as
15
+ the projects it audits.
16
+
17
+ The repository already contained a bare-minimum `src/index.ts` stub when this
18
+ work started, which biased towards TypeScript but did not decide it.
19
+
20
+ ## Decision
21
+
22
+ Build the engine in TypeScript, published as an npm package, distributed as a
23
+ GitHub Action.
24
+
25
+ ## Rationale
26
+
27
+ 1. **The audience is the runtime.** Anyone auditing a Node project already has
28
+ Node. `npx usat audit .` requires no interpreter negotiation, no virtualenv,
29
+ no `pip install --user` foot-gun.
30
+ 2. **TypeScript's type system pays for itself here.** The engine is a
31
+ tree-walker with fifteen check kinds and a scoring formula; those are exactly
32
+ the shapes where a compiler catches the "renamed a field, missed a call site"
33
+ class of bug.
34
+ 3. **GitHub Actions runs Node natively.** A TypeScript action starts in
35
+ milliseconds; a Python action pays interpreter setup on every run.
36
+
37
+ ## Consequences
38
+
39
+ **Good:** zero-install path for the largest audience; `tsc` catches whole bug
40
+ classes; the Action is fast; the tool audits itself with `npm run self-audit`.
41
+
42
+ **Bad:** Python, Go, Rust, and Java projects auditing themselves now need Node
43
+ present, even though the tool has nothing to do with Node. This is a real cost
44
+ and the reason the Action exists — CI brings the runtime so the user does not
45
+ have to. The check _contents_ are language-agnostic (globs and regexes), so a
46
+ future second engine would reuse all 27 rule packs unchanged.
47
+
48
+ **Neutral:** `pyproject.toml` and `src/auditor.py` were deleted; the CONTRIBUTING
49
+ document was rewritten to match.
@@ -0,0 +1,50 @@
1
+ # 3. Rule packs are YAML data, not TypeScript code
2
+
3
+ - **Date:** 2026-09-08
4
+ - **Status:** Accepted
5
+
6
+ ## Context
7
+
8
+ The alternative design was a plugin API: each rule pack is a TypeScript module
9
+ exporting checks, loaded dynamically. That is what Semgrep started with, what
10
+ ESLint does, and what most mature linters converge on — because a rule that can
11
+ run arbitrary code can express anything.
12
+
13
+ ## Decision
14
+
15
+ A rule pack is a **YAML file**. The engine supplies exactly fifteen check kinds
16
+ (`file_exists`, `grep_present`, `json_path`, `count_min`, `manual`, …). A pack
17
+ declares what to look for; the engine does the looking. `manual` is the escape
18
+ hatch for anything a regex cannot honestly decide.
19
+
20
+ ## Rationale
21
+
22
+ 1. **Forkability is the product.** USAT is a _template_. A user who wants their
23
+ company's engineering standard should be able to fork `rules/`, delete what
24
+ they disagree with, and commit. Data can be diffed, reviewed, vendored, and
25
+ disagreed with in a pull request. A plugin ABI can only be _used_.
26
+ 2. **Rules get read by non-authors.** A security lead who will never open
27
+ `src/` can still read `rules/core/security.yaml` and say "that's wrong, and
28
+ here's why."
29
+ 3. **No sandboxing problem.** Running third-party rule code is running
30
+ third-party code. Data cannot exfiltrate your source tree.
31
+ 4. **The audit is reproducible.** Rules are declarative, so the same tree always
32
+ produces the same findings — no rule can depend on the date, the network, or
33
+ a cache.
34
+
35
+ ## Consequences
36
+
37
+ **Good:** packs are auditable by the people who own the standard; adding a
38
+ framework is a five-line diff; no sandboxing; deterministic output; the same
39
+ packs could drive a second engine in another language.
40
+
41
+ **Bad:** the fifteen check kinds are a ceiling. Anything needing real parsing —
42
+ "this SQL string is built by concatenation" — cannot be expressed, and lands in
43
+ the `manual` queue instead. That is deliberate (a scanner that guesses at taint
44
+ analysis produces confident nonsense), but it caps automation coverage at roughly
45
+ 70–80%, which is why the report shows a confidence figure instead of pretending
46
+ otherwise.
47
+
48
+ **Neutral:** adding a sixteenth check kind requires a TypeScript change. This has
49
+ happened zero times since the initial design, which suggests the set is close to
50
+ sufficient.
@@ -0,0 +1,41 @@
1
+ # 4. Markdown is the only output format
2
+
3
+ - **Date:** 2026-09-08
4
+ - **Status:** Accepted
5
+
6
+ ## Context
7
+
8
+ Audit tooling usually emits JSON (for machines), SARIF (for GitHub code
9
+ scanning), HTML (for executives), and JUnit XML (for CI). Shipping all four is
10
+ the conventional move and roughly quadruples the reporter surface.
11
+
12
+ ## Decision
13
+
14
+ USAT renders **Markdown only**.
15
+
16
+ Two things make this sufficient:
17
+
18
+ 1. **Every report ends with a machine-readable YAML trailer** inside an HTML
19
+ comment (`<!-- USAT:TRAILER:BEGIN -->`). `usat diff` parses it to compare two
20
+ runs; CI thresholds read it; a user who wants JSON can extract it in one line.
21
+ The machine interface exists — it just lives inside the human document rather
22
+ than beside it.
23
+ 2. **Markdown is the native format of the places audits are read.** GitHub
24
+ renders it in a pull request comment, a gist, an issue, or a repo file. No
25
+ hosting, no build step, no stylesheet.
26
+
27
+ ## Consequences
28
+
29
+ **Good:** one renderer, one set of golden tests, no format skew. The report
30
+ committed to a repo stays readable forever without a viewer. `usat diff old.md
31
+ new.md` works on any two reports ever generated.
32
+
33
+ **Bad:** no native SARIF, so USAT findings do not appear in GitHub's **Security**
34
+ tab as code-scanning alerts. For teams whose compliance workflow centres on the
35
+ code-scanning API this is a genuine gap. The workaround is the PR comment plus
36
+ `--fail-on`, which covers the enforcement case but not the dashboard case.
37
+
38
+ **Neutral:** if SARIF demand materialises, it is a ~100-line addition fed from
39
+ the same `AuditReport` object — the trailer already carries everything SARIF
40
+ needs, so no engine change would be required. Revisit if three or more issues
41
+ ask for it.
@@ -0,0 +1,56 @@
1
+ # 5. Lifecycle stage dampens severity; CRITICAL is exempt
2
+
3
+ - **Date:** 2026-09-08
4
+ - **Status:** Accepted
5
+
6
+ ## Context
7
+
8
+ Static analysis has a credibility problem. A 200-line weekend prototype and a
9
+ payment service are handed the same list of HIGH findings, the prototype's
10
+ authors correctly conclude that the tool does not understand their situation,
11
+ and they learn to ignore it — right up until the year the tool would have been
12
+ right.
13
+
14
+ Severity that ignores context is not severity. It is a list of things that
15
+ _could_ be wrong.
16
+
17
+ ## Decision
18
+
19
+ USAT detects a lifecycle stage — `prototype`, `mvp`, `beta`, `production`,
20
+ `legacy` — from repository signals (history, tags, CI, tests, security policy,
21
+ contributing guide, containerisation, monitoring).
22
+
23
+ Each stage has a **dampening table**: for each rule class (security,
24
+ correctness, maintainability, documentation, …) a number of severity rungs to
25
+ step down. `production` steps down nothing; `prototype` steps documentation down
26
+ two rungs and security down one.
27
+
28
+ **CRITICAL is never dampened, at any stage.** A committed credential, a
29
+ publicly-exposed secret, and an unauthenticated destructive endpoint are
30
+ emergencies on day one of a prototype.
31
+
32
+ ## Rationale
33
+
34
+ The goal is not to be _nicer_ to prototypes. It is to keep the top of the
35
+ findings list true. If the first five things a founder reads are all things
36
+ they would defend, the scanner has lost them.
37
+
38
+ ## Consequences
39
+
40
+ **Good:** a prototype's report leads with "no rate limiting, no auth on the
41
+ admin route", not "no ADRs and your README lacks a badge". Findings stay
42
+ ordered by what matters _now_. The dampening is visible in every finding, so
43
+ nothing is hidden.
44
+
45
+ **Bad:** a user in a hurry reads a MEDIUM where the rule says HIGH and
46
+ under-reacts. Mitigated by `--profile production`, which turns dampening off
47
+ entirely, and by `usat explain <rule>`, which always shows the rule's intrinsic
48
+ severity.
49
+
50
+ **Bad:** mis-detected stage mis-dampens everything. Because of this the stage is
51
+ always overridable (`--profile`, or `maturity:` in `.usat.yaml`) and always
52
+ printed with the signals that produced it, so a wrong guess is visible rather
53
+ than silent.
54
+
55
+ **Neutral:** the band table (prototype 30–65 … production 75–95) is a heuristic,
56
+ not a law. Scoring _above_ your band is not a goal.
@@ -0,0 +1,48 @@
1
+ # 6. Severity and status are separate axes; WRONG outranks MISSING in credit
2
+
3
+ - **Date:** 2026-09-09
4
+ - **Status:** Accepted
5
+
6
+ ## Context
7
+
8
+ Most checklists conflate two questions: "how bad is this?" and "what did we
9
+ find?". A single pass/fail bit cannot distinguish "auth is missing" from
10
+ "auth is present but checks the wrong field" — yet those demand different
11
+ responses, and an auditor that cannot tell them apart teaches teams that a
12
+ decorative control scores the same as a working one.
13
+
14
+ Worse, binary scoring rewards box-ticking: a `passwords-hashed: true` flag in
15
+ a config file earns full marks whether the hasher is argon2id or ROT13.
16
+
17
+ ## Decision
18
+
19
+ Every rule carries a **severity** (set by the rule author: how bad, if
20
+ violated) and every finding carries a **status** (set by the evidence: what
21
+ was actually observed). Score credit comes from status:
22
+
23
+ | Status | Credit | Meaning |
24
+ | -------------- | ------ | ----------------------------------- |
25
+ | GOOD | 1.00 | Verified present and correct |
26
+ | EXPERIMENTAL | 0.50 | Present, unvalidated |
27
+ | DEPRECATED | 0.40 | Present, EOL |
28
+ | WRONG | 0.15 | Present but incorrectly implemented |
29
+ | MISSING / FAIL | 0.00 | Absent, or present and violated |
30
+
31
+ `WRONG` deliberately scores _above_ `MISSING` (0.15, not 0.00): something
32
+ exists, so there is partial credit — but a wrong implementation is _more
33
+ dangerous_ than nothing, because it looks finished. Surfacing always orders
34
+ `WRONG` above `MISSING` so the dangerous problem gets fixed first.
35
+
36
+ ## Consequences
37
+
38
+ **Good:** reports distinguish "you have no auth" from "your auth checks the
39
+ wrong field", and the roadmap prioritises the scarier of the two. Partial
40
+ credit keeps teams from hiding half-done work to avoid a zero.
41
+
42
+ **Bad:** two axes are harder to explain than pass/fail (see `docs/concepts.md`,
43
+ which exists largely to teach this). Any consumer that collapses status to a
44
+ boolean (a badge, a gate) must decide where WRONG lands — and document it.
45
+
46
+ **Neutral:** `EXPERIMENTAL` and `DEPRECATED` exist so the engine can describe
47
+ reality instead of rounding it to good/bad. They cost scoring-model surface
48
+ in exchange for honest reports.
@@ -0,0 +1,46 @@
1
+ # 7. Accepted risk stays visible, stops costing points, and expires
2
+
3
+ - **Date:** 2026-09-09
4
+ - **Status:** Accepted
5
+
6
+ ## Context
7
+
8
+ Every real project has findings it will not fix: a known N+1 in an admin
9
+ panel with 40 rows, docs living in Notion instead of the repo, a prototype
10
+ with no runbook. An audit tool has three options for these: keep failing the
11
+ build (teams disable the tool), silently ignore them (the audit lies), or
12
+ record the decision.
13
+
14
+ The failure mode that matters most: a waiver granted in 2024 silently
15
+ suppressing a finding in 2027, long after the context changed. An auditing
16
+ tool that honours expired risk acceptances without a murmur produces false
17
+ assurance — the one failure it must never have.
18
+
19
+ ## Decision
20
+
21
+ Suppressions live in `.usat.yaml`, each with a mandatory `reason` and an
22
+ optional `until` date:
23
+
24
+ 1. A suppressed finding is **excluded from the score and the severity
25
+ tallies** but **listed under Accepted Risk** in every report. Deliberate
26
+ decisions stay visible but stop costing points.
27
+ 2. An `until` date in the past — or one that cannot be parsed — **fails
28
+ closed**: the waiver is ignored, the finding reports normally, and a
29
+ warning names the rule. Expired risk is re-audited, not grandfathered.
30
+ 3. Suppressed findings never appear in Immediate Action or the roadmap.
31
+ The Findings Summary counts only active findings, so it cannot contradict
32
+ those sections.
33
+
34
+ ## Consequences
35
+
36
+ **Good:** the report is honest about what was decided vs what was found.
37
+ Stale waivers resurface automatically instead of rotting silently.
38
+
39
+ **Bad:** teams must maintain waiver dates to keep a green build — a small
40
+ recurring cost, and the correct one. `until` parsing follows `Date.parse`,
41
+ so only unambiguous ISO dates should be used (documented in
42
+ `docs/configuration.md`).
43
+
44
+ **Neutral:** suppressions are rule-ID-granular, not file:line-anchored.
45
+ Site-level waivers with unused-suppression reporting (the ESLint model) are
46
+ a documented future direction, not this decision.
@@ -0,0 +1,48 @@
1
+ # 8. Every report embeds a machine-readable trailer; audits are diffable
2
+
3
+ - **Date:** 2026-09-09
4
+ - **Status:** Accepted
5
+
6
+ ## Context
7
+
8
+ An audit you cannot compare to the last one is just a number. Teams need to
9
+ answer "did we get better since June?" and CI needs to gate on "did this PR
10
+ introduce a new HIGH?". The conventional answer is a second machine format
11
+ (JSON/SARIF beside the human report) plus a server to store history in.
12
+
13
+ USAT already decided Markdown is the only output format (ADR-0004). The
14
+ question was how to get trendability without a second artifact or a server.
15
+
16
+ ## Decision
17
+
18
+ Every rendered report ends with a machine-readable YAML trailer inside an
19
+ HTML comment (`<!-- USAT:TRAILER:BEGIN --> … <!-- USAT:TRAILER:END -->`):
20
+ schema version, timestamp, overall score, per-section scores, severity
21
+ totals, and every rule's status/severity/section. `usat diff old.md new.md`
22
+ parses two trailers and reports fixed, regressed, changed-still-open, and
23
+ newly-applicable rules with net point movement.
24
+
25
+ Trailer hygiene rules, learned the hard way:
26
+
27
+ 1. Rule IDs are pack-author-controlled input to a machine channel, so keys
28
+ are YAML-quoted with escaping. An unquoted id containing `:`, `#`, a
29
+ newline, or a fence once corrupted the mapping `usat diff` parses.
30
+ 2. Extraction takes the **last** trailer in the document (concatenations
31
+ yield the newest report, not the oldest) and tolerates trailing spaces,
32
+ CRLF, and `yml` vs `yaml` fences.
33
+ 3. The closing fence is located from the end, because a hostile id may
34
+ legally contain a fence inside its quoted string.
35
+
36
+ ## Consequences
37
+
38
+ **Good:** the report stays a single self-contained artifact — committable,
39
+ emailable, pasteable into a PR — that is also its own database row. Progress
40
+ reports (`usat diff Q2.md Q3.md`) work with zero infrastructure.
41
+
42
+ **Bad:** the trailer duplicates human-visible content, so reports are larger
43
+ than prose alone. Any renderer change must keep the trailer schema stable;
44
+ `schema: usat-report-v1` exists so a future v2 can be detected, not guessed.
45
+
46
+ **Neutral:** the trailer is unsigned. Anyone with write access to the report
47
+ file can edit history. Signed reports (Sigstore) are a documented future
48
+ direction for supply-chain-grade evidence, not this decision.
@@ -0,0 +1,56 @@
1
+ # 9. Malformed input fails closed and loudly, never silently
2
+
3
+ - **Date:** 2026-09-09
4
+ - **Status:** Accepted
5
+
6
+ ## Context
7
+
8
+ USAT consumes untrusted, hand-written input at three boundaries: rule packs
9
+ (third-party YAML), `.usat.yaml` project config, and `applies_when`
10
+ predicates evaluated in the audit hot loop. Each boundary once had a silent
11
+ failure mode, and every one of them corrupted results rather than stopping:
12
+
13
+ - An unknown predicate operator fell through to "present", silently
14
+ **including** rules a typo should have excluded.
15
+ - An empty grep pattern (`new RegExp('')` matches every line) turned a
16
+ malformed rule into a mass-FAIL (or free-credit PASS) score weapon.
17
+ - An invalid `matches` regex threw out of the hot loop and aborted audits.
18
+ - An invalid severity/weight override produced `NaN` overall scores.
19
+ - A corrupt pack or index YAML crashed the run with a stack trace.
20
+
21
+ For an auditing tool, silent corruption is strictly worse than a loud stop:
22
+ a crash is visible; a wrong score ships.
23
+
24
+ ## Decision
25
+
26
+ 1. **Evaluation fails closed.** Unknown predicate operators, unknown
27
+ predicate shapes, non-object predicates, and uncompilable `matches`
28
+ regexes all evaluate to `false` (rule skipped). No-constraint (`undefined`
29
+ or `{}`) still means "applies always".
30
+ 2. **Loading warns.** Every fail-closed case above, plus duplicate rule IDs
31
+ across packs, unsupported check kinds, missing check kinds, empty grep
32
+ patterns, invalid severity/weight overrides, expired or undated
33
+ suppressions, and unknown config sections, produce a named warning on
34
+ stderr. The CLI prints all warnings; the audit continues with the valid
35
+ subset.
36
+ 3. **One corrupt pack never aborts the audit.** Pack-level YAML errors skip
37
+ that pack with a warning. A corrupt rule index warns and loads zero packs
38
+ rather than throwing — with the warning explaining why the report is
39
+ empty.
40
+ 4. **Config overrides are validated.** Severity must be on the ladder;
41
+ weight must be a finite number ≥ 0. Anything else is ignored with a
42
+ warning naming the rule.
43
+
44
+ ## Consequences
45
+
46
+ **Good:** typos and corruption degrade to visible warnings plus conservative
47
+ verdicts, never to wrong scores or stack traces. Each rule documents the
48
+ invariant in a comment at the decision point.
49
+
50
+ **Bad:** fail-closed can _hide_ findings when packs are malformed (a skipped
51
+ rule finds nothing). The warnings channel is load-bearing — CI that swallows
52
+ stderr loses the signal. The self-audit e2e test ("audits itself without
53
+ warnings") exists to keep the shipped packs warning-free.
54
+
55
+ **Neutral:** this ADR does not cover adversarial rule IDs in output channels
56
+ — see ADR-0008 for trailer quoting.