@quantakrypto/core 0.2.2 → 0.4.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/agent-types.d.ts +48 -0
- package/dist/agent-types.d.ts.map +1 -0
- package/dist/agent-types.js +2 -0
- package/dist/agent-types.js.map +1 -0
- package/dist/cache.d.ts +27 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +93 -0
- package/dist/cache.js.map +1 -0
- package/dist/cbom.d.ts.map +1 -1
- package/dist/cbom.js +26 -1
- package/dist/cbom.js.map +1 -1
- package/dist/changed.d.ts.map +1 -1
- package/dist/changed.js +14 -2
- package/dist/changed.js.map +1 -1
- package/dist/codemods/config-toggle.d.ts +3 -0
- package/dist/codemods/config-toggle.d.ts.map +1 -0
- package/dist/codemods/config-toggle.js +26 -0
- package/dist/codemods/config-toggle.js.map +1 -0
- package/dist/codemods/registry.d.ts +26 -0
- package/dist/codemods/registry.d.ts.map +1 -0
- package/dist/codemods/registry.js +8 -0
- package/dist/codemods/registry.js.map +1 -0
- package/dist/comments.d.ts +45 -0
- package/dist/comments.d.ts.map +1 -0
- package/dist/comments.js +164 -0
- package/dist/comments.js.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/dependencies.d.ts +31 -13
- package/dist/dependencies.d.ts.map +1 -1
- package/dist/dependencies.js +460 -40
- package/dist/dependencies.js.map +1 -1
- package/dist/detect-utils.d.ts +70 -2
- package/dist/detect-utils.d.ts.map +1 -1
- package/dist/detect-utils.js +139 -17
- package/dist/detect-utils.js.map +1 -1
- package/dist/detectors/c.d.ts +20 -0
- package/dist/detectors/c.d.ts.map +1 -0
- package/dist/detectors/c.js +104 -0
- package/dist/detectors/c.js.map +1 -0
- package/dist/detectors/csharp.d.ts +15 -0
- package/dist/detectors/csharp.d.ts.map +1 -0
- package/dist/detectors/csharp.js +76 -0
- package/dist/detectors/csharp.js.map +1 -0
- package/dist/detectors/go.d.ts +25 -0
- package/dist/detectors/go.d.ts.map +1 -0
- package/dist/detectors/go.js +132 -0
- package/dist/detectors/go.js.map +1 -0
- package/dist/detectors/java.d.ts +25 -0
- package/dist/detectors/java.d.ts.map +1 -0
- package/dist/detectors/java.js +200 -0
- package/dist/detectors/java.js.map +1 -0
- package/dist/detectors/pem.d.ts +4 -0
- package/dist/detectors/pem.d.ts.map +1 -1
- package/dist/detectors/pem.js +113 -91
- package/dist/detectors/pem.js.map +1 -1
- package/dist/detectors/python.d.ts +26 -0
- package/dist/detectors/python.d.ts.map +1 -0
- package/dist/detectors/python.js +171 -0
- package/dist/detectors/python.js.map +1 -0
- package/dist/detectors/ruby.d.ts +15 -0
- package/dist/detectors/ruby.d.ts.map +1 -0
- package/dist/detectors/ruby.js +75 -0
- package/dist/detectors/ruby.js.map +1 -0
- package/dist/detectors/rust.d.ts +13 -0
- package/dist/detectors/rust.d.ts.map +1 -0
- package/dist/detectors/rust.js +94 -0
- package/dist/detectors/rust.js.map +1 -0
- package/dist/detectors/source.d.ts +9 -2
- package/dist/detectors/source.d.ts.map +1 -1
- package/dist/detectors/source.js +435 -286
- package/dist/detectors/source.js.map +1 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/parallel.d.ts +4 -0
- package/dist/parallel.d.ts.map +1 -1
- package/dist/parallel.js +61 -8
- package/dist/parallel.js.map +1 -1
- package/dist/patch-policy.d.ts +22 -0
- package/dist/patch-policy.d.ts.map +1 -0
- package/dist/patch-policy.js +21 -0
- package/dist/patch-policy.js.map +1 -0
- package/dist/redact.d.ts +19 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +77 -0
- package/dist/redact.js.map +1 -0
- package/dist/registry.d.ts +29 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +62 -4
- package/dist/registry.js.map +1 -1
- package/dist/remediate-pipeline.d.ts +41 -0
- package/dist/remediate-pipeline.d.ts.map +1 -0
- package/dist/remediate-pipeline.js +38 -0
- package/dist/remediate-pipeline.js.map +1 -0
- package/dist/remediate-request.d.ts +21 -0
- package/dist/remediate-request.d.ts.map +1 -0
- package/dist/remediate-request.js +28 -0
- package/dist/remediate-request.js.map +1 -0
- package/dist/remediation.d.ts +10 -0
- package/dist/remediation.d.ts.map +1 -1
- package/dist/remediation.js +21 -8
- package/dist/remediation.js.map +1 -1
- package/dist/report.d.ts +9 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +72 -24
- package/dist/report.js.map +1 -1
- package/dist/scan-worker.js +7 -3
- package/dist/scan-worker.js.map +1 -1
- package/dist/scan.d.ts +6 -5
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +59 -30
- package/dist/scan.js.map +1 -1
- package/dist/triage.d.ts +27 -0
- package/dist/triage.d.ts.map +1 -0
- package/dist/triage.js +33 -0
- package/dist/triage.js.map +1 -0
- package/dist/types.d.ts +106 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/verify.d.ts +30 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +65 -0
- package/dist/verify.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walk.d.ts +7 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +85 -11
- package/dist/walk.js.map +1 -1
- package/dist/worktree.d.ts +6 -0
- package/dist/worktree.d.ts.map +1 -0
- package/dist/worktree.js +43 -0
- package/dist/worktree.js.map +1 -0
- package/package.json +1 -1
- package/src/agent-types.ts +52 -0
- package/src/cache.ts +120 -0
- package/src/cbom.ts +27 -1
- package/src/changed.ts +13 -4
- package/src/codemods/config-toggle.ts +35 -0
- package/src/codemods/registry.ts +31 -0
- package/src/comments.ts +180 -0
- package/src/config.ts +8 -0
- package/src/dependencies.ts +483 -51
- package/src/detect-utils.ts +180 -16
- package/src/detectors/c.ts +132 -0
- package/src/detectors/csharp.ts +98 -0
- package/src/detectors/go.ts +167 -0
- package/src/detectors/java.ts +235 -0
- package/src/detectors/pem.ts +124 -105
- package/src/detectors/python.ts +212 -0
- package/src/detectors/ruby.ts +97 -0
- package/src/detectors/rust.ts +112 -0
- package/src/detectors/source.ts +535 -364
- package/src/index.ts +47 -2
- package/src/parallel.ts +72 -7
- package/src/patch-policy.ts +42 -0
- package/src/redact.ts +92 -0
- package/src/registry.ts +71 -5
- package/src/remediate-pipeline.ts +81 -0
- package/src/remediate-request.ts +52 -0
- package/src/remediation.ts +27 -11
- package/src/report.ts +106 -24
- package/src/scan-worker.ts +18 -7
- package/src/scan.ts +74 -29
- package/src/triage.ts +58 -0
- package/src/types.ts +120 -3
- package/src/verify.ts +82 -0
- package/src/version.ts +1 -1
- package/src/walk.ts +81 -12
- package/src/worktree.ts +45 -0
package/src/index.ts
CHANGED
|
@@ -18,6 +18,41 @@ export type { SarifLog } from "./report.js";
|
|
|
18
18
|
// Core orchestration + built-in detector set.
|
|
19
19
|
export { scan, detectors, detectFile, compareFindings } from "./scan.js";
|
|
20
20
|
|
|
21
|
+
// Snippet-level fix verification (shared by MCP verify_fix + remediation).
|
|
22
|
+
export { verifyFix, languageToExtension } from "./verify.js";
|
|
23
|
+
export type { VerifyResult } from "./verify.js";
|
|
24
|
+
|
|
25
|
+
// Agent-plane shared types + the context redactor (offline; reused by MCP).
|
|
26
|
+
export type {
|
|
27
|
+
ContextLevel,
|
|
28
|
+
RedactedContext,
|
|
29
|
+
TriageVerdict,
|
|
30
|
+
Patch,
|
|
31
|
+
FixProposal,
|
|
32
|
+
} from "./agent-types.js";
|
|
33
|
+
export { buildContext, renderPreflight } from "./redact.js";
|
|
34
|
+
export { TRIAGE_RUBRIC, TRIAGE_VERDICT_SCHEMA, buildTriageRequest } from "./triage.js";
|
|
35
|
+
export type { TriageRequest } from "./triage.js";
|
|
36
|
+
export {
|
|
37
|
+
REMEDIATE_RUBRIC,
|
|
38
|
+
FIX_REQUEST_SCHEMA,
|
|
39
|
+
buildRemediateRequest,
|
|
40
|
+
} from "./remediate-request.js";
|
|
41
|
+
export type { RemediateRequest } from "./remediate-request.js";
|
|
42
|
+
export { checkPatchPolicy } from "./patch-policy.js";
|
|
43
|
+
export type { PolicyContext, PolicyDecision } from "./patch-policy.js";
|
|
44
|
+
export { withWorktree } from "./worktree.js";
|
|
45
|
+
export { codemodRegistry, codemodFor } from "./codemods/registry.js";
|
|
46
|
+
export type { Codemod } from "./codemods/registry.js";
|
|
47
|
+
export { configToggleCodemod } from "./codemods/config-toggle.js";
|
|
48
|
+
export { remediateFindings } from "./remediate-pipeline.js";
|
|
49
|
+
export type {
|
|
50
|
+
RemediateOptions,
|
|
51
|
+
RemediationResult,
|
|
52
|
+
VerifiedPatch,
|
|
53
|
+
RejectedPatch,
|
|
54
|
+
} from "./remediate-pipeline.js";
|
|
55
|
+
|
|
21
56
|
// Scan cancellation / work-budget errors.
|
|
22
57
|
export { AbortError, BudgetExceededError } from "./errors.js";
|
|
23
58
|
|
|
@@ -25,8 +60,9 @@ export { AbortError, BudgetExceededError } from "./errors.js";
|
|
|
25
60
|
export { scanParallel, mergeChunkResults, chunkByBytes } from "./parallel.js";
|
|
26
61
|
export type { ScanChunk, ChunkResult, SizedFile } from "./parallel.js";
|
|
27
62
|
|
|
28
|
-
// Detector registry (plugin point) + helpers.
|
|
63
|
+
// Detector registry (plugin point) + helpers + the rule catalog.
|
|
29
64
|
export { DetectorRegistry, defaultRegistry, detectorScope } from "./registry.js";
|
|
65
|
+
export type { RuleCatalogEntry } from "./registry.js";
|
|
30
66
|
|
|
31
67
|
// Canonical baseline (shared by qScan + the Action).
|
|
32
68
|
export {
|
|
@@ -49,11 +85,20 @@ export type { QuantakryptoFileConfig, LoadConfigResult } from "./config.js";
|
|
|
49
85
|
// Filesystem walker (relative POSIX paths, default ignores, size/binary filters).
|
|
50
86
|
export { walkFiles, isBinaryPath, isGeneratedPath, looksMinified } from "./walk.js";
|
|
51
87
|
|
|
88
|
+
// Analyzable-language coverage (which source languages the scanner inspects).
|
|
89
|
+
export {
|
|
90
|
+
isAnalyzableSource,
|
|
91
|
+
ANALYZABLE_SOURCE_EXTENSIONS,
|
|
92
|
+
ANALYZABLE_LANGUAGES_LABEL,
|
|
93
|
+
} from "./detect-utils.js";
|
|
94
|
+
|
|
52
95
|
// Inventory + readiness score.
|
|
53
96
|
export { buildInventory } from "./inventory.js";
|
|
54
97
|
|
|
55
98
|
// Vulnerable-dependency database (the manifest scanner is used internally by scan()).
|
|
56
|
-
|
|
99
|
+
// `DEP_VULNERABLE_RULE` is the generic catalog entry for dependency findings
|
|
100
|
+
// (which don't come from a Detector, so aren't in the registry's rule catalog).
|
|
101
|
+
export { vulnerableDependencies, DEP_VULNERABLE_RULE, isManifestFile } from "./dependencies.js";
|
|
57
102
|
|
|
58
103
|
// Severity utilities (ordering, threshold, SARIF level) — shared across tools.
|
|
59
104
|
export { SEVERITY_ORDER, severityRank, meetsThreshold, sarifLevel } from "./severity.js";
|
package/src/parallel.ts
CHANGED
|
@@ -18,8 +18,10 @@ import type { Worker as NodeWorker } from "node:worker_threads";
|
|
|
18
18
|
|
|
19
19
|
import type { Finding, ParallelScanOptions, ScanResult } from "./types.js";
|
|
20
20
|
import { walkFiles } from "./walk.js";
|
|
21
|
+
import { isAnalyzableSource } from "./detect-utils.js";
|
|
21
22
|
import { buildInventory } from "./inventory.js";
|
|
22
23
|
import { compareFindings, filterExplicitFileList, scan } from "./scan.js";
|
|
24
|
+
import { AbortError, BudgetExceededError } from "./errors.js";
|
|
23
25
|
import { VERSION } from "./version.js";
|
|
24
26
|
|
|
25
27
|
/** One unit of work dispatched to a worker. */
|
|
@@ -31,6 +33,10 @@ export interface ScanChunk {
|
|
|
31
33
|
export interface ChunkResult {
|
|
32
34
|
findings: Finding[];
|
|
33
35
|
filesScanned: number;
|
|
36
|
+
/** Files that couldn't be read in this chunk (optional; older workers omit it). */
|
|
37
|
+
unreadable?: number;
|
|
38
|
+
/** Files skipped as minified in this chunk (optional; older workers omit it). */
|
|
39
|
+
skippedMinified?: number;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
const DEFAULT_PARALLEL_THRESHOLD_BYTES = 2 * 1024 * 1024;
|
|
@@ -74,12 +80,16 @@ export function chunkByBytes(files: readonly SizedFile[], chunkBytes: number): S
|
|
|
74
80
|
export function mergeChunkResults(results: readonly ChunkResult[]): ChunkResult {
|
|
75
81
|
const findings: Finding[] = [];
|
|
76
82
|
let filesScanned = 0;
|
|
83
|
+
let unreadable = 0;
|
|
84
|
+
let skippedMinified = 0;
|
|
77
85
|
for (const r of results) {
|
|
78
86
|
for (const f of r.findings) findings.push(f);
|
|
79
87
|
filesScanned += r.filesScanned;
|
|
88
|
+
unreadable += r.unreadable ?? 0;
|
|
89
|
+
skippedMinified += r.skippedMinified ?? 0;
|
|
80
90
|
}
|
|
81
91
|
findings.sort(compareFindings);
|
|
82
|
-
return { findings, filesScanned };
|
|
92
|
+
return { findings, filesScanned, unreadable, skippedMinified };
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
/** Resolve the worker count (>= 1). */
|
|
@@ -162,14 +172,29 @@ export async function scanParallel(options: ParallelScanOptions): Promise<ScanRe
|
|
|
162
172
|
const rootStat = await stat(options.root);
|
|
163
173
|
const baseDir = rootStat.isFile() ? path.dirname(options.root) : options.root;
|
|
164
174
|
|
|
165
|
-
// Single-file roots
|
|
166
|
-
// detectors may not be structured-cloneable across the worker
|
|
167
|
-
|
|
175
|
+
// Single-file roots, the override-detectors path, and the scan cache always
|
|
176
|
+
// run serially: detectors may not be structured-cloneable across the worker
|
|
177
|
+
// boundary, and the cache read/write is owned by the in-process `scan()`.
|
|
178
|
+
if (rootStat.isFile() || options.detectors || options.cacheFile) {
|
|
168
179
|
return scan(options);
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
const files = await enumerateFiles(options, baseDir);
|
|
172
183
|
|
|
184
|
+
// Budget + cancellation parity with the serial path. The parallel path
|
|
185
|
+
// enumerates upfront, so budgets are enforced against the whole file set
|
|
186
|
+
// before any worker is dispatched.
|
|
187
|
+
if (options.signal?.aborted) throw new AbortError();
|
|
188
|
+
if (typeof options.maxFiles === "number" && files.length > options.maxFiles) {
|
|
189
|
+
throw new BudgetExceededError(`maxFiles budget exceeded (limit: ${options.maxFiles}).`);
|
|
190
|
+
}
|
|
191
|
+
if (typeof options.maxBytes === "number") {
|
|
192
|
+
const totalBytes = files.reduce((n, f) => n + f.size, 0);
|
|
193
|
+
if (totalBytes > options.maxBytes) {
|
|
194
|
+
throw new BudgetExceededError(`maxBytes budget exceeded (limit: ${options.maxBytes}).`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
173
198
|
if (!shouldParallelize(options, files)) {
|
|
174
199
|
// In-process: reuse the exact serial path over the same file list.
|
|
175
200
|
return scan({ ...options, files: files.map((f) => f.rel) });
|
|
@@ -191,6 +216,8 @@ export async function scanParallel(options: ParallelScanOptions): Promise<ScanRe
|
|
|
191
216
|
config: options.config !== false,
|
|
192
217
|
deps: options.dependencies !== false,
|
|
193
218
|
scanMinified: options.scanMinified === true,
|
|
219
|
+
// Plain string array — structured-cloneable, so it crosses the worker boundary.
|
|
220
|
+
disabledRules: options.disabledRules,
|
|
194
221
|
};
|
|
195
222
|
|
|
196
223
|
let results: ChunkResult[];
|
|
@@ -204,9 +231,12 @@ export async function scanParallel(options: ParallelScanOptions): Promise<ScanRe
|
|
|
204
231
|
chunks,
|
|
205
232
|
concurrency,
|
|
206
233
|
options.onFile,
|
|
234
|
+
options.signal,
|
|
207
235
|
);
|
|
208
|
-
} catch {
|
|
209
|
-
//
|
|
236
|
+
} catch (err) {
|
|
237
|
+
// Cancellation / budget overflow must propagate, not silently degrade.
|
|
238
|
+
if (err instanceof AbortError || err instanceof BudgetExceededError) throw err;
|
|
239
|
+
// Any other worker failure → safe fallback to the serial path.
|
|
210
240
|
return scan({ ...options, files: files.map((f) => f.rel) });
|
|
211
241
|
}
|
|
212
242
|
|
|
@@ -214,10 +244,21 @@ export async function scanParallel(options: ParallelScanOptions): Promise<ScanRe
|
|
|
214
244
|
const inventory = buildInventory(merged.findings);
|
|
215
245
|
const finishedAt = new Date();
|
|
216
246
|
|
|
247
|
+
// Coverage: count analyzable-source files in the enumerated set. Computed from
|
|
248
|
+
// the file list (not per-worker) to avoid worker-boundary plumbing; on the
|
|
249
|
+
// parallel path this can include a minified analyzable file the workers
|
|
250
|
+
// skipped, but that is vanishingly rare and never under-reports coverage.
|
|
251
|
+
const analyzedFiles = files.reduce((n, f) => (isAnalyzableSource(f.rel) ? n + 1 : n), 0);
|
|
252
|
+
|
|
217
253
|
return {
|
|
218
254
|
root: options.root,
|
|
219
255
|
findings: merged.findings,
|
|
220
256
|
filesScanned: merged.filesScanned,
|
|
257
|
+
analyzedFiles,
|
|
258
|
+
diagnostics: {
|
|
259
|
+
unreadable: merged.unreadable ?? 0,
|
|
260
|
+
skippedMinified: merged.skippedMinified ?? 0,
|
|
261
|
+
},
|
|
221
262
|
inventory,
|
|
222
263
|
startedAt: startedAt.toISOString(),
|
|
223
264
|
finishedAt: finishedAt.toISOString(),
|
|
@@ -231,10 +272,17 @@ function runPool(
|
|
|
231
272
|
entry: string,
|
|
232
273
|
execArgv: string[] | undefined,
|
|
233
274
|
baseDir: string,
|
|
234
|
-
toggles: {
|
|
275
|
+
toggles: {
|
|
276
|
+
source: boolean;
|
|
277
|
+
config: boolean;
|
|
278
|
+
deps: boolean;
|
|
279
|
+
scanMinified: boolean;
|
|
280
|
+
disabledRules?: string[];
|
|
281
|
+
},
|
|
235
282
|
chunks: ScanChunk[],
|
|
236
283
|
concurrency: number,
|
|
237
284
|
onFile?: (file: string) => void,
|
|
285
|
+
signal?: AbortSignal,
|
|
238
286
|
): Promise<ChunkResult[]> {
|
|
239
287
|
return new Promise((resolve, reject) => {
|
|
240
288
|
const results: ChunkResult[] = new Array(chunks.length);
|
|
@@ -243,10 +291,27 @@ function runPool(
|
|
|
243
291
|
let failed = false;
|
|
244
292
|
const workers: Array<NodeWorker> = [];
|
|
245
293
|
|
|
294
|
+
const onAbort = (): void => {
|
|
295
|
+
if (failed) return;
|
|
296
|
+
failed = true;
|
|
297
|
+
cleanup();
|
|
298
|
+
reject(new AbortError());
|
|
299
|
+
};
|
|
300
|
+
|
|
246
301
|
const cleanup = () => {
|
|
302
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
247
303
|
for (const w of workers) void w.terminate();
|
|
248
304
|
};
|
|
249
305
|
|
|
306
|
+
// Cooperative cancellation: stop dispatching and tear down on abort.
|
|
307
|
+
if (signal) {
|
|
308
|
+
if (signal.aborted) {
|
|
309
|
+
reject(new AbortError());
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
313
|
+
}
|
|
314
|
+
|
|
250
315
|
const dispatch = (w: NodeWorker): void => {
|
|
251
316
|
if (failed) return;
|
|
252
317
|
if (next >= chunks.length) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch-policy allowlist. A remediation patch may only edit a file that already
|
|
3
|
+
* has a finding, or add dependencies to a manifest. Everything else — CI config,
|
|
4
|
+
* lockfiles, env/secret files, unrelated source — is denied. This is the second
|
|
5
|
+
* safety gate (the first is `verify_fix`): even a "verified" patch is dropped if
|
|
6
|
+
* it strays outside the sanctioned surface.
|
|
7
|
+
*/
|
|
8
|
+
import type { Patch } from "./agent-types.js";
|
|
9
|
+
|
|
10
|
+
/** Files a remediation may write to. */
|
|
11
|
+
export interface PolicyContext {
|
|
12
|
+
/** Relative paths (posix) that contain at least one finding. */
|
|
13
|
+
findingFiles: Set<string>;
|
|
14
|
+
/** Relative manifest paths (package.json, requirements.txt, …). */
|
|
15
|
+
manifestFiles: Set<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Paths a patch may NEVER touch, regardless of anything else: version control,
|
|
20
|
+
* CI, dependency lockfiles, and anything that looks like a secret/env file.
|
|
21
|
+
*/
|
|
22
|
+
const DENY_RE =
|
|
23
|
+
/(^|\/)(\.github|\.git|node_modules)\/|(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|Gemfile\.lock|poetry\.lock)$|(^|\/)\.env(\.[^/]*)?$|\.(pem|key|p12|pfx)$/i;
|
|
24
|
+
|
|
25
|
+
export interface PolicyDecision {
|
|
26
|
+
allowed: boolean;
|
|
27
|
+
reason?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Decide whether `patch` may be applied under `ctx`. */
|
|
31
|
+
export function checkPatchPolicy(patch: Patch, ctx: PolicyContext): PolicyDecision {
|
|
32
|
+
const p = patch.path.replace(/\\/g, "/");
|
|
33
|
+
if (DENY_RE.test(p)) {
|
|
34
|
+
return { allowed: false, reason: `patch touches a protected path (${p})` };
|
|
35
|
+
}
|
|
36
|
+
if (ctx.findingFiles.has(p)) return { allowed: true };
|
|
37
|
+
if (ctx.manifestFiles.has(p)) return { allowed: true };
|
|
38
|
+
return {
|
|
39
|
+
allowed: false,
|
|
40
|
+
reason: `patch edits ${p}, which has no finding and is not a dependency manifest`,
|
|
41
|
+
};
|
|
42
|
+
}
|
package/src/redact.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context redactor: turn a finding + its file into the payload the LLM sees,
|
|
3
|
+
* bounded to a chosen {@link ContextLevel} and with secrets ALWAYS stripped.
|
|
4
|
+
*
|
|
5
|
+
* This is a hard privacy boundary and lives in `@quantakrypto/core` (offline) so
|
|
6
|
+
* the deterministic MCP plane redacts identically to the networked CLI. A
|
|
7
|
+
* `sensitive` finding (the match IS key material) never emits code at any level.
|
|
8
|
+
*/
|
|
9
|
+
import type { Finding } from "./types.js";
|
|
10
|
+
import type { ContextLevel, RedactedContext } from "./agent-types.js";
|
|
11
|
+
|
|
12
|
+
/** Lines of context on each side of the match at `snippet` level. */
|
|
13
|
+
const SNIPPET_RADIUS = 8;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* PEM blocks and long unbroken base64 runs (≥120 chars) are treated as secret
|
|
17
|
+
* material and masked, even inside otherwise-shareable code.
|
|
18
|
+
*/
|
|
19
|
+
const SECRET_RE =
|
|
20
|
+
/-----BEGIN [A-Z0-9 ]+-----[\s\S]*?-----END [A-Z0-9 ]+-----|[A-Za-z0-9+/]{120,}={0,2}/g;
|
|
21
|
+
|
|
22
|
+
function stripSecrets(text: string): { text: string; redacted: boolean } {
|
|
23
|
+
let redacted = false;
|
|
24
|
+
const out = text.replace(SECRET_RE, () => {
|
|
25
|
+
redacted = true;
|
|
26
|
+
return "«redacted-secret»";
|
|
27
|
+
});
|
|
28
|
+
return { text: out, redacted };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Best-effort enclosing brace/colon block around a 0-based line index. */
|
|
32
|
+
function enclosingBlock(lines: string[], idx: number): string {
|
|
33
|
+
let start = idx;
|
|
34
|
+
while (start > 0 && !/[{:]\s*$/.test(lines[start - 1] ?? "")) start--;
|
|
35
|
+
let end = idx;
|
|
36
|
+
let depth = 0;
|
|
37
|
+
for (let i = start; i < lines.length; i++) {
|
|
38
|
+
const line = lines[i] ?? "";
|
|
39
|
+
depth += (line.match(/{/g) ?? []).length - (line.match(/}/g) ?? []).length;
|
|
40
|
+
end = i;
|
|
41
|
+
if (i > idx && depth <= 0) break;
|
|
42
|
+
}
|
|
43
|
+
return lines.slice(start, end + 1).join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build the redacted context for `finding` at `level`. `fileContent` is the full
|
|
48
|
+
* text of the file the finding lives in (unused at `metadata` level). Secrets
|
|
49
|
+
* are always removed; a `sensitive` finding yields `code: null`.
|
|
50
|
+
*/
|
|
51
|
+
export function buildContext(
|
|
52
|
+
finding: Finding,
|
|
53
|
+
level: ContextLevel,
|
|
54
|
+
fileContent: string,
|
|
55
|
+
): RedactedContext {
|
|
56
|
+
const meta = {
|
|
57
|
+
ruleId: finding.ruleId,
|
|
58
|
+
algorithm: finding.algorithm,
|
|
59
|
+
severity: finding.severity,
|
|
60
|
+
hndl: finding.hndl,
|
|
61
|
+
file: finding.location.file,
|
|
62
|
+
line: finding.location.line,
|
|
63
|
+
message: finding.message,
|
|
64
|
+
};
|
|
65
|
+
// Sensitive findings never emit code, at any level.
|
|
66
|
+
if (finding.sensitive) return { level, meta, code: null, redactedSecret: true };
|
|
67
|
+
if (level === "metadata") return { level, meta, code: null, redactedSecret: false };
|
|
68
|
+
|
|
69
|
+
const lines = fileContent.split("\n");
|
|
70
|
+
let code: string;
|
|
71
|
+
if (level === "file") {
|
|
72
|
+
code = fileContent;
|
|
73
|
+
} else if (level === "function") {
|
|
74
|
+
code = enclosingBlock(lines, finding.location.line - 1);
|
|
75
|
+
} else {
|
|
76
|
+
const i = finding.location.line - 1;
|
|
77
|
+
code = lines.slice(Math.max(0, i - SNIPPET_RADIUS), i + SNIPPET_RADIUS + 1).join("\n");
|
|
78
|
+
}
|
|
79
|
+
const { text, redacted } = stripSecrets(code);
|
|
80
|
+
return { level, meta, code: text, redactedSecret: redacted };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Render the exact payload text a `--dry-run` preflight would send. */
|
|
84
|
+
export function renderPreflight(contexts: RedactedContext[]): string {
|
|
85
|
+
return contexts
|
|
86
|
+
.map((c) => {
|
|
87
|
+
const flags = `level=${c.level}${c.redactedSecret ? ", secret-redacted" : ""}`;
|
|
88
|
+
const head = `[${c.meta.severity}] ${c.meta.ruleId} ${c.meta.file}:${c.meta.line} (${flags})`;
|
|
89
|
+
return c.code ? `${head}\n${c.code}` : head;
|
|
90
|
+
})
|
|
91
|
+
.join("\n\n---\n\n");
|
|
92
|
+
}
|
package/src/registry.ts
CHANGED
|
@@ -10,8 +10,15 @@
|
|
|
10
10
|
* To add a language or detector, see the "Adding a detector / language" section
|
|
11
11
|
* of the package README.
|
|
12
12
|
*/
|
|
13
|
-
import type { Detector, DetectorScope } from "./types.js";
|
|
13
|
+
import type { Detector, DetectorScope, RuleMeta } from "./types.js";
|
|
14
14
|
import { sourceDetectors } from "./detectors/source.js";
|
|
15
|
+
import { pythonDetector } from "./detectors/python.js";
|
|
16
|
+
import { goDetector } from "./detectors/go.js";
|
|
17
|
+
import { javaDetector } from "./detectors/java.js";
|
|
18
|
+
import { csharpDetector } from "./detectors/csharp.js";
|
|
19
|
+
import { rustDetector } from "./detectors/rust.js";
|
|
20
|
+
import { rubyDetector } from "./detectors/ruby.js";
|
|
21
|
+
import { cDetector } from "./detectors/c.js";
|
|
15
22
|
import { pemDetector } from "./detectors/pem.js";
|
|
16
23
|
|
|
17
24
|
/** Normalised scope of a detector (defaults to "source" when undeclared). */
|
|
@@ -19,6 +26,12 @@ export function detectorScope(d: Detector): DetectorScope {
|
|
|
19
26
|
return d.scope ?? "source";
|
|
20
27
|
}
|
|
21
28
|
|
|
29
|
+
/** A rule plus the detector that emits it — the result of {@link DetectorRegistry.forRule}. */
|
|
30
|
+
export interface RuleCatalogEntry {
|
|
31
|
+
rule: RuleMeta;
|
|
32
|
+
detector: Detector;
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
/**
|
|
23
36
|
* An ordered, id-indexed collection of detectors. Registration order is
|
|
24
37
|
* preserved by {@link all} for deterministic scan output. Ids must be unique.
|
|
@@ -57,6 +70,38 @@ export class DetectorRegistry {
|
|
|
57
70
|
return this.order.map((id) => this.byId.get(id)!);
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The flattened rule catalog: every {@link RuleMeta} declared by every
|
|
75
|
+
* registered detector, in detector-registration then in-detector order. This
|
|
76
|
+
* is the single source of truth for rule metadata consumed by SARIF
|
|
77
|
+
* `rules[]`, the MCP `explain_finding` resolver, and per-rule enable/disable.
|
|
78
|
+
* Duplicate rule ids across detectors throw (ids are globally unique).
|
|
79
|
+
*/
|
|
80
|
+
ruleCatalog(): RuleMeta[] {
|
|
81
|
+
const out: RuleMeta[] = [];
|
|
82
|
+
const seen = new Set<string>();
|
|
83
|
+
for (const det of this.all()) {
|
|
84
|
+
for (const rule of det.rules ?? []) {
|
|
85
|
+
if (seen.has(rule.id)) {
|
|
86
|
+
throw new Error(`duplicate rule id in catalog: ${rule.id}`);
|
|
87
|
+
}
|
|
88
|
+
seen.add(rule.id);
|
|
89
|
+
out.push(rule);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Resolve a rule id to its {@link RuleMeta} and the detector that emits it. */
|
|
96
|
+
forRule(ruleId: string): RuleCatalogEntry | undefined {
|
|
97
|
+
for (const det of this.all()) {
|
|
98
|
+
for (const rule of det.rules ?? []) {
|
|
99
|
+
if (rule.id === ruleId) return { rule, detector: det };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
60
105
|
/** A shallow copy of this registry (useful to extend the defaults). */
|
|
61
106
|
clone(): DetectorRegistry {
|
|
62
107
|
return new DetectorRegistry(this.all());
|
|
@@ -64,8 +109,29 @@ export class DetectorRegistry {
|
|
|
64
109
|
}
|
|
65
110
|
|
|
66
111
|
/**
|
|
67
|
-
* The
|
|
68
|
-
* the
|
|
69
|
-
* The manifest (dependency) scanner is
|
|
112
|
+
* The built-in detectors, in run order: the JS/TS source + config detectors,
|
|
113
|
+
* then the per-language detectors (Python, Go, Java, C#, Rust, Ruby, C/C++),
|
|
114
|
+
* then the language-agnostic PEM detector. The manifest (dependency) scanner is
|
|
115
|
+
* handled separately by `scan()`.
|
|
116
|
+
*
|
|
117
|
+
* This is the single source of truth for the default detector set: both
|
|
118
|
+
* {@link defaultRegistry} and the public `detectors` export (re-exported from
|
|
119
|
+
* `scan.ts`) are built from it, so the two can never drift out of sync.
|
|
120
|
+
*/
|
|
121
|
+
export const builtinDetectors: Detector[] = [
|
|
122
|
+
...sourceDetectors,
|
|
123
|
+
pythonDetector,
|
|
124
|
+
goDetector,
|
|
125
|
+
javaDetector,
|
|
126
|
+
csharpDetector,
|
|
127
|
+
rustDetector,
|
|
128
|
+
rubyDetector,
|
|
129
|
+
cDetector,
|
|
130
|
+
pemDetector,
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The default registry, preloaded with {@link builtinDetectors}. Used by
|
|
135
|
+
* `scan()` whenever `options.detectors` is not supplied.
|
|
70
136
|
*/
|
|
71
|
-
export const defaultRegistry = new DetectorRegistry(
|
|
137
|
+
export const defaultRegistry = new DetectorRegistry(builtinDetectors);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The remediation pipeline: turn candidate patches (from codemods or the LLM)
|
|
3
|
+
* into VERIFIED, in-policy patches. Pure and deterministic — the verify gate
|
|
4
|
+
* runs `verifyFix` on the patched content in memory, so this needs no
|
|
5
|
+
* filesystem or git. Callers (the `qremediate` CLI) decide what to DO with the
|
|
6
|
+
* verified patches (print a diff, write them, or open a PR in a worktree).
|
|
7
|
+
*
|
|
8
|
+
* Two gates, both must pass:
|
|
9
|
+
* 1. patch-policy — the patch may only touch sanctioned files.
|
|
10
|
+
* 2. verify_fix — the patched content clears the target finding, adds no
|
|
11
|
+
* new finding type, and nets fewer findings than before.
|
|
12
|
+
*/
|
|
13
|
+
import type { Finding } from "./types.js";
|
|
14
|
+
import type { Patch } from "./agent-types.js";
|
|
15
|
+
import { checkPatchPolicy } from "./patch-policy.js";
|
|
16
|
+
import type { PolicyContext } from "./patch-policy.js";
|
|
17
|
+
import { verifyFix } from "./verify.js";
|
|
18
|
+
|
|
19
|
+
export interface VerifiedPatch {
|
|
20
|
+
finding: Finding;
|
|
21
|
+
patch: Patch;
|
|
22
|
+
}
|
|
23
|
+
export interface RejectedPatch {
|
|
24
|
+
finding: Finding;
|
|
25
|
+
reason: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RemediationResult {
|
|
28
|
+
applied: VerifiedPatch[];
|
|
29
|
+
rejected: RejectedPatch[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RemediateOptions {
|
|
33
|
+
/** Current content of the file a finding lives in. */
|
|
34
|
+
readContent: (finding: Finding) => Promise<string> | string;
|
|
35
|
+
/** Produce a candidate patch for a finding (codemod or LLM), or null. */
|
|
36
|
+
patchSource: (finding: Finding, content: string) => Promise<Patch | null> | Patch | null;
|
|
37
|
+
/** Which files a patch is allowed to touch. */
|
|
38
|
+
policy: PolicyContext;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** True when `after` is a strict improvement over `before` for `finding`. */
|
|
42
|
+
function passesVerify(before: Finding[], after: Finding[], finding: Finding): boolean {
|
|
43
|
+
const targetGone = !after.some((x) => x.ruleId === finding.ruleId);
|
|
44
|
+
const noNewRuleTypes = after.every((x) => before.some((b) => b.ruleId === x.ruleId));
|
|
45
|
+
return targetGone && noNewRuleTypes && after.length < before.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run each finding through patchSource → policy → verify, collecting the patches
|
|
50
|
+
* that survive both gates and the reasons the rest were dropped.
|
|
51
|
+
*/
|
|
52
|
+
export async function remediateFindings(
|
|
53
|
+
findings: readonly Finding[],
|
|
54
|
+
opts: RemediateOptions,
|
|
55
|
+
): Promise<RemediationResult> {
|
|
56
|
+
const applied: VerifiedPatch[] = [];
|
|
57
|
+
const rejected: RejectedPatch[] = [];
|
|
58
|
+
|
|
59
|
+
for (const finding of findings) {
|
|
60
|
+
const content = await opts.readContent(finding);
|
|
61
|
+
const patch = await opts.patchSource(finding, content);
|
|
62
|
+
if (!patch) {
|
|
63
|
+
rejected.push({ finding, reason: "no deterministic fix available" });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const decision = checkPatchPolicy(patch, opts.policy);
|
|
67
|
+
if (!decision.allowed) {
|
|
68
|
+
rejected.push({ finding, reason: decision.reason ?? "rejected by patch policy" });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const before = verifyFix(content, { filename: finding.location.file }).findings;
|
|
72
|
+
const after = verifyFix(patch.newContent, { filename: patch.path }).findings;
|
|
73
|
+
if (!passesVerify(before, after, finding)) {
|
|
74
|
+
rejected.push({ finding, reason: "patch did not pass the verify_fix gate" });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
applied.push({ finding, patch });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { applied, rejected };
|
|
81
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remediation rubric, fix schema, and request-bundle builder — offline and
|
|
3
|
+
* deterministic, shared by `@quantakrypto/agent` (which prompts a model with
|
|
4
|
+
* them) and the MCP `remediate_findings` tool (which hands the bundle to the
|
|
5
|
+
* host agent). Keeping them here means the MCP server stays offline/key-free.
|
|
6
|
+
*/
|
|
7
|
+
import type { Finding } from "./types.js";
|
|
8
|
+
import type { ContextLevel, RedactedContext } from "./agent-types.js";
|
|
9
|
+
import { buildContext } from "./redact.js";
|
|
10
|
+
|
|
11
|
+
/** The system rubric for a fix proposal. */
|
|
12
|
+
export const REMEDIATE_RUBRIC =
|
|
13
|
+
"You are a post-quantum cryptography migration engineer. Given the FULL content " +
|
|
14
|
+
"of one source file plus a finding describing classical (quantum-vulnerable) " +
|
|
15
|
+
"cryptography in it, return the FULL corrected file content that removes the " +
|
|
16
|
+
"flagged usage, migrating to a post-quantum or hybrid construction " +
|
|
17
|
+
"(ML-KEM-768 / ML-DSA-65, hybrid X25519MLKEM768) where a safe replacement " +
|
|
18
|
+
"exists. Change as little as possible; preserve all other code and formatting " +
|
|
19
|
+
"exactly. If you cannot safely fix it, return newContent identical to the " +
|
|
20
|
+
"input. NEVER invent or alter secrets/keys. After proposing, VERIFY with the " +
|
|
21
|
+
"verify_fix tool and keep only fixes that clear the finding.";
|
|
22
|
+
|
|
23
|
+
/** JSON Schema every fix proposal must satisfy. */
|
|
24
|
+
export const FIX_REQUEST_SCHEMA: Record<string, unknown> = {
|
|
25
|
+
type: "object",
|
|
26
|
+
required: ["path", "newContent", "explanation"],
|
|
27
|
+
properties: {
|
|
28
|
+
path: { type: "string" },
|
|
29
|
+
newContent: { type: "string" },
|
|
30
|
+
explanation: { type: "string" },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** A remediation request bundle for the host agent. */
|
|
35
|
+
export interface RemediateRequest {
|
|
36
|
+
instructions: string;
|
|
37
|
+
schema: Record<string, unknown>;
|
|
38
|
+
contexts: RedactedContext[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Build a remediation request bundle (offline; metadata level unless `readContent`). */
|
|
42
|
+
export function buildRemediateRequest(
|
|
43
|
+
findings: readonly Finding[],
|
|
44
|
+
level: ContextLevel = "metadata",
|
|
45
|
+
readContent?: (finding: Finding) => string,
|
|
46
|
+
): RemediateRequest {
|
|
47
|
+
const effectiveLevel: ContextLevel = readContent ? level : "metadata";
|
|
48
|
+
const contexts = findings.map((f) =>
|
|
49
|
+
buildContext(f, effectiveLevel, readContent ? readContent(f) : ""),
|
|
50
|
+
);
|
|
51
|
+
return { instructions: REMEDIATE_RUBRIC, schema: FIX_REQUEST_SCHEMA, contexts };
|
|
52
|
+
}
|
package/src/remediation.ts
CHANGED
|
@@ -96,6 +96,31 @@ export function remediationText(algorithm: AlgorithmFamily): string {
|
|
|
96
96
|
return REMEDIATIONS[algorithm].recommendation;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* True when a family's PQC replacement is a KEM — i.e. it's used for
|
|
101
|
+
* confidentiality / key-agreement (and is therefore harvest-now-decrypt-later
|
|
102
|
+
* exposed). RSA is in both this set and {@link isSignatureFamily}. Single
|
|
103
|
+
* source of truth for the family→target taxonomy used by tier guidance and the
|
|
104
|
+
* multi-family remediation composer.
|
|
105
|
+
*/
|
|
106
|
+
export function isConfidentialityFamily(algorithm: AlgorithmFamily): boolean {
|
|
107
|
+
return (
|
|
108
|
+
algorithm === "RSA" ||
|
|
109
|
+
algorithm === "ECDH" ||
|
|
110
|
+
algorithm === "DH" ||
|
|
111
|
+
algorithm === "X25519" ||
|
|
112
|
+
algorithm === "X448" ||
|
|
113
|
+
algorithm === "ECIES"
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** True when a family's PQC replacement is a signature scheme (ML-DSA / SLH-DSA). */
|
|
118
|
+
export function isSignatureFamily(algorithm: AlgorithmFamily): boolean {
|
|
119
|
+
return (
|
|
120
|
+
algorithm === "RSA" || algorithm === "ECDSA" || algorithm === "EdDSA" || algorithm === "DSA"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
99
124
|
/* -------------------------------------------------------------------------- */
|
|
100
125
|
/* Security-tier guidance (CNSA 2.0 Category 5) + stateful HBS (SP 800-208) */
|
|
101
126
|
/* -------------------------------------------------------------------------- */
|
|
@@ -135,14 +160,7 @@ export function remediationForTier(
|
|
|
135
160
|
const base = REMEDIATIONS[algorithm];
|
|
136
161
|
const params = TIER_PARAMS[tier];
|
|
137
162
|
// Confidentiality families lean on the KEM; signature families on the signer.
|
|
138
|
-
const
|
|
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;
|
|
163
|
+
const primary = isConfidentialityFamily(algorithm) ? params.kem : params.signature;
|
|
146
164
|
return {
|
|
147
165
|
algorithm,
|
|
148
166
|
recommendation: `${base.recommendation} — ${tier}: ${primary}`,
|
|
@@ -167,7 +185,5 @@ export const STATEFUL_HBS_NOTE =
|
|
|
167
185
|
/** True when stateful HBS (SP 800-208) is a reasonable alternative for a family. */
|
|
168
186
|
export function statefulHbsApplies(algorithm: AlgorithmFamily): boolean {
|
|
169
187
|
// Signature families only — LMS/XMSS are signatures, not KEMs.
|
|
170
|
-
return (
|
|
171
|
-
algorithm === "RSA" || algorithm === "ECDSA" || algorithm === "EdDSA" || algorithm === "DSA"
|
|
172
|
-
);
|
|
188
|
+
return isSignatureFamily(algorithm);
|
|
173
189
|
}
|