@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.
- package/CHANGELOG.md +236 -0
- package/dist/allowlist/annotate.d.ts +71 -0
- package/dist/allowlist/annotate.d.ts.map +1 -0
- package/dist/allowlist/annotate.js +105 -0
- package/dist/allowlist/annotate.js.map +1 -0
- package/dist/allowlist/cli.d.ts +29 -23
- package/dist/allowlist/cli.d.ts.map +1 -1
- package/dist/allowlist/cli.js +141 -70
- package/dist/allowlist/cli.js.map +1 -1
- package/dist/allowlist/file.d.ts +7 -1
- package/dist/allowlist/file.d.ts.map +1 -1
- package/dist/allowlist/file.js +7 -1
- package/dist/allowlist/file.js.map +1 -1
- package/dist/analysis-result.d.ts +10 -0
- package/dist/analysis-result.d.ts.map +1 -1
- package/dist/analyzers/cache.d.ts +1 -0
- package/dist/analyzers/cache.d.ts.map +1 -1
- package/dist/analyzers/cache.js +69 -0
- package/dist/analyzers/cache.js.map +1 -1
- package/dist/analyzers/dashboard/index.d.ts.map +1 -1
- package/dist/analyzers/dashboard/index.js +6 -1
- package/dist/analyzers/dashboard/index.js.map +1 -1
- package/dist/analyzers/health.d.ts.map +1 -1
- package/dist/analyzers/health.js +17 -2
- package/dist/analyzers/health.js.map +1 -1
- package/dist/analyzers/security/actions.d.ts.map +1 -1
- package/dist/analyzers/security/actions.js +13 -0
- package/dist/analyzers/security/actions.js.map +1 -1
- package/dist/analyzers/security/aggregator.d.ts +97 -79
- package/dist/analyzers/security/aggregator.d.ts.map +1 -1
- package/dist/analyzers/security/aggregator.js +168 -56
- package/dist/analyzers/security/aggregator.js.map +1 -1
- package/dist/analyzers/security/gather.d.ts +2 -0
- package/dist/analyzers/security/gather.d.ts.map +1 -1
- package/dist/analyzers/security/gather.js +36 -4
- package/dist/analyzers/security/gather.js.map +1 -1
- package/dist/analyzers/security/index.d.ts.map +1 -1
- package/dist/analyzers/security/index.js +81 -2
- package/dist/analyzers/security/index.js.map +1 -1
- package/dist/analyzers/security/scanner-drift.d.ts +21 -0
- package/dist/analyzers/security/scanner-drift.d.ts.map +1 -0
- package/dist/analyzers/security/scanner-drift.js +113 -0
- package/dist/analyzers/security/scanner-drift.js.map +1 -0
- package/dist/analyzers/security/shallow.d.ts.map +1 -1
- package/dist/analyzers/security/shallow.js +24 -2
- package/dist/analyzers/security/shallow.js.map +1 -1
- package/dist/analyzers/security/types.d.ts +64 -4
- package/dist/analyzers/security/types.d.ts.map +1 -1
- package/dist/analyzers/tools/fingerprint.d.ts +133 -20
- package/dist/analyzers/tools/fingerprint.d.ts.map +1 -1
- package/dist/analyzers/tools/fingerprint.js +194 -20
- package/dist/analyzers/tools/fingerprint.js.map +1 -1
- package/dist/analyzers/tools/gitleaks.d.ts +2 -2
- package/dist/analyzers/tools/gitleaks.d.ts.map +1 -1
- package/dist/analyzers/tools/gitleaks.js +7 -1
- package/dist/analyzers/tools/gitleaks.js.map +1 -1
- package/dist/analyzers/tools/graphify.d.ts +11 -0
- package/dist/analyzers/tools/graphify.d.ts.map +1 -1
- package/dist/analyzers/tools/graphify.js +457 -413
- package/dist/analyzers/tools/graphify.js.map +1 -1
- package/dist/analyzers/tools/grep-secrets.d.ts.map +1 -1
- package/dist/analyzers/tools/grep-secrets.js +31 -12
- package/dist/analyzers/tools/grep-secrets.js.map +1 -1
- package/dist/analyzers/tools/osv-scanner-fix.d.ts.map +1 -1
- package/dist/analyzers/tools/osv-scanner-fix.js +12 -1
- package/dist/analyzers/tools/osv-scanner-fix.js.map +1 -1
- package/dist/analyzers/tools/salt.d.ts +68 -0
- package/dist/analyzers/tools/salt.d.ts.map +1 -0
- package/dist/{baseline → analyzers/tools}/salt.js +59 -18
- package/dist/analyzers/tools/salt.js.map +1 -0
- package/dist/analyzers/tools/semgrep.d.ts +7 -7
- package/dist/analyzers/tools/semgrep.d.ts.map +1 -1
- package/dist/analyzers/tools/semgrep.js +14 -7
- package/dist/analyzers/tools/semgrep.js.map +1 -1
- package/dist/analyzers/tools/tool-registry.d.ts.map +1 -1
- package/dist/analyzers/tools/tool-registry.js +78 -43
- package/dist/analyzers/tools/tool-registry.js.map +1 -1
- package/dist/analyzers/tools/walk-source-files.d.ts +10 -0
- package/dist/analyzers/tools/walk-source-files.d.ts.map +1 -1
- package/dist/analyzers/tools/walk-source-files.js +14 -0
- package/dist/analyzers/tools/walk-source-files.js.map +1 -1
- package/dist/analyzers/types.d.ts +9 -0
- package/dist/analyzers/types.d.ts.map +1 -1
- package/dist/baseline/baseline-file.d.ts +9 -2
- package/dist/baseline/baseline-file.d.ts.map +1 -1
- package/dist/baseline/baseline-file.js.map +1 -1
- package/dist/baseline/check-renderers.d.ts.map +1 -1
- package/dist/baseline/check-renderers.js +14 -0
- package/dist/baseline/check-renderers.js.map +1 -1
- package/dist/baseline/check.d.ts +33 -0
- package/dist/baseline/check.d.ts.map +1 -1
- package/dist/baseline/check.js +78 -2
- package/dist/baseline/check.js.map +1 -1
- package/dist/baseline/create.d.ts +1 -1
- package/dist/baseline/create.d.ts.map +1 -1
- package/dist/baseline/create.js +3 -1
- package/dist/baseline/create.js.map +1 -1
- package/dist/baseline/entry-to-located.d.ts +12 -5
- package/dist/baseline/entry-to-located.d.ts.map +1 -1
- package/dist/baseline/entry-to-located.js +21 -7
- package/dist/baseline/entry-to-located.js.map +1 -1
- package/dist/baseline/finding-identity.d.ts +20 -13
- package/dist/baseline/finding-identity.d.ts.map +1 -1
- package/dist/baseline/finding-identity.js +51 -20
- package/dist/baseline/finding-identity.js.map +1 -1
- package/dist/baseline/git-aware-match.d.ts +7 -5
- package/dist/baseline/git-aware-match.d.ts.map +1 -1
- package/dist/baseline/git-aware-match.js +78 -5
- package/dist/baseline/git-aware-match.js.map +1 -1
- package/dist/baseline/migrate.d.ts +94 -0
- package/dist/baseline/migrate.d.ts.map +1 -0
- package/dist/baseline/migrate.js +238 -0
- package/dist/baseline/migrate.js.map +1 -0
- package/dist/baseline/producers/security.d.ts +9 -9
- package/dist/baseline/producers/security.d.ts.map +1 -1
- package/dist/baseline/producers/security.js +16 -4
- package/dist/baseline/producers/security.js.map +1 -1
- package/dist/baseline/types.d.ts +145 -95
- package/dist/baseline/types.d.ts.map +1 -1
- package/dist/baseline/types.js +30 -26
- package/dist/baseline/types.js.map +1 -1
- package/dist/explore/context-hook.d.ts +49 -29
- package/dist/explore/context-hook.d.ts.map +1 -1
- package/dist/explore/context-hook.js +304 -29
- package/dist/explore/context-hook.js.map +1 -1
- package/dist/explore/finding-context.d.ts +17 -0
- package/dist/explore/finding-context.d.ts.map +1 -1
- package/dist/explore/finding-context.js +34 -0
- package/dist/explore/finding-context.js.map +1 -1
- package/dist/explore/queries.d.ts +32 -15
- package/dist/explore/queries.d.ts.map +1 -1
- package/dist/explore/queries.js +36 -6
- package/dist/explore/queries.js.map +1 -1
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +13 -7
- package/dist/generator.js.map +1 -1
- package/dist/ingest/normalize.d.ts +1 -1
- package/dist/ingest/normalize.d.ts.map +1 -1
- package/dist/ingest/normalize.js +5 -1
- package/dist/ingest/normalize.js.map +1 -1
- package/dist/ingest/sarif.d.ts.map +1 -1
- package/dist/ingest/sarif.js +16 -7
- package/dist/ingest/sarif.js.map +1 -1
- package/dist/ingest/snyk-policy.d.ts +22 -1
- package/dist/ingest/snyk-policy.d.ts.map +1 -1
- package/dist/ingest/snyk-policy.js +75 -18
- package/dist/ingest/snyk-policy.js.map +1 -1
- package/dist/ingest/types.d.ts +23 -12
- package/dist/ingest/types.d.ts.map +1 -1
- package/dist/languages/capabilities/types.d.ts +64 -53
- package/dist/languages/capabilities/types.d.ts.map +1 -1
- package/dist/languages/capabilities/types.js +4 -4
- package/dist/languages/index.d.ts +28 -5
- package/dist/languages/index.d.ts.map +1 -1
- package/dist/languages/index.js +38 -7
- package/dist/languages/index.js.map +1 -1
- package/dist/languages/typescript.d.ts.map +1 -1
- package/dist/languages/typescript.js +19 -0
- package/dist/languages/typescript.js.map +1 -1
- package/dist/scoring/dimensions/security.d.ts +17 -0
- package/dist/scoring/dimensions/security.d.ts.map +1 -1
- package/dist/scoring/dimensions/security.js +12 -0
- package/dist/scoring/dimensions/security.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +49 -0
- package/dist/update.js.map +1 -1
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +2 -1
- package/dist/upgrade.js.map +1 -1
- package/package.json +6 -3
- package/templates/.claude/skills/dxkit-action/SKILL.md +11 -2
- package/templates/.claude/skills/dxkit-allowlist/SKILL.md +9 -0
- package/templates/.claude/skills/dxkit-onboard/SKILL.md +2 -2
- package/templates/.claude/skills/dxkit-update/SKILL.md +45 -4
- package/dist/baseline/salt.d.ts +0 -45
- package/dist/baseline/salt.d.ts.map +0 -1
- 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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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
|
-
*
|
|
51
|
-
*
|
|
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
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
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
-
*
|
|
161
|
+
* + any future per-pack code-pattern producers), post-dedup. */
|
|
161
162
|
codeBySeverity: SeverityCounts;
|
|
162
163
|
/** Dependency advisories by severity, derived from the
|
|
163
|
-
*
|
|
164
|
-
*
|
|
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
|
-
*
|
|
168
|
-
*
|
|
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
|
-
*
|
|
172
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
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
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
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
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
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
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAmB,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
-
*
|
|
52
|
-
*
|
|
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
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
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
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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
|
|
331
|
+
fingerprint,
|
|
250
332
|
canonicalRule: g.canonicalRule,
|
|
251
333
|
producedBy: [...g.producedBy].sort(),
|
|
252
|
-
|
|
253
|
-
|
|
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,
|