cognium-dev 4.13.2 → 4.14.1
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/README.md +4 -10
- package/dist/cli.js +290 -60
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -307,9 +307,9 @@ Filter to security findings only: `cognium-dev scan ./src --category security`
|
|
|
307
307
|
| Rust | `.rs` | Actix-web, Rocket, Axum |
|
|
308
308
|
| Bash | `.sh`, `.bash` | Shell scripts |
|
|
309
309
|
| HTML | `.html`, `.htm` | Web extraction preprocessor |
|
|
310
|
-
| C#/.NET _(
|
|
310
|
+
| C#/.NET _(Preview)_ | `.cs` | ASP.NET Core, ADO.NET (EF Core not yet verified) |
|
|
311
311
|
|
|
312
|
-
> **C#/.NET is
|
|
312
|
+
> **C#/.NET is Preview** (since 4.0.0). Straight-line taint
|
|
313
313
|
> analysis across 10 CWE families (SQLi, command injection, path traversal,
|
|
314
314
|
> SSRF, code injection, XSS, deserialization, LDAP, XPath, XXE). Not yet
|
|
315
315
|
> benchmark-verified — expect gaps in branch/alias precision and detector breadth.
|
|
@@ -455,15 +455,9 @@ Cognium is built for speed:
|
|
|
455
455
|
- **Core Engine**: [circle-ir](https://github.com/cogniumhq/cognium-dev/tree/main/packages/circle-ir) - High-performance SAST library
|
|
456
456
|
- **Dependencies**: Only 1 runtime dependency (circle-ir)
|
|
457
457
|
|
|
458
|
-
##
|
|
458
|
+
## Data handling
|
|
459
459
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
- **Discovery Mode**: LLM reads source code to locate vulnerable methods from scratch
|
|
463
|
-
- **Verification Mode**: Confirms whether static findings are actually exploitable
|
|
464
|
-
- **Semantic Extraction**: Extracts design intent for automated gap analysis
|
|
465
|
-
|
|
466
|
-
For details on LLM integration and benchmark improvements (42.5% → 78.3% on CWE-Bench with Claude Opus), visit [cognium.net](https://cognium.net).
|
|
460
|
+
cognium-dev is fully deterministic and runs entirely on your machine: source code is not sent to Cognium or any third-party service. Optional AI-assisted analysis is a separate product, [cognium-ai](https://cognium.net), which can send code snippets to a configured model endpoint; use cognium-dev (or cognium-ai with AI-assisted mode off and no remote engine configured) for air-gapped scanning.
|
|
467
461
|
|
|
468
462
|
## Benchmark Results
|
|
469
463
|
|
package/dist/cli.js
CHANGED
|
@@ -5515,6 +5515,14 @@ function csharpBareName(node) {
|
|
|
5515
5515
|
}
|
|
5516
5516
|
return getNodeText(node);
|
|
5517
5517
|
}
|
|
5518
|
+
function csharpCreatedSimpleType(creation) {
|
|
5519
|
+
const typeNode = creation.childForFieldName("type");
|
|
5520
|
+
if (!typeNode)
|
|
5521
|
+
return null;
|
|
5522
|
+
const bare = csharpBareName(typeNode).replace(/<[^>]*>/g, "");
|
|
5523
|
+
const simple = bare.split(".").pop()?.trim() ?? "";
|
|
5524
|
+
return simple && simple !== "var" ? simple : null;
|
|
5525
|
+
}
|
|
5518
5526
|
function extractCSharpCalls(tree, cache) {
|
|
5519
5527
|
const calls = [];
|
|
5520
5528
|
const typeMap = buildCSharpReceiverTypeMap(tree, cache);
|
|
@@ -5523,11 +5531,15 @@ function extractCSharpCalls(tree, cache) {
|
|
|
5523
5531
|
const fn = inv.childForFieldName("function");
|
|
5524
5532
|
let methodName = "unknown";
|
|
5525
5533
|
let receiver = null;
|
|
5534
|
+
let receiverTypeFromCreation = null;
|
|
5526
5535
|
if (fn?.type === "member_access_expression") {
|
|
5527
5536
|
const nameNode = fn.childForFieldName("name");
|
|
5528
5537
|
const exprNode = fn.childForFieldName("expression");
|
|
5529
5538
|
methodName = nameNode ? csharpBareName(nameNode) : "unknown";
|
|
5530
5539
|
receiver = exprNode ? getNodeText(exprNode) : null;
|
|
5540
|
+
if (exprNode?.type === "object_creation_expression") {
|
|
5541
|
+
receiverTypeFromCreation = csharpCreatedSimpleType(exprNode);
|
|
5542
|
+
}
|
|
5531
5543
|
} else if (fn) {
|
|
5532
5544
|
methodName = csharpBareName(fn);
|
|
5533
5545
|
}
|
|
@@ -5545,7 +5557,7 @@ function extractCSharpCalls(tree, cache) {
|
|
|
5545
5557
|
calls.push({
|
|
5546
5558
|
method_name: methodName,
|
|
5547
5559
|
receiver,
|
|
5548
|
-
receiver_type: receiver ? typeMap.get(receiver) ?? null : null,
|
|
5560
|
+
receiver_type: receiverTypeFromCreation ?? (receiver ? typeMap.get(receiver) ?? null : null),
|
|
5549
5561
|
receiver_type_fqn: null,
|
|
5550
5562
|
arguments: args2,
|
|
5551
5563
|
location: { line: inv.startPosition.row + 1, column: inv.startPosition.column },
|
|
@@ -9629,6 +9641,38 @@ function extractFieldDefs(body2, startId) {
|
|
|
9629
9641
|
function extractJSParameterDefs(params, startId) {
|
|
9630
9642
|
const defs = [];
|
|
9631
9643
|
let id = startId;
|
|
9644
|
+
const addPatternDefs = (patternNode, line) => {
|
|
9645
|
+
if (patternNode.type === "identifier" || patternNode.type === "shorthand_property_identifier_pattern") {
|
|
9646
|
+
defs.push({
|
|
9647
|
+
id: id++,
|
|
9648
|
+
variable: getNodeText(patternNode),
|
|
9649
|
+
line,
|
|
9650
|
+
kind: "param"
|
|
9651
|
+
});
|
|
9652
|
+
return;
|
|
9653
|
+
}
|
|
9654
|
+
if (patternNode.type === "rest_pattern" || patternNode.type === "rest_element") {
|
|
9655
|
+
const nameNode = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
|
|
9656
|
+
if (!nameNode)
|
|
9657
|
+
return;
|
|
9658
|
+
addPatternDefs(nameNode, line);
|
|
9659
|
+
return;
|
|
9660
|
+
}
|
|
9661
|
+
if (patternNode.type === "assignment_pattern") {
|
|
9662
|
+
const leftNode = patternNode.childForFieldName("left");
|
|
9663
|
+
if (leftNode)
|
|
9664
|
+
addPatternDefs(leftNode, line);
|
|
9665
|
+
return;
|
|
9666
|
+
}
|
|
9667
|
+
if (patternNode.type === "object_pattern" || patternNode.type === "array_pattern") {
|
|
9668
|
+
const destructDefs = extractDestructuringDefs(patternNode, id);
|
|
9669
|
+
for (const def of destructDefs) {
|
|
9670
|
+
def.kind = "param";
|
|
9671
|
+
defs.push(def);
|
|
9672
|
+
id++;
|
|
9673
|
+
}
|
|
9674
|
+
}
|
|
9675
|
+
};
|
|
9632
9676
|
for (let i2 = 0;i2 < params.childCount; i2++) {
|
|
9633
9677
|
const param = params.child(i2);
|
|
9634
9678
|
if (!param)
|
|
@@ -9678,14 +9722,8 @@ function extractJSParameterDefs(params, startId) {
|
|
|
9678
9722
|
}
|
|
9679
9723
|
} else if (param.type === "required_parameter" || param.type === "optional_parameter") {
|
|
9680
9724
|
const patternNode = param.childForFieldName("pattern");
|
|
9681
|
-
if (patternNode
|
|
9682
|
-
|
|
9683
|
-
id: id++,
|
|
9684
|
-
variable: getNodeText(patternNode),
|
|
9685
|
-
line: param.startPosition.row + 1,
|
|
9686
|
-
kind: "param"
|
|
9687
|
-
});
|
|
9688
|
-
}
|
|
9725
|
+
if (patternNode)
|
|
9726
|
+
addPatternDefs(patternNode, param.startPosition.row + 1);
|
|
9689
9727
|
}
|
|
9690
9728
|
}
|
|
9691
9729
|
return defs;
|
|
@@ -11410,6 +11448,7 @@ var DEFAULT_SOURCES = [
|
|
|
11410
11448
|
{ method: "getFileName", class: "MimeBodyPart", type: "file_input", severity: "high", return_tainted: true },
|
|
11411
11449
|
{ method: "getDisposition", class: "Part", type: "file_input", severity: "medium", return_tainted: true },
|
|
11412
11450
|
{ method: "getName", class: "ZipEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11451
|
+
{ method: "getName", class: "JarEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11413
11452
|
{ method: "getName", class: "ZipArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11414
11453
|
{ method: "getName", class: "TarArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11415
11454
|
{ method: "getName", class: "ArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
@@ -11858,7 +11897,7 @@ var DEFAULT_SINKS = [
|
|
|
11858
11897
|
{ method: "checkout", class: "SCM", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11859
11898
|
{ method: "retrieve", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11860
11899
|
{ method: "add", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11861
|
-
{ method: "CpsFlowExecution", class: "constructor", type: "
|
|
11900
|
+
{ method: "CpsFlowExecution", class: "constructor", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11862
11901
|
{ method: "CpsFlowDefinition", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11863
11902
|
{ method: "FlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11864
11903
|
{ method: "processControlCommand", class: "TransportConnection", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
@@ -13795,7 +13834,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
13795
13834
|
continue;
|
|
13796
13835
|
for (const param of method.parameters) {
|
|
13797
13836
|
const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
|
|
13798
|
-
const
|
|
13837
|
+
const isDestructureBinding = param.name.startsWith("{") || param.name.startsWith("[");
|
|
13838
|
+
const isTaintable = hasCSharpBindingAttr ? true : isDestructureBinding ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
|
|
13799
13839
|
if (isTaintable) {
|
|
13800
13840
|
const paramLine = param.line ?? method.start_line;
|
|
13801
13841
|
sources.push({
|
|
@@ -14009,12 +14049,14 @@ function isInterproceduralTaintableType(typeName, language) {
|
|
|
14009
14049
|
function isParameterizedQueryCall(call, pattern) {
|
|
14010
14050
|
if (pattern.type !== "sql_injection")
|
|
14011
14051
|
return false;
|
|
14012
|
-
const
|
|
14052
|
+
const queryPos = pattern.arg_positions.length > 0 ? Math.min(...pattern.arg_positions) : 0;
|
|
14053
|
+
const queryArg = call.arguments.find((a) => a.position === queryPos);
|
|
14013
14054
|
if (queryArg) {
|
|
14014
14055
|
const queryText = queryArg.literal ?? queryArg.expression ?? "";
|
|
14015
14056
|
const hasPlaceholders = /(\?(?:\s|,|$|\))|\$\d+|:\w+|%s)/.test(queryText);
|
|
14016
14057
|
const hasConcatenation = /\+\s*[^+]/.test(queryText) || queryText.includes("${");
|
|
14017
|
-
|
|
14058
|
+
const hasBoundArg = queryPos === 0 ? call.arguments.length >= 2 : call.arguments.some((a) => a.position > queryPos);
|
|
14059
|
+
if (hasPlaceholders && !hasConcatenation && hasBoundArg) {
|
|
14018
14060
|
return true;
|
|
14019
14061
|
}
|
|
14020
14062
|
}
|
|
@@ -14757,6 +14799,17 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
|
14757
14799
|
}
|
|
14758
14800
|
return false;
|
|
14759
14801
|
}
|
|
14802
|
+
function isArgumentlessJsResponseWriter(call, pattern, language) {
|
|
14803
|
+
if (pattern.type !== "xss" || pattern.class !== "Response")
|
|
14804
|
+
return false;
|
|
14805
|
+
if (language !== "javascript" && language !== "typescript")
|
|
14806
|
+
return false;
|
|
14807
|
+
const positions = pattern.arg_positions;
|
|
14808
|
+
if (!positions || positions.length === 0)
|
|
14809
|
+
return false;
|
|
14810
|
+
const argc = call.arguments?.length ?? 0;
|
|
14811
|
+
return positions.every((pos) => pos >= argc);
|
|
14812
|
+
}
|
|
14760
14813
|
function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
|
|
14761
14814
|
const patternsForLanguage = language === undefined ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
|
|
14762
14815
|
const sinkMap = new Map;
|
|
@@ -14817,6 +14870,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
14817
14870
|
continue;
|
|
14818
14871
|
}
|
|
14819
14872
|
}
|
|
14873
|
+
if (isArgumentlessJsResponseWriter(call, pattern, language)) {
|
|
14874
|
+
continue;
|
|
14875
|
+
}
|
|
14820
14876
|
if ((pattern.type === "command_injection" || pattern.type === "sql_injection") && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
|
|
14821
14877
|
continue;
|
|
14822
14878
|
}
|
|
@@ -18235,6 +18291,9 @@ function buildSanitizersByLine(sanitizers) {
|
|
|
18235
18291
|
}
|
|
18236
18292
|
return out2;
|
|
18237
18293
|
}
|
|
18294
|
+
function concatOutsideStrings(expression) {
|
|
18295
|
+
return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
|
|
18296
|
+
}
|
|
18238
18297
|
function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
|
|
18239
18298
|
let graph;
|
|
18240
18299
|
let sources;
|
|
@@ -18266,6 +18325,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
18266
18325
|
}
|
|
18267
18326
|
const taintedVars = [];
|
|
18268
18327
|
const flows = [];
|
|
18328
|
+
const javaConcat = graph.ir.meta.language === "java";
|
|
18269
18329
|
const reachableSinks = new Map;
|
|
18270
18330
|
const defsByLine = graph.defsByLine;
|
|
18271
18331
|
const usesByLine = graph.usesByLine;
|
|
@@ -18298,12 +18358,14 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
|
|
|
18298
18358
|
continue;
|
|
18299
18359
|
}
|
|
18300
18360
|
}
|
|
18301
|
-
const
|
|
18361
|
+
const expr = typeof arg.expression === "string" ? arg.expression : "";
|
|
18362
|
+
const compoundConcat = javaConcat && !!arg.variable && expr !== arg.variable && concatOutsideStrings(expr);
|
|
18363
|
+
const candidateUses = arg.variable && !compoundConcat ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
|
|
18302
18364
|
{
|
|
18303
18365
|
for (const use of candidateUses) {
|
|
18304
18366
|
if (use.def_id !== null) {
|
|
18305
18367
|
if (allTaintedDefIds.has(use.def_id)) {
|
|
18306
|
-
if (!arg.variable) {
|
|
18368
|
+
if (!arg.variable || compoundConcat) {
|
|
18307
18369
|
if (typeof arg.expression !== "string" || arg.expression.length === 0)
|
|
18308
18370
|
continue;
|
|
18309
18371
|
const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
|
|
@@ -24259,6 +24321,17 @@ class VuePlugin extends BaseLanguagePlugin {
|
|
|
24259
24321
|
}
|
|
24260
24322
|
|
|
24261
24323
|
// ../circle-ir/dist/languages/plugins/go.js
|
|
24324
|
+
function sqlSinks(cls, methods) {
|
|
24325
|
+
return methods.map(([method, argPositions]) => ({
|
|
24326
|
+
method,
|
|
24327
|
+
class: cls,
|
|
24328
|
+
type: "sql_injection",
|
|
24329
|
+
cwe: "CWE-89",
|
|
24330
|
+
severity: "critical",
|
|
24331
|
+
argPositions
|
|
24332
|
+
}));
|
|
24333
|
+
}
|
|
24334
|
+
|
|
24262
24335
|
class GoPlugin extends BaseLanguagePlugin {
|
|
24263
24336
|
id = "go";
|
|
24264
24337
|
name = "Go";
|
|
@@ -24443,6 +24516,32 @@ class GoPlugin extends BaseLanguagePlugin {
|
|
|
24443
24516
|
severity: "critical",
|
|
24444
24517
|
argPositions: [0]
|
|
24445
24518
|
},
|
|
24519
|
+
...sqlSinks("DB", [
|
|
24520
|
+
["QueryContext", [1]],
|
|
24521
|
+
["QueryRowContext", [1]],
|
|
24522
|
+
["ExecContext", [1]],
|
|
24523
|
+
["Prepare", [0]],
|
|
24524
|
+
["PrepareContext", [1]]
|
|
24525
|
+
]),
|
|
24526
|
+
...sqlSinks("Tx", [
|
|
24527
|
+
["Exec", [0]],
|
|
24528
|
+
["QueryRow", [0]],
|
|
24529
|
+
["QueryContext", [1]],
|
|
24530
|
+
["QueryRowContext", [1]],
|
|
24531
|
+
["ExecContext", [1]],
|
|
24532
|
+
["Prepare", [0]],
|
|
24533
|
+
["PrepareContext", [1]]
|
|
24534
|
+
]),
|
|
24535
|
+
...sqlSinks("Conn", [
|
|
24536
|
+
["Query", [0]],
|
|
24537
|
+
["QueryRow", [0]],
|
|
24538
|
+
["Exec", [0]],
|
|
24539
|
+
["QueryContext", [1]],
|
|
24540
|
+
["QueryRowContext", [1]],
|
|
24541
|
+
["ExecContext", [1]],
|
|
24542
|
+
["Prepare", [0]],
|
|
24543
|
+
["PrepareContext", [1]]
|
|
24544
|
+
]),
|
|
24446
24545
|
{
|
|
24447
24546
|
method: "Command",
|
|
24448
24547
|
class: "exec",
|
|
@@ -36411,6 +36510,22 @@ function evaluateSimpleExpression(expr, symbols) {
|
|
|
36411
36510
|
return String(Math.floor(result));
|
|
36412
36511
|
return expr;
|
|
36413
36512
|
}
|
|
36513
|
+
var ARGUMENT_IDENT_SKIP = new Set(["new", "true", "false", "null", "this", "super"]);
|
|
36514
|
+
function isStringConcat(expression) {
|
|
36515
|
+
return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
|
|
36516
|
+
}
|
|
36517
|
+
function argumentIdentifiers(expression) {
|
|
36518
|
+
const stripped = expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ");
|
|
36519
|
+
const found = stripped.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? [];
|
|
36520
|
+
const out2 = [];
|
|
36521
|
+
for (const id of found) {
|
|
36522
|
+
if (ARGUMENT_IDENT_SKIP.has(id))
|
|
36523
|
+
continue;
|
|
36524
|
+
if (!out2.includes(id))
|
|
36525
|
+
out2.push(id);
|
|
36526
|
+
}
|
|
36527
|
+
return out2;
|
|
36528
|
+
}
|
|
36414
36529
|
function isStringLiteralExpression(expr) {
|
|
36415
36530
|
const trimmed = expr.trim();
|
|
36416
36531
|
if (trimmed.length < 2)
|
|
@@ -36489,22 +36604,30 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
|
|
|
36489
36604
|
if (language === "bash" && arg.expression === call.method_name && !arg.variable && arg.literal == null)
|
|
36490
36605
|
continue;
|
|
36491
36606
|
if (arg.variable && !arg.expression?.includes("[")) {
|
|
36492
|
-
const
|
|
36493
|
-
const
|
|
36494
|
-
|
|
36495
|
-
|
|
36496
|
-
|
|
36607
|
+
const expr = arg.expression ?? "";
|
|
36608
|
+
const concatNames = language === "java" && isStringConcat(expr) ? argumentIdentifiers(expr) : [];
|
|
36609
|
+
const names = concatNames.length > 0 ? concatNames : [arg.variable];
|
|
36610
|
+
let argClean = true;
|
|
36611
|
+
for (const varName of names) {
|
|
36612
|
+
const scopedName = methodName ? `${methodName}:${varName}` : varName;
|
|
36613
|
+
if (fieldNames.has(varName) && !isInSynchronizedBlock) {
|
|
36614
|
+
argClean = false;
|
|
36615
|
+
break;
|
|
36616
|
+
}
|
|
36617
|
+
if (sanitizedVars?.has(scopedName) || sanitizedVars?.has(varName))
|
|
36618
|
+
continue;
|
|
36619
|
+
if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
|
|
36620
|
+
argClean = false;
|
|
36621
|
+
break;
|
|
36622
|
+
}
|
|
36623
|
+
const symbolValue = symbols.get(scopedName) ?? symbols.get(varName);
|
|
36624
|
+
if (symbolValue && symbolValue.type !== "unknown")
|
|
36625
|
+
continue;
|
|
36626
|
+
argClean = false;
|
|
36627
|
+
break;
|
|
36497
36628
|
}
|
|
36498
|
-
if (
|
|
36499
|
-
continue;
|
|
36500
|
-
if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
|
|
36629
|
+
if (!argClean)
|
|
36501
36630
|
allArgsAreClean = false;
|
|
36502
|
-
continue;
|
|
36503
|
-
}
|
|
36504
|
-
const symbolValue = symbols.get(scopedName) ?? symbols.get(varName);
|
|
36505
|
-
if (symbolValue && symbolValue.type !== "unknown")
|
|
36506
|
-
continue;
|
|
36507
|
-
allArgsAreClean = false;
|
|
36508
36631
|
} else {
|
|
36509
36632
|
if (arg.literal != null)
|
|
36510
36633
|
continue;
|
|
@@ -39947,6 +40070,8 @@ function analyzeInterprocedural2(graphOrTypes, callsOrSources, dfgOrSinks, sourc
|
|
|
39947
40070
|
"QueryRowContext",
|
|
39948
40071
|
"Exec",
|
|
39949
40072
|
"ExecContext",
|
|
40073
|
+
"Prepare",
|
|
40074
|
+
"PrepareContext",
|
|
39950
40075
|
"EscapeString",
|
|
39951
40076
|
"HTMLEscapeString",
|
|
39952
40077
|
"JSEscapeString",
|
|
@@ -44880,7 +45005,7 @@ function isGeneratedFile(file) {
|
|
|
44880
45005
|
var PROVIDER_PATTERNS = [
|
|
44881
45006
|
{
|
|
44882
45007
|
name: "AWS access key",
|
|
44883
|
-
regex: /\
|
|
45008
|
+
regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
|
|
44884
45009
|
severity: "critical",
|
|
44885
45010
|
level: "error",
|
|
44886
45011
|
fix: "Rotate the AWS access key immediately and move it to an environment variable or AWS Secrets Manager."
|
|
@@ -44948,6 +45073,20 @@ var PROVIDER_PATTERNS = [
|
|
|
44948
45073
|
level: "warning",
|
|
44949
45074
|
fix: "Publishable keys are not secret but should still not be checked in to back-end source files; verify front-end vs back-end context."
|
|
44950
45075
|
},
|
|
45076
|
+
{
|
|
45077
|
+
name: "Stripe test secret key",
|
|
45078
|
+
regex: /\bsk_test_[A-Za-z0-9]{24,}\b/,
|
|
45079
|
+
severity: "high",
|
|
45080
|
+
level: "warning",
|
|
45081
|
+
fix: "Rotate the Stripe test secret key in the Stripe Dashboard and load it from a secrets manager."
|
|
45082
|
+
},
|
|
45083
|
+
{
|
|
45084
|
+
name: "Stripe test restricted key",
|
|
45085
|
+
regex: /\brk_test_[A-Za-z0-9]{24,}\b/,
|
|
45086
|
+
severity: "high",
|
|
45087
|
+
level: "warning",
|
|
45088
|
+
fix: "Rotate the Stripe test restricted key in the Stripe Dashboard and load it from a secrets manager."
|
|
45089
|
+
},
|
|
44951
45090
|
{
|
|
44952
45091
|
name: "OpenAI API key",
|
|
44953
45092
|
regex: /\bsk-[A-Za-z0-9]{48}\b/,
|
|
@@ -45292,7 +45431,7 @@ class ScanSecretsPass {
|
|
|
45292
45431
|
const m = pattern.regex.exec(lineText);
|
|
45293
45432
|
if (!m)
|
|
45294
45433
|
continue;
|
|
45295
|
-
if ((pattern.name === "Stripe live restricted key" || pattern.name === "GitLab personal access token") && PLACEHOLDER_RE.test(m[0])) {
|
|
45434
|
+
if ((pattern.name === "Stripe live restricted key" || pattern.name === "Stripe test secret key" || pattern.name === "Stripe test restricted key" || pattern.name === "GitLab personal access token") && PLACEHOLDER_RE.test(m[0])) {
|
|
45296
45435
|
continue;
|
|
45297
45436
|
}
|
|
45298
45437
|
if (pattern.name === "PEM private key" && !pemHasInlineBody(lines, i2)) {
|
|
@@ -50396,6 +50535,27 @@ function longestLineLength(code) {
|
|
|
50396
50535
|
return max;
|
|
50397
50536
|
}
|
|
50398
50537
|
var MAX_ANALYZABLE_LINE_LENGTH = 1e4;
|
|
50538
|
+
function neutralizeOverlongLines(code) {
|
|
50539
|
+
if (longestLineLength(code) <= MAX_ANALYZABLE_LINE_LENGTH) {
|
|
50540
|
+
return { code, dropped: 0 };
|
|
50541
|
+
}
|
|
50542
|
+
let out2 = "";
|
|
50543
|
+
let start2 = 0;
|
|
50544
|
+
let dropped = 0;
|
|
50545
|
+
for (let i2 = 0;i2 <= code.length; i2++) {
|
|
50546
|
+
if (i2 !== code.length && code.charCodeAt(i2) !== 10)
|
|
50547
|
+
continue;
|
|
50548
|
+
if (i2 - start2 > MAX_ANALYZABLE_LINE_LENGTH)
|
|
50549
|
+
dropped++;
|
|
50550
|
+
else if (i2 > start2)
|
|
50551
|
+
out2 += code.slice(start2, i2);
|
|
50552
|
+
if (i2 < code.length)
|
|
50553
|
+
out2 += `
|
|
50554
|
+
`;
|
|
50555
|
+
start2 = i2 + 1;
|
|
50556
|
+
}
|
|
50557
|
+
return { code: out2, dropped };
|
|
50558
|
+
}
|
|
50399
50559
|
async function analyze(code, filePath, language, options = {}) {
|
|
50400
50560
|
if (!initialized) {
|
|
50401
50561
|
await initAnalyzer(options);
|
|
@@ -50414,6 +50574,15 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
50414
50574
|
parseGrammar = "tsx";
|
|
50415
50575
|
}
|
|
50416
50576
|
}
|
|
50577
|
+
const neutralized = neutralizeOverlongLines(code);
|
|
50578
|
+
if (neutralized.dropped > 0) {
|
|
50579
|
+
logger.warn("Neutralized over-long lines before analysis", {
|
|
50580
|
+
filePath,
|
|
50581
|
+
language,
|
|
50582
|
+
droppedLines: neutralized.dropped
|
|
50583
|
+
});
|
|
50584
|
+
code = neutralized.code;
|
|
50585
|
+
}
|
|
50417
50586
|
logger.debug("Analyzing file", { filePath, language, parseGrammar, codeLength: code.length });
|
|
50418
50587
|
const phaseTimingEnabled = globalThis.__circleIrPassTiming === true;
|
|
50419
50588
|
const phaseStart = () => phaseTimingEnabled ? Date.now() : 0;
|
|
@@ -51986,7 +52155,7 @@ var colors = {
|
|
|
51986
52155
|
};
|
|
51987
52156
|
|
|
51988
52157
|
// src/version.ts
|
|
51989
|
-
var version = "4.
|
|
52158
|
+
var version = "4.14.1";
|
|
51990
52159
|
|
|
51991
52160
|
// src/formatters.ts
|
|
51992
52161
|
var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
|
|
@@ -52447,6 +52616,21 @@ function formatSARIF(results, crossFileData, profileSummary) {
|
|
|
52447
52616
|
}
|
|
52448
52617
|
},
|
|
52449
52618
|
results: generateSarifResults(results, crossFileData),
|
|
52619
|
+
...crossFileData?.projectSizeBudgetExceeded ? {
|
|
52620
|
+
invocations: [
|
|
52621
|
+
{
|
|
52622
|
+
executionSuccessful: false,
|
|
52623
|
+
toolExecutionNotifications: [
|
|
52624
|
+
{
|
|
52625
|
+
level: "warning",
|
|
52626
|
+
message: {
|
|
52627
|
+
text: `Project size ceiling reached — analysed ${crossFileData.filesAnalysed ?? "?"} of ${crossFileData.filesTotal ?? "?"} files.`
|
|
52628
|
+
}
|
|
52629
|
+
}
|
|
52630
|
+
]
|
|
52631
|
+
}
|
|
52632
|
+
]
|
|
52633
|
+
} : {},
|
|
52450
52634
|
...profileSummary ? { properties: { projectProfile: profileSummary } } : {}
|
|
52451
52635
|
}
|
|
52452
52636
|
]
|
|
@@ -52538,8 +52722,8 @@ function generateSarifResults(results, crossFileData) {
|
|
|
52538
52722
|
|
|
52539
52723
|
// src/build-info.ts
|
|
52540
52724
|
var buildInfo = {
|
|
52541
|
-
gitSha: "
|
|
52542
|
-
builtAt: "2026-10-
|
|
52725
|
+
gitSha: "c8c74e9",
|
|
52726
|
+
builtAt: "2026-10-11T07:15:24.915Z"
|
|
52543
52727
|
};
|
|
52544
52728
|
|
|
52545
52729
|
// src/utils/args.ts
|
|
@@ -52781,6 +52965,38 @@ function spinner(text) {
|
|
|
52781
52965
|
}
|
|
52782
52966
|
|
|
52783
52967
|
// src/cli.ts
|
|
52968
|
+
function collapseWs(s) {
|
|
52969
|
+
return s.replace(/\s+/g, " ").trim();
|
|
52970
|
+
}
|
|
52971
|
+
function fileMatches(suppFile, findingFile, basePath) {
|
|
52972
|
+
const wanted = suppFile.replace(/^\.\//, "");
|
|
52973
|
+
const relativeFile = relative4(basePath, findingFile) || findingFile;
|
|
52974
|
+
return wanted === relativeFile || wanted === findingFile;
|
|
52975
|
+
}
|
|
52976
|
+
function findingMatchesSuppression(supp, finding, basePath, lineAt) {
|
|
52977
|
+
if (supp.pass !== finding.pass && supp.pass !== finding.alsoPass)
|
|
52978
|
+
return false;
|
|
52979
|
+
if (supp.file && !fileMatches(supp.file, finding.file, basePath))
|
|
52980
|
+
return false;
|
|
52981
|
+
const snippet = supp.snippet ? collapseWs(supp.snippet) : "";
|
|
52982
|
+
if (snippet) {
|
|
52983
|
+
const fromFile = lineAt?.(finding.file, finding.line);
|
|
52984
|
+
const text = fromFile !== undefined ? fromFile : finding.lineText;
|
|
52985
|
+
return text !== undefined && collapseWs(text).includes(snippet);
|
|
52986
|
+
}
|
|
52987
|
+
if (supp.line !== undefined && supp.line !== finding.line)
|
|
52988
|
+
return false;
|
|
52989
|
+
return true;
|
|
52990
|
+
}
|
|
52991
|
+
function staleSuppressions(suppressions, matched) {
|
|
52992
|
+
return suppressions.filter((_, i2) => !matched.has(i2));
|
|
52993
|
+
}
|
|
52994
|
+
function reportStaleSuppressions(suppressions, matched) {
|
|
52995
|
+
for (const supp of staleSuppressions(suppressions, matched)) {
|
|
52996
|
+
const where = [supp.file, supp.snippet ? "snippet" : "", supp.line !== undefined && !supp.snippet ? `line ${supp.line}` : ""].filter(Boolean).join(" ");
|
|
52997
|
+
console.error(colors.dim(`Suppression for ${supp.pass}${where ? ` (${where})` : ""} matched no finding`));
|
|
52998
|
+
}
|
|
52999
|
+
}
|
|
52784
53000
|
function loadConfig(profilePath) {
|
|
52785
53001
|
const configPath = profilePath || "cognium.config.json";
|
|
52786
53002
|
if (!existsSync(configPath)) {
|
|
@@ -52833,22 +53049,15 @@ function convertConfigToPassOptions(config) {
|
|
|
52833
53049
|
}
|
|
52834
53050
|
return { passOptions, disabledPasses };
|
|
52835
53051
|
}
|
|
52836
|
-
function applySuppressionsToResults(results, suppressions, basePath) {
|
|
53052
|
+
function applySuppressionsToResults(results, suppressions, basePath, match) {
|
|
52837
53053
|
if (suppressions.length === 0)
|
|
52838
53054
|
return results;
|
|
52839
53055
|
return results.map((result) => {
|
|
52840
|
-
const relativeFile = relative4(basePath, result.file) || result.file;
|
|
52841
53056
|
const filteredVulns = result.vulnerabilities.filter((vuln) => {
|
|
52842
|
-
for (
|
|
52843
|
-
if (
|
|
52844
|
-
continue;
|
|
52845
|
-
if (supp.file) {
|
|
52846
|
-
const suppFile = supp.file.replace(/^\.\//, "");
|
|
52847
|
-
if (suppFile !== relativeFile && suppFile !== result.file)
|
|
52848
|
-
continue;
|
|
52849
|
-
}
|
|
52850
|
-
if (supp.line !== undefined && supp.line !== vuln.line)
|
|
53057
|
+
for (let i2 = 0;i2 < suppressions.length; i2++) {
|
|
53058
|
+
if (!findingMatchesSuppression(suppressions[i2], { pass: vuln.type, file: result.file, line: vuln.line }, basePath, match?.lineAt))
|
|
52851
53059
|
continue;
|
|
53060
|
+
match?.matched?.add(i2);
|
|
52852
53061
|
return false;
|
|
52853
53062
|
}
|
|
52854
53063
|
return true;
|
|
@@ -52856,22 +53065,20 @@ function applySuppressionsToResults(results, suppressions, basePath) {
|
|
|
52856
53065
|
return { ...result, vulnerabilities: filteredVulns };
|
|
52857
53066
|
});
|
|
52858
53067
|
}
|
|
52859
|
-
function applySuppressionsToTaintPaths(paths, suppressions, basePath) {
|
|
53068
|
+
function applySuppressionsToTaintPaths(paths, suppressions, basePath, match) {
|
|
52860
53069
|
if (suppressions.length === 0)
|
|
52861
53070
|
return paths;
|
|
52862
53071
|
return paths.filter((path) => {
|
|
52863
|
-
for (
|
|
52864
|
-
|
|
52865
|
-
|
|
52866
|
-
|
|
52867
|
-
|
|
52868
|
-
|
|
52869
|
-
|
|
52870
|
-
|
|
52871
|
-
|
|
52872
|
-
|
|
52873
|
-
if (supp.line !== undefined && supp.line !== path.sink.line)
|
|
52874
|
-
continue;
|
|
53072
|
+
for (let i2 = 0;i2 < suppressions.length; i2++) {
|
|
53073
|
+
if (!findingMatchesSuppression(suppressions[i2], {
|
|
53074
|
+
pass: path.sink.type,
|
|
53075
|
+
alsoPass: `cross-file-${path.sink.type}`,
|
|
53076
|
+
file: path.sink.file,
|
|
53077
|
+
line: path.sink.line,
|
|
53078
|
+
lineText: path.sink.code
|
|
53079
|
+
}, basePath, match?.lineAt))
|
|
53080
|
+
continue;
|
|
53081
|
+
match?.matched?.add(i2);
|
|
52875
53082
|
return false;
|
|
52876
53083
|
}
|
|
52877
53084
|
return true;
|
|
@@ -53394,14 +53601,35 @@ async function runScan(targetPath, options) {
|
|
|
53394
53601
|
}
|
|
53395
53602
|
if (suppressions.length > 0) {
|
|
53396
53603
|
const beforeCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
|
|
53397
|
-
|
|
53604
|
+
const matched = new Set;
|
|
53605
|
+
const needsLines = suppressions.some((s) => s.snippet && collapseWs(s.snippet).length > 0);
|
|
53606
|
+
const lineCache = new Map;
|
|
53607
|
+
const lineAt = needsLines ? (file, line) => {
|
|
53608
|
+
let lines = lineCache.get(file);
|
|
53609
|
+
if (lines === undefined) {
|
|
53610
|
+
try {
|
|
53611
|
+
lines = readFileSync(file, "utf-8").split(`
|
|
53612
|
+
`);
|
|
53613
|
+
} catch {
|
|
53614
|
+
lines = null;
|
|
53615
|
+
}
|
|
53616
|
+
lineCache.set(file, lines);
|
|
53617
|
+
}
|
|
53618
|
+
if (!lines || line < 1)
|
|
53619
|
+
return;
|
|
53620
|
+
return lines[line - 1];
|
|
53621
|
+
} : undefined;
|
|
53622
|
+
const match = { lineAt, matched };
|
|
53623
|
+
results = applySuppressionsToResults(results, suppressions, process.cwd(), match);
|
|
53398
53624
|
if (crossFileData) {
|
|
53399
|
-
crossFileData.taintPaths = applySuppressionsToTaintPaths(crossFileData.taintPaths, suppressions, process.cwd());
|
|
53625
|
+
crossFileData.taintPaths = applySuppressionsToTaintPaths(crossFileData.taintPaths, suppressions, process.cwd(), match);
|
|
53400
53626
|
}
|
|
53401
53627
|
const afterCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
|
|
53402
53628
|
if (!options.quiet && beforeCount !== afterCount) {
|
|
53403
53629
|
console.error(colors.dim(`Suppressed ${beforeCount - afterCount} finding(s) via config`));
|
|
53404
53630
|
}
|
|
53631
|
+
if (!options.quiet)
|
|
53632
|
+
reportStaleSuppressions(suppressions, matched);
|
|
53405
53633
|
}
|
|
53406
53634
|
const severityOrder = ["low", "medium", "high", "critical"];
|
|
53407
53635
|
if (options.severity) {
|
|
@@ -54034,7 +54262,9 @@ main().catch((error) => {
|
|
|
54034
54262
|
process.exit(2);
|
|
54035
54263
|
});
|
|
54036
54264
|
export {
|
|
54265
|
+
staleSuppressions,
|
|
54037
54266
|
resolveScanLanguage,
|
|
54267
|
+
reportStaleSuppressions,
|
|
54038
54268
|
matchesGlob,
|
|
54039
54269
|
loadConfig,
|
|
54040
54270
|
isTestFile2 as isTestFile,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cognium-dev",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.14.1",
|
|
4
4
|
"description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cognium-dev": "dist/cli.js"
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@cognium/project-profile-detect": "1.1.2",
|
|
68
|
-
"circle-ir": "4.
|
|
68
|
+
"circle-ir": "4.14.1"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/node": "^26.6.4",
|