circle-ir-ai 4.27.0 → 4.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +170 -0
- package/dist/agents/mastra/instance.d.ts +1 -1
- package/dist/agents/mastra/workflow.d.ts +32 -2
- package/dist/agents/mastra/workflow.d.ts.map +1 -1
- package/dist/agents/mastra/workflow.js +124 -23
- package/dist/agents/mastra/workflow.js.map +1 -1
- package/dist/cache/discovery-cache.d.ts.map +1 -1
- package/dist/cache/discovery-cache.js +11 -2
- package/dist/cache/discovery-cache.js.map +1 -1
- package/dist/cache/tree-cache.d.ts +14 -1
- package/dist/cache/tree-cache.d.ts.map +1 -1
- package/dist/cache/tree-cache.js +4 -1
- package/dist/cache/tree-cache.js.map +1 -1
- package/dist/llm/ax-client.d.ts +102 -10
- package/dist/llm/ax-client.d.ts.map +1 -1
- package/dist/llm/ax-client.js +3 -2
- package/dist/llm/ax-client.js.map +1 -1
- package/dist/llm/discovery.d.ts +39 -0
- package/dist/llm/discovery.d.ts.map +1 -1
- package/dist/llm/discovery.js +115 -61
- package/dist/llm/discovery.js.map +1 -1
- package/dist/llm/line-numbers.d.ts +36 -0
- package/dist/llm/line-numbers.d.ts.map +1 -0
- package/dist/llm/line-numbers.js +47 -0
- package/dist/llm/line-numbers.js.map +1 -0
- package/dist/llm/prompts/index.d.ts.map +1 -1
- package/dist/llm/prompts/index.js +5 -0
- package/dist/llm/prompts/index.js.map +1 -1
- package/dist/llm/prompts/languages.d.ts +0 -12
- package/dist/llm/prompts/languages.d.ts.map +1 -1
- package/dist/llm/prompts/languages.js +5 -0
- package/dist/llm/prompts/languages.js.map +1 -1
- package/dist/llm/prompts/rust.d.ts.map +1 -1
- package/dist/llm/prompts/rust.js +5 -0
- package/dist/llm/prompts/rust.js.map +1 -1
- package/dist/security-scan/scanner.d.ts +7 -0
- package/dist/security-scan/scanner.d.ts.map +1 -1
- package/dist/security-scan/scanner.js +13 -2
- package/dist/security-scan/scanner.js.map +1 -1
- package/dist/skills/capability-mismatch.js +25 -1
- package/dist/skills/capability-mismatch.js.map +1 -1
- package/dist/skills/code-threat-patterns.d.ts +42 -3
- package/dist/skills/code-threat-patterns.d.ts.map +1 -1
- package/dist/skills/code-threat-patterns.js +99 -25
- package/dist/skills/code-threat-patterns.js.map +1 -1
- package/dist/skills/instruction-safety.d.ts +18 -1
- package/dist/skills/instruction-safety.d.ts.map +1 -1
- package/dist/skills/instruction-safety.js +45 -3
- package/dist/skills/instruction-safety.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -116,6 +116,176 @@ 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.29.0] - 2026-10-17
|
|
120
|
+
|
|
121
|
+
Minor release: `circle-ir` 4.15.0 → 4.16.1, LLM enrichment sinks anchored on the `scan --llm`
|
|
122
|
+
merge path, and corrected SecuriBench and C# oracle scoring. 4.16.0 was not pinned: it carried
|
|
123
|
+
four regressions, fixed upstream in 4.16.1 before this bump.
|
|
124
|
+
|
|
125
|
+
### Changed
|
|
126
|
+
- **`circle-ir` 4.15.0 → 4.16.1** (exact pin). 4.16.0 adds the navigation API (callers per
|
|
127
|
+
overload and through dispatch), `navigationTypes` extraction, `AnalyzerOptions.resources`, and
|
|
128
|
+
many recall and precision fixes across Java, C#, JS/TS, Go, Python and Rust. 4.16.1 fixes four
|
|
129
|
+
regressions found while benchmarking this bump. Java `analyze()` is back to 4.15.0 speed on
|
|
130
|
+
large classes (one NiFi file took 638 s on 4.16.0 and 0.83 s on 4.16.1; cognium-dev#806). A
|
|
131
|
+
`Character.is*` predicate on tainted text is no longer an `external_taint_escape`, which clears
|
|
132
|
+
SecuriBench Sanitizers2/6 (cognium-dev#807). Two C# false positives are fixed: a field
|
|
133
|
+
sanitized in another method (cognium-dev#808) and an `Array.IndexOf` allowlist guard
|
|
134
|
+
(cognium-dev#809). The underlying Java name-collision cause of #807 is cognium-dev#814.
|
|
135
|
+
|
|
136
|
+
Static suites, compared with 4.15.0 (packed 4.16.1, 27 runners):
|
|
137
|
+
- SecuriBench 97.2% / 6.7% → 97.7% / 6.7%;
|
|
138
|
+
- C# oracle 76.6% / 0.8% (3/367) → 77.1% / 0.3% (1/367);
|
|
139
|
+
- CWE-Bench-Java static 64 → 65 of 120;
|
|
140
|
+
- Python synthetic 72.2% → 74.0%;
|
|
141
|
+
- Vulnerability-goapp FPR 14.3% → 0.0%;
|
|
142
|
+
- the other 21 identical.
|
|
143
|
+
|
|
144
|
+
Figures before the scoring changes below.
|
|
145
|
+
|
|
146
|
+
### Fixed
|
|
147
|
+
- **LLM enrichment sinks are anchored in `runMerge`** (#330). The `scan --llm` merge path now
|
|
148
|
+
applies `resolveDiscoveryAnchor`, so an enrichment sink lands on a call node or carries
|
|
149
|
+
`llm-unanchored`, as in discovery mode. `runPatternMatch` forwards the IR calls for anchoring,
|
|
150
|
+
and LLM sinks merge on their post-anchor key.
|
|
151
|
+
- **SecurityScanner test-tree excludes (cognium-ai#615)**. The engine walker no longer uses
|
|
152
|
+
unanchored `**/test/**` / `**/tests/**`, which also matched a Java package segment named
|
|
153
|
+
`test` under main source. It now mirrors the `init` template: `test/**`, `tests/**`,
|
|
154
|
+
`**/src/test/**`, `**/__tests__/**`.
|
|
155
|
+
|
|
156
|
+
### Benchmarks
|
|
157
|
+
- **SecuriBench Micro labels.** The runner read only the `@servlet vuln_count` annotation. Where
|
|
158
|
+
that annotation contradicts both `getVulnerabilityCount()` and the `/* BAD */` markers, the
|
|
159
|
+
method and markers now win, and every disagreement is printed (`benchmarks/lib/securibench-labels.ts`).
|
|
160
|
+
- Overrides: `pred/Pred2.java` 0 → 1 (the suite's one FP was a true positive),
|
|
161
|
+
`collections/Collections10.java` 0 → 1, `inter/Inter10.java` 2 → 1.
|
|
162
|
+
- Six other disagreements keep the annotation, because the markers side with it.
|
|
163
|
+
- On circle-ir 4.16.1: **TPR 97.3%, FPR 0.0%** (107/110, 0/13). With the old labels it was
|
|
164
|
+
97.7% / 6.7% (105/108, 1/15).
|
|
165
|
+
- **C# oracle clean controls are scored against their category's taint sinks**
|
|
166
|
+
(`cleanScope()`, `benchmarks/lib/csharp-oracle-scoring.ts`).
|
|
167
|
+
- A placeholder `category: "clean"` case resolves through its paired `*Tp.cs`.
|
|
168
|
+
- 33 controls for a non-taint signal (for example `IncorrectDefaultPermsSafe.cs`, the
|
|
169
|
+
CWE-276 twin) leave the denominator.
|
|
170
|
+
- 95 general-FP or unmapped cases keep the any-flow rule.
|
|
171
|
+
- On circle-ir 4.16.1: **TPR 77.1% (340/441), FPR 0.0% (0/334)**.
|
|
172
|
+
|
|
173
|
+
## [4.28.0] - 2026-10-15
|
|
174
|
+
|
|
175
|
+
Minor release: `circle-ir` 4.14.2 → 4.15.0, discovery prompts carry line numbers, verifier
|
|
176
|
+
refutations get their own label, and skill-detector fixes. Includes 4.27.1, which was not
|
|
177
|
+
published on its own.
|
|
178
|
+
|
|
179
|
+
### Changed
|
|
180
|
+
- **`circle-ir` 4.14.2 → 4.15.0** (exact pin). A precision release upstream: new sources
|
|
181
|
+
(Express destructured requests, GraphQL resolver-map arguments) and sinks (Spring handlers
|
|
182
|
+
returning request input as HTML, `loadClass` on context class loaders), and many cleared false
|
|
183
|
+
positives (const-origin `fetch` ssrf, const-only `RegExp` redos, `JSON.stringify` and C# hashes
|
|
184
|
+
or dictionary lookups as `external_taint_escape`, length/count/boolean tests of tainted text).
|
|
185
|
+
See circle-ir's changelog for the list.
|
|
186
|
+
|
|
187
|
+
### Fixed
|
|
188
|
+
- **Skill code-threat detectors judge a comment by the file's own comment syntax** (cognium-ai#647
|
|
189
|
+
§1, follow-up to #648/#653). `isCommentOrDoc`, the trailing-comment cut and the #352 block mask
|
|
190
|
+
used the union of every language's openers, so real code passed as a comment and every detector
|
|
191
|
+
skipped it: Python `n = 4 // 2; os.system(…)` (floor division), Python `note: int = …` and JS
|
|
192
|
+
`example: execSync(…)` (an annotated assignment and a label, read as `Note:` / `Example:` prose),
|
|
193
|
+
Python `*_, = [os.system(…)]` and a JS generator method `* run() {…}` (read as JSDoc
|
|
194
|
+
continuations), shell `ls /*` (opened a block comment that masked the rest of the file) and
|
|
195
|
+
shell `echo ${#HOME}; curl … | bash` (a length expansion read as a `#` comment). Each hid a
|
|
196
|
+
`curl … | sh`. Python/shell/Ruby/YAML-family files now use `#` only (shell: at the start of a
|
|
197
|
+
word), C-family files `//` and `/* */`, PHP both; a file with no recognised extension
|
|
198
|
+
(SKILL.md, references) keeps the old union. Real comments and docstrings stay suppressed.
|
|
199
|
+
- **Malformed model output no longer aborts or voids a skill scan in LLM mode** (cognium-ai#647
|
|
200
|
+
§2, follow-up to #648). `capability-mismatch` iterated the model's reply as-is: an object reply
|
|
201
|
+
threw "mismatches is not iterable" and a `null` entry threw on `.type`, and both escaped
|
|
202
|
+
`analyzeSkillBundle` as "Skill analysis failed", taking the deterministic findings with them.
|
|
203
|
+
`instruction-safety` threw on a numeric `severity` or non-string `evidence`, which turned a real
|
|
204
|
+
answer into "could not analyze". Entries with no type are dropped, other fields are coerced
|
|
205
|
+
(`sanitizeRawThreat`, exported).
|
|
206
|
+
- **`cross_skill_interference` from the instruction-safety LLM needs another skill, plugin, tool
|
|
207
|
+
or agent as the target** (cognium-ai#415, the LLM half; the static half shipped in 4.25.0).
|
|
208
|
+
`downweightInstructionOverreads` floors the finding to `info` unless its quote passes the static
|
|
209
|
+
detector's governed-verb rule (`isCrossSkillImperative`, exported), with sibling-qualified tools,
|
|
210
|
+
agents and MCP servers also accepted. Both #415 repros ("These override the generic
|
|
211
|
+
file-writing … instructions below", "Remove the red-eye from this image") floor; "disable the
|
|
212
|
+
other installed skills" keeps its severity. Category 19 of the prompt now says the same.
|
|
213
|
+
Kill switch: `SKILL_INSTRUCTION_OVERREAD_GATE=0`.
|
|
214
|
+
|
|
215
|
+
skills-validation static gate (`.synced` adapter, `SKIP_INSTRUCTION=1`, oracle v21): 57/152
|
|
216
|
+
required types, 0 safe/benign fixtures with findings, before and after; byte-identical except
|
|
217
|
+
ordering. That adapter is not the one behind #647's 97/152 figure.
|
|
218
|
+
- **A verifier refutation is no longer reported as "no verdict"** (cognium-ai#471). The batch
|
|
219
|
+
verifier prompt has the model omit every pair it does not judge a TRUE_POSITIVE. When a file's
|
|
220
|
+
answer held no TRUE_POSITIVE, `runReport` fell through to the static branch and kept every
|
|
221
|
+
detector finding with `verification.verdict: 'UNVERIFIED'`, which circle-pack maps to
|
|
222
|
+
`llm_result.verdict: 'UNCERTAIN'`. That is how 81,842 skill-scan findings showed 31%
|
|
223
|
+
TRUE_POSITIVE, 69% UNCERTAIN and no FALSE_POSITIVE: the refutations were there, under the wrong
|
|
224
|
+
label. Such a finding now carries `verdict: 'FALSE_POSITIVE'`, `refuted_by: 'omission'`,
|
|
225
|
+
`mode: 'llm'`, `llm_verified: false`. It is kept, not dropped, so finding counts do not change;
|
|
226
|
+
`exploitable` stays `false`, as it was for `UNVERIFIED`. The label applies only when the answer
|
|
227
|
+
is unambiguous: every pair behind the finding was submitted, none was in a failed chunk (#564
|
|
228
|
+
still keeps those open), no pair was dropped by the fan-out budget, and no returned pair failed
|
|
229
|
+
to map. Otherwise the finding stays `UNVERIFIED`. `ScanMeta.verification` gains
|
|
230
|
+
`findingsRefutedByOmission`, present only when non-zero. The prompt is unchanged. An explicit
|
|
231
|
+
FALSE_POSITIVE pair is still dropped by the verified branch, as before, so it never reaches a
|
|
232
|
+
finding. `tests/verifier-false-positive-471.test.ts` puts a stubbed FALSE_POSITIVE and an empty
|
|
233
|
+
`pairs` answer through the parser, `runReport` and `scanDirectory`.
|
|
234
|
+
- **Discovery prompts show the line numbers they ask for** (#658). `DiscoveryEngine.analyzeMethod`
|
|
235
|
+
asked the model for an absolute `"line"` but sent the method code without line numbers, so the
|
|
236
|
+
model guessed file lines from the text; a guessed line is how a discovery finding lands on a
|
|
237
|
+
signature, a `throw` or a closing brace (#330). Each line of the method is now prefixed with
|
|
238
|
+
its file line number (`42 | …`), and the prompt says `line` must be one of those numbers. A
|
|
239
|
+
line-number prefix the model copies into `code` is stripped. The #657 anchoring (move onto a
|
|
240
|
+
call the snippet names, else `llm-unanchored`) stays as the safety net. The language
|
|
241
|
+
discoverers on the `scan --llm` path (`discoverSourcesForLanguage` /
|
|
242
|
+
`discoverSinksForLanguage`, called by `runEnrich`) had the same defect on the whole file and
|
|
243
|
+
get the same fix, numbered from line 1. The batch verifier already numbered its windows. The
|
|
244
|
+
user prompt is now built by the exported `buildDiscoveryUserPrompt`.
|
|
245
|
+
|
|
246
|
+
### Changed
|
|
247
|
+
- **Discovery cache key versions `disc-v3` → `disc-v4` and `lang-disc-v2` → `lang-disc-v3`**
|
|
248
|
+
(#658). Warm entries were answered from unnumbered code, so every local and R2 discovery entry
|
|
249
|
+
misses once and re-asks the model. The method cache key still holds no line numbers: an entry
|
|
250
|
+
now records the start line its prompt was numbered from, and a hit on a byte-identical method
|
|
251
|
+
elsewhere shifts its `line` and `source.line` by the difference before anchoring (before, a
|
|
252
|
+
hit kept the other method's lines and was clamped into this one).
|
|
253
|
+
|
|
254
|
+
### Benchmarks (local, circle-ir 4.15.0)
|
|
255
|
+
26 static suites run on 4.14.2 and 4.15.0 back to back, same source, same machine (2026-10-15).
|
|
256
|
+
All 26 score identically: OWASP 1415/1415, Juliet 243/243, CWE-Bench-Java static 64/120
|
|
257
|
+
(64/111 evaluable), secrets recall 100% / F1 98.2%, csharp-oracle and C# Juliet unchanged.
|
|
258
|
+
4.15.0's fixes land on shapes these corpora do not contain. The openclaw `beam-session.js`
|
|
259
|
+
redos (cognium-ai#671) still reports on 4.15.0: circle-ir #681 does not cover that file's
|
|
260
|
+
pattern. No LLM benchmark was re-run for this release; #658's before/after discovery run on
|
|
261
|
+
gemma3:12b is still outstanding.
|
|
262
|
+
|
|
263
|
+
## [4.27.1] - 2026-10-14
|
|
264
|
+
|
|
265
|
+
Patch release: one precision fix on the LLM scan path. `circle-ir` stays on 4.14.2.
|
|
266
|
+
|
|
267
|
+
### Fixed
|
|
268
|
+
- **The LLM scan path applies the same precision gates as `scan --no-llm`** (#460; PR #672).
|
|
269
|
+
`runReport` built its detector index (the no-verification findings and the #264 verify-parity
|
|
270
|
+
gate) with `generateFindings(sources, sinks, dfg, file)` only, so the non-executable-line gate,
|
|
271
|
+
sanitizer credit and the method-scoped proximity gate were all off. It now forwards
|
|
272
|
+
`sourceCode` and `types`, plus a new optional trailing `taintContext { language, sanitizers }`;
|
|
273
|
+
`PatternMatchResult` gains `sanitizers`. `flows` is still not forwarded, because in circle-ir it
|
|
274
|
+
also emits a finding per unsanitized flow (cognium-dev#372). An LLM-discovered pair that crosses
|
|
275
|
+
a function boundary, sits on a comment line or is covered by a sanitizer is now dropped, from
|
|
276
|
+
both the unverified output and the verify-parity gate.
|
|
277
|
+
|
|
278
|
+
### Changed
|
|
279
|
+
- **Tree-cache key version `tree-v1` → `tree-v2`.** Warm entries carry no sanitizers, so every
|
|
280
|
+
local and R2 tree-cache entry re-parses once. The cost is one parse per file; it makes no LLM
|
|
281
|
+
calls.
|
|
282
|
+
|
|
283
|
+
### Benchmarks (local, gemma3:12b)
|
|
284
|
+
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
|
|
285
|
+
67/10 → 70/7, FP/TN 40/37 → 39/38, findings 976 → 975, LLM calls 462 → 462 with 0 failures. One
|
|
286
|
+
run per arm, so this is within LLM noise: no measured recall loss, no measured precision gain.
|
|
287
|
+
The static suites do not exercise `runReport`.
|
|
288
|
+
|
|
119
289
|
## [4.27.0] - 2026-10-12
|
|
120
290
|
|
|
121
291
|
Minor release: one change to which files `scan` analyses, and additive output fields.
|
|
@@ -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,11 +7,12 @@
|
|
|
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';
|
|
14
14
|
import { type ClusterVector } from '../../cluster/cluster.js';
|
|
15
|
+
import { type AnchorCallNode } from '../../llm/anchoring.js';
|
|
15
16
|
export { patternMatchStep, enrichStep, mergeStep, verifyStep, reportStep, } from './steps.js';
|
|
16
17
|
export type VerificationStrategy = 'complete' | 'sampled' | 'prioritized';
|
|
17
18
|
export interface WorkflowInput {
|
|
@@ -61,6 +62,12 @@ export interface WorkflowOutput {
|
|
|
61
62
|
pairsFailed?: number;
|
|
62
63
|
/** cognium-ai#564 — findings kept unverified because their chunk failed (`verification.verification_error`). */
|
|
63
64
|
findingsUnverified?: number;
|
|
65
|
+
/**
|
|
66
|
+
* cognium-ai#471 — findings kept with `verification.verdict: 'FALSE_POSITIVE'`
|
|
67
|
+
* (`refuted_by: 'omission'`): every pair behind them was submitted to the
|
|
68
|
+
* verifier and omitted from a successful answer. Optional; absent means 0.
|
|
69
|
+
*/
|
|
70
|
+
findingsRefutedByOmission?: number;
|
|
64
71
|
/** cognium-ai#564 — the failure reason(s), present only when a chunk failed. */
|
|
65
72
|
verificationError?: string;
|
|
66
73
|
totalTimeMs: number;
|
|
@@ -91,7 +98,7 @@ export interface WorkflowOutput {
|
|
|
91
98
|
}
|
|
92
99
|
/** cognium-ai#591 — why `runEnrich` made no discovery call for a file. */
|
|
93
100
|
export type EnrichmentSkipReason = 'prefilter' | 'minified';
|
|
94
|
-
export declare function runMerge(patternSources: any[], patternSinks: any[], enrichmentResult: any | null, confidenceThreshold: number, sourceCode?: string, filePath?: string, types?: ReadonlyArray<TypeShape>, language?: string): {
|
|
101
|
+
export declare function runMerge(patternSources: any[], patternSinks: any[], enrichmentResult: any | null, confidenceThreshold: number, sourceCode?: string, filePath?: string, types?: ReadonlyArray<TypeShape>, language?: string, irCalls?: ReadonlyArray<AnchorCallNode>): {
|
|
95
102
|
mergedSources: any[];
|
|
96
103
|
mergedSinks: any[];
|
|
97
104
|
};
|
|
@@ -121,6 +128,29 @@ verifyAccounting?: {
|
|
|
121
128
|
unverifiedPairs?: UnverifiedPair[];
|
|
122
129
|
/** cognium-ai#564 — the failure reason, stamped as `verification_error`. */
|
|
123
130
|
verificationError?: string;
|
|
131
|
+
/**
|
|
132
|
+
* cognium-ai#471 — every (source, sink) pair submitted to the verifier.
|
|
133
|
+
* With it, a static finding whose pairs were all submitted, none failed,
|
|
134
|
+
* and none came back is labelled `FALSE_POSITIVE` (refuted by omission —
|
|
135
|
+
* the batch prompt's contract) instead of `UNVERIFIED`. Optional: legacy
|
|
136
|
+
* callers omit it and keep the pre-#471 `UNVERIFIED` label.
|
|
137
|
+
*/
|
|
138
|
+
submittedPairs?: UnverifiedPair[];
|
|
139
|
+
},
|
|
140
|
+
/**
|
|
141
|
+
* cognium-ai#460 — the rest of `generateFindings`' argument list. With
|
|
142
|
+
* only (sources, sinks, dfg, file) every precision gate is off: the
|
|
143
|
+
* non-executable-line gate (needs `sourceCode` + `language`), sanitizer
|
|
144
|
+
* credit, and the method-scoped proximity gate (`types`). The static
|
|
145
|
+
* scanner passes all of them; this makes the detector index — and so the
|
|
146
|
+
* #264 verify-parity gate — use the same gates. (`flows` is not forwarded:
|
|
147
|
+
* it would turn on cognium-dev#372 flow emission, an additive change.) Optional: legacy
|
|
148
|
+
* callers (tests) omit it and keep the pre-#460 behaviour, except that
|
|
149
|
+
* `types` / `sourceCode` (already parameters) are now forwarded too.
|
|
150
|
+
*/
|
|
151
|
+
taintContext?: {
|
|
152
|
+
language?: string;
|
|
153
|
+
sanitizers?: TaintSanitizer[];
|
|
124
154
|
}): WorkflowOutput;
|
|
125
155
|
/**
|
|
126
156
|
* 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":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAoC,GAAG,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../../src/agents/mastra/workflow.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,6 +20,7 @@ import { resetLLMCallLimiter, isSwarmActive } from '../../llm/call-limiter.js';
|
|
|
20
20
|
import { resetLLMPerFileBreaker, resetLLMBreakerForScan } from '../../llm/ax-client.js';
|
|
21
21
|
import { getTreeCache, computeTreeCacheKey, computeRuleSetFingerprint, } from '../../cache/tree-cache.js';
|
|
22
22
|
import { extractClusterVector } from '../../cluster/cluster.js';
|
|
23
|
+
import { resolveDiscoveryAnchor, } from '../../llm/anchoring.js';
|
|
23
24
|
// Re-export the Mastra step definitions
|
|
24
25
|
export { patternMatchStep, enrichStep, mergeStep, verifyStep, reportStep, } from './steps.js';
|
|
25
26
|
// ============================================================================
|
|
@@ -135,6 +136,9 @@ coordinate) {
|
|
|
135
136
|
// DFG/proximity gating + dedup-merge with provenance collapse,
|
|
136
137
|
// replacing the previous N×M cross-product.
|
|
137
138
|
dfg: result.dfg,
|
|
139
|
+
// cognium-ai#460 — sanitizers for `generateFindings` parity with
|
|
140
|
+
// the static scanner (see `runReport`'s `taintContext`).
|
|
141
|
+
sanitizers: result.taint.sanitizers ?? [],
|
|
138
142
|
// cognium-ai#154 (L5c) — extract the CK + import/annotation fingerprint
|
|
139
143
|
// while the IR is live so the top-level cluster pass in
|
|
140
144
|
// `SecurityScanner.scan()` can consume it without re-invoking
|
|
@@ -142,6 +146,9 @@ coordinate) {
|
|
|
142
146
|
// pre-2.33.0 cached entries will deserialize with `clusterVector`
|
|
143
147
|
// undefined and be excluded from clustering on that scan.
|
|
144
148
|
clusterVector: extractClusterVector(result),
|
|
149
|
+
// cognium-ai#330 — forward calls so enrichment sinks can be anchored in
|
|
150
|
+
// `runMerge` the same way discovery mode does in `DiscoveryEngine`.
|
|
151
|
+
calls: (result.calls ?? []),
|
|
145
152
|
};
|
|
146
153
|
// cognium-ai#153 — populate persistent tree cache. Disk-write errors
|
|
147
154
|
// never fail the workflow. #158 Round 2 — coordinate is honored by
|
|
@@ -355,7 +362,17 @@ function findEnclosing(types, line) {
|
|
|
355
362
|
}
|
|
356
363
|
return {};
|
|
357
364
|
}
|
|
358
|
-
|
|
365
|
+
function methodLineBounds(types, line) {
|
|
366
|
+
const { type, method } = findEnclosing(types, line);
|
|
367
|
+
if (method) {
|
|
368
|
+
return { start: method.start_line, end: method.end_line };
|
|
369
|
+
}
|
|
370
|
+
if (type) {
|
|
371
|
+
return { start: type.start_line, end: type.end_line };
|
|
372
|
+
}
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
export function runMerge(patternSources, patternSinks, enrichmentResult, confidenceThreshold, sourceCode, filePath, types, language, irCalls) {
|
|
359
376
|
// #48: previously these maps were keyed by `line` alone, which
|
|
360
377
|
// collapsed any two sources / sinks that landed on the same source
|
|
361
378
|
// line — even when they had different types or different variables.
|
|
@@ -432,34 +449,43 @@ export function runMerge(patternSources, patternSinks, enrichmentResult, confide
|
|
|
432
449
|
}
|
|
433
450
|
if (enrichmentResult) {
|
|
434
451
|
for (const llmSink of enrichmentResult.additionalSinks) {
|
|
435
|
-
const
|
|
436
|
-
|
|
452
|
+
const llmSinkCode = sliceLine(sourceCode, llmSink.line);
|
|
453
|
+
let sinkLine = llmSink.line;
|
|
454
|
+
let anchor;
|
|
455
|
+
const bounds = methodLineBounds(types, llmSink.line);
|
|
456
|
+
if (irCalls && bounds) {
|
|
457
|
+
const snippet = llmSinkCode && llmSinkCode.trim().length > 0
|
|
458
|
+
? llmSinkCode
|
|
459
|
+
: llmSink.method
|
|
460
|
+
? `${llmSink.method}(`
|
|
461
|
+
: '';
|
|
462
|
+
const resolved = resolveDiscoveryAnchor(llmSink.line, patternSinks, irCalls, bounds.start, bounds.end, llmSink.type, snippet);
|
|
463
|
+
sinkLine = resolved.line;
|
|
464
|
+
if (resolved.anchor !== 'ir-sink') {
|
|
465
|
+
anchor = resolved.anchor;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const anchoredKey = sinkKey({ line: sinkLine, type: llmSink.type });
|
|
469
|
+
const existing = sinksByKey.get(anchoredKey);
|
|
437
470
|
if (existing) {
|
|
438
|
-
sinksByKey.set(
|
|
471
|
+
sinksByKey.set(anchoredKey, {
|
|
439
472
|
...existing,
|
|
440
473
|
confidence: Math.min(1.0, (existing.confidence || 0.8) + 0.2),
|
|
441
474
|
});
|
|
442
475
|
}
|
|
443
476
|
else if (llmSink.confidence >= confidenceThreshold) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
// safely returns false on undefined → phantom-line findings
|
|
447
|
-
// leak through to the report. Look up the source line so the
|
|
448
|
-
// existing sink-shape filters (and the new phantom-line guard)
|
|
449
|
-
// can do their job.
|
|
450
|
-
const llmSinkCode = sliceLine(sourceCode, llmSink.line);
|
|
451
|
-
sinksByKey.set(key, {
|
|
452
|
-
line: llmSink.line,
|
|
477
|
+
sinksByKey.set(anchoredKey, {
|
|
478
|
+
line: sinkLine,
|
|
453
479
|
type: llmSink.type,
|
|
454
480
|
cwe: llmSink.cwe,
|
|
455
|
-
location: `LLM-discovered at line ${
|
|
481
|
+
location: `LLM-discovered at line ${sinkLine}`,
|
|
456
482
|
confidence: llmSink.confidence,
|
|
457
483
|
method: llmSink.method,
|
|
458
484
|
argPositions: llmSink.argPositions,
|
|
459
|
-
code: llmSinkCode,
|
|
485
|
+
code: sliceLine(sourceCode, sinkLine) ?? llmSinkCode,
|
|
460
486
|
file: filePath,
|
|
461
|
-
// #26: pure-LLM-discovered sink — see source-side comment above.
|
|
462
487
|
discoveryMethod: 'llm',
|
|
488
|
+
...(anchor ? { anchor } : {}),
|
|
463
489
|
});
|
|
464
490
|
}
|
|
465
491
|
}
|
|
@@ -562,6 +588,8 @@ contextProfile) {
|
|
|
562
588
|
pairsDroppedByBudget: 0,
|
|
563
589
|
pairsFailed: 0,
|
|
564
590
|
unverifiedPairs: [],
|
|
591
|
+
// cognium-ai#471 — nothing was sent to the verifier.
|
|
592
|
+
submittedPairs: [],
|
|
565
593
|
verificationError: undefined,
|
|
566
594
|
processingTimeMs: 0,
|
|
567
595
|
};
|
|
@@ -691,6 +719,13 @@ contextProfile) {
|
|
|
691
719
|
? (batchResult.unverified_pairs ?? []).map((p) => ({ sourceLine: p.source_line, sinkLine: p.sink_line }))
|
|
692
720
|
: [],
|
|
693
721
|
verificationError: batchResult.status === 'error' ? (batchResult.error ?? 'verification_chunk_failed') : undefined,
|
|
722
|
+
// cognium-ai#471 — the (source, sink) pairs this file put to the
|
|
723
|
+
// verifier. The batch prompt has the model OMIT every non-TRUE_POSITIVE
|
|
724
|
+
// pair, so a submitted pair that comes back absent from a successful
|
|
725
|
+
// chunk is the verifier's refutation. runReport needs the set to tell
|
|
726
|
+
// "refuted by omission" from "never asked" (sampling) when it labels the
|
|
727
|
+
// static findings it keeps.
|
|
728
|
+
submittedPairs: limitedSources.flatMap((s) => limitedSinks.map((k) => ({ sourceLine: s.line, sinkLine: k.line }))),
|
|
694
729
|
processingTimeMs: batchResult.processingTimeMs,
|
|
695
730
|
};
|
|
696
731
|
}
|
|
@@ -714,6 +749,7 @@ contextProfile) {
|
|
|
714
749
|
pairsDroppedByBudget: 0,
|
|
715
750
|
pairsFailed: unverifiedPairs.length,
|
|
716
751
|
unverifiedPairs,
|
|
752
|
+
submittedPairs: [], // #471 — nothing was answered
|
|
717
753
|
verificationError: (error instanceof Error ? error.message : String(error)) || 'verification_failed',
|
|
718
754
|
processingTimeMs: Date.now() - startTime,
|
|
719
755
|
};
|
|
@@ -752,7 +788,19 @@ sourceCode,
|
|
|
752
788
|
* much the verifier was asked about. Optional: legacy callers (tests) omit
|
|
753
789
|
* it and the stats fall back to the returned counts.
|
|
754
790
|
*/
|
|
755
|
-
verifyAccounting
|
|
791
|
+
verifyAccounting,
|
|
792
|
+
/**
|
|
793
|
+
* cognium-ai#460 — the rest of `generateFindings`' argument list. With
|
|
794
|
+
* only (sources, sinks, dfg, file) every precision gate is off: the
|
|
795
|
+
* non-executable-line gate (needs `sourceCode` + `language`), sanitizer
|
|
796
|
+
* credit, and the method-scoped proximity gate (`types`). The static
|
|
797
|
+
* scanner passes all of them; this makes the detector index — and so the
|
|
798
|
+
* #264 verify-parity gate — use the same gates. (`flows` is not forwarded:
|
|
799
|
+
* it would turn on cognium-dev#372 flow emission, an additive change.) Optional: legacy
|
|
800
|
+
* callers (tests) omit it and keep the pre-#460 behaviour, except that
|
|
801
|
+
* `types` / `sourceCode` (already parameters) are now forwarded too.
|
|
802
|
+
*/
|
|
803
|
+
taintContext) {
|
|
756
804
|
const vulnerabilities = [];
|
|
757
805
|
// cognium-ai#264 Defect 1 — detector candidate index for verify-path parity.
|
|
758
806
|
// `generateFindings()` applies `canSourceReachSink` (type coverage) + the
|
|
@@ -766,7 +814,7 @@ verifyAccounting) {
|
|
|
766
814
|
// gap #258 left open (path_traversal's ceiling is `high`, so the rule cap alone
|
|
767
815
|
// still reads C+H). Computed once and shared with the no-verification branch.
|
|
768
816
|
const effectiveDfg = dfg ?? { defs: [], uses: [], chains: [] };
|
|
769
|
-
const detectorFindings = generateFindings(mergedSources, mergedSinks, effectiveDfg, filePath);
|
|
817
|
+
const detectorFindings = generateFindings(mergedSources, mergedSinks, effectiveDfg, filePath, sourceCode, taintContext?.language, taintContext?.sanitizers ?? [], (types ?? []));
|
|
770
818
|
const detectorSevRank = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
771
819
|
const detectorSinkSeverity = new Map();
|
|
772
820
|
for (const df of detectorFindings) {
|
|
@@ -1033,6 +1081,32 @@ verifyAccounting) {
|
|
|
1033
1081
|
const srcs = finding.evidence?.sources ?? [{ line: finding.source.line }];
|
|
1034
1082
|
return srcs.some((s) => failedPairKeys.has(`${s.line}:${finding.sink.line}`));
|
|
1035
1083
|
};
|
|
1084
|
+
// cognium-ai#471 — a static finding the verifier was asked about and did not
|
|
1085
|
+
// confirm. The batch prompt has the model OMIT every non-TRUE_POSITIVE pair
|
|
1086
|
+
// ("an omitted pair is counted as assessed-and-safe"), so when a file's
|
|
1087
|
+
// verifier answer contains no TRUE_POSITIVE this branch used to keep every
|
|
1088
|
+
// static finding as `UNVERIFIED` — the verifier's refutation was relabelled
|
|
1089
|
+
// "no verdict", which circle-pack maps to `llm_result.verdict: UNCERTAIN`.
|
|
1090
|
+
// That is how 81,842 skill-scan findings produced 0 FALSE_POSITIVE.
|
|
1091
|
+
//
|
|
1092
|
+
// A finding is refuted by omission only when the answer is unambiguous:
|
|
1093
|
+
// - the caller passed the submitted-pair set (legacy callers: no change);
|
|
1094
|
+
// - no pair was dropped by our fan-out budget, and none of the pairs the
|
|
1095
|
+
// model returned failed to map (a hallucinated line could be a TP we
|
|
1096
|
+
// cannot place);
|
|
1097
|
+
// - every contributing (source, sink) pair was submitted (a pair the
|
|
1098
|
+
// sampler never sent was never asked about);
|
|
1099
|
+
// - no contributing pair sits in a failed chunk (#564 keeps those open).
|
|
1100
|
+
const submittedPairKeys = new Set((verifyAccounting?.submittedPairs ?? []).map((p) => `${p.sourceLine}:${p.sinkLine}`));
|
|
1101
|
+
const omissionIsRefutation = submittedPairKeys.size > 0 &&
|
|
1102
|
+
(verifyAccounting?.pairsDroppedByBudget ?? 0) === 0 &&
|
|
1103
|
+
(verifyAccounting?.pairsUnmapped ?? 0) === 0;
|
|
1104
|
+
const refutedByOmission = (finding) => {
|
|
1105
|
+
if (!omissionIsRefutation || hitsFailedPair(finding))
|
|
1106
|
+
return false;
|
|
1107
|
+
const srcs = finding.evidence?.sources ?? [{ line: finding.source.line }];
|
|
1108
|
+
return srcs.length > 0 && srcs.every((s) => submittedPairKeys.has(`${s.line}:${finding.sink.line}`));
|
|
1109
|
+
};
|
|
1036
1110
|
const verifiedBranch = verificationEnabled && verificationResults.length > 0;
|
|
1037
1111
|
const verifiedLocs = new Set(vulnerabilities.map((v) => `${v.sink?.line}:${v.type}`));
|
|
1038
1112
|
if (!verifiedBranch || failedPairKeys.size > 0) {
|
|
@@ -1151,7 +1225,25 @@ verifyAccounting) {
|
|
|
1151
1225
|
mode: 'attempted',
|
|
1152
1226
|
verification_error: verificationError ?? 'verification_chunk_failed',
|
|
1153
1227
|
}
|
|
1154
|
-
:
|
|
1228
|
+
: refutedByOmission(finding)
|
|
1229
|
+
? {
|
|
1230
|
+
// cognium-ai#471 — the verifier answered, and its answer
|
|
1231
|
+
// (per the batch prompt's omit-non-TP contract) is "not a
|
|
1232
|
+
// true positive". Kept, not dropped: dropping would change
|
|
1233
|
+
// recall on every LLM scan, which is a product decision;
|
|
1234
|
+
// labelling it lets consumers see the refutation.
|
|
1235
|
+
verdict: 'FALSE_POSITIVE',
|
|
1236
|
+
attempted: true,
|
|
1237
|
+
llm_verified: false,
|
|
1238
|
+
mode: 'llm',
|
|
1239
|
+
refuted_by: 'omission',
|
|
1240
|
+
reasoning_source: 'template',
|
|
1241
|
+
reasoning: 'Submitted to the LLM verifier, which did not confirm it: the batch verifier ' +
|
|
1242
|
+
'omits every pair it does not judge a TRUE_POSITIVE (cognium-ai#471).',
|
|
1243
|
+
confidence: 0,
|
|
1244
|
+
exploitability: 'none',
|
|
1245
|
+
}
|
|
1246
|
+
: { verdict: 'UNVERIFIED', attempted: true, llm_verified: false, mode: 'attempted' }, // #331 three-state
|
|
1155
1247
|
// #26: surface the provenance label so downstream consumers
|
|
1156
1248
|
// (cognium-ai CLI output, SARIF emitter) can distinguish
|
|
1157
1249
|
// static-only / LLM-only / mixed findings without re-deriving.
|
|
@@ -1177,6 +1269,9 @@ verifyAccounting) {
|
|
|
1177
1269
|
// cognium-ai#564 — failed-chunk pairs, and the findings kept for them.
|
|
1178
1270
|
pairsFailed: verifyAccounting?.pairsFailed ?? 0,
|
|
1179
1271
|
findingsUnverified: vulnerabilities.filter((v) => v.verification?.verification_error !== undefined).length,
|
|
1272
|
+
// cognium-ai#471 — findings kept with `verdict: 'FALSE_POSITIVE'`
|
|
1273
|
+
// because the verifier omitted (refuted) every pair behind them.
|
|
1274
|
+
findingsRefutedByOmission: vulnerabilities.filter((v) => v.verification?.refuted_by === 'omission').length,
|
|
1180
1275
|
...(verificationError !== undefined ? { verificationError } : {}),
|
|
1181
1276
|
totalTimeMs: patternMatchTimeMs + enrichTimeMs + verifyTimeMs,
|
|
1182
1277
|
},
|
|
@@ -1255,7 +1350,7 @@ export async function analyzeFile(filePath, sourceCode, options) {
|
|
|
1255
1350
|
enrichResult = await runEnrich(filePath, sourceCode, patternResult.types, patternResult.imports, patternResult.patternSources, patternResult.patternSinks, enableEnrichment, language, llmConfig);
|
|
1256
1351
|
}
|
|
1257
1352
|
const mergeResult = runMerge(patternResult.patternSources, patternResult.patternSinks, enrichResult.enrichmentResult, confidenceThreshold, sourceCode, filePath, patternResult.types, // cognium-ai#128 gate
|
|
1258
|
-
language);
|
|
1353
|
+
language, patternResult.calls);
|
|
1259
1354
|
// L2 — same pattern as enrich: wrap when the operator has opted in
|
|
1260
1355
|
// via CP_VERIFY_TIMEOUT_MS.
|
|
1261
1356
|
let verifyResult;
|
|
@@ -1278,7 +1373,10 @@ export async function analyzeFile(filePath, sourceCode, options) {
|
|
|
1278
1373
|
pairsFailed: verifyResult.pairsFailed,
|
|
1279
1374
|
unverifiedPairs: verifyResult.unverifiedPairs,
|
|
1280
1375
|
verificationError: verifyResult.verificationError,
|
|
1281
|
-
|
|
1376
|
+
submittedPairs: verifyResult.submittedPairs, // cognium-ai#471
|
|
1377
|
+
},
|
|
1378
|
+
// cognium-ai#460 — full generateFindings argument list.
|
|
1379
|
+
{ language, sanitizers: patternResult.sanitizers });
|
|
1282
1380
|
// cognium-ai#154 (L5c) — surface the cluster vector extracted while the
|
|
1283
1381
|
// IR was live in `runPatternMatch`. Keeps `runReport`'s signature
|
|
1284
1382
|
// untouched (its job is source×sink → vuln synthesis; the vector is
|
|
@@ -1313,7 +1411,7 @@ export async function* analyzeFileStream(filePath, sourceCode, options) {
|
|
|
1313
1411
|
yield { step: 'enrich', status: 'completed', data: enrichResult };
|
|
1314
1412
|
yield { step: 'merge', status: 'running' };
|
|
1315
1413
|
const mergeResult = runMerge(patternResult.patternSources, patternResult.patternSinks, enrichResult.enrichmentResult, 0.7, sourceCode, filePath, patternResult.types, // cognium-ai#128 gate
|
|
1316
|
-
language);
|
|
1414
|
+
language, patternResult.calls);
|
|
1317
1415
|
yield { step: 'merge', status: 'completed', data: mergeResult };
|
|
1318
1416
|
yield { step: 'verify', status: 'running' };
|
|
1319
1417
|
const verifyResult = await runVerify(filePath, sourceCode, mergeResult.mergedSources, mergeResult.mergedSinks, patternResult.types, enableVerification, maxVerificationTargets, verificationStrategy, llmConfig, language, contextProfile);
|
|
@@ -1331,7 +1429,10 @@ export async function* analyzeFileStream(filePath, sourceCode, options) {
|
|
|
1331
1429
|
pairsFailed: verifyResult.pairsFailed,
|
|
1332
1430
|
unverifiedPairs: verifyResult.unverifiedPairs,
|
|
1333
1431
|
verificationError: verifyResult.verificationError,
|
|
1334
|
-
|
|
1432
|
+
submittedPairs: verifyResult.submittedPairs, // cognium-ai#471
|
|
1433
|
+
},
|
|
1434
|
+
// cognium-ai#460 — full generateFindings argument list.
|
|
1435
|
+
{ language, sanitizers: patternResult.sanitizers });
|
|
1335
1436
|
yield { step: 'report', status: 'completed', data: reportResult };
|
|
1336
1437
|
}
|
|
1337
1438
|
// Legacy export for compatibility
|