cognium-dev 4.13.1 → 4.14.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/README.md +4 -10
- package/dist/cli.js +226 -37
- 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 },
|
|
@@ -11410,6 +11422,7 @@ var DEFAULT_SOURCES = [
|
|
|
11410
11422
|
{ method: "getFileName", class: "MimeBodyPart", type: "file_input", severity: "high", return_tainted: true },
|
|
11411
11423
|
{ method: "getDisposition", class: "Part", type: "file_input", severity: "medium", return_tainted: true },
|
|
11412
11424
|
{ method: "getName", class: "ZipEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11425
|
+
{ method: "getName", class: "JarEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11413
11426
|
{ method: "getName", class: "ZipArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11414
11427
|
{ method: "getName", class: "TarArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
11415
11428
|
{ method: "getName", class: "ArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
|
|
@@ -11858,7 +11871,7 @@ var DEFAULT_SINKS = [
|
|
|
11858
11871
|
{ method: "checkout", class: "SCM", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11859
11872
|
{ method: "retrieve", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11860
11873
|
{ method: "add", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11861
|
-
{ method: "CpsFlowExecution", class: "constructor", type: "
|
|
11874
|
+
{ method: "CpsFlowExecution", class: "constructor", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
11862
11875
|
{ method: "CpsFlowDefinition", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11863
11876
|
{ method: "FlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
11864
11877
|
{ method: "processControlCommand", class: "TransportConnection", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
|
|
@@ -14009,12 +14022,14 @@ function isInterproceduralTaintableType(typeName, language) {
|
|
|
14009
14022
|
function isParameterizedQueryCall(call, pattern) {
|
|
14010
14023
|
if (pattern.type !== "sql_injection")
|
|
14011
14024
|
return false;
|
|
14012
|
-
const
|
|
14025
|
+
const queryPos = pattern.arg_positions.length > 0 ? Math.min(...pattern.arg_positions) : 0;
|
|
14026
|
+
const queryArg = call.arguments.find((a) => a.position === queryPos);
|
|
14013
14027
|
if (queryArg) {
|
|
14014
14028
|
const queryText = queryArg.literal ?? queryArg.expression ?? "";
|
|
14015
14029
|
const hasPlaceholders = /(\?(?:\s|,|$|\))|\$\d+|:\w+|%s)/.test(queryText);
|
|
14016
14030
|
const hasConcatenation = /\+\s*[^+]/.test(queryText) || queryText.includes("${");
|
|
14017
|
-
|
|
14031
|
+
const hasBoundArg = queryPos === 0 ? call.arguments.length >= 2 : call.arguments.some((a) => a.position > queryPos);
|
|
14032
|
+
if (hasPlaceholders && !hasConcatenation && hasBoundArg) {
|
|
14018
14033
|
return true;
|
|
14019
14034
|
}
|
|
14020
14035
|
}
|
|
@@ -14757,6 +14772,17 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
|
14757
14772
|
}
|
|
14758
14773
|
return false;
|
|
14759
14774
|
}
|
|
14775
|
+
function isArgumentlessJsResponseWriter(call, pattern, language) {
|
|
14776
|
+
if (pattern.type !== "xss" || pattern.class !== "Response")
|
|
14777
|
+
return false;
|
|
14778
|
+
if (language !== "javascript" && language !== "typescript")
|
|
14779
|
+
return false;
|
|
14780
|
+
const positions = pattern.arg_positions;
|
|
14781
|
+
if (!positions || positions.length === 0)
|
|
14782
|
+
return false;
|
|
14783
|
+
const argc = call.arguments?.length ?? 0;
|
|
14784
|
+
return positions.every((pos) => pos >= argc);
|
|
14785
|
+
}
|
|
14760
14786
|
function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
|
|
14761
14787
|
const patternsForLanguage = language === undefined ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
|
|
14762
14788
|
const sinkMap = new Map;
|
|
@@ -14817,6 +14843,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
14817
14843
|
continue;
|
|
14818
14844
|
}
|
|
14819
14845
|
}
|
|
14846
|
+
if (isArgumentlessJsResponseWriter(call, pattern, language)) {
|
|
14847
|
+
continue;
|
|
14848
|
+
}
|
|
14820
14849
|
if ((pattern.type === "command_injection" || pattern.type === "sql_injection") && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
|
|
14821
14850
|
continue;
|
|
14822
14851
|
}
|
|
@@ -24259,6 +24288,17 @@ class VuePlugin extends BaseLanguagePlugin {
|
|
|
24259
24288
|
}
|
|
24260
24289
|
|
|
24261
24290
|
// ../circle-ir/dist/languages/plugins/go.js
|
|
24291
|
+
function sqlSinks(cls, methods) {
|
|
24292
|
+
return methods.map(([method, argPositions]) => ({
|
|
24293
|
+
method,
|
|
24294
|
+
class: cls,
|
|
24295
|
+
type: "sql_injection",
|
|
24296
|
+
cwe: "CWE-89",
|
|
24297
|
+
severity: "critical",
|
|
24298
|
+
argPositions
|
|
24299
|
+
}));
|
|
24300
|
+
}
|
|
24301
|
+
|
|
24262
24302
|
class GoPlugin extends BaseLanguagePlugin {
|
|
24263
24303
|
id = "go";
|
|
24264
24304
|
name = "Go";
|
|
@@ -24443,6 +24483,32 @@ class GoPlugin extends BaseLanguagePlugin {
|
|
|
24443
24483
|
severity: "critical",
|
|
24444
24484
|
argPositions: [0]
|
|
24445
24485
|
},
|
|
24486
|
+
...sqlSinks("DB", [
|
|
24487
|
+
["QueryContext", [1]],
|
|
24488
|
+
["QueryRowContext", [1]],
|
|
24489
|
+
["ExecContext", [1]],
|
|
24490
|
+
["Prepare", [0]],
|
|
24491
|
+
["PrepareContext", [1]]
|
|
24492
|
+
]),
|
|
24493
|
+
...sqlSinks("Tx", [
|
|
24494
|
+
["Exec", [0]],
|
|
24495
|
+
["QueryRow", [0]],
|
|
24496
|
+
["QueryContext", [1]],
|
|
24497
|
+
["QueryRowContext", [1]],
|
|
24498
|
+
["ExecContext", [1]],
|
|
24499
|
+
["Prepare", [0]],
|
|
24500
|
+
["PrepareContext", [1]]
|
|
24501
|
+
]),
|
|
24502
|
+
...sqlSinks("Conn", [
|
|
24503
|
+
["Query", [0]],
|
|
24504
|
+
["QueryRow", [0]],
|
|
24505
|
+
["Exec", [0]],
|
|
24506
|
+
["QueryContext", [1]],
|
|
24507
|
+
["QueryRowContext", [1]],
|
|
24508
|
+
["ExecContext", [1]],
|
|
24509
|
+
["Prepare", [0]],
|
|
24510
|
+
["PrepareContext", [1]]
|
|
24511
|
+
]),
|
|
24446
24512
|
{
|
|
24447
24513
|
method: "Command",
|
|
24448
24514
|
class: "exec",
|
|
@@ -34356,8 +34422,7 @@ class CSharpCredit {
|
|
|
34356
34422
|
}
|
|
34357
34423
|
if (tainted.size === 0)
|
|
34358
34424
|
return false;
|
|
34359
|
-
const
|
|
34360
|
-
const rest = this.withoutSanitizerCalls(type, lastCallArguments(call) ?? call);
|
|
34425
|
+
const rest = this.withoutSanitizerCalls(type, this.receivedOperands(type, sinkLine));
|
|
34361
34426
|
if (this.builtinSanitizes(type, rest))
|
|
34362
34427
|
return false;
|
|
34363
34428
|
for (const v of tainted)
|
|
@@ -34365,6 +34430,23 @@ class CSharpCredit {
|
|
|
34365
34430
|
return true;
|
|
34366
34431
|
return false;
|
|
34367
34432
|
}
|
|
34433
|
+
receivedOperands(type, sinkLine) {
|
|
34434
|
+
const text = withoutStringLiterals(this.sinkText(sinkLine)).replace(/\s*;?\s*(?:\/\/.*)?$/, "");
|
|
34435
|
+
const property = /^\s*[A-Za-z_][\w.]*\.[A-Za-z_]\w*\s*=(?!=)\s*(.*)$/.exec(text);
|
|
34436
|
+
if (property)
|
|
34437
|
+
return property[1];
|
|
34438
|
+
if (!text.endsWith(")"))
|
|
34439
|
+
return text;
|
|
34440
|
+
const args2 = lastCallArguments(text);
|
|
34441
|
+
if (args2 === null)
|
|
34442
|
+
return text;
|
|
34443
|
+
const open = text.length - args2.length - 2;
|
|
34444
|
+
const callStart = text.slice(0, open).search(/[A-Za-z_][\w.]*\s*$/);
|
|
34445
|
+
const lastCall = callStart < 0 ? "" : text.slice(callStart);
|
|
34446
|
+
if (lastCall && this.withoutSanitizerCalls(type, lastCall).trim() === "")
|
|
34447
|
+
return text;
|
|
34448
|
+
return args2;
|
|
34449
|
+
}
|
|
34368
34450
|
withoutSanitizerCalls(type, text) {
|
|
34369
34451
|
const res = [];
|
|
34370
34452
|
for (const { re, type: t } of CSHARP_SANITIZER_RES)
|
|
@@ -39931,6 +40013,8 @@ function analyzeInterprocedural2(graphOrTypes, callsOrSources, dfgOrSinks, sourc
|
|
|
39931
40013
|
"QueryRowContext",
|
|
39932
40014
|
"Exec",
|
|
39933
40015
|
"ExecContext",
|
|
40016
|
+
"Prepare",
|
|
40017
|
+
"PrepareContext",
|
|
39934
40018
|
"EscapeString",
|
|
39935
40019
|
"HTMLEscapeString",
|
|
39936
40020
|
"JSEscapeString",
|
|
@@ -44864,7 +44948,7 @@ function isGeneratedFile(file) {
|
|
|
44864
44948
|
var PROVIDER_PATTERNS = [
|
|
44865
44949
|
{
|
|
44866
44950
|
name: "AWS access key",
|
|
44867
|
-
regex: /\
|
|
44951
|
+
regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
|
|
44868
44952
|
severity: "critical",
|
|
44869
44953
|
level: "error",
|
|
44870
44954
|
fix: "Rotate the AWS access key immediately and move it to an environment variable or AWS Secrets Manager."
|
|
@@ -44932,6 +45016,20 @@ var PROVIDER_PATTERNS = [
|
|
|
44932
45016
|
level: "warning",
|
|
44933
45017
|
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."
|
|
44934
45018
|
},
|
|
45019
|
+
{
|
|
45020
|
+
name: "Stripe test secret key",
|
|
45021
|
+
regex: /\bsk_test_[A-Za-z0-9]{24,}\b/,
|
|
45022
|
+
severity: "high",
|
|
45023
|
+
level: "warning",
|
|
45024
|
+
fix: "Rotate the Stripe test secret key in the Stripe Dashboard and load it from a secrets manager."
|
|
45025
|
+
},
|
|
45026
|
+
{
|
|
45027
|
+
name: "Stripe test restricted key",
|
|
45028
|
+
regex: /\brk_test_[A-Za-z0-9]{24,}\b/,
|
|
45029
|
+
severity: "high",
|
|
45030
|
+
level: "warning",
|
|
45031
|
+
fix: "Rotate the Stripe test restricted key in the Stripe Dashboard and load it from a secrets manager."
|
|
45032
|
+
},
|
|
44935
45033
|
{
|
|
44936
45034
|
name: "OpenAI API key",
|
|
44937
45035
|
regex: /\bsk-[A-Za-z0-9]{48}\b/,
|
|
@@ -45276,7 +45374,7 @@ class ScanSecretsPass {
|
|
|
45276
45374
|
const m = pattern.regex.exec(lineText);
|
|
45277
45375
|
if (!m)
|
|
45278
45376
|
continue;
|
|
45279
|
-
if ((pattern.name === "Stripe live restricted key" || pattern.name === "GitLab personal access token") && PLACEHOLDER_RE.test(m[0])) {
|
|
45377
|
+
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])) {
|
|
45280
45378
|
continue;
|
|
45281
45379
|
}
|
|
45282
45380
|
if (pattern.name === "PEM private key" && !pemHasInlineBody(lines, i2)) {
|
|
@@ -50380,6 +50478,27 @@ function longestLineLength(code) {
|
|
|
50380
50478
|
return max;
|
|
50381
50479
|
}
|
|
50382
50480
|
var MAX_ANALYZABLE_LINE_LENGTH = 1e4;
|
|
50481
|
+
function neutralizeOverlongLines(code) {
|
|
50482
|
+
if (longestLineLength(code) <= MAX_ANALYZABLE_LINE_LENGTH) {
|
|
50483
|
+
return { code, dropped: 0 };
|
|
50484
|
+
}
|
|
50485
|
+
let out2 = "";
|
|
50486
|
+
let start2 = 0;
|
|
50487
|
+
let dropped = 0;
|
|
50488
|
+
for (let i2 = 0;i2 <= code.length; i2++) {
|
|
50489
|
+
if (i2 !== code.length && code.charCodeAt(i2) !== 10)
|
|
50490
|
+
continue;
|
|
50491
|
+
if (i2 - start2 > MAX_ANALYZABLE_LINE_LENGTH)
|
|
50492
|
+
dropped++;
|
|
50493
|
+
else if (i2 > start2)
|
|
50494
|
+
out2 += code.slice(start2, i2);
|
|
50495
|
+
if (i2 < code.length)
|
|
50496
|
+
out2 += `
|
|
50497
|
+
`;
|
|
50498
|
+
start2 = i2 + 1;
|
|
50499
|
+
}
|
|
50500
|
+
return { code: out2, dropped };
|
|
50501
|
+
}
|
|
50383
50502
|
async function analyze(code, filePath, language, options = {}) {
|
|
50384
50503
|
if (!initialized) {
|
|
50385
50504
|
await initAnalyzer(options);
|
|
@@ -50398,6 +50517,15 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
50398
50517
|
parseGrammar = "tsx";
|
|
50399
50518
|
}
|
|
50400
50519
|
}
|
|
50520
|
+
const neutralized = neutralizeOverlongLines(code);
|
|
50521
|
+
if (neutralized.dropped > 0) {
|
|
50522
|
+
logger.warn("Neutralized over-long lines before analysis", {
|
|
50523
|
+
filePath,
|
|
50524
|
+
language,
|
|
50525
|
+
droppedLines: neutralized.dropped
|
|
50526
|
+
});
|
|
50527
|
+
code = neutralized.code;
|
|
50528
|
+
}
|
|
50401
50529
|
logger.debug("Analyzing file", { filePath, language, parseGrammar, codeLength: code.length });
|
|
50402
50530
|
const phaseTimingEnabled = globalThis.__circleIrPassTiming === true;
|
|
50403
50531
|
const phaseStart = () => phaseTimingEnabled ? Date.now() : 0;
|
|
@@ -51970,7 +52098,7 @@ var colors = {
|
|
|
51970
52098
|
};
|
|
51971
52099
|
|
|
51972
52100
|
// src/version.ts
|
|
51973
|
-
var version = "4.
|
|
52101
|
+
var version = "4.14.0";
|
|
51974
52102
|
|
|
51975
52103
|
// src/formatters.ts
|
|
51976
52104
|
var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
|
|
@@ -52431,6 +52559,21 @@ function formatSARIF(results, crossFileData, profileSummary) {
|
|
|
52431
52559
|
}
|
|
52432
52560
|
},
|
|
52433
52561
|
results: generateSarifResults(results, crossFileData),
|
|
52562
|
+
...crossFileData?.projectSizeBudgetExceeded ? {
|
|
52563
|
+
invocations: [
|
|
52564
|
+
{
|
|
52565
|
+
executionSuccessful: false,
|
|
52566
|
+
toolExecutionNotifications: [
|
|
52567
|
+
{
|
|
52568
|
+
level: "warning",
|
|
52569
|
+
message: {
|
|
52570
|
+
text: `Project size ceiling reached — analysed ${crossFileData.filesAnalysed ?? "?"} of ${crossFileData.filesTotal ?? "?"} files.`
|
|
52571
|
+
}
|
|
52572
|
+
}
|
|
52573
|
+
]
|
|
52574
|
+
}
|
|
52575
|
+
]
|
|
52576
|
+
} : {},
|
|
52434
52577
|
...profileSummary ? { properties: { projectProfile: profileSummary } } : {}
|
|
52435
52578
|
}
|
|
52436
52579
|
]
|
|
@@ -52522,8 +52665,8 @@ function generateSarifResults(results, crossFileData) {
|
|
|
52522
52665
|
|
|
52523
52666
|
// src/build-info.ts
|
|
52524
52667
|
var buildInfo = {
|
|
52525
|
-
gitSha: "
|
|
52526
|
-
builtAt: "2026-10-
|
|
52668
|
+
gitSha: "c44e104",
|
|
52669
|
+
builtAt: "2026-10-11T06:19:30.837Z"
|
|
52527
52670
|
};
|
|
52528
52671
|
|
|
52529
52672
|
// src/utils/args.ts
|
|
@@ -52765,6 +52908,38 @@ function spinner(text) {
|
|
|
52765
52908
|
}
|
|
52766
52909
|
|
|
52767
52910
|
// src/cli.ts
|
|
52911
|
+
function collapseWs(s) {
|
|
52912
|
+
return s.replace(/\s+/g, " ").trim();
|
|
52913
|
+
}
|
|
52914
|
+
function fileMatches(suppFile, findingFile, basePath) {
|
|
52915
|
+
const wanted = suppFile.replace(/^\.\//, "");
|
|
52916
|
+
const relativeFile = relative4(basePath, findingFile) || findingFile;
|
|
52917
|
+
return wanted === relativeFile || wanted === findingFile;
|
|
52918
|
+
}
|
|
52919
|
+
function findingMatchesSuppression(supp, finding, basePath, lineAt) {
|
|
52920
|
+
if (supp.pass !== finding.pass && supp.pass !== finding.alsoPass)
|
|
52921
|
+
return false;
|
|
52922
|
+
if (supp.file && !fileMatches(supp.file, finding.file, basePath))
|
|
52923
|
+
return false;
|
|
52924
|
+
const snippet = supp.snippet ? collapseWs(supp.snippet) : "";
|
|
52925
|
+
if (snippet) {
|
|
52926
|
+
const fromFile = lineAt?.(finding.file, finding.line);
|
|
52927
|
+
const text = fromFile !== undefined ? fromFile : finding.lineText;
|
|
52928
|
+
return text !== undefined && collapseWs(text).includes(snippet);
|
|
52929
|
+
}
|
|
52930
|
+
if (supp.line !== undefined && supp.line !== finding.line)
|
|
52931
|
+
return false;
|
|
52932
|
+
return true;
|
|
52933
|
+
}
|
|
52934
|
+
function staleSuppressions(suppressions, matched) {
|
|
52935
|
+
return suppressions.filter((_, i2) => !matched.has(i2));
|
|
52936
|
+
}
|
|
52937
|
+
function reportStaleSuppressions(suppressions, matched) {
|
|
52938
|
+
for (const supp of staleSuppressions(suppressions, matched)) {
|
|
52939
|
+
const where = [supp.file, supp.snippet ? "snippet" : "", supp.line !== undefined && !supp.snippet ? `line ${supp.line}` : ""].filter(Boolean).join(" ");
|
|
52940
|
+
console.error(colors.dim(`Suppression for ${supp.pass}${where ? ` (${where})` : ""} matched no finding`));
|
|
52941
|
+
}
|
|
52942
|
+
}
|
|
52768
52943
|
function loadConfig(profilePath) {
|
|
52769
52944
|
const configPath = profilePath || "cognium.config.json";
|
|
52770
52945
|
if (!existsSync(configPath)) {
|
|
@@ -52817,22 +52992,15 @@ function convertConfigToPassOptions(config) {
|
|
|
52817
52992
|
}
|
|
52818
52993
|
return { passOptions, disabledPasses };
|
|
52819
52994
|
}
|
|
52820
|
-
function applySuppressionsToResults(results, suppressions, basePath) {
|
|
52995
|
+
function applySuppressionsToResults(results, suppressions, basePath, match) {
|
|
52821
52996
|
if (suppressions.length === 0)
|
|
52822
52997
|
return results;
|
|
52823
52998
|
return results.map((result) => {
|
|
52824
|
-
const relativeFile = relative4(basePath, result.file) || result.file;
|
|
52825
52999
|
const filteredVulns = result.vulnerabilities.filter((vuln) => {
|
|
52826
|
-
for (
|
|
52827
|
-
if (
|
|
52828
|
-
continue;
|
|
52829
|
-
if (supp.file) {
|
|
52830
|
-
const suppFile = supp.file.replace(/^\.\//, "");
|
|
52831
|
-
if (suppFile !== relativeFile && suppFile !== result.file)
|
|
52832
|
-
continue;
|
|
52833
|
-
}
|
|
52834
|
-
if (supp.line !== undefined && supp.line !== vuln.line)
|
|
53000
|
+
for (let i2 = 0;i2 < suppressions.length; i2++) {
|
|
53001
|
+
if (!findingMatchesSuppression(suppressions[i2], { pass: vuln.type, file: result.file, line: vuln.line }, basePath, match?.lineAt))
|
|
52835
53002
|
continue;
|
|
53003
|
+
match?.matched?.add(i2);
|
|
52836
53004
|
return false;
|
|
52837
53005
|
}
|
|
52838
53006
|
return true;
|
|
@@ -52840,22 +53008,20 @@ function applySuppressionsToResults(results, suppressions, basePath) {
|
|
|
52840
53008
|
return { ...result, vulnerabilities: filteredVulns };
|
|
52841
53009
|
});
|
|
52842
53010
|
}
|
|
52843
|
-
function applySuppressionsToTaintPaths(paths, suppressions, basePath) {
|
|
53011
|
+
function applySuppressionsToTaintPaths(paths, suppressions, basePath, match) {
|
|
52844
53012
|
if (suppressions.length === 0)
|
|
52845
53013
|
return paths;
|
|
52846
53014
|
return paths.filter((path) => {
|
|
52847
|
-
for (
|
|
52848
|
-
|
|
52849
|
-
|
|
52850
|
-
|
|
52851
|
-
|
|
52852
|
-
|
|
52853
|
-
|
|
52854
|
-
|
|
52855
|
-
|
|
52856
|
-
|
|
52857
|
-
if (supp.line !== undefined && supp.line !== path.sink.line)
|
|
52858
|
-
continue;
|
|
53015
|
+
for (let i2 = 0;i2 < suppressions.length; i2++) {
|
|
53016
|
+
if (!findingMatchesSuppression(suppressions[i2], {
|
|
53017
|
+
pass: path.sink.type,
|
|
53018
|
+
alsoPass: `cross-file-${path.sink.type}`,
|
|
53019
|
+
file: path.sink.file,
|
|
53020
|
+
line: path.sink.line,
|
|
53021
|
+
lineText: path.sink.code
|
|
53022
|
+
}, basePath, match?.lineAt))
|
|
53023
|
+
continue;
|
|
53024
|
+
match?.matched?.add(i2);
|
|
52859
53025
|
return false;
|
|
52860
53026
|
}
|
|
52861
53027
|
return true;
|
|
@@ -53378,14 +53544,35 @@ async function runScan(targetPath, options) {
|
|
|
53378
53544
|
}
|
|
53379
53545
|
if (suppressions.length > 0) {
|
|
53380
53546
|
const beforeCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
|
|
53381
|
-
|
|
53547
|
+
const matched = new Set;
|
|
53548
|
+
const needsLines = suppressions.some((s) => s.snippet && collapseWs(s.snippet).length > 0);
|
|
53549
|
+
const lineCache = new Map;
|
|
53550
|
+
const lineAt = needsLines ? (file, line) => {
|
|
53551
|
+
let lines = lineCache.get(file);
|
|
53552
|
+
if (lines === undefined) {
|
|
53553
|
+
try {
|
|
53554
|
+
lines = readFileSync(file, "utf-8").split(`
|
|
53555
|
+
`);
|
|
53556
|
+
} catch {
|
|
53557
|
+
lines = null;
|
|
53558
|
+
}
|
|
53559
|
+
lineCache.set(file, lines);
|
|
53560
|
+
}
|
|
53561
|
+
if (!lines || line < 1)
|
|
53562
|
+
return;
|
|
53563
|
+
return lines[line - 1];
|
|
53564
|
+
} : undefined;
|
|
53565
|
+
const match = { lineAt, matched };
|
|
53566
|
+
results = applySuppressionsToResults(results, suppressions, process.cwd(), match);
|
|
53382
53567
|
if (crossFileData) {
|
|
53383
|
-
crossFileData.taintPaths = applySuppressionsToTaintPaths(crossFileData.taintPaths, suppressions, process.cwd());
|
|
53568
|
+
crossFileData.taintPaths = applySuppressionsToTaintPaths(crossFileData.taintPaths, suppressions, process.cwd(), match);
|
|
53384
53569
|
}
|
|
53385
53570
|
const afterCount = results.reduce((sum, r) => sum + r.vulnerabilities.length, 0) + (crossFileData?.taintPaths.length ?? 0);
|
|
53386
53571
|
if (!options.quiet && beforeCount !== afterCount) {
|
|
53387
53572
|
console.error(colors.dim(`Suppressed ${beforeCount - afterCount} finding(s) via config`));
|
|
53388
53573
|
}
|
|
53574
|
+
if (!options.quiet)
|
|
53575
|
+
reportStaleSuppressions(suppressions, matched);
|
|
53389
53576
|
}
|
|
53390
53577
|
const severityOrder = ["low", "medium", "high", "critical"];
|
|
53391
53578
|
if (options.severity) {
|
|
@@ -54018,7 +54205,9 @@ main().catch((error) => {
|
|
|
54018
54205
|
process.exit(2);
|
|
54019
54206
|
});
|
|
54020
54207
|
export {
|
|
54208
|
+
staleSuppressions,
|
|
54021
54209
|
resolveScanLanguage,
|
|
54210
|
+
reportStaleSuppressions,
|
|
54022
54211
|
matchesGlob,
|
|
54023
54212
|
loadConfig,
|
|
54024
54213
|
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.0",
|
|
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.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/node": "^26.6.4",
|