@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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the regex-based source detectors: turning a string offset
|
|
3
|
+
* into a 1-based line/column, extracting a trimmed single-line snippet, and a
|
|
4
|
+
* small factory for building Finding objects with consistent remediation text.
|
|
5
|
+
*/
|
|
6
|
+
import type { AlgorithmFamily, Confidence, Finding, FindingCategory, Severity } from "./types.js";
|
|
7
|
+
import { remediationText } from "./remediation.js";
|
|
8
|
+
|
|
9
|
+
/** A 1-based line/column position derived from a character offset. */
|
|
10
|
+
export interface LineCol {
|
|
11
|
+
line: number;
|
|
12
|
+
column: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Convert a 0-based character offset within `content` into a 1-based
|
|
17
|
+
* line/column. Newlines are LF; CR is treated as an ordinary character, so on
|
|
18
|
+
* CRLF files the column includes the trailing CR offset harmlessly.
|
|
19
|
+
*/
|
|
20
|
+
export function offsetToLineCol(content: string, offset: number): LineCol {
|
|
21
|
+
let line = 1;
|
|
22
|
+
let lastNewline = -1;
|
|
23
|
+
for (let i = 0; i < offset && i < content.length; i++) {
|
|
24
|
+
if (content.charCodeAt(i) === 10 /* \n */) {
|
|
25
|
+
line++;
|
|
26
|
+
lastNewline = i;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { line, column: offset - lastNewline };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Extract the (trimmed) single source line containing `offset`. */
|
|
33
|
+
export function lineAt(content: string, offset: number): string {
|
|
34
|
+
let start = offset;
|
|
35
|
+
while (start > 0 && content.charCodeAt(start - 1) !== 10) start--;
|
|
36
|
+
let end = offset;
|
|
37
|
+
while (end < content.length && content.charCodeAt(end) !== 10) end++;
|
|
38
|
+
return content.slice(start, end).replace(/\r$/, "").trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Inputs for {@link makeFinding}. */
|
|
42
|
+
export interface FindingSpec {
|
|
43
|
+
ruleId: string;
|
|
44
|
+
title: string;
|
|
45
|
+
category: FindingCategory;
|
|
46
|
+
severity: Severity;
|
|
47
|
+
confidence: Confidence;
|
|
48
|
+
algorithm?: AlgorithmFamily;
|
|
49
|
+
hndl: boolean;
|
|
50
|
+
message: string;
|
|
51
|
+
/** Override the auto-derived remediation text. */
|
|
52
|
+
remediation?: string;
|
|
53
|
+
/** Associated CWE id (e.g. "CWE-327"). */
|
|
54
|
+
cwe?: string;
|
|
55
|
+
/** The matched source text and its start offset within `content`. */
|
|
56
|
+
file: string;
|
|
57
|
+
content: string;
|
|
58
|
+
index: number;
|
|
59
|
+
/** Length of the match, used to compute endLine for multi-line matches. */
|
|
60
|
+
matchLength?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build a {@link Finding} with location info derived from a match offset. When
|
|
65
|
+
* no explicit remediation is given but an algorithm is, the canonical
|
|
66
|
+
* remediation text for that family is used.
|
|
67
|
+
*/
|
|
68
|
+
export function makeFinding(spec: FindingSpec): Finding {
|
|
69
|
+
const { line, column } = offsetToLineCol(spec.content, spec.index);
|
|
70
|
+
const snippet = lineAt(spec.content, spec.index);
|
|
71
|
+
|
|
72
|
+
const remediation =
|
|
73
|
+
spec.remediation ?? (spec.algorithm ? remediationText(spec.algorithm) : undefined);
|
|
74
|
+
|
|
75
|
+
const location: Finding["location"] = {
|
|
76
|
+
file: spec.file,
|
|
77
|
+
line,
|
|
78
|
+
column,
|
|
79
|
+
snippet: snippet.length > 200 ? `${snippet.slice(0, 197)}...` : snippet,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (spec.matchLength && spec.matchLength > 0) {
|
|
83
|
+
const matched = spec.content.slice(spec.index, spec.index + spec.matchLength);
|
|
84
|
+
const extraLines = (matched.match(/\n/g) ?? []).length;
|
|
85
|
+
if (extraLines > 0) location.endLine = line + extraLines;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const finding: Finding = {
|
|
89
|
+
ruleId: spec.ruleId,
|
|
90
|
+
title: spec.title,
|
|
91
|
+
category: spec.category,
|
|
92
|
+
severity: spec.severity,
|
|
93
|
+
confidence: spec.confidence,
|
|
94
|
+
hndl: spec.hndl,
|
|
95
|
+
message: spec.message,
|
|
96
|
+
location,
|
|
97
|
+
};
|
|
98
|
+
if (spec.algorithm) finding.algorithm = spec.algorithm;
|
|
99
|
+
if (remediation) finding.remediation = remediation;
|
|
100
|
+
if (spec.cwe) finding.cwe = spec.cwe;
|
|
101
|
+
return finding;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** True if `filePath` has one of the given (lower-case, dotted) extensions. */
|
|
105
|
+
export function hasExtension(filePath: string, exts: readonly string[]): boolean {
|
|
106
|
+
const lower = filePath.toLowerCase();
|
|
107
|
+
return exts.some((e) => lower.endsWith(e));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** JavaScript / TypeScript source extensions handled by the source detectors. */
|
|
111
|
+
export const JS_TS_EXTENSIONS: readonly string[] = [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Given a SORTED ascending array of call offsets, return true when `idx` is at
|
|
115
|
+
* or after some call offset `c` with `idx - c < window`. Runs in O(log n) by
|
|
116
|
+
* binary-searching the largest call offset ≤ `idx` and checking the gap. This
|
|
117
|
+
* replaces the previous O(matches × calls) linear scan (`nearCall`).
|
|
118
|
+
*/
|
|
119
|
+
export function nearSortedCall(
|
|
120
|
+
sortedCalls: readonly number[],
|
|
121
|
+
idx: number,
|
|
122
|
+
window: number,
|
|
123
|
+
): boolean {
|
|
124
|
+
// Find the rightmost element <= idx.
|
|
125
|
+
let lo = 0;
|
|
126
|
+
let hi = sortedCalls.length - 1;
|
|
127
|
+
let best = -1;
|
|
128
|
+
while (lo <= hi) {
|
|
129
|
+
const mid = (lo + hi) >>> 1;
|
|
130
|
+
if (sortedCalls[mid] <= idx) {
|
|
131
|
+
best = mid;
|
|
132
|
+
lo = mid + 1;
|
|
133
|
+
} else {
|
|
134
|
+
hi = mid - 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (best < 0) return false;
|
|
138
|
+
return idx - sortedCalls[best] < window;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Run a global regex over `content`, invoking `onMatch` for each hit. Resets
|
|
143
|
+
* lastIndex and guards against zero-width matches (which would loop forever).
|
|
144
|
+
*/
|
|
145
|
+
export function eachMatch(
|
|
146
|
+
re: RegExp,
|
|
147
|
+
content: string,
|
|
148
|
+
onMatch: (match: RegExpExecArray) => void,
|
|
149
|
+
): void {
|
|
150
|
+
const g = re.global ? re : new RegExp(re.source, `${re.flags}g`);
|
|
151
|
+
g.lastIndex = 0;
|
|
152
|
+
let m: RegExpExecArray | null;
|
|
153
|
+
while ((m = g.exec(content)) !== null) {
|
|
154
|
+
onMatch(m);
|
|
155
|
+
if (m.index === g.lastIndex) g.lastIndex++; // avoid infinite loop on empty match
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config / certificate detector: finds PEM-encoded cryptographic material in
|
|
3
|
+
* any text file (source, config, .pem, .key, .crt, .env, …). This catches
|
|
4
|
+
* embedded private keys and X.509 certificates regardless of language.
|
|
5
|
+
*/
|
|
6
|
+
import type { Detector, Finding } from "../types.js";
|
|
7
|
+
import { eachMatch, makeFinding } from "../detect-utils.js";
|
|
8
|
+
import { CWE_BROKEN_CRYPTO, CWE_HARDCODED_KEY } from "../cwe.js";
|
|
9
|
+
|
|
10
|
+
interface PemRule {
|
|
11
|
+
/** Regex matching the PEM begin marker. */
|
|
12
|
+
re: RegExp;
|
|
13
|
+
ruleId: string;
|
|
14
|
+
title: string;
|
|
15
|
+
category: Finding["category"];
|
|
16
|
+
severity: Finding["severity"];
|
|
17
|
+
algorithm?: Finding["algorithm"];
|
|
18
|
+
message: string;
|
|
19
|
+
remediation: string;
|
|
20
|
+
hndl: boolean;
|
|
21
|
+
cwe: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PEM_RULES: PemRule[] = [
|
|
25
|
+
{
|
|
26
|
+
re: /-----BEGIN RSA PRIVATE KEY-----/g,
|
|
27
|
+
ruleId: "pem-rsa-private-key",
|
|
28
|
+
title: "RSA private key (PEM)",
|
|
29
|
+
category: "certificate",
|
|
30
|
+
severity: "critical",
|
|
31
|
+
algorithm: "RSA",
|
|
32
|
+
hndl: true,
|
|
33
|
+
cwe: CWE_HARDCODED_KEY,
|
|
34
|
+
message: "Embedded RSA private key (PKCS#1 PEM); classical and not quantum-safe.",
|
|
35
|
+
remediation: "Migrate to ML-DSA / ML-KEM keys and remove embedded private keys from source.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
re: /-----BEGIN EC PRIVATE KEY-----/g,
|
|
39
|
+
ruleId: "pem-ec-private-key",
|
|
40
|
+
title: "EC private key (PEM)",
|
|
41
|
+
category: "certificate",
|
|
42
|
+
severity: "critical",
|
|
43
|
+
algorithm: "ECDSA",
|
|
44
|
+
hndl: true,
|
|
45
|
+
cwe: CWE_HARDCODED_KEY,
|
|
46
|
+
message: "Embedded EC private key (SEC1 PEM); classical ECDSA/ECDH key, not quantum-safe.",
|
|
47
|
+
remediation: "Migrate to ML-DSA (FIPS 204) keys and remove embedded private keys from source.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
re: /-----BEGIN DSA PRIVATE KEY-----/g,
|
|
51
|
+
ruleId: "pem-dsa-private-key",
|
|
52
|
+
title: "DSA private key (PEM)",
|
|
53
|
+
category: "certificate",
|
|
54
|
+
severity: "critical",
|
|
55
|
+
algorithm: "DSA",
|
|
56
|
+
hndl: false,
|
|
57
|
+
cwe: CWE_HARDCODED_KEY,
|
|
58
|
+
message: "Embedded DSA private key (PEM); classical, already deprecated, and not quantum-safe.",
|
|
59
|
+
remediation: "Rotate immediately (DSA is deprecated) and migrate to ML-DSA-65 (FIPS 204).",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
re: /-----BEGIN OPENSSH PRIVATE KEY-----/g,
|
|
63
|
+
ruleId: "pem-openssh-private-key",
|
|
64
|
+
title: "OpenSSH private key",
|
|
65
|
+
category: "certificate",
|
|
66
|
+
severity: "critical",
|
|
67
|
+
algorithm: "unknown",
|
|
68
|
+
hndl: true,
|
|
69
|
+
cwe: CWE_HARDCODED_KEY,
|
|
70
|
+
message: "Embedded OpenSSH private key (RSA/ECDSA/Ed25519); classical and not quantum-safe.",
|
|
71
|
+
remediation: "Rotate the key; plan migration to PQC-capable SSH (e.g. sntrup761x25519).",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
re: /-----BEGIN PGP PRIVATE KEY BLOCK-----/g,
|
|
75
|
+
ruleId: "pem-pgp-private-key",
|
|
76
|
+
title: "PGP/GPG private key block",
|
|
77
|
+
category: "certificate",
|
|
78
|
+
severity: "critical",
|
|
79
|
+
algorithm: "unknown",
|
|
80
|
+
hndl: true,
|
|
81
|
+
cwe: CWE_HARDCODED_KEY,
|
|
82
|
+
message:
|
|
83
|
+
"Embedded PGP/GPG private key block (RSA/ECDSA/EdDSA/ElGamal); classical and not quantum-safe.",
|
|
84
|
+
remediation: "Rotate the key; track OpenPGP PQC drafts for migration.",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
re: /-----BEGIN PGP MESSAGE-----/g,
|
|
88
|
+
ruleId: "pem-pgp-message",
|
|
89
|
+
title: "PGP/GPG encrypted message",
|
|
90
|
+
category: "certificate",
|
|
91
|
+
severity: "low",
|
|
92
|
+
algorithm: "unknown",
|
|
93
|
+
hndl: true,
|
|
94
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
95
|
+
message:
|
|
96
|
+
"Embedded PGP/GPG message; likely encrypted with classical RSA/ElGamal (harvest-now-decrypt-later).",
|
|
97
|
+
remediation: "Re-encrypt with PQC-capable tooling as OpenPGP PQC profiles mature.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
re: /-----BEGIN (?:ENCRYPTED )?PRIVATE KEY-----/g,
|
|
101
|
+
ruleId: "pem-pkcs8-private-key",
|
|
102
|
+
title: "Private key (PKCS#8 PEM)",
|
|
103
|
+
category: "certificate",
|
|
104
|
+
severity: "critical",
|
|
105
|
+
algorithm: "unknown",
|
|
106
|
+
hndl: true,
|
|
107
|
+
cwe: CWE_HARDCODED_KEY,
|
|
108
|
+
message: "Embedded PKCS#8 private key; likely classical RSA/EC, not quantum-safe.",
|
|
109
|
+
remediation: "Migrate to PQC keys and remove embedded private keys from source.",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
re: /-----BEGIN CERTIFICATE-----/g,
|
|
113
|
+
ruleId: "pem-certificate",
|
|
114
|
+
title: "X.509 certificate (PEM)",
|
|
115
|
+
category: "certificate",
|
|
116
|
+
severity: "low",
|
|
117
|
+
algorithm: "unknown",
|
|
118
|
+
hndl: false,
|
|
119
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
120
|
+
message: "Embedded X.509 certificate; almost certainly signed with classical RSA/ECDSA.",
|
|
121
|
+
remediation: "Plan re-issuance with PQC-capable CAs as ML-DSA certificate profiles mature.",
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
/** Detects PEM key/certificate material in arbitrary files. */
|
|
126
|
+
export const pemDetector: Detector = {
|
|
127
|
+
id: "pem-material",
|
|
128
|
+
description: "PEM-encoded private keys and X.509 certificates in any file",
|
|
129
|
+
scope: "config",
|
|
130
|
+
language: "any",
|
|
131
|
+
// Applies to every text file; the walker already filters out binaries.
|
|
132
|
+
appliesTo: () => true,
|
|
133
|
+
detect({ file, content }): Finding[] {
|
|
134
|
+
// Fast reject: only proceed if a PEM header is present at all.
|
|
135
|
+
if (!content.includes("-----BEGIN ")) return [];
|
|
136
|
+
|
|
137
|
+
const findings: Finding[] = [];
|
|
138
|
+
for (const rule of PEM_RULES) {
|
|
139
|
+
eachMatch(rule.re, content, (m) => {
|
|
140
|
+
findings.push(
|
|
141
|
+
makeFinding({
|
|
142
|
+
ruleId: rule.ruleId,
|
|
143
|
+
title: rule.title,
|
|
144
|
+
category: rule.category,
|
|
145
|
+
severity: rule.severity,
|
|
146
|
+
confidence: "high",
|
|
147
|
+
algorithm: rule.algorithm,
|
|
148
|
+
hndl: rule.hndl,
|
|
149
|
+
cwe: rule.cwe,
|
|
150
|
+
message: rule.message,
|
|
151
|
+
remediation: rule.remediation,
|
|
152
|
+
file,
|
|
153
|
+
content,
|
|
154
|
+
index: m.index,
|
|
155
|
+
matchLength: m[0].length,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return findings;
|
|
161
|
+
},
|
|
162
|
+
};
|