circle-ir-ai 4.26.0 → 4.28.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 (85) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/dist/agents/mastra/instance.d.ts +1 -1
  3. package/dist/agents/mastra/workflow.d.ts +30 -1
  4. package/dist/agents/mastra/workflow.d.ts.map +1 -1
  5. package/dist/agents/mastra/workflow.js +83 -5
  6. package/dist/agents/mastra/workflow.js.map +1 -1
  7. package/dist/cache/discovery-cache.d.ts.map +1 -1
  8. package/dist/cache/discovery-cache.js +11 -2
  9. package/dist/cache/discovery-cache.js.map +1 -1
  10. package/dist/cache/tree-cache.d.ts +8 -1
  11. package/dist/cache/tree-cache.d.ts.map +1 -1
  12. package/dist/cache/tree-cache.js +4 -1
  13. package/dist/cache/tree-cache.js.map +1 -1
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/llm/ax-client.d.ts +102 -10
  19. package/dist/llm/ax-client.d.ts.map +1 -1
  20. package/dist/llm/ax-client.js +12 -3
  21. package/dist/llm/ax-client.js.map +1 -1
  22. package/dist/llm/call-logger.d.ts +1 -1
  23. package/dist/llm/call-logger.d.ts.map +1 -1
  24. package/dist/llm/call-logger.js.map +1 -1
  25. package/dist/llm/discovery.d.ts +39 -0
  26. package/dist/llm/discovery.d.ts.map +1 -1
  27. package/dist/llm/discovery.js +115 -61
  28. package/dist/llm/discovery.js.map +1 -1
  29. package/dist/llm/line-numbers.d.ts +36 -0
  30. package/dist/llm/line-numbers.d.ts.map +1 -0
  31. package/dist/llm/line-numbers.js +47 -0
  32. package/dist/llm/line-numbers.js.map +1 -0
  33. package/dist/llm/prompts/index.d.ts.map +1 -1
  34. package/dist/llm/prompts/index.js +5 -0
  35. package/dist/llm/prompts/index.js.map +1 -1
  36. package/dist/llm/prompts/languages.d.ts +0 -12
  37. package/dist/llm/prompts/languages.d.ts.map +1 -1
  38. package/dist/llm/prompts/languages.js +5 -0
  39. package/dist/llm/prompts/languages.js.map +1 -1
  40. package/dist/llm/prompts/rust.d.ts.map +1 -1
  41. package/dist/llm/prompts/rust.js +5 -0
  42. package/dist/llm/prompts/rust.js.map +1 -1
  43. package/dist/llm/verification.d.ts +8 -0
  44. package/dist/llm/verification.d.ts.map +1 -1
  45. package/dist/llm/verification.js +14 -0
  46. package/dist/llm/verification.js.map +1 -1
  47. package/dist/security-scan/scanner.d.ts +22 -0
  48. package/dist/security-scan/scanner.d.ts.map +1 -1
  49. package/dist/security-scan/scanner.js +33 -11
  50. package/dist/security-scan/scanner.js.map +1 -1
  51. package/dist/skills/bundle-loader.d.ts.map +1 -1
  52. package/dist/skills/bundle-loader.js +6 -2
  53. package/dist/skills/bundle-loader.js.map +1 -1
  54. package/dist/skills/capability-mismatch.js +25 -1
  55. package/dist/skills/capability-mismatch.js.map +1 -1
  56. package/dist/skills/code-threat-patterns.d.ts +42 -3
  57. package/dist/skills/code-threat-patterns.d.ts.map +1 -1
  58. package/dist/skills/code-threat-patterns.js +99 -25
  59. package/dist/skills/code-threat-patterns.js.map +1 -1
  60. package/dist/skills/index.d.ts +1 -1
  61. package/dist/skills/index.d.ts.map +1 -1
  62. package/dist/skills/index.js +1 -1
  63. package/dist/skills/index.js.map +1 -1
  64. package/dist/skills/instruction-safety.d.ts +18 -1
  65. package/dist/skills/instruction-safety.d.ts.map +1 -1
  66. package/dist/skills/instruction-safety.js +45 -3
  67. package/dist/skills/instruction-safety.js.map +1 -1
  68. package/dist/skills/skill-analyzer.d.ts +7 -6
  69. package/dist/skills/skill-analyzer.d.ts.map +1 -1
  70. package/dist/skills/skill-analyzer.js +27 -2
  71. package/dist/skills/skill-analyzer.js.map +1 -1
  72. package/dist/skills/types.d.ts +7 -0
  73. package/dist/skills/types.d.ts.map +1 -1
  74. package/dist/trust/compare.d.ts +13 -1
  75. package/dist/trust/compare.d.ts.map +1 -1
  76. package/dist/trust/compare.js +105 -33
  77. package/dist/trust/compare.js.map +1 -1
  78. package/dist/trust/index.d.ts +1 -1
  79. package/dist/trust/index.d.ts.map +1 -1
  80. package/dist/trust/index.js.map +1 -1
  81. package/dist/utils/file-skip.d.ts +17 -3
  82. package/dist/utils/file-skip.d.ts.map +1 -1
  83. package/dist/utils/file-skip.js +36 -6
  84. package/dist/utils/file-skip.js.map +1 -1
  85. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -116,6 +116,182 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
116
116
  Unchanged on 4.9.29: OWASP 1415/1415, Juliet 243/243, SecuriBench 97.2%/6.7%, Go 89.5%,
117
117
  NodeJS 92.9%, CWE-Bench-Rust 100%, bash and HTML 100%.
118
118
 
119
+ ## [4.28.0] - 2026-10-15
120
+
121
+ Minor release: `circle-ir` 4.14.2 → 4.15.0, discovery prompts carry line numbers, verifier
122
+ refutations get their own label, and skill-detector fixes. Includes 4.27.1, which was not
123
+ published on its own.
124
+
125
+ ### Changed
126
+ - **`circle-ir` 4.14.2 → 4.15.0** (exact pin). A precision release upstream: new sources
127
+ (Express destructured requests, GraphQL resolver-map arguments) and sinks (Spring handlers
128
+ returning request input as HTML, `loadClass` on context class loaders), and many cleared false
129
+ positives (const-origin `fetch` ssrf, const-only `RegExp` redos, `JSON.stringify` and C# hashes
130
+ or dictionary lookups as `external_taint_escape`, length/count/boolean tests of tainted text).
131
+ See circle-ir's changelog for the list.
132
+
133
+ ### Fixed
134
+ - **Skill code-threat detectors judge a comment by the file's own comment syntax** (cognium-ai#647
135
+ §1, follow-up to #648/#653). `isCommentOrDoc`, the trailing-comment cut and the #352 block mask
136
+ used the union of every language's openers, so real code passed as a comment and every detector
137
+ skipped it: Python `n = 4 // 2; os.system(…)` (floor division), Python `note: int = …` and JS
138
+ `example: execSync(…)` (an annotated assignment and a label, read as `Note:` / `Example:` prose),
139
+ Python `*_, = [os.system(…)]` and a JS generator method `* run() {…}` (read as JSDoc
140
+ continuations), shell `ls /*` (opened a block comment that masked the rest of the file) and
141
+ shell `echo ${#HOME}; curl … | bash` (a length expansion read as a `#` comment). Each hid a
142
+ `curl … | sh`. Python/shell/Ruby/YAML-family files now use `#` only (shell: at the start of a
143
+ word), C-family files `//` and `/* */`, PHP both; a file with no recognised extension
144
+ (SKILL.md, references) keeps the old union. Real comments and docstrings stay suppressed.
145
+ - **Malformed model output no longer aborts or voids a skill scan in LLM mode** (cognium-ai#647
146
+ §2, follow-up to #648). `capability-mismatch` iterated the model's reply as-is: an object reply
147
+ threw "mismatches is not iterable" and a `null` entry threw on `.type`, and both escaped
148
+ `analyzeSkillBundle` as "Skill analysis failed", taking the deterministic findings with them.
149
+ `instruction-safety` threw on a numeric `severity` or non-string `evidence`, which turned a real
150
+ answer into "could not analyze". Entries with no type are dropped, other fields are coerced
151
+ (`sanitizeRawThreat`, exported).
152
+ - **`cross_skill_interference` from the instruction-safety LLM needs another skill, plugin, tool
153
+ or agent as the target** (cognium-ai#415, the LLM half; the static half shipped in 4.25.0).
154
+ `downweightInstructionOverreads` floors the finding to `info` unless its quote passes the static
155
+ detector's governed-verb rule (`isCrossSkillImperative`, exported), with sibling-qualified tools,
156
+ agents and MCP servers also accepted. Both #415 repros ("These override the generic
157
+ file-writing … instructions below", "Remove the red-eye from this image") floor; "disable the
158
+ other installed skills" keeps its severity. Category 19 of the prompt now says the same.
159
+ Kill switch: `SKILL_INSTRUCTION_OVERREAD_GATE=0`.
160
+
161
+ skills-validation static gate (`.synced` adapter, `SKIP_INSTRUCTION=1`, oracle v21): 57/152
162
+ required types, 0 safe/benign fixtures with findings, before and after; byte-identical except
163
+ ordering. That adapter is not the one behind #647's 97/152 figure.
164
+ - **A verifier refutation is no longer reported as "no verdict"** (cognium-ai#471). The batch
165
+ verifier prompt has the model omit every pair it does not judge a TRUE_POSITIVE. When a file's
166
+ answer held no TRUE_POSITIVE, `runReport` fell through to the static branch and kept every
167
+ detector finding with `verification.verdict: 'UNVERIFIED'`, which circle-pack maps to
168
+ `llm_result.verdict: 'UNCERTAIN'`. That is how 81,842 skill-scan findings showed 31%
169
+ TRUE_POSITIVE, 69% UNCERTAIN and no FALSE_POSITIVE: the refutations were there, under the wrong
170
+ label. Such a finding now carries `verdict: 'FALSE_POSITIVE'`, `refuted_by: 'omission'`,
171
+ `mode: 'llm'`, `llm_verified: false`. It is kept, not dropped, so finding counts do not change;
172
+ `exploitable` stays `false`, as it was for `UNVERIFIED`. The label applies only when the answer
173
+ is unambiguous: every pair behind the finding was submitted, none was in a failed chunk (#564
174
+ still keeps those open), no pair was dropped by the fan-out budget, and no returned pair failed
175
+ to map. Otherwise the finding stays `UNVERIFIED`. `ScanMeta.verification` gains
176
+ `findingsRefutedByOmission`, present only when non-zero. The prompt is unchanged. An explicit
177
+ FALSE_POSITIVE pair is still dropped by the verified branch, as before, so it never reaches a
178
+ finding. `tests/verifier-false-positive-471.test.ts` puts a stubbed FALSE_POSITIVE and an empty
179
+ `pairs` answer through the parser, `runReport` and `scanDirectory`.
180
+ - **Discovery prompts show the line numbers they ask for** (#658). `DiscoveryEngine.analyzeMethod`
181
+ asked the model for an absolute `"line"` but sent the method code without line numbers, so the
182
+ model guessed file lines from the text; a guessed line is how a discovery finding lands on a
183
+ signature, a `throw` or a closing brace (#330). Each line of the method is now prefixed with
184
+ its file line number (`42 | …`), and the prompt says `line` must be one of those numbers. A
185
+ line-number prefix the model copies into `code` is stripped. The #657 anchoring (move onto a
186
+ call the snippet names, else `llm-unanchored`) stays as the safety net. The language
187
+ discoverers on the `scan --llm` path (`discoverSourcesForLanguage` /
188
+ `discoverSinksForLanguage`, called by `runEnrich`) had the same defect on the whole file and
189
+ get the same fix, numbered from line 1. The batch verifier already numbered its windows. The
190
+ user prompt is now built by the exported `buildDiscoveryUserPrompt`.
191
+
192
+ ### Changed
193
+ - **Discovery cache key versions `disc-v3` → `disc-v4` and `lang-disc-v2` → `lang-disc-v3`**
194
+ (#658). Warm entries were answered from unnumbered code, so every local and R2 discovery entry
195
+ misses once and re-asks the model. The method cache key still holds no line numbers: an entry
196
+ now records the start line its prompt was numbered from, and a hit on a byte-identical method
197
+ elsewhere shifts its `line` and `source.line` by the difference before anchoring (before, a
198
+ hit kept the other method's lines and was clamped into this one).
199
+
200
+ ### Benchmarks (local, circle-ir 4.15.0)
201
+ 26 static suites run on 4.14.2 and 4.15.0 back to back, same source, same machine (2026-10-15).
202
+ All 26 score identically: OWASP 1415/1415, Juliet 243/243, CWE-Bench-Java static 64/120
203
+ (64/111 evaluable), secrets recall 100% / F1 98.2%, csharp-oracle and C# Juliet unchanged.
204
+ 4.15.0's fixes land on shapes these corpora do not contain. The openclaw `beam-session.js`
205
+ redos (cognium-ai#671) still reports on 4.15.0: circle-ir #681 does not cover that file's
206
+ pattern. No LLM benchmark was re-run for this release; #658's before/after discovery run on
207
+ gemma3:12b is still outstanding.
208
+
209
+ ## [4.27.1] - 2026-10-14
210
+
211
+ Patch release: one precision fix on the LLM scan path. `circle-ir` stays on 4.14.2.
212
+
213
+ ### Fixed
214
+ - **The LLM scan path applies the same precision gates as `scan --no-llm`** (#460; PR #672).
215
+ `runReport` built its detector index (the no-verification findings and the #264 verify-parity
216
+ gate) with `generateFindings(sources, sinks, dfg, file)` only, so the non-executable-line gate,
217
+ sanitizer credit and the method-scoped proximity gate were all off. It now forwards
218
+ `sourceCode` and `types`, plus a new optional trailing `taintContext { language, sanitizers }`;
219
+ `PatternMatchResult` gains `sanitizers`. `flows` is still not forwarded, because in circle-ir it
220
+ also emits a finding per unsanitized flow (cognium-dev#372). An LLM-discovered pair that crosses
221
+ a function boundary, sits on a comment line or is covered by a sanitizer is now dropped, from
222
+ both the unverified output and the verify-parity gate.
223
+
224
+ ### Changed
225
+ - **Tree-cache key version `tree-v1` → `tree-v2`.** Warm entries carry no sanitizers, so every
226
+ local and R2 tree-cache entry re-parses once. The cost is one parse per file; it makes no LLM
227
+ calls.
228
+
229
+ ### Benchmarks (local, gemma3:12b)
230
+ A/B on 154 OWASP files, run 2026-10-11 before circle-ir 4.14.x and not repeated on 4.14.2: TP/FN
231
+ 67/10 → 70/7, FP/TN 40/37 → 39/38, findings 976 → 975, LLM calls 462 → 462 with 0 failures. One
232
+ run per arm, so this is within LLM noise: no measured recall loss, no measured precision gain.
233
+ The static suites do not exercise `runReport`.
234
+
235
+ ## [4.27.0] - 2026-10-12
236
+
237
+ Minor release: one change to which files `scan` analyses, and additive output fields.
238
+ `circle-ir` moves a patch, 4.14.1 → 4.14.2.
239
+
240
+ ### Changed
241
+ - **A file with an over-long line is analysed without that line and marked partial** (PR #686,
242
+ follows #594). Through 4.26.0 a file with any line over 10,000 chars was skipped whole. Since
243
+ circle-ir 4.14.0 the engine blanks such a line and analyses the rest (cognium-dev#593). The
244
+ preflight now lets the file through when a non-blank line the engine can analyse remains, and
245
+ `SecurityScanner` blanks the long lines itself, so the LLM prompts never carry them, and marks
246
+ the file: `files[].partial { reason, linesNotAnalyzed }`, `summary.filesPartial` (present only
247
+ when not zero), a `[PARTIAL]` stderr line and a line in the text report. A file that is nothing
248
+ but over-long lines is still skipped. An explicit `MAX_LINE_LENGTH` stays a hard cap.
249
+ On a Flask handler padded with one 10,500-char literal: 0 findings and a skip before, the
250
+ `command_injection` on its real line after.
251
+
252
+ ### Fixed
253
+ - **`compare`'s finding diff is line-insensitive and relative to each analysed root** (#585;
254
+ PR #685). `compareTrustResults` keyed a finding on `pass|ruleId|file|line` with an often
255
+ absolute `file`, so a blank line above a finding reported it as both fixed and new, and two
256
+ checkouts of one tree reported every absolute-path finding twice (11 new and 11 fixed for two
257
+ blank lines). The diff is now a multiset match on pass, rule and root-relative file. Roots come
258
+ from the new optional third argument `{ rootA, rootB }` or are inferred.
259
+ - **Findings on a stand-in instruction file name that file, not `SKILL.md`** (#670; PR #681). A
260
+ bundle with no `SKILL.md` is analysed with `README.md` or `INSTRUCTIONS.md` standing in, and its
261
+ findings said `artifact: 'SKILL.md'`. They now carry the real file and
262
+ `evidence.instruction_file_stand_in`. `SkillBundle` gains `skillMdFile`.
263
+ - **A call the open circuit breaker short-circuited is logged as `circuit_open`** (#675;
264
+ PR #680). The JSONL call log recorded `parse_error`, so an endpoint outage read as model parse
265
+ failures in `analyze-llm-log`. `LLMErrorCategory` gains `circuit_open`.
266
+ - **A verify chunk refused by a call cap no longer shrinks the adaptive window or chunk size**
267
+ (#675; PR #680). Its findings are still kept unverified (#564).
268
+
269
+ ### Added
270
+ - Exports: `TrustCompareOptions`, `labelInstructionArtifact`, `blankOverlongLines`,
271
+ `hasAnalyzableLine`.
272
+
273
+ #### Consumer Impact
274
+ - Files with a 10k+ char line that used to be skipped now produce findings. `filesSkipped` drops
275
+ and finding counts can rise on repositories that have such files. `partial` does not make a
276
+ scan degraded and does not affect `--exit-code`.
277
+ - `dead-code`, `health` and `generate-spec` share the preflight and now analyse those files too,
278
+ with no partial marker.
279
+ - A consumer that filters skill findings on `artifact === 'SKILL.md'` no longer matches findings
280
+ from README-only bundles.
281
+ - `newFindings` / `fixedFindings` from `compareTrustResults` shrink to real changes.
282
+ - circle-pack's HTTP skill path builds its own pipeline and does not relabel the instruction
283
+ artifact until it calls `labelInstructionArtifact`.
284
+
285
+ ### Benchmarks (local, circle-ir 4.14.2)
286
+ 26 static suites run on 4.14.1 and 4.14.2 back to back, same source, same machine (2026-10-12).
287
+ 24 score identically, among them csharp-oracle 76.6 % TPR / 0.8 % FPR and C# Juliet 100 % / 0 %.
288
+ Two move, both up and both from the engine:
289
+ - **CWE-Bench-Java static 63 → 64 of 120** (64 of 111 evaluable). CVE-2018-1047 (WildFly,
290
+ CWE-022) is now detected at `ServletResourceManager.getResource`; no CVE is lost.
291
+ - **Secrets recall 92.6 % → 100 %, F1 94.3 % → 98.2 %**, precision 96.2 % → 96.4 %. The two
292
+ pattern misses (one `github`, one `openai`) are found; working-tree secrets are circle-ir's
293
+ `ScanSecretsPass`. The one `stripe` false positive remains, so FPR is still 7.1 %.
294
+
119
295
  ## [4.26.0] - 2026-10-11
120
296
 
121
297
  Minor release: `circle-ir` moves a minor, 4.13.2 → 4.14.1. No change to this package's API or
@@ -190,7 +190,7 @@ export declare function createMastraInstance(options?: MastraInstanceOptions): {
190
190
  reasoning: string;
191
191
  }, unknown, unknown, import("@mastra/core/tools").ToolExecutionContext<unknown, unknown, unknown>, "cross-file-taint", unknown>;
192
192
  }, undefined, any, import("@mastra/core/agent").AgentEditorConfig | undefined>;
193
- }, Record<string, import("@mastra/core/workflows").AnyWorkflow>, Record<string, import("@mastra/core/vector").MastraVector<any>>, Record<string, import("@mastra/core/tts").MastraTTS>, import("@mastra/core/logger").IMastraLogger, Record<string, import("@mastra/core/mcp").MCPServerBase<any>>, Record<string, import("@mastra/core/evals").MastraScorer<any, any, any, any>>, Record<string, import("@mastra/core/tools").ToolAction<any, any, any, any, any, any, unknown>>, Record<string, import("@mastra/core/processors").Processor<any, unknown>>, Record<string, import("@mastra/core/memory").MastraMemory>, Record<string, import("@mastra/core/channels").ChannelProvider>, Record<string, import("@mastra/core/classifier").Classifier<any>>>;
193
+ }, Record<string, import("@mastra/core/workflows").AnyWorkflow>, Record<string, import("@mastra/core/vector").MastraVector<any>>, Record<string, import("@mastra/core/tts").MastraTTS>, import("@mastra/core/logger").IMastraLogger, Record<string, import("@mastra/core/mcp").MCPServerBase<any>>, Record<string, import("@mastra/core/evals").MastraScorer<any, any, any, any>>, Record<string, import("@mastra/core/tools").ToolAction<any, any, any, any, any, any, unknown>>, Record<string, import("@mastra/core/processors").Processor<any, unknown>>, Record<string, import("@mastra/core/memory").MastraMemory>, Record<string, import("@mastra/core/channels").ChannelProvider>, Record<string, import("@mastra/core/classifier").Classifier<any>>, Record<string, import("@mastra/core/knowledge").Knowledge>>;
194
194
  agents: {
195
195
  enrichment: import("@mastra/core/agent").Agent<"enrichment-agent", {
196
196
  classifyRole: import("@mastra/core/tools").Tool<{
@@ -7,7 +7,7 @@
7
7
  * This module provides convenience functions for running the complete
8
8
  * analysis pipeline using the core analysis logic.
9
9
  */
10
- import type { DFG, ProjectProfile } from 'circle-ir';
10
+ import type { DFG, ProjectProfile, TaintSanitizer } from 'circle-ir';
11
11
  import type { TypeShape } from '../../security-scan/source-classifier.js';
12
12
  import type { LLMConfig } from '../../llm/config.js';
13
13
  import type { PackageCoordinate } from '../../cache/coordinate.js';
@@ -61,6 +61,12 @@ export interface WorkflowOutput {
61
61
  pairsFailed?: number;
62
62
  /** cognium-ai#564 — findings kept unverified because their chunk failed (`verification.verification_error`). */
63
63
  findingsUnverified?: number;
64
+ /**
65
+ * cognium-ai#471 — findings kept with `verification.verdict: 'FALSE_POSITIVE'`
66
+ * (`refuted_by: 'omission'`): every pair behind them was submitted to the
67
+ * verifier and omitted from a successful answer. Optional; absent means 0.
68
+ */
69
+ findingsRefutedByOmission?: number;
64
70
  /** cognium-ai#564 — the failure reason(s), present only when a chunk failed. */
65
71
  verificationError?: string;
66
72
  totalTimeMs: number;
@@ -121,6 +127,29 @@ verifyAccounting?: {
121
127
  unverifiedPairs?: UnverifiedPair[];
122
128
  /** cognium-ai#564 — the failure reason, stamped as `verification_error`. */
123
129
  verificationError?: string;
130
+ /**
131
+ * cognium-ai#471 — every (source, sink) pair submitted to the verifier.
132
+ * With it, a static finding whose pairs were all submitted, none failed,
133
+ * and none came back is labelled `FALSE_POSITIVE` (refuted by omission —
134
+ * the batch prompt's contract) instead of `UNVERIFIED`. Optional: legacy
135
+ * callers omit it and keep the pre-#471 `UNVERIFIED` label.
136
+ */
137
+ submittedPairs?: UnverifiedPair[];
138
+ },
139
+ /**
140
+ * cognium-ai#460 — the rest of `generateFindings`' argument list. With
141
+ * only (sources, sinks, dfg, file) every precision gate is off: the
142
+ * non-executable-line gate (needs `sourceCode` + `language`), sanitizer
143
+ * credit, and the method-scoped proximity gate (`types`). The static
144
+ * scanner passes all of them; this makes the detector index — and so the
145
+ * #264 verify-parity gate — use the same gates. (`flows` is not forwarded:
146
+ * it would turn on cognium-dev#372 flow emission, an additive change.) Optional: legacy
147
+ * callers (tests) omit it and keep the pre-#460 behaviour, except that
148
+ * `types` / `sourceCode` (already parameters) are now forwarded too.
149
+ */
150
+ taintContext?: {
151
+ language?: string;
152
+ sanitizers?: TaintSanitizer[];
124
153
  }): WorkflowOutput;
125
154
  /**
126
155
  * Run security analysis on a single file
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../../src/agents/mastra/workflow.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../../src/agents/mastra/workflow.ts"],"names":[],"mappings":"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"}
@@ -135,6 +135,9 @@ coordinate) {
135
135
  // DFG/proximity gating + dedup-merge with provenance collapse,
136
136
  // replacing the previous N×M cross-product.
137
137
  dfg: result.dfg,
138
+ // cognium-ai#460 — sanitizers for `generateFindings` parity with
139
+ // the static scanner (see `runReport`'s `taintContext`).
140
+ sanitizers: result.taint.sanitizers ?? [],
138
141
  // cognium-ai#154 (L5c) — extract the CK + import/annotation fingerprint
139
142
  // while the IR is live so the top-level cluster pass in
140
143
  // `SecurityScanner.scan()` can consume it without re-invoking
@@ -562,6 +565,8 @@ contextProfile) {
562
565
  pairsDroppedByBudget: 0,
563
566
  pairsFailed: 0,
564
567
  unverifiedPairs: [],
568
+ // cognium-ai#471 — nothing was sent to the verifier.
569
+ submittedPairs: [],
565
570
  verificationError: undefined,
566
571
  processingTimeMs: 0,
567
572
  };
@@ -691,6 +696,13 @@ contextProfile) {
691
696
  ? (batchResult.unverified_pairs ?? []).map((p) => ({ sourceLine: p.source_line, sinkLine: p.sink_line }))
692
697
  : [],
693
698
  verificationError: batchResult.status === 'error' ? (batchResult.error ?? 'verification_chunk_failed') : undefined,
699
+ // cognium-ai#471 — the (source, sink) pairs this file put to the
700
+ // verifier. The batch prompt has the model OMIT every non-TRUE_POSITIVE
701
+ // pair, so a submitted pair that comes back absent from a successful
702
+ // chunk is the verifier's refutation. runReport needs the set to tell
703
+ // "refuted by omission" from "never asked" (sampling) when it labels the
704
+ // static findings it keeps.
705
+ submittedPairs: limitedSources.flatMap((s) => limitedSinks.map((k) => ({ sourceLine: s.line, sinkLine: k.line }))),
694
706
  processingTimeMs: batchResult.processingTimeMs,
695
707
  };
696
708
  }
@@ -714,6 +726,7 @@ contextProfile) {
714
726
  pairsDroppedByBudget: 0,
715
727
  pairsFailed: unverifiedPairs.length,
716
728
  unverifiedPairs,
729
+ submittedPairs: [], // #471 — nothing was answered
717
730
  verificationError: (error instanceof Error ? error.message : String(error)) || 'verification_failed',
718
731
  processingTimeMs: Date.now() - startTime,
719
732
  };
@@ -752,7 +765,19 @@ sourceCode,
752
765
  * much the verifier was asked about. Optional: legacy callers (tests) omit
753
766
  * it and the stats fall back to the returned counts.
754
767
  */
755
- verifyAccounting) {
768
+ verifyAccounting,
769
+ /**
770
+ * cognium-ai#460 — the rest of `generateFindings`' argument list. With
771
+ * only (sources, sinks, dfg, file) every precision gate is off: the
772
+ * non-executable-line gate (needs `sourceCode` + `language`), sanitizer
773
+ * credit, and the method-scoped proximity gate (`types`). The static
774
+ * scanner passes all of them; this makes the detector index — and so the
775
+ * #264 verify-parity gate — use the same gates. (`flows` is not forwarded:
776
+ * it would turn on cognium-dev#372 flow emission, an additive change.) Optional: legacy
777
+ * callers (tests) omit it and keep the pre-#460 behaviour, except that
778
+ * `types` / `sourceCode` (already parameters) are now forwarded too.
779
+ */
780
+ taintContext) {
756
781
  const vulnerabilities = [];
757
782
  // cognium-ai#264 Defect 1 — detector candidate index for verify-path parity.
758
783
  // `generateFindings()` applies `canSourceReachSink` (type coverage) + the
@@ -766,7 +791,7 @@ verifyAccounting) {
766
791
  // gap #258 left open (path_traversal's ceiling is `high`, so the rule cap alone
767
792
  // still reads C+H). Computed once and shared with the no-verification branch.
768
793
  const effectiveDfg = dfg ?? { defs: [], uses: [], chains: [] };
769
- const detectorFindings = generateFindings(mergedSources, mergedSinks, effectiveDfg, filePath);
794
+ const detectorFindings = generateFindings(mergedSources, mergedSinks, effectiveDfg, filePath, sourceCode, taintContext?.language, taintContext?.sanitizers ?? [], (types ?? []));
770
795
  const detectorSevRank = { critical: 4, high: 3, medium: 2, low: 1 };
771
796
  const detectorSinkSeverity = new Map();
772
797
  for (const df of detectorFindings) {
@@ -1033,6 +1058,32 @@ verifyAccounting) {
1033
1058
  const srcs = finding.evidence?.sources ?? [{ line: finding.source.line }];
1034
1059
  return srcs.some((s) => failedPairKeys.has(`${s.line}:${finding.sink.line}`));
1035
1060
  };
1061
+ // cognium-ai#471 — a static finding the verifier was asked about and did not
1062
+ // confirm. The batch prompt has the model OMIT every non-TRUE_POSITIVE pair
1063
+ // ("an omitted pair is counted as assessed-and-safe"), so when a file's
1064
+ // verifier answer contains no TRUE_POSITIVE this branch used to keep every
1065
+ // static finding as `UNVERIFIED` — the verifier's refutation was relabelled
1066
+ // "no verdict", which circle-pack maps to `llm_result.verdict: UNCERTAIN`.
1067
+ // That is how 81,842 skill-scan findings produced 0 FALSE_POSITIVE.
1068
+ //
1069
+ // A finding is refuted by omission only when the answer is unambiguous:
1070
+ // - the caller passed the submitted-pair set (legacy callers: no change);
1071
+ // - no pair was dropped by our fan-out budget, and none of the pairs the
1072
+ // model returned failed to map (a hallucinated line could be a TP we
1073
+ // cannot place);
1074
+ // - every contributing (source, sink) pair was submitted (a pair the
1075
+ // sampler never sent was never asked about);
1076
+ // - no contributing pair sits in a failed chunk (#564 keeps those open).
1077
+ const submittedPairKeys = new Set((verifyAccounting?.submittedPairs ?? []).map((p) => `${p.sourceLine}:${p.sinkLine}`));
1078
+ const omissionIsRefutation = submittedPairKeys.size > 0 &&
1079
+ (verifyAccounting?.pairsDroppedByBudget ?? 0) === 0 &&
1080
+ (verifyAccounting?.pairsUnmapped ?? 0) === 0;
1081
+ const refutedByOmission = (finding) => {
1082
+ if (!omissionIsRefutation || hitsFailedPair(finding))
1083
+ return false;
1084
+ const srcs = finding.evidence?.sources ?? [{ line: finding.source.line }];
1085
+ return srcs.length > 0 && srcs.every((s) => submittedPairKeys.has(`${s.line}:${finding.sink.line}`));
1086
+ };
1036
1087
  const verifiedBranch = verificationEnabled && verificationResults.length > 0;
1037
1088
  const verifiedLocs = new Set(vulnerabilities.map((v) => `${v.sink?.line}:${v.type}`));
1038
1089
  if (!verifiedBranch || failedPairKeys.size > 0) {
@@ -1151,7 +1202,25 @@ verifyAccounting) {
1151
1202
  mode: 'attempted',
1152
1203
  verification_error: verificationError ?? 'verification_chunk_failed',
1153
1204
  }
1154
- : { verdict: 'UNVERIFIED', attempted: true, llm_verified: false, mode: 'attempted' }, // #331 three-state
1205
+ : refutedByOmission(finding)
1206
+ ? {
1207
+ // cognium-ai#471 — the verifier answered, and its answer
1208
+ // (per the batch prompt's omit-non-TP contract) is "not a
1209
+ // true positive". Kept, not dropped: dropping would change
1210
+ // recall on every LLM scan, which is a product decision;
1211
+ // labelling it lets consumers see the refutation.
1212
+ verdict: 'FALSE_POSITIVE',
1213
+ attempted: true,
1214
+ llm_verified: false,
1215
+ mode: 'llm',
1216
+ refuted_by: 'omission',
1217
+ reasoning_source: 'template',
1218
+ reasoning: 'Submitted to the LLM verifier, which did not confirm it: the batch verifier ' +
1219
+ 'omits every pair it does not judge a TRUE_POSITIVE (cognium-ai#471).',
1220
+ confidence: 0,
1221
+ exploitability: 'none',
1222
+ }
1223
+ : { verdict: 'UNVERIFIED', attempted: true, llm_verified: false, mode: 'attempted' }, // #331 three-state
1155
1224
  // #26: surface the provenance label so downstream consumers
1156
1225
  // (cognium-ai CLI output, SARIF emitter) can distinguish
1157
1226
  // static-only / LLM-only / mixed findings without re-deriving.
@@ -1177,6 +1246,9 @@ verifyAccounting) {
1177
1246
  // cognium-ai#564 — failed-chunk pairs, and the findings kept for them.
1178
1247
  pairsFailed: verifyAccounting?.pairsFailed ?? 0,
1179
1248
  findingsUnverified: vulnerabilities.filter((v) => v.verification?.verification_error !== undefined).length,
1249
+ // cognium-ai#471 — findings kept with `verdict: 'FALSE_POSITIVE'`
1250
+ // because the verifier omitted (refuted) every pair behind them.
1251
+ findingsRefutedByOmission: vulnerabilities.filter((v) => v.verification?.refuted_by === 'omission').length,
1180
1252
  ...(verificationError !== undefined ? { verificationError } : {}),
1181
1253
  totalTimeMs: patternMatchTimeMs + enrichTimeMs + verifyTimeMs,
1182
1254
  },
@@ -1278,7 +1350,10 @@ export async function analyzeFile(filePath, sourceCode, options) {
1278
1350
  pairsFailed: verifyResult.pairsFailed,
1279
1351
  unverifiedPairs: verifyResult.unverifiedPairs,
1280
1352
  verificationError: verifyResult.verificationError,
1281
- });
1353
+ submittedPairs: verifyResult.submittedPairs, // cognium-ai#471
1354
+ },
1355
+ // cognium-ai#460 — full generateFindings argument list.
1356
+ { language, sanitizers: patternResult.sanitizers });
1282
1357
  // cognium-ai#154 (L5c) — surface the cluster vector extracted while the
1283
1358
  // IR was live in `runPatternMatch`. Keeps `runReport`'s signature
1284
1359
  // untouched (its job is source×sink → vuln synthesis; the vector is
@@ -1331,7 +1406,10 @@ export async function* analyzeFileStream(filePath, sourceCode, options) {
1331
1406
  pairsFailed: verifyResult.pairsFailed,
1332
1407
  unverifiedPairs: verifyResult.unverifiedPairs,
1333
1408
  verificationError: verifyResult.verificationError,
1334
- });
1409
+ submittedPairs: verifyResult.submittedPairs, // cognium-ai#471
1410
+ },
1411
+ // cognium-ai#460 — full generateFindings argument list.
1412
+ { language, sanitizers: patternResult.sanitizers });
1335
1413
  yield { step: 'report', status: 'completed', data: reportResult };
1336
1414
  }
1337
1415
  // Legacy export for compatibility