cognium-dev 3.45.0 → 3.47.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/dist/cli.js +266 -2
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -3349,6 +3349,40 @@ async function parse(code, language) {
|
|
|
3349
3349
|
}
|
|
3350
3350
|
return tree;
|
|
3351
3351
|
}
|
|
3352
|
+
function extractParseStatus(tree) {
|
|
3353
|
+
const root = tree.rootNode;
|
|
3354
|
+
if (!root.hasError) {
|
|
3355
|
+
return { success: true, has_errors: false, error_count: 0, error_locations: [] };
|
|
3356
|
+
}
|
|
3357
|
+
const MAX_LOCATIONS = 50;
|
|
3358
|
+
const locations = [];
|
|
3359
|
+
let errorCount = 0;
|
|
3360
|
+
const stack = [root];
|
|
3361
|
+
while (stack.length > 0) {
|
|
3362
|
+
const node = stack.pop();
|
|
3363
|
+
if (node.type === "ERROR" || node.isMissing) {
|
|
3364
|
+
errorCount++;
|
|
3365
|
+
if (locations.length < MAX_LOCATIONS) {
|
|
3366
|
+
locations.push({
|
|
3367
|
+
line: node.startPosition.row + 1,
|
|
3368
|
+
column: node.startPosition.column
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
for (let i2 = 0;i2 < node.childCount; i2++) {
|
|
3373
|
+
const child = node.child(i2);
|
|
3374
|
+
if (child && (child.hasError || child.isMissing)) {
|
|
3375
|
+
stack.push(child);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
return {
|
|
3380
|
+
success: false,
|
|
3381
|
+
has_errors: true,
|
|
3382
|
+
error_count: errorCount,
|
|
3383
|
+
error_locations: locations
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3352
3386
|
function disposeTree(tree) {
|
|
3353
3387
|
if (!tree)
|
|
3354
3388
|
return;
|
|
@@ -26513,6 +26547,214 @@ class ScanSecretsPass {
|
|
|
26513
26547
|
}
|
|
26514
26548
|
}
|
|
26515
26549
|
|
|
26550
|
+
// ../circle-ir/dist/analysis/passes/spring4shell-pass.js
|
|
26551
|
+
var CONTROLLER_ANNOTATIONS = new Set([
|
|
26552
|
+
"Controller",
|
|
26553
|
+
"RestController",
|
|
26554
|
+
"ControllerAdvice",
|
|
26555
|
+
"RestControllerAdvice"
|
|
26556
|
+
]);
|
|
26557
|
+
var ROUTE_ANNOTATIONS = new Set([
|
|
26558
|
+
"RequestMapping",
|
|
26559
|
+
"GetMapping",
|
|
26560
|
+
"PostMapping",
|
|
26561
|
+
"PutMapping",
|
|
26562
|
+
"DeleteMapping",
|
|
26563
|
+
"PatchMapping"
|
|
26564
|
+
]);
|
|
26565
|
+
var BINDING_ANNOTATIONS = new Set([
|
|
26566
|
+
"RequestBody",
|
|
26567
|
+
"RequestParam",
|
|
26568
|
+
"PathVariable",
|
|
26569
|
+
"RequestHeader",
|
|
26570
|
+
"CookieValue",
|
|
26571
|
+
"MatrixVariable",
|
|
26572
|
+
"ModelAttribute",
|
|
26573
|
+
"Valid",
|
|
26574
|
+
"Validated",
|
|
26575
|
+
"RequestPart",
|
|
26576
|
+
"SessionAttribute",
|
|
26577
|
+
"RequestAttribute"
|
|
26578
|
+
]);
|
|
26579
|
+
var FRAMEWORK_PARAM_TYPES = new Set([
|
|
26580
|
+
"HttpServletRequest",
|
|
26581
|
+
"HttpServletResponse",
|
|
26582
|
+
"ServletRequest",
|
|
26583
|
+
"ServletResponse",
|
|
26584
|
+
"HttpSession",
|
|
26585
|
+
"ServletContext",
|
|
26586
|
+
"Cookie",
|
|
26587
|
+
"Model",
|
|
26588
|
+
"ModelMap",
|
|
26589
|
+
"ModelAndView",
|
|
26590
|
+
"Map",
|
|
26591
|
+
"BindingResult",
|
|
26592
|
+
"Errors",
|
|
26593
|
+
"RedirectAttributes",
|
|
26594
|
+
"SessionStatus",
|
|
26595
|
+
"WebRequest",
|
|
26596
|
+
"NativeWebRequest",
|
|
26597
|
+
"ServletWebRequest",
|
|
26598
|
+
"UriComponentsBuilder",
|
|
26599
|
+
"UriBuilder",
|
|
26600
|
+
"HttpEntity",
|
|
26601
|
+
"RequestEntity",
|
|
26602
|
+
"ResponseEntity",
|
|
26603
|
+
"HttpHeaders",
|
|
26604
|
+
"InputStream",
|
|
26605
|
+
"OutputStream",
|
|
26606
|
+
"Reader",
|
|
26607
|
+
"Writer",
|
|
26608
|
+
"ServerHttpRequest",
|
|
26609
|
+
"ServerHttpResponse",
|
|
26610
|
+
"ServerWebExchange",
|
|
26611
|
+
"Principal",
|
|
26612
|
+
"Authentication",
|
|
26613
|
+
"Locale",
|
|
26614
|
+
"TimeZone",
|
|
26615
|
+
"ZoneId",
|
|
26616
|
+
"MultipartFile",
|
|
26617
|
+
"Part",
|
|
26618
|
+
"TimeZone"
|
|
26619
|
+
]);
|
|
26620
|
+
var SIMPLE_JAVA_TYPES = new Set([
|
|
26621
|
+
"boolean",
|
|
26622
|
+
"byte",
|
|
26623
|
+
"char",
|
|
26624
|
+
"short",
|
|
26625
|
+
"int",
|
|
26626
|
+
"long",
|
|
26627
|
+
"float",
|
|
26628
|
+
"double",
|
|
26629
|
+
"void",
|
|
26630
|
+
"Boolean",
|
|
26631
|
+
"Byte",
|
|
26632
|
+
"Character",
|
|
26633
|
+
"Short",
|
|
26634
|
+
"Integer",
|
|
26635
|
+
"Long",
|
|
26636
|
+
"Float",
|
|
26637
|
+
"Double",
|
|
26638
|
+
"String",
|
|
26639
|
+
"CharSequence",
|
|
26640
|
+
"BigInteger",
|
|
26641
|
+
"BigDecimal",
|
|
26642
|
+
"UUID",
|
|
26643
|
+
"Date",
|
|
26644
|
+
"Calendar",
|
|
26645
|
+
"Instant",
|
|
26646
|
+
"LocalDate",
|
|
26647
|
+
"LocalTime",
|
|
26648
|
+
"LocalDateTime",
|
|
26649
|
+
"OffsetDateTime",
|
|
26650
|
+
"OffsetTime",
|
|
26651
|
+
"ZonedDateTime",
|
|
26652
|
+
"Duration",
|
|
26653
|
+
"Period",
|
|
26654
|
+
"List",
|
|
26655
|
+
"Set",
|
|
26656
|
+
"Collection",
|
|
26657
|
+
"Iterable",
|
|
26658
|
+
"Optional"
|
|
26659
|
+
]);
|
|
26660
|
+
|
|
26661
|
+
class Spring4ShellPass {
|
|
26662
|
+
name = "spring4shell";
|
|
26663
|
+
category = "security";
|
|
26664
|
+
run(ctx) {
|
|
26665
|
+
const { graph, language } = ctx;
|
|
26666
|
+
if (language !== "java") {
|
|
26667
|
+
return { controllerMethodsScanned: 0, findingsEmitted: 0 };
|
|
26668
|
+
}
|
|
26669
|
+
const file = graph.ir.meta.file;
|
|
26670
|
+
let scanned = 0;
|
|
26671
|
+
let emitted = 0;
|
|
26672
|
+
for (const type of graph.ir.types) {
|
|
26673
|
+
if (!isController(type))
|
|
26674
|
+
continue;
|
|
26675
|
+
for (const method of type.methods) {
|
|
26676
|
+
if (!isRouteHandler(method))
|
|
26677
|
+
continue;
|
|
26678
|
+
scanned++;
|
|
26679
|
+
for (const param of method.parameters) {
|
|
26680
|
+
if (!isVulnerableParameter(param))
|
|
26681
|
+
continue;
|
|
26682
|
+
ctx.addFinding({
|
|
26683
|
+
id: `${this.name}-${file}-${method.start_line}-${param.name}`,
|
|
26684
|
+
pass: this.name,
|
|
26685
|
+
category: this.category,
|
|
26686
|
+
rule_id: this.name,
|
|
26687
|
+
cwe: "CWE-94",
|
|
26688
|
+
severity: "high",
|
|
26689
|
+
level: "error",
|
|
26690
|
+
message: `Spring MVC controller method '${type.name}.${method.name}' binds parameter '${param.name}' of type '${param.type ?? "?"}' via implicit form-data binding (no @RequestBody / @RequestParam / @ModelAttribute) — vulnerable to Spring4Shell (CVE-2022-22965) class-graph RCE on Spring < 5.3.18 / 5.2.20`,
|
|
26691
|
+
file,
|
|
26692
|
+
line: param.line ?? method.start_line,
|
|
26693
|
+
fix: "Annotate the parameter with @RequestBody (JSON) or @ModelAttribute + @InitBinder/setAllowedFields whitelisting, upgrade Spring to ≥ 5.3.18 / 5.2.20, and ensure JDK is patched.",
|
|
26694
|
+
evidence: {
|
|
26695
|
+
controller_class: type.name,
|
|
26696
|
+
controller_annotations: type.annotations,
|
|
26697
|
+
method: method.name,
|
|
26698
|
+
method_annotations: method.annotations,
|
|
26699
|
+
parameter_name: param.name,
|
|
26700
|
+
parameter_type: param.type
|
|
26701
|
+
}
|
|
26702
|
+
});
|
|
26703
|
+
emitted++;
|
|
26704
|
+
}
|
|
26705
|
+
}
|
|
26706
|
+
}
|
|
26707
|
+
return { controllerMethodsScanned: scanned, findingsEmitted: emitted };
|
|
26708
|
+
}
|
|
26709
|
+
}
|
|
26710
|
+
function annotationHead(annotation) {
|
|
26711
|
+
const parenIdx = annotation.indexOf("(");
|
|
26712
|
+
return parenIdx >= 0 ? annotation.slice(0, parenIdx) : annotation;
|
|
26713
|
+
}
|
|
26714
|
+
function hasAnnotation(annotations, names) {
|
|
26715
|
+
for (const a of annotations) {
|
|
26716
|
+
if (names.has(annotationHead(a)))
|
|
26717
|
+
return true;
|
|
26718
|
+
}
|
|
26719
|
+
return false;
|
|
26720
|
+
}
|
|
26721
|
+
function isController(type) {
|
|
26722
|
+
return hasAnnotation(type.annotations, CONTROLLER_ANNOTATIONS);
|
|
26723
|
+
}
|
|
26724
|
+
function isRouteHandler(method) {
|
|
26725
|
+
return hasAnnotation(method.annotations, ROUTE_ANNOTATIONS);
|
|
26726
|
+
}
|
|
26727
|
+
function isVulnerableParameter(param) {
|
|
26728
|
+
if (hasAnnotation(param.annotations, BINDING_ANNOTATIONS))
|
|
26729
|
+
return false;
|
|
26730
|
+
if (!param.type)
|
|
26731
|
+
return false;
|
|
26732
|
+
const type = stripGenerics2(param.type).trim();
|
|
26733
|
+
if (!type)
|
|
26734
|
+
return false;
|
|
26735
|
+
if (type.endsWith("[]")) {
|
|
26736
|
+
const elem = type.slice(0, -2).trim();
|
|
26737
|
+
return !SIMPLE_JAVA_TYPES.has(elem) && isPotentialPojo(elem);
|
|
26738
|
+
}
|
|
26739
|
+
if (SIMPLE_JAVA_TYPES.has(type))
|
|
26740
|
+
return false;
|
|
26741
|
+
if (FRAMEWORK_PARAM_TYPES.has(type))
|
|
26742
|
+
return false;
|
|
26743
|
+
if (!isPotentialPojo(type))
|
|
26744
|
+
return false;
|
|
26745
|
+
return true;
|
|
26746
|
+
}
|
|
26747
|
+
function stripGenerics2(type) {
|
|
26748
|
+
const ltIdx = type.indexOf("<");
|
|
26749
|
+
return ltIdx >= 0 ? type.slice(0, ltIdx) : type;
|
|
26750
|
+
}
|
|
26751
|
+
function isPotentialPojo(type) {
|
|
26752
|
+
if (type.length === 0)
|
|
26753
|
+
return false;
|
|
26754
|
+
const first = type.charCodeAt(0);
|
|
26755
|
+
return first >= 65 && first <= 90;
|
|
26756
|
+
}
|
|
26757
|
+
|
|
26516
26758
|
// ../circle-ir/dist/graph/import-graph.js
|
|
26517
26759
|
function dirname(filePath) {
|
|
26518
26760
|
const idx = filePath.lastIndexOf("/");
|
|
@@ -27509,6 +27751,15 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
27509
27751
|
const tree = await parse(code, language);
|
|
27510
27752
|
try {
|
|
27511
27753
|
logger.trace("Parsed AST", { rootNodeType: tree.rootNode.type });
|
|
27754
|
+
const parseStatus = extractParseStatus(tree);
|
|
27755
|
+
if (parseStatus.has_errors) {
|
|
27756
|
+
logger.warn("Partial parse — IR may be incomplete", {
|
|
27757
|
+
filePath,
|
|
27758
|
+
language,
|
|
27759
|
+
errorCount: parseStatus.error_count,
|
|
27760
|
+
firstErrorLine: parseStatus.error_locations[0]?.line
|
|
27761
|
+
});
|
|
27762
|
+
}
|
|
27512
27763
|
const nodeCache = collectAllNodes(tree.rootNode, getNodeTypesForLanguage(language));
|
|
27513
27764
|
const meta = extractMeta(code, tree, filePath, language);
|
|
27514
27765
|
const types = extractTypes(tree, nodeCache, language);
|
|
@@ -27612,6 +27863,8 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
27612
27863
|
pipeline.add(new NamingConventionPass(passOpts.namingConvention));
|
|
27613
27864
|
if (!disabledPasses.has("security-headers"))
|
|
27614
27865
|
pipeline.add(new SecurityHeadersPass(passOpts.securityHeaders));
|
|
27866
|
+
if (!disabledPasses.has("spring4shell"))
|
|
27867
|
+
pipeline.add(new Spring4ShellPass);
|
|
27615
27868
|
const { results, findings } = pipeline.run(graph, code, language, config);
|
|
27616
27869
|
const sinkFilter = results.get("sink-filter");
|
|
27617
27870
|
const interProc = results.get("interprocedural");
|
|
@@ -27645,7 +27898,8 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
27645
27898
|
enriched,
|
|
27646
27899
|
findings: findings.length > 0 ? findings : undefined,
|
|
27647
27900
|
metrics: { file: filePath, metrics: metricValues },
|
|
27648
|
-
runtime_registrations: runtimeRegistrations.length > 0 ? runtimeRegistrations : undefined
|
|
27901
|
+
runtime_registrations: runtimeRegistrations.length > 0 ? runtimeRegistrations : undefined,
|
|
27902
|
+
parse_status: parseStatus
|
|
27649
27903
|
};
|
|
27650
27904
|
} finally {
|
|
27651
27905
|
disposeTree(tree);
|
|
@@ -27656,6 +27910,15 @@ async function analyzeHtmlFile(code, filePath, options) {
|
|
|
27656
27910
|
const tree = await parse(code, "html");
|
|
27657
27911
|
try {
|
|
27658
27912
|
const meta = extractMeta(code, tree, filePath, "html");
|
|
27913
|
+
const htmlParseStatus = extractParseStatus(tree);
|
|
27914
|
+
if (htmlParseStatus.has_errors) {
|
|
27915
|
+
logger.warn("Partial parse — IR may be incomplete", {
|
|
27916
|
+
filePath,
|
|
27917
|
+
language: "html",
|
|
27918
|
+
errorCount: htmlParseStatus.error_count,
|
|
27919
|
+
firstErrorLine: htmlParseStatus.error_locations[0]?.line
|
|
27920
|
+
});
|
|
27921
|
+
}
|
|
27659
27922
|
const { scriptBlocks, eventHandlers } = extractHtmlContent(tree.rootNode);
|
|
27660
27923
|
logger.debug("HTML extraction", {
|
|
27661
27924
|
filePath,
|
|
@@ -27695,6 +27958,7 @@ async function analyzeHtmlFile(code, filePath, options) {
|
|
|
27695
27958
|
}
|
|
27696
27959
|
const attributeFindings = runHtmlAttributeSecurityChecks(tree.rootNode, filePath);
|
|
27697
27960
|
const result = mergeHtmlResults(meta, scriptResults, attributeFindings);
|
|
27961
|
+
result.parse_status = htmlParseStatus;
|
|
27698
27962
|
logger.debug("HTML analysis complete", {
|
|
27699
27963
|
filePath,
|
|
27700
27964
|
scriptBlocks: scriptResults.length,
|
|
@@ -27794,7 +28058,7 @@ var colors = {
|
|
|
27794
28058
|
};
|
|
27795
28059
|
|
|
27796
28060
|
// src/version.ts
|
|
27797
|
-
var version = "3.
|
|
28061
|
+
var version = "3.47.0";
|
|
27798
28062
|
|
|
27799
28063
|
// src/formatters.ts
|
|
27800
28064
|
var SINK_SEVERITY = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cognium-dev",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.47.0",
|
|
4
4
|
"description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"registry": "https://registry.npmjs.org/"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"circle-ir": "^3.
|
|
68
|
+
"circle-ir": "^3.47.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/node": "^25.5.0",
|