@quantakrypto/core 0.5.0 → 0.7.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/dist/crypto-agility.d.ts +158 -0
- package/dist/crypto-agility.d.ts.map +1 -0
- package/dist/crypto-agility.js +285 -0
- package/dist/crypto-agility.js.map +1 -0
- package/dist/dependencies.d.ts.map +1 -1
- package/dist/dependencies.js +180 -18
- package/dist/dependencies.js.map +1 -1
- package/dist/detectors/source.d.ts.map +1 -1
- package/dist/detectors/source.js +52 -0
- package/dist/detectors/source.js.map +1 -1
- package/dist/hndl.d.ts +241 -0
- package/dist/hndl.d.ts.map +1 -0
- package/dist/hndl.js +752 -0
- package/dist/hndl.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/mandates.d.ts +138 -0
- package/dist/mandates.d.ts.map +1 -0
- package/dist/mandates.js +228 -0
- package/dist/mandates.js.map +1 -0
- package/dist/parallel.d.ts.map +1 -1
- package/dist/parallel.js +22 -23
- package/dist/parallel.js.map +1 -1
- package/dist/report.d.ts +8 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +75 -19
- package/dist/report.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/walk.d.ts +31 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +30 -9
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto-agility.ts +407 -0
- package/src/dependencies.ts +182 -16
- package/src/detectors/source.ts +62 -0
- package/src/hndl.ts +1012 -0
- package/src/index.ts +71 -0
- package/src/mandates.ts +365 -0
- package/src/parallel.ts +21 -20
- package/src/report.ts +84 -19
- package/src/types.ts +9 -1
- package/src/version.ts +1 -1
- package/src/walk.ts +47 -10
package/dist/mandates.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { PQC_STANDARDS } from "./standards.js";
|
|
2
|
+
/**
|
|
3
|
+
* All Shor-broken classical asymmetric families — the mandate's SCOPE. A finding
|
|
4
|
+
* on one of these is adjudicated against the selected mandates; findings on
|
|
5
|
+
* anything else (hashes, RNG, dependency, or TLS-configuration findings) are out
|
|
6
|
+
* of scope for a PQC-asymmetric mandate and are tallied as `notInScope` instead
|
|
7
|
+
* of inflating the conformant count.
|
|
8
|
+
*/
|
|
9
|
+
const CLASSICAL_PUBLIC_KEY = [
|
|
10
|
+
"RSA",
|
|
11
|
+
"ECDH",
|
|
12
|
+
"ECDSA",
|
|
13
|
+
"EdDSA",
|
|
14
|
+
"DH",
|
|
15
|
+
"DSA",
|
|
16
|
+
"X25519",
|
|
17
|
+
"X448",
|
|
18
|
+
"ECIES",
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* The PROHIBITED subset the dated clauses apply to. X25519 and X448 are
|
|
22
|
+
* deliberately excluded: they are the classical half of the recommended hybrid
|
|
23
|
+
* key exchange (X25519MLKEM768 — permitted and recommended under the NIST
|
|
24
|
+
* profile), and a static scan cannot distinguish a standalone exchange from the
|
|
25
|
+
* hybrid's classical leg. Prohibiting them would false-positive exactly the orgs
|
|
26
|
+
* that hybridized correctly, so they stay in scope but read `conformant`.
|
|
27
|
+
*/
|
|
28
|
+
const PROHIBITED_FAMILIES = CLASSICAL_PUBLIC_KEY.filter((family) => family !== "X25519" && family !== "X448");
|
|
29
|
+
/**
|
|
30
|
+
* Effective dates derived from the standards source of truth
|
|
31
|
+
* (`PQC_STANDARDS.transitionTimeline`), so a quarterly standards update moves the
|
|
32
|
+
* mandate deadlines automatically (test/standards.test.ts asserts they agree).
|
|
33
|
+
*
|
|
34
|
+
* Boundary choice: "deprecate AFTER 2030" leaves the whole stated year permitted,
|
|
35
|
+
* so each clause takes effect on the LAST day of its year (`YYYY-12-31`) —
|
|
36
|
+
* conservative by a single day, unlike `YYYY-01-01`, which would bite roughly a
|
|
37
|
+
* year early.
|
|
38
|
+
*/
|
|
39
|
+
const { deprecateAfter, disallowAfter } = PQC_STANDARDS.transitionTimeline;
|
|
40
|
+
const DEPRECATE_EFFECTIVE = `${deprecateAfter}-12-31`;
|
|
41
|
+
const DISALLOW_EFFECTIVE = `${disallowAfter}-12-31`;
|
|
42
|
+
/** The bundled mandate catalog. Keyed by `--mandate <id>`. */
|
|
43
|
+
export const MANDATES = {
|
|
44
|
+
"cnsa-2.0": {
|
|
45
|
+
id: "cnsa-2.0",
|
|
46
|
+
name: "CNSA 2.0",
|
|
47
|
+
authority: "NSA",
|
|
48
|
+
citation: "NSA Commercial National Security Algorithm Suite 2.0 (CNSA 2.0)",
|
|
49
|
+
asOf: "2026-07",
|
|
50
|
+
rules: [
|
|
51
|
+
{
|
|
52
|
+
clause: `CNSA 2.0 — deprecate classical PKC after ${deprecateAfter}`,
|
|
53
|
+
tier: "deprecate",
|
|
54
|
+
prohibits: [...PROHIBITED_FAMILIES],
|
|
55
|
+
effective: DEPRECATE_EFFECTIVE,
|
|
56
|
+
note: "Classical public-key cryptography deprecated; systems should use CNSA 2.0 PQC exclusively.",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
clause: `CNSA 2.0 — disallow classical PKC after ${disallowAfter}`,
|
|
60
|
+
tier: "disallow",
|
|
61
|
+
prohibits: [...PROHIBITED_FAMILIES],
|
|
62
|
+
effective: DISALLOW_EFFECTIVE,
|
|
63
|
+
note: "Classical public-key cryptography disallowed; the migration must be complete.",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
"nist-ir-8547": {
|
|
68
|
+
id: "nist-ir-8547",
|
|
69
|
+
name: "NIST IR 8547",
|
|
70
|
+
authority: "NIST",
|
|
71
|
+
citation: "NIST IR 8547 (Transition to Post-Quantum Cryptography Standards)",
|
|
72
|
+
asOf: "2026-07",
|
|
73
|
+
rules: [
|
|
74
|
+
{
|
|
75
|
+
clause: `NIST IR 8547 — deprecate classical PKC after ${deprecateAfter}`,
|
|
76
|
+
tier: "deprecate",
|
|
77
|
+
prohibits: [...PROHIBITED_FAMILIES],
|
|
78
|
+
effective: DEPRECATE_EFFECTIVE,
|
|
79
|
+
note: `112-bit-security classical public-key algorithms deprecated after ${deprecateAfter}.`,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
clause: `NIST IR 8547 — disallow classical PKC after ${disallowAfter}`,
|
|
83
|
+
tier: "disallow",
|
|
84
|
+
prohibits: [...PROHIBITED_FAMILIES],
|
|
85
|
+
effective: DISALLOW_EFFECTIVE,
|
|
86
|
+
note: `Classical public-key algorithms disallowed after ${disallowAfter}.`,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
export const mandateIds = () => Object.keys(MANDATES);
|
|
92
|
+
export const getMandate = (id) => MANDATES[id];
|
|
93
|
+
/**
|
|
94
|
+
* Validate mandate ids loudly, matching `parseCryptoPolicy`'s fail-loud
|
|
95
|
+
* convention: a mistyped id must never silently evaluate to an empty gate.
|
|
96
|
+
* Throws an `Error` naming every unknown id and the known catalog.
|
|
97
|
+
* `evaluateMandates` itself stays lenient (unknown ids are skipped) so callers
|
|
98
|
+
* decide where to fail.
|
|
99
|
+
*/
|
|
100
|
+
export function assertKnownMandates(ids) {
|
|
101
|
+
const unknown = ids.filter((id) => !MANDATES[id]);
|
|
102
|
+
if (unknown.length > 0) {
|
|
103
|
+
throw new Error(`unknown mandate id(s): ${unknown.join(", ")}; known mandates: ${mandateIds().join(", ")}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const MONTH_MS = 2_629_800_000; // average month
|
|
107
|
+
function monthsBetween(fromMs, toMs) {
|
|
108
|
+
return Math.round((toMs - fromMs) / MONTH_MS);
|
|
109
|
+
}
|
|
110
|
+
const STATUS_RANK = {
|
|
111
|
+
conformant: 0,
|
|
112
|
+
due: 1,
|
|
113
|
+
deprecated: 2,
|
|
114
|
+
violation: 3,
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Evaluate findings against the selected mandates as of `now`. Unknown mandate
|
|
118
|
+
* ids are ignored — callers validate up front with {@link assertKnownMandates}.
|
|
119
|
+
* A finding outside the classical-asymmetric scope is counted in `notInScope`;
|
|
120
|
+
* an in-scope finding no selected mandate prohibits is `conformant`. Neither
|
|
121
|
+
* contributes a verdict row.
|
|
122
|
+
*/
|
|
123
|
+
export function evaluateMandates(findings, mandateIdList, now) {
|
|
124
|
+
const nowMs = now.getTime();
|
|
125
|
+
const selected = mandateIdList.map(getMandate).filter((m) => Boolean(m));
|
|
126
|
+
const rows = [];
|
|
127
|
+
const perFindingWorst = [];
|
|
128
|
+
let notInScope = 0;
|
|
129
|
+
let nextDeadlineMs = null;
|
|
130
|
+
for (const f of findings) {
|
|
131
|
+
const algo = f.algorithm ?? "unknown";
|
|
132
|
+
if (!CLASSICAL_PUBLIC_KEY.includes(algo)) {
|
|
133
|
+
notInScope++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
let worst = "conformant";
|
|
137
|
+
for (const mandate of selected) {
|
|
138
|
+
// The applicable clauses for this family, earliest deadline first.
|
|
139
|
+
const applicable = mandate.rules
|
|
140
|
+
.filter((r) => r.prohibits.includes(algo))
|
|
141
|
+
.sort((a, b) => a.effective.localeCompare(b.effective));
|
|
142
|
+
if (applicable.length === 0)
|
|
143
|
+
continue;
|
|
144
|
+
// Tier the clauses so both stay live: a passed DISALLOW clause is a
|
|
145
|
+
// violation; a passed DEPRECATE clause (disallow still ahead) is the
|
|
146
|
+
// deprecated warning tier; otherwise the finding is due against the next
|
|
147
|
+
// upcoming clause.
|
|
148
|
+
const passed = applicable.filter((r) => nowMs >= new Date(r.effective).getTime());
|
|
149
|
+
const passedDisallow = passed.filter((r) => r.tier === "disallow");
|
|
150
|
+
let status;
|
|
151
|
+
let governing;
|
|
152
|
+
if (passedDisallow.length > 0) {
|
|
153
|
+
status = "violation";
|
|
154
|
+
governing = passedDisallow[0];
|
|
155
|
+
}
|
|
156
|
+
else if (passed.length > 0) {
|
|
157
|
+
status = "deprecated";
|
|
158
|
+
governing = passed[passed.length - 1];
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
status = "due";
|
|
162
|
+
governing = applicable[0];
|
|
163
|
+
}
|
|
164
|
+
// The disallow clause (earliest, if several) anchors `leadMonths`.
|
|
165
|
+
const disallowRule = applicable.find((r) => r.tier === "disallow") ?? null;
|
|
166
|
+
const disallowMs = disallowRule ? new Date(disallowRule.effective).getTime() : null;
|
|
167
|
+
if (status !== "violation") {
|
|
168
|
+
for (const r of applicable) {
|
|
169
|
+
const effMs = new Date(r.effective).getTime();
|
|
170
|
+
if (effMs > nowMs && (nextDeadlineMs === null || effMs < nextDeadlineMs))
|
|
171
|
+
nextDeadlineMs = effMs;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (STATUS_RANK[status] > STATUS_RANK[worst])
|
|
175
|
+
worst = status;
|
|
176
|
+
rows.push({
|
|
177
|
+
ruleId: f.ruleId,
|
|
178
|
+
algorithm: algo,
|
|
179
|
+
file: f.location.file,
|
|
180
|
+
line: f.location.line,
|
|
181
|
+
mandate: mandate.id,
|
|
182
|
+
clause: governing.clause,
|
|
183
|
+
effective: governing.effective,
|
|
184
|
+
status,
|
|
185
|
+
monthsUntil: monthsBetween(nowMs, new Date(governing.effective).getTime()),
|
|
186
|
+
disallowEffective: disallowRule ? disallowRule.effective : null,
|
|
187
|
+
monthsUntilDisallow: disallowMs !== null ? monthsBetween(nowMs, disallowMs) : null,
|
|
188
|
+
citation: mandate.citation,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
perFindingWorst.push(worst);
|
|
192
|
+
}
|
|
193
|
+
const summary = {
|
|
194
|
+
conformant: 0,
|
|
195
|
+
due: 0,
|
|
196
|
+
deprecated: 0,
|
|
197
|
+
violation: 0,
|
|
198
|
+
};
|
|
199
|
+
for (const s of perFindingWorst)
|
|
200
|
+
summary[s]++;
|
|
201
|
+
return {
|
|
202
|
+
now: now.toISOString(),
|
|
203
|
+
mandates: selected.map((m) => m.id),
|
|
204
|
+
summary,
|
|
205
|
+
notInScope,
|
|
206
|
+
findings: rows,
|
|
207
|
+
nextDeadline: nextDeadlineMs !== null ? new Date(nextDeadlineMs).toISOString().slice(0, 10) : null,
|
|
208
|
+
hasViolation: summary.violation > 0,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The gate decision under the "deadline-aware" default: fail only once a DISALLOW
|
|
213
|
+
* deadline has passed (`violation`). A passed DEPRECATE date (`deprecated`) is a
|
|
214
|
+
* warning and does not fail the build. `leadMonths` fails early when a disallow
|
|
215
|
+
* deadline is within the window; `failNow` fails on any prohibited finding
|
|
216
|
+
* immediately.
|
|
217
|
+
*/
|
|
218
|
+
export function mandateGateFails(ev, opts = {}) {
|
|
219
|
+
if (ev.hasViolation)
|
|
220
|
+
return true;
|
|
221
|
+
if (opts.failNow)
|
|
222
|
+
return ev.findings.length > 0;
|
|
223
|
+
if (opts.leadMonths !== undefined) {
|
|
224
|
+
return ev.findings.some((v) => v.monthsUntilDisallow !== null && v.monthsUntilDisallow <= opts.leadMonths);
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=mandates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mandates.js","sourceRoot":"","sources":["../src/mandates.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;;;;;GAMG;AACH,MAAM,oBAAoB,GAA+B;IACvD,KAAK;IACL,MAAM;IACN,OAAO;IACP,OAAO;IACP,IAAI;IACJ,KAAK;IACL,QAAQ;IACR,MAAM;IACN,OAAO;CACR,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,mBAAmB,GAA+B,oBAAoB,CAAC,MAAM,CACjF,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,MAAM,CACrD,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,GAAG,aAAa,CAAC,kBAAkB,CAAC;AAC3E,MAAM,mBAAmB,GAAG,GAAG,cAAc,QAAQ,CAAC;AACtD,MAAM,kBAAkB,GAAG,GAAG,aAAa,QAAQ,CAAC;AA4BpD,8DAA8D;AAC9D,MAAM,CAAC,MAAM,QAAQ,GAA4B;IAC/C,UAAU,EAAE;QACV,EAAE,EAAE,UAAU;QACd,IAAI,EAAE,UAAU;QAChB,SAAS,EAAE,KAAK;QAChB,QAAQ,EAAE,iEAAiE;QAC3E,IAAI,EAAE,SAAS;QACf,KAAK,EAAE;YACL;gBACE,MAAM,EAAE,4CAA4C,cAAc,EAAE;gBACpE,IAAI,EAAE,WAAW;gBACjB,SAAS,EAAE,CAAC,GAAG,mBAAmB,CAAC;gBACnC,SAAS,EAAE,mBAAmB;gBAC9B,IAAI,EAAE,4FAA4F;aACnG;YACD;gBACE,MAAM,EAAE,2CAA2C,aAAa,EAAE;gBAClE,IAAI,EAAE,UAAU;gBAChB,SAAS,EAAE,CAAC,GAAG,mBAAmB,CAAC;gBACnC,SAAS,EAAE,kBAAkB;gBAC7B,IAAI,EAAE,+EAA+E;aACtF;SACF;KACF;IACD,cAAc,EAAE;QACd,EAAE,EAAE,cAAc;QAClB,IAAI,EAAE,cAAc;QACpB,SAAS,EAAE,MAAM;QACjB,QAAQ,EAAE,kEAAkE;QAC5E,IAAI,EAAE,SAAS;QACf,KAAK,EAAE;YACL;gBACE,MAAM,EAAE,gDAAgD,cAAc,EAAE;gBACxE,IAAI,EAAE,WAAW;gBACjB,SAAS,EAAE,CAAC,GAAG,mBAAmB,CAAC;gBACnC,SAAS,EAAE,mBAAmB;gBAC9B,IAAI,EAAE,qEAAqE,cAAc,GAAG;aAC7F;YACD;gBACE,MAAM,EAAE,+CAA+C,aAAa,EAAE;gBACtE,IAAI,EAAE,UAAU;gBAChB,SAAS,EAAE,CAAC,GAAG,mBAAmB,CAAC;gBACnC,SAAS,EAAE,kBAAkB;gBAC7B,IAAI,EAAE,oDAAoD,aAAa,GAAG;aAC3E;SACF;KACF;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,GAAa,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAChE,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EAAU,EAAuB,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAE5E;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAsB;IACxD,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IAClD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,0BAA0B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,qBAAqB,UAAU,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC3F,CAAC;IACJ,CAAC;AACH,CAAC;AA+DD,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,gBAAgB;AAEhD,SAAS,aAAa,CAAC,MAAc,EAAE,IAAY;IACjD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,WAAW,GAAkC;IACjD,UAAU,EAAE,CAAC;IACb,GAAG,EAAE,CAAC;IACN,UAAU,EAAE,CAAC;IACb,SAAS,EAAE,CAAC;CACb,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAA4B,EAC5B,aAAgC,EAChC,GAAS;IAET,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;IAC5B,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAgB,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvF,MAAM,IAAI,GAA4B,EAAE,CAAC;IACzC,MAAM,eAAe,GAAoB,EAAE,CAAC;IAC5C,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,cAAc,GAAkB,IAAI,CAAC;IAEzC,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,CAAC,CAAC,SAAS,IAAI,SAAS,CAAC;QACtC,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,IAAuB,CAAC,EAAE,CAAC;YAC5D,UAAU,EAAE,CAAC;YACb,SAAS;QACX,CAAC;QACD,IAAI,KAAK,GAAkB,YAAY,CAAC;QACxC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,mEAAmE;YACnE,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK;iBAC7B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAuB,CAAC,CAAC;iBAC5D,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;YAC1D,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAEtC,oEAAoE;YACpE,qEAAqE;YACrE,yEAAyE;YACzE,mBAAmB;YACnB,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAClF,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;YACnE,IAAI,MAAqB,CAAC;YAC1B,IAAI,SAAsB,CAAC;YAC3B,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC9B,MAAM,GAAG,WAAW,CAAC;gBACrB,SAAS,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;YAChC,CAAC;iBAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7B,MAAM,GAAG,YAAY,CAAC;gBACtB,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACxC,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,KAAK,CAAC;gBACf,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;YAC5B,CAAC;YAED,mEAAmE;YACnE,MAAM,YAAY,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,IAAI,IAAI,CAAC;YAC3E,MAAM,UAAU,GAAG,YAAY,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YAEpF,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;gBAC3B,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;oBAC3B,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC;oBAC9C,IAAI,KAAK,GAAG,KAAK,IAAI,CAAC,cAAc,KAAK,IAAI,IAAI,KAAK,GAAG,cAAc,CAAC;wBACtE,cAAc,GAAG,KAAK,CAAC;gBAC3B,CAAC;YACH,CAAC;YACD,IAAI,WAAW,CAAC,MAAM,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC;gBAAE,KAAK,GAAG,MAAM,CAAC;YAC7D,IAAI,CAAC,IAAI,CAAC;gBACR,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,SAAS,EAAE,IAAI;gBACf,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI;gBACrB,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI;gBACrB,OAAO,EAAE,OAAO,CAAC,EAAE;gBACnB,MAAM,EAAE,SAAS,CAAC,MAAM;gBACxB,SAAS,EAAE,SAAS,CAAC,SAAS;gBAC9B,MAAM;gBACN,WAAW,EAAE,aAAa,CAAC,KAAK,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC;gBAC1E,iBAAiB,EAAE,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;gBAC/D,mBAAmB,EAAE,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI;gBAClF,QAAQ,EAAE,OAAO,CAAC,QAAQ;aAC3B,CAAC,CAAC;QACL,CAAC;QACD,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,MAAM,OAAO,GAAkC;QAC7C,UAAU,EAAE,CAAC;QACb,GAAG,EAAE,CAAC;QACN,UAAU,EAAE,CAAC;QACb,SAAS,EAAE,CAAC;KACb,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,eAAe;QAAE,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAE9C,OAAO;QACL,GAAG,EAAE,GAAG,CAAC,WAAW,EAAE;QACtB,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnC,OAAO;QACP,UAAU;QACV,QAAQ,EAAE,IAAI;QACd,YAAY,EACV,cAAc,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;QACtF,YAAY,EAAE,OAAO,CAAC,SAAS,GAAG,CAAC;KACpC,CAAC;AACJ,CAAC;AASD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAqB,EAAE,OAA2B,EAAE;IACnF,IAAI,EAAE,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IAChD,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QAClC,OAAO,EAAE,CAAC,QAAQ,CAAC,IAAI,CACrB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,mBAAmB,KAAK,IAAI,IAAI,CAAC,CAAC,mBAAmB,IAAI,IAAI,CAAC,UAAW,CACnF,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["/**\n * Policy-as-code compliance mandates → dated, clause-named verdicts for findings.\n *\n * `CryptoPolicy` (policy.ts) classifies findings by algorithm family but is\n * date-blind. A mandate adds the missing dimension: named clauses with an effective\n * DATE (\"CNSA 2.0 disallows classical public-key crypto after 2035\"). The evaluator\n * compares each finding's algorithm against the selected mandates and today's date,\n * so a finding on a prohibited family reads as `due` (every deadline still ahead),\n * `deprecated` (the DEPRECATE deadline has passed — a warning), or `violation` (the\n * DISALLOW deadline has passed — a failure), always naming the governing clause,\n * deadline, and citation. This is what turns the inventory into an enforceable,\n * mandate-mapped gate rather than a neutral list.\n *\n * Pure and deterministic (the caller supplies `now`), so it is trivially testable.\n * qScan consumes it today for the `--mandate` gate; because it operates on the\n * shared `Finding[]`, qProbe or the GitHub Action can reuse it unchanged.\n *\n * Catalog scope: the two regimes that carry hard algorithm deadlines — CNSA 2.0 and\n * NIST IR 8547. DORA / NIS2 / PCI DSS require approved cryptography but set no\n * independent algorithm date; they inherit these timelines and are cited in docs.\n */\nimport type { AlgorithmFamily, Finding } from \"./types.js\";\nimport { PQC_STANDARDS } from \"./standards.js\";\n\n/**\n * All Shor-broken classical asymmetric families — the mandate's SCOPE. A finding\n * on one of these is adjudicated against the selected mandates; findings on\n * anything else (hashes, RNG, dependency, or TLS-configuration findings) are out\n * of scope for a PQC-asymmetric mandate and are tallied as `notInScope` instead\n * of inflating the conformant count.\n */\nconst CLASSICAL_PUBLIC_KEY: readonly AlgorithmFamily[] = [\n \"RSA\",\n \"ECDH\",\n \"ECDSA\",\n \"EdDSA\",\n \"DH\",\n \"DSA\",\n \"X25519\",\n \"X448\",\n \"ECIES\",\n];\n\n/**\n * The PROHIBITED subset the dated clauses apply to. X25519 and X448 are\n * deliberately excluded: they are the classical half of the recommended hybrid\n * key exchange (X25519MLKEM768 — permitted and recommended under the NIST\n * profile), and a static scan cannot distinguish a standalone exchange from the\n * hybrid's classical leg. Prohibiting them would false-positive exactly the orgs\n * that hybridized correctly, so they stay in scope but read `conformant`.\n */\nconst PROHIBITED_FAMILIES: readonly AlgorithmFamily[] = CLASSICAL_PUBLIC_KEY.filter(\n (family) => family !== \"X25519\" && family !== \"X448\",\n);\n\n/**\n * Effective dates derived from the standards source of truth\n * (`PQC_STANDARDS.transitionTimeline`), so a quarterly standards update moves the\n * mandate deadlines automatically (test/standards.test.ts asserts they agree).\n *\n * Boundary choice: \"deprecate AFTER 2030\" leaves the whole stated year permitted,\n * so each clause takes effect on the LAST day of its year (`YYYY-12-31`) —\n * conservative by a single day, unlike `YYYY-01-01`, which would bite roughly a\n * year early.\n */\nconst { deprecateAfter, disallowAfter } = PQC_STANDARDS.transitionTimeline;\nconst DEPRECATE_EFFECTIVE = `${deprecateAfter}-12-31`;\nconst DISALLOW_EFFECTIVE = `${disallowAfter}-12-31`;\n\n/** Which enforcement tier a clause encodes: warn (`deprecate`) or fail (`disallow`). */\nexport type MandateRuleTier = \"deprecate\" | \"disallow\";\n\nexport interface MandateRule {\n /** The named clause this rule encodes (verbatim in the gate's failure message). */\n clause: string;\n /** Enforcement tier: a passed `deprecate` date warns; a passed `disallow` date fails. */\n tier: MandateRuleTier;\n /** Algorithm families prohibited from the effective date onward. */\n prohibits: AlgorithmFamily[];\n /** ISO date (YYYY-MM-DD) the prohibition takes effect. */\n effective: string;\n /** One-line human description of the clause. */\n note: string;\n}\n\nexport interface Mandate {\n id: string;\n name: string;\n authority: string;\n citation: string;\n /** When this catalog entry was last reviewed against the source. */\n asOf: string;\n rules: MandateRule[];\n}\n\n/** The bundled mandate catalog. Keyed by `--mandate <id>`. */\nexport const MANDATES: Record<string, Mandate> = {\n \"cnsa-2.0\": {\n id: \"cnsa-2.0\",\n name: \"CNSA 2.0\",\n authority: \"NSA\",\n citation: \"NSA Commercial National Security Algorithm Suite 2.0 (CNSA 2.0)\",\n asOf: \"2026-07\",\n rules: [\n {\n clause: `CNSA 2.0 — deprecate classical PKC after ${deprecateAfter}`,\n tier: \"deprecate\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: DEPRECATE_EFFECTIVE,\n note: \"Classical public-key cryptography deprecated; systems should use CNSA 2.0 PQC exclusively.\",\n },\n {\n clause: `CNSA 2.0 — disallow classical PKC after ${disallowAfter}`,\n tier: \"disallow\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: DISALLOW_EFFECTIVE,\n note: \"Classical public-key cryptography disallowed; the migration must be complete.\",\n },\n ],\n },\n \"nist-ir-8547\": {\n id: \"nist-ir-8547\",\n name: \"NIST IR 8547\",\n authority: \"NIST\",\n citation: \"NIST IR 8547 (Transition to Post-Quantum Cryptography Standards)\",\n asOf: \"2026-07\",\n rules: [\n {\n clause: `NIST IR 8547 — deprecate classical PKC after ${deprecateAfter}`,\n tier: \"deprecate\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: DEPRECATE_EFFECTIVE,\n note: `112-bit-security classical public-key algorithms deprecated after ${deprecateAfter}.`,\n },\n {\n clause: `NIST IR 8547 — disallow classical PKC after ${disallowAfter}`,\n tier: \"disallow\",\n prohibits: [...PROHIBITED_FAMILIES],\n effective: DISALLOW_EFFECTIVE,\n note: `Classical public-key algorithms disallowed after ${disallowAfter}.`,\n },\n ],\n },\n};\n\nexport const mandateIds = (): string[] => Object.keys(MANDATES);\nexport const getMandate = (id: string): Mandate | undefined => MANDATES[id];\n\n/**\n * Validate mandate ids loudly, matching `parseCryptoPolicy`'s fail-loud\n * convention: a mistyped id must never silently evaluate to an empty gate.\n * Throws an `Error` naming every unknown id and the known catalog.\n * `evaluateMandates` itself stays lenient (unknown ids are skipped) so callers\n * decide where to fail.\n */\nexport function assertKnownMandates(ids: readonly string[]): void {\n const unknown = ids.filter((id) => !MANDATES[id]);\n if (unknown.length > 0) {\n throw new Error(\n `unknown mandate id(s): ${unknown.join(\", \")}; known mandates: ${mandateIds().join(\", \")}`,\n );\n }\n}\n\n/**\n * A finding's status against a mandate, worst last:\n * - `conformant` — in scope (classical asymmetric) but prohibited by no selected\n * clause (e.g. X25519 as the presumed hybrid leg).\n * - `due` — prohibited, with every deadline still ahead.\n * - `deprecated` — the DEPRECATE deadline has passed; a warning, not a failure.\n * - `violation` — the DISALLOW deadline has passed; fails the default gate.\n */\nexport type MandateStatus = \"conformant\" | \"due\" | \"deprecated\" | \"violation\";\n\nexport interface MandateFindingVerdict {\n ruleId: string;\n algorithm: AlgorithmFamily | \"unknown\";\n file: string;\n line: number;\n /** Mandate id (e.g. \"cnsa-2.0\"). */\n mandate: string;\n /**\n * The governing clause: the next upcoming clause when `due`, the passed\n * DEPRECATE clause when `deprecated`, the passed DISALLOW clause on `violation`.\n */\n clause: string;\n /** ISO effective date of the governing clause. */\n effective: string;\n status: MandateStatus;\n /** Whole months from `now` to the governing deadline; negative once it has passed. */\n monthsUntil: number;\n /**\n * ISO effective date of this mandate's DISALLOW clause for the family, or null\n * when the mandate carries none. The gate's `leadMonths` measures against this.\n */\n disallowEffective: string | null;\n /** Whole months from `now` to `disallowEffective`; null when there is none. */\n monthsUntilDisallow: number | null;\n citation: string;\n}\n\nexport interface MandateEvaluation {\n /** The `now` the evaluation was computed against (ISO). */\n now: string;\n /** Mandate ids evaluated. */\n mandates: string[];\n /**\n * Counts of IN-SCOPE findings (classical asymmetric families) by their worst\n * status across the selected mandates. Out-of-scope findings are excluded so\n * the conformant count is an honest statement about asymmetric crypto only.\n */\n summary: Record<MandateStatus, number>;\n /**\n * Findings outside the mandate's scope (hashes, RNG, dependency, TLS-config…),\n * which a PQC-asymmetric mandate does not adjudicate.\n */\n notInScope: number;\n /** One row per (prohibited finding × applicable mandate). */\n findings: MandateFindingVerdict[];\n /** Earliest still-future deadline across non-violation rows, or null. */\n nextDeadline: string | null;\n /** True when at least one DISALLOW deadline has passed (a `violation` exists). */\n hasViolation: boolean;\n}\n\nconst MONTH_MS = 2_629_800_000; // average month\n\nfunction monthsBetween(fromMs: number, toMs: number): number {\n return Math.round((toMs - fromMs) / MONTH_MS);\n}\n\nconst STATUS_RANK: Record<MandateStatus, number> = {\n conformant: 0,\n due: 1,\n deprecated: 2,\n violation: 3,\n};\n\n/**\n * Evaluate findings against the selected mandates as of `now`. Unknown mandate\n * ids are ignored — callers validate up front with {@link assertKnownMandates}.\n * A finding outside the classical-asymmetric scope is counted in `notInScope`;\n * an in-scope finding no selected mandate prohibits is `conformant`. Neither\n * contributes a verdict row.\n */\nexport function evaluateMandates(\n findings: readonly Finding[],\n mandateIdList: readonly string[],\n now: Date,\n): MandateEvaluation {\n const nowMs = now.getTime();\n const selected = mandateIdList.map(getMandate).filter((m): m is Mandate => Boolean(m));\n\n const rows: MandateFindingVerdict[] = [];\n const perFindingWorst: MandateStatus[] = [];\n let notInScope = 0;\n let nextDeadlineMs: number | null = null;\n\n for (const f of findings) {\n const algo = f.algorithm ?? \"unknown\";\n if (!CLASSICAL_PUBLIC_KEY.includes(algo as AlgorithmFamily)) {\n notInScope++;\n continue;\n }\n let worst: MandateStatus = \"conformant\";\n for (const mandate of selected) {\n // The applicable clauses for this family, earliest deadline first.\n const applicable = mandate.rules\n .filter((r) => r.prohibits.includes(algo as AlgorithmFamily))\n .sort((a, b) => a.effective.localeCompare(b.effective));\n if (applicable.length === 0) continue;\n\n // Tier the clauses so both stay live: a passed DISALLOW clause is a\n // violation; a passed DEPRECATE clause (disallow still ahead) is the\n // deprecated warning tier; otherwise the finding is due against the next\n // upcoming clause.\n const passed = applicable.filter((r) => nowMs >= new Date(r.effective).getTime());\n const passedDisallow = passed.filter((r) => r.tier === \"disallow\");\n let status: MandateStatus;\n let governing: MandateRule;\n if (passedDisallow.length > 0) {\n status = \"violation\";\n governing = passedDisallow[0];\n } else if (passed.length > 0) {\n status = \"deprecated\";\n governing = passed[passed.length - 1];\n } else {\n status = \"due\";\n governing = applicable[0];\n }\n\n // The disallow clause (earliest, if several) anchors `leadMonths`.\n const disallowRule = applicable.find((r) => r.tier === \"disallow\") ?? null;\n const disallowMs = disallowRule ? new Date(disallowRule.effective).getTime() : null;\n\n if (status !== \"violation\") {\n for (const r of applicable) {\n const effMs = new Date(r.effective).getTime();\n if (effMs > nowMs && (nextDeadlineMs === null || effMs < nextDeadlineMs))\n nextDeadlineMs = effMs;\n }\n }\n if (STATUS_RANK[status] > STATUS_RANK[worst]) worst = status;\n rows.push({\n ruleId: f.ruleId,\n algorithm: algo,\n file: f.location.file,\n line: f.location.line,\n mandate: mandate.id,\n clause: governing.clause,\n effective: governing.effective,\n status,\n monthsUntil: monthsBetween(nowMs, new Date(governing.effective).getTime()),\n disallowEffective: disallowRule ? disallowRule.effective : null,\n monthsUntilDisallow: disallowMs !== null ? monthsBetween(nowMs, disallowMs) : null,\n citation: mandate.citation,\n });\n }\n perFindingWorst.push(worst);\n }\n\n const summary: Record<MandateStatus, number> = {\n conformant: 0,\n due: 0,\n deprecated: 0,\n violation: 0,\n };\n for (const s of perFindingWorst) summary[s]++;\n\n return {\n now: now.toISOString(),\n mandates: selected.map((m) => m.id),\n summary,\n notInScope,\n findings: rows,\n nextDeadline:\n nextDeadlineMs !== null ? new Date(nextDeadlineMs).toISOString().slice(0, 10) : null,\n hasViolation: summary.violation > 0,\n };\n}\n\nexport interface MandateGateOptions {\n /** Fail when a DISALLOW deadline is within this many months (early enforcement). */\n leadMonths?: number;\n /** Fail on any mandate-prohibited finding regardless of the deadlines. */\n failNow?: boolean;\n}\n\n/**\n * The gate decision under the \"deadline-aware\" default: fail only once a DISALLOW\n * deadline has passed (`violation`). A passed DEPRECATE date (`deprecated`) is a\n * warning and does not fail the build. `leadMonths` fails early when a disallow\n * deadline is within the window; `failNow` fails on any prohibited finding\n * immediately.\n */\nexport function mandateGateFails(ev: MandateEvaluation, opts: MandateGateOptions = {}): boolean {\n if (ev.hasViolation) return true;\n if (opts.failNow) return ev.findings.length > 0;\n if (opts.leadMonths !== undefined) {\n return ev.findings.some(\n (v) => v.monthsUntilDisallow !== null && v.monthsUntilDisallow <= opts.leadMonths!,\n );\n }\n return false;\n}\n"]}
|
package/dist/parallel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parallel.d.ts","sourceRoot":"","sources":["../src/parallel.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAQ3E,+CAA+C;AAC/C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAMD,kEAAkE;AAClE,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,CAgBzF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,CAa9E;
|
|
1
|
+
{"version":3,"file":"parallel.d.ts","sourceRoot":"","sources":["../src/parallel.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAQ3E,+CAA+C;AAC/C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAMD,kEAAkE;AAClE,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,CAgBzF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,CAa9E;AAwED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAkGpF"}
|
package/dist/parallel.js
CHANGED
|
@@ -13,7 +13,7 @@ import { existsSync } from "node:fs";
|
|
|
13
13
|
import * as os from "node:os";
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import {
|
|
16
|
+
import { walkFilesSized } from "./walk.js";
|
|
17
17
|
import { isAnalyzableSource } from "./detect-utils.js";
|
|
18
18
|
import { buildInventory } from "./inventory.js";
|
|
19
19
|
import { compareFindings, filterExplicitFileList, scan } from "./scan.js";
|
|
@@ -87,32 +87,31 @@ function shouldParallelize(options, files) {
|
|
|
87
87
|
* explicit `files` list or the walker. Sizes power byte-balanced chunking.
|
|
88
88
|
*/
|
|
89
89
|
async function enumerateFiles(options, baseDir) {
|
|
90
|
-
const
|
|
90
|
+
const sized = [];
|
|
91
91
|
if (options.files) {
|
|
92
|
+
// Explicit file list: the walker isn't involved, so stat each once here.
|
|
92
93
|
// Apply the SAME include/exclude/binary filtering the serial path uses via
|
|
93
|
-
// `
|
|
94
|
-
for (const rel of filterExplicitFileList(options.files, options))
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
})) {
|
|
104
|
-
rels.push(rel);
|
|
94
|
+
// `filterExplicitFileList`, so `--parallel` is byte-for-byte identical to serial.
|
|
95
|
+
for (const rel of filterExplicitFileList(options.files, options)) {
|
|
96
|
+
let size = 0;
|
|
97
|
+
try {
|
|
98
|
+
size = (await stat(path.join(baseDir, ...rel.split("/")))).size;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Unreadable now; keep with size 0 (the worker read will skip if it's gone).
|
|
102
|
+
}
|
|
103
|
+
sized.push({ rel, size });
|
|
105
104
|
}
|
|
105
|
+
return sized;
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
107
|
+
// Directory walk: reuse the size the walker already stat'd while enforcing the
|
|
108
|
+
// size limit, so files are not stat'd a second time here (double-stat fix).
|
|
109
|
+
for await (const { rel, size } of walkFilesSized(options.root, {
|
|
110
|
+
include: options.include,
|
|
111
|
+
exclude: options.exclude,
|
|
112
|
+
noDefaultIgnores: options.noDefaultIgnores,
|
|
113
|
+
maxFileSize: options.maxFileSize,
|
|
114
|
+
})) {
|
|
116
115
|
sized.push({ rel, size });
|
|
117
116
|
}
|
|
118
117
|
return sized;
|
package/dist/parallel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parallel.js","sourceRoot":"","sources":["../src/parallel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Parallel scanning over a worker-thread pool (P2-1). The scan is embarrassingly\n * parallel: each file is read and regex-scanned independently, and the merge is\n * a concatenation + the existing deterministic sort. `Finding` is plain\n * structured-cloneable data, so it crosses the worker boundary cleanly.\n *\n * The merge and chunking logic are pure and exported for direct unit testing.\n * `scanParallel` falls back to the in-process serial path below the crossover\n * (small file counts / small total bytes) and whenever workers are unavailable.\n */\nimport { stat } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type { Worker as NodeWorker } from \"node:worker_threads\";\n\nimport type { Finding, ParallelScanOptions, ScanResult } from \"./types.js\";\nimport { walkFiles } from \"./walk.js\";\nimport { isAnalyzableSource } from \"./detect-utils.js\";\nimport { buildInventory } from \"./inventory.js\";\nimport { compareFindings, filterExplicitFileList, scan } from \"./scan.js\";\nimport { AbortError, BudgetExceededError } from \"./errors.js\";\nimport { VERSION } from \"./version.js\";\n\n/** One unit of work dispatched to a worker. */\nexport interface ScanChunk {\n files: string[];\n}\n\n/** What a worker returns for a chunk. */\nexport interface ChunkResult {\n findings: Finding[];\n filesScanned: number;\n /** Files that couldn't be read in this chunk (optional; older workers omit it). */\n unreadable?: number;\n /** Files skipped as minified in this chunk (optional; older workers omit it). */\n skippedMinified?: number;\n}\n\nconst DEFAULT_PARALLEL_THRESHOLD_BYTES = 2 * 1024 * 1024;\nconst DEFAULT_PARALLEL_FILE_THRESHOLD = 200;\nconst DEFAULT_CHUNK_BYTES = 4 * 1024 * 1024;\n\n/** A file plus its byte size, used for byte-balanced chunking. */\nexport interface SizedFile {\n rel: string;\n size: number;\n}\n\n/**\n * Bucket files into chunks of roughly `chunkBytes` total bytes each, so one\n * large file doesn't starve a worker holding many tiny files. Pure +\n * deterministic: input order is preserved within and across chunks.\n */\nexport function chunkByBytes(files: readonly SizedFile[], chunkBytes: number): ScanChunk[] {\n const limit = Math.max(1, chunkBytes);\n const chunks: ScanChunk[] = [];\n let current: string[] = [];\n let currentBytes = 0;\n for (const f of files) {\n if (current.length > 0 && currentBytes + f.size > limit) {\n chunks.push({ files: current });\n current = [];\n currentBytes = 0;\n }\n current.push(f.rel);\n currentBytes += f.size;\n }\n if (current.length > 0) chunks.push({ files: current });\n return chunks;\n}\n\n/**\n * Merge per-chunk results into a single ordered finding set + total file count.\n * Pure: applies the SAME comparator as the serial scan, so the result is\n * byte-identical regardless of chunk completion order. Exported for unit tests.\n */\nexport function mergeChunkResults(results: readonly ChunkResult[]): ChunkResult {\n const findings: Finding[] = [];\n let filesScanned = 0;\n let unreadable = 0;\n let skippedMinified = 0;\n for (const r of results) {\n for (const f of r.findings) findings.push(f);\n filesScanned += r.filesScanned;\n unreadable += r.unreadable ?? 0;\n skippedMinified += r.skippedMinified ?? 0;\n }\n findings.sort(compareFindings);\n return { findings, filesScanned, unreadable, skippedMinified };\n}\n\n/** Resolve the worker count (>= 1). */\nfunction resolveConcurrency(options: ParallelScanOptions): number {\n const raw = options.concurrency;\n if (typeof raw === \"number\" && raw >= 1) return Math.floor(raw);\n const avail =\n typeof os.availableParallelism === \"function\" ? os.availableParallelism() : os.cpus().length;\n return Math.max(1, avail);\n}\n\n/** Decide whether the workload is large enough to justify spawning workers. */\nfunction shouldParallelize(options: ParallelScanOptions, files: SizedFile[]): boolean {\n const totalBytes = files.reduce((acc, f) => acc + f.size, 0);\n const byteFloor = options.parallelThresholdBytes ?? DEFAULT_PARALLEL_THRESHOLD_BYTES;\n const fileFloor = options.parallelFileThreshold ?? DEFAULT_PARALLEL_FILE_THRESHOLD;\n if (resolveConcurrency(options) <= 1) return false;\n return totalBytes >= byteFloor && files.length >= fileFloor;\n}\n\n/**\n * Enumerate the files to scan (relative POSIX paths + sizes), honouring an\n * explicit `files` list or the walker. Sizes power byte-balanced chunking.\n */\nasync function enumerateFiles(options: ParallelScanOptions, baseDir: string): Promise<SizedFile[]> {\n const rels: string[] = [];\n if (options.files) {\n // Apply the SAME include/exclude/binary filtering the serial path uses via\n // `filterExplicitFiles`, so `--parallel` is byte-for-byte identical to serial.\n for (const rel of filterExplicitFileList(options.files, options)) rels.push(rel);\n } else {\n for await (const rel of walkFiles(options.root, {\n include: options.include,\n exclude: options.exclude,\n noDefaultIgnores: options.noDefaultIgnores,\n maxFileSize: options.maxFileSize,\n })) {\n rels.push(rel);\n }\n }\n\n const sized: SizedFile[] = [];\n for (const rel of rels) {\n let size = 0;\n try {\n size = (await stat(path.join(baseDir, ...rel.split(\"/\")))).size;\n } catch {\n // Unreadable now; keep with size 0 — worker read will skip if it's gone.\n }\n sized.push({ rel, size });\n }\n return sized;\n}\n\n/**\n * Resolve the worker entry next to this module. In a normal build it's\n * `dist/scan-worker.js`. When running from source under a TypeScript loader\n * (tsx) the built JS doesn't exist, so fall back to `scan-worker.ts` and tell\n * the worker to load tsx as well — so the parallel scanner works in dev/source\n * mode (and is exercisable by tests), not only after a build.\n */\nfunction workerEntry(): { entry: string; execArgv?: string[] } {\n const here = fileURLToPath(import.meta.url);\n const dir = path.dirname(here);\n const js = path.join(dir, \"scan-worker.js\");\n if (existsSync(js)) return { entry: js };\n const ts = path.join(dir, \"scan-worker.ts\");\n if (existsSync(ts)) return { entry: ts, execArgv: [\"--import\", \"tsx\"] };\n return { entry: js }; // neither present: let Worker surface a clear ENOENT\n}\n\n/**\n * Scan in parallel across a worker-thread pool, falling back to the in-process\n * serial {@link scan} for small workloads or when workers can't be used. The\n * result is deterministic and identical to the serial path.\n */\nexport async function scanParallel(options: ParallelScanOptions): Promise<ScanResult> {\n const startedAt = new Date();\n\n const rootStat = await stat(options.root);\n const baseDir = rootStat.isFile() ? path.dirname(options.root) : options.root;\n\n // Single-file roots, the override-detectors path, and the scan cache always\n // run serially: detectors may not be structured-cloneable across the worker\n // boundary, and the cache read/write is owned by the in-process `scan()`.\n if (rootStat.isFile() || options.detectors || options.cacheFile) {\n return scan(options);\n }\n\n const files = await enumerateFiles(options, baseDir);\n\n // Budget + cancellation parity with the serial path. The parallel path\n // enumerates upfront, so budgets are enforced against the whole file set\n // before any worker is dispatched.\n if (options.signal?.aborted) throw new AbortError();\n if (typeof options.maxFiles === \"number\" && files.length > options.maxFiles) {\n throw new BudgetExceededError(`maxFiles budget exceeded (limit: ${options.maxFiles}).`);\n }\n if (typeof options.maxBytes === \"number\") {\n const totalBytes = files.reduce((n, f) => n + f.size, 0);\n if (totalBytes > options.maxBytes) {\n throw new BudgetExceededError(`maxBytes budget exceeded (limit: ${options.maxBytes}).`);\n }\n }\n\n if (!shouldParallelize(options, files)) {\n // In-process: reuse the exact serial path over the same file list.\n return scan({ ...options, files: files.map((f) => f.rel) });\n }\n\n let WorkerCtor: typeof import(\"node:worker_threads\").Worker;\n try {\n ({ Worker: WorkerCtor } = await import(\"node:worker_threads\"));\n } catch {\n return scan({ ...options, files: files.map((f) => f.rel) });\n }\n\n const chunks = chunkByBytes(files, options.chunkBytes ?? DEFAULT_CHUNK_BYTES);\n const concurrency = Math.min(resolveConcurrency(options), chunks.length);\n const { entry, execArgv } = workerEntry();\n\n const toggles = {\n source: options.source !== false,\n config: options.config !== false,\n deps: options.dependencies !== false,\n scanMinified: options.scanMinified === true,\n // Plain string array — structured-cloneable, so it crosses the worker boundary.\n disabledRules: options.disabledRules,\n };\n\n let results: ChunkResult[];\n try {\n results = await runPool(\n WorkerCtor,\n entry,\n execArgv,\n baseDir,\n toggles,\n chunks,\n concurrency,\n options.onFile,\n options.signal,\n );\n } catch (err) {\n // Cancellation / budget overflow must propagate, not silently degrade.\n if (err instanceof AbortError || err instanceof BudgetExceededError) throw err;\n // Any other worker failure → safe fallback to the serial path.\n return scan({ ...options, files: files.map((f) => f.rel) });\n }\n\n const merged = mergeChunkResults(results);\n const inventory = buildInventory(merged.findings);\n const finishedAt = new Date();\n\n // Coverage: count analyzable-source files in the enumerated set. Computed from\n // the file list (not per-worker) to avoid worker-boundary plumbing; on the\n // parallel path this can include a minified analyzable file the workers\n // skipped, but that is vanishingly rare and never under-reports coverage.\n const analyzedFiles = files.reduce((n, f) => (isAnalyzableSource(f.rel) ? n + 1 : n), 0);\n\n return {\n root: options.root,\n findings: merged.findings,\n filesScanned: merged.filesScanned,\n analyzedFiles,\n diagnostics: {\n unreadable: merged.unreadable ?? 0,\n skippedMinified: merged.skippedMinified ?? 0,\n },\n inventory,\n startedAt: startedAt.toISOString(),\n finishedAt: finishedAt.toISOString(),\n toolVersion: VERSION,\n };\n}\n\n/** Worker-pool driver: at most `concurrency` workers, each pulls chunks. */\nfunction runPool(\n WorkerCtor: typeof import(\"node:worker_threads\").Worker,\n entry: string,\n execArgv: string[] | undefined,\n baseDir: string,\n toggles: {\n source: boolean;\n config: boolean;\n deps: boolean;\n scanMinified: boolean;\n disabledRules?: string[];\n },\n chunks: ScanChunk[],\n concurrency: number,\n onFile?: (file: string) => void,\n signal?: AbortSignal,\n): Promise<ChunkResult[]> {\n return new Promise((resolve, reject) => {\n const results: ChunkResult[] = new Array(chunks.length);\n let next = 0;\n let done = 0;\n let failed = false;\n const workers: Array<NodeWorker> = [];\n\n const onAbort = (): void => {\n if (failed) return;\n failed = true;\n cleanup();\n reject(new AbortError());\n };\n\n const cleanup = () => {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n for (const w of workers) void w.terminate();\n };\n\n // Cooperative cancellation: stop dispatching and tear down on abort.\n if (signal) {\n if (signal.aborted) {\n reject(new AbortError());\n return;\n }\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n // Workers we intentionally terminate (no more chunks / on cleanup). Their\n // `exit` event is expected and must NOT be treated as a crash.\n const retired = new WeakSet<NodeWorker>();\n\n const dispatch = (w: NodeWorker): void => {\n if (failed) return;\n if (next >= chunks.length) {\n retired.add(w);\n void w.terminate();\n return;\n }\n const idx = next++;\n w.postMessage({ index: idx, files: chunks[idx].files });\n };\n\n const spawn = (): NodeWorker => {\n const w = new WorkerCtor(entry, {\n workerData: { baseDir, toggles },\n ...(execArgv ? { execArgv } : {}),\n });\n w.on(\n \"message\",\n (msg: { index: number; result?: ChunkResult; files?: string[]; error?: string }) => {\n if (msg.error) {\n if (!failed) {\n failed = true;\n cleanup();\n reject(new Error(msg.error));\n }\n return;\n }\n if (msg.files && onFile) for (const f of msg.files) onFile(f);\n if (msg.result) {\n results[msg.index] = msg.result;\n done++;\n if (done === chunks.length) {\n cleanup();\n resolve(results);\n return;\n }\n dispatch(w);\n }\n },\n );\n w.on(\"error\", (err) => {\n if (!failed) {\n failed = true;\n cleanup();\n reject(err);\n }\n });\n // A worker that dies WITHOUT an `error` or `message` (e.g. a `process.exit`\n // in loaded code) would otherwise leave the pool pending forever. Reject so\n // scanParallel falls back to the serial path (audit: arch #5). Expected\n // exits (retired workers, post-completion cleanup) are ignored.\n w.on(\"exit\", (code) => {\n if (failed || retired.has(w) || done === chunks.length) return;\n failed = true;\n cleanup();\n reject(new Error(`scan worker exited early (code ${code}) before completing its chunk`));\n });\n return w;\n };\n\n const n = Math.max(1, Math.min(concurrency, chunks.length));\n for (let i = 0; i < n; i++) {\n const w = spawn();\n workers.push(w);\n dispatch(w);\n }\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"parallel.js","sourceRoot":"","sources":["../src/parallel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Parallel scanning over a worker-thread pool (P2-1). The scan is embarrassingly\n * parallel: each file is read and regex-scanned independently, and the merge is\n * a concatenation + the existing deterministic sort. `Finding` is plain\n * structured-cloneable data, so it crosses the worker boundary cleanly.\n *\n * The merge and chunking logic are pure and exported for direct unit testing.\n * `scanParallel` falls back to the in-process serial path below the crossover\n * (small file counts / small total bytes) and whenever workers are unavailable.\n */\nimport { stat } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type { Worker as NodeWorker } from \"node:worker_threads\";\n\nimport type { Finding, ParallelScanOptions, ScanResult } from \"./types.js\";\nimport { walkFilesSized } from \"./walk.js\";\nimport { isAnalyzableSource } from \"./detect-utils.js\";\nimport { buildInventory } from \"./inventory.js\";\nimport { compareFindings, filterExplicitFileList, scan } from \"./scan.js\";\nimport { AbortError, BudgetExceededError } from \"./errors.js\";\nimport { VERSION } from \"./version.js\";\n\n/** One unit of work dispatched to a worker. */\nexport interface ScanChunk {\n files: string[];\n}\n\n/** What a worker returns for a chunk. */\nexport interface ChunkResult {\n findings: Finding[];\n filesScanned: number;\n /** Files that couldn't be read in this chunk (optional; older workers omit it). */\n unreadable?: number;\n /** Files skipped as minified in this chunk (optional; older workers omit it). */\n skippedMinified?: number;\n}\n\nconst DEFAULT_PARALLEL_THRESHOLD_BYTES = 2 * 1024 * 1024;\nconst DEFAULT_PARALLEL_FILE_THRESHOLD = 200;\nconst DEFAULT_CHUNK_BYTES = 4 * 1024 * 1024;\n\n/** A file plus its byte size, used for byte-balanced chunking. */\nexport interface SizedFile {\n rel: string;\n size: number;\n}\n\n/**\n * Bucket files into chunks of roughly `chunkBytes` total bytes each, so one\n * large file doesn't starve a worker holding many tiny files. Pure +\n * deterministic: input order is preserved within and across chunks.\n */\nexport function chunkByBytes(files: readonly SizedFile[], chunkBytes: number): ScanChunk[] {\n const limit = Math.max(1, chunkBytes);\n const chunks: ScanChunk[] = [];\n let current: string[] = [];\n let currentBytes = 0;\n for (const f of files) {\n if (current.length > 0 && currentBytes + f.size > limit) {\n chunks.push({ files: current });\n current = [];\n currentBytes = 0;\n }\n current.push(f.rel);\n currentBytes += f.size;\n }\n if (current.length > 0) chunks.push({ files: current });\n return chunks;\n}\n\n/**\n * Merge per-chunk results into a single ordered finding set + total file count.\n * Pure: applies the SAME comparator as the serial scan, so the result is\n * byte-identical regardless of chunk completion order. Exported for unit tests.\n */\nexport function mergeChunkResults(results: readonly ChunkResult[]): ChunkResult {\n const findings: Finding[] = [];\n let filesScanned = 0;\n let unreadable = 0;\n let skippedMinified = 0;\n for (const r of results) {\n for (const f of r.findings) findings.push(f);\n filesScanned += r.filesScanned;\n unreadable += r.unreadable ?? 0;\n skippedMinified += r.skippedMinified ?? 0;\n }\n findings.sort(compareFindings);\n return { findings, filesScanned, unreadable, skippedMinified };\n}\n\n/** Resolve the worker count (>= 1). */\nfunction resolveConcurrency(options: ParallelScanOptions): number {\n const raw = options.concurrency;\n if (typeof raw === \"number\" && raw >= 1) return Math.floor(raw);\n const avail =\n typeof os.availableParallelism === \"function\" ? os.availableParallelism() : os.cpus().length;\n return Math.max(1, avail);\n}\n\n/** Decide whether the workload is large enough to justify spawning workers. */\nfunction shouldParallelize(options: ParallelScanOptions, files: SizedFile[]): boolean {\n const totalBytes = files.reduce((acc, f) => acc + f.size, 0);\n const byteFloor = options.parallelThresholdBytes ?? DEFAULT_PARALLEL_THRESHOLD_BYTES;\n const fileFloor = options.parallelFileThreshold ?? DEFAULT_PARALLEL_FILE_THRESHOLD;\n if (resolveConcurrency(options) <= 1) return false;\n return totalBytes >= byteFloor && files.length >= fileFloor;\n}\n\n/**\n * Enumerate the files to scan (relative POSIX paths + sizes), honouring an\n * explicit `files` list or the walker. Sizes power byte-balanced chunking.\n */\nasync function enumerateFiles(options: ParallelScanOptions, baseDir: string): Promise<SizedFile[]> {\n const sized: SizedFile[] = [];\n if (options.files) {\n // Explicit file list: the walker isn't involved, so stat each once here.\n // Apply the SAME include/exclude/binary filtering the serial path uses via\n // `filterExplicitFileList`, so `--parallel` is byte-for-byte identical to serial.\n for (const rel of filterExplicitFileList(options.files, options)) {\n let size = 0;\n try {\n size = (await stat(path.join(baseDir, ...rel.split(\"/\")))).size;\n } catch {\n // Unreadable now; keep with size 0 (the worker read will skip if it's gone).\n }\n sized.push({ rel, size });\n }\n return sized;\n }\n\n // Directory walk: reuse the size the walker already stat'd while enforcing the\n // size limit, so files are not stat'd a second time here (double-stat fix).\n for await (const { rel, size } of walkFilesSized(options.root, {\n include: options.include,\n exclude: options.exclude,\n noDefaultIgnores: options.noDefaultIgnores,\n maxFileSize: options.maxFileSize,\n })) {\n sized.push({ rel, size });\n }\n return sized;\n}\n\n/**\n * Resolve the worker entry next to this module. In a normal build it's\n * `dist/scan-worker.js`. When running from source under a TypeScript loader\n * (tsx) the built JS doesn't exist, so fall back to `scan-worker.ts` and tell\n * the worker to load tsx as well — so the parallel scanner works in dev/source\n * mode (and is exercisable by tests), not only after a build.\n */\nfunction workerEntry(): { entry: string; execArgv?: string[] } {\n const here = fileURLToPath(import.meta.url);\n const dir = path.dirname(here);\n const js = path.join(dir, \"scan-worker.js\");\n if (existsSync(js)) return { entry: js };\n const ts = path.join(dir, \"scan-worker.ts\");\n if (existsSync(ts)) return { entry: ts, execArgv: [\"--import\", \"tsx\"] };\n return { entry: js }; // neither present: let Worker surface a clear ENOENT\n}\n\n/**\n * Scan in parallel across a worker-thread pool, falling back to the in-process\n * serial {@link scan} for small workloads or when workers can't be used. The\n * result is deterministic and identical to the serial path.\n */\nexport async function scanParallel(options: ParallelScanOptions): Promise<ScanResult> {\n const startedAt = new Date();\n\n const rootStat = await stat(options.root);\n const baseDir = rootStat.isFile() ? path.dirname(options.root) : options.root;\n\n // Single-file roots, the override-detectors path, and the scan cache always\n // run serially: detectors may not be structured-cloneable across the worker\n // boundary, and the cache read/write is owned by the in-process `scan()`.\n if (rootStat.isFile() || options.detectors || options.cacheFile) {\n return scan(options);\n }\n\n const files = await enumerateFiles(options, baseDir);\n\n // Budget + cancellation parity with the serial path. The parallel path\n // enumerates upfront, so budgets are enforced against the whole file set\n // before any worker is dispatched.\n if (options.signal?.aborted) throw new AbortError();\n if (typeof options.maxFiles === \"number\" && files.length > options.maxFiles) {\n throw new BudgetExceededError(`maxFiles budget exceeded (limit: ${options.maxFiles}).`);\n }\n if (typeof options.maxBytes === \"number\") {\n const totalBytes = files.reduce((n, f) => n + f.size, 0);\n if (totalBytes > options.maxBytes) {\n throw new BudgetExceededError(`maxBytes budget exceeded (limit: ${options.maxBytes}).`);\n }\n }\n\n if (!shouldParallelize(options, files)) {\n // In-process: reuse the exact serial path over the same file list.\n return scan({ ...options, files: files.map((f) => f.rel) });\n }\n\n let WorkerCtor: typeof import(\"node:worker_threads\").Worker;\n try {\n ({ Worker: WorkerCtor } = await import(\"node:worker_threads\"));\n } catch {\n return scan({ ...options, files: files.map((f) => f.rel) });\n }\n\n const chunks = chunkByBytes(files, options.chunkBytes ?? DEFAULT_CHUNK_BYTES);\n const concurrency = Math.min(resolveConcurrency(options), chunks.length);\n const { entry, execArgv } = workerEntry();\n\n const toggles = {\n source: options.source !== false,\n config: options.config !== false,\n deps: options.dependencies !== false,\n scanMinified: options.scanMinified === true,\n // Plain string array — structured-cloneable, so it crosses the worker boundary.\n disabledRules: options.disabledRules,\n };\n\n let results: ChunkResult[];\n try {\n results = await runPool(\n WorkerCtor,\n entry,\n execArgv,\n baseDir,\n toggles,\n chunks,\n concurrency,\n options.onFile,\n options.signal,\n );\n } catch (err) {\n // Cancellation / budget overflow must propagate, not silently degrade.\n if (err instanceof AbortError || err instanceof BudgetExceededError) throw err;\n // Any other worker failure → safe fallback to the serial path.\n return scan({ ...options, files: files.map((f) => f.rel) });\n }\n\n const merged = mergeChunkResults(results);\n const inventory = buildInventory(merged.findings);\n const finishedAt = new Date();\n\n // Coverage: count analyzable-source files in the enumerated set. Computed from\n // the file list (not per-worker) to avoid worker-boundary plumbing; on the\n // parallel path this can include a minified analyzable file the workers\n // skipped, but that is vanishingly rare and never under-reports coverage.\n const analyzedFiles = files.reduce((n, f) => (isAnalyzableSource(f.rel) ? n + 1 : n), 0);\n\n return {\n root: options.root,\n findings: merged.findings,\n filesScanned: merged.filesScanned,\n analyzedFiles,\n diagnostics: {\n unreadable: merged.unreadable ?? 0,\n skippedMinified: merged.skippedMinified ?? 0,\n },\n inventory,\n startedAt: startedAt.toISOString(),\n finishedAt: finishedAt.toISOString(),\n toolVersion: VERSION,\n };\n}\n\n/** Worker-pool driver: at most `concurrency` workers, each pulls chunks. */\nfunction runPool(\n WorkerCtor: typeof import(\"node:worker_threads\").Worker,\n entry: string,\n execArgv: string[] | undefined,\n baseDir: string,\n toggles: {\n source: boolean;\n config: boolean;\n deps: boolean;\n scanMinified: boolean;\n disabledRules?: string[];\n },\n chunks: ScanChunk[],\n concurrency: number,\n onFile?: (file: string) => void,\n signal?: AbortSignal,\n): Promise<ChunkResult[]> {\n return new Promise((resolve, reject) => {\n const results: ChunkResult[] = new Array(chunks.length);\n let next = 0;\n let done = 0;\n let failed = false;\n const workers: Array<NodeWorker> = [];\n\n const onAbort = (): void => {\n if (failed) return;\n failed = true;\n cleanup();\n reject(new AbortError());\n };\n\n const cleanup = () => {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n for (const w of workers) void w.terminate();\n };\n\n // Cooperative cancellation: stop dispatching and tear down on abort.\n if (signal) {\n if (signal.aborted) {\n reject(new AbortError());\n return;\n }\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n // Workers we intentionally terminate (no more chunks / on cleanup). Their\n // `exit` event is expected and must NOT be treated as a crash.\n const retired = new WeakSet<NodeWorker>();\n\n const dispatch = (w: NodeWorker): void => {\n if (failed) return;\n if (next >= chunks.length) {\n retired.add(w);\n void w.terminate();\n return;\n }\n const idx = next++;\n w.postMessage({ index: idx, files: chunks[idx].files });\n };\n\n const spawn = (): NodeWorker => {\n const w = new WorkerCtor(entry, {\n workerData: { baseDir, toggles },\n ...(execArgv ? { execArgv } : {}),\n });\n w.on(\n \"message\",\n (msg: { index: number; result?: ChunkResult; files?: string[]; error?: string }) => {\n if (msg.error) {\n if (!failed) {\n failed = true;\n cleanup();\n reject(new Error(msg.error));\n }\n return;\n }\n if (msg.files && onFile) for (const f of msg.files) onFile(f);\n if (msg.result) {\n results[msg.index] = msg.result;\n done++;\n if (done === chunks.length) {\n cleanup();\n resolve(results);\n return;\n }\n dispatch(w);\n }\n },\n );\n w.on(\"error\", (err) => {\n if (!failed) {\n failed = true;\n cleanup();\n reject(err);\n }\n });\n // A worker that dies WITHOUT an `error` or `message` (e.g. a `process.exit`\n // in loaded code) would otherwise leave the pool pending forever. Reject so\n // scanParallel falls back to the serial path (audit: arch #5). Expected\n // exits (retired workers, post-completion cleanup) are ignored.\n w.on(\"exit\", (code) => {\n if (failed || retired.has(w) || done === chunks.length) return;\n failed = true;\n cleanup();\n reject(new Error(`scan worker exited early (code ${code}) before completing its chunk`));\n });\n return w;\n };\n\n const n = Math.max(1, Math.min(concurrency, chunks.length));\n for (let i = 0; i < n; i++) {\n const w = spawn();\n workers.push(w);\n dispatch(w);\n }\n });\n}\n"]}
|
package/dist/report.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { RuleMeta, ScanResult } from "./types.js";
|
|
7
7
|
import type { SecurityTier } from "./remediation.js";
|
|
8
8
|
import type { StandardsProfile } from "./standards-profiles.js";
|
|
9
|
+
import type { HndlReport } from "./hndl.js";
|
|
9
10
|
/** Minimal SARIF 2.1.0 log shape (kept permissive on purpose). */
|
|
10
11
|
export interface SarifLog {
|
|
11
12
|
$schema: string;
|
|
@@ -29,6 +30,13 @@ export interface ReportOptions {
|
|
|
29
30
|
* {@link toJson}.
|
|
30
31
|
*/
|
|
31
32
|
catalog?: RuleMeta[];
|
|
33
|
+
/**
|
|
34
|
+
* Optional HNDL exposure analysis ({@link computeHndl}). When supplied, each
|
|
35
|
+
* finding gains its `exposure` fields (score, bound data asset, rationale)
|
|
36
|
+
* keyed by fingerprint, and the report carries the repo-level HNDL summary.
|
|
37
|
+
* Purely additive: it never changes finding identity, ordering, or exit codes.
|
|
38
|
+
*/
|
|
39
|
+
hndl?: HndlReport;
|
|
32
40
|
}
|
|
33
41
|
/** Serialize a scan result as SARIF 2.1.0. */
|
|
34
42
|
export declare function toSarif(result: ScanResult, opts?: ReportOptions): SarifLog;
|
package/dist/report.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAA4B,QAAQ,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAK3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,kEAAkE;AAClE,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB;AAED,qFAAqF;AACrF,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAA4B,QAAQ,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AAK3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,KAAK,EAAmB,UAAU,EAAE,MAAM,WAAW,CAAC;AAE7D,kEAAkE;AAClE,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB;AAED,qFAAqF;AACrF,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AA+FD,8CAA8C;AAC9C,wBAAgB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,QAAQ,CAyI1E;AAkBD,+DAA+D;AAC/D,wBAAgB,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA6DxF;AAwCD;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,YAAY,CAAA;CAAE,GACjD,MAAM,CAyGR;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,IAAI,EAAE,YAAY,GACjB,MAAM,EAAE,CAmBV;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACnC,OAAO,EAAE,gBAAgB,GACxB,MAAM,EAAE,CAsBV"}
|
package/dist/report.js
CHANGED
|
@@ -3,6 +3,22 @@ import { SEVERITY_ORDER, sarifLevel } from "./severity.js";
|
|
|
3
3
|
import { ANALYZABLE_LANGUAGES_LABEL } from "./detect-utils.js";
|
|
4
4
|
import { remediationFor, remediationForTier, remediationForProfile } from "./remediation.js";
|
|
5
5
|
import { fingerprintFinding } from "./baseline.js";
|
|
6
|
+
import { findingFingerprint } from "./hndl.js";
|
|
7
|
+
/** The per-finding exposure block emitted in JSON / SARIF, or undefined. */
|
|
8
|
+
function exposureFor(f, hndl) {
|
|
9
|
+
if (!hndl)
|
|
10
|
+
return undefined;
|
|
11
|
+
return hndl.byFingerprint.get(findingFingerprint(f));
|
|
12
|
+
}
|
|
13
|
+
/** The repo HNDL summary block shared by JSON output and the SARIF run. */
|
|
14
|
+
function hndlSummaryBlock(hndl) {
|
|
15
|
+
return {
|
|
16
|
+
modelVersion: hndl.modelVersion,
|
|
17
|
+
horizon: hndl.horizon,
|
|
18
|
+
summary: hndl.summary,
|
|
19
|
+
assets: hndl.assets,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
6
22
|
const SARIF_SCHEMA = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json";
|
|
7
23
|
const INFORMATION_URI = "https://github.com/quantakrypto/pqc-tools";
|
|
8
24
|
/**
|
|
@@ -55,6 +71,16 @@ function sarifRule(spec) {
|
|
|
55
71
|
: {}),
|
|
56
72
|
};
|
|
57
73
|
}
|
|
74
|
+
/** SARIF result.properties fragment for a finding's HNDL exposure, or empty. */
|
|
75
|
+
function exposureProperties(exposure) {
|
|
76
|
+
if (!exposure)
|
|
77
|
+
return {};
|
|
78
|
+
return {
|
|
79
|
+
exposureScore: exposure.exposureScore,
|
|
80
|
+
dataAsset: exposure.dataAsset,
|
|
81
|
+
exposureRationale: exposure.rationale,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
58
84
|
/** Serialize a scan result as SARIF 2.1.0. */
|
|
59
85
|
export function toSarif(result, opts) {
|
|
60
86
|
const redactSnippets = opts?.redactSnippets ?? false;
|
|
@@ -119,6 +145,10 @@ export function toSarif(result, opts) {
|
|
|
119
145
|
// edit above it. `quantakrypto/v1` names our scheme.
|
|
120
146
|
partialFingerprints: { "quantakrypto/v1": fingerprintFinding(f) },
|
|
121
147
|
properties: {
|
|
148
|
+
// Same stable identity mirrored into properties so non-GitHub SARIF
|
|
149
|
+
// consumers (our platform ingest) can read one uniform `fingerprint`
|
|
150
|
+
// field across JSON and SARIF without reaching into partialFingerprints.
|
|
151
|
+
fingerprint: fingerprintFinding(f),
|
|
122
152
|
category: f.category,
|
|
123
153
|
severity: f.severity,
|
|
124
154
|
confidence: f.confidence,
|
|
@@ -126,6 +156,7 @@ export function toSarif(result, opts) {
|
|
|
126
156
|
...(f.algorithm ? { algorithm: f.algorithm } : {}),
|
|
127
157
|
...(f.remediation ? { remediation: f.remediation } : {}),
|
|
128
158
|
...(f.cwe ? { cwe: f.cwe } : {}),
|
|
159
|
+
...exposureProperties(exposureFor(f, opts?.hndl)),
|
|
129
160
|
},
|
|
130
161
|
...(f.cwe
|
|
131
162
|
? {
|
|
@@ -178,6 +209,7 @@ export function toSarif(result, opts) {
|
|
|
178
209
|
},
|
|
179
210
|
},
|
|
180
211
|
...(taxonomies.length > 0 ? { taxonomies } : {}),
|
|
212
|
+
...(opts?.hndl ? { properties: { hndl: hndlSummaryBlock(opts.hndl) } } : {}),
|
|
181
213
|
results,
|
|
182
214
|
},
|
|
183
215
|
],
|
|
@@ -201,6 +233,7 @@ function securitySeverity(severity) {
|
|
|
201
233
|
/** Serialize a scan result as a plain JSON-friendly object. */
|
|
202
234
|
export function toJson(result, opts) {
|
|
203
235
|
const redactSnippets = opts?.redactSnippets ?? false;
|
|
236
|
+
const hndl = opts?.hndl;
|
|
204
237
|
return {
|
|
205
238
|
toolVersion: result.toolVersion,
|
|
206
239
|
root: result.root,
|
|
@@ -216,25 +249,48 @@ export function toJson(result, opts) {
|
|
|
216
249
|
byCategory: result.inventory.byCategory,
|
|
217
250
|
byAlgorithm: result.inventory.byAlgorithm,
|
|
218
251
|
},
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
252
|
+
...(hndl ? { hndl: hndlSummaryBlock(hndl) } : {}),
|
|
253
|
+
findings: result.findings.map((f) => {
|
|
254
|
+
const exposure = exposureFor(f, hndl);
|
|
255
|
+
return {
|
|
256
|
+
// Stable, line-INSENSITIVE identity of the finding: sha256 of
|
|
257
|
+
// ruleId | normalized-POSIX-repo-relative-path | normalized-snippet
|
|
258
|
+
// (the SARIF partialFingerprints trick, line number deliberately
|
|
259
|
+
// excluded). Reused verbatim from the baseline module so JSON identity,
|
|
260
|
+
// SARIF partialFingerprints, and the baseline suppression set are one and
|
|
261
|
+
// the same value. A line move does NOT change it; when no snippet context
|
|
262
|
+
// exists it falls back to ruleId|path. This is the cross-run identity the
|
|
263
|
+
// platform keys posture drift on.
|
|
264
|
+
fingerprint: fingerprintFinding(f),
|
|
265
|
+
ruleId: f.ruleId,
|
|
266
|
+
title: f.title,
|
|
267
|
+
category: f.category,
|
|
268
|
+
severity: f.severity,
|
|
269
|
+
confidence: f.confidence,
|
|
270
|
+
algorithm: f.algorithm,
|
|
271
|
+
hndl: f.hndl,
|
|
272
|
+
message: f.message,
|
|
273
|
+
remediation: f.remediation,
|
|
274
|
+
cwe: f.cwe,
|
|
275
|
+
location: {
|
|
276
|
+
file: f.location.file,
|
|
277
|
+
line: f.location.line,
|
|
278
|
+
column: f.location.column,
|
|
279
|
+
endLine: f.location.endLine,
|
|
280
|
+
snippet: emittedSnippet(f, redactSnippets),
|
|
281
|
+
},
|
|
282
|
+
...(exposure
|
|
283
|
+
? {
|
|
284
|
+
exposure: {
|
|
285
|
+
fingerprint: exposure.fingerprint,
|
|
286
|
+
exposureScore: exposure.exposureScore,
|
|
287
|
+
dataAsset: exposure.dataAsset,
|
|
288
|
+
rationale: exposure.rationale,
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
: {}),
|
|
292
|
+
};
|
|
293
|
+
}),
|
|
238
294
|
};
|
|
239
295
|
}
|
|
240
296
|
/* -------------------------------------------------------------------------- */
|