@vyuhlabs/dxkit 2.9.3 → 2.10.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 +170 -0
- package/README.md +9 -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 +6 -0
- package/dist/allowlist/cli.d.ts.map +1 -1
- package/dist/allowlist/cli.js +70 -37
- package/dist/allowlist/cli.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/developer/gather.d.ts +16 -0
- package/dist/analyzers/developer/gather.d.ts.map +1 -1
- package/dist/analyzers/developer/gather.js +2 -0
- package/dist/analyzers/developer/gather.js.map +1 -1
- package/dist/analyzers/developer/ownership.d.ts +86 -0
- package/dist/analyzers/developer/ownership.d.ts.map +1 -0
- package/dist/analyzers/developer/ownership.js +180 -0
- package/dist/analyzers/developer/ownership.js.map +1 -0
- 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/quality/detailed.d.ts +5 -1
- package/dist/analyzers/quality/detailed.d.ts.map +1 -1
- package/dist/analyzers/quality/detailed.js +30 -29
- package/dist/analyzers/quality/detailed.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 +18 -0
- package/dist/analyzers/security/aggregator.d.ts.map +1 -1
- package/dist/analyzers/security/aggregator.js +28 -0
- package/dist/analyzers/security/aggregator.js.map +1 -1
- package/dist/analyzers/security/detailed.d.ts +7 -1
- package/dist/analyzers/security/detailed.d.ts.map +1 -1
- package/dist/analyzers/security/detailed.js +31 -15
- package/dist/analyzers/security/detailed.js.map +1 -1
- package/dist/analyzers/security/gather.d.ts.map +1 -1
- package/dist/analyzers/security/gather.js +6 -0
- 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 +38 -0
- package/dist/analyzers/security/types.d.ts.map +1 -1
- package/dist/analyzers/tests/detailed.d.ts +5 -1
- package/dist/analyzers/tests/detailed.d.ts.map +1 -1
- package/dist/analyzers/tests/detailed.js +27 -20
- package/dist/analyzers/tests/detailed.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 +429 -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 +9 -0
- 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/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/attribution/attribute.d.ts +57 -0
- package/dist/attribution/attribute.d.ts.map +1 -0
- package/dist/attribution/attribute.js +149 -0
- package/dist/attribution/attribute.js.map +1 -0
- 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/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/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -5
- package/dist/cli.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/generator.d.ts.map +1 -1
- package/dist/generator.js +13 -7
- package/dist/generator.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/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/reviewers-cli.d.ts +57 -0
- package/dist/reviewers-cli.d.ts.map +1 -0
- package/dist/reviewers-cli.js +263 -0
- package/dist/reviewers-cli.js.map +1 -0
- 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/package.json +1 -1
- package/templates/.claude/skills/dxkit-action/SKILL.md +13 -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-pr/SKILL.md +22 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,176 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.10.0] - 2026-06-13
|
|
11
|
+
|
|
12
|
+
### Honest scoring under changing scanners, passive graph delivery, tool-robustness
|
|
13
|
+
|
|
14
|
+
Closes a set of brownfield-install and guardrail-matcher defects (the original
|
|
15
|
+
2.9.5 hardening), a class of scoring-honesty bugs (a Security score that could get
|
|
16
|
+
worse on an unchanged commit, with nothing explaining why), a defensive
|
|
17
|
+
tool-version-pin sweep, and the agentic-delivery redesign that finally routes the
|
|
18
|
+
code graph to the agent in a real fix workflow.
|
|
19
|
+
|
|
20
|
+
#### Scoring honesty
|
|
21
|
+
|
|
22
|
+
A Security score could drop on an **unchanged commit** — e.g. after an upgrade
|
|
23
|
+
enabled more scanners, or because a repo's own reviewed-and-accepted findings kept
|
|
24
|
+
holding it at a cap. The measurement was getting more honest, but the output
|
|
25
|
+
didn't explain it, and a properly-triaged repo couldn't recover its score. These
|
|
26
|
+
close that gap.
|
|
27
|
+
|
|
28
|
+
- **Symmetric unavailable-scanner caps.** A missing dependency-audit
|
|
29
|
+
already capped the Security score at the uncertainty tier, but missing
|
|
30
|
+
secret/code-pattern scanners silently scored as "0 findings" — so enabling
|
|
31
|
+
those scanners later read as a phantom regression. The secret and code-pattern
|
|
32
|
+
axes now get the same uncertainty cap when their scan didn't run, surfaced in
|
|
33
|
+
`metrics.toolsUnavailable` and the standalone vuln-scan report.
|
|
34
|
+
- **The score respects the allowlist.** Findings reviewed-and-accepted as
|
|
35
|
+
`false-positive` / `test-fixture` are now lifted from the Security penalties and
|
|
36
|
+
caps (not just the guardrail), so a triaged repo scores honestly instead of
|
|
37
|
+
staying capped on noise it has already accepted. `accepted-risk` / `deferred` /
|
|
38
|
+
`mitigated-externally` still count — accepting a real risk can't earn an A. The
|
|
39
|
+
vulnerability report and dashboard also annotate allowlisted findings and render
|
|
40
|
+
`Subtotal N (M allowlisted)` so the raw counts are explained, not alarming.
|
|
41
|
+
- **Scanner-coverage drift is disclosed.** When the active scanner set grew
|
|
42
|
+
since the last run, the vuln-scan report leads with a note: findings the new
|
|
43
|
+
scanners surface are newly **visible**, not newly **introduced**. This is the
|
|
44
|
+
root-cause explanation for a score that moved on unchanged code.
|
|
45
|
+
- **Secret severity is never lowered by file path.** A hardcoded credential keeps
|
|
46
|
+
its natural severity whether it sits in production code or a test — the generic
|
|
47
|
+
matcher can't tell a throwaway fixture from a real secret leaked into a test, so
|
|
48
|
+
lowering severity by path would silently hide genuine leaks. Test-file noise is
|
|
49
|
+
managed by the allowlist score-lift above (review fixtures once with
|
|
50
|
+
`--category test-fixture`), not by hiding. The vulnerability report now flags how
|
|
51
|
+
many secret findings sit in test files and points fixtures at the allowlist; the
|
|
52
|
+
`dxkit-action` and `dxkit-allowlist` skills gain an explicit triage step
|
|
53
|
+
(confirm fixture vs. real, allowlist fakes, rotate reals) so an agent handles
|
|
54
|
+
this judgment per finding rather than blanket-ignoring the test directory.
|
|
55
|
+
- **Systematic test-file detection.** Tests organized under Jest's `__tests__/`
|
|
56
|
+
directory — or named with the widespread `.unit.` / `.e2e.` / `.cy.` suffixes —
|
|
57
|
+
were classified as source, corrupting the test ratio, coverage, and test-gap
|
|
58
|
+
analysis. The cross-ecosystem test directories (`__tests__/`, `test/`, `tests/`,
|
|
59
|
+
`spec/`, `e2e/`) are now recognized in any language; the TS pack gains the
|
|
60
|
+
co-located suffix conventions.
|
|
61
|
+
- **Dependency-audit cleanup on Windows (EPERM).** The osv-scanner-fix temp-dir
|
|
62
|
+
cleanup now retries with backoff and never throws out of its `finally`, so a
|
|
63
|
+
Windows handle race (npm-install grandchildren / antivirus) can no longer
|
|
64
|
+
discard the already-parsed fix plans — which had let dependency vulnerabilities
|
|
65
|
+
go silently unreported.
|
|
66
|
+
|
|
67
|
+
#### Passive graph delivery (agentic value)
|
|
68
|
+
|
|
69
|
+
- **Context-hook fires on the tools agents actually use.** Pre-2.10 the graph
|
|
70
|
+
context-hook fired only on the native `Grep`/`Glob` tools and only when the
|
|
71
|
+
search pattern substring-matched a symbol name — so in a real fix workflow
|
|
72
|
+
(agents search via `Bash grep` for a symptom, and read files directly) it
|
|
73
|
+
almost never engaged. It now fires on **Read/Edit** (keyed on the file touched
|
|
74
|
+
→ that file's structural summary: symbols, callers, callees, module group),
|
|
75
|
+
**Bash** (parses grep/rg commands; a named source file delivers its summary,
|
|
76
|
+
else a symbol match on the pattern), and the original **Grep/Glob** path.
|
|
77
|
+
Per-session, per-file dedup keeps it cheap; the FAIL-OPEN + ADDITIVE contract is
|
|
78
|
+
preserved (any problem is a silent no-op). **Existing repos must re-run
|
|
79
|
+
`vyuh-dxkit init`** (or update `.claude/settings.json`) to pick up the broadened
|
|
80
|
+
`Read|Edit|Bash|Grep|Glob` matcher.
|
|
81
|
+
|
|
82
|
+
#### Snyk sync
|
|
83
|
+
|
|
84
|
+
- **`.dxkit-ignore` → `.snyk` exclude sync.** `allowlist export --snyk` now also
|
|
85
|
+
emits the paths dxkit's analyzers skip (`.dxkit-ignore`) into the `.snyk`
|
|
86
|
+
`exclude.global` block, so Snyk and dxkit agree on what's out of scope —
|
|
87
|
+
mirroring the existing allowlist → `.snyk` ignore sync. An export carrying only
|
|
88
|
+
exclusions still writes.
|
|
89
|
+
|
|
90
|
+
#### Tool-robustness + matcher rename fixes
|
|
91
|
+
|
|
92
|
+
Hardening pass closing a set of brownfield-install and guardrail-matcher
|
|
93
|
+
defects surfaced while benchmarking on Python 3.14 and large real-world repos.
|
|
94
|
+
|
|
95
|
+
#### Fixed
|
|
96
|
+
|
|
97
|
+
- **graphify on Python 3.14.** Python 3.14 made `forkserver` the default
|
|
98
|
+
multiprocessing start method on Linux. graphify parallelises extraction with a
|
|
99
|
+
`ProcessPoolExecutor`, and under spawn/forkserver each worker re-imports the
|
|
100
|
+
generated script — re-running top-level extraction and crashing the run (no
|
|
101
|
+
`.dxkit/reports/graph.json` written; every graph-dependent feature silently
|
|
102
|
+
degraded). The generated script now wraps its execution body in
|
|
103
|
+
`if __name__ == '__main__'` — graphify's own documented requirement for
|
|
104
|
+
parallel extraction — so it is correct on every platform and start method
|
|
105
|
+
(Linux fork/forkserver, macOS/Windows spawn) while keeping multi-core
|
|
106
|
+
extraction. The previous forced `set_start_method('fork')` workaround is
|
|
107
|
+
removed.
|
|
108
|
+
- **graphify cache redirect.** The on-disk cache is now redirected via
|
|
109
|
+
graphify's public `extract(cache_root=...)` parameter instead of
|
|
110
|
+
monkeypatching the internal `graphify.cache.cache_dir`, whose signature
|
|
111
|
+
changed in graphifyy 0.8 (`cache_dir(root)` → `cache_dir(root, kind)`) and
|
|
112
|
+
crashed the run. This also stops graphify's `atexit` stat-index flush from
|
|
113
|
+
writing a stray `graphify-out/` into the scanned repo. The temp cache lives
|
|
114
|
+
under the caller-owned script dir and is reclaimed after the process (and its
|
|
115
|
+
atexit handlers) exit. `graphifyy` is pinned to `0.8.36`.
|
|
116
|
+
- **jscpd version pin.** jscpd is pinned to `4.2.5`. jscpd 5.x is a Rust
|
|
117
|
+
rewrite that dropped the `--gitignore` flag (dxkit passed it → exit 2) and
|
|
118
|
+
changed the report JSON schema dxkit parses.
|
|
119
|
+
- **Guardrail matcher — whole-file rename relocation.** Renaming a source
|
|
120
|
+
file no longer reports its whole-file findings (test-gap, coverage-gap,
|
|
121
|
+
test-file-degradation, god-file, stale-file, large-file) as removed + added,
|
|
122
|
+
which falsely blocked the guardrail on a pure rename. The git-aware matcher
|
|
123
|
+
now relocates these line-less, file-anchored findings through git's rename
|
|
124
|
+
detection, keyed on `(renamed-path, kind)` so two different whole-file kinds
|
|
125
|
+
on the same renamed file never cross-pair.
|
|
126
|
+
|
|
127
|
+
#### Tool-version pins
|
|
128
|
+
|
|
129
|
+
- **Defensive pin sweep.** Nine more dxkit-owned, deterministic-output scanners
|
|
130
|
+
are pinned to their current releases (semgrep `1.165.0`, ruff `0.15.17`,
|
|
131
|
+
pip-audit `2.10.1`, pip-licenses `5.5.5`, coverage `7.14.1`,
|
|
132
|
+
license-checker-rseidelsohn `5.0.1`, golangci-lint `v1.64.8` — the v1 line,
|
|
133
|
+
since v2 is a breaking rewrite on a separate module path — govulncheck `v1.3.0`,
|
|
134
|
+
go-licenses `v1.6.0`), so a future breaking major can't silently change parsed
|
|
135
|
+
output or exit codes the way jscpd 5.x and graphifyy 0.8 did. Five tools stay
|
|
136
|
+
unpinned by design and are now documented as such: `eslint` + `vitest-coverage`
|
|
137
|
+
(project-local — the consumer owns the version), `snyk` (a SaaS client that
|
|
138
|
+
self-manages backend compatibility), `codeql` (a GitHub-managed bundle paired
|
|
139
|
+
with query packs), and `cloc` (non-semver npm tag, lowest-risk schema). Proper
|
|
140
|
+
schema-adaptive multi-version handling is planned for a later release.
|
|
141
|
+
|
|
142
|
+
#### Internal
|
|
143
|
+
|
|
144
|
+
- The version-pin guard test partitions every registry tool into pinned /
|
|
145
|
+
unpinned-by-design / package-manager-tracked, so a tool can't be added or
|
|
146
|
+
un-pinned without a deliberate decision.
|
|
147
|
+
|
|
148
|
+
## [2.9.4] - 2026-06-09
|
|
149
|
+
|
|
150
|
+
### Connecting findings + PRs to the people who know the code
|
|
151
|
+
|
|
152
|
+
Two features on a shared **active-owner model** — recency-weighted git history
|
|
153
|
+
scoped to who is still active, with bots and departed contributors filtered, the
|
|
154
|
+
change author excluded, and a bus-factor signal. Output renders names + GitHub
|
|
155
|
+
@handles, never raw emails (the @handle is both privacy-safe and the actionable
|
|
156
|
+
identifier — it's @-mentionable and feeds `gh --reviewer`).
|
|
157
|
+
|
|
158
|
+
- **`vyuh-dxkit reviewers`** suggests reviewers for a change (`--base <ref>` /
|
|
159
|
+
`--staged`). It ranks the active owners of the touched files — recency-weighted,
|
|
160
|
+
bot-free, departed-dev-aware, author-excluded — blended with `CODEOWNERS`, and
|
|
161
|
+
warns on a bus factor of 1. The differentiation over a platform's naive
|
|
162
|
+
last-touch suggestion is the activity grounding + active-only scoping. The
|
|
163
|
+
`dxkit-pr` skill consumes it for a "Suggested reviewers" block and
|
|
164
|
+
`gh pr create --reviewer`.
|
|
165
|
+
- **`--attribute` "who to ask"** on the detailed vulnerability / test-gaps /
|
|
166
|
+
quality reports. For a pre-existing finding it adds a "Who to ask" column:
|
|
167
|
+
line-level findings are `git blame`d and routed through the owner model (an
|
|
168
|
+
inactive author is forwarded to the file's current owner); file-level findings
|
|
169
|
+
(test gaps) attribute to the file's current owner. Opt-in and historical only —
|
|
170
|
+
a net-new finding the guardrail just blocked was introduced by your own change,
|
|
171
|
+
so its owner is the PR author. The column is honest that blame is last-touch,
|
|
172
|
+
not necessarily who introduced the finding.
|
|
173
|
+
|
|
174
|
+
### Privacy
|
|
175
|
+
|
|
176
|
+
Author emails are used only as the internal identity key for clustering; they
|
|
177
|
+
are never rendered in any report or PR output. Everything user-facing is a
|
|
178
|
+
display name or a GitHub @handle.
|
|
179
|
+
|
|
10
180
|
## [2.9.3] - 2026-06-09
|
|
11
181
|
|
|
12
182
|
### Targetable fix loop + test generation
|
package/README.md
CHANGED
|
@@ -210,6 +210,15 @@ dxkit builds a deterministic code graph of your repo (its symbols, call edges, a
|
|
|
210
210
|
|
|
211
211
|
This is an additive, fail-open layer. When the graph is missing, or a language's call edges can't be resolved, every command behaves exactly as it did before. It's reliable on TypeScript, Python, and Go. Where the call graph can't be resolved (C#), blast radius is suppressed rather than faked, so a "no callers" reading is never mistaken for "safe to change."
|
|
212
212
|
|
|
213
|
+
### Connect findings and PRs to the people who know the code
|
|
214
|
+
|
|
215
|
+
A finding or a PR is more actionable when you know who to ask. dxkit grounds that in an **active-owner model** — recency-weighted git history, scoped to who is still active, with bots and departed contributors filtered, the change author excluded, and a bus-factor signal.
|
|
216
|
+
|
|
217
|
+
- **`vyuh-dxkit reviewers`** suggests reviewers for a change, ranked by active ownership of the touched files and blended with `CODEOWNERS` — a better signal than a platform's naive last-touch suggestion. The `dxkit-pr` skill folds it into the PR body.
|
|
218
|
+
- **`--attribute`** adds a "who to ask" column to a detailed report: a pre-existing finding is traced to its current owner (an inactive author is routed to whoever owns the file now). It's opt-in and historical — a net-new finding is introduced by your own change.
|
|
219
|
+
|
|
220
|
+
Output is names + GitHub @handles, never raw emails — the @handle is both privacy-safe and @-mentionable.
|
|
221
|
+
|
|
213
222
|
### Deep SAST: interprocedural findings from any engine
|
|
214
223
|
|
|
215
224
|
dxkit's bundled SAST (community semgrep) is intraprocedural — it can't follow tainted data across function boundaries, so it misses the path-traversal / information-exposure / SSRF / injection class that an interprocedural engine like Snyk Code or CodeQL catches. dxkit doesn't try to re-detect that class; it **ingests** it and makes it first-class.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annotate security findings with their active-allowlist status
|
|
3
|
+
* for REPORTING (not gating).
|
|
4
|
+
*
|
|
5
|
+
* The guardrail already consults the allowlist to decide whether a
|
|
6
|
+
* net-new finding blocks a push (`src/baseline/check.ts`). But the
|
|
7
|
+
* vulnerability-scan report and dashboard rendered raw counts with no
|
|
8
|
+
* indication that some findings are reviewed-and-accepted — a repo that
|
|
9
|
+
* has correctly allowlisted, say, its unit-test fixtures still showed
|
|
10
|
+
* them as headline criticals with no visual distinction, which reads as
|
|
11
|
+
* "the score is lying."
|
|
12
|
+
*
|
|
13
|
+
* This module marks each finding whose fingerprint matches an ACTIVE
|
|
14
|
+
* (unexpired) allowlist entry so renderers can show "(N allowlisted)"
|
|
15
|
+
* beside the subtotal. It does NOT change raw counts and does NOT change
|
|
16
|
+
* the score — dxkit's raw-truth model is preserved; only the
|
|
17
|
+
* presentation gains an honesty annotation.
|
|
18
|
+
*
|
|
19
|
+
* Identity contract (CLAUDE.md Rule 9): this module never computes a
|
|
20
|
+
* fingerprint. It matches against the fingerprint the aggregator
|
|
21
|
+
* already stamped on each code/secret/config finding (plus the
|
|
22
|
+
* `absorbedFingerprints` recorded when cross-tool dedup collapsed
|
|
23
|
+
* contributors — same robust-match set the guardrail uses). Dependency
|
|
24
|
+
* findings are keyed by `(package, version, id)` through a producer and
|
|
25
|
+
* carry no inline fingerprint, so they are out of scope here.
|
|
26
|
+
*/
|
|
27
|
+
import { type AllowlistFile } from './file';
|
|
28
|
+
import type { AllowlistCategory } from './categories';
|
|
29
|
+
import type { FindingCategory } from '../analyzers/security/types';
|
|
30
|
+
/**
|
|
31
|
+
* The minimal finding shape this module reads + writes. The runtime
|
|
32
|
+
* objects are richer (`CodeFinding` carries `fingerprint` +
|
|
33
|
+
* `absorbedFingerprints`); we accept the structural subset so callers
|
|
34
|
+
* pass their findings directly without a cast.
|
|
35
|
+
*/
|
|
36
|
+
export interface AnnotatableFinding {
|
|
37
|
+
readonly category: FindingCategory;
|
|
38
|
+
readonly fingerprint?: string;
|
|
39
|
+
readonly absorbedFingerprints?: readonly string[];
|
|
40
|
+
allowlisted?: boolean;
|
|
41
|
+
allowlistCategory?: AllowlistCategory;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether an active allowlist entry of this category should LIFT the
|
|
45
|
+
* finding from the dimension score (penalties + caps), not just from
|
|
46
|
+
* the guardrail.
|
|
47
|
+
*
|
|
48
|
+
* `false-positive` and `test-fixture` declare the finding is "not a real
|
|
49
|
+
* finding" — a misfire or throwaway test data — so a properly-triaged
|
|
50
|
+
* repo shouldn't carry a score penalty for it (the failure mode where a
|
|
51
|
+
* repo stays capped at the trust-broken tier despite having reviewed and
|
|
52
|
+
* accepted every flagged secret). `accepted-risk` and `deferred`, by
|
|
53
|
+
* contrast, accept a REAL risk: the guardrail stops blocking on them,
|
|
54
|
+
* but the score must still reflect the residual exposure — you can't
|
|
55
|
+
* `accepted-risk` your way to an A. `mitigated-externally` counts too:
|
|
56
|
+
* the risk is real, just handled outside dxkit.
|
|
57
|
+
*/
|
|
58
|
+
export declare function allowlistLiftsScore(category: AllowlistCategory | undefined): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Mutate `findings` in place, setting `allowlisted` + `allowlistCategory`
|
|
61
|
+
* on each finding matched by an active allowlist entry. Returns the count
|
|
62
|
+
* of findings annotated, so callers can short-circuit rendering when zero.
|
|
63
|
+
*
|
|
64
|
+
* A finding matches when ANY of its candidate fingerprints (its own
|
|
65
|
+
* `fingerprint`, then any `absorbedFingerprints`) resolves to an
|
|
66
|
+
* allowlist entry whose `kind` equals the finding's kind and which is
|
|
67
|
+
* active at `now`. The kind guard rules out a cross-kind hash collision
|
|
68
|
+
* waiving the wrong finding — mirrors `allowlistSuppressionFor`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function annotateFindingsWithAllowlist(findings: AnnotatableFinding[], allowlist: AllowlistFile | null, now?: Date): number;
|
|
71
|
+
//# sourceMappingURL=annotate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annotate.d.ts","sourceRoot":"","sources":["../../src/allowlist/annotate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,KAAK,aAAa,EAA4B,MAAM,QAAQ,CAAC;AACtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAGnE;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC;AAoBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,SAAS,GAAG,OAAO,CAEpF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,kBAAkB,EAAE,EAC9B,SAAS,EAAE,aAAa,GAAG,IAAI,EAC/B,GAAG,GAAE,IAAiB,GACrB,MAAM,CAuBR"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.allowlistLiftsScore = allowlistLiftsScore;
|
|
4
|
+
exports.annotateFindingsWithAllowlist = annotateFindingsWithAllowlist;
|
|
5
|
+
/**
|
|
6
|
+
* Annotate security findings with their active-allowlist status
|
|
7
|
+
* for REPORTING (not gating).
|
|
8
|
+
*
|
|
9
|
+
* The guardrail already consults the allowlist to decide whether a
|
|
10
|
+
* net-new finding blocks a push (`src/baseline/check.ts`). But the
|
|
11
|
+
* vulnerability-scan report and dashboard rendered raw counts with no
|
|
12
|
+
* indication that some findings are reviewed-and-accepted — a repo that
|
|
13
|
+
* has correctly allowlisted, say, its unit-test fixtures still showed
|
|
14
|
+
* them as headline criticals with no visual distinction, which reads as
|
|
15
|
+
* "the score is lying."
|
|
16
|
+
*
|
|
17
|
+
* This module marks each finding whose fingerprint matches an ACTIVE
|
|
18
|
+
* (unexpired) allowlist entry so renderers can show "(N allowlisted)"
|
|
19
|
+
* beside the subtotal. It does NOT change raw counts and does NOT change
|
|
20
|
+
* the score — dxkit's raw-truth model is preserved; only the
|
|
21
|
+
* presentation gains an honesty annotation.
|
|
22
|
+
*
|
|
23
|
+
* Identity contract (CLAUDE.md Rule 9): this module never computes a
|
|
24
|
+
* fingerprint. It matches against the fingerprint the aggregator
|
|
25
|
+
* already stamped on each code/secret/config finding (plus the
|
|
26
|
+
* `absorbedFingerprints` recorded when cross-tool dedup collapsed
|
|
27
|
+
* contributors — same robust-match set the guardrail uses). Dependency
|
|
28
|
+
* findings are keyed by `(package, version, id)` through a producer and
|
|
29
|
+
* carry no inline fingerprint, so they are out of scope here.
|
|
30
|
+
*/
|
|
31
|
+
const file_1 = require("./file");
|
|
32
|
+
/**
|
|
33
|
+
* Map a report `FindingCategory` to the canonical `IdentityKind` used
|
|
34
|
+
* by allowlist entries. Only the three fingerprint-bearing categories
|
|
35
|
+
* resolve; `dependency` returns null (out of scope — see module doc).
|
|
36
|
+
*/
|
|
37
|
+
function kindForCategory(category) {
|
|
38
|
+
switch (category) {
|
|
39
|
+
case 'secret':
|
|
40
|
+
return 'secret';
|
|
41
|
+
case 'code':
|
|
42
|
+
return 'code';
|
|
43
|
+
case 'config':
|
|
44
|
+
return 'config';
|
|
45
|
+
case 'dependency':
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether an active allowlist entry of this category should LIFT the
|
|
51
|
+
* finding from the dimension score (penalties + caps), not just from
|
|
52
|
+
* the guardrail.
|
|
53
|
+
*
|
|
54
|
+
* `false-positive` and `test-fixture` declare the finding is "not a real
|
|
55
|
+
* finding" — a misfire or throwaway test data — so a properly-triaged
|
|
56
|
+
* repo shouldn't carry a score penalty for it (the failure mode where a
|
|
57
|
+
* repo stays capped at the trust-broken tier despite having reviewed and
|
|
58
|
+
* accepted every flagged secret). `accepted-risk` and `deferred`, by
|
|
59
|
+
* contrast, accept a REAL risk: the guardrail stops blocking on them,
|
|
60
|
+
* but the score must still reflect the residual exposure — you can't
|
|
61
|
+
* `accepted-risk` your way to an A. `mitigated-externally` counts too:
|
|
62
|
+
* the risk is real, just handled outside dxkit.
|
|
63
|
+
*/
|
|
64
|
+
function allowlistLiftsScore(category) {
|
|
65
|
+
return category === 'false-positive' || category === 'test-fixture';
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Mutate `findings` in place, setting `allowlisted` + `allowlistCategory`
|
|
69
|
+
* on each finding matched by an active allowlist entry. Returns the count
|
|
70
|
+
* of findings annotated, so callers can short-circuit rendering when zero.
|
|
71
|
+
*
|
|
72
|
+
* A finding matches when ANY of its candidate fingerprints (its own
|
|
73
|
+
* `fingerprint`, then any `absorbedFingerprints`) resolves to an
|
|
74
|
+
* allowlist entry whose `kind` equals the finding's kind and which is
|
|
75
|
+
* active at `now`. The kind guard rules out a cross-kind hash collision
|
|
76
|
+
* waiving the wrong finding — mirrors `allowlistSuppressionFor`.
|
|
77
|
+
*/
|
|
78
|
+
function annotateFindingsWithAllowlist(findings, allowlist, now = new Date()) {
|
|
79
|
+
if (!allowlist || allowlist.entries.length === 0)
|
|
80
|
+
return 0;
|
|
81
|
+
let annotated = 0;
|
|
82
|
+
for (const f of findings) {
|
|
83
|
+
const kind = kindForCategory(f.category);
|
|
84
|
+
if (!kind)
|
|
85
|
+
continue;
|
|
86
|
+
const candidates = [];
|
|
87
|
+
if (f.fingerprint)
|
|
88
|
+
candidates.push(f.fingerprint);
|
|
89
|
+
if (f.absorbedFingerprints)
|
|
90
|
+
candidates.push(...f.absorbedFingerprints);
|
|
91
|
+
for (const fp of candidates) {
|
|
92
|
+
const entry = (0, file_1.findEntry)(allowlist, fp);
|
|
93
|
+
if (!entry || entry.kind !== kind)
|
|
94
|
+
continue;
|
|
95
|
+
if (!(0, file_1.isEntryActive)(entry, now))
|
|
96
|
+
continue;
|
|
97
|
+
f.allowlisted = true;
|
|
98
|
+
f.allowlistCategory = entry.category;
|
|
99
|
+
annotated++;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return annotated;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=annotate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annotate.js","sourceRoot":"","sources":["../../src/allowlist/annotate.ts"],"names":[],"mappings":";;AA8EA,kDAEC;AAaD,sEA2BC;AAxHD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,iCAAsE;AAmBtE;;;;GAIG;AACH,SAAS,eAAe,CAAC,QAAyB;IAChD,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAC;QAClB,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAC;QAClB,KAAK,YAAY;YACf,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAgB,mBAAmB,CAAC,QAAuC;IACzE,OAAO,QAAQ,KAAK,gBAAgB,IAAI,QAAQ,KAAK,cAAc,CAAC;AACtE,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,6BAA6B,CAC3C,QAA8B,EAC9B,SAA+B,EAC/B,MAAY,IAAI,IAAI,EAAE;IAEtB,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAE3D,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI;YAAE,SAAS;QAEpB,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,IAAI,CAAC,CAAC,WAAW;YAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;QAClD,IAAI,CAAC,CAAC,oBAAoB;YAAE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,oBAAoB,CAAC,CAAC;QAEvE,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;YAC5B,MAAM,KAAK,GAAG,IAAA,gBAAS,EAAC,SAAS,EAAE,EAAE,CAAC,CAAC;YACvC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;gBAAE,SAAS;YAC5C,IAAI,CAAC,IAAA,oBAAa,EAAC,KAAK,EAAE,GAAG,CAAC;gBAAE,SAAS;YACzC,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC;YACrB,CAAC,CAAC,iBAAiB,GAAG,KAAK,CAAC,QAAQ,CAAC;YACrC,SAAS,EAAE,CAAC;YACZ,MAAM;QACR,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/dist/allowlist/cli.d.ts
CHANGED
|
@@ -125,6 +125,12 @@ export declare function runAllowlistRemove(cwd: string, opts: AllowlistRemoveOpt
|
|
|
125
125
|
* reason + expiry. Expired entries are skipped (they no longer
|
|
126
126
|
* suppress). Only `snyk-code` findings export — native semgrep /
|
|
127
127
|
* gitleaks findings have no Snyk equivalent.
|
|
128
|
+
*
|
|
129
|
+
* 2.10 also syncs the PATH-EXCLUSION half: `.dxkit-ignore` patterns
|
|
130
|
+
* (the paths dxkit's own analyzers skip) are emitted into the `.snyk`
|
|
131
|
+
* `exclude.global` block, so Snyk and dxkit agree on what's out of
|
|
132
|
+
* scope. The two halves compose into one `.snyk`; an export carrying
|
|
133
|
+
* only exclusions (no allowlisted Snyk findings yet) still writes.
|
|
128
134
|
*/
|
|
129
135
|
export declare function runAllowlistExport(cwd: string, opts: AllowlistExportOpts): Promise<void>;
|
|
130
136
|
export { DEFAULT_EXPIRY_DAYS };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/allowlist/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/allowlist/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAuBH,OAAO,EAEL,mBAAmB,EAMpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAelB,KAAK,aAAa,EAEnB,MAAM,QAAQ,CAAC;AAGhB,2DAA2D;AAC3D,eAAO,MAAM,qBAAqB,wEAQxB,CAAC;AACX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,gBAAgB;IAC/B;;uBAEmB;IACnB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;4CAEwC;IACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,mDAAmD;IACnD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;kDAG8C;IAC9C,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,uDAAuD;IACvD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,4CAA4C;IAC5C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB;8CAC0C;IAC1C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;2CAEuC;IACvC,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,IAAI,EAAE;IACJ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,GACA,OAAO,CAAC,IAAI,CAAC,CAyDf;AAID,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBxF;AAyHD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB1F;AAID,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B1F;AAID,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuG5F;AAID,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmC5F;AAID,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B9F;AAID;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+F9F;AAmJD,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAG/B,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
package/dist/allowlist/cli.js
CHANGED
|
@@ -462,23 +462,25 @@ async function runAllowlistRemove(cwd, opts) {
|
|
|
462
462
|
* reason + expiry. Expired entries are skipped (they no longer
|
|
463
463
|
* suppress). Only `snyk-code` findings export — native semgrep /
|
|
464
464
|
* gitleaks findings have no Snyk equivalent.
|
|
465
|
+
*
|
|
466
|
+
* 2.10 also syncs the PATH-EXCLUSION half: `.dxkit-ignore` patterns
|
|
467
|
+
* (the paths dxkit's own analyzers skip) are emitted into the `.snyk`
|
|
468
|
+
* `exclude.global` block, so Snyk and dxkit agree on what's out of
|
|
469
|
+
* scope. The two halves compose into one `.snyk`; an export carrying
|
|
470
|
+
* only exclusions (no allowlisted Snyk findings yet) still writes.
|
|
465
471
|
*/
|
|
466
472
|
async function runAllowlistExport(cwd, opts) {
|
|
467
473
|
if (!opts.snyk) {
|
|
468
474
|
logger.fail(`allowlist export currently supports only --snyk. Usage: allowlist export --snyk`);
|
|
469
475
|
process.exit(1);
|
|
470
476
|
}
|
|
477
|
+
// Path-exclusion half of the sync: `.dxkit-ignore` → Snyk
|
|
478
|
+
// `exclude.global`. Independent of allowlist findings, so it's read up
|
|
479
|
+
// front and can carry an export even when no Snyk findings are
|
|
480
|
+
// allowlisted (the two halves compose into one `.snyk`).
|
|
481
|
+
const excludes = readDxkitIgnoreExcludes(cwd);
|
|
471
482
|
const file = (0, file_1.loadAllowlist)(cwd);
|
|
472
|
-
if (!file || file.entries.length === 0) {
|
|
473
|
-
logger.info(`No allowlist entries — nothing to export.`);
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
483
|
const snapshots = (0, snapshot_1.readAllSnapshots)(cwd).filter((f) => f.engine === 'snyk-code');
|
|
477
|
-
if (snapshots.length === 0) {
|
|
478
|
-
logger.info(`No Snyk Code findings have been ingested yet. ` +
|
|
479
|
-
`Run \`vyuh-dxkit ingest --from-snyk\` first.`);
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
484
|
// Recompute each Snyk finding's canonical fingerprint and match it to
|
|
483
485
|
// an active allowlist entry. Dedup (rule, path) so several findings on
|
|
484
486
|
// the same rule+path collapse to one ignore directive.
|
|
@@ -486,48 +488,79 @@ async function runAllowlistExport(cwd, opts) {
|
|
|
486
488
|
const ignores = [];
|
|
487
489
|
const seenRulePath = new Set();
|
|
488
490
|
let skippedExpired = 0;
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
491
|
+
if (file && file.entries.length > 0) {
|
|
492
|
+
for (const f of snapshots) {
|
|
493
|
+
const fingerprint = (0, fingerprint_1.computeCodeFingerprint)((0, fingerprint_1.canonicalRuleFor)(f.engine, f.rule), f.file, f.line);
|
|
494
|
+
const entry = (0, file_1.findEntry)(file, fingerprint);
|
|
495
|
+
if (!entry)
|
|
496
|
+
continue;
|
|
497
|
+
if (!(0, file_1.isEntryActive)(entry)) {
|
|
498
|
+
skippedExpired++;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const key = `${f.rule}\0${f.file}`;
|
|
502
|
+
if (seenRulePath.has(key))
|
|
503
|
+
continue;
|
|
504
|
+
seenRulePath.add(key);
|
|
505
|
+
ignores.push({
|
|
506
|
+
ruleId: f.rule,
|
|
507
|
+
path: f.file,
|
|
508
|
+
reason: entry.reason,
|
|
509
|
+
expires: (0, snyk_policy_1.expiryToSnykDatetime)(entry.expiresAt),
|
|
510
|
+
created,
|
|
511
|
+
});
|
|
497
512
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
513
|
+
}
|
|
514
|
+
// Bail only when there's nothing to act on at all: no usable allowlist
|
|
515
|
+
// context (entries + ingested snapshots to match them against) AND no
|
|
516
|
+
// path exclusions. When an allowlist+snapshots context exists we still
|
|
517
|
+
// write — an empty policy + JSON is meaningful output (preserves the
|
|
518
|
+
// pre-2.10 behavior the export tests pin).
|
|
519
|
+
const hasAllowlistContext = !!file && file.entries.length > 0 && snapshots.length > 0;
|
|
520
|
+
if (!hasAllowlistContext && excludes.length === 0) {
|
|
521
|
+
if (!file || file.entries.length === 0) {
|
|
522
|
+
logger.info(`No allowlist entries and no .dxkit-ignore exclusions — nothing to export.`);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
logger.info(`No Snyk Code findings have been ingested yet and no .dxkit-ignore ` +
|
|
526
|
+
`exclusions are present. Run \`vyuh-dxkit ingest --from-snyk\` first.`);
|
|
527
|
+
}
|
|
528
|
+
return;
|
|
509
529
|
}
|
|
510
530
|
const outPath = path.resolve(cwd, opts.out ?? '.snyk');
|
|
511
|
-
const policy = (0, snyk_policy_1.buildSnykPolicy)(ignores);
|
|
531
|
+
const policy = (0, snyk_policy_1.buildSnykPolicy)(ignores, excludes);
|
|
512
532
|
fs.writeFileSync(outPath, policy, 'utf8');
|
|
513
533
|
if (opts.json) {
|
|
514
|
-
process.stdout.write(JSON.stringify({ out: outPath, ignores: ignores.length, skippedExpired }, null, 2) + '\n');
|
|
534
|
+
process.stdout.write(JSON.stringify({ out: outPath, ignores: ignores.length, excludes: excludes.length, skippedExpired }, null, 2) + '\n');
|
|
515
535
|
return;
|
|
516
536
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
: ''));
|
|
522
|
-
return;
|
|
537
|
+
const parts = [];
|
|
538
|
+
parts.push(`${ignores.length} Snyk ignore${ignores.length === 1 ? '' : 's'}`);
|
|
539
|
+
if (excludes.length > 0) {
|
|
540
|
+
parts.push(`${excludes.length} path exclusion${excludes.length === 1 ? '' : 's'}`);
|
|
523
541
|
}
|
|
524
|
-
logger.success(`Wrote ${
|
|
542
|
+
logger.success(`Wrote ${parts.join(' + ')} to ${outPath}` +
|
|
525
543
|
(skippedExpired > 0 ? ` (${skippedExpired} expired skipped)` : '') +
|
|
526
544
|
'.');
|
|
527
545
|
logger.dim(' Note: Snyk Code (SAST) honors .snyk ignores only with the "consistent ignores" ' +
|
|
528
546
|
'feature enabled for your org; SCA/dependency ignores are standard.');
|
|
529
547
|
}
|
|
530
548
|
// ─── Internals ────────────────────────────────────────────────────────────
|
|
549
|
+
/**
|
|
550
|
+
* Read `.dxkit-ignore` (if present) and convert its patterns into Snyk
|
|
551
|
+
* `exclude.global` globs. Returns [] when the file is absent or
|
|
552
|
+
* unreadable — the exclusion sync is best-effort and never blocks an
|
|
553
|
+
* allowlist export.
|
|
554
|
+
*/
|
|
555
|
+
function readDxkitIgnoreExcludes(cwd) {
|
|
556
|
+
try {
|
|
557
|
+
const raw = fs.readFileSync(path.join(cwd, '.dxkit-ignore'), 'utf8');
|
|
558
|
+
return (0, snyk_policy_1.dxkitIgnoreLinesToSnykExcludes)(raw.split('\n'));
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
}
|
|
531
564
|
function isAllowlistSubcommand(value) {
|
|
532
565
|
return exports.ALLOWLIST_SUBCOMMANDS.includes(value);
|
|
533
566
|
}
|