@quantakrypto/core 0.1.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 (102) hide show
  1. package/README.md +337 -0
  2. package/dist/baseline.d.ts +37 -0
  3. package/dist/baseline.d.ts.map +1 -0
  4. package/dist/baseline.js +99 -0
  5. package/dist/baseline.js.map +1 -0
  6. package/dist/cbom.d.ts +25 -0
  7. package/dist/cbom.d.ts.map +1 -0
  8. package/dist/cbom.js +131 -0
  9. package/dist/cbom.js.map +1 -0
  10. package/dist/changed.d.ts +13 -0
  11. package/dist/changed.d.ts.map +1 -0
  12. package/dist/changed.js +67 -0
  13. package/dist/changed.js.map +1 -0
  14. package/dist/config.d.ts +55 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +232 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/cwe.d.ts +16 -0
  19. package/dist/cwe.d.ts.map +1 -0
  20. package/dist/cwe.js +16 -0
  21. package/dist/cwe.js.map +1 -0
  22. package/dist/dependencies.d.ts +25 -0
  23. package/dist/dependencies.d.ts.map +1 -0
  24. package/dist/dependencies.js +276 -0
  25. package/dist/dependencies.js.map +1 -0
  26. package/dist/detect-utils.d.ts +63 -0
  27. package/dist/detect-utils.d.ts.map +1 -0
  28. package/dist/detect-utils.js +113 -0
  29. package/dist/detect-utils.js.map +1 -0
  30. package/dist/detectors/pem.d.ts +9 -0
  31. package/dist/detectors/pem.d.ts.map +1 -0
  32. package/dist/detectors/pem.js +137 -0
  33. package/dist/detectors/pem.js.map +1 -0
  34. package/dist/detectors/source.d.ts +25 -0
  35. package/dist/detectors/source.d.ts.map +1 -0
  36. package/dist/detectors/source.js +575 -0
  37. package/dist/detectors/source.js.map +1 -0
  38. package/dist/index.d.ts +31 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +39 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/inventory.d.ts +15 -0
  43. package/dist/inventory.d.ts.map +1 -0
  44. package/dist/inventory.js +74 -0
  45. package/dist/inventory.js.map +1 -0
  46. package/dist/parallel.d.ts +34 -0
  47. package/dist/parallel.d.ts.map +1 -0
  48. package/dist/parallel.js +237 -0
  49. package/dist/parallel.js.map +1 -0
  50. package/dist/registry.d.ts +42 -0
  51. package/dist/registry.d.ts.map +1 -0
  52. package/dist/registry.js +51 -0
  53. package/dist/registry.js.map +1 -0
  54. package/dist/remediation.d.ts +42 -0
  55. package/dist/remediation.d.ts.map +1 -0
  56. package/dist/remediation.js +131 -0
  57. package/dist/remediation.js.map +1 -0
  58. package/dist/report.d.ts +24 -0
  59. package/dist/report.d.ts.map +1 -0
  60. package/dist/report.js +286 -0
  61. package/dist/report.js.map +1 -0
  62. package/dist/scan-worker.d.ts +2 -0
  63. package/dist/scan-worker.d.ts.map +1 -0
  64. package/dist/scan-worker.js +63 -0
  65. package/dist/scan-worker.js.map +1 -0
  66. package/dist/scan.d.ts +27 -0
  67. package/dist/scan.d.ts.map +1 -0
  68. package/dist/scan.js +168 -0
  69. package/dist/scan.js.map +1 -0
  70. package/dist/types.d.ts +190 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +9 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/version.d.ts +7 -0
  75. package/dist/version.d.ts.map +1 -0
  76. package/dist/version.js +7 -0
  77. package/dist/version.js.map +1 -0
  78. package/dist/walk.d.ts +37 -0
  79. package/dist/walk.d.ts.map +1 -0
  80. package/dist/walk.js +260 -0
  81. package/dist/walk.js.map +1 -0
  82. package/package.json +40 -0
  83. package/src/baseline.ts +111 -0
  84. package/src/cbom.ts +166 -0
  85. package/src/changed.ts +76 -0
  86. package/src/config.ts +295 -0
  87. package/src/cwe.ts +20 -0
  88. package/src/dependencies.ts +299 -0
  89. package/src/detect-utils.ts +157 -0
  90. package/src/detectors/pem.ts +162 -0
  91. package/src/detectors/source.ts +733 -0
  92. package/src/index.ts +79 -0
  93. package/src/inventory.ts +94 -0
  94. package/src/parallel.ts +288 -0
  95. package/src/registry.ts +71 -0
  96. package/src/remediation.ts +173 -0
  97. package/src/report.ts +340 -0
  98. package/src/scan-worker.ts +80 -0
  99. package/src/scan.ts +187 -0
  100. package/src/types.ts +224 -0
  101. package/src/version.ts +6 -0
  102. package/src/walk.ts +289 -0
package/README.md ADDED
@@ -0,0 +1,337 @@
1
+ # @quantakrypto/core
2
+
3
+ Shared post-quantum readiness library for the quantakrypto toolchain. It finds
4
+ **classical, non-quantum-safe asymmetric cryptography** in a codebase — inline
5
+ crypto calls, embedded keys/certificates, and quantum-vulnerable npm
6
+ dependencies — and turns the results into an inventory, a readiness score, and
7
+ machine- or human-readable reports.
8
+
9
+ - **Zero runtime dependencies.** Node built-ins only.
10
+ - **ESM + NodeNext**, TypeScript strict, Node ≥ 20.
11
+ - Powers `@quantakrypto/qscan` (CLI), the MCP server, and the GitHub Action.
12
+
13
+ ## Why it exists
14
+
15
+ Shor's algorithm breaks RSA, (EC)DH, ECDSA, DSA and EdDSA. Two threats follow:
16
+
17
+ - **Harvest now, decrypt later (HNDL).** Traffic protected by classical key
18
+ exchange / public-key encryption (ECDH, DH, RSA-OAEP) can be recorded today
19
+ and decrypted once a cryptographically relevant quantum computer exists.
20
+ Findings carry an `hndl: true` flag.
21
+ - **Forgery.** Classical signatures (RSA-PSS, ECDSA, EdDSA, DSA, JWT `RS*/PS*/ES*/EdDSA`)
22
+ can be forged by a quantum attacker. These are `hndl: false` but still high
23
+ severity.
24
+
25
+ `@quantakrypto/core` flags both, and points each finding at a NIST PQC replacement
26
+ (ML-KEM / FIPS 203, ML-DSA / FIPS 204, SLH-DSA / FIPS 205, hybrid
27
+ `X25519MLKEM768`).
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm install @quantakrypto/core
33
+ ```
34
+
35
+ ## Quick start
36
+
37
+ ```ts
38
+ import { scan, formatSummary, toSarif, toJson } from "@quantakrypto/core";
39
+
40
+ const result = await scan({ root: "./", onFile: (f) => process.stderr.write(`scanning ${f}\n`) });
41
+
42
+ console.log(formatSummary(result, { color: true })); // human report
43
+ const sarif = toSarif(result); // SARIF 2.1.0 for CI
44
+ const json = toJson(result); // structured object
45
+ ```
46
+
47
+ ## API
48
+
49
+ ### `scan(options: ScanOptions): Promise<ScanResult>`
50
+
51
+ Recursively scans a directory (or a single file) and returns findings, an
52
+ inventory, file count, timing, and the tool version.
53
+
54
+ ```ts
55
+ interface ScanOptions {
56
+ root: string; // directory or single file
57
+ include?: string[]; // restrict the walk to matching paths (substring/prefix)
58
+ exclude?: string[]; // extra exclude patterns (substring/prefix)
59
+ noDefaultIgnores?: boolean; // disable node_modules/.git/dist/… ignores
60
+ source?: boolean; // scan source files (default true)
61
+ dependencies?: boolean; // scan package.json / package-lock.json (default true)
62
+ config?: boolean; // scan PEM/TLS/cert config (default true)
63
+ maxFileSize?: number; // bytes; default 2 MiB (manifests are exempt)
64
+ scanMinified?: boolean; // scan minified/generated files (default false: skip them)
65
+ files?: string[]; // explicit relative file list (incremental scans)
66
+ detectors?: Detector[]; // override/extend the built-in detector set
67
+ onFile?: (file: string) => void; // progress callback (relative POSIX path)
68
+ }
69
+ ```
70
+
71
+ - **`include`** is now wired into the walker: when set, only paths matching one
72
+ of the patterns are scanned.
73
+ - **`files`** bypasses the directory walk entirely and scans the given relative
74
+ paths (used for incremental / changed-files scans — pair it with
75
+ `changedFiles`). Binary and missing files are skipped; manifests over the size
76
+ cap are still read.
77
+ - **`scanMinified`** is off by default; machine-minified / generated / bundled
78
+ content (`*.bundle.js`, `*.generated.ts`, long single-line files, …) is
79
+ skipped for speed unless you opt in.
80
+
81
+ ### `scanParallel(options): Promise<ScanResult>`
82
+
83
+ A worker-thread pool over the file list with a **deterministic** result merge
84
+ (byte-identical to `scan`). Falls back automatically to the in-process `scan`
85
+ for small workloads (below ~200 files / ~2 MiB) and whenever `worker_threads`
86
+ is unavailable. Extra options:
87
+
88
+ ```ts
89
+ interface ParallelScanOptions extends ScanOptions {
90
+ concurrency?: number; // worker count; default os.availableParallelism()
91
+ parallelThresholdBytes?: number; // serial below this total size (default 2 MiB)
92
+ parallelFileThreshold?: number; // serial below this file count (default 200)
93
+ chunkBytes?: number; // target bytes per worker chunk (default 4 MiB)
94
+ }
95
+ ```
96
+
97
+ ### `changedFiles(root, since?): Promise<string[]>`
98
+
99
+ Returns relative POSIX paths that changed in a git work tree (uncommitted +
100
+ untracked, plus a `since` ref/range when provided). Tolerant of non-git
101
+ directories — returns `[]`. Feed the result into `ScanOptions.files` for
102
+ incremental scans.
103
+
104
+ ```ts
105
+ const files = await changedFiles(".", "origin/main...HEAD");
106
+ const result = await scan({ root: ".", files });
107
+ ```
108
+
109
+ ### `walkFiles(root, options?): AsyncGenerator<string>`
110
+
111
+ Recursive async generator yielding scannable text files as **relative POSIX
112
+ paths**. Skips default-ignored directories (`node_modules`, `.git`, `dist`,
113
+ `build`, `.next`, `out`, `coverage`, `vendor`, `.turbo`, `.cache`), honours
114
+ `exclude` patterns (substring or path-prefix), skips obvious binaries by
115
+ extension, and skips files larger than `maxFileSize` (default 2 MiB). `root`
116
+ may be a single file.
117
+
118
+ ```ts
119
+ for await (const file of walkFiles("./src", { exclude: ["legacy"] })) {
120
+ console.log(file); // e.g. "components/Button.tsx"
121
+ }
122
+ ```
123
+
124
+ ### `detectors: Detector[]` / `defaultRegistry` / `DetectorRegistry`
125
+
126
+ The built-in, pure detectors. Each declares a `scope` (`"source"` | `"config"`),
127
+ a `language`, `appliesTo(filePath)`, and `detect({ file, content })`. `scan()`
128
+ drives the source/config scope toggles from the detector's **declared `scope`**
129
+ (not from ruleId prefixes). Detector families:
130
+
131
+ | Detector | Scope | Catches |
132
+ | --- | --- | --- |
133
+ | `node-crypto` | source | `generateKeyPair(Sync)('rsa'\|'ec'\|'dsa'\|'dh'\|'x25519'\|'x448'\|'ed25519'\|'ed448')`, `createSign/createVerify`, one-shot `crypto.sign/verify`, `createDiffieHellman`, `getDiffieHellman('modpN')`, `createECDH`, `publicEncrypt/privateDecrypt`, `diffieHellman` |
134
+ | `webcrypto` | source | `subtle.{generateKey,importKey,deriveKey,deriveBits,sign,verify,…}` with `RSA-OAEP`, `RSA-PSS`, `RSASSA-PKCS1-v1_5`, `ECDH`, `ECDSA` |
135
+ | `crypto-libs` | source | `node-forge` (`pki.rsa.generateKeyPair`, `ed25519`), `elliptic` (`new EC(...)`), `jsrsasign`, `node-rsa`, direct `secp256k1.*` usage |
136
+ | `jwt-jose` | source | JWT/JOSE alg strings (`RS/PS/ES*`, `EdDSA`) and `ECDH-ES*` key agreement (HNDL) |
137
+ | `tls-config` | config | `minVersion/secureProtocol: 'TLSv1'/'TLSv1.1'`, `rejectUnauthorized: false`, weak ciphers (RC4/DES/3DES/MD5/NULL/EXPORT) |
138
+ | `pem-material` | config | PEM keys/certs in any file: `RSA/EC/DSA/PKCS#8/OPENSSH/PGP PRIVATE KEY`, `PGP MESSAGE`, `CERTIFICATE` |
139
+ | `ssh-cert` | config | SSH public keys (`ssh-rsa`, `ssh-ed25519`, `ecdsa-sha2-*`) and X.509 certificate signature algorithms (`sha256WithRSAEncryption`, `ecdsa-with-SHA256`, …) |
140
+
141
+ `defaultRegistry` is a `DetectorRegistry` preloaded with these built-ins. The
142
+ registry is the plugin point:
143
+
144
+ ```ts
145
+ import { DetectorRegistry, defaultRegistry, scan } from "@quantakrypto/core";
146
+
147
+ const registry = defaultRegistry.clone().register(myDetector);
148
+ const result = await scan({ root: ".", detectors: registry.all() });
149
+ ```
150
+
151
+ `DetectorRegistry` exposes `register(d)`, `get(id)`, `has(id)`, `all()` and
152
+ `clone()`. Ids must be unique (duplicate registration throws).
153
+
154
+ #### Adding a detector / language
155
+
156
+ 1. Create `src/detectors/<lang>.ts` exporting one or more `Detector`s. Set
157
+ `language` (`"js" | "python" | "go" | "java" | "any"`), `scope`
158
+ (`"source" | "config"`), an `appliesTo(path)` extension check, and a pure
159
+ `detect({ file, content })` returning `Finding[]` (use `makeFinding` from
160
+ `detect-utils` for consistent location/remediation/CWE handling).
161
+ 2. If the language uses new file extensions, ensure the walker treats them as
162
+ text (they are scanned unless listed as binary in `walk.ts`).
163
+ 3. For a new dependency ecosystem, extend `VulnerableDependency.ecosystem` and
164
+ add a manifest matcher in `dependencies.ts`.
165
+ 4. Register it: `defaultRegistry.register(myDetector)`, or pass
166
+ `{ detectors: [...] }` to `scan`. No edit to `scan()` is required — scope is
167
+ honoured from the detector's declared `scope`.
168
+
169
+ ### `vulnerableDependencies: VulnerableDependency[]`
170
+
171
+ Curated database (~20 entries) of npm packages whose purpose is classical
172
+ asymmetric crypto: `node-forge`, `elliptic`, `jsrsasign`, `node-rsa`, `ursa`,
173
+ `sshpk`, `jsonwebtoken`, `jose`, `jws`, `eccrypto`, `secp256k1`, `tweetnacl`,
174
+ `ed25519`, `@noble/curves`, `@noble/secp256k1`, `@noble/ed25519`, `paseto`,
175
+ `bcrypto`, `ecpair`, `keypair`. `scan()` matches these against `package.json`
176
+ and `package-lock.json` and emits `category: "dependency"` findings located at
177
+ the manifest.
178
+
179
+ ### `buildInventory(findings: Finding[]): CryptoInventory`
180
+
181
+ Aggregates findings into per-algorithm / per-category / per-severity counts, the
182
+ HNDL count, and a `readinessScore` (0–100, 100 = no classical asymmetric crypto
183
+ found). The score starts at 100 and subtracts severity-weighted penalties with
184
+ diminishing returns per severity bucket, clamped to `[0, 100]`.
185
+
186
+ ### Reporters
187
+
188
+ - `toSarif(result): SarifLog` — valid **SARIF 2.1.0** (`$schema`, `version`,
189
+ `runs[0].tool.driver { name: "qScan", informationUri, version, rules[] }`,
190
+ `results[]` with `ruleId`, `level` (error/warning/note), `message.text`,
191
+ `locations[].physicalLocation` with `artifactLocation.uri` and
192
+ `region.startLine/startColumn`).
193
+ - `toJson(result): Record<string, unknown>` — clean, JSON-serialisable object.
194
+ - `formatSummary(result, { color? }): string` — human report with the readiness
195
+ score, severity/algorithm breakdown, top findings, and an HNDL note. Colour is
196
+ off by default and uses raw ANSI codes when enabled.
197
+
198
+ ### `remediationFor(algorithm): Remediation | undefined` / `remediationForTier(algorithm, tier?)`
199
+
200
+ `remediationFor` returns the recommended PQC replacement for a classical family.
201
+ `remediationForTier` adds a **security tier**: `"category-3"` (default,
202
+ commercial — ML-KEM-768 / ML-DSA-65) or `"category-5"` (CNSA 2.0 / long-lived —
203
+ ML-KEM-1024 / ML-DSA-87).
204
+
205
+ ```ts
206
+ remediationFor("ECDH");
207
+ // { algorithm: "ECDH", recommendation: "hybrid X25519MLKEM768 (ML-KEM-768)", detail: … }
208
+
209
+ remediationForTier("ECDH", "category-5");
210
+ // recommendation mentions ML-KEM-1024; detail cites CNSA 2.0 (2030/2033 milestones)
211
+ ```
212
+
213
+ `STATEFUL_HBS_NOTE` / `statefulHbsApplies(algorithm)` surface the SP 800-208
214
+ stateful hash-based signatures (LMS / XMSS / HSS) guidance for firmware / boot
215
+ signing (stateful — use only with rigorous state management).
216
+
217
+ ### Baseline (`fingerprintFinding`, `baselineFromFindings`, `applyBaseline`, `loadBaseline`, `saveBaseline`)
218
+
219
+ The single canonical baseline scheme shared by qScan and the Action. A baseline
220
+ is `{ version, fingerprints: string[] }`. A fingerprint is the SHA-256 hex of
221
+ `ruleId|file|normalizedSnippet` — **line-insensitive** (survives line shifts)
222
+ and snippet-whitespace-normalized (survives reformatting).
223
+
224
+ ```ts
225
+ import { saveBaseline, loadBaseline, applyBaseline } from "@quantakrypto/core";
226
+
227
+ await saveBaseline(".quantakrypto-baseline.json", result.findings); // write
228
+ const baseline = await loadBaseline(".quantakrypto-baseline.json"); // read (tolerant)
229
+ const { newFindings, suppressed } = applyBaseline(result.findings, baseline);
230
+ ```
231
+
232
+ ### `toCbom(result): CycloneDxBom`
233
+
234
+ A CycloneDX 1.6 **cryptographic bill of materials** (CBOM): one
235
+ `cryptographic-asset` component per distinct (algorithm, primitive) pair, with
236
+ occurrence evidence and `quantumVulnerable` / `harvestNowDecryptLater` flags.
237
+ Deterministic output.
238
+
239
+ ### CWE tagging
240
+
241
+ Every detector sets a `Finding.cwe` (`CWE-327` broken crypto, `CWE-326` weak
242
+ strength, `CWE-295` cert validation, `CWE-798` hardcoded key). `toSarif` maps it
243
+ into `rules[].properties`, result `taxa`, and a run-level CWE `taxonomies`
244
+ component; `toJson` includes it. Constants are exported (`CWE_BROKEN_CRYPTO`, …).
245
+
246
+ ### `VERSION: string`
247
+
248
+ Tool version surfaced in reports (kept in sync with `package.json`).
249
+
250
+ ## API reference
251
+
252
+ | Export | Kind | Summary |
253
+ | --- | --- | --- |
254
+ | `scan` | fn | Scan a dir / file / explicit file list → `ScanResult` |
255
+ | `scanParallel` | fn | Worker-pool scan with deterministic merge + serial fallback |
256
+ | `changedFiles` | fn | Git-aware changed-files list for incremental scans |
257
+ | `detectFile` | fn | Pure per-file detect (used by workers / tests) |
258
+ | `compareFindings` | fn | Stable finding comparator (file → line → ruleId) |
259
+ | `mergeChunkResults`, `chunkByBytes` | fn | Pure parallel merge / byte-chunking helpers |
260
+ | `walkFiles`, `isBinaryPath`, `isGeneratedPath`, `looksMinified` | fn | Walker + file-classification helpers |
261
+ | `detectors` | const | Built-in detector array (mirrors `defaultRegistry.all()`) |
262
+ | `DetectorRegistry`, `defaultRegistry`, `detectorScope` | class/const/fn | Detector plugin point |
263
+ | `buildInventory` | fn | Aggregate findings → `CryptoInventory` |
264
+ | `vulnerableDependencies` | const | Curated quantum-vulnerable npm DB |
265
+ | `toSarif`, `toJson`, `formatSummary` | fn | Reporters (SARIF 2.1.0 / JSON / human) |
266
+ | `toCbom` | fn | CycloneDX 1.6 CBOM export |
267
+ | `remediationFor`, `remediationForTier`, `TIER_PARAMS` | fn/const | PQC remediation (family + CNSA tier) |
268
+ | `STATEFUL_HBS_NOTE`, `statefulHbsApplies` | const/fn | SP 800-208 LMS/XMSS guidance |
269
+ | `fingerprintFinding`, `baselineFromFindings`, `applyBaseline`, `loadBaseline`, `saveBaseline`, `BASELINE_VERSION` | fn/const | Canonical baseline |
270
+ | `CWE_BROKEN_CRYPTO`, `CWE_WEAK_STRENGTH`, `CWE_CERT_VALIDATION`, `CWE_HARDCODED_KEY`, `CWE_RISKY_PRIMITIVE` | const | CWE identifiers |
271
+ | `VERSION` | const | Tool version |
272
+
273
+ Types: `Finding` (now with optional `cwe`), `ScanOptions`/`ParallelScanOptions`,
274
+ `ScanResult`, `Detector` (now with `scope`/`language`), `DetectorScope`,
275
+ `DetectorLanguage`, `Baseline`, `CycloneDxBom`, `CbomComponent`, `SecurityTier`,
276
+ `AlgorithmFamily` (now includes `X448`), and the rest of the locked contract in
277
+ [`src/types.ts`](./src/types.ts).
278
+
279
+ ## Core types
280
+
281
+ See [`src/types.ts`](./src/types.ts) for the locked contract. Highlights:
282
+
283
+ ```ts
284
+ type Severity = "critical" | "high" | "medium" | "low" | "info";
285
+
286
+ type AlgorithmFamily =
287
+ | "RSA" | "ECDH" | "ECDSA" | "EdDSA"
288
+ | "DH" | "DSA" | "X25519" | "X448" | "ECIES" | "unknown";
289
+
290
+ type FindingCategory =
291
+ | "kem" | "key-exchange" | "signature"
292
+ | "tls" | "certificate" | "dependency" | "hash" | "rng";
293
+
294
+ interface Finding {
295
+ ruleId: string;
296
+ title: string;
297
+ category: FindingCategory;
298
+ severity: Severity;
299
+ confidence: "high" | "medium" | "low";
300
+ algorithm?: AlgorithmFamily;
301
+ hndl: boolean; // harvest-now-decrypt-later exposure
302
+ message: string;
303
+ remediation?: string;
304
+ cwe?: string; // e.g. "CWE-327"
305
+ location: { file: string; line: number; column?: number; endLine?: number; snippet?: string };
306
+ }
307
+
308
+ interface CryptoInventory {
309
+ byAlgorithm: Partial<Record<AlgorithmFamily, number>>;
310
+ byCategory: Partial<Record<FindingCategory, number>>;
311
+ bySeverity: Record<Severity, number>;
312
+ hndlCount: number;
313
+ readinessScore: number; // 0–100
314
+ }
315
+ ```
316
+
317
+ ## Example
318
+
319
+ A runnable example lives in [`examples/scan-example.mjs`](./examples/scan-example.mjs):
320
+
321
+ ```bash
322
+ node examples/scan-example.mjs ./path/to/project
323
+ ```
324
+
325
+ ## Development
326
+
327
+ ```bash
328
+ npm run build # tsc -b
329
+ npm test # node --import tsx --test test/*.test.ts
330
+ ```
331
+
332
+ Tests use only `node:test` + `node:assert`. The package has **zero runtime
333
+ dependencies**.
334
+
335
+ ## License
336
+
337
+ Apache-2.0
@@ -0,0 +1,37 @@
1
+ import type { Finding } from "./types.js";
2
+ /** Current on-disk baseline schema version. */
3
+ export declare const BASELINE_VERSION: 1;
4
+ /** The on-disk baseline shape: a version tag and a set of fingerprints. */
5
+ export interface Baseline {
6
+ version: number;
7
+ fingerprints: string[];
8
+ }
9
+ /**
10
+ * Stable, line-INSENSITIVE fingerprint of a finding: the hex SHA-256 of
11
+ * `ruleId|file|normalizedSnippet`. The line number is deliberately excluded so
12
+ * the fingerprint survives line shifts; the snippet's whitespace is normalized
13
+ * so it survives reformatting.
14
+ */
15
+ export declare function fingerprintFinding(f: Finding): string;
16
+ /** Build a {@link Baseline} from a set of findings (deduped, sorted). */
17
+ export declare function baselineFromFindings(findings: readonly Finding[]): Baseline;
18
+ /**
19
+ * Split findings into those NOT in the baseline (`newFindings`) and those that
20
+ * ARE (`suppressed`). Order within each group is preserved from the input.
21
+ */
22
+ export declare function applyBaseline(findings: readonly Finding[], baseline: Baseline): {
23
+ newFindings: Finding[];
24
+ suppressed: Finding[];
25
+ };
26
+ /**
27
+ * Load a baseline from disk. Returns an empty baseline (rather than throwing)
28
+ * when the file is missing or unparseable, so callers can treat "no baseline"
29
+ * and "absent baseline" uniformly.
30
+ */
31
+ export declare function loadBaseline(path: string): Promise<Baseline>;
32
+ /**
33
+ * Write a baseline derived from the given findings to disk as pretty JSON
34
+ * (trailing newline). Returns the baseline that was written.
35
+ */
36
+ export declare function saveBaseline(path: string, findings: readonly Finding[]): Promise<Baseline>;
37
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,+CAA+C;AAC/C,eAAO,MAAM,gBAAgB,EAAG,CAAU,CAAC;AAE3C,2EAA2E;AAC3E,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAQD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAIrD;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,QAAQ,CAO3E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,QAAQ,EAAE,QAAQ,GACjB;IAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAAC,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE,CASnD;AAeD;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAYlE;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAIhG"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Canonical baseline module — the single source of truth for suppressing known
3
+ * findings across qScan and the GitHub Action (replacing their two divergent,
4
+ * mutually-unintelligible schemes).
5
+ *
6
+ * A baseline is a versioned set of finding fingerprints. A fingerprint is
7
+ * **line-insensitive** (so unrelated edits that shift line numbers don't
8
+ * invalidate it) and snippet-whitespace-normalized (so reformatting doesn't
9
+ * either). Identity = `sha256(ruleId | file | normalizedSnippet)`.
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ import { readFile, writeFile } from "node:fs/promises";
13
+ /** Current on-disk baseline schema version. */
14
+ export const BASELINE_VERSION = 1;
15
+ /** Collapse all whitespace runs to single spaces and trim (snippet stability). */
16
+ function normalizeSnippet(snippet) {
17
+ if (!snippet)
18
+ return "";
19
+ return snippet.replace(/\s+/g, " ").trim();
20
+ }
21
+ /**
22
+ * Stable, line-INSENSITIVE fingerprint of a finding: the hex SHA-256 of
23
+ * `ruleId|file|normalizedSnippet`. The line number is deliberately excluded so
24
+ * the fingerprint survives line shifts; the snippet's whitespace is normalized
25
+ * so it survives reformatting.
26
+ */
27
+ export function fingerprintFinding(f) {
28
+ const snippet = normalizeSnippet(f.location.snippet);
29
+ const input = `${f.ruleId}|${f.location.file}|${snippet}`;
30
+ return createHash("sha256").update(input, "utf8").digest("hex");
31
+ }
32
+ /** Build a {@link Baseline} from a set of findings (deduped, sorted). */
33
+ export function baselineFromFindings(findings) {
34
+ const set = new Set();
35
+ for (const f of findings)
36
+ set.add(fingerprintFinding(f));
37
+ return {
38
+ version: BASELINE_VERSION,
39
+ fingerprints: [...set].sort(),
40
+ };
41
+ }
42
+ /**
43
+ * Split findings into those NOT in the baseline (`newFindings`) and those that
44
+ * ARE (`suppressed`). Order within each group is preserved from the input.
45
+ */
46
+ export function applyBaseline(findings, baseline) {
47
+ const accepted = new Set(baseline.fingerprints);
48
+ const newFindings = [];
49
+ const suppressed = [];
50
+ for (const f of findings) {
51
+ if (accepted.has(fingerprintFinding(f)))
52
+ suppressed.push(f);
53
+ else
54
+ newFindings.push(f);
55
+ }
56
+ return { newFindings, suppressed };
57
+ }
58
+ /** Normalize an arbitrary parsed object into a valid {@link Baseline}. */
59
+ function coerceBaseline(value) {
60
+ if (value === null || typeof value !== "object") {
61
+ return { version: BASELINE_VERSION, fingerprints: [] };
62
+ }
63
+ const obj = value;
64
+ const version = typeof obj.version === "number" ? obj.version : BASELINE_VERSION;
65
+ const fingerprints = Array.isArray(obj.fingerprints)
66
+ ? obj.fingerprints.filter((x) => typeof x === "string")
67
+ : [];
68
+ return { version, fingerprints };
69
+ }
70
+ /**
71
+ * Load a baseline from disk. Returns an empty baseline (rather than throwing)
72
+ * when the file is missing or unparseable, so callers can treat "no baseline"
73
+ * and "absent baseline" uniformly.
74
+ */
75
+ export async function loadBaseline(path) {
76
+ let text;
77
+ try {
78
+ text = await readFile(path, "utf8");
79
+ }
80
+ catch {
81
+ return { version: BASELINE_VERSION, fingerprints: [] };
82
+ }
83
+ try {
84
+ return coerceBaseline(JSON.parse(text));
85
+ }
86
+ catch {
87
+ return { version: BASELINE_VERSION, fingerprints: [] };
88
+ }
89
+ }
90
+ /**
91
+ * Write a baseline derived from the given findings to disk as pretty JSON
92
+ * (trailing newline). Returns the baseline that was written.
93
+ */
94
+ export async function saveBaseline(path, findings) {
95
+ const baseline = baselineFromFindings(findings);
96
+ await writeFile(path, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
97
+ return baseline;
98
+ }
99
+ //# sourceMappingURL=baseline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}
package/dist/cbom.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { ScanResult } from "./types.js";
2
+ /** A CycloneDX 1.6 cryptographic bill of materials (kept permissive). */
3
+ export interface CycloneDxBom {
4
+ bomFormat: "CycloneDX";
5
+ specVersion: "1.6";
6
+ serialNumber: string;
7
+ version: number;
8
+ metadata: Record<string, unknown>;
9
+ components: CbomComponent[];
10
+ }
11
+ /** A single CycloneDX `cryptographic-asset` component. */
12
+ export interface CbomComponent {
13
+ type: "cryptographic-asset";
14
+ "bom-ref": string;
15
+ name: string;
16
+ cryptoProperties: Record<string, unknown>;
17
+ evidence?: Record<string, unknown>;
18
+ }
19
+ /**
20
+ * Build a CycloneDX 1.6 CBOM from a scan result. One component per distinct
21
+ * (algorithm + primitive) pair, with occurrence evidence (file:line) per
22
+ * finding. Output is deterministic (components and occurrences are sorted).
23
+ */
24
+ export declare function toCbom(result: ScanResult): CycloneDxBom;
25
+ //# sourceMappingURL=cbom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cbom.d.ts","sourceRoot":"","sources":["../src/cbom.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAA6C,UAAU,EAAE,MAAM,YAAY,CAAC;AAGxF,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,WAAW,CAAC;IACvB,WAAW,EAAE,KAAK,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,EAAE,aAAa,EAAE,CAAC;CAC7B;AAED,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AA8BD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,YAAY,CAqFvD"}
package/dist/cbom.js ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * CBOM (Cryptographic Bill of Materials) export — CycloneDX 1.6 with
3
+ * `cryptographic-asset` components. Turns a {@link ScanResult} into a
4
+ * machine-readable inventory of the classical cryptographic assets discovered,
5
+ * for compliance / supply-chain tooling.
6
+ *
7
+ * Reference: CycloneDX 1.6 cryptography properties
8
+ * (https://cyclonedx.org/capabilities/cbom/). We emit one
9
+ * `cryptographic-asset` component per distinct (algorithm, primitive) pair
10
+ * observed, with occurrence evidence pointing back at the findings.
11
+ */
12
+ import { createHash } from "node:crypto";
13
+ import { VERSION } from "./version.js";
14
+ /** CycloneDX cryptographic primitive for a finding category. */
15
+ function primitiveFor(category) {
16
+ switch (category) {
17
+ case "kem":
18
+ return "kem";
19
+ case "key-exchange":
20
+ return "key-agree";
21
+ case "signature":
22
+ return "signature";
23
+ case "certificate":
24
+ return "pki";
25
+ case "tls":
26
+ return "other";
27
+ default:
28
+ return "other";
29
+ }
30
+ }
31
+ /** True when the algorithm family is broken by Shor's algorithm (quantum). */
32
+ function isQuantumVulnerable(algorithm) {
33
+ return algorithm !== "unknown";
34
+ }
35
+ /** Deterministic bom-ref for a (algorithm, primitive) asset key. */
36
+ function bomRef(key) {
37
+ return `crypto:${createHash("sha256").update(key, "utf8").digest("hex").slice(0, 16)}`;
38
+ }
39
+ /**
40
+ * Build a CycloneDX 1.6 CBOM from a scan result. One component per distinct
41
+ * (algorithm + primitive) pair, with occurrence evidence (file:line) per
42
+ * finding. Output is deterministic (components and occurrences are sorted).
43
+ */
44
+ export function toCbom(result) {
45
+ // Group findings by (algorithm | primitive).
46
+ const groups = new Map();
47
+ for (const f of result.findings) {
48
+ const algorithm = f.algorithm ?? "unknown";
49
+ const primitive = primitiveFor(f.category);
50
+ const key = `${algorithm}|${primitive}`;
51
+ let g = groups.get(key);
52
+ if (!g) {
53
+ g = { algorithm, primitive, findings: [] };
54
+ groups.set(key, g);
55
+ }
56
+ g.findings.push(f);
57
+ }
58
+ const components = [...groups.entries()]
59
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
60
+ .map(([key, g]) => {
61
+ const occurrences = g.findings
62
+ .map((f) => ({
63
+ location: `${f.location.file}:${f.location.line}`,
64
+ ...(f.cwe ? { additionalContext: f.cwe } : {}),
65
+ }))
66
+ .sort((a, b) => (a.location < b.location ? -1 : a.location > b.location ? 1 : 0));
67
+ const anyHndl = g.findings.some((f) => f.hndl);
68
+ return {
69
+ type: "cryptographic-asset",
70
+ "bom-ref": bomRef(key),
71
+ name: `${g.algorithm} (${g.primitive})`,
72
+ cryptoProperties: {
73
+ assetType: "algorithm",
74
+ algorithmProperties: {
75
+ primitive: g.primitive,
76
+ parameterSetIdentifier: g.algorithm,
77
+ executionEnvironment: "software-plain-ram",
78
+ classicalSecurityLevel: 0,
79
+ nistQuantumSecurityLevel: 0,
80
+ cryptoFunctions: g.primitive === "signature"
81
+ ? ["sign", "verify"]
82
+ : g.primitive === "kem"
83
+ ? ["encapsulate", "decapsulate"]
84
+ : g.primitive === "key-agree"
85
+ ? ["keygen"]
86
+ : ["other"],
87
+ },
88
+ quantumVulnerable: isQuantumVulnerable(g.algorithm),
89
+ harvestNowDecryptLater: anyHndl,
90
+ },
91
+ evidence: { occurrences },
92
+ };
93
+ });
94
+ const serial = `urn:uuid:${stableUuid(result)}`;
95
+ return {
96
+ bomFormat: "CycloneDX",
97
+ specVersion: "1.6",
98
+ serialNumber: serial,
99
+ version: 1,
100
+ metadata: {
101
+ timestamp: result.finishedAt,
102
+ tools: {
103
+ components: [
104
+ {
105
+ type: "application",
106
+ name: "qScan",
107
+ version: result.toolVersion || VERSION,
108
+ },
109
+ ],
110
+ },
111
+ component: {
112
+ type: "application",
113
+ "bom-ref": "root",
114
+ name: result.root,
115
+ },
116
+ },
117
+ components,
118
+ };
119
+ }
120
+ /**
121
+ * Derive a stable UUID-shaped serial from the scan result so re-exporting the
122
+ * same result yields the same serial number (deterministic output).
123
+ */
124
+ function stableUuid(result) {
125
+ const h = createHash("sha256")
126
+ .update(`${result.root}|${result.toolVersion}|${result.findings.length}`, "utf8")
127
+ .digest("hex");
128
+ // Shape as a v4-ish UUID (variant/version nibbles forced).
129
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
130
+ }
131
+ //# sourceMappingURL=cbom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cbom.js","sourceRoot":"","sources":["../src/cbom.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Return the list of changed files (relative POSIX paths) under `root`.
3
+ *
4
+ * - With `since` (a ref / range), returns `git diff --name-only --diff-filter=ACMR <since>`
5
+ * plus currently-modified-but-uncommitted files, deduped.
6
+ * - Without `since`, returns uncommitted changes (`git diff` working+staged) plus
7
+ * untracked files (`git ls-files --others --exclude-standard`).
8
+ * - When `root` is not a git repository (or git is unavailable), returns `[]`.
9
+ *
10
+ * Deleted files are excluded (ACMR filter / existence is the caller's concern).
11
+ */
12
+ export declare function changedFiles(root: string, since?: string): Promise<string[]>;
13
+ //# sourceMappingURL=changed.d.ts.map