@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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-quantum remediation guidance for each classical asymmetric algorithm
|
|
3
|
+
* family. The recommendations follow NIST's standardized PQC algorithms
|
|
4
|
+
* (ML-KEM / FIPS 203, ML-DSA / FIPS 204, SLH-DSA / FIPS 205) and the IETF
|
|
5
|
+
* hybrid key-exchange drafts (X25519MLKEM768).
|
|
6
|
+
*/
|
|
7
|
+
import type { AlgorithmFamily, Remediation } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/** Canonical remediation table, keyed by algorithm family. */
|
|
10
|
+
const REMEDIATIONS: Record<AlgorithmFamily, Remediation> = {
|
|
11
|
+
RSA: {
|
|
12
|
+
algorithm: "RSA",
|
|
13
|
+
recommendation: "ML-KEM-768 for encryption/KEM; ML-DSA-65 for signatures",
|
|
14
|
+
detail:
|
|
15
|
+
"RSA is broken by Shor's algorithm. For key transport / encryption move to " +
|
|
16
|
+
"ML-KEM-768 (FIPS 203), ideally as the hybrid X25519MLKEM768. For digital " +
|
|
17
|
+
"signatures move to ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205).",
|
|
18
|
+
},
|
|
19
|
+
ECDH: {
|
|
20
|
+
algorithm: "ECDH",
|
|
21
|
+
recommendation: "hybrid X25519MLKEM768 (ML-KEM-768)",
|
|
22
|
+
detail:
|
|
23
|
+
"Elliptic-curve Diffie-Hellman is broken by Shor's algorithm and is exposed " +
|
|
24
|
+
"to harvest-now-decrypt-later. Adopt the hybrid X25519MLKEM768 key exchange so " +
|
|
25
|
+
"confidentiality survives even if one component is broken.",
|
|
26
|
+
},
|
|
27
|
+
ECDSA: {
|
|
28
|
+
algorithm: "ECDSA",
|
|
29
|
+
recommendation: "ML-DSA-65 (FIPS 204)",
|
|
30
|
+
detail:
|
|
31
|
+
"ECDSA signatures can be forged by a quantum attacker via Shor's algorithm. " +
|
|
32
|
+
"Migrate to ML-DSA (Dilithium, FIPS 204) or SLH-DSA (SPHINCS+, FIPS 205) for " +
|
|
33
|
+
"long-lived signatures.",
|
|
34
|
+
},
|
|
35
|
+
EdDSA: {
|
|
36
|
+
algorithm: "EdDSA",
|
|
37
|
+
recommendation: "ML-DSA-65 (FIPS 204)",
|
|
38
|
+
detail:
|
|
39
|
+
"Ed25519 / Ed448 (EdDSA) are classical signatures broken by Shor's algorithm. " +
|
|
40
|
+
"Replace with ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205) for forgery resistance.",
|
|
41
|
+
},
|
|
42
|
+
DH: {
|
|
43
|
+
algorithm: "DH",
|
|
44
|
+
recommendation: "hybrid X25519MLKEM768 (ML-KEM-768)",
|
|
45
|
+
detail:
|
|
46
|
+
"Finite-field Diffie-Hellman is broken by Shor's algorithm and exposed to " +
|
|
47
|
+
"harvest-now-decrypt-later. Move to a hybrid PQC KEM such as X25519MLKEM768.",
|
|
48
|
+
},
|
|
49
|
+
DSA: {
|
|
50
|
+
algorithm: "DSA",
|
|
51
|
+
recommendation: "ML-DSA-65 (FIPS 204)",
|
|
52
|
+
detail:
|
|
53
|
+
"DSA is a classical, quantum-broken signature scheme (and already deprecated). " +
|
|
54
|
+
"Replace with ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205).",
|
|
55
|
+
},
|
|
56
|
+
X25519: {
|
|
57
|
+
algorithm: "X25519",
|
|
58
|
+
recommendation: "hybrid X25519MLKEM768 (ML-KEM-768)",
|
|
59
|
+
detail:
|
|
60
|
+
"X25519 is a modern, well-built classical key-agreement primitive but is still " +
|
|
61
|
+
"broken by Shor's algorithm. Wrap it in the hybrid X25519MLKEM768 construction " +
|
|
62
|
+
"so it stays useful during the PQC transition.",
|
|
63
|
+
},
|
|
64
|
+
X448: {
|
|
65
|
+
algorithm: "X448",
|
|
66
|
+
recommendation: "hybrid X25519MLKEM768 (ML-KEM-768)",
|
|
67
|
+
detail:
|
|
68
|
+
"X448 (Goldilocks curve) is a modern classical key-agreement primitive at a " +
|
|
69
|
+
"higher classical security level, but it is still broken by Shor's algorithm. " +
|
|
70
|
+
"Adopt a hybrid PQC KEM (X25519MLKEM768 / ML-KEM-768) during the transition.",
|
|
71
|
+
},
|
|
72
|
+
ECIES: {
|
|
73
|
+
algorithm: "ECIES",
|
|
74
|
+
recommendation: "ML-KEM-768 hybrid encryption",
|
|
75
|
+
detail:
|
|
76
|
+
"ECIES relies on classical ECDH for its key encapsulation and is exposed to " +
|
|
77
|
+
"harvest-now-decrypt-later. Replace the KEM step with ML-KEM-768 (FIPS 203), " +
|
|
78
|
+
"preferably in a hybrid construction.",
|
|
79
|
+
},
|
|
80
|
+
unknown: {
|
|
81
|
+
algorithm: "unknown",
|
|
82
|
+
recommendation: "review for post-quantum migration",
|
|
83
|
+
detail:
|
|
84
|
+
"This usage involves classical public-key cryptography. Audit it and plan a " +
|
|
85
|
+
"migration to NIST PQC standards (ML-KEM / FIPS 203, ML-DSA / FIPS 204).",
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Look up the recommended post-quantum remediation for a classical algorithm. */
|
|
90
|
+
export function remediationFor(algorithm: AlgorithmFamily): Remediation | undefined {
|
|
91
|
+
return REMEDIATIONS[algorithm];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Convenience: just the short recommendation string for a family (always defined). */
|
|
95
|
+
export function remediationText(algorithm: AlgorithmFamily): string {
|
|
96
|
+
return REMEDIATIONS[algorithm].recommendation;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* -------------------------------------------------------------------------- */
|
|
100
|
+
/* Security-tier guidance (CNSA 2.0 Category 5) + stateful HBS (SP 800-208) */
|
|
101
|
+
/* -------------------------------------------------------------------------- */
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Security tier for remediation guidance.
|
|
105
|
+
* - `"category-3"` (default): commercial use — ML-KEM-768 / ML-DSA-65.
|
|
106
|
+
* - `"category-5"`: CNSA 2.0 national-security-systems / long-lived secrets —
|
|
107
|
+
* ML-KEM-1024 / ML-DSA-87.
|
|
108
|
+
*/
|
|
109
|
+
export type SecurityTier = "category-3" | "category-5";
|
|
110
|
+
|
|
111
|
+
/** Per-tier KEM / signature parameter sets. */
|
|
112
|
+
export const TIER_PARAMS: Record<SecurityTier, { kem: string; signature: string; note: string }> = {
|
|
113
|
+
"category-3": {
|
|
114
|
+
kem: "ML-KEM-768 (FIPS 203)",
|
|
115
|
+
signature: "ML-DSA-65 (FIPS 204)",
|
|
116
|
+
note: "NIST Category 3 — default for general commercial use.",
|
|
117
|
+
},
|
|
118
|
+
"category-5": {
|
|
119
|
+
kem: "ML-KEM-1024 (FIPS 203)",
|
|
120
|
+
signature: "ML-DSA-87 (FIPS 204)",
|
|
121
|
+
note: "NIST Category 5 — CNSA 2.0 for national-security systems and long-lived secrets (2030/2033 milestones).",
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Tier-aware remediation. Returns the base family remediation plus the
|
|
127
|
+
* parameter sets for the requested CNSA tier. Category 5 surfaces the
|
|
128
|
+
* ML-KEM-1024 / ML-DSA-87 sets mandated by CNSA 2.0; category 3 is the
|
|
129
|
+
* commercial default.
|
|
130
|
+
*/
|
|
131
|
+
export function remediationForTier(
|
|
132
|
+
algorithm: AlgorithmFamily,
|
|
133
|
+
tier: SecurityTier = "category-3",
|
|
134
|
+
): Remediation {
|
|
135
|
+
const base = REMEDIATIONS[algorithm];
|
|
136
|
+
const params = TIER_PARAMS[tier];
|
|
137
|
+
// Confidentiality families lean on the KEM; signature families on the signer.
|
|
138
|
+
const isConfidentiality =
|
|
139
|
+
algorithm === "RSA" ||
|
|
140
|
+
algorithm === "ECDH" ||
|
|
141
|
+
algorithm === "DH" ||
|
|
142
|
+
algorithm === "X25519" ||
|
|
143
|
+
algorithm === "X448" ||
|
|
144
|
+
algorithm === "ECIES";
|
|
145
|
+
const primary = isConfidentiality ? params.kem : params.signature;
|
|
146
|
+
return {
|
|
147
|
+
algorithm,
|
|
148
|
+
recommendation: `${base.recommendation} — ${tier}: ${primary}`,
|
|
149
|
+
detail: `${base.detail} ${params.note} For this tier use ${params.kem} (KEM) and ${params.signature} (signatures).`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Guidance for stateful hash-based signatures (SP 800-208: LMS / XMSS / HSS).
|
|
155
|
+
* These are NIST-approved for firmware / boot signing but are STATEFUL — each
|
|
156
|
+
* private key may sign a bounded number of messages and the state MUST be
|
|
157
|
+
* managed to avoid catastrophic key reuse. Surfaced where a long-lived,
|
|
158
|
+
* low-volume signing root is appropriate.
|
|
159
|
+
*/
|
|
160
|
+
export const STATEFUL_HBS_NOTE =
|
|
161
|
+
"For firmware / secure-boot signing, the stateful hash-based signatures " +
|
|
162
|
+
"LMS, XMSS and HSS (NIST SP 800-208) are approved alternatives to ML-DSA, " +
|
|
163
|
+
"but they are STATEFUL: the signer must never reuse a one-time key index. " +
|
|
164
|
+
"Use only with rigorous state management; otherwise prefer stateless ML-DSA " +
|
|
165
|
+
"(FIPS 204) or SLH-DSA (FIPS 205).";
|
|
166
|
+
|
|
167
|
+
/** True when stateful HBS (SP 800-208) is a reasonable alternative for a family. */
|
|
168
|
+
export function statefulHbsApplies(algorithm: AlgorithmFamily): boolean {
|
|
169
|
+
// Signature families only — LMS/XMSS are signatures, not KEMs.
|
|
170
|
+
return (
|
|
171
|
+
algorithm === "RSA" || algorithm === "ECDSA" || algorithm === "EdDSA" || algorithm === "DSA"
|
|
172
|
+
);
|
|
173
|
+
}
|
package/src/report.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporters: turn a {@link ScanResult} into SARIF 2.1.0, a clean JSON object,
|
|
3
|
+
* or a human-readable text summary. No third-party dependencies — ANSI colour
|
|
4
|
+
* is emitted with raw escape codes and is off by default.
|
|
5
|
+
*/
|
|
6
|
+
import type { ScanResult, Severity } from "./types.js";
|
|
7
|
+
import { VERSION } from "./version.js";
|
|
8
|
+
|
|
9
|
+
/** Minimal SARIF 2.1.0 log shape (kept permissive on purpose). */
|
|
10
|
+
export interface SarifLog {
|
|
11
|
+
$schema: string;
|
|
12
|
+
version: "2.1.0";
|
|
13
|
+
runs: unknown[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SARIF_SCHEMA =
|
|
17
|
+
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json";
|
|
18
|
+
|
|
19
|
+
const INFORMATION_URI = "https://github.com/qproof-tools/qproof";
|
|
20
|
+
|
|
21
|
+
/** Map our severity to a SARIF result level. */
|
|
22
|
+
function sarifLevel(severity: Severity): "error" | "warning" | "note" {
|
|
23
|
+
switch (severity) {
|
|
24
|
+
case "critical":
|
|
25
|
+
case "high":
|
|
26
|
+
return "error";
|
|
27
|
+
case "medium":
|
|
28
|
+
return "warning";
|
|
29
|
+
default:
|
|
30
|
+
return "note";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Map our severity to a SARIF rule-level default (used in rules[].defaultConfiguration). */
|
|
35
|
+
function sarifRank(severity: Severity): number {
|
|
36
|
+
switch (severity) {
|
|
37
|
+
case "critical":
|
|
38
|
+
return 100;
|
|
39
|
+
case "high":
|
|
40
|
+
return 80;
|
|
41
|
+
case "medium":
|
|
42
|
+
return 50;
|
|
43
|
+
case "low":
|
|
44
|
+
return 20;
|
|
45
|
+
default:
|
|
46
|
+
return 5;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Serialize a scan result as SARIF 2.1.0. */
|
|
51
|
+
export function toSarif(result: ScanResult): SarifLog {
|
|
52
|
+
// Build the unique rule set (one rule per ruleId encountered) and collect the
|
|
53
|
+
// set of CWE taxa referenced by any rule.
|
|
54
|
+
const ruleIndex = new Map<string, number>();
|
|
55
|
+
const rules: Array<Record<string, unknown>> = [];
|
|
56
|
+
const cweTaxa = new Set<string>();
|
|
57
|
+
for (const f of result.findings) {
|
|
58
|
+
if (f.cwe) cweTaxa.add(f.cwe);
|
|
59
|
+
if (ruleIndex.has(f.ruleId)) continue;
|
|
60
|
+
ruleIndex.set(f.ruleId, rules.length);
|
|
61
|
+
rules.push({
|
|
62
|
+
id: f.ruleId,
|
|
63
|
+
name: f.ruleId,
|
|
64
|
+
shortDescription: { text: f.title },
|
|
65
|
+
fullDescription: { text: f.message },
|
|
66
|
+
defaultConfiguration: { level: sarifLevel(f.severity), rank: sarifRank(f.severity) },
|
|
67
|
+
...(f.remediation ? { help: { text: `Remediation: ${f.remediation}` } } : {}),
|
|
68
|
+
properties: {
|
|
69
|
+
category: f.category,
|
|
70
|
+
...(f.algorithm ? { algorithm: f.algorithm } : {}),
|
|
71
|
+
hndl: f.hndl,
|
|
72
|
+
...(f.cwe ? { cwe: f.cwe, "security-severity": securitySeverity(f.severity) } : {}),
|
|
73
|
+
...(f.cwe ? { tags: ["security", f.cwe] } : {}),
|
|
74
|
+
},
|
|
75
|
+
...(f.cwe
|
|
76
|
+
? {
|
|
77
|
+
relationships: [
|
|
78
|
+
{ target: { id: f.cwe, toolComponent: { name: "CWE" } }, kinds: ["relevant"] },
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
: {}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const results = result.findings.map((f) => {
|
|
86
|
+
const region: Record<string, number> = { startLine: f.location.line };
|
|
87
|
+
if (typeof f.location.column === "number") region.startColumn = f.location.column;
|
|
88
|
+
if (typeof f.location.endLine === "number") region.endLine = f.location.endLine;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
ruleId: f.ruleId,
|
|
92
|
+
ruleIndex: ruleIndex.get(f.ruleId),
|
|
93
|
+
level: sarifLevel(f.severity),
|
|
94
|
+
message: { text: f.message },
|
|
95
|
+
properties: {
|
|
96
|
+
category: f.category,
|
|
97
|
+
severity: f.severity,
|
|
98
|
+
confidence: f.confidence,
|
|
99
|
+
hndl: f.hndl,
|
|
100
|
+
...(f.algorithm ? { algorithm: f.algorithm } : {}),
|
|
101
|
+
...(f.remediation ? { remediation: f.remediation } : {}),
|
|
102
|
+
...(f.cwe ? { cwe: f.cwe } : {}),
|
|
103
|
+
},
|
|
104
|
+
...(f.cwe
|
|
105
|
+
? {
|
|
106
|
+
taxa: [
|
|
107
|
+
{
|
|
108
|
+
target: { id: f.cwe, toolComponent: { name: "CWE" } },
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
113
|
+
locations: [
|
|
114
|
+
{
|
|
115
|
+
physicalLocation: {
|
|
116
|
+
artifactLocation: { uri: f.location.file },
|
|
117
|
+
region: {
|
|
118
|
+
...region,
|
|
119
|
+
...(f.location.snippet ? { snippet: { text: f.location.snippet } } : {}),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// CWE taxonomy component (SARIF taxonomies), referenced by rules + results.
|
|
128
|
+
const taxonomies =
|
|
129
|
+
cweTaxa.size > 0
|
|
130
|
+
? [
|
|
131
|
+
{
|
|
132
|
+
name: "CWE",
|
|
133
|
+
informationUri: "https://cwe.mitre.org/",
|
|
134
|
+
organization: "MITRE",
|
|
135
|
+
shortDescription: { text: "The MITRE Common Weakness Enumeration" },
|
|
136
|
+
taxa: [...cweTaxa].sort().map((id) => ({
|
|
137
|
+
id,
|
|
138
|
+
helpUri: `https://cwe.mitre.org/data/definitions/${id.replace(/^CWE-/, "")}.html`,
|
|
139
|
+
})),
|
|
140
|
+
},
|
|
141
|
+
]
|
|
142
|
+
: [];
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
$schema: SARIF_SCHEMA,
|
|
146
|
+
version: "2.1.0",
|
|
147
|
+
runs: [
|
|
148
|
+
{
|
|
149
|
+
tool: {
|
|
150
|
+
driver: {
|
|
151
|
+
name: "qScan",
|
|
152
|
+
informationUri: INFORMATION_URI,
|
|
153
|
+
version: result.toolVersion || VERSION,
|
|
154
|
+
rules,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
...(taxonomies.length > 0 ? { taxonomies } : {}),
|
|
158
|
+
results,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** GitHub-code-scanning `security-severity` (0–10) derived from our severity. */
|
|
165
|
+
function securitySeverity(severity: Severity): string {
|
|
166
|
+
switch (severity) {
|
|
167
|
+
case "critical":
|
|
168
|
+
return "9.5";
|
|
169
|
+
case "high":
|
|
170
|
+
return "8.0";
|
|
171
|
+
case "medium":
|
|
172
|
+
return "5.0";
|
|
173
|
+
case "low":
|
|
174
|
+
return "3.0";
|
|
175
|
+
default:
|
|
176
|
+
return "1.0";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Serialize a scan result as a plain JSON-friendly object. */
|
|
181
|
+
export function toJson(result: ScanResult): Record<string, unknown> {
|
|
182
|
+
return {
|
|
183
|
+
toolVersion: result.toolVersion,
|
|
184
|
+
root: result.root,
|
|
185
|
+
startedAt: result.startedAt,
|
|
186
|
+
finishedAt: result.finishedAt,
|
|
187
|
+
filesScanned: result.filesScanned,
|
|
188
|
+
inventory: {
|
|
189
|
+
readinessScore: result.inventory.readinessScore,
|
|
190
|
+
hndlCount: result.inventory.hndlCount,
|
|
191
|
+
bySeverity: result.inventory.bySeverity,
|
|
192
|
+
byCategory: result.inventory.byCategory,
|
|
193
|
+
byAlgorithm: result.inventory.byAlgorithm,
|
|
194
|
+
},
|
|
195
|
+
findings: result.findings.map((f) => ({
|
|
196
|
+
ruleId: f.ruleId,
|
|
197
|
+
title: f.title,
|
|
198
|
+
category: f.category,
|
|
199
|
+
severity: f.severity,
|
|
200
|
+
confidence: f.confidence,
|
|
201
|
+
algorithm: f.algorithm,
|
|
202
|
+
hndl: f.hndl,
|
|
203
|
+
message: f.message,
|
|
204
|
+
remediation: f.remediation,
|
|
205
|
+
cwe: f.cwe,
|
|
206
|
+
location: {
|
|
207
|
+
file: f.location.file,
|
|
208
|
+
line: f.location.line,
|
|
209
|
+
column: f.location.column,
|
|
210
|
+
endLine: f.location.endLine,
|
|
211
|
+
snippet: f.location.snippet,
|
|
212
|
+
},
|
|
213
|
+
})),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* -------------------------------------------------------------------------- */
|
|
218
|
+
/* Human-readable summary */
|
|
219
|
+
/* -------------------------------------------------------------------------- */
|
|
220
|
+
|
|
221
|
+
/** Raw ANSI codes (no chalk). Disabled when colour is off. */
|
|
222
|
+
const ANSI = {
|
|
223
|
+
reset: "\x1b[0m",
|
|
224
|
+
bold: "\x1b[1m",
|
|
225
|
+
dim: "\x1b[2m",
|
|
226
|
+
red: "\x1b[31m",
|
|
227
|
+
green: "\x1b[32m",
|
|
228
|
+
yellow: "\x1b[33m",
|
|
229
|
+
blue: "\x1b[34m",
|
|
230
|
+
magenta: "\x1b[35m",
|
|
231
|
+
cyan: "\x1b[36m",
|
|
232
|
+
} as const;
|
|
233
|
+
|
|
234
|
+
/** Severity ordering for grouping / sorting, most → least severe. */
|
|
235
|
+
const SEVERITY_ORDER: Severity[] = ["critical", "high", "medium", "low", "info"];
|
|
236
|
+
|
|
237
|
+
function severityColor(sev: Severity): string {
|
|
238
|
+
switch (sev) {
|
|
239
|
+
case "critical":
|
|
240
|
+
return ANSI.magenta;
|
|
241
|
+
case "high":
|
|
242
|
+
return ANSI.red;
|
|
243
|
+
case "medium":
|
|
244
|
+
return ANSI.yellow;
|
|
245
|
+
case "low":
|
|
246
|
+
return ANSI.blue;
|
|
247
|
+
default:
|
|
248
|
+
return ANSI.dim;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function scoreColor(score: number): string {
|
|
253
|
+
if (score >= 80) return ANSI.green;
|
|
254
|
+
if (score >= 50) return ANSI.yellow;
|
|
255
|
+
return ANSI.red;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Render a human-readable summary of a scan result. Colour is off by default;
|
|
260
|
+
* pass `{ color: true }` to emit ANSI escape codes.
|
|
261
|
+
*/
|
|
262
|
+
export function formatSummary(result: ScanResult, options?: { color?: boolean }): string {
|
|
263
|
+
const color = options?.color ?? false;
|
|
264
|
+
const c = (code: string, text: string): string => (color ? `${code}${text}${ANSI.reset}` : text);
|
|
265
|
+
|
|
266
|
+
const lines: string[] = [];
|
|
267
|
+
const inv = result.inventory;
|
|
268
|
+
|
|
269
|
+
lines.push(c(ANSI.bold, "qScan — post-quantum readiness report"));
|
|
270
|
+
lines.push(c(ANSI.dim, `tool v${result.toolVersion} · root: ${result.root}`));
|
|
271
|
+
lines.push("");
|
|
272
|
+
|
|
273
|
+
// Readiness score banner.
|
|
274
|
+
lines.push(
|
|
275
|
+
`Readiness score: ${c(`${ANSI.bold}${scoreColor(inv.readinessScore)}`, `${inv.readinessScore}/100`)}`,
|
|
276
|
+
);
|
|
277
|
+
lines.push(
|
|
278
|
+
`Files scanned: ${result.filesScanned} Findings: ${result.findings.length} HNDL-exposed: ${c(
|
|
279
|
+
inv.hndlCount > 0 ? ANSI.red : ANSI.green,
|
|
280
|
+
String(inv.hndlCount),
|
|
281
|
+
)}`,
|
|
282
|
+
);
|
|
283
|
+
lines.push("");
|
|
284
|
+
|
|
285
|
+
// Severity breakdown.
|
|
286
|
+
const sevParts = SEVERITY_ORDER.filter((s) => inv.bySeverity[s] > 0).map((s) =>
|
|
287
|
+
c(severityColor(s), `${s}: ${inv.bySeverity[s]}`),
|
|
288
|
+
);
|
|
289
|
+
lines.push(`By severity: ${sevParts.length ? sevParts.join(" ") : c(ANSI.green, "none")}`);
|
|
290
|
+
|
|
291
|
+
// Algorithm breakdown.
|
|
292
|
+
const algoParts = Object.entries(inv.byAlgorithm)
|
|
293
|
+
.sort((a, b) => b[1] - a[1])
|
|
294
|
+
.map(([k, v]) => `${k}: ${v}`);
|
|
295
|
+
if (algoParts.length) lines.push(`By algorithm: ${algoParts.join(" ")}`);
|
|
296
|
+
lines.push("");
|
|
297
|
+
|
|
298
|
+
if (result.findings.length === 0) {
|
|
299
|
+
lines.push(c(ANSI.green, "No classical asymmetric cryptography detected. ✓"));
|
|
300
|
+
return lines.join("\n");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Top findings, grouped by severity (most severe first), capped for readability.
|
|
304
|
+
const sorted = [...result.findings].sort(
|
|
305
|
+
(a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity),
|
|
306
|
+
);
|
|
307
|
+
const MAX_SHOWN = 25;
|
|
308
|
+
lines.push(
|
|
309
|
+
c(ANSI.bold, `Top findings (${Math.min(MAX_SHOWN, sorted.length)} of ${sorted.length}):`),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
for (const f of sorted.slice(0, MAX_SHOWN)) {
|
|
313
|
+
const loc = `${f.location.file}:${f.location.line}${
|
|
314
|
+
f.location.column ? `:${f.location.column}` : ""
|
|
315
|
+
}`;
|
|
316
|
+
const tag = c(severityColor(f.severity), `[${f.severity}]`);
|
|
317
|
+
const hndl = f.hndl ? c(ANSI.red, " (HNDL)") : "";
|
|
318
|
+
lines.push(` ${tag} ${f.title}${hndl}`);
|
|
319
|
+
lines.push(c(ANSI.dim, ` ${loc} — ${f.message}`));
|
|
320
|
+
if (f.remediation) lines.push(c(ANSI.cyan, ` → ${f.remediation}`));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (sorted.length > MAX_SHOWN) {
|
|
324
|
+
lines.push(c(ANSI.dim, ` …and ${sorted.length - MAX_SHOWN} more.`));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
lines.push("");
|
|
328
|
+
if (inv.hndlCount > 0) {
|
|
329
|
+
lines.push(
|
|
330
|
+
c(
|
|
331
|
+
ANSI.yellow,
|
|
332
|
+
`Note: ${inv.hndlCount} finding(s) are exposed to "harvest now, decrypt later" — ` +
|
|
333
|
+
"encrypted traffic captured today can be decrypted once a quantum computer exists. " +
|
|
334
|
+
"Prioritise migrating key exchange / encryption to hybrid PQC (X25519MLKEM768).",
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-thread entry for {@link scanParallel}. Built to `dist/scan-worker.js`
|
|
3
|
+
* and spawned by `parallel.ts`. Each worker reads its assigned files and runs
|
|
4
|
+
* the SAME pure detector pipeline as the serial scan (`detectFile`), returning
|
|
5
|
+
* `{ findings, filesScanned }` per chunk. No shared mutable state.
|
|
6
|
+
*
|
|
7
|
+
* This file performs side effects (wires up message handlers) only when it is
|
|
8
|
+
* actually running inside a worker thread, so importing it from the main thread
|
|
9
|
+
* (e.g. for coverage) is harmless.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
14
|
+
|
|
15
|
+
import type { Finding } from "./types.js";
|
|
16
|
+
import { defaultRegistry } from "./registry.js";
|
|
17
|
+
import { detectFile } from "./scan.js";
|
|
18
|
+
import { looksMinified } from "./walk.js";
|
|
19
|
+
import { isManifestFile } from "./dependencies.js";
|
|
20
|
+
|
|
21
|
+
interface WorkerToggles {
|
|
22
|
+
source: boolean;
|
|
23
|
+
config: boolean;
|
|
24
|
+
deps: boolean;
|
|
25
|
+
scanMinified: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ChunkRequest {
|
|
29
|
+
index: number;
|
|
30
|
+
files: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (parentPort) {
|
|
34
|
+
const data = (workerData ?? {}) as { baseDir: string; toggles: WorkerToggles };
|
|
35
|
+
const baseDir = data.baseDir;
|
|
36
|
+
const toggles = data.toggles;
|
|
37
|
+
const dets = defaultRegistry.all();
|
|
38
|
+
const port = parentPort;
|
|
39
|
+
|
|
40
|
+
port.on("message", (req: ChunkRequest) => {
|
|
41
|
+
try {
|
|
42
|
+
const findings: Finding[] = [];
|
|
43
|
+
let filesScanned = 0;
|
|
44
|
+
const scannedNames: string[] = [];
|
|
45
|
+
|
|
46
|
+
for (const rel of req.files) {
|
|
47
|
+
const abs = path.join(baseDir, ...rel.split("/"));
|
|
48
|
+
let content: string;
|
|
49
|
+
try {
|
|
50
|
+
content = readFileSync(abs, "utf8");
|
|
51
|
+
} catch {
|
|
52
|
+
continue; // vanished / unreadable — skip.
|
|
53
|
+
}
|
|
54
|
+
if (!toggles.scanMinified && !isManifestFile(rel) && looksMinified(content)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
filesScanned += 1;
|
|
58
|
+
scannedNames.push(rel);
|
|
59
|
+
findings.push(
|
|
60
|
+
...detectFile(rel, content, dets, {
|
|
61
|
+
source: toggles.source,
|
|
62
|
+
config: toggles.config,
|
|
63
|
+
deps: toggles.deps,
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
port.postMessage({
|
|
69
|
+
index: req.index,
|
|
70
|
+
files: scannedNames,
|
|
71
|
+
result: { findings, filesScanned },
|
|
72
|
+
});
|
|
73
|
+
} catch (err) {
|
|
74
|
+
port.postMessage({
|
|
75
|
+
index: req.index,
|
|
76
|
+
error: err instanceof Error ? err.message : String(err),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|