@quantakrypto/core 0.2.2 → 0.3.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.
Files changed (49) hide show
  1. package/dist/detect-utils.d.ts +26 -1
  2. package/dist/detect-utils.d.ts.map +1 -1
  3. package/dist/detect-utils.js +26 -0
  4. package/dist/detect-utils.js.map +1 -1
  5. package/dist/detectors/pem.d.ts +4 -0
  6. package/dist/detectors/pem.d.ts.map +1 -1
  7. package/dist/detectors/pem.js +113 -91
  8. package/dist/detectors/pem.js.map +1 -1
  9. package/dist/detectors/source.d.ts +9 -2
  10. package/dist/detectors/source.d.ts.map +1 -1
  11. package/dist/detectors/source.js +366 -265
  12. package/dist/detectors/source.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/parallel.d.ts.map +1 -1
  18. package/dist/parallel.js +2 -0
  19. package/dist/parallel.js.map +1 -1
  20. package/dist/registry.d.ts +16 -1
  21. package/dist/registry.d.ts.map +1 -1
  22. package/dist/registry.js +31 -0
  23. package/dist/registry.js.map +1 -1
  24. package/dist/report.d.ts +9 -1
  25. package/dist/report.d.ts.map +1 -1
  26. package/dist/report.js +56 -23
  27. package/dist/report.js.map +1 -1
  28. package/dist/scan-worker.js +1 -1
  29. package/dist/scan-worker.js.map +1 -1
  30. package/dist/scan.d.ts +1 -1
  31. package/dist/scan.d.ts.map +1 -1
  32. package/dist/scan.js +8 -2
  33. package/dist/scan.js.map +1 -1
  34. package/dist/types.d.ts +53 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/package.json +1 -1
  39. package/src/detect-utils.ts +57 -1
  40. package/src/detectors/pem.ts +124 -105
  41. package/src/detectors/source.ts +466 -343
  42. package/src/index.ts +2 -1
  43. package/src/parallel.ts +9 -1
  44. package/src/registry.ts +39 -1
  45. package/src/report.ts +79 -23
  46. package/src/scan-worker.ts +12 -5
  47. package/src/scan.ts +19 -5
  48. package/src/types.ts +54 -0
  49. package/src/version.ts +1 -1
package/src/index.ts CHANGED
@@ -25,8 +25,9 @@ export { AbortError, BudgetExceededError } from "./errors.js";
25
25
  export { scanParallel, mergeChunkResults, chunkByBytes } from "./parallel.js";
26
26
  export type { ScanChunk, ChunkResult, SizedFile } from "./parallel.js";
27
27
 
28
- // Detector registry (plugin point) + helpers.
28
+ // Detector registry (plugin point) + helpers + the rule catalog.
29
29
  export { DetectorRegistry, defaultRegistry, detectorScope } from "./registry.js";
30
+ export type { RuleCatalogEntry } from "./registry.js";
30
31
 
31
32
  // Canonical baseline (shared by qScan + the Action).
32
33
  export {
package/src/parallel.ts CHANGED
@@ -191,6 +191,8 @@ export async function scanParallel(options: ParallelScanOptions): Promise<ScanRe
191
191
  config: options.config !== false,
192
192
  deps: options.dependencies !== false,
193
193
  scanMinified: options.scanMinified === true,
194
+ // Plain string array — structured-cloneable, so it crosses the worker boundary.
195
+ disabledRules: options.disabledRules,
194
196
  };
195
197
 
196
198
  let results: ChunkResult[];
@@ -231,7 +233,13 @@ function runPool(
231
233
  entry: string,
232
234
  execArgv: string[] | undefined,
233
235
  baseDir: string,
234
- toggles: { source: boolean; config: boolean; deps: boolean; scanMinified: boolean },
236
+ toggles: {
237
+ source: boolean;
238
+ config: boolean;
239
+ deps: boolean;
240
+ scanMinified: boolean;
241
+ disabledRules?: string[];
242
+ },
235
243
  chunks: ScanChunk[],
236
244
  concurrency: number,
237
245
  onFile?: (file: string) => void,
package/src/registry.ts CHANGED
@@ -10,7 +10,7 @@
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
15
  import { pemDetector } from "./detectors/pem.js";
16
16
 
@@ -19,6 +19,12 @@ export function detectorScope(d: Detector): DetectorScope {
19
19
  return d.scope ?? "source";
20
20
  }
21
21
 
22
+ /** A rule plus the detector that emits it — the result of {@link DetectorRegistry.forRule}. */
23
+ export interface RuleCatalogEntry {
24
+ rule: RuleMeta;
25
+ detector: Detector;
26
+ }
27
+
22
28
  /**
23
29
  * An ordered, id-indexed collection of detectors. Registration order is
24
30
  * preserved by {@link all} for deterministic scan output. Ids must be unique.
@@ -57,6 +63,38 @@ export class DetectorRegistry {
57
63
  return this.order.map((id) => this.byId.get(id)!);
58
64
  }
59
65
 
66
+ /**
67
+ * The flattened rule catalog: every {@link RuleMeta} declared by every
68
+ * registered detector, in detector-registration then in-detector order. This
69
+ * is the single source of truth for rule metadata consumed by SARIF
70
+ * `rules[]`, the MCP `explain_finding` resolver, and per-rule enable/disable.
71
+ * Duplicate rule ids across detectors throw (ids are globally unique).
72
+ */
73
+ ruleCatalog(): RuleMeta[] {
74
+ const out: RuleMeta[] = [];
75
+ const seen = new Set<string>();
76
+ for (const det of this.all()) {
77
+ for (const rule of det.rules ?? []) {
78
+ if (seen.has(rule.id)) {
79
+ throw new Error(`duplicate rule id in catalog: ${rule.id}`);
80
+ }
81
+ seen.add(rule.id);
82
+ out.push(rule);
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+
88
+ /** Resolve a rule id to its {@link RuleMeta} and the detector that emits it. */
89
+ forRule(ruleId: string): RuleCatalogEntry | undefined {
90
+ for (const det of this.all()) {
91
+ for (const rule of det.rules ?? []) {
92
+ if (rule.id === ruleId) return { rule, detector: det };
93
+ }
94
+ }
95
+ return undefined;
96
+ }
97
+
60
98
  /** A shallow copy of this registry (useful to extend the defaults). */
61
99
  clone(): DetectorRegistry {
62
100
  return new DetectorRegistry(this.all());
package/src/report.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * or a human-readable text summary. No third-party dependencies — ANSI colour
4
4
  * is emitted with raw escape codes and is off by default.
5
5
  */
6
- import type { Finding, ScanResult, Severity } from "./types.js";
6
+ import type { Finding, RuleMeta, ScanResult, Severity } from "./types.js";
7
7
  import { VERSION } from "./version.js";
8
8
  import { SEVERITY_ORDER, sarifLevel } from "./severity.js";
9
9
 
@@ -23,6 +23,14 @@ export interface ReportOptions {
23
23
  * snippet there IS the sensitive value.
24
24
  */
25
25
  redactSnippets?: boolean;
26
+ /**
27
+ * Full rule catalog to advertise in SARIF `tool.driver.rules[]`, even for
28
+ * rules that produced no finding in this run. Pass
29
+ * `defaultRegistry.ruleCatalog()`. When omitted, only the rules that actually
30
+ * fired are emitted (the historical behaviour). SARIF-only; ignored by
31
+ * {@link toJson}.
32
+ */
33
+ catalog?: RuleMeta[];
26
34
  }
27
35
 
28
36
  const SARIF_SCHEMA =
@@ -56,40 +64,88 @@ function sarifRank(severity: Severity): number {
56
64
  }
57
65
  }
58
66
 
67
+ /** Build a SARIF `rules[]` entry from a rule's severity/title/message/etc. */
68
+ function sarifRule(spec: {
69
+ id: string;
70
+ title: string;
71
+ message: string;
72
+ severity: Severity;
73
+ category: string;
74
+ algorithm?: string;
75
+ hndl: boolean;
76
+ cwe?: string;
77
+ remediation?: string;
78
+ }): Record<string, unknown> {
79
+ return {
80
+ id: spec.id,
81
+ name: spec.id,
82
+ shortDescription: { text: spec.title },
83
+ fullDescription: { text: spec.message },
84
+ defaultConfiguration: { level: sarifLevel(spec.severity), rank: sarifRank(spec.severity) },
85
+ ...(spec.remediation ? { help: { text: `Remediation: ${spec.remediation}` } } : {}),
86
+ properties: {
87
+ category: spec.category,
88
+ ...(spec.algorithm ? { algorithm: spec.algorithm } : {}),
89
+ hndl: spec.hndl,
90
+ ...(spec.cwe ? { cwe: spec.cwe, "security-severity": securitySeverity(spec.severity) } : {}),
91
+ ...(spec.cwe ? { tags: ["security", spec.cwe] } : {}),
92
+ },
93
+ ...(spec.cwe
94
+ ? {
95
+ relationships: [
96
+ { target: { id: spec.cwe, toolComponent: { name: "CWE" } }, kinds: ["relevant"] },
97
+ ],
98
+ }
99
+ : {}),
100
+ };
101
+ }
102
+
59
103
  /** Serialize a scan result as SARIF 2.1.0. */
60
104
  export function toSarif(result: ScanResult, opts?: ReportOptions): SarifLog {
61
105
  const redactSnippets = opts?.redactSnippets ?? false;
62
- // Build the unique rule set (one rule per ruleId encountered) and collect the
63
- // set of CWE taxa referenced by any rule.
106
+ // Build the rule set and collect the CWE taxa referenced by any rule. When a
107
+ // full catalog is supplied, advertise every rule (even ones that didn't fire);
108
+ // otherwise emit one rule per ruleId encountered (the historical behaviour).
64
109
  const ruleIndex = new Map<string, number>();
65
110
  const rules: Array<Record<string, unknown>> = [];
66
111
  const cweTaxa = new Set<string>();
112
+
113
+ for (const r of opts?.catalog ?? []) {
114
+ if (ruleIndex.has(r.id)) continue;
115
+ if (r.cwe) cweTaxa.add(r.cwe);
116
+ ruleIndex.set(r.id, rules.length);
117
+ rules.push(
118
+ sarifRule({
119
+ id: r.id,
120
+ title: r.title,
121
+ message: r.message,
122
+ severity: r.severity,
123
+ category: r.category,
124
+ algorithm: r.algorithm,
125
+ hndl: r.hndl,
126
+ cwe: r.cwe,
127
+ remediation: r.remediation,
128
+ }),
129
+ );
130
+ }
131
+
67
132
  for (const f of result.findings) {
68
133
  if (f.cwe) cweTaxa.add(f.cwe);
69
134
  if (ruleIndex.has(f.ruleId)) continue;
70
135
  ruleIndex.set(f.ruleId, rules.length);
71
- rules.push({
72
- id: f.ruleId,
73
- name: f.ruleId,
74
- shortDescription: { text: f.title },
75
- fullDescription: { text: f.message },
76
- defaultConfiguration: { level: sarifLevel(f.severity), rank: sarifRank(f.severity) },
77
- ...(f.remediation ? { help: { text: `Remediation: ${f.remediation}` } } : {}),
78
- properties: {
136
+ rules.push(
137
+ sarifRule({
138
+ id: f.ruleId,
139
+ title: f.title,
140
+ message: f.message,
141
+ severity: f.severity,
79
142
  category: f.category,
80
- ...(f.algorithm ? { algorithm: f.algorithm } : {}),
143
+ algorithm: f.algorithm,
81
144
  hndl: f.hndl,
82
- ...(f.cwe ? { cwe: f.cwe, "security-severity": securitySeverity(f.severity) } : {}),
83
- ...(f.cwe ? { tags: ["security", f.cwe] } : {}),
84
- },
85
- ...(f.cwe
86
- ? {
87
- relationships: [
88
- { target: { id: f.cwe, toolComponent: { name: "CWE" } }, kinds: ["relevant"] },
89
- ],
90
- }
91
- : {}),
92
- });
145
+ cwe: f.cwe,
146
+ remediation: f.remediation,
147
+ }),
148
+ );
93
149
  }
94
150
 
95
151
  const results = result.findings.map((f) => {
@@ -23,6 +23,7 @@ interface WorkerToggles {
23
23
  config: boolean;
24
24
  deps: boolean;
25
25
  scanMinified: boolean;
26
+ disabledRules?: string[];
26
27
  }
27
28
 
28
29
  interface ChunkRequest {
@@ -57,11 +58,17 @@ if (parentPort) {
57
58
  filesScanned += 1;
58
59
  scannedNames.push(rel);
59
60
  findings.push(
60
- ...detectFile(rel, content, dets, {
61
- source: toggles.source,
62
- config: toggles.config,
63
- deps: toggles.deps,
64
- }),
61
+ ...detectFile(
62
+ rel,
63
+ content,
64
+ dets,
65
+ {
66
+ source: toggles.source,
67
+ config: toggles.config,
68
+ deps: toggles.deps,
69
+ },
70
+ toggles.disabledRules,
71
+ ),
65
72
  );
66
73
  }
67
74
 
package/src/scan.ts CHANGED
@@ -52,6 +52,7 @@ export function detectFile(
52
52
  content: string,
53
53
  dets: readonly Detector[],
54
54
  toggles: { source: boolean; config: boolean; deps: boolean },
55
+ disabledRules?: readonly string[],
55
56
  ): Finding[] {
56
57
  const out: Finding[] = [];
57
58
 
@@ -66,6 +67,13 @@ export function detectFile(
66
67
  out.push(...scanManifest(file, content));
67
68
  }
68
69
 
70
+ // Per-rule suppression: drop findings whose ruleId was disabled. Applied here
71
+ // so both the serial scan and the worker path honour it identically.
72
+ if (disabledRules && disabledRules.length > 0) {
73
+ const disabled = new Set(disabledRules);
74
+ return out.filter((f) => !disabled.has(f.ruleId));
75
+ }
76
+
69
77
  return out;
70
78
  }
71
79
 
@@ -144,11 +152,17 @@ export async function scan(options: ScanOptions): Promise<ScanResult> {
144
152
 
145
153
  filesScanned += 1;
146
154
  findings.push(
147
- ...detectFile(reportedPath, content, dets, {
148
- source: doSource,
149
- config: doConfig,
150
- deps: doDeps,
151
- }),
155
+ ...detectFile(
156
+ reportedPath,
157
+ content,
158
+ dets,
159
+ {
160
+ source: doSource,
161
+ config: doConfig,
162
+ deps: doDeps,
163
+ },
164
+ options.disabledRules,
165
+ ),
152
166
  );
153
167
  }
154
168
 
package/src/types.ts CHANGED
@@ -101,6 +101,46 @@ export type DetectorScope = "source" | "config";
101
101
  */
102
102
  export type DetectorLanguage = "js" | "python" | "go" | "java" | "any";
103
103
 
104
+ /**
105
+ * Declarative metadata for a single rule a detector can emit. This is the
106
+ * catalog entry: the stable, queryable description of a rule keyed by the
107
+ * `ruleId` it stamps onto findings. It is the single source of truth for a
108
+ * rule's title / severity / category / remediation, consumed by the SARIF
109
+ * `rules[]` block, the MCP `explain_finding` resolver, and future per-rule
110
+ * enable/disable + language-pack work.
111
+ *
112
+ * For most rules the metadata is fixed and `detect()` builds findings straight
113
+ * from it (see `findingFromRule`). A few rules are inherently multi-variant
114
+ * (e.g. `node-crypto-keygen` spans RSA/EC/DSA/DH/Ed25519 at different
115
+ * severities); for those the metadata here is a REPRESENTATIVE umbrella and
116
+ * `detect()` refines the per-finding fields at match time. The catalog always
117
+ * enumerates every emittable ruleId regardless.
118
+ */
119
+ export interface RuleMeta {
120
+ /** Stable rule id — matches {@link Finding.ruleId}. Unique across the catalog. */
121
+ id: string;
122
+ /** Canonical human title. */
123
+ title: string;
124
+ category: FindingCategory;
125
+ severity: Severity;
126
+ /** Default confidence for findings of this rule. */
127
+ confidence: Confidence;
128
+ /** Harvest-now-decrypt-later exposure. */
129
+ hndl: boolean;
130
+ /** Representative classical algorithm family; refined per-finding when it varies. */
131
+ algorithm?: AlgorithmFamily;
132
+ /** Associated CWE identifier (e.g. "CWE-327"). */
133
+ cwe?: string;
134
+ /** Suggested post-quantum remediation. When omitted, derived from {@link algorithm}. */
135
+ remediation?: string;
136
+ /** Canonical one-line human explanation; may be refined per-finding. */
137
+ message: string;
138
+ /** True when this rule's matched snippet IS sensitive key material. */
139
+ sensitive?: boolean;
140
+ /** Short description of what the rule detects (for catalog / MCP surfaces). */
141
+ description?: string;
142
+ }
143
+
104
144
  /** A pluggable source detector. Detectors are pure and stateless. */
105
145
  export interface Detector {
106
146
  /** Unique id, used as the Finding.ruleId prefix space. */
@@ -118,6 +158,13 @@ export interface Detector {
118
158
  * Defaults to `"js"` when omitted.
119
159
  */
120
160
  language?: DetectorLanguage;
161
+ /**
162
+ * The rules this detector can emit, as declarative metadata. Together across
163
+ * all detectors these form the rule catalog ({@link DetectorRegistry.ruleCatalog}).
164
+ * Optional for backward compatibility with externally-defined detectors, but
165
+ * all built-in detectors declare it.
166
+ */
167
+ rules?: RuleMeta[];
121
168
  /** Whether this detector should run against the given file path. */
122
169
  appliesTo(filePath: string): boolean;
123
170
  /** Inspect a single file's contents and return zero or more findings. */
@@ -170,6 +217,13 @@ export interface ScanOptions {
170
217
  * registry's detectors are used.
171
218
  */
172
219
  detectors?: Detector[];
220
+ /**
221
+ * Rule ids to suppress. Any finding whose `ruleId` is listed here is dropped
222
+ * after detection. Serializable (a plain string array), so it is honoured on
223
+ * both the serial and the worker-thread (`scanParallel`) paths. See the rule
224
+ * catalog ({@link DetectorRegistry.ruleCatalog}) for the valid ids.
225
+ */
226
+ disabledRules?: string[];
173
227
  /** Optional progress callback. */
174
228
  onFile?: (file: string) => void;
175
229
  /**
package/src/version.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  * the scan orchestrator can import it without creating a cycle through index.ts.
4
4
  * Keep in sync with packages/core/package.json.
5
5
  */
6
- export const VERSION = "0.2.2";
6
+ export const VERSION = "0.3.0";