@quantakrypto/core 0.4.0 → 0.4.2
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/cbom.d.ts.map +1 -1
- package/dist/cbom.js +13 -4
- package/dist/cbom.js.map +1 -1
- package/dist/codemods/config-toggle.d.ts.map +1 -1
- package/dist/codemods/config-toggle.js +7 -9
- package/dist/codemods/config-toggle.js.map +1 -1
- package/dist/dependencies.d.ts.map +1 -1
- package/dist/dependencies.js +13 -2
- package/dist/dependencies.js.map +1 -1
- package/dist/detectors/source.d.ts.map +1 -1
- package/dist/detectors/source.js +14 -6
- package/dist/detectors/source.js.map +1 -1
- package/dist/parallel.js +15 -0
- package/dist/parallel.js.map +1 -1
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +85 -9
- package/dist/redact.js.map +1 -1
- package/dist/remediation.d.ts.map +1 -1
- package/dist/remediation.js +16 -1
- package/dist/remediation.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +8 -1
- package/dist/scan.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +10 -1
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
- package/src/cbom.ts +13 -4
- package/src/codemods/config-toggle.ts +7 -8
- package/src/dependencies.ts +13 -2
- package/src/detectors/source.ts +14 -6
- package/src/parallel.ts +15 -0
- package/src/redact.ts +86 -10
- package/src/remediation.ts +18 -1
- package/src/scan.ts +8 -1
- package/src/types.ts +7 -0
- package/src/version.ts +1 -1
- package/src/worktree.ts +9 -1
package/src/redact.ts
CHANGED
|
@@ -12,20 +12,96 @@ import type { ContextLevel, RedactedContext } from "./agent-types.js";
|
|
|
12
12
|
/** Lines of context on each side of the match at `snippet` level. */
|
|
13
13
|
const SNIPPET_RADIUS = 8;
|
|
14
14
|
|
|
15
|
+
/** The placeholder that replaces any redacted secret. */
|
|
16
|
+
const REDACTED = "«redacted-secret»";
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
19
|
+
* Text longer than this is not scanned pattern-by-pattern — we fail CLOSED
|
|
20
|
+
* (redact the whole thing). Bounds worst-case work and sidesteps any engine
|
|
21
|
+
* limit on pathological single-token runs.
|
|
18
22
|
*/
|
|
19
|
-
const
|
|
20
|
-
/-----BEGIN [A-Z0-9 ]+-----[\s\S]*?-----END [A-Z0-9 ]+-----|[A-Za-z0-9+/]{120,}={0,2}/g;
|
|
23
|
+
const MAX_SECRET_SCAN = 2_000_000;
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
/**
|
|
26
|
+
* High-signal secret shapes. EVERY quantifier has an explicit upper bound so
|
|
27
|
+
* matching stays linear — no catastrophic backtracking and no regex-engine
|
|
28
|
+
* stack overflow on multi-megabyte runs (both were real DoS vectors with the
|
|
29
|
+
* old unbounded `{120,}` / `[\s\S]*?` patterns). Private-key BLOCKS are handled
|
|
30
|
+
* separately, line-by-line, so a truncated key (missing `-----END-----`) is
|
|
31
|
+
* still caught.
|
|
32
|
+
*/
|
|
33
|
+
const TOKEN_PATTERNS: readonly RegExp[] = [
|
|
34
|
+
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, // AWS access key id
|
|
35
|
+
/\bgh[posru]_[A-Za-z0-9]{20,255}\b/g, // GitHub token
|
|
36
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,255}\b/g, // GitHub fine-grained PAT
|
|
37
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,255}\b/g, // Slack
|
|
38
|
+
/\bsk-(?:proj-)?[A-Za-z0-9_-]{20,255}\b/g, // OpenAI
|
|
39
|
+
/\b[rs]k_live_[A-Za-z0-9]{20,255}\b/g, // Stripe
|
|
40
|
+
/\bAIza[A-Za-z0-9_-]{35}\b/g, // Google API key
|
|
41
|
+
/\bglpat-[A-Za-z0-9_-]{20,255}\b/g, // GitLab PAT
|
|
42
|
+
/\beyJ[A-Za-z0-9_-]{8,4096}\.[A-Za-z0-9_-]{8,4096}\.[A-Za-z0-9_-]{6,4096}\b/g, // JWT
|
|
43
|
+
// Assignment of a secret-looking key (.env / config lines).
|
|
44
|
+
/(?:secret|token|passwd|password|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)["'`]?\s*[:=]\s*["'`]?[^\s"'`,;]{6,4096}/gi,
|
|
45
|
+
/\b[0-9a-fA-F]{40,4096}\b/g, // long hex run (≥20 bytes)
|
|
46
|
+
/[A-Za-z0-9+/]{44,4096}={0,2}/g, // long base64 run (≥32 bytes)
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Redact PEM/OpenSSH/PGP private-key blocks line-by-line (linear; tolerant of
|
|
50
|
+
* a missing END marker — a truncated key is still fully redacted). */
|
|
51
|
+
function redactPrivateKeyBlocks(text: string): { text: string; redacted: boolean } {
|
|
52
|
+
const begin =
|
|
53
|
+
/-----BEGIN (?:[A-Z0-9 ]*PRIVATE KEY|OPENSSH PRIVATE KEY|PGP PRIVATE KEY BLOCK)-----/;
|
|
54
|
+
const end = /-----END /;
|
|
23
55
|
let redacted = false;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
56
|
+
let inKey = false;
|
|
57
|
+
const out: string[] = [];
|
|
58
|
+
for (const line of text.split("\n")) {
|
|
59
|
+
if (!inKey && begin.test(line)) {
|
|
60
|
+
inKey = true;
|
|
61
|
+
redacted = true;
|
|
62
|
+
out.push(REDACTED);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (inKey) {
|
|
66
|
+
if (end.test(line)) inKey = false;
|
|
67
|
+
continue; // drop key-body / END lines
|
|
68
|
+
}
|
|
69
|
+
out.push(line);
|
|
70
|
+
}
|
|
71
|
+
return { text: out.join("\n"), redacted };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Single-entry memo: same-file findings re-request the identical file content;
|
|
75
|
+
// this makes the whole-file redaction O(n) across all of them instead of O(n·k).
|
|
76
|
+
let memoInput: string | undefined;
|
|
77
|
+
let memoResult: { text: string; redacted: boolean } | undefined;
|
|
78
|
+
|
|
79
|
+
/** Replace every secret-looking token/block in `text` with {@link REDACTED}.
|
|
80
|
+
* Fails CLOSED: on oversized input or any error, the whole text is redacted. */
|
|
81
|
+
function stripSecrets(text: string): { text: string; redacted: boolean } {
|
|
82
|
+
if (memoInput === text && memoResult) return memoResult;
|
|
83
|
+
let result: { text: string; redacted: boolean };
|
|
84
|
+
if (text.length > MAX_SECRET_SCAN) {
|
|
85
|
+
result = { text: REDACTED, redacted: true };
|
|
86
|
+
} else {
|
|
87
|
+
try {
|
|
88
|
+
const pem = redactPrivateKeyBlocks(text);
|
|
89
|
+
let out = pem.text;
|
|
90
|
+
let redacted = pem.redacted;
|
|
91
|
+
for (const re of TOKEN_PATTERNS) {
|
|
92
|
+
out = out.replace(re, () => {
|
|
93
|
+
redacted = true;
|
|
94
|
+
return REDACTED;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
result = { text: out, redacted };
|
|
98
|
+
} catch {
|
|
99
|
+
result = { text: REDACTED, redacted: true };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
memoInput = text;
|
|
103
|
+
memoResult = result;
|
|
104
|
+
return result;
|
|
29
105
|
}
|
|
30
106
|
|
|
31
107
|
/** Best-effort enclosing brace/colon block around a 0-based line index. */
|
package/src/remediation.ts
CHANGED
|
@@ -160,7 +160,24 @@ export function remediationForTier(
|
|
|
160
160
|
const base = REMEDIATIONS[algorithm];
|
|
161
161
|
const params = TIER_PARAMS[tier];
|
|
162
162
|
// Confidentiality families lean on the KEM; signature families on the signer.
|
|
163
|
-
const
|
|
163
|
+
const isConf = isConfidentialityFamily(algorithm);
|
|
164
|
+
const primary = isConf ? params.kem : params.signature;
|
|
165
|
+
|
|
166
|
+
// Category-5 (CNSA 2.0 / NSS) mandates the ML-KEM-1024 / ML-DSA-87 parameter
|
|
167
|
+
// sets and must NOT surface the category-3 X25519MLKEM768 hybrid (its PQC
|
|
168
|
+
// component is ML-KEM-768 — sub-CNSA). Lead with the mandated set; only point
|
|
169
|
+
// at a hybrid TLS group at the 1024 level (audit: quantum #1, crypto S5).
|
|
170
|
+
if (tier === "category-5") {
|
|
171
|
+
const hybridNote = isConf
|
|
172
|
+
? " If a hybrid TLS group is required, use SecP384r1MLKEM1024 (draft-ietf-tls-ecdhe-mlkem) — not X25519MLKEM768, whose ML-KEM-768 component does not meet CNSA 2.0."
|
|
173
|
+
: "";
|
|
174
|
+
return {
|
|
175
|
+
algorithm,
|
|
176
|
+
recommendation: `${primary} — CNSA 2.0 mandates this parameter set (hybrids optional)`,
|
|
177
|
+
detail: `${base.detail} ${params.note} For CNSA 2.0 / national-security systems use ${params.kem} (KEM) and ${params.signature} (signatures).${hybridNote}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
164
181
|
return {
|
|
165
182
|
algorithm,
|
|
166
183
|
recommendation: `${base.recommendation} — ${tier}: ${primary}`,
|
package/src/scan.ts
CHANGED
|
@@ -118,7 +118,14 @@ export async function scan(options: ScanOptions): Promise<ScanResult> {
|
|
|
118
118
|
const cacheFile = options.cacheFile;
|
|
119
119
|
const ruleset = cacheFile ? rulesetFingerprint(dets, options.disabledRules) : "";
|
|
120
120
|
const cache = cacheFile ? await loadCache(cacheFile, ruleset) : null;
|
|
121
|
-
|
|
121
|
+
// A full scan starts with an empty next-cache so files that vanished are
|
|
122
|
+
// evicted. An INCREMENTAL scan (explicit `files` list) only visits a subset,
|
|
123
|
+
// so seed the next-cache with the existing entries — otherwise saving would
|
|
124
|
+
// drop every file we didn't happen to scan this run (audit: arch #4).
|
|
125
|
+
const incremental = Array.isArray(options.files);
|
|
126
|
+
const nextEntries: Map<string, CacheEntry> | null = cacheFile
|
|
127
|
+
? new Map(incremental && cache ? cache : [])
|
|
128
|
+
: null;
|
|
122
129
|
|
|
123
130
|
// Work-budget / cancellation controls (all optional, unlimited when omitted).
|
|
124
131
|
const signal = options.signal;
|
package/src/types.ts
CHANGED
|
@@ -107,6 +107,13 @@ export interface VulnerableDependency {
|
|
|
107
107
|
/** Algorithm families the package primarily exposes. */
|
|
108
108
|
algorithms: AlgorithmFamily[];
|
|
109
109
|
severity: Severity;
|
|
110
|
+
/**
|
|
111
|
+
* Explicit harvest-now-decrypt-later override. When omitted, HNDL is inferred
|
|
112
|
+
* from whether any listed family is a confidentiality family. Set `false` for
|
|
113
|
+
* signing-only packages (e.g. JWS/JWT libraries) that list RSA/EC as families
|
|
114
|
+
* but never do key transport or key agreement — signatures are not HNDL-exposed.
|
|
115
|
+
*/
|
|
116
|
+
hndl?: boolean;
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
/**
|
package/src/version.ts
CHANGED
package/src/worktree.ts
CHANGED
|
@@ -31,8 +31,10 @@ export async function withWorktree<T>(
|
|
|
31
31
|
}
|
|
32
32
|
const base = await mkdtemp(join(tmpdir(), "quantakrypto-wt-"));
|
|
33
33
|
const dir = join(base, "wt");
|
|
34
|
-
|
|
34
|
+
// `worktree add` is INSIDE the try so a failure (locked index, disk full)
|
|
35
|
+
// still cleans up the temp dir in `finally` (audit: arch #6).
|
|
35
36
|
try {
|
|
37
|
+
await git(["worktree", "add", "--detach", dir], repoRoot);
|
|
36
38
|
return await fn(dir);
|
|
37
39
|
} finally {
|
|
38
40
|
try {
|
|
@@ -40,6 +42,12 @@ export async function withWorktree<T>(
|
|
|
40
42
|
} catch {
|
|
41
43
|
// best effort — the temp dir removal below still reclaims the disk.
|
|
42
44
|
}
|
|
45
|
+
// Prune stale `.git/worktrees/<name>` metadata a failed add/remove can leave.
|
|
46
|
+
try {
|
|
47
|
+
await git(["worktree", "prune"], repoRoot);
|
|
48
|
+
} catch {
|
|
49
|
+
// best effort
|
|
50
|
+
}
|
|
43
51
|
await rm(base, { recursive: true, force: true });
|
|
44
52
|
}
|
|
45
53
|
}
|