@clear-capabilities/agentic-security-scanner 0.151.2 → 0.152.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 +184 -0
- package/dist/1905.index.js +30 -2
- package/dist/3499.index.js +20 -1
- package/dist/agentic-security.mjs +80 -80
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +8 -4
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +842 -17
- package/src/dataflow/engine.js +865 -90
- package/src/dataflow/incremental.js +13 -0
- package/src/dataflow/index.js +18 -5
- package/src/dataflow/path-feasibility.js +86 -9
- package/src/dataflow/string-domain.js +132 -0
- package/src/dataflow/summaries.js +69 -8
- package/src/discovery/THREAT_MODEL.md +122 -0
- package/src/discovery/disprove.js +10 -1
- package/src/discovery/lenses.js +20 -1
- package/src/engine.js +108 -3
- package/src/ir/CLAUDE.md +2 -1
- package/src/ir/balanced-call.js +33 -1
- package/src/ir/callgraph.js +26 -2
- package/src/ir/class-hierarchy.js +119 -7
- package/src/ir/csharp-ir.js +97 -8
- package/src/ir/index.js +21 -1
- package/src/ir/parser-cs.js +706 -24
- package/src/ir/parser-java.js +527 -24
- package/src/ir/parser-js.js +22 -0
- package/src/ir/parser-php.js +446 -43
- package/src/ir/parser-rust.js +1338 -0
- package/src/ir/php-include-merge.js +190 -0
- package/src/lineage/sink-registry.js +18 -0
- package/src/lineage/source-registry.js +23 -0
- package/src/posture/csharp-analysis.js +341 -43
- package/src/posture/finding-defaults.js +5 -1
- package/src/posture/fix-coverage.js +1 -0
- package/src/posture/proof-coverage.js +1 -0
- package/src/sast/CLAUDE.md +1 -1
- package/src/sast/authz.js +144 -1
- package/src/sast/crypto-protocol.js +65 -7
- package/src/sast/csharp-cross-file.js +125 -0
- package/src/sast/csharp-structural.js +78 -2
- package/src/sast/csharp.js +574 -42
- package/src/sast/java-ast-folding.js +268 -11
- package/src/sast/java-bench-extras.js +530 -2
- package/src/sast/java-deserialization.js +31 -2
- package/src/sast/java-structural-cross-file.js +146 -0
- package/src/sast/java-structural.js +499 -6
- package/src/sast/ldap-injection.js +133 -3
- package/src/sast/rust.js +57 -0
- package/src/sast/xss-reflected-multilang.js +252 -3
package/src/ir/csharp-ir.js
CHANGED
|
@@ -47,6 +47,10 @@ const TYPE_MODIFIERS = new Set(['readonly', 'const', 'static', 'public', 'privat
|
|
|
47
47
|
|
|
48
48
|
const BUILTIN_TYPES = new Set(['void', 'bool', 'byte', 'sbyte', 'char', 'short', 'ushort', 'int', 'uint', 'long', 'ulong', 'float', 'double', 'decimal', 'string', 'object', 'var', 'dynamic']);
|
|
49
49
|
|
|
50
|
+
// SARD_80_F1 W4.C11 — compound assignment operators, tokenized as single
|
|
51
|
+
// 'op' tokens by csharp-tokenizer.js but previously never checked for here.
|
|
52
|
+
const CS_COMPOUND_ASSIGN_OPS = new Set(['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '??=']);
|
|
53
|
+
|
|
50
54
|
function isType(tok) {
|
|
51
55
|
if (!tok) return false;
|
|
52
56
|
if (tok.kind === 'kw' && BUILTIN_TYPES.has(tok.value)) return true;
|
|
@@ -268,7 +272,7 @@ function readMethodHeader(tokens, i, attachedAttrs) {
|
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
function walkMethodBody(method) {
|
|
271
|
-
const out = { calls: [], ctors: [], assignments: [], decls: [], strings: [] };
|
|
275
|
+
const out = { calls: [], ctors: [], assignments: [], decls: [], strings: [], returns: [] };
|
|
272
276
|
const tokens = method.bodyTokens;
|
|
273
277
|
for (let i = 0; i < tokens.length; i++) {
|
|
274
278
|
const t = tokens[i];
|
|
@@ -277,6 +281,45 @@ function walkMethodBody(method) {
|
|
|
277
281
|
out.strings.push({ kind: t.kind, value: t.value || tokenText([t]), line: t.line, parts: t.parts || null });
|
|
278
282
|
continue;
|
|
279
283
|
}
|
|
284
|
+
// `return <expr>;` (bare `return;` produces no expr and is skipped —
|
|
285
|
+
// interprocedural return-taint resolution has nothing to check there).
|
|
286
|
+
// SARD_80_F1 W3.x: gives `analyzeMethodFlow` visibility into what a
|
|
287
|
+
// same-file callee actually returns, so a call-shaped assignment RHS
|
|
288
|
+
// (`data = GoodG2BSource(req, resp);`) can be resolved against the
|
|
289
|
+
// callee's OWN return taint instead of blindly matching every
|
|
290
|
+
// identifier NAME in the call text (which wrongly taints on `req`/
|
|
291
|
+
// `resp` merely being passed as arguments — see the taintMap loop
|
|
292
|
+
// below and its `_asBareCall`/`resolveCalleeReturnTaint` usage).
|
|
293
|
+
if (t.kind === 'kw' && t.value === 'return') {
|
|
294
|
+
let j = i + 1;
|
|
295
|
+
let depth = 0;
|
|
296
|
+
while (j < tokens.length && (depth > 0 || tokens[j].kind !== 'semi')) {
|
|
297
|
+
const tk = tokens[j];
|
|
298
|
+
if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
|
|
299
|
+
else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
|
|
300
|
+
// SARD_80_F1 W3.x — an unbalanced closer (depth already 0) closes
|
|
301
|
+
// an ENCLOSING construct we didn't open (e.g. this `return` sits
|
|
302
|
+
// inside a `using (...)`/`if (...)` clause with no `;` of its
|
|
303
|
+
// own before the surrounding `)`), not part of our own
|
|
304
|
+
// expression — stop here rather than scanning past it into
|
|
305
|
+
// whatever follows. See the identical fix on the decl/assign/
|
|
306
|
+
// field loops below for the full incident writeup.
|
|
307
|
+
if (depth === 0) break;
|
|
308
|
+
depth--;
|
|
309
|
+
}
|
|
310
|
+
j++;
|
|
311
|
+
}
|
|
312
|
+
const exprTokens = tokens.slice(i + 1, j);
|
|
313
|
+
if (exprTokens.length) {
|
|
314
|
+
out.returns.push({ line: t.line, exprTokens, exprText: tokenText(exprTokens) });
|
|
315
|
+
}
|
|
316
|
+
// Deliberately do NOT skip past the expression tokens (`i = j`): the
|
|
317
|
+
// main loop's own ident/call/assignment branches below still need to
|
|
318
|
+
// walk them, so a statement-level call inside the return
|
|
319
|
+
// (`return string.Format(...)`) is still recorded in `out.calls`
|
|
320
|
+
// exactly as it was before this branch existed.
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
280
323
|
// new Type(args)
|
|
281
324
|
if (t.kind === 'kw' && t.value === 'new') {
|
|
282
325
|
const tr = readType(tokens, i + 1);
|
|
@@ -301,8 +344,27 @@ function walkMethodBody(method) {
|
|
|
301
344
|
let j = tr.next + 2;
|
|
302
345
|
let depth = 0;
|
|
303
346
|
while (j < tokens.length && (depth > 0 || tokens[j].kind !== 'semi')) {
|
|
304
|
-
|
|
305
|
-
if (
|
|
347
|
+
const tk = tokens[j];
|
|
348
|
+
if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
|
|
349
|
+
else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
|
|
350
|
+
// SARD_80_F1 W3.x — a declaration with no `;` of its own,
|
|
351
|
+
// sitting inside an enclosing `using (Type x = expr)` / `for
|
|
352
|
+
// (Type x = expr; …)` clause: the FIRST `)` we see here is
|
|
353
|
+
// that outer construct's own close, not one WE opened (depth
|
|
354
|
+
// is already 0), so stop before consuming it. Previously this
|
|
355
|
+
// fell through to `depth--` unconditionally, going negative
|
|
356
|
+
// and then scanning straight through into the following
|
|
357
|
+
// block body looking for a `;` that could be arbitrarily far
|
|
358
|
+
// away — corrupting this decl's rhsText with everything up to
|
|
359
|
+
// that stray semicolon AND silently absorbing whatever real
|
|
360
|
+
// statement lived in between (found via SARD_80_F1 W4.C14:
|
|
361
|
+
// `using (StreamReader sr = new StreamReader("f")) { data =
|
|
362
|
+
// sr.ReadLine(); }` swallowed the `data = sr.ReadLine()`
|
|
363
|
+
// assignment into `sr`'s own rhsText, deleting it from
|
|
364
|
+
// `out.assignments` entirely).
|
|
365
|
+
if (depth === 0) break;
|
|
366
|
+
depth--;
|
|
367
|
+
}
|
|
306
368
|
j++;
|
|
307
369
|
}
|
|
308
370
|
const rhs = tokens.slice(tr.next + 2, j);
|
|
@@ -329,12 +391,31 @@ function walkMethodBody(method) {
|
|
|
329
391
|
targetParts.push(tokens[j + 1].value);
|
|
330
392
|
j += 2;
|
|
331
393
|
}
|
|
332
|
-
|
|
394
|
+
// SARD_80_F1 W4.C11 — compound assignment (`+=`, `-=`, …) was
|
|
395
|
+
// previously invisible here (only bare `=` matched), so a statement
|
|
396
|
+
// like `cmd.CommandText += "..." + tainted + "...";` — Juliet's own
|
|
397
|
+
// universal convention for building CommandText — produced NO
|
|
398
|
+
// assignment record at all, silently dropping it from this
|
|
399
|
+
// detector's taint model. This model (`analyzeMethodFlow`, see
|
|
400
|
+
// csharp-analysis.js) has no un-taint step — a taintMap entry is
|
|
401
|
+
// only ever set true, never cleared — so treating the compound RHS
|
|
402
|
+
// exactly like a plain assign's RHS is already correct: if the
|
|
403
|
+
// target was tainted before, that status is never touched (no
|
|
404
|
+
// clearing branch exists); if this RHS is tainted, it becomes
|
|
405
|
+
// tainted now, same as a `+=` should.
|
|
406
|
+
if (tokens[j] && tokens[j].kind === 'op' &&
|
|
407
|
+
(tokens[j].value === '=' || CS_COMPOUND_ASSIGN_OPS.has(tokens[j].value))) {
|
|
333
408
|
let k = j + 1;
|
|
334
409
|
let depth = 0;
|
|
335
410
|
while (k < tokens.length && (depth > 0 || tokens[k].kind !== 'semi')) {
|
|
336
|
-
|
|
337
|
-
if (
|
|
411
|
+
const tk = tokens[k];
|
|
412
|
+
if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
|
|
413
|
+
else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
|
|
414
|
+
// SARD_80_F1 W3.x — see the decl "typed init" loop above for the
|
|
415
|
+
// full incident writeup; same enclosing-construct boundary fix.
|
|
416
|
+
if (depth === 0) break;
|
|
417
|
+
depth--;
|
|
418
|
+
}
|
|
338
419
|
k++;
|
|
339
420
|
}
|
|
340
421
|
const rhs = tokens.slice(j + 1, k);
|
|
@@ -419,6 +500,7 @@ function walkMethodBody(method) {
|
|
|
419
500
|
method.ctors = out.ctors;
|
|
420
501
|
method.assignments = out.assignments;
|
|
421
502
|
method.decls = out.decls;
|
|
503
|
+
method.returns = out.returns;
|
|
422
504
|
method.strings = out.strings;
|
|
423
505
|
return out;
|
|
424
506
|
}
|
|
@@ -555,8 +637,15 @@ export function buildCSharpIR(source) {
|
|
|
555
637
|
if (tokens[f] && tokens[f].kind === 'op' && tokens[f].value === '=') {
|
|
556
638
|
let fj = f + 1, depth = 0;
|
|
557
639
|
while (fj < tokens.length && (depth > 0 || tokens[fj].kind !== 'semi')) {
|
|
558
|
-
|
|
559
|
-
if (
|
|
640
|
+
const tk = tokens[fj];
|
|
641
|
+
if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
|
|
642
|
+
else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
|
|
643
|
+
// SARD_80_F1 W3.x — see the decl "typed init" loop's
|
|
644
|
+
// header comment for the full incident writeup; same
|
|
645
|
+
// enclosing-construct boundary fix.
|
|
646
|
+
if (depth === 0) break;
|
|
647
|
+
depth--;
|
|
648
|
+
}
|
|
560
649
|
fj++;
|
|
561
650
|
}
|
|
562
651
|
ir.decls.push({ name: tokens[tr.next].value, type: tr.type, rhsTokens: tokens.slice(f + 1, fj), rhsText: tokenText(tokens.slice(f + 1, fj)), line: tk.line, isVar: false, isField: true });
|
package/src/ir/index.js
CHANGED
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
import { parseJavaFile } from './parser-java.js';
|
|
19
19
|
import { parseGoFile } from './parser-go.js';
|
|
20
20
|
import { parsePhpFile } from './parser-php.js';
|
|
21
|
+
import { mergePhpIncludes } from './php-include-merge.js';
|
|
21
22
|
import { parseRubyFile } from './parser-rb.js';
|
|
23
|
+
import { parseRustFile } from './parser-rust.js';
|
|
22
24
|
import { parseCppFile, cppExtRe } from './parser-cpp.js';
|
|
23
25
|
import { buildCallGraph } from './callgraph.js';
|
|
24
26
|
import { buildClassHierarchy } from './class-hierarchy.js';
|
|
@@ -164,6 +166,9 @@ export function buildProjectIR(fileContents) {
|
|
|
164
166
|
} else if (/\.rb$/i.test(file)) {
|
|
165
167
|
const ir = parseRubyFile(file, code);
|
|
166
168
|
if (ir) perFile[file] = ir;
|
|
169
|
+
} else if (/\.rs$/i.test(file)) {
|
|
170
|
+
const ir = parseRustFile(file, code);
|
|
171
|
+
if (ir) perFile[file] = ir;
|
|
167
172
|
} else if (cppExtRe().test(file)) {
|
|
168
173
|
const ir = parseCppFile(file, code);
|
|
169
174
|
if (ir) perFile[file] = ir;
|
|
@@ -175,6 +180,12 @@ export function buildProjectIR(fileContents) {
|
|
|
175
180
|
if (ir && ir.file) perFile[ir.file] = ir;
|
|
176
181
|
}
|
|
177
182
|
}
|
|
183
|
+
// SARD_80_F1 W5.7 — merge PHP include/require targets' top-level
|
|
184
|
+
// assignments into the includer's own scope before anything else touches
|
|
185
|
+
// the CFGs (SSA numbering included), since PHP's `include` genuinely
|
|
186
|
+
// executes in the caller's variable namespace. See php-include-merge.js's
|
|
187
|
+
// module header for the full rationale and scope.
|
|
188
|
+
try { mergePhpIncludes(perFile); } catch {}
|
|
178
189
|
if (isSSAEnabled()) {
|
|
179
190
|
for (const ir of Object.values(perFile)) {
|
|
180
191
|
for (const fn of (ir.functions || [])) {
|
|
@@ -226,6 +237,9 @@ export async function buildProjectIRAsync(fileContents) {
|
|
|
226
237
|
} else if (/\.rb$/i.test(file)) {
|
|
227
238
|
const ir = parseRubyFile(file, code);
|
|
228
239
|
if (ir) perFile[file] = ir;
|
|
240
|
+
} else if (/\.rs$/i.test(file)) {
|
|
241
|
+
const ir = parseRustFile(file, code);
|
|
242
|
+
if (ir) perFile[file] = ir;
|
|
229
243
|
} else if (cppExtRe().test(file)) {
|
|
230
244
|
const ir = parseCppFile(file, code);
|
|
231
245
|
if (ir) perFile[file] = ir;
|
|
@@ -237,6 +251,12 @@ export async function buildProjectIRAsync(fileContents) {
|
|
|
237
251
|
if (ir && ir.file) perFile[ir.file] = ir;
|
|
238
252
|
}
|
|
239
253
|
}
|
|
254
|
+
// SARD_80_F1 W5.7 — merge PHP include/require targets' top-level
|
|
255
|
+
// assignments into the includer's own scope before anything else touches
|
|
256
|
+
// the CFGs (SSA numbering included), since PHP's `include` genuinely
|
|
257
|
+
// executes in the caller's variable namespace. See php-include-merge.js's
|
|
258
|
+
// module header for the full rationale and scope.
|
|
259
|
+
try { mergePhpIncludes(perFile); } catch {}
|
|
240
260
|
if (isSSAEnabled()) {
|
|
241
261
|
for (const ir of Object.values(perFile)) {
|
|
242
262
|
for (const fn of (ir.functions || [])) {
|
|
@@ -269,4 +289,4 @@ export function parsePythonFile(file, code) {
|
|
|
269
289
|
return parsePythonFileRegex(file, code);
|
|
270
290
|
}
|
|
271
291
|
|
|
272
|
-
export { parseJsFile, parseJavaFile, parseCSharpFile, parseKotlinFile, parseGoFile, parsePhpFile, parseRubyFile, parseCppFile, buildCallGraph, buildClassHierarchy, computeSSA, isSSAEnabled, probePythonAvailable, pythonParserDegradation, resetPythonParserDegradation };
|
|
292
|
+
export { parseJsFile, parseJavaFile, parseCSharpFile, parseKotlinFile, parseGoFile, parsePhpFile, parseRubyFile, parseRustFile, parseCppFile, buildCallGraph, buildClassHierarchy, computeSSA, isSSAEnabled, probePythonAvailable, pythonParserDegradation, resetPythonParserDegradation };
|