@vyuhlabs/dxkit 2.9.4 → 2.11.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 (177) hide show
  1. package/CHANGELOG.md +236 -0
  2. package/dist/allowlist/annotate.d.ts +71 -0
  3. package/dist/allowlist/annotate.d.ts.map +1 -0
  4. package/dist/allowlist/annotate.js +105 -0
  5. package/dist/allowlist/annotate.js.map +1 -0
  6. package/dist/allowlist/cli.d.ts +29 -23
  7. package/dist/allowlist/cli.d.ts.map +1 -1
  8. package/dist/allowlist/cli.js +141 -70
  9. package/dist/allowlist/cli.js.map +1 -1
  10. package/dist/allowlist/file.d.ts +7 -1
  11. package/dist/allowlist/file.d.ts.map +1 -1
  12. package/dist/allowlist/file.js +7 -1
  13. package/dist/allowlist/file.js.map +1 -1
  14. package/dist/analysis-result.d.ts +10 -0
  15. package/dist/analysis-result.d.ts.map +1 -1
  16. package/dist/analyzers/cache.d.ts +1 -0
  17. package/dist/analyzers/cache.d.ts.map +1 -1
  18. package/dist/analyzers/cache.js +69 -0
  19. package/dist/analyzers/cache.js.map +1 -1
  20. package/dist/analyzers/dashboard/index.d.ts.map +1 -1
  21. package/dist/analyzers/dashboard/index.js +6 -1
  22. package/dist/analyzers/dashboard/index.js.map +1 -1
  23. package/dist/analyzers/health.d.ts.map +1 -1
  24. package/dist/analyzers/health.js +17 -2
  25. package/dist/analyzers/health.js.map +1 -1
  26. package/dist/analyzers/security/actions.d.ts.map +1 -1
  27. package/dist/analyzers/security/actions.js +13 -0
  28. package/dist/analyzers/security/actions.js.map +1 -1
  29. package/dist/analyzers/security/aggregator.d.ts +97 -79
  30. package/dist/analyzers/security/aggregator.d.ts.map +1 -1
  31. package/dist/analyzers/security/aggregator.js +168 -56
  32. package/dist/analyzers/security/aggregator.js.map +1 -1
  33. package/dist/analyzers/security/gather.d.ts +2 -0
  34. package/dist/analyzers/security/gather.d.ts.map +1 -1
  35. package/dist/analyzers/security/gather.js +36 -4
  36. package/dist/analyzers/security/gather.js.map +1 -1
  37. package/dist/analyzers/security/index.d.ts.map +1 -1
  38. package/dist/analyzers/security/index.js +81 -2
  39. package/dist/analyzers/security/index.js.map +1 -1
  40. package/dist/analyzers/security/scanner-drift.d.ts +21 -0
  41. package/dist/analyzers/security/scanner-drift.d.ts.map +1 -0
  42. package/dist/analyzers/security/scanner-drift.js +113 -0
  43. package/dist/analyzers/security/scanner-drift.js.map +1 -0
  44. package/dist/analyzers/security/shallow.d.ts.map +1 -1
  45. package/dist/analyzers/security/shallow.js +24 -2
  46. package/dist/analyzers/security/shallow.js.map +1 -1
  47. package/dist/analyzers/security/types.d.ts +64 -4
  48. package/dist/analyzers/security/types.d.ts.map +1 -1
  49. package/dist/analyzers/tools/fingerprint.d.ts +133 -20
  50. package/dist/analyzers/tools/fingerprint.d.ts.map +1 -1
  51. package/dist/analyzers/tools/fingerprint.js +194 -20
  52. package/dist/analyzers/tools/fingerprint.js.map +1 -1
  53. package/dist/analyzers/tools/gitleaks.d.ts +2 -2
  54. package/dist/analyzers/tools/gitleaks.d.ts.map +1 -1
  55. package/dist/analyzers/tools/gitleaks.js +7 -1
  56. package/dist/analyzers/tools/gitleaks.js.map +1 -1
  57. package/dist/analyzers/tools/graphify.d.ts +11 -0
  58. package/dist/analyzers/tools/graphify.d.ts.map +1 -1
  59. package/dist/analyzers/tools/graphify.js +457 -413
  60. package/dist/analyzers/tools/graphify.js.map +1 -1
  61. package/dist/analyzers/tools/grep-secrets.d.ts.map +1 -1
  62. package/dist/analyzers/tools/grep-secrets.js +31 -12
  63. package/dist/analyzers/tools/grep-secrets.js.map +1 -1
  64. package/dist/analyzers/tools/osv-scanner-fix.d.ts.map +1 -1
  65. package/dist/analyzers/tools/osv-scanner-fix.js +12 -1
  66. package/dist/analyzers/tools/osv-scanner-fix.js.map +1 -1
  67. package/dist/analyzers/tools/salt.d.ts +68 -0
  68. package/dist/analyzers/tools/salt.d.ts.map +1 -0
  69. package/dist/{baseline → analyzers/tools}/salt.js +59 -18
  70. package/dist/analyzers/tools/salt.js.map +1 -0
  71. package/dist/analyzers/tools/semgrep.d.ts +7 -7
  72. package/dist/analyzers/tools/semgrep.d.ts.map +1 -1
  73. package/dist/analyzers/tools/semgrep.js +14 -7
  74. package/dist/analyzers/tools/semgrep.js.map +1 -1
  75. package/dist/analyzers/tools/tool-registry.d.ts.map +1 -1
  76. package/dist/analyzers/tools/tool-registry.js +78 -43
  77. package/dist/analyzers/tools/tool-registry.js.map +1 -1
  78. package/dist/analyzers/tools/walk-source-files.d.ts +10 -0
  79. package/dist/analyzers/tools/walk-source-files.d.ts.map +1 -1
  80. package/dist/analyzers/tools/walk-source-files.js +14 -0
  81. package/dist/analyzers/tools/walk-source-files.js.map +1 -1
  82. package/dist/analyzers/types.d.ts +9 -0
  83. package/dist/analyzers/types.d.ts.map +1 -1
  84. package/dist/baseline/baseline-file.d.ts +9 -2
  85. package/dist/baseline/baseline-file.d.ts.map +1 -1
  86. package/dist/baseline/baseline-file.js.map +1 -1
  87. package/dist/baseline/check-renderers.d.ts.map +1 -1
  88. package/dist/baseline/check-renderers.js +14 -0
  89. package/dist/baseline/check-renderers.js.map +1 -1
  90. package/dist/baseline/check.d.ts +33 -0
  91. package/dist/baseline/check.d.ts.map +1 -1
  92. package/dist/baseline/check.js +78 -2
  93. package/dist/baseline/check.js.map +1 -1
  94. package/dist/baseline/create.d.ts +1 -1
  95. package/dist/baseline/create.d.ts.map +1 -1
  96. package/dist/baseline/create.js +3 -1
  97. package/dist/baseline/create.js.map +1 -1
  98. package/dist/baseline/entry-to-located.d.ts +12 -5
  99. package/dist/baseline/entry-to-located.d.ts.map +1 -1
  100. package/dist/baseline/entry-to-located.js +21 -7
  101. package/dist/baseline/entry-to-located.js.map +1 -1
  102. package/dist/baseline/finding-identity.d.ts +20 -13
  103. package/dist/baseline/finding-identity.d.ts.map +1 -1
  104. package/dist/baseline/finding-identity.js +51 -20
  105. package/dist/baseline/finding-identity.js.map +1 -1
  106. package/dist/baseline/git-aware-match.d.ts +7 -5
  107. package/dist/baseline/git-aware-match.d.ts.map +1 -1
  108. package/dist/baseline/git-aware-match.js +78 -5
  109. package/dist/baseline/git-aware-match.js.map +1 -1
  110. package/dist/baseline/migrate.d.ts +94 -0
  111. package/dist/baseline/migrate.d.ts.map +1 -0
  112. package/dist/baseline/migrate.js +238 -0
  113. package/dist/baseline/migrate.js.map +1 -0
  114. package/dist/baseline/producers/security.d.ts +9 -9
  115. package/dist/baseline/producers/security.d.ts.map +1 -1
  116. package/dist/baseline/producers/security.js +16 -4
  117. package/dist/baseline/producers/security.js.map +1 -1
  118. package/dist/baseline/types.d.ts +145 -95
  119. package/dist/baseline/types.d.ts.map +1 -1
  120. package/dist/baseline/types.js +30 -26
  121. package/dist/baseline/types.js.map +1 -1
  122. package/dist/explore/context-hook.d.ts +49 -29
  123. package/dist/explore/context-hook.d.ts.map +1 -1
  124. package/dist/explore/context-hook.js +304 -29
  125. package/dist/explore/context-hook.js.map +1 -1
  126. package/dist/explore/finding-context.d.ts +17 -0
  127. package/dist/explore/finding-context.d.ts.map +1 -1
  128. package/dist/explore/finding-context.js +34 -0
  129. package/dist/explore/finding-context.js.map +1 -1
  130. package/dist/explore/queries.d.ts +32 -15
  131. package/dist/explore/queries.d.ts.map +1 -1
  132. package/dist/explore/queries.js +36 -6
  133. package/dist/explore/queries.js.map +1 -1
  134. package/dist/generator.d.ts.map +1 -1
  135. package/dist/generator.js +13 -7
  136. package/dist/generator.js.map +1 -1
  137. package/dist/ingest/normalize.d.ts +1 -1
  138. package/dist/ingest/normalize.d.ts.map +1 -1
  139. package/dist/ingest/normalize.js +5 -1
  140. package/dist/ingest/normalize.js.map +1 -1
  141. package/dist/ingest/sarif.d.ts.map +1 -1
  142. package/dist/ingest/sarif.js +16 -7
  143. package/dist/ingest/sarif.js.map +1 -1
  144. package/dist/ingest/snyk-policy.d.ts +22 -1
  145. package/dist/ingest/snyk-policy.d.ts.map +1 -1
  146. package/dist/ingest/snyk-policy.js +75 -18
  147. package/dist/ingest/snyk-policy.js.map +1 -1
  148. package/dist/ingest/types.d.ts +23 -12
  149. package/dist/ingest/types.d.ts.map +1 -1
  150. package/dist/languages/capabilities/types.d.ts +64 -53
  151. package/dist/languages/capabilities/types.d.ts.map +1 -1
  152. package/dist/languages/capabilities/types.js +4 -4
  153. package/dist/languages/index.d.ts +28 -5
  154. package/dist/languages/index.d.ts.map +1 -1
  155. package/dist/languages/index.js +38 -7
  156. package/dist/languages/index.js.map +1 -1
  157. package/dist/languages/typescript.d.ts.map +1 -1
  158. package/dist/languages/typescript.js +19 -0
  159. package/dist/languages/typescript.js.map +1 -1
  160. package/dist/scoring/dimensions/security.d.ts +17 -0
  161. package/dist/scoring/dimensions/security.d.ts.map +1 -1
  162. package/dist/scoring/dimensions/security.js +12 -0
  163. package/dist/scoring/dimensions/security.js.map +1 -1
  164. package/dist/update.d.ts.map +1 -1
  165. package/dist/update.js +49 -0
  166. package/dist/update.js.map +1 -1
  167. package/dist/upgrade.d.ts.map +1 -1
  168. package/dist/upgrade.js +2 -1
  169. package/dist/upgrade.js.map +1 -1
  170. package/package.json +6 -3
  171. package/templates/.claude/skills/dxkit-action/SKILL.md +11 -2
  172. package/templates/.claude/skills/dxkit-allowlist/SKILL.md +9 -0
  173. package/templates/.claude/skills/dxkit-onboard/SKILL.md +2 -2
  174. package/templates/.claude/skills/dxkit-update/SKILL.md +45 -4
  175. package/dist/baseline/salt.d.ts +0 -45
  176. package/dist/baseline/salt.d.ts.map +0 -1
  177. package/dist/baseline/salt.js.map +0 -1
@@ -9,46 +9,46 @@
9
9
  *
10
10
  * The disease this closes (D086 / D087 / D091):
11
11
  *
12
- * - **D086** Health Security section and standalone vuln-scan Code
13
- * Findings table both reported "code findings by severity" but
14
- * came up with different numbers (`0C 11H 18M 0L` vs
15
- * `0C 17H 14M 0L`) on the same repo. Two consumers, two
16
- * aggregation paths, slightly-different inclusion rules.
12
+ * - **D086** Health Security section and standalone vuln-scan Code
13
+ * Findings table both reported "code findings by severity" but
14
+ * came up with different numbers (`0C 11H 18M 0L` vs
15
+ * `0C 17H 14M 0L`) on the same repo. Two consumers, two
16
+ * aggregation paths, slightly-different inclusion rules.
17
17
  *
18
- * - **D087** Vuln-scan exec summary said "Subtotal: 70" (sum of
19
- * dep-vuln severity buckets) and the same page later said
20
- * "81 advisories" (findings.length). 70 vs 81 on one page.
18
+ * - **D087** Vuln-scan exec summary said "Subtotal: 70" (sum of
19
+ * dep-vuln severity buckets) and the same page later said
20
+ * "81 advisories" (findings.length). 70 vs 81 on one page.
21
21
  *
22
- * - **D091** A single TLS-bypass root finding surfaced twice in the
23
- * Code Findings table (registry-grep at `:74` HIGH, semgrep at
24
- * `:72` MEDIUM) because code findings carried no fingerprint and
25
- * no cross-tool dedup ran.
22
+ * - **D091** A single TLS-bypass root finding surfaced twice in the
23
+ * Code Findings table (registry-grep at `:74` HIGH, semgrep at
24
+ * `:72` MEDIUM) because code findings carried no fingerprint and
25
+ * no cross-tool dedup ran.
26
26
  *
27
27
  * Architectural posture:
28
28
  *
29
- * - The aggregator sits BETWEEN gather and reports. Gather still
30
- * produces raw envelopes (`gatherSecrets`, `gatherFileFindings`,
31
- * `gatherCodePatterns`, `gatherTlsBypassFindings`, `gatherDepVulns`);
32
- * the aggregator merges + dedups + buckets them into the canonical
33
- * shape; consumers read by field name.
29
+ * - The aggregator sits BETWEEN gather and reports. Gather still
30
+ * produces raw envelopes (`gatherSecrets`, `gatherFileFindings`,
31
+ * `gatherCodePatterns`, `gatherTlsBypassFindings`, `gatherDepVulns`);
32
+ * the aggregator merges + dedups + buckets them into the canonical
33
+ * shape; consumers read by field name.
34
34
  *
35
- * - Three separately-named severity buckets (`codeBySeverity`,
36
- * `depBySeverity`, `secretsBySeverity`) — the shape forbids any
37
- * consumer from accidentally summing cross-axis again.
35
+ * - Three separately-named severity buckets (`codeBySeverity`,
36
+ * `depBySeverity`, `secretsBySeverity`) — the shape forbids any
37
+ * consumer from accidentally summing cross-axis again.
38
38
  *
39
- * - Two named dep counts (`dependencyAdvisoryUniqueCount` for the
40
- * canonical user-facing total; `dependencyFindingsRawCount` for
41
- * diagnostic audit). Renderers cannot pick "the wrong number"
42
- * without naming which they want.
39
+ * - Two named dep counts (`dependencyAdvisoryUniqueCount` for the
40
+ * canonical user-facing total; `dependencyFindingsRawCount` for
41
+ * diagnostic audit). Renderers cannot pick "the wrong number"
42
+ * without naming which they want.
43
43
  *
44
- * - Code findings get a canonical-rule + line-window fingerprint;
45
- * cross-tool collisions collapse to ONE CodeFinding with
46
- * `keptSeverity = max(severities)` and `producedBy` listing all
47
- * contributing tools. The `dedupCollisions` audit trail records
48
- * every collapse for `--detailed` visibility.
44
+ * - Code findings get a canonical-rule + line-window fingerprint;
45
+ * cross-tool collisions collapse to ONE CodeFinding with
46
+ * `keptSeverity = max(severities)` and `producedBy` listing all
47
+ * contributing tools. The `dedupCollisions` audit trail records
48
+ * every collapse for `--detailed` visibility.
49
49
  *
50
- * - `provenance` distinguishes "tool ran, 0 findings" from "tool
51
- * didn't run" — drives D080-style "(not run: typescript)" labels.
50
+ * - `provenance` distinguishes "tool ran, 0 findings" from "tool
51
+ * didn't run" — drives D080-style "(not run: typescript)" labels.
52
52
  *
53
53
  * G_v4_8 architectural gate (`scripts/check-architecture.sh`) blocks
54
54
  * `countBySeverity` / severity-Record accumulator declarations
@@ -56,6 +56,7 @@
56
56
  */
57
57
  import type { DepVulnFinding } from '../../languages/capabilities/types';
58
58
  import type { Severity, SecurityFinding } from './types';
59
+ import type { AllowlistFile } from '../../allowlist/file';
59
60
  export type { Severity, FindingCategory, SecurityFinding } from './types';
60
61
  /**
61
62
  * Per-severity counts. Local copy (avoids cross-module import friction
@@ -73,26 +74,26 @@ export interface SeverityCounts {
73
74
  * `SecurityFinding` with the identity + provenance fields the
74
75
  * aggregator stamps:
75
76
  *
76
- * - `fingerprint` — stable 16-char hash of
77
- * `(canonicalRule | file | lineWindow)`. Same key across runs;
78
- * enables diff tooling and dedup-by-identity downstream.
79
- * - `canonicalRule` — normalized rule id from the canonical-rule
80
- * registry. Different raw tool/rule pairs that describe the same
81
- * root finding collapse to the same `canonicalRule`. Unmapped
82
- * pairs pass through as `raw:${tool}:${rule}` — conservative
83
- * default; new collapse rules require explicit registry entries.
84
- * - `producedBy` — every raw `tool` that contributed to this
85
- * finding. Length > 1 means cross-tool dedup fired.
77
+ * - `fingerprint` — stable 16-char hash of
78
+ * `(canonicalRule | file | lineWindow)`. Same key across runs;
79
+ * enables diff tooling and dedup-by-identity downstream.
80
+ * - `canonicalRule` — normalized rule id from the canonical-rule
81
+ * registry. Different raw tool/rule pairs that describe the same
82
+ * root finding collapse to the same `canonicalRule`. Unmapped
83
+ * pairs pass through as `raw:${tool}:${rule}` — conservative
84
+ * default; new collapse rules require explicit registry entries.
85
+ * - `producedBy` — every raw `tool` that contributed to this
86
+ * finding. Length > 1 means cross-tool dedup fired.
86
87
  */
87
88
  export interface CodeFinding extends SecurityFinding {
88
89
  fingerprint: string;
89
90
  canonicalRule: string;
90
91
  producedBy: string[];
91
92
  /** Fingerprints of the cross-tool / neighbor-bucket / CWE-bridge
92
- * findings that collapsed into this one, when their own fingerprint
93
- * differed from `fingerprint`. Present only when such a merge fired.
94
- * Lets a suppression keyed on a contributing fingerprint still match
95
- * the merged finding (robust matching against dedup nondeterminism). */
93
+ * findings that collapsed into this one, when their own fingerprint
94
+ * differed from `fingerprint`. Present only when such a merge fired.
95
+ * Lets a suppression keyed on a contributing fingerprint still match
96
+ * the merged finding (robust matching against dedup nondeterminism). */
96
97
  absorbedFingerprints?: string[];
97
98
  }
98
99
  /**
@@ -129,10 +130,10 @@ export interface AggregateProvenance {
129
130
  ran: boolean;
130
131
  };
131
132
  /** Ingested external-engine provenance. `tools` is the set of
132
- * engines whose findings were ingested this run (e.g. `['codeql']`,
133
- * `['snyk-code']`); `ran` is true when ingestion contributed. Always
134
- * populated by `buildSecurityAggregate`; optional in the type only so
135
- * pre-existing test mocks needn't be rewritten. */
133
+ * engines whose findings were ingested this run (e.g. `['codeql']`,
134
+ * `['snyk-code']`); `ran` is true when ingestion contributed. Always
135
+ * populated by `buildSecurityAggregate`; optional in the type only so
136
+ * pre-existing test mocks needn't be rewritten. */
136
137
  external?: {
137
138
  tools: string[];
138
139
  ran: boolean;
@@ -157,19 +158,29 @@ export interface AggregateProvenance {
157
158
  */
158
159
  export interface SecurityAggregate {
159
160
  /** Code-pattern findings by severity (semgrep + tls-bypass-registry
160
- * + any future per-pack code-pattern producers), post-dedup. */
161
+ * + any future per-pack code-pattern producers), post-dedup. */
161
162
  codeBySeverity: SeverityCounts;
162
163
  /** Dependency advisories by severity, derived from the
163
- * fingerprint-unique advisory set (NOT the per-pack envelope
164
- * count sum). Sums to `dependencyAdvisoryUniqueCount`. */
164
+ * fingerprint-unique advisory set (NOT the per-pack envelope
165
+ * count sum). Sums to `dependencyAdvisoryUniqueCount`. */
165
166
  depBySeverity: SeverityCounts;
166
167
  /** Secret + secret-adjacent findings (gitleaks + private-key files +
167
- * .env-in-git) by severity. Each axis stays separate so consumers
168
- * pick which they own. */
168
+ * .env-in-git) by severity. Each axis stays separate so consumers
169
+ * pick which they own. */
169
170
  secretsBySeverity: SeverityCounts;
171
+ /** Code-pattern findings by severity, EXCLUDING findings an active
172
+ * allowlist entry lifts from the score (`false-positive` /
173
+ * `test-fixture`). The dimension scorer reads these; reports read the
174
+ * raw `codeBySeverity`. Equal to `codeBySeverity` when no allowlist
175
+ * was supplied or none of the findings are score-lifted. */
176
+ scoreableCodeBySeverity: SeverityCounts;
177
+ /** Secret + secret-adjacent findings by severity, EXCLUDING
178
+ * score-lifting allowlisted findings. Scorer reads this; reports read
179
+ * raw `secretsBySeverity`. */
180
+ scoreableSecretsBySeverity: SeverityCounts;
170
181
  /** Findings partitioned by category, post-dedup. Renderers iterate
171
- * these — never iterate raw envelope arrays. `dependency` is the
172
- * fingerprint-unique advisory set. */
182
+ * these — never iterate raw envelope arrays. `dependency` is the
183
+ * fingerprint-unique advisory set. */
173
184
  findingsByCategory: {
174
185
  secret: ReadonlyArray<CodeFinding>;
175
186
  code: ReadonlyArray<CodeFinding>;
@@ -192,7 +203,7 @@ export interface SecurityAggregate {
192
203
  */
193
204
  dependencyFindingsRawCount: number;
194
205
  /** Audit trail of every cross-tool / cross-line-window collapse.
195
- * Empty in the no-collision case. */
206
+ * Empty in the no-collision case. */
196
207
  dedupCollisions: ReadonlyArray<DedupCollision>;
197
208
  /** Per-source provenance — drives "(not run: typescript)" labels. */
198
209
  provenance: AggregateProvenance;
@@ -214,21 +225,21 @@ export interface SecurityAggregateInput {
214
225
  toolUsed: string | null;
215
226
  };
216
227
  /** Findings ingested from external interprocedural-SAST engines
217
- * (Snyk Code, CodeQL, …) via `src/ingest`. Already mapped to
218
- * `SecurityFinding` with the engine as the `tool`. They join the
219
- * same code-side dedup pipeline as native findings, so a Snyk and a
220
- * semgrep finding on the same line collapse to one `CodeFinding`.
221
- * Optional: absent (or empty) yields output identical to a run with
222
- * no ingestion configured. */
228
+ * (Snyk Code, CodeQL, …) via `src/ingest`. Already mapped to
229
+ * `SecurityFinding` with the engine as the `tool`. They join the
230
+ * same code-side dedup pipeline as native findings, so a Snyk and a
231
+ * semgrep finding on the same line collapse to one `CodeFinding`.
232
+ * Optional: absent (or empty) yields output identical to a run with
233
+ * no ingestion configured. */
223
234
  external?: {
224
235
  findings: SecurityFinding[];
225
236
  toolsUsed: string[];
226
237
  };
227
238
  tlsBypass: SecurityFinding[];
228
239
  /** Pattern count from `allTlsBypassPatterns()` — drives the
229
- * `provenance.tlsBypass.ran` flag (ran=false when no patterns were
230
- * registered, NOT when 0 findings matched against a non-empty
231
- * pattern set). */
240
+ * `provenance.tlsBypass.ran` flag (ran=false when no patterns were
241
+ * registered, NOT when 0 findings matched against a non-empty
242
+ * pattern set). */
232
243
  tlsBypassPatternCount: number;
233
244
  depVulns: {
234
245
  findings: DepVulnFinding[];
@@ -236,27 +247,34 @@ export interface SecurityAggregateInput {
236
247
  available: boolean;
237
248
  unavailableReason: string;
238
249
  };
250
+ /** The repo's allowlist, loaded by the caller (the aggregator stays
251
+ * pure / does no I/O). When present, each code/secret/config finding
252
+ * is annotated with its active-allowlist status, and the `scoreable*`
253
+ * severity buckets exclude findings allowlisted under a category that
254
+ * lifts the score (`false-positive` / `test-fixture`). Absent/null →
255
+ * `scoreable*` buckets equal the raw buckets. */
256
+ allowlist?: AllowlistFile | null;
239
257
  }
240
258
  /**
241
259
  * Build the canonical aggregate from per-gatherer envelopes. Pure
242
260
  * function — same input always produces the same output.
243
261
  *
244
262
  * Dedup pipeline (code-side):
245
- * 1. Concat raw findings from secrets/fileFindings/codePatterns/tlsBypass.
246
- * 2. Group by `(canonicalRule, file, lineWindow)` key.
247
- * 3. For each group:
248
- * - Emit ONE `CodeFinding` with `keptSeverity = max(severities)`,
249
- * `producedBy` = unique sources.
250
- * - If the group had >1 raw finding, record a `DedupCollision`
251
- * audit entry.
263
+ * 1. Concat raw findings from secrets/fileFindings/codePatterns/tlsBypass.
264
+ * 2. Group by `(canonicalRule, file, lineWindow)` key.
265
+ * 3. For each group:
266
+ * - Emit ONE `CodeFinding` with `keptSeverity = max(severities)`,
267
+ * `producedBy` = unique sources.
268
+ * - If the group had >1 raw finding, record a `DedupCollision`
269
+ * audit entry.
252
270
  *
253
271
  * Dedup pipeline (dep-side):
254
- * - Group `depVulns.findings` by `fingerprint`.
255
- * - For each group: pick the highest-severity entry as the
256
- * representative; severity counts are derived from the unique
257
- * set so they match `dependencyAdvisoryUniqueCount`.
258
- * - Findings without a fingerprint pass through unchanged (defensive;
259
- * `stampFingerprints` in `gatherDepVulns` runs before this).
272
+ * - Group `depVulns.findings` by `fingerprint`.
273
+ * - For each group: pick the highest-severity entry as the
274
+ * representative; severity counts are derived from the unique
275
+ * set so they match `dependencyAdvisoryUniqueCount`.
276
+ * - Findings without a fingerprint pass through unchanged (defensive;
277
+ * `stampFingerprints` in `gatherDepVulns` runs before this).
260
278
  */
261
279
  export declare function buildSecurityAggregate(input: SecurityAggregateInput): SecurityAggregate;
262
280
  //# sourceMappingURL=aggregator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"aggregator.d.ts","sourceRoot":"","sources":["../../../src/analyzers/security/aggregator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"aggregator.d.ts","sourceRoot":"","sources":["../../../src/analyzers/security/aggregator.ts"],"names":[],"mappings":"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"}
@@ -10,46 +10,46 @@
10
10
  *
11
11
  * The disease this closes (D086 / D087 / D091):
12
12
  *
13
- * - **D086** Health Security section and standalone vuln-scan Code
14
- * Findings table both reported "code findings by severity" but
15
- * came up with different numbers (`0C 11H 18M 0L` vs
16
- * `0C 17H 14M 0L`) on the same repo. Two consumers, two
17
- * aggregation paths, slightly-different inclusion rules.
13
+ * - **D086** Health Security section and standalone vuln-scan Code
14
+ * Findings table both reported "code findings by severity" but
15
+ * came up with different numbers (`0C 11H 18M 0L` vs
16
+ * `0C 17H 14M 0L`) on the same repo. Two consumers, two
17
+ * aggregation paths, slightly-different inclusion rules.
18
18
  *
19
- * - **D087** Vuln-scan exec summary said "Subtotal: 70" (sum of
20
- * dep-vuln severity buckets) and the same page later said
21
- * "81 advisories" (findings.length). 70 vs 81 on one page.
19
+ * - **D087** Vuln-scan exec summary said "Subtotal: 70" (sum of
20
+ * dep-vuln severity buckets) and the same page later said
21
+ * "81 advisories" (findings.length). 70 vs 81 on one page.
22
22
  *
23
- * - **D091** A single TLS-bypass root finding surfaced twice in the
24
- * Code Findings table (registry-grep at `:74` HIGH, semgrep at
25
- * `:72` MEDIUM) because code findings carried no fingerprint and
26
- * no cross-tool dedup ran.
23
+ * - **D091** A single TLS-bypass root finding surfaced twice in the
24
+ * Code Findings table (registry-grep at `:74` HIGH, semgrep at
25
+ * `:72` MEDIUM) because code findings carried no fingerprint and
26
+ * no cross-tool dedup ran.
27
27
  *
28
28
  * Architectural posture:
29
29
  *
30
- * - The aggregator sits BETWEEN gather and reports. Gather still
31
- * produces raw envelopes (`gatherSecrets`, `gatherFileFindings`,
32
- * `gatherCodePatterns`, `gatherTlsBypassFindings`, `gatherDepVulns`);
33
- * the aggregator merges + dedups + buckets them into the canonical
34
- * shape; consumers read by field name.
30
+ * - The aggregator sits BETWEEN gather and reports. Gather still
31
+ * produces raw envelopes (`gatherSecrets`, `gatherFileFindings`,
32
+ * `gatherCodePatterns`, `gatherTlsBypassFindings`, `gatherDepVulns`);
33
+ * the aggregator merges + dedups + buckets them into the canonical
34
+ * shape; consumers read by field name.
35
35
  *
36
- * - Three separately-named severity buckets (`codeBySeverity`,
37
- * `depBySeverity`, `secretsBySeverity`) — the shape forbids any
38
- * consumer from accidentally summing cross-axis again.
36
+ * - Three separately-named severity buckets (`codeBySeverity`,
37
+ * `depBySeverity`, `secretsBySeverity`) — the shape forbids any
38
+ * consumer from accidentally summing cross-axis again.
39
39
  *
40
- * - Two named dep counts (`dependencyAdvisoryUniqueCount` for the
41
- * canonical user-facing total; `dependencyFindingsRawCount` for
42
- * diagnostic audit). Renderers cannot pick "the wrong number"
43
- * without naming which they want.
40
+ * - Two named dep counts (`dependencyAdvisoryUniqueCount` for the
41
+ * canonical user-facing total; `dependencyFindingsRawCount` for
42
+ * diagnostic audit). Renderers cannot pick "the wrong number"
43
+ * without naming which they want.
44
44
  *
45
- * - Code findings get a canonical-rule + line-window fingerprint;
46
- * cross-tool collisions collapse to ONE CodeFinding with
47
- * `keptSeverity = max(severities)` and `producedBy` listing all
48
- * contributing tools. The `dedupCollisions` audit trail records
49
- * every collapse for `--detailed` visibility.
45
+ * - Code findings get a canonical-rule + line-window fingerprint;
46
+ * cross-tool collisions collapse to ONE CodeFinding with
47
+ * `keptSeverity = max(severities)` and `producedBy` listing all
48
+ * contributing tools. The `dedupCollisions` audit trail records
49
+ * every collapse for `--detailed` visibility.
50
50
  *
51
- * - `provenance` distinguishes "tool ran, 0 findings" from "tool
52
- * didn't run" — drives D080-style "(not run: typescript)" labels.
51
+ * - `provenance` distinguishes "tool ran, 0 findings" from "tool
52
+ * didn't run" — drives D080-style "(not run: typescript)" labels.
53
53
  *
54
54
  * G_v4_8 architectural gate (`scripts/check-architecture.sh`) blocks
55
55
  * `countBySeverity` / severity-Record accumulator declarations
@@ -58,6 +58,7 @@
58
58
  Object.defineProperty(exports, "__esModule", { value: true });
59
59
  exports.buildSecurityAggregate = buildSecurityAggregate;
60
60
  const fingerprint_1 = require("../tools/fingerprint");
61
+ const annotate_1 = require("../../allowlist/annotate");
61
62
  // ─── Canonical-rule registry ──────────────────────────────────────────────
62
63
  /**
63
64
  * Maps raw `(tool, rule)` pairs to a canonical rule id. Two raw
@@ -94,21 +95,21 @@ function bumpCounts(counts, severity) {
94
95
  * function — same input always produces the same output.
95
96
  *
96
97
  * Dedup pipeline (code-side):
97
- * 1. Concat raw findings from secrets/fileFindings/codePatterns/tlsBypass.
98
- * 2. Group by `(canonicalRule, file, lineWindow)` key.
99
- * 3. For each group:
100
- * - Emit ONE `CodeFinding` with `keptSeverity = max(severities)`,
101
- * `producedBy` = unique sources.
102
- * - If the group had >1 raw finding, record a `DedupCollision`
103
- * audit entry.
98
+ * 1. Concat raw findings from secrets/fileFindings/codePatterns/tlsBypass.
99
+ * 2. Group by `(canonicalRule, file, lineWindow)` key.
100
+ * 3. For each group:
101
+ * - Emit ONE `CodeFinding` with `keptSeverity = max(severities)`,
102
+ * `producedBy` = unique sources.
103
+ * - If the group had >1 raw finding, record a `DedupCollision`
104
+ * audit entry.
104
105
  *
105
106
  * Dedup pipeline (dep-side):
106
- * - Group `depVulns.findings` by `fingerprint`.
107
- * - For each group: pick the highest-severity entry as the
108
- * representative; severity counts are derived from the unique
109
- * set so they match `dependencyAdvisoryUniqueCount`.
110
- * - Findings without a fingerprint pass through unchanged (defensive;
111
- * `stampFingerprints` in `gatherDepVulns` runs before this).
107
+ * - Group `depVulns.findings` by `fingerprint`.
108
+ * - For each group: pick the highest-severity entry as the
109
+ * representative; severity counts are derived from the unique
110
+ * set so they match `dependencyAdvisoryUniqueCount`.
111
+ * - Findings without a fingerprint pass through unchanged (defensive;
112
+ * `stampFingerprints` in `gatherDepVulns` runs before this).
112
113
  */
113
114
  function buildSecurityAggregate(input) {
114
115
  // ─── Code-side dedup ────────────────────────────────────────────────
@@ -179,14 +180,8 @@ function buildSecurityAggregate(input) {
179
180
  rule: f.rule,
180
181
  line: f.line,
181
182
  severity: f.severity,
183
+ spanHash: f.spanHash,
182
184
  });
183
- // Record the merged finding's own fingerprint when it differs
184
- // from the representative — that's the identity a suppression
185
- // might have been keyed on in a run where the merge landed the
186
- // other way around.
187
- if (naturalFingerprint !== existing.fingerprint) {
188
- existing.absorbedFingerprints.add(naturalFingerprint);
189
- }
190
185
  // Prefer the lower line number as the canonical line — semgrep
191
186
  // typically reports the declaration (earlier line) while
192
187
  // registry-grep reports the assignment; the declaration is the
@@ -197,6 +192,9 @@ function buildSecurityAggregate(input) {
197
192
  existing.rule = f.rule;
198
193
  existing.tool = f.tool;
199
194
  existing.cwe = f.cwe || existing.cwe;
195
+ // Keep the anchor material aligned with the chosen representative.
196
+ existing.spanHash = f.spanHash;
197
+ existing.scope = f.scope;
200
198
  }
201
199
  }
202
200
  else {
@@ -211,6 +209,8 @@ function buildSecurityAggregate(input) {
211
209
  rule: f.rule,
212
210
  title: f.title,
213
211
  tool: f.tool,
212
+ spanHash: f.spanHash,
213
+ scope: f.scope,
214
214
  producedBy: new Set([f.tool]),
215
215
  raws: [
216
216
  {
@@ -218,9 +218,9 @@ function buildSecurityAggregate(input) {
218
218
  rule: f.rule,
219
219
  line: f.line,
220
220
  severity: f.severity,
221
+ spanHash: f.spanHash,
221
222
  },
222
223
  ],
223
- absorbedFingerprints: new Set(),
224
224
  });
225
225
  }
226
226
  // Index this finding's CWE + location → its group, so a later
@@ -228,6 +228,66 @@ function buildSecurityAggregate(input) {
228
228
  if (f.cwe)
229
229
  byCweLoc.set(cweLocKey(f.cwe, f.file, f.line), fingerprint);
230
230
  }
231
+ // ─── Ordinal assignment ────────────────────────────────────────
232
+ // Findings sharing one anchor bucket get a stable in-document-order
233
+ // ordinal so identical constructs stay distinct:
234
+ // • code groups sharing (file, scope, spanHash) — three
235
+ // `eval(userInput)` in one function stay three findings;
236
+ // • secret groups sharing (file) — two leaked credentials in one file
237
+ // stay two findings. Keyed on file ALONE (not the per-tool rule):
238
+ // secret identity discriminates on the tool-independent
239
+ // SECRET_CANONICAL_RULE, so the ordinal must be unique per file
240
+ // across every secret regardless of which scanner/rule found it.
241
+ // Config (file-stable line 0) and anchorless findings need no ordinal.
242
+ // The bucket key is prefixed by category so the code and secret
243
+ // namespaces can never collide. Deterministic regardless of Map
244
+ // iteration order: sorted by line, then by the line-based group key.
245
+ const ordinalBuckets = new Map();
246
+ for (const g of groups.values()) {
247
+ let key;
248
+ if (g.category === 'code' && g.spanHash !== undefined) {
249
+ key = `code\0${g.file}\0${g.scope ?? ''}\0${g.spanHash}`;
250
+ }
251
+ else if (g.category === 'secret') {
252
+ key = `secret\0${g.file}`;
253
+ }
254
+ if (key !== undefined) {
255
+ const list = ordinalBuckets.get(key) ?? [];
256
+ list.push(g);
257
+ ordinalBuckets.set(key, list);
258
+ }
259
+ }
260
+ for (const list of ordinalBuckets.values()) {
261
+ list.sort((a, b) => a.line - b.line ||
262
+ (a.fingerprint < b.fingerprint ? -1 : a.fingerprint > b.fingerprint ? 1 : 0));
263
+ list.forEach((g, i) => {
264
+ g.ordinal = i;
265
+ });
266
+ }
267
+ // The durable content anchor for a group (scheme v2), or undefined when
268
+ // none is resolvable → line-window fallback. Secrets: (ordinal) alone —
269
+ // value/salt-free, so identity is tool- and environment-independent.
270
+ // Code: (scope, spanHash, ordinal) when a span was captured. Config +
271
+ // anchorless: undefined (config's line-0 identity is already
272
+ // (canonicalRule, file)-stable, so it stays on the line path unchanged).
273
+ const anchorFor = (g) => {
274
+ if (g.category === 'secret')
275
+ return (0, fingerprint_1.secretContentAnchor)(g.ordinal ?? 0);
276
+ if (g.category === 'code' && g.spanHash !== undefined) {
277
+ return (0, fingerprint_1.codeContentAnchorFromHash)(g.scope ?? '', g.spanHash, g.ordinal ?? 0);
278
+ }
279
+ return undefined;
280
+ };
281
+ const fingerprintFor = (canonicalRule, file, line, anchor) => anchor !== undefined
282
+ ? (0, fingerprint_1.computeContentFingerprint)(canonicalRule, file, anchor)
283
+ : (0, fingerprint_1.computeCodeFingerprint)(canonicalRule, file, line);
284
+ // The rule discriminator used for IDENTITY (not display/grouping).
285
+ // Secrets fold onto the tool-independent SECRET_CANONICAL_RULE so the same
286
+ // leak fingerprints identically no matter which scanner/rule found it;
287
+ // code/config keep their per-tool canonical rule. Mirrors the secret
288
+ // branch in `identityFor` so the aggregator's stamped fingerprint and the
289
+ // baseline producer's recomputed id always agree.
290
+ const identityRuleFor = (g) => g.category === 'secret' ? fingerprint_1.SECRET_CANONICAL_RULE : g.canonicalRule;
231
291
  const codeFindingsByCategory = {
232
292
  secret: [],
233
293
  code: [],
@@ -237,6 +297,28 @@ function buildSecurityAggregate(input) {
237
297
  const secretsBySeverity = emptyCounts();
238
298
  const dedupCollisions = [];
239
299
  for (const g of groups.values()) {
300
+ const anchor = anchorFor(g);
301
+ const identityRule = identityRuleFor(g);
302
+ const fingerprint = fingerprintFor(identityRule, g.file, g.line, anchor);
303
+ // Absorbed fingerprints: the content fingerprint each merged raw WOULD
304
+ // have had as representative (its own span/HMAC, the group's scope +
305
+ // ordinal). Lets a suppression keyed on a contributing finding's
306
+ // identity still match after the representative flips between runs.
307
+ // Secrets fold onto SECRET_CANONICAL_RULE and a per-file ordinal, so
308
+ // every secret raw in a group resolves to the SAME fingerprint — nothing
309
+ // to absorb (the cross-tool divergence this guarded against is gone).
310
+ const absorbed = new Set();
311
+ for (const raw of g.raws) {
312
+ const rawCanonical = g.category === 'secret' ? fingerprint_1.SECRET_CANONICAL_RULE : (0, fingerprint_1.canonicalRuleFor)(raw.tool, raw.rule);
313
+ let rawAnchor;
314
+ if (g.category === 'secret')
315
+ rawAnchor = (0, fingerprint_1.secretContentAnchor)(g.ordinal ?? 0);
316
+ else if (g.category === 'code' && raw.spanHash !== undefined)
317
+ rawAnchor = (0, fingerprint_1.codeContentAnchorFromHash)(g.scope ?? '', raw.spanHash, g.ordinal ?? 0);
318
+ const rawFp = fingerprintFor(rawCanonical, g.file, raw.line, rawAnchor);
319
+ if (rawFp !== fingerprint)
320
+ absorbed.add(rawFp);
321
+ }
240
322
  const finding = {
241
323
  severity: g.severity,
242
324
  category: g.category,
@@ -246,12 +328,15 @@ function buildSecurityAggregate(input) {
246
328
  file: g.file,
247
329
  line: g.line,
248
330
  tool: g.tool,
249
- fingerprint: g.fingerprint,
331
+ fingerprint,
250
332
  canonicalRule: g.canonicalRule,
251
333
  producedBy: [...g.producedBy].sort(),
252
- ...(g.absorbedFingerprints.size > 0
253
- ? { absorbedFingerprints: [...g.absorbedFingerprints].sort() }
254
- : {}),
334
+ // Content-anchored identity: stamp the FINAL content anchor (the producer reads it back to
335
+ // recompute the same identity). Omitted when absent (→ line fallback).
336
+ ...(anchor !== undefined ? { contentAnchor: anchor } : {}),
337
+ ...(g.spanHash !== undefined ? { spanHash: g.spanHash } : {}),
338
+ ...(g.scope !== undefined ? { scope: g.scope } : {}),
339
+ ...(absorbed.size > 0 ? { absorbedFingerprints: [...absorbed].sort() } : {}),
255
340
  };
256
341
  if (g.category === 'secret') {
257
342
  codeFindingsByCategory.secret.push(finding);
@@ -279,6 +364,31 @@ function buildSecurityAggregate(input) {
279
364
  });
280
365
  }
281
366
  }
367
+ // ─── Allowlist annotation + scoreable buckets ───────────────────────
368
+ // Mark every code/secret/config finding an active allowlist entry
369
+ // covers (renderers show "(N allowlisted)"), then derive the
370
+ // score-only buckets that EXCLUDE findings allowlisted under a
371
+ // category that lifts the score. This is what lets a repo that has
372
+ // reviewed-and-accepted its findings (false-positive / test-fixture)
373
+ // score honestly instead of staying capped on noise — while still
374
+ // counting accepted-risk / deferred, which accept a real exposure.
375
+ const allCodeSideFindings = [
376
+ ...codeFindingsByCategory.secret,
377
+ ...codeFindingsByCategory.code,
378
+ ...codeFindingsByCategory.config,
379
+ ];
380
+ (0, annotate_1.annotateFindingsWithAllowlist)(allCodeSideFindings, input.allowlist ?? null);
381
+ const scoreableCodeBySeverity = emptyCounts();
382
+ const scoreableSecretsBySeverity = emptyCounts();
383
+ const scoreLifted = (f) => !!f.allowlisted && (0, annotate_1.allowlistLiftsScore)(f.allowlistCategory);
384
+ for (const f of codeFindingsByCategory.code) {
385
+ if (!scoreLifted(f))
386
+ bumpCounts(scoreableCodeBySeverity, f.severity);
387
+ }
388
+ for (const f of [...codeFindingsByCategory.secret, ...codeFindingsByCategory.config]) {
389
+ if (!scoreLifted(f))
390
+ bumpCounts(scoreableSecretsBySeverity, f.severity);
391
+ }
282
392
  // ─── Dep-side dedup ─────────────────────────────────────────────────
283
393
  // Group by fingerprint. Findings without a fingerprint (defensive
284
394
  // path — shouldn't happen post-`stampFingerprints`) get a synthetic
@@ -338,6 +448,8 @@ function buildSecurityAggregate(input) {
338
448
  codeBySeverity,
339
449
  depBySeverity,
340
450
  secretsBySeverity,
451
+ scoreableCodeBySeverity,
452
+ scoreableSecretsBySeverity,
341
453
  findingsByCategory: {
342
454
  secret: codeFindingsByCategory.secret,
343
455
  code: codeFindingsByCategory.code,