circle-ir 3.9.10 → 3.11.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/analysis/passes/cleanup-verify-pass.d.ts +28 -0
- package/dist/analysis/passes/cleanup-verify-pass.js +130 -0
- package/dist/analysis/passes/cleanup-verify-pass.js.map +1 -0
- package/dist/analysis/passes/missing-guard-dom-pass.d.ts +25 -0
- package/dist/analysis/passes/missing-guard-dom-pass.js +99 -0
- package/dist/analysis/passes/missing-guard-dom-pass.js.map +1 -0
- package/dist/analysis/passes/missing-override-pass.d.ts +27 -0
- package/dist/analysis/passes/missing-override-pass.js +110 -0
- package/dist/analysis/passes/missing-override-pass.js.map +1 -0
- package/dist/analysis/passes/sink-filter-pass.js +81 -8
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.js +6 -1
- package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +2 -3
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/unused-interface-method-pass.d.ts +27 -0
- package/dist/analysis/passes/unused-interface-method-pass.js +62 -0
- package/dist/analysis/passes/unused-interface-method-pass.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts +2 -1
- package/dist/analysis/taint-matcher.js +9 -5
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts +5 -1
- package/dist/analyzer.js +13 -1
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +1029 -16
- package/dist/core/circle-ir-core.cjs +8 -5
- package/dist/core/circle-ir-core.js +8 -5
- package/package.json +1 -1
|
@@ -10257,9 +10257,9 @@ var PYTHON_TAINTED_PATTERNS = [
|
|
|
10257
10257
|
{ pattern: /\brequest\.query_params\b/, sourceType: "http_param" },
|
|
10258
10258
|
{ pattern: /\brequest\.path_params\b/, sourceType: "http_param" }
|
|
10259
10259
|
];
|
|
10260
|
-
function analyzeTaint(calls, types, config = getDefaultConfig()) {
|
|
10260
|
+
function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy) {
|
|
10261
10261
|
const sources = findSources(calls, types, config.sources);
|
|
10262
|
-
const sinks = findSinks(calls, config.sinks);
|
|
10262
|
+
const sinks = findSinks(calls, config.sinks, typeHierarchy);
|
|
10263
10263
|
const sanitizers = findSanitizers(calls, types, config.sanitizers);
|
|
10264
10264
|
return { sources, sinks, sanitizers };
|
|
10265
10265
|
}
|
|
@@ -10465,11 +10465,11 @@ function isParameterizedQueryCall(call, pattern) {
|
|
|
10465
10465
|
}
|
|
10466
10466
|
return false;
|
|
10467
10467
|
}
|
|
10468
|
-
function findSinks(calls, patterns) {
|
|
10468
|
+
function findSinks(calls, patterns, typeHierarchy) {
|
|
10469
10469
|
const sinkMap = /* @__PURE__ */ new Map();
|
|
10470
10470
|
for (const call of calls) {
|
|
10471
10471
|
for (const pattern of patterns) {
|
|
10472
|
-
if (matchesSinkPattern(call, pattern)) {
|
|
10472
|
+
if (matchesSinkPattern(call, pattern, typeHierarchy)) {
|
|
10473
10473
|
if (isParameterizedQueryCall(call, pattern)) {
|
|
10474
10474
|
continue;
|
|
10475
10475
|
}
|
|
@@ -10561,7 +10561,7 @@ function isJavaScriptTaintedArgument(argExpression, sourcePatterns) {
|
|
|
10561
10561
|
}
|
|
10562
10562
|
return { isTainted: false, sourceType: null };
|
|
10563
10563
|
}
|
|
10564
|
-
function matchesSinkPattern(call, pattern) {
|
|
10564
|
+
function matchesSinkPattern(call, pattern, typeHierarchy) {
|
|
10565
10565
|
const callMethodName = call.method_name;
|
|
10566
10566
|
const patternMethod = pattern.method;
|
|
10567
10567
|
let methodMatches = callMethodName === patternMethod;
|
|
@@ -10577,6 +10577,9 @@ function matchesSinkPattern(call, pattern) {
|
|
|
10577
10577
|
return true;
|
|
10578
10578
|
}
|
|
10579
10579
|
if (call.receiver && !receiverMightBeClass(call.receiver, pattern.class)) {
|
|
10580
|
+
if (typeHierarchy && typeHierarchy.couldBeType(call.receiver, pattern.class)) {
|
|
10581
|
+
return true;
|
|
10582
|
+
}
|
|
10580
10583
|
return false;
|
|
10581
10584
|
}
|
|
10582
10585
|
if (!call.receiver) {
|
|
@@ -11279,6 +11282,633 @@ var CodeGraph = class {
|
|
|
11279
11282
|
}
|
|
11280
11283
|
};
|
|
11281
11284
|
|
|
11285
|
+
// src/resolution/type-hierarchy.ts
|
|
11286
|
+
var TypeHierarchyResolver = class {
|
|
11287
|
+
// All known types by FQN
|
|
11288
|
+
types = /* @__PURE__ */ new Map();
|
|
11289
|
+
// Simple name to FQN mapping (for resolution)
|
|
11290
|
+
nameToFqn = /* @__PURE__ */ new Map();
|
|
11291
|
+
// Subtype relationships: parent FQN -> child FQNs
|
|
11292
|
+
subtypes = /* @__PURE__ */ new Map();
|
|
11293
|
+
// Implementation relationships: interface FQN -> implementing class FQNs
|
|
11294
|
+
implementations = /* @__PURE__ */ new Map();
|
|
11295
|
+
/**
|
|
11296
|
+
* Add types from a CircleIR analysis result
|
|
11297
|
+
*/
|
|
11298
|
+
addFromIR(ir, filePath) {
|
|
11299
|
+
for (const type of ir.types) {
|
|
11300
|
+
this.addType(type, filePath, ir.meta.package || null);
|
|
11301
|
+
}
|
|
11302
|
+
}
|
|
11303
|
+
/**
|
|
11304
|
+
* Add a single type to the hierarchy
|
|
11305
|
+
*/
|
|
11306
|
+
addType(type, filePath, defaultPackage = null) {
|
|
11307
|
+
const pkg = type.package || defaultPackage || "";
|
|
11308
|
+
const fqn = pkg ? `${pkg}.${type.name}` : type.name;
|
|
11309
|
+
const node = {
|
|
11310
|
+
name: type.name,
|
|
11311
|
+
fqn,
|
|
11312
|
+
kind: type.kind,
|
|
11313
|
+
extends: type.extends,
|
|
11314
|
+
implements: type.implements,
|
|
11315
|
+
extendsInterfaces: type.kind === "interface" ? type.implements : [],
|
|
11316
|
+
file: filePath,
|
|
11317
|
+
line: type.start_line
|
|
11318
|
+
};
|
|
11319
|
+
this.types.set(fqn, node);
|
|
11320
|
+
if (!this.nameToFqn.has(type.name)) {
|
|
11321
|
+
this.nameToFqn.set(type.name, /* @__PURE__ */ new Set());
|
|
11322
|
+
}
|
|
11323
|
+
this.nameToFqn.get(type.name).add(fqn);
|
|
11324
|
+
if (type.kind === "class" || type.kind === "enum") {
|
|
11325
|
+
if (type.extends) {
|
|
11326
|
+
const parentFqn = this.resolveTypeName(type.extends, pkg);
|
|
11327
|
+
if (!this.subtypes.has(parentFqn)) {
|
|
11328
|
+
this.subtypes.set(parentFqn, /* @__PURE__ */ new Set());
|
|
11329
|
+
}
|
|
11330
|
+
this.subtypes.get(parentFqn).add(fqn);
|
|
11331
|
+
}
|
|
11332
|
+
for (const iface of type.implements) {
|
|
11333
|
+
const ifaceFqn = this.resolveTypeName(iface, pkg);
|
|
11334
|
+
if (!this.implementations.has(ifaceFqn)) {
|
|
11335
|
+
this.implementations.set(ifaceFqn, /* @__PURE__ */ new Set());
|
|
11336
|
+
}
|
|
11337
|
+
this.implementations.get(ifaceFqn).add(fqn);
|
|
11338
|
+
}
|
|
11339
|
+
} else if (type.kind === "interface") {
|
|
11340
|
+
for (const parentIface of type.implements) {
|
|
11341
|
+
const parentFqn = this.resolveTypeName(parentIface, pkg);
|
|
11342
|
+
if (!this.subtypes.has(parentFqn)) {
|
|
11343
|
+
this.subtypes.set(parentFqn, /* @__PURE__ */ new Set());
|
|
11344
|
+
}
|
|
11345
|
+
this.subtypes.get(parentFqn).add(fqn);
|
|
11346
|
+
}
|
|
11347
|
+
}
|
|
11348
|
+
}
|
|
11349
|
+
/**
|
|
11350
|
+
* Get all direct subtypes of a class
|
|
11351
|
+
*/
|
|
11352
|
+
getDirectSubtypes(className) {
|
|
11353
|
+
const fqn = this.resolveFqn(className);
|
|
11354
|
+
return Array.from(this.subtypes.get(fqn) || []);
|
|
11355
|
+
}
|
|
11356
|
+
/**
|
|
11357
|
+
* Get all subtypes (transitive) of a class
|
|
11358
|
+
*/
|
|
11359
|
+
getAllSubtypes(className) {
|
|
11360
|
+
const fqn = this.resolveFqn(className);
|
|
11361
|
+
const result = /* @__PURE__ */ new Set();
|
|
11362
|
+
const queue = [fqn];
|
|
11363
|
+
while (queue.length > 0) {
|
|
11364
|
+
const current = queue.shift();
|
|
11365
|
+
const children = this.subtypes.get(current);
|
|
11366
|
+
if (children) {
|
|
11367
|
+
for (const child of children) {
|
|
11368
|
+
if (!result.has(child)) {
|
|
11369
|
+
result.add(child);
|
|
11370
|
+
queue.push(child);
|
|
11371
|
+
}
|
|
11372
|
+
}
|
|
11373
|
+
}
|
|
11374
|
+
}
|
|
11375
|
+
return Array.from(result);
|
|
11376
|
+
}
|
|
11377
|
+
/**
|
|
11378
|
+
* Get all direct implementations of an interface
|
|
11379
|
+
*/
|
|
11380
|
+
getDirectImplementations(interfaceName) {
|
|
11381
|
+
const fqn = this.resolveFqn(interfaceName);
|
|
11382
|
+
return Array.from(this.implementations.get(fqn) || []);
|
|
11383
|
+
}
|
|
11384
|
+
/**
|
|
11385
|
+
* Get all implementations (including through subinterfaces) of an interface
|
|
11386
|
+
*/
|
|
11387
|
+
getAllImplementations(interfaceName) {
|
|
11388
|
+
const fqn = this.resolveFqn(interfaceName);
|
|
11389
|
+
const result = /* @__PURE__ */ new Set();
|
|
11390
|
+
const visited = /* @__PURE__ */ new Set();
|
|
11391
|
+
const queue = [fqn];
|
|
11392
|
+
while (queue.length > 0) {
|
|
11393
|
+
const current = queue.shift();
|
|
11394
|
+
if (visited.has(current)) continue;
|
|
11395
|
+
visited.add(current);
|
|
11396
|
+
const impls = this.implementations.get(current);
|
|
11397
|
+
if (impls) {
|
|
11398
|
+
for (const impl of impls) {
|
|
11399
|
+
result.add(impl);
|
|
11400
|
+
const subtypes = this.getAllSubtypes(impl);
|
|
11401
|
+
for (const subtype of subtypes) {
|
|
11402
|
+
result.add(subtype);
|
|
11403
|
+
}
|
|
11404
|
+
}
|
|
11405
|
+
}
|
|
11406
|
+
const subInterfaces = this.subtypes.get(current);
|
|
11407
|
+
if (subInterfaces) {
|
|
11408
|
+
for (const sub of subInterfaces) {
|
|
11409
|
+
queue.push(sub);
|
|
11410
|
+
}
|
|
11411
|
+
}
|
|
11412
|
+
}
|
|
11413
|
+
return Array.from(result);
|
|
11414
|
+
}
|
|
11415
|
+
/**
|
|
11416
|
+
* Check if a type is a subtype of another (including transitive)
|
|
11417
|
+
*/
|
|
11418
|
+
isSubtypeOf(childName, parentName) {
|
|
11419
|
+
const childFqn = this.resolveFqn(childName);
|
|
11420
|
+
const parentFqn = this.resolveFqn(parentName);
|
|
11421
|
+
if (childFqn === parentFqn) return true;
|
|
11422
|
+
const allSubtypes = this.getAllSubtypes(parentFqn);
|
|
11423
|
+
if (allSubtypes.includes(childFqn)) return true;
|
|
11424
|
+
const allImpls = this.getAllImplementations(parentFqn);
|
|
11425
|
+
if (allImpls.includes(childFqn)) return true;
|
|
11426
|
+
return false;
|
|
11427
|
+
}
|
|
11428
|
+
/**
|
|
11429
|
+
* Check if a type implements an interface (directly or through inheritance)
|
|
11430
|
+
* Also handles interface-extends-interface relationships
|
|
11431
|
+
*/
|
|
11432
|
+
implementsInterface(typeName, interfaceName) {
|
|
11433
|
+
const typeFqn = this.resolveFqn(typeName);
|
|
11434
|
+
const ifaceFqn = this.resolveFqn(interfaceName);
|
|
11435
|
+
const allImpls = this.getAllImplementations(ifaceFqn);
|
|
11436
|
+
if (allImpls.includes(typeFqn)) return true;
|
|
11437
|
+
const allSubtypes = this.getAllSubtypes(ifaceFqn);
|
|
11438
|
+
if (allSubtypes.includes(typeFqn)) return true;
|
|
11439
|
+
return false;
|
|
11440
|
+
}
|
|
11441
|
+
/**
|
|
11442
|
+
* Get type info by name
|
|
11443
|
+
*/
|
|
11444
|
+
getType(name2) {
|
|
11445
|
+
const fqn = this.resolveFqn(name2);
|
|
11446
|
+
return this.types.get(fqn);
|
|
11447
|
+
}
|
|
11448
|
+
/**
|
|
11449
|
+
* Get all types matching a simple name
|
|
11450
|
+
*/
|
|
11451
|
+
getTypesByName(simpleName) {
|
|
11452
|
+
const fqns = this.nameToFqn.get(simpleName);
|
|
11453
|
+
if (!fqns) return [];
|
|
11454
|
+
return Array.from(fqns).map((fqn) => this.types.get(fqn)).filter((t) => t !== void 0);
|
|
11455
|
+
}
|
|
11456
|
+
/**
|
|
11457
|
+
* Get the file where a type is defined
|
|
11458
|
+
*/
|
|
11459
|
+
getTypeFile(name2) {
|
|
11460
|
+
const type = this.getType(name2);
|
|
11461
|
+
return type?.file;
|
|
11462
|
+
}
|
|
11463
|
+
/**
|
|
11464
|
+
* Check if a receiver type could match a target class
|
|
11465
|
+
* Handles: exact match, subtype, implementation, simple name match
|
|
11466
|
+
*/
|
|
11467
|
+
couldBeType(receiverType, targetClass) {
|
|
11468
|
+
if (receiverType === targetClass) return true;
|
|
11469
|
+
const receiverSimple = this.getSimpleName(receiverType);
|
|
11470
|
+
const targetSimple = this.getSimpleName(targetClass);
|
|
11471
|
+
if (receiverSimple === targetSimple) return true;
|
|
11472
|
+
if (this.isSubtypeOf(receiverType, targetClass)) return true;
|
|
11473
|
+
const allSubtypes = this.getAllSubtypes(targetClass);
|
|
11474
|
+
const allImpls = this.getAllImplementations(targetClass);
|
|
11475
|
+
for (const sub of [...allSubtypes, ...allImpls]) {
|
|
11476
|
+
const subSimple = this.getSimpleName(sub);
|
|
11477
|
+
if (subSimple === receiverSimple) return true;
|
|
11478
|
+
}
|
|
11479
|
+
return false;
|
|
11480
|
+
}
|
|
11481
|
+
/**
|
|
11482
|
+
* Export hierarchy data in the CircleIR format
|
|
11483
|
+
*/
|
|
11484
|
+
toTypeHierarchyData() {
|
|
11485
|
+
const classes = {};
|
|
11486
|
+
const interfaces = {};
|
|
11487
|
+
for (const [fqn, node] of this.types) {
|
|
11488
|
+
if (node.kind === "class" || node.kind === "enum") {
|
|
11489
|
+
classes[fqn] = {
|
|
11490
|
+
file: node.file,
|
|
11491
|
+
extends: node.extends ? this.resolveTypeName(node.extends, this.getPackage(fqn)) : null,
|
|
11492
|
+
implements: node.implements.map((i2) => this.resolveTypeName(i2, this.getPackage(fqn))),
|
|
11493
|
+
subclasses: this.getDirectSubtypes(fqn)
|
|
11494
|
+
};
|
|
11495
|
+
} else if (node.kind === "interface") {
|
|
11496
|
+
interfaces[fqn] = {
|
|
11497
|
+
file: node.file,
|
|
11498
|
+
extends: node.extendsInterfaces.map((i2) => this.resolveTypeName(i2, this.getPackage(fqn))),
|
|
11499
|
+
implementations: this.getDirectImplementations(fqn)
|
|
11500
|
+
};
|
|
11501
|
+
}
|
|
11502
|
+
}
|
|
11503
|
+
return { classes, interfaces };
|
|
11504
|
+
}
|
|
11505
|
+
/**
|
|
11506
|
+
* Get statistics about the hierarchy
|
|
11507
|
+
*/
|
|
11508
|
+
getStats() {
|
|
11509
|
+
let classes = 0, interfaces = 0, enums = 0;
|
|
11510
|
+
for (const node of this.types.values()) {
|
|
11511
|
+
if (node.kind === "class") classes++;
|
|
11512
|
+
else if (node.kind === "interface") interfaces++;
|
|
11513
|
+
else if (node.kind === "enum") enums++;
|
|
11514
|
+
}
|
|
11515
|
+
return { totalTypes: this.types.size, classes, interfaces, enums };
|
|
11516
|
+
}
|
|
11517
|
+
/**
|
|
11518
|
+
* Get all types in the hierarchy
|
|
11519
|
+
*/
|
|
11520
|
+
getAllTypes() {
|
|
11521
|
+
return Array.from(this.types.values());
|
|
11522
|
+
}
|
|
11523
|
+
/**
|
|
11524
|
+
* Clear all data
|
|
11525
|
+
*/
|
|
11526
|
+
clear() {
|
|
11527
|
+
this.types.clear();
|
|
11528
|
+
this.nameToFqn.clear();
|
|
11529
|
+
this.subtypes.clear();
|
|
11530
|
+
this.implementations.clear();
|
|
11531
|
+
}
|
|
11532
|
+
// --- Private helpers ---
|
|
11533
|
+
/**
|
|
11534
|
+
* Resolve a type name to its FQN
|
|
11535
|
+
*/
|
|
11536
|
+
resolveTypeName(name2, currentPackage) {
|
|
11537
|
+
if (name2.includes(".")) return name2;
|
|
11538
|
+
const fqns = this.nameToFqn.get(name2);
|
|
11539
|
+
if (fqns && fqns.size === 1) {
|
|
11540
|
+
return Array.from(fqns)[0];
|
|
11541
|
+
}
|
|
11542
|
+
return currentPackage ? `${currentPackage}.${name2}` : name2;
|
|
11543
|
+
}
|
|
11544
|
+
/**
|
|
11545
|
+
* Resolve a name (simple or FQN) to its FQN
|
|
11546
|
+
*/
|
|
11547
|
+
resolveFqn(name2) {
|
|
11548
|
+
if (this.types.has(name2)) return name2;
|
|
11549
|
+
const fqns = this.nameToFqn.get(name2);
|
|
11550
|
+
if (fqns && fqns.size > 0) {
|
|
11551
|
+
return Array.from(fqns)[0];
|
|
11552
|
+
}
|
|
11553
|
+
return name2;
|
|
11554
|
+
}
|
|
11555
|
+
/**
|
|
11556
|
+
* Get simple name from FQN
|
|
11557
|
+
*/
|
|
11558
|
+
getSimpleName(name2) {
|
|
11559
|
+
const lastDot = name2.lastIndexOf(".");
|
|
11560
|
+
return lastDot === -1 ? name2 : name2.substring(lastDot + 1);
|
|
11561
|
+
}
|
|
11562
|
+
/**
|
|
11563
|
+
* Get package from FQN
|
|
11564
|
+
*/
|
|
11565
|
+
getPackage(fqn) {
|
|
11566
|
+
const lastDot = fqn.lastIndexOf(".");
|
|
11567
|
+
return lastDot === -1 ? "" : fqn.substring(0, lastDot);
|
|
11568
|
+
}
|
|
11569
|
+
};
|
|
11570
|
+
function createWithJdkTypes() {
|
|
11571
|
+
const resolver = new TypeHierarchyResolver();
|
|
11572
|
+
const jdbcTypes = [
|
|
11573
|
+
{
|
|
11574
|
+
name: "Statement",
|
|
11575
|
+
kind: "interface",
|
|
11576
|
+
package: "java.sql",
|
|
11577
|
+
extends: null,
|
|
11578
|
+
implements: [],
|
|
11579
|
+
annotations: [],
|
|
11580
|
+
methods: [],
|
|
11581
|
+
fields: [],
|
|
11582
|
+
start_line: 0,
|
|
11583
|
+
end_line: 0
|
|
11584
|
+
},
|
|
11585
|
+
{
|
|
11586
|
+
name: "PreparedStatement",
|
|
11587
|
+
kind: "interface",
|
|
11588
|
+
package: "java.sql",
|
|
11589
|
+
extends: null,
|
|
11590
|
+
implements: ["Statement"],
|
|
11591
|
+
annotations: [],
|
|
11592
|
+
methods: [],
|
|
11593
|
+
fields: [],
|
|
11594
|
+
start_line: 0,
|
|
11595
|
+
end_line: 0
|
|
11596
|
+
},
|
|
11597
|
+
{
|
|
11598
|
+
name: "CallableStatement",
|
|
11599
|
+
kind: "interface",
|
|
11600
|
+
package: "java.sql",
|
|
11601
|
+
extends: null,
|
|
11602
|
+
implements: ["PreparedStatement"],
|
|
11603
|
+
annotations: [],
|
|
11604
|
+
methods: [],
|
|
11605
|
+
fields: [],
|
|
11606
|
+
start_line: 0,
|
|
11607
|
+
end_line: 0
|
|
11608
|
+
}
|
|
11609
|
+
];
|
|
11610
|
+
const ioTypes = [
|
|
11611
|
+
{
|
|
11612
|
+
name: "InputStream",
|
|
11613
|
+
kind: "class",
|
|
11614
|
+
package: "java.io",
|
|
11615
|
+
extends: null,
|
|
11616
|
+
implements: [],
|
|
11617
|
+
annotations: [],
|
|
11618
|
+
methods: [],
|
|
11619
|
+
fields: [],
|
|
11620
|
+
start_line: 0,
|
|
11621
|
+
end_line: 0
|
|
11622
|
+
},
|
|
11623
|
+
{
|
|
11624
|
+
name: "FileInputStream",
|
|
11625
|
+
kind: "class",
|
|
11626
|
+
package: "java.io",
|
|
11627
|
+
extends: "InputStream",
|
|
11628
|
+
implements: [],
|
|
11629
|
+
annotations: [],
|
|
11630
|
+
methods: [],
|
|
11631
|
+
fields: [],
|
|
11632
|
+
start_line: 0,
|
|
11633
|
+
end_line: 0
|
|
11634
|
+
},
|
|
11635
|
+
{
|
|
11636
|
+
name: "OutputStream",
|
|
11637
|
+
kind: "class",
|
|
11638
|
+
package: "java.io",
|
|
11639
|
+
extends: null,
|
|
11640
|
+
implements: [],
|
|
11641
|
+
annotations: [],
|
|
11642
|
+
methods: [],
|
|
11643
|
+
fields: [],
|
|
11644
|
+
start_line: 0,
|
|
11645
|
+
end_line: 0
|
|
11646
|
+
},
|
|
11647
|
+
{
|
|
11648
|
+
name: "FileOutputStream",
|
|
11649
|
+
kind: "class",
|
|
11650
|
+
package: "java.io",
|
|
11651
|
+
extends: "OutputStream",
|
|
11652
|
+
implements: [],
|
|
11653
|
+
annotations: [],
|
|
11654
|
+
methods: [],
|
|
11655
|
+
fields: [],
|
|
11656
|
+
start_line: 0,
|
|
11657
|
+
end_line: 0
|
|
11658
|
+
},
|
|
11659
|
+
{
|
|
11660
|
+
name: "Writer",
|
|
11661
|
+
kind: "class",
|
|
11662
|
+
package: "java.io",
|
|
11663
|
+
extends: null,
|
|
11664
|
+
implements: [],
|
|
11665
|
+
annotations: [],
|
|
11666
|
+
methods: [],
|
|
11667
|
+
fields: [],
|
|
11668
|
+
start_line: 0,
|
|
11669
|
+
end_line: 0
|
|
11670
|
+
},
|
|
11671
|
+
{
|
|
11672
|
+
name: "PrintWriter",
|
|
11673
|
+
kind: "class",
|
|
11674
|
+
package: "java.io",
|
|
11675
|
+
extends: "Writer",
|
|
11676
|
+
implements: [],
|
|
11677
|
+
annotations: [],
|
|
11678
|
+
methods: [],
|
|
11679
|
+
fields: [],
|
|
11680
|
+
start_line: 0,
|
|
11681
|
+
end_line: 0
|
|
11682
|
+
}
|
|
11683
|
+
];
|
|
11684
|
+
const servletTypes = [
|
|
11685
|
+
{
|
|
11686
|
+
name: "ServletRequest",
|
|
11687
|
+
kind: "interface",
|
|
11688
|
+
package: "javax.servlet",
|
|
11689
|
+
extends: null,
|
|
11690
|
+
implements: [],
|
|
11691
|
+
annotations: [],
|
|
11692
|
+
methods: [],
|
|
11693
|
+
fields: [],
|
|
11694
|
+
start_line: 0,
|
|
11695
|
+
end_line: 0
|
|
11696
|
+
},
|
|
11697
|
+
{
|
|
11698
|
+
name: "HttpServletRequest",
|
|
11699
|
+
kind: "interface",
|
|
11700
|
+
package: "javax.servlet.http",
|
|
11701
|
+
extends: null,
|
|
11702
|
+
implements: ["javax.servlet.ServletRequest"],
|
|
11703
|
+
// FQN for cross-package reference
|
|
11704
|
+
annotations: [],
|
|
11705
|
+
methods: [],
|
|
11706
|
+
fields: [],
|
|
11707
|
+
start_line: 0,
|
|
11708
|
+
end_line: 0
|
|
11709
|
+
},
|
|
11710
|
+
{
|
|
11711
|
+
name: "ServletResponse",
|
|
11712
|
+
kind: "interface",
|
|
11713
|
+
package: "javax.servlet",
|
|
11714
|
+
extends: null,
|
|
11715
|
+
implements: [],
|
|
11716
|
+
annotations: [],
|
|
11717
|
+
methods: [],
|
|
11718
|
+
fields: [],
|
|
11719
|
+
start_line: 0,
|
|
11720
|
+
end_line: 0
|
|
11721
|
+
},
|
|
11722
|
+
{
|
|
11723
|
+
name: "HttpServletResponse",
|
|
11724
|
+
kind: "interface",
|
|
11725
|
+
package: "javax.servlet.http",
|
|
11726
|
+
extends: null,
|
|
11727
|
+
implements: ["javax.servlet.ServletResponse"],
|
|
11728
|
+
// FQN for cross-package reference
|
|
11729
|
+
annotations: [],
|
|
11730
|
+
methods: [],
|
|
11731
|
+
fields: [],
|
|
11732
|
+
start_line: 0,
|
|
11733
|
+
end_line: 0
|
|
11734
|
+
}
|
|
11735
|
+
];
|
|
11736
|
+
for (const type of [...jdbcTypes, ...ioTypes, ...servletTypes]) {
|
|
11737
|
+
resolver.addType(type, "jdk", type.package);
|
|
11738
|
+
}
|
|
11739
|
+
return resolver;
|
|
11740
|
+
}
|
|
11741
|
+
|
|
11742
|
+
// src/graph/dominator-graph.ts
|
|
11743
|
+
function computeRPO(cfg, entryId) {
|
|
11744
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
11745
|
+
for (const edge of cfg.edges) {
|
|
11746
|
+
const list = outgoing.get(edge.from) ?? [];
|
|
11747
|
+
list.push(edge.to);
|
|
11748
|
+
outgoing.set(edge.from, list);
|
|
11749
|
+
}
|
|
11750
|
+
const visited = /* @__PURE__ */ new Set();
|
|
11751
|
+
const postOrder = [];
|
|
11752
|
+
const stack = [{ id: entryId, childIndex: 0 }];
|
|
11753
|
+
visited.add(entryId);
|
|
11754
|
+
while (stack.length > 0) {
|
|
11755
|
+
const top = stack[stack.length - 1];
|
|
11756
|
+
const children = outgoing.get(top.id) ?? [];
|
|
11757
|
+
let pushed = false;
|
|
11758
|
+
while (top.childIndex < children.length) {
|
|
11759
|
+
const child = children[top.childIndex++];
|
|
11760
|
+
if (!visited.has(child)) {
|
|
11761
|
+
visited.add(child);
|
|
11762
|
+
stack.push({ id: child, childIndex: 0 });
|
|
11763
|
+
pushed = true;
|
|
11764
|
+
break;
|
|
11765
|
+
}
|
|
11766
|
+
}
|
|
11767
|
+
if (!pushed) {
|
|
11768
|
+
postOrder.push(top.id);
|
|
11769
|
+
stack.pop();
|
|
11770
|
+
}
|
|
11771
|
+
}
|
|
11772
|
+
const rpoOrder = postOrder.reverse();
|
|
11773
|
+
const rpoIndex = /* @__PURE__ */ new Map();
|
|
11774
|
+
for (let i2 = 0; i2 < rpoOrder.length; i2++) {
|
|
11775
|
+
rpoIndex.set(rpoOrder[i2], i2);
|
|
11776
|
+
}
|
|
11777
|
+
return { rpoOrder, rpoIndex };
|
|
11778
|
+
}
|
|
11779
|
+
function intersect(b1, b2, idom, rpoIndex) {
|
|
11780
|
+
let finger1 = b1;
|
|
11781
|
+
let finger2 = b2;
|
|
11782
|
+
while (finger1 !== finger2) {
|
|
11783
|
+
while ((rpoIndex.get(finger1) ?? Number.MAX_SAFE_INTEGER) > (rpoIndex.get(finger2) ?? Number.MAX_SAFE_INTEGER)) {
|
|
11784
|
+
const parent = idom.get(finger1);
|
|
11785
|
+
if (parent === void 0 || parent === finger1) break;
|
|
11786
|
+
finger1 = parent;
|
|
11787
|
+
}
|
|
11788
|
+
while ((rpoIndex.get(finger2) ?? Number.MAX_SAFE_INTEGER) > (rpoIndex.get(finger1) ?? Number.MAX_SAFE_INTEGER)) {
|
|
11789
|
+
const parent = idom.get(finger2);
|
|
11790
|
+
if (parent === void 0 || parent === finger2) break;
|
|
11791
|
+
finger2 = parent;
|
|
11792
|
+
}
|
|
11793
|
+
if (finger1 === finger2) break;
|
|
11794
|
+
const rpo1 = rpoIndex.get(finger1) ?? Number.MAX_SAFE_INTEGER;
|
|
11795
|
+
const rpo2 = rpoIndex.get(finger2) ?? Number.MAX_SAFE_INTEGER;
|
|
11796
|
+
if (rpo1 === rpo2 && finger1 !== finger2) break;
|
|
11797
|
+
}
|
|
11798
|
+
return finger1;
|
|
11799
|
+
}
|
|
11800
|
+
function computeIdom(cfg, rpoOrder, rpoIndex, entryId) {
|
|
11801
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
11802
|
+
for (const edge of cfg.edges) {
|
|
11803
|
+
const list = incoming.get(edge.to) ?? [];
|
|
11804
|
+
list.push(edge.from);
|
|
11805
|
+
incoming.set(edge.to, list);
|
|
11806
|
+
}
|
|
11807
|
+
const idom = /* @__PURE__ */ new Map();
|
|
11808
|
+
idom.set(entryId, entryId);
|
|
11809
|
+
let changed = true;
|
|
11810
|
+
while (changed) {
|
|
11811
|
+
changed = false;
|
|
11812
|
+
for (let i2 = 1; i2 < rpoOrder.length; i2++) {
|
|
11813
|
+
const b = rpoOrder[i2];
|
|
11814
|
+
const preds = incoming.get(b) ?? [];
|
|
11815
|
+
let newIdom;
|
|
11816
|
+
for (const p of preds) {
|
|
11817
|
+
if (idom.has(p)) {
|
|
11818
|
+
newIdom = p;
|
|
11819
|
+
break;
|
|
11820
|
+
}
|
|
11821
|
+
}
|
|
11822
|
+
if (newIdom === void 0) continue;
|
|
11823
|
+
for (const p of preds) {
|
|
11824
|
+
if (p === newIdom) continue;
|
|
11825
|
+
if (idom.has(p)) {
|
|
11826
|
+
newIdom = intersect(p, newIdom, idom, rpoIndex);
|
|
11827
|
+
}
|
|
11828
|
+
}
|
|
11829
|
+
if (idom.get(b) !== newIdom) {
|
|
11830
|
+
idom.set(b, newIdom);
|
|
11831
|
+
changed = true;
|
|
11832
|
+
}
|
|
11833
|
+
}
|
|
11834
|
+
}
|
|
11835
|
+
return idom;
|
|
11836
|
+
}
|
|
11837
|
+
var DominatorGraph = class {
|
|
11838
|
+
idom;
|
|
11839
|
+
rpoIndex;
|
|
11840
|
+
entryId;
|
|
11841
|
+
/** Cached reverse map: blockId → all blockIds it strictly dominates. */
|
|
11842
|
+
_dominated = null;
|
|
11843
|
+
constructor(cfg, entryId) {
|
|
11844
|
+
if (cfg.blocks.length === 0) {
|
|
11845
|
+
this.entryId = entryId ?? 0;
|
|
11846
|
+
this.idom = /* @__PURE__ */ new Map();
|
|
11847
|
+
this.rpoIndex = /* @__PURE__ */ new Map();
|
|
11848
|
+
return;
|
|
11849
|
+
}
|
|
11850
|
+
this.entryId = entryId ?? cfg.blocks.find((b) => b.type === "entry")?.id ?? cfg.blocks.reduce((a, b) => a.id < b.id ? a : b).id;
|
|
11851
|
+
const { rpoOrder, rpoIndex } = computeRPO(cfg, this.entryId);
|
|
11852
|
+
this.rpoIndex = rpoIndex;
|
|
11853
|
+
this.idom = computeIdom(cfg, rpoOrder, rpoIndex, this.entryId);
|
|
11854
|
+
this.idom.delete(this.entryId);
|
|
11855
|
+
}
|
|
11856
|
+
/**
|
|
11857
|
+
* Returns true if block `a` dominates block `b`.
|
|
11858
|
+
* A block dominates itself (reflexive).
|
|
11859
|
+
*/
|
|
11860
|
+
dominates(a, b) {
|
|
11861
|
+
if (a === b) return true;
|
|
11862
|
+
return this.strictlyDominates(a, b);
|
|
11863
|
+
}
|
|
11864
|
+
/**
|
|
11865
|
+
* Returns true if block `a` strictly dominates block `b` (a ≠ b and a dom b).
|
|
11866
|
+
*/
|
|
11867
|
+
strictlyDominates(a, b) {
|
|
11868
|
+
if (a === b) return false;
|
|
11869
|
+
const visited = /* @__PURE__ */ new Set();
|
|
11870
|
+
let cur = this.idom.get(b);
|
|
11871
|
+
while (cur !== void 0 && !visited.has(cur)) {
|
|
11872
|
+
if (cur === a) return true;
|
|
11873
|
+
visited.add(cur);
|
|
11874
|
+
cur = this.idom.get(cur);
|
|
11875
|
+
}
|
|
11876
|
+
return false;
|
|
11877
|
+
}
|
|
11878
|
+
/**
|
|
11879
|
+
* Returns the immediate dominator of `blockId`, or null for the entry block
|
|
11880
|
+
* (or any block not in the dominator tree).
|
|
11881
|
+
*/
|
|
11882
|
+
immediateDominator(blockId) {
|
|
11883
|
+
return this.idom.get(blockId) ?? null;
|
|
11884
|
+
}
|
|
11885
|
+
/**
|
|
11886
|
+
* Returns all block IDs strictly dominated by `blockId`.
|
|
11887
|
+
* (Computed lazily and cached on first call.)
|
|
11888
|
+
*/
|
|
11889
|
+
dominated(blockId) {
|
|
11890
|
+
if (!this._dominated) {
|
|
11891
|
+
this._dominated = /* @__PURE__ */ new Map();
|
|
11892
|
+
for (const [child, parent] of this.idom.entries()) {
|
|
11893
|
+
const ancestors = [];
|
|
11894
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11895
|
+
let cur = parent;
|
|
11896
|
+
while (cur !== void 0 && !seen.has(cur)) {
|
|
11897
|
+
seen.add(cur);
|
|
11898
|
+
ancestors.push(cur);
|
|
11899
|
+
cur = this.idom.get(cur);
|
|
11900
|
+
}
|
|
11901
|
+
for (const anc of ancestors) {
|
|
11902
|
+
const list = this._dominated.get(anc) ?? [];
|
|
11903
|
+
list.push(child);
|
|
11904
|
+
this._dominated.set(anc, list);
|
|
11905
|
+
}
|
|
11906
|
+
}
|
|
11907
|
+
}
|
|
11908
|
+
return this._dominated.get(blockId) ?? [];
|
|
11909
|
+
}
|
|
11910
|
+
};
|
|
11911
|
+
|
|
11282
11912
|
// src/graph/exception-flow-graph.ts
|
|
11283
11913
|
var ExceptionFlowGraph = class {
|
|
11284
11914
|
/** All try/catch pairs found in the CFG. */
|
|
@@ -16899,7 +17529,9 @@ var TaintMatcherPass = class {
|
|
|
16899
17529
|
};
|
|
16900
17530
|
}
|
|
16901
17531
|
}
|
|
16902
|
-
const
|
|
17532
|
+
const hierarchy = createWithJdkTypes();
|
|
17533
|
+
hierarchy.addFromIR(graph.ir, graph.ir.meta.file);
|
|
17534
|
+
const taint = analyzeTaint(calls, types, mergedConfig, hierarchy);
|
|
16903
17535
|
const sanitizerMethods = [];
|
|
16904
17536
|
for (const type of types) {
|
|
16905
17537
|
for (const method of type.methods) {
|
|
@@ -17424,6 +18056,73 @@ var SinkFilterPass = class {
|
|
|
17424
18056
|
return { sources, sinks: filtered, sanitizers };
|
|
17425
18057
|
}
|
|
17426
18058
|
};
|
|
18059
|
+
function evalArithmetic(input) {
|
|
18060
|
+
let pos = 0;
|
|
18061
|
+
function peek() {
|
|
18062
|
+
return input[pos] ?? "";
|
|
18063
|
+
}
|
|
18064
|
+
function consume() {
|
|
18065
|
+
return input[pos++] ?? "";
|
|
18066
|
+
}
|
|
18067
|
+
function skipWs() {
|
|
18068
|
+
while (pos < input.length && input[pos] === " ") pos++;
|
|
18069
|
+
}
|
|
18070
|
+
function parseNumber() {
|
|
18071
|
+
skipWs();
|
|
18072
|
+
let s = "";
|
|
18073
|
+
if (peek() === "-") {
|
|
18074
|
+
s += consume();
|
|
18075
|
+
}
|
|
18076
|
+
while (pos < input.length && /[\d.]/.test(input[pos])) s += consume();
|
|
18077
|
+
if (s === "" || s === "-") return null;
|
|
18078
|
+
const n = parseFloat(s);
|
|
18079
|
+
return isFinite(n) ? n : null;
|
|
18080
|
+
}
|
|
18081
|
+
function parseFactor() {
|
|
18082
|
+
skipWs();
|
|
18083
|
+
if (peek() === "(") {
|
|
18084
|
+
consume();
|
|
18085
|
+
const val = parseExpr();
|
|
18086
|
+
skipWs();
|
|
18087
|
+
if (peek() === ")") consume();
|
|
18088
|
+
return val;
|
|
18089
|
+
}
|
|
18090
|
+
return parseNumber();
|
|
18091
|
+
}
|
|
18092
|
+
function parseTerm() {
|
|
18093
|
+
let left = parseFactor();
|
|
18094
|
+
if (left === null) return null;
|
|
18095
|
+
while (true) {
|
|
18096
|
+
skipWs();
|
|
18097
|
+
const op = peek();
|
|
18098
|
+
if (op !== "*" && op !== "/") break;
|
|
18099
|
+
consume();
|
|
18100
|
+
const right = parseFactor();
|
|
18101
|
+
if (right === null) return null;
|
|
18102
|
+
left = op === "*" ? left * right : right === 0 ? null : left / right;
|
|
18103
|
+
if (left === null) return null;
|
|
18104
|
+
}
|
|
18105
|
+
return left;
|
|
18106
|
+
}
|
|
18107
|
+
function parseExpr() {
|
|
18108
|
+
let left = parseTerm();
|
|
18109
|
+
if (left === null) return null;
|
|
18110
|
+
while (true) {
|
|
18111
|
+
skipWs();
|
|
18112
|
+
const op = peek();
|
|
18113
|
+
if (op !== "+" && op !== "-") break;
|
|
18114
|
+
consume();
|
|
18115
|
+
const right = parseTerm();
|
|
18116
|
+
if (right === null) return null;
|
|
18117
|
+
left = op === "+" ? left + right : left - right;
|
|
18118
|
+
}
|
|
18119
|
+
return left;
|
|
18120
|
+
}
|
|
18121
|
+
if (!/^[\d\s+\-*/().]+$/.test(input)) return null;
|
|
18122
|
+
const result = parseExpr();
|
|
18123
|
+
skipWs();
|
|
18124
|
+
return pos === input.length ? result : null;
|
|
18125
|
+
}
|
|
17427
18126
|
function evaluateSimpleExpression(expr, symbols) {
|
|
17428
18127
|
let evaluated = expr;
|
|
17429
18128
|
for (const [name2, val] of symbols) {
|
|
@@ -17432,13 +18131,8 @@ function evaluateSimpleExpression(expr, symbols) {
|
|
|
17432
18131
|
evaluated = evaluated.replace(regex, String(val.value));
|
|
17433
18132
|
}
|
|
17434
18133
|
}
|
|
17435
|
-
|
|
17436
|
-
|
|
17437
|
-
const result = Function('"use strict"; return (' + evaluated + ")")();
|
|
17438
|
-
if (typeof result === "number" && !isNaN(result)) return String(Math.floor(result));
|
|
17439
|
-
}
|
|
17440
|
-
} catch {
|
|
17441
|
-
}
|
|
18134
|
+
const result = evalArithmetic(evaluated);
|
|
18135
|
+
if (result !== null && !isNaN(result)) return String(Math.floor(result));
|
|
17442
18136
|
return expr;
|
|
17443
18137
|
}
|
|
17444
18138
|
function isStringLiteralExpression(expr) {
|
|
@@ -17606,8 +18300,8 @@ var TaintPropagationPass = class {
|
|
|
17606
18300
|
for (const f of collectionFlows) {
|
|
17607
18301
|
if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) continue;
|
|
17608
18302
|
const flowForCheck = {
|
|
17609
|
-
source: { line: f.source_line
|
|
17610
|
-
sink: { line: f.sink_line
|
|
18303
|
+
source: { line: f.source_line },
|
|
18304
|
+
sink: { line: f.sink_line },
|
|
17611
18305
|
path: f.path.map((p) => ({ variable: p.variable, line: p.line }))
|
|
17612
18306
|
};
|
|
17613
18307
|
if (isCorrelatedPredicateFP(constProp, flowForCheck)) continue;
|
|
@@ -20182,6 +20876,325 @@ var UseAfterClosePass = class {
|
|
|
20182
20876
|
}
|
|
20183
20877
|
};
|
|
20184
20878
|
|
|
20879
|
+
// src/analysis/passes/missing-guard-dom-pass.ts
|
|
20880
|
+
var AUTH_METHODS = /* @__PURE__ */ new Set([
|
|
20881
|
+
"authenticate",
|
|
20882
|
+
"isAuthenticated",
|
|
20883
|
+
"isAuthorized",
|
|
20884
|
+
"isAdmin",
|
|
20885
|
+
"checkAuth",
|
|
20886
|
+
"hasPermission",
|
|
20887
|
+
"requiresAuth",
|
|
20888
|
+
"verifyToken",
|
|
20889
|
+
"validateToken",
|
|
20890
|
+
"checkRole",
|
|
20891
|
+
"authorize",
|
|
20892
|
+
"isLoggedIn"
|
|
20893
|
+
]);
|
|
20894
|
+
var SENSITIVE_METHODS = /* @__PURE__ */ new Set([
|
|
20895
|
+
"delete",
|
|
20896
|
+
"deleteById",
|
|
20897
|
+
"drop",
|
|
20898
|
+
"truncate",
|
|
20899
|
+
"executeUpdate",
|
|
20900
|
+
"createUser",
|
|
20901
|
+
"createAdmin",
|
|
20902
|
+
"modifyPermission",
|
|
20903
|
+
"grantRole",
|
|
20904
|
+
"setAdmin",
|
|
20905
|
+
"elevatePrivilege"
|
|
20906
|
+
]);
|
|
20907
|
+
var MissingGuardDomPass = class {
|
|
20908
|
+
name = "missing-guard-dom";
|
|
20909
|
+
category = "security";
|
|
20910
|
+
run(ctx) {
|
|
20911
|
+
const { graph, language } = ctx;
|
|
20912
|
+
if (language !== "java") return { findings: 0 };
|
|
20913
|
+
const { cfg, calls } = graph.ir;
|
|
20914
|
+
if (cfg.blocks.length === 0 || cfg.edges.length === 0) return { findings: 0 };
|
|
20915
|
+
const dom = new DominatorGraph(cfg);
|
|
20916
|
+
const file = graph.ir.meta.file;
|
|
20917
|
+
const authCallLines = [];
|
|
20918
|
+
const sensitiveOps = [];
|
|
20919
|
+
for (const call of calls) {
|
|
20920
|
+
if (AUTH_METHODS.has(call.method_name)) {
|
|
20921
|
+
authCallLines.push(call.location.line);
|
|
20922
|
+
}
|
|
20923
|
+
if (SENSITIVE_METHODS.has(call.method_name)) {
|
|
20924
|
+
sensitiveOps.push({ line: call.location.line, method: call.method_name });
|
|
20925
|
+
}
|
|
20926
|
+
}
|
|
20927
|
+
if (sensitiveOps.length === 0) return { findings: 0 };
|
|
20928
|
+
const blockContainingLine = (line) => cfg.blocks.find((b) => b.start_line <= line && line <= b.end_line) ?? null;
|
|
20929
|
+
const reportedMethods = /* @__PURE__ */ new Set();
|
|
20930
|
+
let count = 0;
|
|
20931
|
+
for (const op of sensitiveOps) {
|
|
20932
|
+
const opBlock = blockContainingLine(op.line);
|
|
20933
|
+
if (!opBlock) continue;
|
|
20934
|
+
const methodInfo = graph.methodAtLine(op.line);
|
|
20935
|
+
if (!methodInfo) continue;
|
|
20936
|
+
const methodKey = `${methodInfo.type.name}::${methodInfo.method.name}`;
|
|
20937
|
+
if (reportedMethods.has(methodKey)) continue;
|
|
20938
|
+
const { start_line, end_line } = methodInfo.method;
|
|
20939
|
+
const authInMethod = authCallLines.filter((l) => l >= start_line && l <= end_line);
|
|
20940
|
+
const dominated = authInMethod.some((authLine) => {
|
|
20941
|
+
const authBlock = blockContainingLine(authLine);
|
|
20942
|
+
return authBlock !== null && dom.dominates(authBlock.id, opBlock.id);
|
|
20943
|
+
});
|
|
20944
|
+
if (!dominated) {
|
|
20945
|
+
reportedMethods.add(methodKey);
|
|
20946
|
+
count++;
|
|
20947
|
+
ctx.addFinding({
|
|
20948
|
+
id: `missing-guard-dom-${file}-${op.line}`,
|
|
20949
|
+
pass: this.name,
|
|
20950
|
+
category: this.category,
|
|
20951
|
+
rule_id: "missing-guard-dom",
|
|
20952
|
+
cwe: "CWE-285",
|
|
20953
|
+
severity: "high",
|
|
20954
|
+
level: "error",
|
|
20955
|
+
message: `Sensitive operation \`${op.method}()\` at line ${op.line} is not dominated by an authentication check`,
|
|
20956
|
+
file,
|
|
20957
|
+
line: op.line,
|
|
20958
|
+
fix: `Add authentication/authorization check on all paths leading to line ${op.line}`,
|
|
20959
|
+
evidence: { method: op.method }
|
|
20960
|
+
});
|
|
20961
|
+
}
|
|
20962
|
+
}
|
|
20963
|
+
return { findings: count };
|
|
20964
|
+
}
|
|
20965
|
+
};
|
|
20966
|
+
|
|
20967
|
+
// src/analysis/passes/cleanup-verify-pass.ts
|
|
20968
|
+
var RESOURCE_CTORS4 = /* @__PURE__ */ new Set([
|
|
20969
|
+
"FileInputStream",
|
|
20970
|
+
"FileOutputStream",
|
|
20971
|
+
"FileReader",
|
|
20972
|
+
"FileWriter",
|
|
20973
|
+
"BufferedReader",
|
|
20974
|
+
"BufferedWriter",
|
|
20975
|
+
"PrintWriter",
|
|
20976
|
+
"InputStreamReader",
|
|
20977
|
+
"OutputStreamWriter",
|
|
20978
|
+
"RandomAccessFile",
|
|
20979
|
+
"DataInputStream",
|
|
20980
|
+
"DataOutputStream",
|
|
20981
|
+
"ObjectInputStream",
|
|
20982
|
+
"ObjectOutputStream",
|
|
20983
|
+
"ZipInputStream",
|
|
20984
|
+
"ZipOutputStream",
|
|
20985
|
+
"JarInputStream",
|
|
20986
|
+
"JarOutputStream",
|
|
20987
|
+
"GZIPInputStream",
|
|
20988
|
+
"GZIPOutputStream",
|
|
20989
|
+
"FileChannel",
|
|
20990
|
+
"Socket",
|
|
20991
|
+
"ServerSocket",
|
|
20992
|
+
"DatagramSocket"
|
|
20993
|
+
]);
|
|
20994
|
+
var RESOURCE_FACTORY_METHODS4 = /* @__PURE__ */ new Set([
|
|
20995
|
+
"openConnection",
|
|
20996
|
+
"openStream",
|
|
20997
|
+
"newInputStream",
|
|
20998
|
+
"newOutputStream",
|
|
20999
|
+
"newBufferedReader",
|
|
21000
|
+
"newBufferedWriter",
|
|
21001
|
+
"newByteChannel",
|
|
21002
|
+
"open",
|
|
21003
|
+
"createReadStream",
|
|
21004
|
+
"createWriteStream",
|
|
21005
|
+
"createConnection"
|
|
21006
|
+
]);
|
|
21007
|
+
var CLOSE_METHODS4 = /* @__PURE__ */ new Set([
|
|
21008
|
+
"close",
|
|
21009
|
+
"dispose",
|
|
21010
|
+
"shutdown",
|
|
21011
|
+
"disconnect",
|
|
21012
|
+
"release",
|
|
21013
|
+
"destroy",
|
|
21014
|
+
"free",
|
|
21015
|
+
"shutdownNow",
|
|
21016
|
+
"terminate"
|
|
21017
|
+
]);
|
|
21018
|
+
function buildPostDomGraph(cfg) {
|
|
21019
|
+
const exitBlock = cfg.blocks.find((b) => b.type === "exit") ?? cfg.blocks.find((b) => !cfg.edges.some((e) => e.from === b.id));
|
|
21020
|
+
if (!exitBlock || cfg.blocks.length === 0) {
|
|
21021
|
+
return new DominatorGraph({ blocks: [], edges: [] });
|
|
21022
|
+
}
|
|
21023
|
+
const reversed = {
|
|
21024
|
+
blocks: cfg.blocks,
|
|
21025
|
+
edges: cfg.edges.map((e) => ({ from: e.to, to: e.from, type: e.type }))
|
|
21026
|
+
};
|
|
21027
|
+
return new DominatorGraph(reversed, exitBlock.id);
|
|
21028
|
+
}
|
|
21029
|
+
var CleanupVerifyPass = class {
|
|
21030
|
+
name = "cleanup-verify";
|
|
21031
|
+
category = "reliability";
|
|
21032
|
+
run(ctx) {
|
|
21033
|
+
const { graph, language } = ctx;
|
|
21034
|
+
if (language === "rust" || language === "bash") return { findings: 0 };
|
|
21035
|
+
const { cfg, calls } = graph.ir;
|
|
21036
|
+
const file = graph.ir.meta.file;
|
|
21037
|
+
if (cfg.blocks.length === 0) return { findings: 0 };
|
|
21038
|
+
const postDom = buildPostDomGraph(cfg);
|
|
21039
|
+
const blockContainingLine = (line) => cfg.blocks.find((b) => b.start_line <= line && line <= b.end_line) ?? null;
|
|
21040
|
+
let count = 0;
|
|
21041
|
+
for (const call of calls) {
|
|
21042
|
+
const name2 = call.method_name;
|
|
21043
|
+
const isConstructor = call.is_constructor === true && RESOURCE_CTORS4.has(name2);
|
|
21044
|
+
const isFactory = !call.is_constructor && RESOURCE_FACTORY_METHODS4.has(name2);
|
|
21045
|
+
if (!isConstructor && !isFactory) continue;
|
|
21046
|
+
const openLine = call.location.line;
|
|
21047
|
+
const defs = graph.defsAtLine(openLine);
|
|
21048
|
+
if (defs.length === 0) continue;
|
|
21049
|
+
const resourceVar = defs[0].variable;
|
|
21050
|
+
const methodInfo = graph.methodAtLine(openLine);
|
|
21051
|
+
if (!methodInfo) continue;
|
|
21052
|
+
const methodEnd = methodInfo.method.end_line;
|
|
21053
|
+
const closeCall = calls.find(
|
|
21054
|
+
(c) => CLOSE_METHODS4.has(c.method_name) && c.receiver === resourceVar && c.location.line > openLine && c.location.line <= methodEnd
|
|
21055
|
+
);
|
|
21056
|
+
if (!closeCall) continue;
|
|
21057
|
+
const openBlock = blockContainingLine(openLine);
|
|
21058
|
+
const closeBlock = blockContainingLine(closeCall.location.line);
|
|
21059
|
+
if (!openBlock || !closeBlock) continue;
|
|
21060
|
+
if (postDom.dominates(closeBlock.id, openBlock.id)) continue;
|
|
21061
|
+
count++;
|
|
21062
|
+
ctx.addFinding({
|
|
21063
|
+
id: `cleanup-verify-${file}-${openLine}`,
|
|
21064
|
+
pass: this.name,
|
|
21065
|
+
category: this.category,
|
|
21066
|
+
rule_id: "cleanup-verify",
|
|
21067
|
+
cwe: "CWE-772",
|
|
21068
|
+
severity: "medium",
|
|
21069
|
+
level: "warning",
|
|
21070
|
+
message: `Resource \`${resourceVar}\` opened at line ${openLine} may not close on all paths \u2014 close() at line ${closeCall.location.line} does not post-dominate the acquisition`,
|
|
21071
|
+
file,
|
|
21072
|
+
line: openLine,
|
|
21073
|
+
fix: "Use try-with-resources (Java) or a finally block to guarantee cleanup on all paths",
|
|
21074
|
+
evidence: {
|
|
21075
|
+
resource: name2,
|
|
21076
|
+
variable: resourceVar,
|
|
21077
|
+
close_line: closeCall.location.line
|
|
21078
|
+
}
|
|
21079
|
+
});
|
|
21080
|
+
}
|
|
21081
|
+
return { findings: count };
|
|
21082
|
+
}
|
|
21083
|
+
};
|
|
21084
|
+
|
|
21085
|
+
// src/analysis/passes/missing-override-pass.ts
|
|
21086
|
+
var MissingOverridePass = class {
|
|
21087
|
+
name = "missing-override";
|
|
21088
|
+
category = "maintainability";
|
|
21089
|
+
run(ctx) {
|
|
21090
|
+
const { graph, language } = ctx;
|
|
21091
|
+
if (language !== "java") return { findings: 0 };
|
|
21092
|
+
const { types } = graph.ir;
|
|
21093
|
+
const file = graph.ir.meta.file;
|
|
21094
|
+
if (types.length === 0) return { findings: 0 };
|
|
21095
|
+
const methodsByClass = /* @__PURE__ */ new Map();
|
|
21096
|
+
for (const type of types) {
|
|
21097
|
+
methodsByClass.set(type.name, new Set(type.methods.map((m) => m.name)));
|
|
21098
|
+
}
|
|
21099
|
+
const parentMap = /* @__PURE__ */ new Map();
|
|
21100
|
+
for (const type of types) {
|
|
21101
|
+
if (type.extends) {
|
|
21102
|
+
const parent = type.extends.replace(/<[^>]*>/g, "").trim();
|
|
21103
|
+
parentMap.set(type.name, parent);
|
|
21104
|
+
}
|
|
21105
|
+
}
|
|
21106
|
+
if (parentMap.size === 0) return { findings: 0 };
|
|
21107
|
+
const getAncestorMethods = (className) => {
|
|
21108
|
+
const methods = /* @__PURE__ */ new Set();
|
|
21109
|
+
const visited = /* @__PURE__ */ new Set();
|
|
21110
|
+
let current = parentMap.get(className);
|
|
21111
|
+
let hops = 0;
|
|
21112
|
+
while (current && !visited.has(current) && hops < 10) {
|
|
21113
|
+
visited.add(current);
|
|
21114
|
+
const parentMethods = methodsByClass.get(current);
|
|
21115
|
+
if (parentMethods) {
|
|
21116
|
+
for (const m of parentMethods) methods.add(m);
|
|
21117
|
+
}
|
|
21118
|
+
current = parentMap.get(current);
|
|
21119
|
+
hops++;
|
|
21120
|
+
}
|
|
21121
|
+
return methods;
|
|
21122
|
+
};
|
|
21123
|
+
const dedup = /* @__PURE__ */ new Set();
|
|
21124
|
+
let count = 0;
|
|
21125
|
+
for (const type of types) {
|
|
21126
|
+
if (!parentMap.has(type.name)) continue;
|
|
21127
|
+
const ancestorMethods = getAncestorMethods(type.name);
|
|
21128
|
+
if (ancestorMethods.size === 0) continue;
|
|
21129
|
+
for (const method of type.methods) {
|
|
21130
|
+
if (method.name === type.name) continue;
|
|
21131
|
+
if (method.modifiers.includes("private")) continue;
|
|
21132
|
+
if (method.modifiers.includes("static")) continue;
|
|
21133
|
+
if (method.modifiers.includes("abstract")) continue;
|
|
21134
|
+
if (!ancestorMethods.has(method.name)) continue;
|
|
21135
|
+
if (method.annotations.includes("Override")) continue;
|
|
21136
|
+
const key = `${type.name}:${method.name}`;
|
|
21137
|
+
if (dedup.has(key)) continue;
|
|
21138
|
+
dedup.add(key);
|
|
21139
|
+
count++;
|
|
21140
|
+
ctx.addFinding({
|
|
21141
|
+
id: `missing-override-${file}-${method.start_line}`,
|
|
21142
|
+
pass: this.name,
|
|
21143
|
+
category: this.category,
|
|
21144
|
+
rule_id: "missing-override",
|
|
21145
|
+
severity: "low",
|
|
21146
|
+
level: "warning",
|
|
21147
|
+
message: `Method \`${method.name}()\` in \`${type.name}\` overrides a parent method but lacks @Override`,
|
|
21148
|
+
file,
|
|
21149
|
+
line: method.start_line,
|
|
21150
|
+
fix: "Add @Override to make the intent explicit and catch signature mismatches at compile time",
|
|
21151
|
+
evidence: { className: type.name, methodName: method.name }
|
|
21152
|
+
});
|
|
21153
|
+
}
|
|
21154
|
+
}
|
|
21155
|
+
return { findings: count };
|
|
21156
|
+
}
|
|
21157
|
+
};
|
|
21158
|
+
|
|
21159
|
+
// src/analysis/passes/unused-interface-method-pass.ts
|
|
21160
|
+
var UnusedInterfaceMethodPass = class {
|
|
21161
|
+
name = "unused-interface-method";
|
|
21162
|
+
category = "maintainability";
|
|
21163
|
+
run(ctx) {
|
|
21164
|
+
const { graph, language } = ctx;
|
|
21165
|
+
if (language !== "java" && language !== "typescript") return { findings: 0 };
|
|
21166
|
+
const { types, calls } = graph.ir;
|
|
21167
|
+
const file = graph.ir.meta.file;
|
|
21168
|
+
const calledMethods = new Set(calls.map((c) => c.method_name));
|
|
21169
|
+
const dedup = /* @__PURE__ */ new Set();
|
|
21170
|
+
let count = 0;
|
|
21171
|
+
for (const type of types) {
|
|
21172
|
+
if (type.kind !== "interface") continue;
|
|
21173
|
+
for (const method of type.methods) {
|
|
21174
|
+
if (calledMethods.has(method.name)) continue;
|
|
21175
|
+
const key = `${type.name}:${method.name}`;
|
|
21176
|
+
if (dedup.has(key)) continue;
|
|
21177
|
+
dedup.add(key);
|
|
21178
|
+
count++;
|
|
21179
|
+
ctx.addFinding({
|
|
21180
|
+
id: `unused-interface-method-${file}-${method.start_line}`,
|
|
21181
|
+
pass: this.name,
|
|
21182
|
+
category: this.category,
|
|
21183
|
+
rule_id: "unused-interface-method",
|
|
21184
|
+
severity: "low",
|
|
21185
|
+
level: "note",
|
|
21186
|
+
message: `Interface method \`${method.name}()\` in \`${type.name}\` is never called in this file`,
|
|
21187
|
+
file,
|
|
21188
|
+
line: method.start_line,
|
|
21189
|
+
fix: "Remove this method or verify it is used from other files; unused interface methods inflate the public API",
|
|
21190
|
+
evidence: { interfaceName: type.name, methodName: method.name }
|
|
21191
|
+
});
|
|
21192
|
+
}
|
|
21193
|
+
}
|
|
21194
|
+
return { findings: count };
|
|
21195
|
+
}
|
|
21196
|
+
};
|
|
21197
|
+
|
|
20185
21198
|
// src/analysis/metrics/passes/size-metrics-pass.ts
|
|
20186
21199
|
var SizeMetricsPass = class {
|
|
20187
21200
|
name = "size-metrics";
|
|
@@ -20981,7 +21994,7 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
20981
21994
|
enriched: {}
|
|
20982
21995
|
});
|
|
20983
21996
|
const config = options.taintConfig ?? getDefaultConfig();
|
|
20984
|
-
const { results, findings } = new AnalysisPipeline().add(new TaintMatcherPass()).add(new ConstantPropagationPass(tree)).add(new LanguageSourcesPass()).add(new SinkFilterPass()).add(new TaintPropagationPass()).add(new InterproceduralPass()).add(new DeadCodePass()).add(new MissingAwaitPass()).add(new NPlusOnePass()).add(new MissingPublicDocPass()).add(new TodoInProdPass()).add(new StringConcatLoopPass()).add(new SyncIoAsyncPass()).add(new UncheckedReturnPass()).add(new NullDerefPass()).add(new ResourceLeakPass()).add(new VariableShadowingPass()).add(new LeakedGlobalPass()).add(new UnusedVariablePass()).add(new DependencyFanOutPass()).add(new StaleDocRefPass()).add(new InfiniteLoopPass()).add(new DeepInheritancePass()).add(new RedundantLoopPass()).add(new UnboundedCollectionPass()).add(new SerialAwaitPass()).add(new ReactInlineJsxPass()).add(new SwallowedExceptionPass()).add(new BroadCatchPass()).add(new UnhandledExceptionPass()).add(new DoubleClosePass()).add(new UseAfterClosePass()).run(graph, code, language, config);
|
|
21997
|
+
const { results, findings } = new AnalysisPipeline().add(new TaintMatcherPass()).add(new ConstantPropagationPass(tree)).add(new LanguageSourcesPass()).add(new SinkFilterPass()).add(new TaintPropagationPass()).add(new InterproceduralPass()).add(new DeadCodePass()).add(new MissingAwaitPass()).add(new NPlusOnePass()).add(new MissingPublicDocPass()).add(new TodoInProdPass()).add(new StringConcatLoopPass()).add(new SyncIoAsyncPass()).add(new UncheckedReturnPass()).add(new NullDerefPass()).add(new ResourceLeakPass()).add(new VariableShadowingPass()).add(new LeakedGlobalPass()).add(new UnusedVariablePass()).add(new DependencyFanOutPass()).add(new StaleDocRefPass()).add(new InfiniteLoopPass()).add(new DeepInheritancePass()).add(new RedundantLoopPass()).add(new UnboundedCollectionPass()).add(new SerialAwaitPass()).add(new ReactInlineJsxPass()).add(new SwallowedExceptionPass()).add(new BroadCatchPass()).add(new UnhandledExceptionPass()).add(new DoubleClosePass()).add(new UseAfterClosePass()).add(new MissingGuardDomPass()).add(new CleanupVerifyPass()).add(new MissingOverridePass()).add(new UnusedInterfaceMethodPass()).run(graph, code, language, config);
|
|
20985
21998
|
const sinkFilter = results.get("sink-filter");
|
|
20986
21999
|
const interProc = results.get("interprocedural");
|
|
20987
22000
|
const taint = {
|