@quantakrypto/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +337 -0
- package/dist/baseline.d.ts +37 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +99 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cbom.d.ts +25 -0
- package/dist/cbom.d.ts.map +1 -0
- package/dist/cbom.js +131 -0
- package/dist/cbom.js.map +1 -0
- package/dist/changed.d.ts +13 -0
- package/dist/changed.d.ts.map +1 -0
- package/dist/changed.js +67 -0
- package/dist/changed.js.map +1 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +232 -0
- package/dist/config.js.map +1 -0
- package/dist/cwe.d.ts +16 -0
- package/dist/cwe.d.ts.map +1 -0
- package/dist/cwe.js +16 -0
- package/dist/cwe.js.map +1 -0
- package/dist/dependencies.d.ts +25 -0
- package/dist/dependencies.d.ts.map +1 -0
- package/dist/dependencies.js +276 -0
- package/dist/dependencies.js.map +1 -0
- package/dist/detect-utils.d.ts +63 -0
- package/dist/detect-utils.d.ts.map +1 -0
- package/dist/detect-utils.js +113 -0
- package/dist/detect-utils.js.map +1 -0
- package/dist/detectors/pem.d.ts +9 -0
- package/dist/detectors/pem.d.ts.map +1 -0
- package/dist/detectors/pem.js +137 -0
- package/dist/detectors/pem.js.map +1 -0
- package/dist/detectors/source.d.ts +25 -0
- package/dist/detectors/source.d.ts.map +1 -0
- package/dist/detectors/source.js +575 -0
- package/dist/detectors/source.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +15 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +74 -0
- package/dist/inventory.js.map +1 -0
- package/dist/parallel.d.ts +34 -0
- package/dist/parallel.d.ts.map +1 -0
- package/dist/parallel.js +237 -0
- package/dist/parallel.js.map +1 -0
- package/dist/registry.d.ts +42 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +51 -0
- package/dist/registry.js.map +1 -0
- package/dist/remediation.d.ts +42 -0
- package/dist/remediation.d.ts.map +1 -0
- package/dist/remediation.js +131 -0
- package/dist/remediation.js.map +1 -0
- package/dist/report.d.ts +24 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +286 -0
- package/dist/report.js.map +1 -0
- package/dist/scan-worker.d.ts +2 -0
- package/dist/scan-worker.d.ts.map +1 -0
- package/dist/scan-worker.js +63 -0
- package/dist/scan-worker.js.map +1 -0
- package/dist/scan.d.ts +27 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +168 -0
- package/dist/scan.js.map +1 -0
- package/dist/types.d.ts +190 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/dist/walk.d.ts +37 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +260 -0
- package/dist/walk.js.map +1 -0
- package/package.json +40 -0
- package/src/baseline.ts +111 -0
- package/src/cbom.ts +166 -0
- package/src/changed.ts +76 -0
- package/src/config.ts +295 -0
- package/src/cwe.ts +20 -0
- package/src/dependencies.ts +299 -0
- package/src/detect-utils.ts +157 -0
- package/src/detectors/pem.ts +162 -0
- package/src/detectors/source.ts +733 -0
- package/src/index.ts +79 -0
- package/src/inventory.ts +94 -0
- package/src/parallel.ts +288 -0
- package/src/registry.ts +71 -0
- package/src/remediation.ts +173 -0
- package/src/report.ts +340 -0
- package/src/scan-worker.ts +80 -0
- package/src/scan.ts +187 -0
- package/src/types.ts +224 -0
- package/src/version.ts +6 -0
- package/src/walk.ts +289 -0
package/dist/walk.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem walker. A zero-dependency recursive async generator that yields
|
|
3
|
+
* scannable text files as relative POSIX paths. It honours a default ignore
|
|
4
|
+
* list, user-supplied exclude patterns, a max file size, and a binary-extension
|
|
5
|
+
* filter. The root may be a directory or a single file.
|
|
6
|
+
*/
|
|
7
|
+
import { readdir, stat } from "node:fs/promises";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
/** Directories ignored by default (can be disabled with noDefaultIgnores). */
|
|
10
|
+
export const DEFAULT_IGNORES = [
|
|
11
|
+
"node_modules",
|
|
12
|
+
".git",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
".next",
|
|
16
|
+
"out",
|
|
17
|
+
"coverage",
|
|
18
|
+
"vendor",
|
|
19
|
+
".turbo",
|
|
20
|
+
".cache",
|
|
21
|
+
];
|
|
22
|
+
/** Default maximum file size to read: 2 MiB. */
|
|
23
|
+
export const DEFAULT_MAX_FILE_SIZE = 2 * 1024 * 1024;
|
|
24
|
+
/**
|
|
25
|
+
* File extensions we treat as binary / non-text and therefore skip. Keeping this
|
|
26
|
+
* as an extension allow-list-by-exclusion is cheap and avoids reading bytes.
|
|
27
|
+
*/
|
|
28
|
+
const BINARY_EXTENSIONS = new Set([
|
|
29
|
+
// images
|
|
30
|
+
".png",
|
|
31
|
+
".jpg",
|
|
32
|
+
".jpeg",
|
|
33
|
+
".gif",
|
|
34
|
+
".webp",
|
|
35
|
+
".bmp",
|
|
36
|
+
".ico",
|
|
37
|
+
".tiff",
|
|
38
|
+
".avif",
|
|
39
|
+
// fonts
|
|
40
|
+
".woff",
|
|
41
|
+
".woff2",
|
|
42
|
+
".ttf",
|
|
43
|
+
".otf",
|
|
44
|
+
".eot",
|
|
45
|
+
// archives / compressed
|
|
46
|
+
".zip",
|
|
47
|
+
".gz",
|
|
48
|
+
".tgz",
|
|
49
|
+
".bz2",
|
|
50
|
+
".xz",
|
|
51
|
+
".7z",
|
|
52
|
+
".rar",
|
|
53
|
+
".tar",
|
|
54
|
+
// media
|
|
55
|
+
".mp3",
|
|
56
|
+
".mp4",
|
|
57
|
+
".mov",
|
|
58
|
+
".avi",
|
|
59
|
+
".mkv",
|
|
60
|
+
".wav",
|
|
61
|
+
".flac",
|
|
62
|
+
".ogg",
|
|
63
|
+
".webm",
|
|
64
|
+
// documents / binaries
|
|
65
|
+
".pdf",
|
|
66
|
+
".doc",
|
|
67
|
+
".docx",
|
|
68
|
+
".xls",
|
|
69
|
+
".xlsx",
|
|
70
|
+
".ppt",
|
|
71
|
+
".pptx",
|
|
72
|
+
".exe",
|
|
73
|
+
".dll",
|
|
74
|
+
".so",
|
|
75
|
+
".dylib",
|
|
76
|
+
".bin",
|
|
77
|
+
".o",
|
|
78
|
+
".a",
|
|
79
|
+
".class",
|
|
80
|
+
".wasm",
|
|
81
|
+
// data blobs / db
|
|
82
|
+
".db",
|
|
83
|
+
".sqlite",
|
|
84
|
+
".sqlite3",
|
|
85
|
+
".dat",
|
|
86
|
+
".pack",
|
|
87
|
+
".idx",
|
|
88
|
+
// misc
|
|
89
|
+
".lock",
|
|
90
|
+
".map",
|
|
91
|
+
".min.js",
|
|
92
|
+
".node",
|
|
93
|
+
]);
|
|
94
|
+
/** Normalise a path to forward-slash POSIX separators. */
|
|
95
|
+
export function toPosix(p) {
|
|
96
|
+
return p.split(path.sep).join("/");
|
|
97
|
+
}
|
|
98
|
+
/** True if `rel` (a POSIX relative path) matches any pattern (substring/prefix). */
|
|
99
|
+
function matchesAny(rel, patterns) {
|
|
100
|
+
for (const pattern of patterns) {
|
|
101
|
+
if (!pattern)
|
|
102
|
+
continue;
|
|
103
|
+
const p = toPosix(pattern).replace(/\/+$/, "");
|
|
104
|
+
// Substring match (handles "src/legacy" or "secrets")...
|
|
105
|
+
if (rel.includes(p))
|
|
106
|
+
return true;
|
|
107
|
+
// ...and explicit path-prefix match ("foo" should match "foo/bar.ts").
|
|
108
|
+
if (rel === p || rel.startsWith(`${p}/`))
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
/** True if `rel` (a POSIX relative path) matches any exclude pattern. */
|
|
114
|
+
function isExcluded(rel, exclude) {
|
|
115
|
+
return matchesAny(rel, exclude);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* True if `rel` passes the include filter. An empty include list means "include
|
|
119
|
+
* everything"; otherwise the file must match at least one include pattern.
|
|
120
|
+
*/
|
|
121
|
+
function isIncluded(rel, include) {
|
|
122
|
+
if (include.length === 0)
|
|
123
|
+
return true;
|
|
124
|
+
return matchesAny(rel, include);
|
|
125
|
+
}
|
|
126
|
+
/** True if the file's extension marks it as binary / non-text. */
|
|
127
|
+
export function isBinaryPath(rel) {
|
|
128
|
+
const lower = rel.toLowerCase();
|
|
129
|
+
// Handle compound extensions like ".min.js" first.
|
|
130
|
+
if (lower.endsWith(".min.js"))
|
|
131
|
+
return true;
|
|
132
|
+
const ext = path.posix.extname(lower);
|
|
133
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Compound / pattern extensions that mark generated or bundled output we skip
|
|
137
|
+
* by default (beyond `.min.js` / `.map`, which {@link isBinaryPath} handles).
|
|
138
|
+
*/
|
|
139
|
+
const GENERATED_PATH_RE = /(?:\.min\.[mc]?js|[.-]min\.[mc]?js|\.bundle\.[mc]?js|\.chunk\.[mc]?js|\.generated\.[jt]sx?|_pb\.js|\.pb\.go)$/i;
|
|
140
|
+
/** True if the path looks like generated / bundled output (by name). */
|
|
141
|
+
export function isGeneratedPath(rel) {
|
|
142
|
+
return GENERATED_PATH_RE.test(rel.toLowerCase());
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Heuristic content check for machine-minified / generated files with no
|
|
146
|
+
* telltale extension: a very long average line length, or any single line over
|
|
147
|
+
* ~50 KB, in the first ~64 KB sampled. Used at read time, not in the walker.
|
|
148
|
+
*/
|
|
149
|
+
export function looksMinified(content) {
|
|
150
|
+
const sample = content.length > 65_536 ? content.slice(0, 65_536) : content;
|
|
151
|
+
if (sample.length === 0)
|
|
152
|
+
return false;
|
|
153
|
+
let maxLine = 0;
|
|
154
|
+
let cur = 0;
|
|
155
|
+
let lines = 1;
|
|
156
|
+
for (let i = 0; i < sample.length; i++) {
|
|
157
|
+
if (sample.charCodeAt(i) === 10 /* \n */) {
|
|
158
|
+
if (cur > maxLine)
|
|
159
|
+
maxLine = cur;
|
|
160
|
+
cur = 0;
|
|
161
|
+
lines++;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
cur++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (cur > maxLine)
|
|
168
|
+
maxLine = cur;
|
|
169
|
+
if (maxLine > 50_000)
|
|
170
|
+
return true;
|
|
171
|
+
const avgLine = sample.length / lines;
|
|
172
|
+
return avgLine > 1_000;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Recursively yield scannable file paths (relative to `root`, POSIX) under a
|
|
176
|
+
* directory. If `root` points at a single file, yields just that file's
|
|
177
|
+
* basename (subject to the size / binary filters).
|
|
178
|
+
*/
|
|
179
|
+
export async function* walkFiles(root, options = {}) {
|
|
180
|
+
const include = options.include ?? [];
|
|
181
|
+
const exclude = options.exclude ?? [];
|
|
182
|
+
const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
183
|
+
const ignores = options.noDefaultIgnores ? [] : DEFAULT_IGNORES;
|
|
184
|
+
const rootStat = await stat(root);
|
|
185
|
+
// Single-file mode: yield the file itself (by basename) if it passes filters.
|
|
186
|
+
if (rootStat.isFile()) {
|
|
187
|
+
const name = toPosix(path.basename(root));
|
|
188
|
+
if (!isBinaryPath(name) &&
|
|
189
|
+
isIncluded(name, include) &&
|
|
190
|
+
passesSizeLimit(name, rootStat.size, maxFileSize)) {
|
|
191
|
+
yield name;
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
yield* walkDir(root, "", { include, exclude, maxFileSize, ignores });
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* True if a file passes the size limit. Dependency manifests (package.json /
|
|
199
|
+
* package-lock.json) are exempt from the cap so large lockfiles still get
|
|
200
|
+
* scanned for vulnerable dependencies instead of being silently dropped.
|
|
201
|
+
*/
|
|
202
|
+
function passesSizeLimit(rel, size, maxFileSize) {
|
|
203
|
+
if (isManifestPath(rel))
|
|
204
|
+
return true;
|
|
205
|
+
return size <= maxFileSize;
|
|
206
|
+
}
|
|
207
|
+
/** True if the path's basename is a dependency manifest we always read. */
|
|
208
|
+
function isManifestPath(rel) {
|
|
209
|
+
const base = rel.split("/").pop() ?? rel;
|
|
210
|
+
return base === "package.json" || base === "package-lock.json";
|
|
211
|
+
}
|
|
212
|
+
/** Internal recursive directory walker. `relDir` is POSIX-relative to the root. */
|
|
213
|
+
async function* walkDir(absDir, relDir, ctx) {
|
|
214
|
+
let entries;
|
|
215
|
+
try {
|
|
216
|
+
entries = await readdir(absDir, { withFileTypes: true });
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Unreadable directory (permissions, transient races) — skip silently.
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Stable, deterministic ordering for reproducible scans.
|
|
223
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
224
|
+
for (const entry of entries) {
|
|
225
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
226
|
+
const abs = path.join(absDir, entry.name);
|
|
227
|
+
if (entry.isSymbolicLink()) {
|
|
228
|
+
// Don't follow symlinks: avoids cycles and escaping the root.
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (entry.isDirectory()) {
|
|
232
|
+
if (ctx.ignores.includes(entry.name))
|
|
233
|
+
continue;
|
|
234
|
+
if (isExcluded(rel, ctx.exclude))
|
|
235
|
+
continue;
|
|
236
|
+
yield* walkDir(abs, rel, ctx);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (!entry.isFile())
|
|
240
|
+
continue;
|
|
241
|
+
if (isExcluded(rel, ctx.exclude))
|
|
242
|
+
continue;
|
|
243
|
+
if (!isIncluded(rel, ctx.include))
|
|
244
|
+
continue;
|
|
245
|
+
if (isBinaryPath(rel))
|
|
246
|
+
continue;
|
|
247
|
+
if (isGeneratedPath(rel))
|
|
248
|
+
continue;
|
|
249
|
+
try {
|
|
250
|
+
const s = await stat(abs);
|
|
251
|
+
if (!passesSizeLimit(rel, s.size, ctx.maxFileSize))
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
yield rel;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=walk.js.map
|
package/dist/walk.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"walk.js","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quantakrypto/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared post-quantum readiness library: crypto detectors, vulnerable-dependency database, inventory + SARIF reporting. Zero runtime dependencies.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Dandelion Labs <hello@dandelionlabs.io> (https://dandelionlabs.io)",
|
|
7
|
+
"homepage": "https://github.com/dandelionlabs-io/qproof-tools/tree/main/packages/core#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/dandelionlabs-io/qproof-tools.git",
|
|
11
|
+
"directory": "packages/core"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/dandelionlabs-io/qproof-tools/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -b",
|
|
38
|
+
"test": "node --import tsx --test test/*.test.ts"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/baseline.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical baseline module — the single source of truth for suppressing known
|
|
3
|
+
* findings across qScan and the GitHub Action (replacing their two divergent,
|
|
4
|
+
* mutually-unintelligible schemes).
|
|
5
|
+
*
|
|
6
|
+
* A baseline is a versioned set of finding fingerprints. A fingerprint is
|
|
7
|
+
* **line-insensitive** (so unrelated edits that shift line numbers don't
|
|
8
|
+
* invalidate it) and snippet-whitespace-normalized (so reformatting doesn't
|
|
9
|
+
* either). Identity = `sha256(ruleId | file | normalizedSnippet)`.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
|
|
14
|
+
import type { Finding } from "./types.js";
|
|
15
|
+
|
|
16
|
+
/** Current on-disk baseline schema version. */
|
|
17
|
+
export const BASELINE_VERSION = 1 as const;
|
|
18
|
+
|
|
19
|
+
/** The on-disk baseline shape: a version tag and a set of fingerprints. */
|
|
20
|
+
export interface Baseline {
|
|
21
|
+
version: number;
|
|
22
|
+
fingerprints: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Collapse all whitespace runs to single spaces and trim (snippet stability). */
|
|
26
|
+
function normalizeSnippet(snippet: string | undefined): string {
|
|
27
|
+
if (!snippet) return "";
|
|
28
|
+
return snippet.replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Stable, line-INSENSITIVE fingerprint of a finding: the hex SHA-256 of
|
|
33
|
+
* `ruleId|file|normalizedSnippet`. The line number is deliberately excluded so
|
|
34
|
+
* the fingerprint survives line shifts; the snippet's whitespace is normalized
|
|
35
|
+
* so it survives reformatting.
|
|
36
|
+
*/
|
|
37
|
+
export function fingerprintFinding(f: Finding): string {
|
|
38
|
+
const snippet = normalizeSnippet(f.location.snippet);
|
|
39
|
+
const input = `${f.ruleId}|${f.location.file}|${snippet}`;
|
|
40
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Build a {@link Baseline} from a set of findings (deduped, sorted). */
|
|
44
|
+
export function baselineFromFindings(findings: readonly Finding[]): Baseline {
|
|
45
|
+
const set = new Set<string>();
|
|
46
|
+
for (const f of findings) set.add(fingerprintFinding(f));
|
|
47
|
+
return {
|
|
48
|
+
version: BASELINE_VERSION,
|
|
49
|
+
fingerprints: [...set].sort(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Split findings into those NOT in the baseline (`newFindings`) and those that
|
|
55
|
+
* ARE (`suppressed`). Order within each group is preserved from the input.
|
|
56
|
+
*/
|
|
57
|
+
export function applyBaseline(
|
|
58
|
+
findings: readonly Finding[],
|
|
59
|
+
baseline: Baseline,
|
|
60
|
+
): { newFindings: Finding[]; suppressed: Finding[] } {
|
|
61
|
+
const accepted = new Set(baseline.fingerprints);
|
|
62
|
+
const newFindings: Finding[] = [];
|
|
63
|
+
const suppressed: Finding[] = [];
|
|
64
|
+
for (const f of findings) {
|
|
65
|
+
if (accepted.has(fingerprintFinding(f))) suppressed.push(f);
|
|
66
|
+
else newFindings.push(f);
|
|
67
|
+
}
|
|
68
|
+
return { newFindings, suppressed };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Normalize an arbitrary parsed object into a valid {@link Baseline}. */
|
|
72
|
+
function coerceBaseline(value: unknown): Baseline {
|
|
73
|
+
if (value === null || typeof value !== "object") {
|
|
74
|
+
return { version: BASELINE_VERSION, fingerprints: [] };
|
|
75
|
+
}
|
|
76
|
+
const obj = value as Record<string, unknown>;
|
|
77
|
+
const version = typeof obj.version === "number" ? obj.version : BASELINE_VERSION;
|
|
78
|
+
const fingerprints = Array.isArray(obj.fingerprints)
|
|
79
|
+
? obj.fingerprints.filter((x): x is string => typeof x === "string")
|
|
80
|
+
: [];
|
|
81
|
+
return { version, fingerprints };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Load a baseline from disk. Returns an empty baseline (rather than throwing)
|
|
86
|
+
* when the file is missing or unparseable, so callers can treat "no baseline"
|
|
87
|
+
* and "absent baseline" uniformly.
|
|
88
|
+
*/
|
|
89
|
+
export async function loadBaseline(path: string): Promise<Baseline> {
|
|
90
|
+
let text: string;
|
|
91
|
+
try {
|
|
92
|
+
text = await readFile(path, "utf8");
|
|
93
|
+
} catch {
|
|
94
|
+
return { version: BASELINE_VERSION, fingerprints: [] };
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
return coerceBaseline(JSON.parse(text));
|
|
98
|
+
} catch {
|
|
99
|
+
return { version: BASELINE_VERSION, fingerprints: [] };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Write a baseline derived from the given findings to disk as pretty JSON
|
|
105
|
+
* (trailing newline). Returns the baseline that was written.
|
|
106
|
+
*/
|
|
107
|
+
export async function saveBaseline(path: string, findings: readonly Finding[]): Promise<Baseline> {
|
|
108
|
+
const baseline = baselineFromFindings(findings);
|
|
109
|
+
await writeFile(path, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
|
|
110
|
+
return baseline;
|
|
111
|
+
}
|
package/src/cbom.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CBOM (Cryptographic Bill of Materials) export — CycloneDX 1.6 with
|
|
3
|
+
* `cryptographic-asset` components. Turns a {@link ScanResult} into a
|
|
4
|
+
* machine-readable inventory of the classical cryptographic assets discovered,
|
|
5
|
+
* for compliance / supply-chain tooling.
|
|
6
|
+
*
|
|
7
|
+
* Reference: CycloneDX 1.6 cryptography properties
|
|
8
|
+
* (https://cyclonedx.org/capabilities/cbom/). We emit one
|
|
9
|
+
* `cryptographic-asset` component per distinct (algorithm, primitive) pair
|
|
10
|
+
* observed, with occurrence evidence pointing back at the findings.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
import type { AlgorithmFamily, Finding, FindingCategory, ScanResult } from "./types.js";
|
|
15
|
+
import { VERSION } from "./version.js";
|
|
16
|
+
|
|
17
|
+
/** A CycloneDX 1.6 cryptographic bill of materials (kept permissive). */
|
|
18
|
+
export interface CycloneDxBom {
|
|
19
|
+
bomFormat: "CycloneDX";
|
|
20
|
+
specVersion: "1.6";
|
|
21
|
+
serialNumber: string;
|
|
22
|
+
version: number;
|
|
23
|
+
metadata: Record<string, unknown>;
|
|
24
|
+
components: CbomComponent[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A single CycloneDX `cryptographic-asset` component. */
|
|
28
|
+
export interface CbomComponent {
|
|
29
|
+
type: "cryptographic-asset";
|
|
30
|
+
"bom-ref": string;
|
|
31
|
+
name: string;
|
|
32
|
+
cryptoProperties: Record<string, unknown>;
|
|
33
|
+
evidence?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** CycloneDX cryptographic primitive for a finding category. */
|
|
37
|
+
function primitiveFor(category: FindingCategory): string {
|
|
38
|
+
switch (category) {
|
|
39
|
+
case "kem":
|
|
40
|
+
return "kem";
|
|
41
|
+
case "key-exchange":
|
|
42
|
+
return "key-agree";
|
|
43
|
+
case "signature":
|
|
44
|
+
return "signature";
|
|
45
|
+
case "certificate":
|
|
46
|
+
return "pki";
|
|
47
|
+
case "tls":
|
|
48
|
+
return "other";
|
|
49
|
+
default:
|
|
50
|
+
return "other";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True when the algorithm family is broken by Shor's algorithm (quantum). */
|
|
55
|
+
function isQuantumVulnerable(algorithm: AlgorithmFamily): boolean {
|
|
56
|
+
return algorithm !== "unknown";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Deterministic bom-ref for a (algorithm, primitive) asset key. */
|
|
60
|
+
function bomRef(key: string): string {
|
|
61
|
+
return `crypto:${createHash("sha256").update(key, "utf8").digest("hex").slice(0, 16)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a CycloneDX 1.6 CBOM from a scan result. One component per distinct
|
|
66
|
+
* (algorithm + primitive) pair, with occurrence evidence (file:line) per
|
|
67
|
+
* finding. Output is deterministic (components and occurrences are sorted).
|
|
68
|
+
*/
|
|
69
|
+
export function toCbom(result: ScanResult): CycloneDxBom {
|
|
70
|
+
// Group findings by (algorithm | primitive).
|
|
71
|
+
const groups = new Map<
|
|
72
|
+
string,
|
|
73
|
+
{ algorithm: AlgorithmFamily; primitive: string; findings: Finding[] }
|
|
74
|
+
>();
|
|
75
|
+
|
|
76
|
+
for (const f of result.findings) {
|
|
77
|
+
const algorithm: AlgorithmFamily = f.algorithm ?? "unknown";
|
|
78
|
+
const primitive = primitiveFor(f.category);
|
|
79
|
+
const key = `${algorithm}|${primitive}`;
|
|
80
|
+
let g = groups.get(key);
|
|
81
|
+
if (!g) {
|
|
82
|
+
g = { algorithm, primitive, findings: [] };
|
|
83
|
+
groups.set(key, g);
|
|
84
|
+
}
|
|
85
|
+
g.findings.push(f);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const components: CbomComponent[] = [...groups.entries()]
|
|
89
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
90
|
+
.map(([key, g]) => {
|
|
91
|
+
const occurrences = g.findings
|
|
92
|
+
.map((f) => ({
|
|
93
|
+
location: `${f.location.file}:${f.location.line}`,
|
|
94
|
+
...(f.cwe ? { additionalContext: f.cwe } : {}),
|
|
95
|
+
}))
|
|
96
|
+
.sort((a, b) => (a.location < b.location ? -1 : a.location > b.location ? 1 : 0));
|
|
97
|
+
|
|
98
|
+
const anyHndl = g.findings.some((f) => f.hndl);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
type: "cryptographic-asset" as const,
|
|
102
|
+
"bom-ref": bomRef(key),
|
|
103
|
+
name: `${g.algorithm} (${g.primitive})`,
|
|
104
|
+
cryptoProperties: {
|
|
105
|
+
assetType: "algorithm",
|
|
106
|
+
algorithmProperties: {
|
|
107
|
+
primitive: g.primitive,
|
|
108
|
+
parameterSetIdentifier: g.algorithm,
|
|
109
|
+
executionEnvironment: "software-plain-ram",
|
|
110
|
+
classicalSecurityLevel: 0,
|
|
111
|
+
nistQuantumSecurityLevel: 0,
|
|
112
|
+
cryptoFunctions:
|
|
113
|
+
g.primitive === "signature"
|
|
114
|
+
? ["sign", "verify"]
|
|
115
|
+
: g.primitive === "kem"
|
|
116
|
+
? ["encapsulate", "decapsulate"]
|
|
117
|
+
: g.primitive === "key-agree"
|
|
118
|
+
? ["keygen"]
|
|
119
|
+
: ["other"],
|
|
120
|
+
},
|
|
121
|
+
quantumVulnerable: isQuantumVulnerable(g.algorithm),
|
|
122
|
+
harvestNowDecryptLater: anyHndl,
|
|
123
|
+
},
|
|
124
|
+
evidence: { occurrences },
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const serial = `urn:uuid:${stableUuid(result)}`;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
bomFormat: "CycloneDX",
|
|
132
|
+
specVersion: "1.6",
|
|
133
|
+
serialNumber: serial,
|
|
134
|
+
version: 1,
|
|
135
|
+
metadata: {
|
|
136
|
+
timestamp: result.finishedAt,
|
|
137
|
+
tools: {
|
|
138
|
+
components: [
|
|
139
|
+
{
|
|
140
|
+
type: "application",
|
|
141
|
+
name: "qScan",
|
|
142
|
+
version: result.toolVersion || VERSION,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
component: {
|
|
147
|
+
type: "application",
|
|
148
|
+
"bom-ref": "root",
|
|
149
|
+
name: result.root,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
components,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Derive a stable UUID-shaped serial from the scan result so re-exporting the
|
|
158
|
+
* same result yields the same serial number (deterministic output).
|
|
159
|
+
*/
|
|
160
|
+
function stableUuid(result: ScanResult): string {
|
|
161
|
+
const h = createHash("sha256")
|
|
162
|
+
.update(`${result.root}|${result.toolVersion}|${result.findings.length}`, "utf8")
|
|
163
|
+
.digest("hex");
|
|
164
|
+
// Shape as a v4-ish UUID (variant/version nibbles forced).
|
|
165
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
166
|
+
}
|
package/src/changed.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental-scan helper: list the files that changed in a git working tree,
|
|
3
|
+
* for feeding into {@link ScanOptions.files}. Tolerant of non-git directories —
|
|
4
|
+
* returns an empty list rather than throwing.
|
|
5
|
+
*/
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
/** Run a git command in `cwd`, returning stdout or `null` on any failure. */
|
|
12
|
+
async function git(cwd: string, args: string[]): Promise<string | null> {
|
|
13
|
+
try {
|
|
14
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
15
|
+
cwd,
|
|
16
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
});
|
|
19
|
+
return stdout;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Split git output into trimmed, non-empty, POSIX-relative path lines. */
|
|
26
|
+
function toLines(stdout: string | null): string[] {
|
|
27
|
+
if (!stdout) return [];
|
|
28
|
+
return stdout
|
|
29
|
+
.split("\n")
|
|
30
|
+
.map((l) => l.trim())
|
|
31
|
+
.filter((l) => l.length > 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return the list of changed files (relative POSIX paths) under `root`.
|
|
36
|
+
*
|
|
37
|
+
* - With `since` (a ref / range), returns `git diff --name-only --diff-filter=ACMR <since>`
|
|
38
|
+
* plus currently-modified-but-uncommitted files, deduped.
|
|
39
|
+
* - Without `since`, returns uncommitted changes (`git diff` working+staged) plus
|
|
40
|
+
* untracked files (`git ls-files --others --exclude-standard`).
|
|
41
|
+
* - When `root` is not a git repository (or git is unavailable), returns `[]`.
|
|
42
|
+
*
|
|
43
|
+
* Deleted files are excluded (ACMR filter / existence is the caller's concern).
|
|
44
|
+
*/
|
|
45
|
+
export async function changedFiles(root: string, since?: string): Promise<string[]> {
|
|
46
|
+
// Confirm this is a git work tree first; bail out tolerantly if not.
|
|
47
|
+
const inside = await git(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
48
|
+
if (inside === null || inside.trim() !== "true") return [];
|
|
49
|
+
|
|
50
|
+
const out = new Set<string>();
|
|
51
|
+
|
|
52
|
+
if (since) {
|
|
53
|
+
for (const f of toLines(
|
|
54
|
+
await git(root, ["diff", "--name-only", "--diff-filter=ACMR", since]),
|
|
55
|
+
)) {
|
|
56
|
+
out.add(f);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Always include local uncommitted edits (staged + unstaged), filtered to ACMR.
|
|
61
|
+
for (const f of toLines(await git(root, ["diff", "--name-only", "--diff-filter=ACMR"]))) {
|
|
62
|
+
out.add(f);
|
|
63
|
+
}
|
|
64
|
+
for (const f of toLines(
|
|
65
|
+
await git(root, ["diff", "--name-only", "--diff-filter=ACMR", "--cached"]),
|
|
66
|
+
)) {
|
|
67
|
+
out.add(f);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Untracked (but not ignored) files.
|
|
71
|
+
for (const f of toLines(await git(root, ["ls-files", "--others", "--exclude-standard"]))) {
|
|
72
|
+
out.add(f);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return [...out].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
76
|
+
}
|