@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.
- package/README.md +337 -0
- package/dist/baseline.d.ts +37 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +99 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cbom.d.ts +25 -0
- package/dist/cbom.d.ts.map +1 -0
- package/dist/cbom.js +131 -0
- package/dist/cbom.js.map +1 -0
- package/dist/changed.d.ts +13 -0
- package/dist/changed.d.ts.map +1 -0
- package/dist/changed.js +67 -0
- package/dist/changed.js.map +1 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +232 -0
- package/dist/config.js.map +1 -0
- package/dist/cwe.d.ts +16 -0
- package/dist/cwe.d.ts.map +1 -0
- package/dist/cwe.js +16 -0
- package/dist/cwe.js.map +1 -0
- package/dist/dependencies.d.ts +25 -0
- package/dist/dependencies.d.ts.map +1 -0
- package/dist/dependencies.js +276 -0
- package/dist/dependencies.js.map +1 -0
- package/dist/detect-utils.d.ts +63 -0
- package/dist/detect-utils.d.ts.map +1 -0
- package/dist/detect-utils.js +113 -0
- package/dist/detect-utils.js.map +1 -0
- package/dist/detectors/pem.d.ts +9 -0
- package/dist/detectors/pem.d.ts.map +1 -0
- package/dist/detectors/pem.js +137 -0
- package/dist/detectors/pem.js.map +1 -0
- package/dist/detectors/source.d.ts +25 -0
- package/dist/detectors/source.d.ts.map +1 -0
- package/dist/detectors/source.js +575 -0
- package/dist/detectors/source.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +15 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +74 -0
- package/dist/inventory.js.map +1 -0
- package/dist/parallel.d.ts +34 -0
- package/dist/parallel.d.ts.map +1 -0
- package/dist/parallel.js +237 -0
- package/dist/parallel.js.map +1 -0
- package/dist/registry.d.ts +42 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +51 -0
- package/dist/registry.js.map +1 -0
- package/dist/remediation.d.ts +42 -0
- package/dist/remediation.d.ts.map +1 -0
- package/dist/remediation.js +131 -0
- package/dist/remediation.js.map +1 -0
- package/dist/report.d.ts +24 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +286 -0
- package/dist/report.js.map +1 -0
- package/dist/scan-worker.d.ts +2 -0
- package/dist/scan-worker.d.ts.map +1 -0
- package/dist/scan-worker.js +63 -0
- package/dist/scan-worker.js.map +1 -0
- package/dist/scan.d.ts +27 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +168 -0
- package/dist/scan.js.map +1 -0
- package/dist/types.d.ts +190 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/dist/walk.d.ts +37 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +260 -0
- package/dist/walk.js.map +1 -0
- package/package.json +40 -0
- package/src/baseline.ts +111 -0
- package/src/cbom.ts +166 -0
- package/src/changed.ts +76 -0
- package/src/config.ts +295 -0
- package/src/cwe.ts +20 -0
- package/src/dependencies.ts +299 -0
- package/src/detect-utils.ts +157 -0
- package/src/detectors/pem.ts +162 -0
- package/src/detectors/source.ts +733 -0
- package/src/index.ts +79 -0
- package/src/inventory.ts +94 -0
- package/src/parallel.ts +288 -0
- package/src/registry.ts +71 -0
- package/src/remediation.ts +173 -0
- package/src/report.ts +340 -0
- package/src/scan-worker.ts +80 -0
- package/src/scan.ts +187 -0
- package/src/types.ts +224 -0
- package/src/version.ts +6 -0
- 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"}
|
package/dist/baseline.js
ADDED
|
@@ -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
|
package/dist/cbom.js.map
ADDED
|
@@ -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
|