@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.
Files changed (123) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/README.md +9 -0
  3. package/dist/allowlist/annotate.d.ts +71 -0
  4. package/dist/allowlist/annotate.d.ts.map +1 -0
  5. package/dist/allowlist/annotate.js +105 -0
  6. package/dist/allowlist/annotate.js.map +1 -0
  7. package/dist/allowlist/cli.d.ts +6 -0
  8. package/dist/allowlist/cli.d.ts.map +1 -1
  9. package/dist/allowlist/cli.js +70 -37
  10. package/dist/allowlist/cli.js.map +1 -1
  11. package/dist/analyzers/dashboard/index.d.ts.map +1 -1
  12. package/dist/analyzers/dashboard/index.js +6 -1
  13. package/dist/analyzers/dashboard/index.js.map +1 -1
  14. package/dist/analyzers/developer/gather.d.ts +16 -0
  15. package/dist/analyzers/developer/gather.d.ts.map +1 -1
  16. package/dist/analyzers/developer/gather.js +2 -0
  17. package/dist/analyzers/developer/gather.js.map +1 -1
  18. package/dist/analyzers/developer/ownership.d.ts +86 -0
  19. package/dist/analyzers/developer/ownership.d.ts.map +1 -0
  20. package/dist/analyzers/developer/ownership.js +180 -0
  21. package/dist/analyzers/developer/ownership.js.map +1 -0
  22. package/dist/analyzers/health.d.ts.map +1 -1
  23. package/dist/analyzers/health.js +17 -2
  24. package/dist/analyzers/health.js.map +1 -1
  25. package/dist/analyzers/quality/detailed.d.ts +5 -1
  26. package/dist/analyzers/quality/detailed.d.ts.map +1 -1
  27. package/dist/analyzers/quality/detailed.js +30 -29
  28. package/dist/analyzers/quality/detailed.js.map +1 -1
  29. package/dist/analyzers/security/actions.d.ts.map +1 -1
  30. package/dist/analyzers/security/actions.js +13 -0
  31. package/dist/analyzers/security/actions.js.map +1 -1
  32. package/dist/analyzers/security/aggregator.d.ts +18 -0
  33. package/dist/analyzers/security/aggregator.d.ts.map +1 -1
  34. package/dist/analyzers/security/aggregator.js +28 -0
  35. package/dist/analyzers/security/aggregator.js.map +1 -1
  36. package/dist/analyzers/security/detailed.d.ts +7 -1
  37. package/dist/analyzers/security/detailed.d.ts.map +1 -1
  38. package/dist/analyzers/security/detailed.js +31 -15
  39. package/dist/analyzers/security/detailed.js.map +1 -1
  40. package/dist/analyzers/security/gather.d.ts.map +1 -1
  41. package/dist/analyzers/security/gather.js +6 -0
  42. package/dist/analyzers/security/gather.js.map +1 -1
  43. package/dist/analyzers/security/index.d.ts.map +1 -1
  44. package/dist/analyzers/security/index.js +81 -2
  45. package/dist/analyzers/security/index.js.map +1 -1
  46. package/dist/analyzers/security/scanner-drift.d.ts +21 -0
  47. package/dist/analyzers/security/scanner-drift.d.ts.map +1 -0
  48. package/dist/analyzers/security/scanner-drift.js +113 -0
  49. package/dist/analyzers/security/scanner-drift.js.map +1 -0
  50. package/dist/analyzers/security/shallow.d.ts.map +1 -1
  51. package/dist/analyzers/security/shallow.js +24 -2
  52. package/dist/analyzers/security/shallow.js.map +1 -1
  53. package/dist/analyzers/security/types.d.ts +38 -0
  54. package/dist/analyzers/security/types.d.ts.map +1 -1
  55. package/dist/analyzers/tests/detailed.d.ts +5 -1
  56. package/dist/analyzers/tests/detailed.d.ts.map +1 -1
  57. package/dist/analyzers/tests/detailed.js +27 -20
  58. package/dist/analyzers/tests/detailed.js.map +1 -1
  59. package/dist/analyzers/tools/graphify.d.ts +11 -0
  60. package/dist/analyzers/tools/graphify.d.ts.map +1 -1
  61. package/dist/analyzers/tools/graphify.js +429 -413
  62. package/dist/analyzers/tools/graphify.js.map +1 -1
  63. package/dist/analyzers/tools/grep-secrets.d.ts.map +1 -1
  64. package/dist/analyzers/tools/grep-secrets.js +9 -0
  65. package/dist/analyzers/tools/grep-secrets.js.map +1 -1
  66. package/dist/analyzers/tools/osv-scanner-fix.d.ts.map +1 -1
  67. package/dist/analyzers/tools/osv-scanner-fix.js +12 -1
  68. package/dist/analyzers/tools/osv-scanner-fix.js.map +1 -1
  69. package/dist/analyzers/tools/tool-registry.d.ts.map +1 -1
  70. package/dist/analyzers/tools/tool-registry.js +78 -43
  71. package/dist/analyzers/tools/tool-registry.js.map +1 -1
  72. package/dist/analyzers/tools/walk-source-files.d.ts +10 -0
  73. package/dist/analyzers/tools/walk-source-files.d.ts.map +1 -1
  74. package/dist/analyzers/tools/walk-source-files.js +14 -0
  75. package/dist/analyzers/tools/walk-source-files.js.map +1 -1
  76. package/dist/analyzers/types.d.ts +9 -0
  77. package/dist/analyzers/types.d.ts.map +1 -1
  78. package/dist/attribution/attribute.d.ts +57 -0
  79. package/dist/attribution/attribute.d.ts.map +1 -0
  80. package/dist/attribution/attribute.js +149 -0
  81. package/dist/attribution/attribute.js.map +1 -0
  82. package/dist/baseline/entry-to-located.d.ts +12 -5
  83. package/dist/baseline/entry-to-located.d.ts.map +1 -1
  84. package/dist/baseline/entry-to-located.js +21 -7
  85. package/dist/baseline/entry-to-located.js.map +1 -1
  86. package/dist/baseline/git-aware-match.d.ts +7 -5
  87. package/dist/baseline/git-aware-match.d.ts.map +1 -1
  88. package/dist/baseline/git-aware-match.js +78 -5
  89. package/dist/baseline/git-aware-match.js.map +1 -1
  90. package/dist/cli.d.ts.map +1 -1
  91. package/dist/cli.js +53 -5
  92. package/dist/cli.js.map +1 -1
  93. package/dist/explore/context-hook.d.ts +49 -29
  94. package/dist/explore/context-hook.d.ts.map +1 -1
  95. package/dist/explore/context-hook.js +304 -29
  96. package/dist/explore/context-hook.js.map +1 -1
  97. package/dist/generator.d.ts.map +1 -1
  98. package/dist/generator.js +13 -7
  99. package/dist/generator.js.map +1 -1
  100. package/dist/ingest/snyk-policy.d.ts +22 -1
  101. package/dist/ingest/snyk-policy.d.ts.map +1 -1
  102. package/dist/ingest/snyk-policy.js +75 -18
  103. package/dist/ingest/snyk-policy.js.map +1 -1
  104. package/dist/languages/index.d.ts +28 -5
  105. package/dist/languages/index.d.ts.map +1 -1
  106. package/dist/languages/index.js +38 -7
  107. package/dist/languages/index.js.map +1 -1
  108. package/dist/languages/typescript.d.ts.map +1 -1
  109. package/dist/languages/typescript.js +19 -0
  110. package/dist/languages/typescript.js.map +1 -1
  111. package/dist/reviewers-cli.d.ts +57 -0
  112. package/dist/reviewers-cli.d.ts.map +1 -0
  113. package/dist/reviewers-cli.js +263 -0
  114. package/dist/reviewers-cli.js.map +1 -0
  115. package/dist/scoring/dimensions/security.d.ts +17 -0
  116. package/dist/scoring/dimensions/security.d.ts.map +1 -1
  117. package/dist/scoring/dimensions/security.js +12 -0
  118. package/dist/scoring/dimensions/security.js.map +1 -1
  119. package/package.json +1 -1
  120. package/templates/.claude/skills/dxkit-action/SKILL.md +13 -2
  121. package/templates/.claude/skills/dxkit-allowlist/SKILL.md +9 -0
  122. package/templates/.claude/skills/dxkit-onboard/SKILL.md +2 -2
  123. 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"}
@@ -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":"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"}
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"}
@@ -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
- for (const f of snapshots) {
490
- const fingerprint = (0, fingerprint_1.computeCodeFingerprint)((0, fingerprint_1.canonicalRuleFor)(f.engine, f.rule), f.file, f.line);
491
- const entry = (0, file_1.findEntry)(file, fingerprint);
492
- if (!entry)
493
- continue;
494
- if (!(0, file_1.isEntryActive)(entry)) {
495
- skippedExpired++;
496
- continue;
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
- const key = `${f.rule}\0${f.file}`;
499
- if (seenRulePath.has(key))
500
- continue;
501
- seenRulePath.add(key);
502
- ignores.push({
503
- ruleId: f.rule,
504
- path: f.file,
505
- reason: entry.reason,
506
- expires: (0, snyk_policy_1.expiryToSnykDatetime)(entry.expiresAt),
507
- created,
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
- if (ignores.length === 0) {
518
- logger.info(`No Snyk-originated findings are allowlisted wrote an empty policy to ${outPath}.` +
519
- (skippedExpired > 0
520
- ? ` (${skippedExpired} expired entr${skippedExpired === 1 ? 'y' : 'ies'} skipped.)`
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 ${ignores.length} Snyk ignore${ignores.length === 1 ? '' : 's'} to ${outPath}` +
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
  }