fallow 2.89.0 → 2.90.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow",
3
- "version": "2.89.0",
3
+ "version": "2.90.0",
4
4
  "description": "Deterministic codebase intelligence for TypeScript and JavaScript. Quality, risk, architecture, dependencies, duplication, and safe cleanup evidence for humans, CI, and agents. Optional runtime intelligence layer (Fallow Runtime) adds production execution evidence. Rust-native, sub-second, 96 framework plugins.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -83,13 +83,13 @@
83
83
  "@tanstack/intent": "0.0.41"
84
84
  },
85
85
  "optionalDependencies": {
86
- "@fallow-cli/darwin-arm64": "2.89.0",
87
- "@fallow-cli/darwin-x64": "2.89.0",
88
- "@fallow-cli/linux-x64-gnu": "2.89.0",
89
- "@fallow-cli/linux-arm64-gnu": "2.89.0",
90
- "@fallow-cli/linux-x64-musl": "2.89.0",
91
- "@fallow-cli/linux-arm64-musl": "2.89.0",
92
- "@fallow-cli/win32-arm64-msvc": "2.89.0",
93
- "@fallow-cli/win32-x64-msvc": "2.89.0"
86
+ "@fallow-cli/darwin-arm64": "2.90.0",
87
+ "@fallow-cli/darwin-x64": "2.90.0",
88
+ "@fallow-cli/linux-x64-gnu": "2.90.0",
89
+ "@fallow-cli/linux-arm64-gnu": "2.90.0",
90
+ "@fallow-cli/linux-x64-musl": "2.90.0",
91
+ "@fallow-cli/linux-arm64-musl": "2.90.0",
92
+ "@fallow-cli/win32-arm64-msvc": "2.90.0",
93
+ "@fallow-cli/win32-x64-msvc": "2.90.0"
94
94
  }
95
95
  }
package/schema.json CHANGED
@@ -1188,11 +1188,11 @@
1188
1188
  ]
1189
1189
  },
1190
1190
  "SecurityConfig": {
1191
- "description": "Scopes the data-driven security matcher catalogue used by `fallow security`.\nAn absent block (or both `include`/`exclude` unset) admits every category.",
1191
+ "description": "Scopes the security categories used by `fallow security`. An absent block\nadmits every catalogue category. `hardcoded-secret` is include-required and\nonly runs when explicitly listed in `security.categories.include`.",
1192
1192
  "type": "object",
1193
1193
  "properties": {
1194
1194
  "categories": {
1195
- "description": "Include/exclude filter over catalogue category ids (e.g. `dangerous-html`).",
1195
+ "description": "Include/exclude filter over category ids (e.g. `dangerous-html`).",
1196
1196
  "anyOf": [
1197
1197
  {
1198
1198
  "$ref": "#/$defs/SecurityCategories"
@@ -1206,7 +1206,7 @@
1206
1206
  "additionalProperties": false
1207
1207
  },
1208
1208
  "SecurityCategories": {
1209
- "description": "Include/exclude lists scoping the active security matcher categories. When\n`include` is set, only those categories are active; `exclude` removes\ncategories from the admitted set. Both unset admits every category.",
1209
+ "description": "Include/exclude lists scoping the active security categories. When `include`\nis set, only those categories are active; `exclude` removes categories from\nthe admitted set. Both unset admits catalogue categories. `hardcoded-secret`\nstill requires explicit inclusion.",
1210
1210
  "type": "object",
1211
1211
  "properties": {
1212
1212
  "include": {
@@ -1762,7 +1762,15 @@
1762
1762
  "CacheConfig": {
1763
1763
  "type": "object",
1764
1764
  "properties": {
1765
+ "dir": {
1766
+ "description": "Directory for fallow's persistent analysis cache. Relative paths resolve\nfrom the project root.",
1767
+ "type": [
1768
+ "string",
1769
+ "null"
1770
+ ]
1771
+ },
1765
1772
  "maxSizeMb": {
1773
+ "description": "Maximum size of the persistent extraction cache, in megabytes.",
1766
1774
  "type": [
1767
1775
  "integer",
1768
1776
  "null"
@@ -75,7 +75,7 @@ cargo install fallow-cli # build from source
75
75
  | `health` | Function complexity analysis (also covers Angular templates as synthetic `<template>` findings: external `.html` files via `templateUrl` AND inline `@Component({ template: \`...\` })` literals; suppress external with `<!-- fallow-ignore-file complexity -->` at the top of the `.html` file, suppress inline with `// fallow-ignore-next-line complexity` directly above the `@Component` decorator) | `--complexity`, `--max-cyclomatic`, `--max-cognitive`, `--max-crap`, `--top`, `--sort`, `--file-scores`, `--hotspots`, `--ownership`, `--ownership-emails`, `--targets`, `--effort`, `--score`, `--min-score`, `--since`, `--min-commits`, `--save-snapshot`, `--trend`, `--coverage-gaps`, `--coverage`, `--coverage-root`, `--runtime-coverage`, `--min-invocations-hot`, `--min-observation-volume`, `--low-traffic-threshold`, `--workspace`, `--changed-workspaces`, `--baseline`, `--save-baseline` |
76
76
  | `audit` | Combined dead-code + complexity + duplication for changed files | `--base`, `--gate`, `--production`, `--production-dead-code`, `--production-health`, `--production-dupes`, `--workspace`, `--changed-workspaces`, `--ci`, `--fail-on-issues`, `--explain`, `--explain-skipped`, `--dead-code-baseline`, `--health-baseline`, `--dupes-baseline`, `--max-crap`, `--coverage`, `--coverage-root`, `--include-entry-exports` |
77
77
  | `flags` | Detect feature flag patterns (env vars, SDK calls, config objects) | `--top` |
78
- | `security` | Surface opt-in local security candidates for agent verification (not confirmed vulnerabilities). Two rule families: the graph rule `client-server-leak` (a `"use client"` file reaching a non-public `process.env` secret) and a data-driven `tainted-sink` catalogue across 9 CWE categories (dangerous-html, command-injection, code-injection, sql-injection, ssrf, path-traversal, open-redirect, weak-crypto, unsafe-deserialization). Conservative non-literal trigger; parameterized SQL not flagged. Rules default off; suppress a file with `// fallow-ignore-file security-sink`; scope categories with `security.categories`. | `--format human|json|sarif`, `--changed-since`, `--diff-file`, `--workspace`, `--changed-workspaces`, `--ci`, `--fail-on-issues`, `--sarif-file`, `--summary` |
78
+ | `security` | Surface opt-in local security candidates for agent verification (not confirmed vulnerabilities). Rule families include the graph rule `client-server-leak`, a data-driven `tainted-sink` catalogue, and the include-required `hardcoded-secret` category for provider-prefix credentials and high-entropy literals assigned to secret-shaped identifiers. Most catalogue rows require non-literal input; narrowly literal-aware rows flag deterministic unsafe literals. Rules default off; suppress a file with `// fallow-ignore-file security-sink`; scope categories with `security.categories`. `hardcoded-secret` runs only when listed in `security.categories.include`. | `--format human|json|sarif`, `--changed-since`, `--file`, `--diff-file`, `--workspace`, `--changed-workspaces`, `--ci`, `--fail-on-issues`, `--sarif-file`, `--summary` |
79
79
  | `explain` | Explain one issue type without running analysis | `<issue-type>`, `--format json` |
80
80
  | `license` | Manage the local license JWT for continuous/cloud runtime monitoring (activate, status, refresh, deactivate) | `activate --trial --email <addr>`, `activate --from-file`, `activate --stdin`, `status`, `refresh`, `deactivate` |
81
81
  | `telemetry` | Manage opt-in, off-by-default product telemetry (never collects code, paths, or names). Agents must not enable it; only the user may | `status`, `enable`, `disable`, `inspect --example` |
@@ -118,7 +118,7 @@ When using fallow via MCP (`fallow-mcp`), the following tools are available:
118
118
  |------|-------------|
119
119
  | `analyze` | Full dead code analysis (unused files/exports/types/dependencies/members + circular dependencies + re-export cycles (barrel files that form a structural loop, silently breaking re-exports) + boundary violations + stale suppressions). Private type leaks are an opt-in API hygiene check via `issue_types: ["private-type-leaks"]`. Set `boundary_violations: true` as a convenience alias for `issue_types: ["boundary-violations"]`. Set `group_by` to `"owner"`, `"directory"`, `"package"`, or `"section"` to partition results. The `section` mode reads GitLab CODEOWNERS `[Section]` headers and emits `owners` metadata per group |
120
120
  | `check_changed` | Incremental analysis of files changed since a git ref |
121
- | `security_candidates` | Unverified local security candidates, not confirmed vulnerabilities (`fallow security --format json`). Read `security_findings[]` for category, CWE, evidence, and trace; verify trace and evidence before editing code. Supports `root`, `config`, `workspace`, `changed_since`, `changed_workspaces`, `no_cache`, and `threads`; inherits `FALLOW_DIFF_FILE` from the server environment for line-level diff scoping; raise `FALLOW_TIMEOUT_SECS` for large repos. |
121
+ | `security_candidates` | Unverified local security candidates, not confirmed vulnerabilities (`fallow security --format json`). Read `security_findings[]` for category, CWE, evidence, trace, optional `reachability`, and blind-spot counters. Each finding also carries an agent-actionable `candidate` (`source_kind`/`sink`/`boundary`), an optional `taint_flow` source-to-sink triple, and a stable `finding_id` (equal to the SARIF fingerprint) for cross-run correlation; there is no `impact` field (deciding exploitability is the agent's job). `reachability.untrusted_source_trace` is module-level import context only and does not prove value flow. Verify trace, reachability context, and evidence before editing code. Supports `root`, `config`, `workspace`, `paths`, `changed_since`, `changed_workspaces`, `no_cache`, and `threads`; `paths` forwards repeated `fallow security --file` filters for finding anchors, trace hops, or untrusted-source reachability trace hops. Inherits `FALLOW_DIFF_FILE` from the server environment for line-level diff scoping; raise `FALLOW_TIMEOUT_SECS` for large repos. |
122
122
  | `find_dupes` | Code duplication detection. Set `changed_since` to scope to changed files since a git ref. Set `min_occurrences` (≥ 2, default 2) to hide pair-only clones and focus on widespread copy-paste; JSON gains `stats.clone_groups_below_min_occurrences` when the filter hides anything. Each `clone_groups[]` entry carries a stable `fingerprint`, usually `dup:<8hex>` and widened only on rare report collisions; pass it to `trace_clone` to deep-dive that group |
123
123
  | `fix_preview` | Dry-run auto-fix preview |
124
124
  | `fix_apply` | Apply auto-fixes (destructive) |
@@ -366,6 +366,8 @@ Config fields:
366
366
  - `ignoreExportsUsedInFile`: knip-compatible; suppress unused-export findings when the exported symbol is referenced inside the file that declares it. Boolean (`true` covers all kinds) or `{ "type": true, "interface": true }` object form for knip parity. Fallow groups type aliases and interfaces under the same `unused-types` issue, so both type-kind fields behave identically. References inside the export specifier itself (`export { foo }`, `export default foo`) do not count as same-file uses; those exports are still reported when no other in-file expression references the binding
367
367
  - `publicPackages`: workspace packages that are public libraries; exported API surface from these packages is not flagged as unused
368
368
  - `dynamicallyLoaded`: glob patterns for files loaded at runtime (plugin dirs, locale files); treated as always-used
369
+ - `cache.dir`: override the persistent extraction cache directory. `FALLOW_CACHE_DIR` wins over this config field, and `--no-cache` disables caching entirely
370
+ - `cache.maxSizeMb`: cap the serialized extraction cache size in megabytes. `FALLOW_CACHE_MAX_SIZE` wins over this config field
369
371
  - `usedClassMembers`: class method/property names that extend the built-in Angular/React lifecycle allowlist with framework-invoked names. Each entry is a plain string (global suppression) or a scoped object `{ extends?, implements?, members }` matching only classes with the given heritage. Strings can be exact names (`"agInit"`) or glob patterns (`"*"` matches every member, `"enter*"` prefix, `"*Handler"` suffix, `"on*Event"` combined). Use scoped rules for common names like `refresh` or `execute` to avoid false negatives on unrelated classes; global strings for unique names like `agInit`. Example: `["agInit", { "implements": "ICellRendererAngularComp", "members": ["refresh"] }, { "extends": "BaseCommand", "members": ["execute"] }, { "extends": "GrammarBaseListener", "members": ["enter*", "exit*"] }]`. Glob patterns that match zero members emit a `WARN` so dead allowlist entries surface. An unconstrained scoped rule (no `extends` or `implements`) is rejected at load time. Use plugin-level `usedClassMembers` in a `.fallow/plugins/*.jsonc` file for library-specific allowlists
370
372
  - `resolve.conditions`: additional package.json `exports` / `imports` condition names to honor during module resolution. Baseline conditions (`development`, `import`, `require`, `default`, `types`, `node`, plus `react-native` / `browser` under RN/Expo) are always included; user entries prepend ahead of them. Use for community conditions like `worker`, `edge-light`, `deno`, or custom bundler conditions. Example: `{ "resolve": { "conditions": ["worker", "edge-light"] } }`
371
373
 
@@ -40,9 +40,11 @@ Analyzes the project for unused files, exports, dependencies, types, members, an
40
40
  |------|------|---------|-------------|
41
41
  | `--format` | `human\|json\|sarif\|compact\|markdown\|codeclimate\|gitlab-codequality\|pr-comment-github\|pr-comment-gitlab\|review-github\|review-gitlab` | `human` | Output format |
42
42
  | `--quiet` | bool | `false` | Suppress progress bars and timing on stderr |
43
+ | `-o, --output-file` | path | (none) | Write the report to a file instead of stdout, for any `--format` (no ANSI codes). Progress and a confirmation stay on stderr (suppressed by `--quiet`). Valid with dead-code/dupes/health/security/bare; composes with `--sarif-file`. |
43
44
  | `--legacy-envelope` | bool | `false` | Remove the top-level `kind` field from typed JSON roots for one migration cycle |
44
45
  | `--changed-since` | string | — | Only analyze files changed since a git ref (e.g., `main`, `HEAD~3`) |
45
46
  | `--production` | bool | `false` | Exclude test/dev files, only start/build scripts (applies to every analysis) |
47
+ | `--no-production` | bool | `false` | Force production mode off, overriding a project config's `production: true` (applies to every analysis; conflicts with `--production`) |
46
48
  | `--production-dead-code` | bool | `false` | Per-analysis production mode for dead-code. Bare combined runs and `fallow audit` only. |
47
49
  | `--production-health` | bool | `false` | Per-analysis production mode for health. Bare combined runs and `fallow audit` only. |
48
50
  | `--production-dupes` | bool | `false` | Per-analysis production mode for duplication. Bare combined runs and `fallow audit` only. |
@@ -234,7 +236,7 @@ Auto-removes unused exports, dependencies, enum members, and pnpm catalog entrie
234
236
 
235
237
  ### On-disk drift protection
236
238
 
237
- `fallow fix` captures every parsed source file's xxh3 content hash during the in-process analysis and recomputes it at fix time. Files whose hash drifted between analysis and write (parallel editor save, CI rebase, concurrent tool) are skipped with `{"type": "skipped", "path": "...", "skipped": true, "skip_reason": "content_changed"}` in the JSON output and `Skipping <path>: file content changed since fallow check ran. Re-run fallow fix to refresh the analysis first.` on stderr (gated on non-quiet). A run with any content-changed skip exits with code 2 so CI does not treat the partial run as a clean no-op. The JSON envelope's top-level `skipped_content_changed: number` is always present and disjoint from `skipped` (which still tallies catalog / YAML guard skips only). Per-file writes are batched: each rewrite is staged to a sibling temp file, and the orchestrator promotes the batch only after every stage succeeds. A stage failure leaves every target file at its original content. Hash precondition covers source files (TS, JS, Vue, Svelte, Astro, MDX); `package.json` and `pnpm-workspace.yaml` are not in the captured hash map because the extract layer does not parse them, but the dep and catalog fixers re-parse those files at fix time as the natural safety net.
239
+ `fallow fix` captures every parsed source file's xxh3 content hash during the in-process analysis and recomputes it at fix time. Files whose hash drifted between analysis and write (parallel editor save, CI rebase, concurrent tool) are skipped with `{"type": "skipped", "path": "...", "skipped": true, "skip_reason": "content_changed"}` in the JSON output and `Skipping <path>: file content changed since fallow dead-code ran. Re-run fallow fix to refresh the analysis first.` on stderr (gated on non-quiet). A run with any content-changed skip exits with code 2 so CI does not treat the partial run as a clean no-op. The JSON envelope's top-level `skipped_content_changed: number` is always present and disjoint from `skipped` (which still tallies catalog / YAML guard skips only). Per-file writes are batched: each rewrite is staged to a sibling temp file, and the orchestrator promotes the batch only after every stage succeeds. A stage failure leaves every target file at its original content. Hash precondition covers source files (TS, JS, Vue, Svelte, Astro, MDX); `package.json` and `pnpm-workspace.yaml` are not in the captured hash map because the extract layer does not parse them, but the dep and catalog fixers re-parse those files at fix time as the natural safety net.
238
240
 
239
241
  ### Low-confidence export removals
240
242
 
@@ -243,7 +245,7 @@ Issue #602: `fallow fix` withholds unused-export removals when the consumer may
243
245
  - **Off-graph consumer directories.** The file is under any of `__mocks__`, `__fixtures__`, `fixtures`, `e2e`, `e2e-tests`, `cypress`, `playwright`, `examples`, `evals`, `golden` (matched on any path segment). Catches Vitest mock aliases, off-workspace e2e suites, and fixture / golden harnesses. Plain `test` / `tests` / `__tests__` are deliberately NOT on the list, so genuinely-dead test helpers still auto-remove.
244
246
  - **Files with an unresolved import.** The file itself imports something fallow could not resolve, so its local usage graph is incomplete.
245
247
 
246
- JSON output carries `{"type": "skipped", "path": "...", "skipped": true, "skip_reason": "low_confidence_off_graph"}` (or `"low_confidence_unresolved_imports"`) plus a top-level counter `skipped_low_confidence_exports: number` (always present), disjoint from `skipped`. Unlike the drift and encoding skips this is INTENTIONAL and does NOT change the exit code; the export stays reported by `fallow check` for manual review. High-confidence exports in normal source files are removed unchanged. The AI agent should report kept exports to the user and let them decide whether the export is truly unused before removing it by hand.
248
+ JSON output carries `{"type": "skipped", "path": "...", "skipped": true, "skip_reason": "low_confidence_off_graph"}` (or `"low_confidence_unresolved_imports"`) plus a top-level counter `skipped_low_confidence_exports: number` (always present), disjoint from `skipped`. Unlike the drift and encoding skips this is INTENTIONAL and does NOT change the exit code; the export stays reported by `fallow dead-code` for manual review. High-confidence exports in normal source files are removed unchanged. The AI agent should report kept exports to the user and let them decide whether the export is truly unused before removing it by hand.
247
249
 
248
250
  ### File encoding contract
249
251
 
@@ -294,7 +296,7 @@ fallow list --workspaces --format json --quiet
294
296
  fallow workspaces --format json --quiet # alias of `fallow list --workspaces`
295
297
  ```
296
298
 
297
- The `--workspaces` JSON output carries `workspaces[]` (name, project-root-relative path, `is_internal_dependency` bool) plus `workspace_diagnostics[]`. Each diagnostic has a `kind` discriminator (`undeclared-workspace`, `malformed-package-json`, `glob-matched-no-package-json`, `malformed-tsconfig`, `tsconfig-reference-dir-missing`) with a typed payload (`error`, `pattern`, or none). The same `workspace_diagnostics[]` array is also surfaced on `fallow check --format json`, `fallow dupes --format json`, and `fallow health --format json` envelopes (omitted when empty). A malformed ROOT `package.json` exits 2 at config load; everything else warns and continues.
299
+ The `--workspaces` JSON output carries `workspaces[]` (name, project-root-relative path, `is_internal_dependency` bool) plus `workspace_diagnostics[]`. Each diagnostic has a `kind` discriminator (`undeclared-workspace`, `malformed-package-json`, `glob-matched-no-package-json`, `malformed-tsconfig`, `tsconfig-reference-dir-missing`) with a typed payload (`error`, `pattern`, or none). The same `workspace_diagnostics[]` array is also surfaced on `fallow dead-code --format json`, `fallow dupes --format json`, and `fallow health --format json` envelopes (omitted when empty). A malformed ROOT `package.json` exits 2 at config load; everything else warns and continues.
298
300
 
299
301
  The `--boundaries` JSON output carries `boundaries.logical_groups[]` alongside the existing `zones[]` / `rules[]` arrays. Each logical-group entry surfaces a user-authored `autoDiscover` parent zone (which expansion otherwise flattens into per-child zones like `features/auth` / `features/billing`): `name`, `children`, `auto_discover` (verbatim user strings), `status` (`ok` / `empty` / `invalid_path`), `source_zone_index`, summed `file_count`, optional `authored_rule` (the pre-expansion `{ allow, allowTypeOnly }` keyed on the parent), optional `fallback_zone` cross-reference when the parent also kept its own `patterns` (Bulletproof case), optional `merged_from` (parent zone indices when the user declared the same parent name twice; surfaces the duplicate in JSON instead of only in `tracing::warn!`), optional `original_zone_root` (echo of the parent's `root` subtree scope for monorepo patchers), and optional `child_source_indices` (parallel to `children`, attributing each child to a specific `auto_discover` entry when multiple paths were authored). The full shape is documented in `docs/output-schema.json` under `ListBoundariesOutput`.
300
302
 
@@ -495,7 +497,7 @@ fallow health --format json --quiet --trend
495
497
  {
496
498
  "kind": "health",
497
499
  "schema_version": 7,
498
- "version": "2.89.0",
500
+ "version": "2.90.0",
499
501
  "elapsed_ms": 32,
500
502
  "summary": {
501
503
  "files_analyzed": 482,
@@ -805,6 +807,7 @@ Audits changed files for dead code, complexity, and duplication. Returns a verdi
805
807
  | `--base` | string | auto-detect | Git ref to compare against (alias for `--changed-since`) |
806
808
  | `--gate` | `new-only\|all` | `new-only` | Which findings affect the verdict. `new-only` gates only introduced findings; `all` gates every finding in changed files and skips the extra base-snapshot attribution pass. |
807
809
  | `--production` | bool | false | Exclude test/story/dev files (applies to dead-code, health, and dupes) |
810
+ | `--no-production` | bool | false | Force production mode off, overriding a project config's `production: true` (conflicts with `--production`) |
808
811
  | `--production-dead-code` | bool | false | Per-analysis production mode for the dead-code sub-analysis only |
809
812
  | `--production-health` | bool | false | Per-analysis production mode for the health sub-analysis only |
810
813
  | `--production-dupes` | bool | false | Per-analysis production mode for the duplication sub-analysis only |
@@ -885,7 +888,7 @@ fallow audit \
885
888
  {
886
889
  "kind": "audit",
887
890
  "schema_version": 7,
888
- "version": "2.89.0",
891
+ "version": "2.90.0",
889
892
  "command": "audit",
890
893
  "verdict": "fail",
891
894
  "changed_files_count": 12,
@@ -958,7 +961,7 @@ fallow flags --format json --quiet --workspace my-package
958
961
  ```json
959
962
  {
960
963
  "schema_version": 7,
961
- "version": "2.89.0",
964
+ "version": "2.90.0",
962
965
  "elapsed_ms": 116,
963
966
  "feature_flags": [],
964
967
  "total_flags": 0
@@ -973,21 +976,57 @@ Surfaces local security candidates for agent or human verification. The first ru
973
976
 
974
977
  Findings are not confirmed vulnerabilities. Use the structural trace to verify whether the value can actually reach client-bundled code. Public env conventions (`NODE_ENV`, `NEXT_PUBLIC_*`, `VITE_*`, `NUXT_PUBLIC_*`, `REACT_APP_*`, `PUBLIC_*`, `GATSBY_*`, `EXPO_PUBLIC_*`, `STORYBOOK_*`) are excluded.
975
978
 
976
- The second rule family is a data-driven `tainted-sink` catalogue: syntactic dangerous-sink candidates across 9 CWE categories. A candidate fires only when the relevant argument is non-literal, so a fully-literal value (`el.innerHTML = "<b>x</b>"`, `child_process.exec("ls")`) never fires; fallow prefers false-negatives over false-positives.
979
+ The second rule family is a data-driven `tainted-sink` catalogue: syntactic dangerous-sink candidates across the catalogue categories listed below. Most rows require a non-literal argument; narrowly literal-aware rows flag deterministic unsafe literals such as wildcard `postMessage` origins, weak crypto algorithms, disabled TLS validation, and JWT algorithm issues. Fallow prefers false-negatives over false-positives.
977
980
 
978
981
  | Category | CWE | Sink |
979
982
  |----------|-----|------|
980
983
  | `dangerous-html` | 79 | `innerHTML` / `outerHTML` / `insertAdjacentHTML` / `dangerouslySetInnerHTML` |
984
+ | `template-escape-bypass` | 79 | template-engine `SafeString(...)` wrapping a non-literal value |
981
985
  | `command-injection` | 78 | `child_process` `exec` / `execSync` / `spawn` / `spawnSync` (provenance-gated to `node:child_process`) |
982
986
  | `code-injection` | 94 | `eval` / `vm.runInNewContext` |
987
+ | `dynamic-regex` | 1333 | `RegExp(...)` / `new RegExp(...)` with a non-literal pattern |
988
+ | `redos-regex` | 1333 | vulnerable regex literals tested with source-backed input |
989
+ | `resource-amplification` | 400 | source-backed size into `Array(...)` / `new Array(...)` / `Buffer.alloc*` / `String.prototype.repeat` / `padStart` / `padEnd` (directly `Math.min`-clamped sizes stay quiet) |
990
+ | `dynamic-module-load` | 95 | dynamic `require(...)` |
983
991
  | `sql-injection` | 89 | string concat or interpolated template into `.query()` / `.execute()`, and `sql.raw(...)`. Parameterized `` sql`${x}` `` and the object form `.execute({ sql, args })` are NOT flagged |
984
- | `ssrf` | 918 | `fetch` / `axios` / `http(s).request` |
985
- | `path-traversal` | 22 | `fs.*` / `path.join` / `path.resolve` |
986
- | `open-redirect` | 601 | `res.redirect` |
992
+ | `ssrf` | 918 | `fetch` / `got` / `ky` / `needle` / `request` / `axios` / `superagent` / `undici` / `http(s).request` |
993
+ | `path-traversal` | 22 | `path.join` / `path.resolve` / `node:fs` path methods / route `sendFile` |
994
+ | `header-injection` | 113 | response `setHeader` / `writeHead` |
995
+ | `open-redirect` | 601 | `res.redirect` / `location.href` / `location.assign` / `window.open` |
996
+ | `postmessage-wildcard-origin` | 346 | `postMessage(..., "*")` |
997
+ | `tls-validation-disabled` | 295 | HTTPS/TLS options with `rejectUnauthorized: false`, plus `NODE_TLS_REJECT_UNAUTHORIZED = "0"` |
998
+ | `cleartext-transport` | 319 | cleartext `http://` URLs in fetch-like calls and WebSocket constructors |
999
+ | `electron-unsafe-webpreferences` | 1188 | Electron `webPreferences` with unsafe literal options |
1000
+ | `world-writable-permission` | 732 | `chmod` / `chmodSync` with world-writable modes |
1001
+ | `insecure-temp-file` | 377 | predictable temporary file paths in `fs` writes |
1002
+ | `mysql-multiple-statements` | 89 | MySQL connection options with `multipleStatements: true` |
1003
+ | `permissive-cors` | 942 | CORS wildcard origin with credentials |
1004
+ | `insecure-cookie` | 614 | cookie options missing or disabling `httpOnly` / `secure` |
1005
+ | `mass-assignment` | 915 | source-backed `Object.assign(target, source)` |
987
1006
  | `weak-crypto` | 327 | runtime-selectable hash / cipher algorithm |
1007
+ | `deprecated-cipher` | 327 | `crypto.createCipher` / `createDecipher` |
1008
+ | `insecure-randomness` | 338 | `crypto.pseudoRandomBytes(...)` and token-like `Math.random()` use |
1009
+ | `jwt-alg-none` | 347 | JWT signing with algorithm `none` |
1010
+ | `jwt-verify-missing-algorithms` | 347 | `jsonwebtoken` verify calls missing an `algorithms` allowlist |
1011
+ | `unsafe-buffer-alloc` | 1188 | `Buffer.allocUnsafe` / `allocUnsafeSlow` |
988
1012
  | `unsafe-deserialization` | 502 | `js-yaml` `load` / `node-serialize` |
989
-
990
- Build-config and test files are excluded from candidate generation. Both rule families default to `off` and are surfaced only by `fallow security`, never under bare `fallow` or the `audit` gate. Scope which catalogue categories run with `security.categories` include / exclude lists in config.
1013
+ | `angular-trusted-html` | 79 | Angular `bypassSecurityTrust*` |
1014
+ | `nextjs-open-redirect` | 601 | Next.js `redirect` / `permanentRedirect` |
1015
+ | `dom-document-write` | 79 | `document.write` / `document.writeln` |
1016
+ | `jquery-html` | 79 | jQuery `.html(value)` |
1017
+ | `route-send-file` | 22 | Express / Fastify / Hono route `sendFile` |
1018
+ | `webview-injection` | 94 | react-native-webview injected JavaScript |
1019
+ | `prototype-pollution` | 1321 | `__proto__` writes and recursive merge sources |
1020
+ | `zip-slip` | 22 | archive extraction destination paths |
1021
+ | `nosql-injection` | 943 | Mongo / Mongoose query object passthrough |
1022
+ | `ssti` | 1336 | template engine compile / render calls |
1023
+ | `xxe` | 611 | XML parse calls |
1024
+ | `secret-pii-log` | 532 | source-backed secrets or request PII reaching logs |
1025
+ | `hardcoded-secret` | 798 | provider-prefix credentials and high-entropy literals assigned to secret-shaped identifiers (include-required) |
1026
+ | `secret-to-network` | 201 | a non-public `process.env` / `import.meta.env` secret reaching a network call body (`fetch` / `axios` / `got` / ...) via same-identifier flow (include-required) |
1027
+ | `xpath-injection` | 643 | `xpath.select` / `select1` with a non-literal expression |
1028
+
1029
+ Build-config and test files are excluded from candidate generation. Security rule families default to `off` and are surfaced only by `fallow security`, never under bare `fallow` or the `audit` gate. Scope which catalogue categories run with `security.categories` include / exclude lists in config. `hardcoded-secret` and `secret-to-network` are intentionally include-required and only run when listed in `security.categories.include` (`secret-to-network` is opt-in because legitimate auth is also a secret reaching a network call). Public-by-convention env vars (`NEXT_PUBLIC_`, `VITE_`, ...) are never treated as secrets.
991
1030
 
992
1031
  ### Flags
993
1032
 
@@ -1000,7 +1039,10 @@ Build-config and test files are excluded from candidate generation. Both rule fa
1000
1039
  | `--fail-on-issues` | bool | `false` | Exit 1 when candidates are found |
1001
1040
  | `--sarif-file` | path | none | Write SARIF in addition to the primary output |
1002
1041
  | `--changed-since` | git ref | none | Scope to candidates whose client anchor or trace hops touch changed files |
1042
+ | `--file` | path, repeatable | none | Scope output to candidates whose finding anchor or trace hop matches the selected file. The full graph is still analyzed |
1003
1043
  | `--diff-file` | path | none | Scope candidates to added hunks on the client anchor or import trace. Secret-source hops use file-level retention because member-access spans are not yet stored. Use `-` for stdin |
1044
+ | `--diff-stdin` | bool | `false` | Read the unified diff from stdin (equivalent to `--diff-file -`) for line-level scoping and the regression gate |
1045
+ | `--gate` | `new` | none | Fail (exit code **8**) only when the change introduces a NEW security-sink candidate in the changed lines, not on the whole candidate backlog. Requires a diff source (`--changed-since`, `--diff-file`, or `--diff-stdin`); a diff the gate cannot compute is a loud exit 2, never a green gate. Human output says `REVIEW REQUIRED` (not `FAIL`); SARIF keeps every result at `level: note` with the verdict in `run.properties.fallowGate`; `--format json` carries an additive `gate` block (`mode` / `verdict` / `new_count`) |
1004
1046
  | `--workspace` | string | none | Scope to selected workspace packages |
1005
1047
  | `--changed-workspaces` | git ref | none | Scope to workspaces changed since a git ref |
1006
1048
 
@@ -1010,6 +1052,9 @@ Build-config and test files are excluded from candidate generation. Both rule fa
1010
1052
  fallow security --format json --quiet
1011
1053
  fallow security --ci --sarif-file fallow-security.sarif
1012
1054
  git diff --unified=0 origin/main...HEAD | fallow security --diff-file -
1055
+ # Regression gate: fail (exit 8) only on candidates introduced in the changed lines
1056
+ fallow security --gate new --changed-since origin/main
1057
+ git diff --cached --unified=0 | fallow security --gate new --diff-stdin
1013
1058
  ```
1014
1059
 
1015
1060
  ### JSON Output Structure
@@ -1024,7 +1069,17 @@ git diff --unified=0 origin/main...HEAD | fallow security --diff-file -
1024
1069
  }
1025
1070
  ```
1026
1071
 
1027
- Each finding includes `kind`, `path`, `line`, `col`, `evidence`, `trace`, and `actions`. `tainted-sink` findings additionally carry `category` (the catalogue id, e.g. `"dangerous-html"`) and `cwe`; `client-server-leak` findings omit both. `unresolved_edge_files` (client-server-leak) and `unresolved_callee_sites` (tainted-sink) are in-band blind-spot counters: a zero finding count with a non-zero counter is not a clean bill. Suppress a verified false positive with `// fallow-ignore-file security-client-server-leak` (client-server-leak) or `// fallow-ignore-file security-sink` (any tainted-sink category).
1072
+ Each finding includes `kind`, `path`, `line`, `col`, `evidence`, `trace`, `actions`, and optional `reachability`. `tainted-sink` findings additionally carry `category` (the catalogue id, e.g. `"dangerous-html"`) and `cwe`; `client-server-leak` findings omit both. `tainted-sink` findings can also include `reachability.untrusted_source_trace` when a module with a known untrusted source imports the sink module; it is ranking and triage context only, not proof that a specific value reaches the sink. `unresolved_edge_files` (client-server-leak) and `unresolved_callee_sites` (tainted-sink) are in-band blind-spot counters: a zero finding count with a non-zero counter is not a clean bill. Suppress a verified false positive with `// fallow-ignore-file security-client-server-leak` (client-server-leak) or `// fallow-ignore-file security-sink` (any tainted-sink category).
1073
+
1074
+ Every finding also carries an agent-actionable `candidate { source_kind, sink, boundary }`, an optional `taint_flow { source, sink, path }`, and a stable `finding_id`:
1075
+
1076
+ - `candidate.source_kind`: the untrusted-input kind that reaches the sink, as a stable catalogue id (`"http-request-input"`, `"process-env"`, `"process-argv"`, `"message-event-data"`, `"location-input"`, ...). Absent when no source matched (always absent for `client-server-leak`). Treat an unknown id as an untrusted source of unknown kind; never drop the candidate on that basis.
1077
+ - `candidate.sink`: a self-contained sink (`path`, `line`, `col`, `category`, `cwe`, `callee`), actionable without reading the rest of the finding.
1078
+ - `candidate.boundary`: `client_server` (a `"use client"` file in the trace), `cross_module` (the source reaches the sink across import hops), and optional `architecture_zone` (`from`/`to`) when the anchor also crosses a declared architecture boundary.
1079
+ - `candidate.network`: present only on `secret-to-network` (#890) candidates. `destination` is the network call's URL when it is a static literal (usually intended auth) or absent when the destination is dynamic (the higher-signal exfil case). Use it to triage exfil from intended auth without re-reading source.
1080
+ - There is no `impact` field: deciding exploitability is the verifying agent's job.
1081
+ - `taint_flow`: present only when an untrusted source is import-reachable to the sink. `path` is the compact `{ intra_module, cross_module_hops }` shape; the full ordered hops stay in `reachability.untrusted_source_trace`.
1082
+ - `finding_id`: a stable correlation id, identical across runs for the same rule/path/line and identical to the SARIF `partialFingerprints` value, for tracking a candidate across runs and joining JSON with SARIF.
1028
1083
 
1029
1084
  ---
1030
1085
 
@@ -1396,6 +1451,7 @@ Available on all commands:
1396
1451
  | `FALLOW_BIN` | Path to fallow binary (used by the MCP server). |
1397
1452
  | `FALLOW_TIMEOUT_SECS` | MCP server subprocess timeout in seconds (default: `120`). Increase for very large codebases. |
1398
1453
  | `FALLOW_EXTENDS_TIMEOUT_SECS` | Timeout for fetching remote config inheritance in seconds (default: `5`). Do not raise this for untrusted sources. |
1454
+ | `FALLOW_CACHE_DIR` | Override the persistent extraction cache directory. Wins over `cache.dir`. Useful for read-only checkouts or CI cache volumes. `--no-cache` disables this knob. |
1399
1455
  | `FALLOW_CACHE_MAX_SIZE` | Maximum on-disk extraction cache (`.fallow/cache.bin`) size in megabytes (default: `256`). Triggers LRU eviction when crossed. Wins over `cache.maxSizeMb` config field. Intended for CI runners with disk quotas. `--no-cache` short-circuits this knob. |
1400
1456
  | `FALLOW_AUDIT_CACHE_MAX_AGE_DAYS` | Max age (in days since last reuse or fresh create) of a persistent reusable `fallow audit` base-snapshot worktree cache. Older entries are reclaimed at the top of the next `fallow audit` invocation (default: `30`). Wins over `audit.cacheMaxAgeDays` config field. `0` disables the GC; invalid values silently fall back to config / default. |
1401
1457
  | `FALLOW_UPDATE_CHECK` | Set to `off`, `0`, `false`, `disabled`, or `no` to disable the human-TTY upgrade nudge and its background latest-version check. `DO_NOT_TRACK`, `FALLOW_TELEMETRY_DISABLED`, and CI also suppress it. |
@@ -1498,7 +1554,7 @@ The HTTP layer mirrors the bash `gh_api_retry` / `curl_retry` helpers: `FALLOW_A
1498
1554
  {
1499
1555
  "kind": "dead-code",
1500
1556
  "schema_version": 7,
1501
- "version": "2.89.0",
1557
+ "version": "2.90.0",
1502
1558
  "elapsed_ms": 45,
1503
1559
  "total_issues": 12,
1504
1560
  "entry_points": {
@@ -1658,7 +1714,7 @@ When `--baseline` is used in combined output, the JSON includes a `baseline_delt
1658
1714
  {
1659
1715
  "kind": "dupes",
1660
1716
  "schema_version": 7,
1661
- "version": "2.89.0",
1717
+ "version": "2.90.0",
1662
1718
  "elapsed_ms": 82,
1663
1719
  "total_clones": 15,
1664
1720
  "total_lines_duplicated": 230,
@@ -1702,11 +1758,11 @@ When running `fallow` with no subcommand (all analyses), the JSON output combine
1702
1758
  {
1703
1759
  "kind": "combined",
1704
1760
  "schema_version": 7,
1705
- "version": "2.89.0",
1761
+ "version": "2.90.0",
1706
1762
  "elapsed_ms": 159,
1707
1763
  "check": {
1708
1764
  "schema_version": 7,
1709
- "version": "2.89.0",
1765
+ "version": "2.90.0",
1710
1766
  "elapsed_ms": 45,
1711
1767
  "total_issues": 12,
1712
1768
  "unused_files": [],
@@ -1814,6 +1870,12 @@ Config files are searched in priority order: `.fallowrc.json` > `.fallowrc.jsonc
1814
1870
  "ignorePatterns": ["**/*.generated.ts"]
1815
1871
  },
1816
1872
 
1873
+ // Extraction cache settings. FALLOW_CACHE_DIR overrides cache.dir.
1874
+ "cache": {
1875
+ "dir": "/tmp/fallow-cache",
1876
+ "maxSizeMb": 256
1877
+ },
1878
+
1817
1879
  // Architecture boundaries (preset, custom zones/rules, or auto-discovered feature zones)
1818
1880
  // Presets: "layered", "hexagonal", "feature-sliced", "bulletproof"
1819
1881
  // Rules accept an optional `allowTypeOnly: [zones]` list that admits type-only imports
@@ -61,6 +61,8 @@ kind: "coverage-setup"
61
61
  kind: "coverage-analyze"
62
62
  }) | (ListBoundariesOutput & {
63
63
  kind: "list-boundaries"
64
+ }) | (WorkspacesOutput & {
65
+ kind: "list-workspaces"
64
66
  }) | (HealthOutput & {
65
67
  kind: "health"
66
68
  }) | (DupesOutput & {
@@ -599,6 +601,17 @@ export type ImpactTrendDirection = ("improving" | "declining" | "stable")
599
601
  * from the main contract, mirroring `ImpactReportSchemaVersion`.
600
602
  */
601
603
  export type SecuritySchemaVersion = "1"
604
+ /**
605
+ * Gate mode for `fallow security --gate <mode>` (issue #886). Tier 2 reserves
606
+ * the value `newly-reachable`.
607
+ */
608
+ export type SecurityGateMode = "new"
609
+ /**
610
+ * Gate verdict on the wire. `fail` is the CI-state token; human output renders
611
+ * it as "REVIEW REQUIRED" because these stay unverified candidates, never
612
+ * confirmed vulnerabilities.
613
+ */
614
+ export type SecurityGateVerdict = ("pass" | "fail")
602
615
  /**
603
616
  * The kind of security candidate. Findings are CANDIDATES for downstream agent
604
617
  * verification, NOT verified vulnerabilities.
@@ -607,7 +620,20 @@ export type SecurityFindingKind = ("client-server-leak" | "tainted-sink")
607
620
  /**
608
621
  * The role a hop plays in a security finding's structural import trace.
609
622
  */
610
- export type TraceHopRole = ("client-boundary" | "intermediate" | "secret-source" | "sink")
623
+ export type TraceHopRole = ("client-boundary" | "untrusted-source" | "intermediate" | "secret-source" | "sink")
624
+ /**
625
+ * Dead-code issue kind linked to a security candidate.
626
+ */
627
+ export type SecurityDeadCodeKind = ("unused-file" | "unused-export")
628
+ /**
629
+ * Runtime coverage state for the function enclosing a security sink.
630
+ * This is production-observation evidence, not an exploitability verdict.
631
+ */
632
+ export type SecurityRuntimeState = ("runtime-hot" | "runtime-cold" | "never-executed" | "low-traffic" | "coverage-unavailable" | "runtime-unknown")
633
+ /**
634
+ * Defensive control family detected on a source to sink path.
635
+ */
636
+ export type SecurityControlKind = ("sanitization" | "validation" | "authentication" | "authorization")
611
637
  /**
612
638
  * Discriminator value for [`CodeClimateIssue::kind`].
613
639
  */
@@ -4168,6 +4194,46 @@ allow: string[]
4168
4194
  */
4169
4195
  allow_type_only?: string[]
4170
4196
  }
4197
+ /**
4198
+ * `fallow workspaces --format json` envelope.
4199
+ */
4200
+ export interface WorkspacesOutput {
4201
+ /**
4202
+ * Number of workspace package entries in `workspaces`.
4203
+ */
4204
+ workspace_count: number
4205
+ /**
4206
+ * Workspace packages discovered from package manager and tsconfig workspace
4207
+ * declarations. Paths are project-root-relative and use forward slashes.
4208
+ */
4209
+ workspaces: WorkspaceInfo[]
4210
+ /**
4211
+ * Workspace discovery diagnostics produced while reading workspace
4212
+ * declarations. Present for compatibility with the current wire contract,
4213
+ * even when empty.
4214
+ */
4215
+ workspace_diagnostics: WorkspaceDiagnostic[]
4216
+ }
4217
+ /**
4218
+ * One workspace package emitted by `fallow workspaces --format json`.
4219
+ */
4220
+ export interface WorkspaceInfo {
4221
+ /**
4222
+ * Package name from the workspace package.json. This is the value accepted
4223
+ * by `--workspace <name>`.
4224
+ */
4225
+ name: string
4226
+ /**
4227
+ * Project-root-relative path to the workspace directory, normalized to
4228
+ * forward slashes for cross-platform JSON consumers.
4229
+ */
4230
+ path: string
4231
+ /**
4232
+ * Whether the package is a generated or platform-specific dependency
4233
+ * package rather than a hand-authored workspace.
4234
+ */
4235
+ is_internal_dependency: boolean
4236
+ }
4171
4237
  /**
4172
4238
  * Envelope emitted by `fallow health --format json` (plus the `health` block
4173
4239
  * inside the combined and audit envelopes).
@@ -4791,15 +4857,27 @@ git_sha?: (string | null)
4791
4857
  timestamp: string
4792
4858
  }
4793
4859
  /**
4794
- * The `fallow security --format json` envelope. `security_findings` is the
4795
- * unique required field used for untagged narrowing in `FallowOutput`.
4860
+ * The `fallow security --format json` envelope. `FallowOutput` discriminates it
4861
+ * by the `kind: "security"` tag; the optional `gate` block is additive and is
4862
+ * not part of that discrimination.
4796
4863
  */
4797
4864
  export interface SecurityOutput {
4798
4865
  schema_version: SecuritySchemaVersion
4866
+ /**
4867
+ * Gate verdict, present only when `--gate <mode>` was set (issue #886).
4868
+ * Emitted on pass too (`verdict: "pass"`, `new_count: 0`) so consumers
4869
+ * distinguish "gate ran and passed" from "gate did not run" (absent).
4870
+ */
4871
+ gate?: (SecurityGate | null)
4799
4872
  /**
4800
4873
  * Security candidates. Paths are project-root-relative, forward-slash.
4801
4874
  */
4802
4875
  security_findings: SecurityFinding[]
4876
+ /**
4877
+ * Opt-in attack-surface inventory from untrusted entry points to reachable
4878
+ * sinks. Present only when `--surface` was requested.
4879
+ */
4880
+ attack_surface?: (SecurityAttackSurfaceEntry[] | null)
4803
4881
  /**
4804
4882
  * In-band blind spot: number of `"use client"` files whose transitive
4805
4883
  * import cone contains a dynamic `import()` the reachability BFS could not
@@ -4815,6 +4893,18 @@ unresolved_edge_files: number
4815
4893
  */
4816
4894
  unresolved_callee_sites: number
4817
4895
  }
4896
+ /**
4897
+ * The `gate` block on `SecurityOutput`, present only when `--gate <mode>` ran.
4898
+ * Invariant: `verdict == Fail IFF exit code 8 IFF new_count > 0`.
4899
+ */
4900
+ export interface SecurityGate {
4901
+ mode: SecurityGateMode
4902
+ verdict: SecurityGateVerdict
4903
+ /**
4904
+ * Number of candidates introduced in the changed lines.
4905
+ */
4906
+ new_count: number
4907
+ }
4818
4908
  /**
4819
4909
  * A local security CANDIDATE for downstream agent verification, NOT a verified
4820
4910
  * vulnerability. Emitted only by `fallow security`, never under bare `fallow`
@@ -4823,6 +4913,14 @@ unresolved_callee_sites: number
4823
4913
  * (its hops and length) is the only honest signal.
4824
4914
  */
4825
4915
  export interface SecurityFinding {
4916
+ /**
4917
+ * Stable per-finding correlation id, identical across runs for the same
4918
+ * rule + anchor path + line. An autonomous agent that triaged this
4919
+ * candidate on a prior run uses it to correlate the candidate after a
4920
+ * rebase. Equal to the SARIF `partialFingerprints` value for the same
4921
+ * finding (one shared helper computes both).
4922
+ */
4923
+ finding_id: string
4826
4924
  kind: SecurityFindingKind
4827
4925
  /**
4828
4926
  * The catalogue category id (e.g. `"dangerous-html"`). `None` for
@@ -4876,12 +4974,37 @@ trace: TraceHop[]
4876
4974
  */
4877
4975
  actions: IssueAction[]
4878
4976
  /**
4879
- * Graph-derived reachability ranking signal (issue #860). `None` until the
4880
- * post-detection ranking pass fills it; additive on the wire (skipped when
4881
- * absent). Drives the order findings are emitted in: candidates reachable
4882
- * from a runtime entry point with a wider blast radius sort first.
4977
+ * Dead-code cross-link when the same sink candidate sits in code fallow also
4978
+ * reports as removable. Agents should verify the dead-code finding and delete
4979
+ * the code instead of hardening the sink when deletion is safe.
4980
+ */
4981
+ dead_code?: (SecurityDeadCodeContext | null)
4982
+ /**
4983
+ * Graph-derived reachability ranking signal (issues #860 and #885). `None`
4984
+ * until the post-detection ranking pass fills it; additive on the wire
4985
+ * (skipped when absent). Drives the order findings are emitted in:
4986
+ * runtime-reachable candidates sort first, followed by source-backed and
4987
+ * source-reachable candidates, then wider blast radius.
4883
4988
  */
4884
4989
  reachability?: (SecurityReachability | null)
4990
+ candidate: SecurityCandidate
4991
+ /**
4992
+ * Source-to-sink taint-flow triple, present only when an untrusted source
4993
+ * is import-reachable to this sink. Absent (skipped) otherwise.
4994
+ */
4995
+ taint_flow?: (SecurityTaintFlow | null)
4996
+ /**
4997
+ * Production runtime coverage context for the function enclosing this
4998
+ * security sink. Present only when `fallow security --runtime-coverage`
4999
+ * runs and the candidate is a `tainted-sink`.
5000
+ */
5001
+ runtime?: (SecurityRuntimeContext | null)
5002
+ /**
5003
+ * Internal projection used by `fallow security --surface`. The CLI strips
5004
+ * this from per-finding JSON and promotes it to the top-level
5005
+ * `attack_surface` field only when requested.
5006
+ */
5007
+ attack_surface?: (SecurityAttackSurfaceEntry | null)
4885
5008
  }
4886
5009
  /**
4887
5010
  * One hop in a security finding's structural trace. Stored as an absolute path
@@ -4905,11 +5028,31 @@ line: number
4905
5028
  col: number
4906
5029
  role: TraceHopRole
4907
5030
  }
5031
+ /**
5032
+ * Dead-code cross-link attached to a security candidate when fallow's dead-code
5033
+ * pass reports the same anchor as removable code.
5034
+ */
5035
+ export interface SecurityDeadCodeContext {
5036
+ kind: SecurityDeadCodeKind
5037
+ /**
5038
+ * Unused export name when `kind` is `unused-export`.
5039
+ */
5040
+ export_name?: (string | null)
5041
+ /**
5042
+ * Dead-code finding line when available.
5043
+ */
5044
+ line?: (number | null)
5045
+ /**
5046
+ * Agent-facing guidance for deciding between deletion and hardening.
5047
+ */
5048
+ guidance: string
5049
+ }
4908
5050
  /**
4909
5051
  * Graph-derived reachability ranking signal for a security candidate. Computed
4910
- * from the EXISTING module graph (runtime reachability + reverse-dep fan-in)
4911
- * after detection, never proven exploitable. Used to surface candidates that
4912
- * sit on a request/runtime-reachable surface above isolated helpers or scripts.
5052
+ * from the existing module graph after detection, never proven exploitable.
5053
+ * Used to surface candidates that sit on a request/runtime-reachable surface,
5054
+ * receive same-module source evidence, or are import-reachable from an
5055
+ * untrusted-source module above isolated helpers or scripts.
4913
5056
  *
4914
5057
  * This is a relative-ordering signal, NOT a `confidence` or `signal_strength`
4915
5058
  * score: fallow does not prove the path is exploitable.
@@ -4922,6 +5065,23 @@ export interface SecurityReachability {
4922
5065
  * only from test entry points does not count.
4923
5066
  */
4924
5067
  reachable_from_entry: boolean
5068
+ /**
5069
+ * Whether the anchor module is reachable over value imports from a module
5070
+ * that reads a known untrusted input source. Module-level only: this does
5071
+ * not prove a specific source value reaches the sink argument.
5072
+ */
5073
+ reachable_from_untrusted_source?: boolean
5074
+ /**
5075
+ * Number of value-import hops from the untrusted-source module to the sink
5076
+ * module when `reachable_from_untrusted_source` is true.
5077
+ */
5078
+ untrusted_source_hop_count?: (number | null)
5079
+ /**
5080
+ * Module-level import path from the untrusted-source module to the sink
5081
+ * anchor. Empty when no source module reaches this candidate. The path is a
5082
+ * ranking explanation, not a value-flow proof.
5083
+ */
5084
+ untrusted_source_trace?: TraceHop[]
4925
5085
  /**
4926
5086
  * Number of distinct modules that transitively depend on the anchor module
4927
5087
  * (fan-in via the graph's reverse-dependency index). A higher value means a
@@ -4936,6 +5096,256 @@ blast_radius: number
4936
5096
  */
4937
5097
  crosses_boundary: boolean
4938
5098
  }
5099
+ /**
5100
+ * An agent-actionable candidate record on a [`SecurityFinding`]. fallow fills
5101
+ * `source_kind`, `sink`, and `boundary`. The exploitability IMPACT is
5102
+ * deliberately NOT a field: deciding severity / exploitability is the consuming
5103
+ * agent's job, not fallow's, and a perpetually-null `impact` key would only
5104
+ * train consumers to ignore it. The agent reads this record, then writes its
5105
+ * own impact verdict downstream.
5106
+ */
5107
+ export interface SecurityCandidate {
5108
+ /**
5109
+ * The kind of untrusted input that reaches the sink, as a stable catalogue
5110
+ * source id (`"http-request-input"`, `"process-env"`, `"process-argv"`,
5111
+ * `"message-event-data"`, `"location-input"`, ...). `None`/absent when no
5112
+ * untrusted source was matched (always `None` for `client-server-leak`).
5113
+ * This is an OPEN string set, driven by the data-driven source catalogue; a
5114
+ * consumer should treat an unknown id as "untrusted source of unknown kind"
5115
+ * and never drop the candidate on that basis.
5116
+ */
5117
+ source_kind?: (string | null)
5118
+ sink: SecurityCandidateSink
5119
+ boundary: SecurityCandidateBoundary
5120
+ /**
5121
+ * Network-destination context, present only on `secret-to-network` (#890)
5122
+ * candidates: the host the secret-bearing call targets, so an agent can
5123
+ * triage exfil from intended auth. Absent for every other category.
5124
+ */
5125
+ network?: (SecurityNetworkContext | null)
5126
+ }
5127
+ /**
5128
+ * The sink slot of a [`SecurityCandidate`]: a self-contained description of the
5129
+ * matched sink site. Echoes the finding's own span (`path`/`line`/`col`) plus
5130
+ * the catalogue `category`/`cwe` and the captured `callee`, so an agent can act
5131
+ * on `candidate.sink` in isolation (e.g. after fanning a finding out to a
5132
+ * sub-agent) without reading the parent finding.
5133
+ */
5134
+ export interface SecurityCandidateSink {
5135
+ /**
5136
+ * File of the sink site. Absolute internally; JSON strips the project root
5137
+ * via `serde_path::serialize`.
5138
+ */
5139
+ path: string
5140
+ /**
5141
+ * 1-based line of the sink site.
5142
+ */
5143
+ line: number
5144
+ /**
5145
+ * 0-based byte column of the sink site.
5146
+ */
5147
+ col: number
5148
+ /**
5149
+ * Catalogue category id of the sink (e.g. `"dangerous-html"`). `None` for
5150
+ * `client-server-leak`.
5151
+ */
5152
+ category?: (string | null)
5153
+ /**
5154
+ * CWE number declared by the catalogue entry. `None` for
5155
+ * `client-server-leak`; never fabricated beyond the catalogue's value.
5156
+ */
5157
+ cwe?: (number | null)
5158
+ /**
5159
+ * The sink callee (the dangerous function or member path, e.g.
5160
+ * `"el.innerHTML"`, `"child_process.exec"`) captured by the catalogue match.
5161
+ * `None` for `client-server-leak` and matches that name no callee.
5162
+ */
5163
+ callee?: (string | null)
5164
+ }
5165
+ /**
5166
+ * The boundary slot of a [`SecurityCandidate`]: which structural boundaries the
5167
+ * candidate's flow crosses. A flow that crosses a client/server or module
5168
+ * boundary is a stronger review target than a self-contained one; the boundary
5169
+ * is fallow's structural signal over a pure source-sink match.
5170
+ *
5171
+ * Two further boundary kinds are RESERVED for a follow-up and are deliberately
5172
+ * absent here rather than emitted as always-false: `export_visibility` (is the
5173
+ * sink on a publicly-exported symbol?) and a package boundary (does the flow
5174
+ * cross an npm-package edge?). Both need new graph derivation that does not
5175
+ * exist today; emitting them as `false` would misreport "we checked and it does
5176
+ * not cross" when fallow has not checked at all.
5177
+ */
5178
+ export interface SecurityCandidateBoundary {
5179
+ /**
5180
+ * Whether the finding crosses a client/server boundary (a `"use client"`
5181
+ * file appears in the trace). True only for `client-server-leak` today;
5182
+ * `tainted-sink` candidates carry no client/server marker.
5183
+ */
5184
+ client_server: boolean
5185
+ /**
5186
+ * Whether an untrusted source reaches the sink across one or more
5187
+ * value-import (module) hops. Derived from the reachability hop count.
5188
+ */
5189
+ cross_module: boolean
5190
+ /**
5191
+ * The architecture-zone crossing when the anchor participates in a declared
5192
+ * boundary-rule violation in the same run. `None` when it crosses no
5193
+ * declared zone boundary.
5194
+ */
5195
+ architecture_zone?: (SecurityZoneCrossing | null)
5196
+ }
5197
+ /**
5198
+ * A declared architecture-zone crossing, recovered by correlating a finding's
5199
+ * anchor against the run's architecture-boundary violations.
5200
+ */
5201
+ export interface SecurityZoneCrossing {
5202
+ /**
5203
+ * Zone the importing side belongs to.
5204
+ */
5205
+ from: string
5206
+ /**
5207
+ * Zone the imported side belongs to.
5208
+ */
5209
+ to: string
5210
+ }
5211
+ /**
5212
+ * Network-destination context for a `secret-to-network` candidate (#890): where
5213
+ * the secret-bearing network call sends its data. Present only on
5214
+ * network-category candidates. A consuming agent uses it to triage exfil
5215
+ * (dynamic / untrusted destination) from intended auth (a literal provider
5216
+ * host) without re-reading source.
5217
+ */
5218
+ export interface SecurityNetworkContext {
5219
+ /**
5220
+ * The network call's destination as a static URL string literal, or absent
5221
+ * when the destination is DYNAMIC (not a literal). A dynamic destination is
5222
+ * the higher-signal exfil case; a literal provider host is usually intended
5223
+ * auth.
5224
+ */
5225
+ destination?: (string | null)
5226
+ }
5227
+ /**
5228
+ * A source-to-sink taint-flow triple, emitted only when an untrusted source is
5229
+ * import-reachable to the sink (`reachability.reachable_from_untrusted_source`).
5230
+ * The `{ source, sink, path }` shape matches the model agent SAST tooling
5231
+ * expects (cf. Semgrep `taint_source` / `taint_sink`, SARIF `threadFlows`).
5232
+ */
5233
+ export interface SecurityTaintFlow {
5234
+ source: TaintEndpoint
5235
+ sink: TaintEndpoint
5236
+ path: TaintPath
5237
+ }
5238
+ /**
5239
+ * One endpoint (source or sink node) of a [`SecurityTaintFlow`].
5240
+ */
5241
+ export interface TaintEndpoint {
5242
+ /**
5243
+ * File of the endpoint. Absolute internally; JSON strips the project root.
5244
+ */
5245
+ path: string
5246
+ /**
5247
+ * 1-based line of the endpoint.
5248
+ */
5249
+ line: number
5250
+ /**
5251
+ * 0-based byte column of the endpoint.
5252
+ */
5253
+ col: number
5254
+ }
5255
+ /**
5256
+ * Compact taint-flow path shape. The ordered per-hop trace is NOT duplicated
5257
+ * here: it lives on [`SecurityReachability::untrusted_source_trace`]. This
5258
+ * carries only the flow's structural summary (intra-module flow plus the
5259
+ * cross-module hop count) so consumers do not parse two copies of the hops.
5260
+ */
5261
+ export interface TaintPath {
5262
+ /**
5263
+ * Whether the source and sink sit in the same module (no import hop between
5264
+ * them); the source-to-sink association is intra-module.
5265
+ */
5266
+ intra_module: boolean
5267
+ /**
5268
+ * Number of value-import hops from the untrusted-source module to the sink
5269
+ * module. Zero for an intra-module flow.
5270
+ */
5271
+ cross_module_hops: number
5272
+ }
5273
+ /**
5274
+ * Runtime coverage context attached to a security candidate when
5275
+ * `fallow security --runtime-coverage` is supplied.
5276
+ */
5277
+ export interface SecurityRuntimeContext {
5278
+ state: SecurityRuntimeState
5279
+ /**
5280
+ * Enclosing function name from static extraction.
5281
+ */
5282
+ function: string
5283
+ /**
5284
+ * 1-based line where the enclosing function starts.
5285
+ */
5286
+ line: number
5287
+ /**
5288
+ * Observed invocation count when the runtime report provides it.
5289
+ */
5290
+ invocations?: (number | null)
5291
+ /**
5292
+ * Runtime coverage stable function id, when available.
5293
+ */
5294
+ stable_id?: (string | null)
5295
+ /**
5296
+ * Short candidate-framed explanation of the runtime evidence.
5297
+ */
5298
+ evidence?: (string | null)
5299
+ }
5300
+ /**
5301
+ * One untrusted entry to reachable sink path for `fallow security --surface`.
5302
+ */
5303
+ export interface SecurityAttackSurfaceEntry {
5304
+ source: TaintEndpoint
5305
+ sink: SecurityCandidateSink
5306
+ /**
5307
+ * Ordered source to sink path. Same shape as the reachability trace so
5308
+ * consumers can reuse existing path handling.
5309
+ */
5310
+ path: TraceHop[]
5311
+ defensive_boundary: SecurityDefensiveBoundary
5312
+ }
5313
+ /**
5314
+ * Agent-facing defensive-boundary verification context for one surface path.
5315
+ */
5316
+ export interface SecurityDefensiveBoundary {
5317
+ /**
5318
+ * Known controls detected along this path.
5319
+ */
5320
+ controls: SecurityDefensiveControl[]
5321
+ /**
5322
+ * Verification question for the consuming agent. It is a prompt, not a
5323
+ * missing-guard verdict.
5324
+ */
5325
+ verification_prompt: string
5326
+ }
5327
+ /**
5328
+ * Defensive control found on an attack-surface path.
5329
+ */
5330
+ export interface SecurityDefensiveControl {
5331
+ kind: SecurityControlKind
5332
+ /**
5333
+ * File of the control site. Absolute internally; JSON strips the project root.
5334
+ */
5335
+ path: string
5336
+ /**
5337
+ * 1-based line of the control site.
5338
+ */
5339
+ line: number
5340
+ /**
5341
+ * 0-based byte column of the control site.
5342
+ */
5343
+ col: number
5344
+ /**
5345
+ * Flattened callee path or a stable synthetic guard name.
5346
+ */
5347
+ callee: string
5348
+ }
4939
5349
  /**
4940
5350
  * Bare `fallow --format json` envelope.
4941
5351
  */