circle-ir 3.8.3 → 3.9.7
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 +82 -5
- package/dist/analysis/config-loader.js +1 -1
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/dfg-verifier.d.ts +3 -14
- package/dist/analysis/dfg-verifier.js +43 -74
- package/dist/analysis/dfg-verifier.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts +5 -1
- package/dist/analysis/interprocedural.js +62 -60
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/metrics/index.d.ts +2 -0
- package/dist/analysis/metrics/index.js +2 -0
- package/dist/analysis/metrics/index.js.map +1 -0
- package/dist/analysis/metrics/metric-pass.d.ts +27 -0
- package/dist/analysis/metrics/metric-pass.js +2 -0
- package/dist/analysis/metrics/metric-pass.js.map +1 -0
- package/dist/analysis/metrics/metric-runner.d.ts +21 -0
- package/dist/analysis/metrics/metric-runner.js +47 -0
- package/dist/analysis/metrics/metric-runner.js.map +1 -0
- package/dist/analysis/metrics/passes/cohesion-metrics-pass.d.ts +21 -0
- package/dist/analysis/metrics/passes/cohesion-metrics-pass.js +100 -0
- package/dist/analysis/metrics/passes/cohesion-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/complexity-metrics-pass.d.ts +15 -0
- package/dist/analysis/metrics/passes/complexity-metrics-pass.js +76 -0
- package/dist/analysis/metrics/passes/complexity-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/composite-metrics-pass.d.ts +17 -0
- package/dist/analysis/metrics/passes/composite-metrics-pass.js +77 -0
- package/dist/analysis/metrics/passes/composite-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/coupling-metrics-pass.d.ts +19 -0
- package/dist/analysis/metrics/passes/coupling-metrics-pass.js +94 -0
- package/dist/analysis/metrics/passes/coupling-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/data-flow-metrics-pass.d.ts +14 -0
- package/dist/analysis/metrics/passes/data-flow-metrics-pass.js +25 -0
- package/dist/analysis/metrics/passes/data-flow-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/documentation-metrics-pass.d.ts +15 -0
- package/dist/analysis/metrics/passes/documentation-metrics-pass.js +64 -0
- package/dist/analysis/metrics/passes/documentation-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/halstead-metrics-pass.d.ts +16 -0
- package/dist/analysis/metrics/passes/halstead-metrics-pass.js +95 -0
- package/dist/analysis/metrics/passes/halstead-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/inheritance-metrics-pass.d.ts +18 -0
- package/dist/analysis/metrics/passes/inheritance-metrics-pass.js +73 -0
- package/dist/analysis/metrics/passes/inheritance-metrics-pass.js.map +1 -0
- package/dist/analysis/metrics/passes/size-metrics-pass.d.ts +11 -0
- package/dist/analysis/metrics/passes/size-metrics-pass.js +64 -0
- package/dist/analysis/metrics/passes/size-metrics-pass.js.map +1 -0
- package/dist/analysis/passes/circular-dependency-pass.d.ts +18 -0
- package/dist/analysis/passes/circular-dependency-pass.js +39 -0
- package/dist/analysis/passes/circular-dependency-pass.js.map +1 -0
- package/dist/analysis/passes/constant-propagation-pass.d.ts +22 -0
- package/dist/analysis/passes/constant-propagation-pass.js +44 -0
- package/dist/analysis/passes/constant-propagation-pass.js.map +1 -0
- package/dist/analysis/passes/cross-file-pass.d.ts +27 -0
- package/dist/analysis/passes/cross-file-pass.js +102 -0
- package/dist/analysis/passes/cross-file-pass.js.map +1 -0
- package/dist/analysis/passes/dead-code-pass.d.ts +25 -0
- package/dist/analysis/passes/dead-code-pass.js +117 -0
- package/dist/analysis/passes/dead-code-pass.js.map +1 -0
- package/dist/analysis/passes/dependency-fan-out-pass.d.ts +19 -0
- package/dist/analysis/passes/dependency-fan-out-pass.js +35 -0
- package/dist/analysis/passes/dependency-fan-out-pass.js.map +1 -0
- package/dist/analysis/passes/interprocedural-pass.d.ts +29 -0
- package/dist/analysis/passes/interprocedural-pass.js +169 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -0
- package/dist/analysis/passes/language-sources-pass.d.ts +76 -0
- package/dist/analysis/passes/language-sources-pass.js +491 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -0
- package/dist/analysis/passes/leaked-global-pass.d.ts +34 -0
- package/dist/analysis/passes/leaked-global-pass.js +108 -0
- package/dist/analysis/passes/leaked-global-pass.js.map +1 -0
- package/dist/analysis/passes/missing-await-pass.d.ts +29 -0
- package/dist/analysis/passes/missing-await-pass.js +90 -0
- package/dist/analysis/passes/missing-await-pass.js.map +1 -0
- package/dist/analysis/passes/missing-public-doc-pass.d.ts +35 -0
- package/dist/analysis/passes/missing-public-doc-pass.js +148 -0
- package/dist/analysis/passes/missing-public-doc-pass.js.map +1 -0
- package/dist/analysis/passes/n-plus-one-pass.d.ts +29 -0
- package/dist/analysis/passes/n-plus-one-pass.js +100 -0
- package/dist/analysis/passes/n-plus-one-pass.js.map +1 -0
- package/dist/analysis/passes/null-deref-pass.d.ts +32 -0
- package/dist/analysis/passes/null-deref-pass.js +130 -0
- package/dist/analysis/passes/null-deref-pass.js.map +1 -0
- package/dist/analysis/passes/orphan-module-pass.d.ts +21 -0
- package/dist/analysis/passes/orphan-module-pass.js +38 -0
- package/dist/analysis/passes/orphan-module-pass.js.map +1 -0
- package/dist/analysis/passes/resource-leak-pass.d.ts +43 -0
- package/dist/analysis/passes/resource-leak-pass.js +156 -0
- package/dist/analysis/passes/resource-leak-pass.js.map +1 -0
- package/dist/analysis/passes/sink-filter-pass.d.ts +39 -0
- package/dist/analysis/passes/sink-filter-pass.js +231 -0
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -0
- package/dist/analysis/passes/stale-doc-ref-pass.d.ts +21 -0
- package/dist/analysis/passes/stale-doc-ref-pass.js +96 -0
- package/dist/analysis/passes/stale-doc-ref-pass.js.map +1 -0
- package/dist/analysis/passes/string-concat-loop-pass.d.ts +26 -0
- package/dist/analysis/passes/string-concat-loop-pass.js +87 -0
- package/dist/analysis/passes/string-concat-loop-pass.js.map +1 -0
- package/dist/analysis/passes/sync-io-async-pass.d.ts +28 -0
- package/dist/analysis/passes/sync-io-async-pass.js +80 -0
- package/dist/analysis/passes/sync-io-async-pass.js.map +1 -0
- package/dist/analysis/passes/taint-matcher-pass.d.ts +24 -0
- package/dist/analysis/passes/taint-matcher-pass.js +71 -0
- package/dist/analysis/passes/taint-matcher-pass.js.map +1 -0
- package/dist/analysis/passes/taint-propagation-pass.d.ts +22 -0
- package/dist/analysis/passes/taint-propagation-pass.js +266 -0
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -0
- package/dist/analysis/passes/todo-in-prod-pass.d.ts +28 -0
- package/dist/analysis/passes/todo-in-prod-pass.js +71 -0
- package/dist/analysis/passes/todo-in-prod-pass.js.map +1 -0
- package/dist/analysis/passes/unchecked-return-pass.d.ts +34 -0
- package/dist/analysis/passes/unchecked-return-pass.js +106 -0
- package/dist/analysis/passes/unchecked-return-pass.js.map +1 -0
- package/dist/analysis/passes/unused-variable-pass.d.ts +36 -0
- package/dist/analysis/passes/unused-variable-pass.js +150 -0
- package/dist/analysis/passes/unused-variable-pass.js.map +1 -0
- package/dist/analysis/passes/variable-shadowing-pass.d.ts +41 -0
- package/dist/analysis/passes/variable-shadowing-pass.js +211 -0
- package/dist/analysis/passes/variable-shadowing-pass.js.map +1 -0
- package/dist/analysis/path-finder.d.ts +3 -13
- package/dist/analysis/path-finder.js +48 -63
- package/dist/analysis/path-finder.js.map +1 -1
- package/dist/analysis/taint-matcher.js +8 -1
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +5 -1
- package/dist/analysis/taint-propagation.js +44 -41
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts +42 -1
- package/dist/analyzer.js +234 -1476
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +3414 -1272
- package/dist/core/circle-ir-core.cjs +361 -107
- package/dist/core/circle-ir-core.js +361 -107
- package/dist/core/extractors/imports.js +18 -0
- package/dist/core/extractors/imports.js.map +1 -1
- package/dist/graph/analysis-pass.d.ts +68 -0
- package/dist/graph/analysis-pass.js +51 -0
- package/dist/graph/analysis-pass.js.map +1 -0
- package/dist/graph/code-graph.d.ts +92 -0
- package/dist/graph/code-graph.js +262 -0
- package/dist/graph/code-graph.js.map +1 -0
- package/dist/graph/import-graph.d.ts +33 -0
- package/dist/graph/import-graph.js +170 -0
- package/dist/graph/import-graph.js.map +1 -0
- package/dist/graph/index.d.ts +4 -0
- package/dist/graph/index.js +5 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/project-graph.d.ts +43 -0
- package/dist/graph/project-graph.js +80 -0
- package/dist/graph/project-graph.js.map +1 -0
- package/dist/graph/scope-graph.d.ts +63 -0
- package/dist/graph/scope-graph.js +89 -0
- package/dist/graph/scope-graph.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/resolution/cross-file.js +52 -19
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/types/index.d.ts +151 -0
- package/docs/SPEC.md +10 -6
- package/package.json +3 -2
|
@@ -6800,6 +6800,20 @@ function extractJavaScriptImports(tree) {
|
|
|
6800
6800
|
const importInfos = extractJSImportInfo(importStmt);
|
|
6801
6801
|
imports.push(...importInfos);
|
|
6802
6802
|
}
|
|
6803
|
+
const exportStatements = findNodes(tree.rootNode, "export_statement");
|
|
6804
|
+
for (const exportStmt of exportStatements) {
|
|
6805
|
+
const sourceNode = exportStmt.childForFieldName("source");
|
|
6806
|
+
if (!sourceNode) continue;
|
|
6807
|
+
const fromPackage = getNodeText(sourceNode).replace(/['"]/g, "");
|
|
6808
|
+
if (!fromPackage) continue;
|
|
6809
|
+
imports.push({
|
|
6810
|
+
imported_name: "*",
|
|
6811
|
+
from_package: fromPackage,
|
|
6812
|
+
alias: null,
|
|
6813
|
+
is_wildcard: true,
|
|
6814
|
+
line_number: exportStmt.startPosition.row + 1
|
|
6815
|
+
});
|
|
6816
|
+
}
|
|
6803
6817
|
const requireCalls = findRequireCalls(tree);
|
|
6804
6818
|
imports.push(...requireCalls);
|
|
6805
6819
|
return imports;
|
|
@@ -9566,7 +9580,7 @@ var DEFAULT_SINKS = [
|
|
|
9566
9580
|
// Jenkins/CI Pipeline execution
|
|
9567
9581
|
{ method: "executeScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
9568
9582
|
{ method: "runScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
9569
|
-
{ method: "evaluate", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
9583
|
+
{ method: "evaluate", class: "ScriptEngine", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
|
|
9570
9584
|
{ method: "execute", class: "Script", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [] },
|
|
9571
9585
|
{ method: "run", class: "Script", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [] },
|
|
9572
9586
|
{ method: "checkout", class: "SCM", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0] },
|
|
@@ -10526,7 +10540,10 @@ function matchesSourcePattern(call, pattern) {
|
|
|
10526
10540
|
return false;
|
|
10527
10541
|
}
|
|
10528
10542
|
if (pattern.class && pattern.class !== "constructor") {
|
|
10529
|
-
if (
|
|
10543
|
+
if (!call.receiver) {
|
|
10544
|
+
return false;
|
|
10545
|
+
}
|
|
10546
|
+
if (!receiverMightBeClass(call.receiver, pattern.class)) {
|
|
10530
10547
|
return false;
|
|
10531
10548
|
}
|
|
10532
10549
|
}
|
|
@@ -10885,38 +10902,296 @@ function formatSanitizerMethod(call) {
|
|
|
10885
10902
|
return `${call.method_name}()`;
|
|
10886
10903
|
}
|
|
10887
10904
|
|
|
10888
|
-
// src/
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
|
|
10899
|
-
|
|
10900
|
-
|
|
10901
|
-
|
|
10902
|
-
|
|
10905
|
+
// src/graph/code-graph.ts
|
|
10906
|
+
var CodeGraph = class {
|
|
10907
|
+
ir;
|
|
10908
|
+
constructor(ir) {
|
|
10909
|
+
this.ir = ir;
|
|
10910
|
+
}
|
|
10911
|
+
// ---------------------------------------------------------------------------
|
|
10912
|
+
// DFG indexes
|
|
10913
|
+
// ---------------------------------------------------------------------------
|
|
10914
|
+
_defById = null;
|
|
10915
|
+
get defById() {
|
|
10916
|
+
if (!this._defById) {
|
|
10917
|
+
this._defById = /* @__PURE__ */ new Map();
|
|
10918
|
+
for (const def of this.ir.dfg.defs) {
|
|
10919
|
+
this._defById.set(def.id, def);
|
|
10920
|
+
}
|
|
10921
|
+
}
|
|
10922
|
+
return this._defById;
|
|
10923
|
+
}
|
|
10924
|
+
_defsByLine = null;
|
|
10925
|
+
get defsByLine() {
|
|
10926
|
+
if (!this._defsByLine) {
|
|
10927
|
+
this._defsByLine = /* @__PURE__ */ new Map();
|
|
10928
|
+
for (const def of this.ir.dfg.defs) {
|
|
10929
|
+
const arr = this._defsByLine.get(def.line) ?? [];
|
|
10930
|
+
arr.push(def);
|
|
10931
|
+
this._defsByLine.set(def.line, arr);
|
|
10932
|
+
}
|
|
10933
|
+
}
|
|
10934
|
+
return this._defsByLine;
|
|
10935
|
+
}
|
|
10936
|
+
_defsByVar = null;
|
|
10937
|
+
get defsByVar() {
|
|
10938
|
+
if (!this._defsByVar) {
|
|
10939
|
+
this._defsByVar = /* @__PURE__ */ new Map();
|
|
10940
|
+
for (const def of this.ir.dfg.defs) {
|
|
10941
|
+
const arr = this._defsByVar.get(def.variable) ?? [];
|
|
10942
|
+
arr.push(def);
|
|
10943
|
+
this._defsByVar.set(def.variable, arr);
|
|
10944
|
+
}
|
|
10945
|
+
}
|
|
10946
|
+
return this._defsByVar;
|
|
10947
|
+
}
|
|
10948
|
+
_usesByLine = null;
|
|
10949
|
+
get usesByLine() {
|
|
10950
|
+
if (!this._usesByLine) {
|
|
10951
|
+
this._usesByLine = /* @__PURE__ */ new Map();
|
|
10952
|
+
for (const use of this.ir.dfg.uses) {
|
|
10953
|
+
const arr = this._usesByLine.get(use.line) ?? [];
|
|
10954
|
+
arr.push(use);
|
|
10955
|
+
this._usesByLine.set(use.line, arr);
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10958
|
+
return this._usesByLine;
|
|
10959
|
+
}
|
|
10960
|
+
_usesByDefId = null;
|
|
10961
|
+
get usesByDefId() {
|
|
10962
|
+
if (!this._usesByDefId) {
|
|
10963
|
+
this._usesByDefId = /* @__PURE__ */ new Map();
|
|
10964
|
+
for (const use of this.ir.dfg.uses) {
|
|
10965
|
+
if (use.def_id !== null) {
|
|
10966
|
+
const arr = this._usesByDefId.get(use.def_id) ?? [];
|
|
10967
|
+
arr.push(use);
|
|
10968
|
+
this._usesByDefId.set(use.def_id, arr);
|
|
10969
|
+
}
|
|
10970
|
+
}
|
|
10971
|
+
}
|
|
10972
|
+
return this._usesByDefId;
|
|
10973
|
+
}
|
|
10974
|
+
_chainsByFromDef = null;
|
|
10975
|
+
get chainsByFromDef() {
|
|
10976
|
+
if (!this._chainsByFromDef) {
|
|
10977
|
+
this._chainsByFromDef = /* @__PURE__ */ new Map();
|
|
10978
|
+
for (const chain of this.ir.dfg.chains ?? []) {
|
|
10979
|
+
const arr = this._chainsByFromDef.get(chain.from_def) ?? [];
|
|
10980
|
+
arr.push(chain);
|
|
10981
|
+
this._chainsByFromDef.set(chain.from_def, arr);
|
|
10982
|
+
}
|
|
10983
|
+
}
|
|
10984
|
+
return this._chainsByFromDef;
|
|
10985
|
+
}
|
|
10986
|
+
// ---------------------------------------------------------------------------
|
|
10987
|
+
// Call indexes
|
|
10988
|
+
// ---------------------------------------------------------------------------
|
|
10989
|
+
_callsByLine = null;
|
|
10990
|
+
get callsByLine() {
|
|
10991
|
+
if (!this._callsByLine) {
|
|
10992
|
+
this._callsByLine = /* @__PURE__ */ new Map();
|
|
10993
|
+
for (const call of this.ir.calls) {
|
|
10994
|
+
const arr = this._callsByLine.get(call.location.line) ?? [];
|
|
10995
|
+
arr.push(call);
|
|
10996
|
+
this._callsByLine.set(call.location.line, arr);
|
|
10997
|
+
}
|
|
10998
|
+
}
|
|
10999
|
+
return this._callsByLine;
|
|
11000
|
+
}
|
|
11001
|
+
_callsByMethod = null;
|
|
11002
|
+
get callsByMethod() {
|
|
11003
|
+
if (!this._callsByMethod) {
|
|
11004
|
+
this._callsByMethod = /* @__PURE__ */ new Map();
|
|
11005
|
+
for (const call of this.ir.calls) {
|
|
11006
|
+
const arr = this._callsByMethod.get(call.method_name) ?? [];
|
|
11007
|
+
arr.push(call);
|
|
11008
|
+
this._callsByMethod.set(call.method_name, arr);
|
|
11009
|
+
}
|
|
11010
|
+
}
|
|
11011
|
+
return this._callsByMethod;
|
|
10903
11012
|
}
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
11013
|
+
// ---------------------------------------------------------------------------
|
|
11014
|
+
// Type / method indexes
|
|
11015
|
+
// ---------------------------------------------------------------------------
|
|
11016
|
+
_methodsByName = null;
|
|
11017
|
+
get methodsByName() {
|
|
11018
|
+
if (!this._methodsByName) {
|
|
11019
|
+
this._methodsByName = /* @__PURE__ */ new Map();
|
|
11020
|
+
for (const type of this.ir.types) {
|
|
11021
|
+
for (const method of type.methods) {
|
|
11022
|
+
const arr = this._methodsByName.get(method.name) ?? [];
|
|
11023
|
+
arr.push({ type, method });
|
|
11024
|
+
this._methodsByName.set(method.name, arr);
|
|
11025
|
+
}
|
|
11026
|
+
}
|
|
11027
|
+
}
|
|
11028
|
+
return this._methodsByName;
|
|
10908
11029
|
}
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
11030
|
+
/**
|
|
11031
|
+
* Returns the TypeInfo + MethodInfo whose line range contains `line`, or null.
|
|
11032
|
+
* Used by passes that need the enclosing method context for a given line.
|
|
11033
|
+
*/
|
|
11034
|
+
methodAtLine(line) {
|
|
11035
|
+
for (const type of this.ir.types) {
|
|
11036
|
+
for (const method of type.methods) {
|
|
11037
|
+
if (line >= method.start_line && line <= method.end_line) {
|
|
11038
|
+
return { type, method };
|
|
11039
|
+
}
|
|
11040
|
+
}
|
|
11041
|
+
}
|
|
11042
|
+
return null;
|
|
11043
|
+
}
|
|
11044
|
+
// ---------------------------------------------------------------------------
|
|
11045
|
+
// Taint indexes
|
|
11046
|
+
// ---------------------------------------------------------------------------
|
|
11047
|
+
_sanitizersByLine = null;
|
|
11048
|
+
get sanitizersByLine() {
|
|
11049
|
+
if (!this._sanitizersByLine) {
|
|
11050
|
+
this._sanitizersByLine = /* @__PURE__ */ new Map();
|
|
11051
|
+
for (const san of this.ir.taint.sanitizers ?? []) {
|
|
11052
|
+
const arr = this._sanitizersByLine.get(san.line) ?? [];
|
|
11053
|
+
arr.push(san);
|
|
11054
|
+
this._sanitizersByLine.set(san.line, arr);
|
|
11055
|
+
}
|
|
11056
|
+
}
|
|
11057
|
+
return this._sanitizersByLine;
|
|
11058
|
+
}
|
|
11059
|
+
// ---------------------------------------------------------------------------
|
|
11060
|
+
// Query primitives
|
|
11061
|
+
// ---------------------------------------------------------------------------
|
|
11062
|
+
/** All DFGDefs at a given line. Returns [] if none. */
|
|
11063
|
+
defsAtLine(line) {
|
|
11064
|
+
return this.defsByLine.get(line) ?? [];
|
|
11065
|
+
}
|
|
11066
|
+
/** All DFGUses at a given line. Returns [] if none. */
|
|
11067
|
+
usesAtLine(line) {
|
|
11068
|
+
return this.usesByLine.get(line) ?? [];
|
|
11069
|
+
}
|
|
11070
|
+
/** All DFGUses that reach a specific definition ID. Returns [] if none. */
|
|
11071
|
+
usesOfDef(defId) {
|
|
11072
|
+
return this.usesByDefId.get(defId) ?? [];
|
|
10913
11073
|
}
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
11074
|
+
/** All CallInfos at a given line. Returns [] if none. */
|
|
11075
|
+
callsAtLine(line) {
|
|
11076
|
+
return this.callsByLine.get(line) ?? [];
|
|
11077
|
+
}
|
|
11078
|
+
/** DFGChains outgoing from a definition ID. Returns [] if none. */
|
|
11079
|
+
chainsFrom(defId) {
|
|
11080
|
+
return this.chainsByFromDef.get(defId) ?? [];
|
|
11081
|
+
}
|
|
11082
|
+
/**
|
|
11083
|
+
* All definitions of `variable` that appear strictly after `afterLine`
|
|
11084
|
+
* and at or before `upToLine`. Used to detect whether a variable is
|
|
11085
|
+
* redefined between a taint source and a sink.
|
|
11086
|
+
*/
|
|
11087
|
+
laterDefsOfVar(variable, afterLine, upToLine) {
|
|
11088
|
+
return (this.defsByVar.get(variable) ?? []).filter(
|
|
11089
|
+
(d) => d.line > afterLine && d.line <= upToLine
|
|
11090
|
+
);
|
|
10918
11091
|
}
|
|
10919
|
-
|
|
11092
|
+
// ---------------------------------------------------------------------------
|
|
11093
|
+
// CFG indexes
|
|
11094
|
+
// ---------------------------------------------------------------------------
|
|
11095
|
+
_blockById = null;
|
|
11096
|
+
get blockById() {
|
|
11097
|
+
if (!this._blockById) {
|
|
11098
|
+
this._blockById = /* @__PURE__ */ new Map();
|
|
11099
|
+
for (const block of this.ir.cfg.blocks) {
|
|
11100
|
+
this._blockById.set(block.id, block);
|
|
11101
|
+
}
|
|
11102
|
+
}
|
|
11103
|
+
return this._blockById;
|
|
11104
|
+
}
|
|
11105
|
+
/**
|
|
11106
|
+
* Returns the line range of each detected loop body in the file.
|
|
11107
|
+
*
|
|
11108
|
+
* A loop is identified by CFG back-edges (type = "back"). For each back edge
|
|
11109
|
+
* `A → B`, B is the loop header and A is the last block before the back-jump.
|
|
11110
|
+
* The loop body spans from `header.start_line` to `A.end_line` (inclusive).
|
|
11111
|
+
*
|
|
11112
|
+
* Returns `{ start_line, end_line }` — one entry per back-edge.
|
|
11113
|
+
* Overlapping ranges are returned separately; callers can merge as needed.
|
|
11114
|
+
*
|
|
11115
|
+
* Usage: check whether line L is inside any loop with
|
|
11116
|
+
* `graph.loopBodies().some(r => L >= r.start_line && L <= r.end_line)`
|
|
11117
|
+
*/
|
|
11118
|
+
loopBodies() {
|
|
11119
|
+
const loops = [];
|
|
11120
|
+
for (const edge of this.ir.cfg.edges) {
|
|
11121
|
+
if (edge.type !== "back") continue;
|
|
11122
|
+
const header = this.blockById.get(edge.to);
|
|
11123
|
+
const tail = this.blockById.get(edge.from);
|
|
11124
|
+
if (header && tail) {
|
|
11125
|
+
loops.push({ start_line: header.start_line, end_line: tail.end_line });
|
|
11126
|
+
}
|
|
11127
|
+
}
|
|
11128
|
+
return loops;
|
|
11129
|
+
}
|
|
11130
|
+
/**
|
|
11131
|
+
* Propagate a set of tainted definition IDs through DFGChains to a fixpoint.
|
|
11132
|
+
*
|
|
11133
|
+
* Returns a new Set (does not mutate the input). Each chain edge
|
|
11134
|
+
* `from_def → to_def` spreads taint: if `from_def` is tainted, `to_def`
|
|
11135
|
+
* becomes tainted. Iterates until no new IDs are added.
|
|
11136
|
+
*/
|
|
11137
|
+
propagateTaintedDefIds(seed) {
|
|
11138
|
+
const result = new Set(seed);
|
|
11139
|
+
let changed = true;
|
|
11140
|
+
while (changed) {
|
|
11141
|
+
changed = false;
|
|
11142
|
+
for (const [fromDef, chains] of this.chainsByFromDef) {
|
|
11143
|
+
if (!result.has(fromDef)) continue;
|
|
11144
|
+
for (const chain of chains) {
|
|
11145
|
+
if (!result.has(chain.to_def)) {
|
|
11146
|
+
result.add(chain.to_def);
|
|
11147
|
+
changed = true;
|
|
11148
|
+
}
|
|
11149
|
+
}
|
|
11150
|
+
}
|
|
11151
|
+
}
|
|
11152
|
+
return result;
|
|
11153
|
+
}
|
|
11154
|
+
};
|
|
11155
|
+
|
|
11156
|
+
// src/analysis/taint-propagation.ts
|
|
11157
|
+
function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
|
|
11158
|
+
let graph;
|
|
11159
|
+
let sources;
|
|
11160
|
+
let sinks;
|
|
11161
|
+
let sanitizers;
|
|
11162
|
+
if (graphOrDfg instanceof CodeGraph) {
|
|
11163
|
+
graph = graphOrDfg;
|
|
11164
|
+
sources = callsOrSources;
|
|
11165
|
+
sinks = sourcesOrSinks;
|
|
11166
|
+
sanitizers = sinksOrSanitizers;
|
|
11167
|
+
} else {
|
|
11168
|
+
const dfg = graphOrDfg;
|
|
11169
|
+
const calls = callsOrSources;
|
|
11170
|
+
sources = sourcesOrSinks;
|
|
11171
|
+
sinks = sinksOrSanitizers;
|
|
11172
|
+
sanitizers = sanitizersArg ?? [];
|
|
11173
|
+
graph = new CodeGraph({
|
|
11174
|
+
meta: { circle_ir: "3.0", file: "", language: "java", loc: 0, hash: "" },
|
|
11175
|
+
types: [],
|
|
11176
|
+
calls,
|
|
11177
|
+
cfg: { blocks: [], edges: [] },
|
|
11178
|
+
dfg,
|
|
11179
|
+
taint: { sources: [], sinks: [], sanitizers },
|
|
11180
|
+
imports: [],
|
|
11181
|
+
exports: [],
|
|
11182
|
+
unresolved: [],
|
|
11183
|
+
enriched: {}
|
|
11184
|
+
});
|
|
11185
|
+
}
|
|
11186
|
+
const taintedVars = [];
|
|
11187
|
+
const flows = [];
|
|
11188
|
+
const reachableSinks = /* @__PURE__ */ new Map();
|
|
11189
|
+
const defsByLine = graph.defsByLine;
|
|
11190
|
+
const usesByLine = graph.usesByLine;
|
|
11191
|
+
const callsByLine = graph.callsByLine;
|
|
11192
|
+
const sanitizersByLine = graph.sanitizersByLine;
|
|
11193
|
+
const defById = graph.defById;
|
|
11194
|
+
const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine);
|
|
10920
11195
|
const initialTaint = rawInitialTaint.filter((tv) => {
|
|
10921
11196
|
if (tv.line === tv.sourceLine) return true;
|
|
10922
11197
|
const sanCheck = checkSanitized(tv.sourceLine, tv.line, tv.sourceType, sanitizersByLine);
|
|
@@ -10925,7 +11200,7 @@ function propagateTaint(dfg, calls, sources, sinks, sanitizers) {
|
|
|
10925
11200
|
taintedVars.push(...initialTaint);
|
|
10926
11201
|
const propagatedTaint = propagateThroughChains(
|
|
10927
11202
|
initialTaint,
|
|
10928
|
-
|
|
11203
|
+
graph.chainsByFromDef,
|
|
10929
11204
|
defById,
|
|
10930
11205
|
sanitizersByLine
|
|
10931
11206
|
);
|
|
@@ -10959,9 +11234,7 @@ function propagateTaint(dfg, calls, sources, sinks, sanitizers) {
|
|
|
10959
11234
|
const flow = buildTaintFlow(
|
|
10960
11235
|
source,
|
|
10961
11236
|
sink,
|
|
10962
|
-
taintInfo
|
|
10963
|
-
dfg,
|
|
10964
|
-
defById
|
|
11237
|
+
taintInfo
|
|
10965
11238
|
);
|
|
10966
11239
|
flows.push(flow);
|
|
10967
11240
|
const existingSources = reachableSinks.get(sink) ?? [];
|
|
@@ -10981,7 +11254,7 @@ function propagateTaint(dfg, calls, sources, sinks, sanitizers) {
|
|
|
10981
11254
|
}
|
|
10982
11255
|
return { taintedVars, flows, reachableSinks };
|
|
10983
11256
|
}
|
|
10984
|
-
function findInitialTaint(sources,
|
|
11257
|
+
function findInitialTaint(sources, callsByLine, defsByLine) {
|
|
10985
11258
|
const tainted = [];
|
|
10986
11259
|
for (const source of sources) {
|
|
10987
11260
|
const defsOnLine = defsByLine.get(source.line) ?? [];
|
|
@@ -11013,19 +11286,13 @@ function findInitialTaint(sources, dfg, callsByLine, defsByLine) {
|
|
|
11013
11286
|
}
|
|
11014
11287
|
return tainted;
|
|
11015
11288
|
}
|
|
11016
|
-
function propagateThroughChains(initialTaint,
|
|
11289
|
+
function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitizersByLine) {
|
|
11017
11290
|
const propagated = [];
|
|
11018
11291
|
const taintedDefIds = new Set(initialTaint.map((t) => t.defId));
|
|
11019
11292
|
const taintInfoByDefId = /* @__PURE__ */ new Map();
|
|
11020
11293
|
for (const t of initialTaint) {
|
|
11021
11294
|
taintInfoByDefId.set(t.defId, t);
|
|
11022
11295
|
}
|
|
11023
|
-
const chainsByFromDef = /* @__PURE__ */ new Map();
|
|
11024
|
-
for (const chain of chains) {
|
|
11025
|
-
const existing = chainsByFromDef.get(chain.from_def) ?? [];
|
|
11026
|
-
existing.push(chain);
|
|
11027
|
-
chainsByFromDef.set(chain.from_def, existing);
|
|
11028
|
-
}
|
|
11029
11296
|
const queue = [...initialTaint.map((t) => t.defId)];
|
|
11030
11297
|
const visited = new Set(queue);
|
|
11031
11298
|
while (queue.length > 0) {
|
|
@@ -11095,7 +11362,7 @@ function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
|
|
|
11095
11362
|
}
|
|
11096
11363
|
return { sanitized: false };
|
|
11097
11364
|
}
|
|
11098
|
-
function buildTaintFlow(source, sink, taintInfo
|
|
11365
|
+
function buildTaintFlow(source, sink, taintInfo) {
|
|
11099
11366
|
const path = [];
|
|
11100
11367
|
path.push({
|
|
11101
11368
|
variable: taintInfo.variable,
|
|
@@ -13472,65 +13739,54 @@ function normalizeCondition(cond) {
|
|
|
13472
13739
|
|
|
13473
13740
|
// src/analysis/path-finder.ts
|
|
13474
13741
|
var PathFinder = class {
|
|
13475
|
-
|
|
13476
|
-
calls;
|
|
13742
|
+
graph;
|
|
13477
13743
|
sources;
|
|
13478
13744
|
sinks;
|
|
13479
13745
|
sanitizers;
|
|
13480
13746
|
config;
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
|
|
13487
|
-
|
|
13488
|
-
|
|
13489
|
-
|
|
13490
|
-
|
|
13491
|
-
|
|
13492
|
-
|
|
13493
|
-
|
|
13494
|
-
|
|
13495
|
-
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
|
|
13500
|
-
|
|
13501
|
-
|
|
13502
|
-
|
|
13503
|
-
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
this.
|
|
13515
|
-
|
|
13516
|
-
|
|
13517
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
|
|
13521
|
-
|
|
13522
|
-
byDefId.push(use);
|
|
13523
|
-
this.usesByDefId.set(use.def_id, byDefId);
|
|
13524
|
-
}
|
|
13525
|
-
}
|
|
13526
|
-
for (const call of this.calls) {
|
|
13527
|
-
const byLine = this.callsByLine.get(call.location.line) ?? [];
|
|
13528
|
-
byLine.push(call);
|
|
13529
|
-
this.callsByLine.set(call.location.line, byLine);
|
|
13530
|
-
}
|
|
13531
|
-
for (const sanitizer of this.sanitizers) {
|
|
13532
|
-
this.sanitizerLines.add(sanitizer.line);
|
|
13747
|
+
sanitizerLines;
|
|
13748
|
+
constructor(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersOrConfig, config = {}) {
|
|
13749
|
+
if (graphOrDfg instanceof CodeGraph) {
|
|
13750
|
+
this.graph = graphOrDfg;
|
|
13751
|
+
this.sources = callsOrSources;
|
|
13752
|
+
this.sinks = sourcesOrSinks;
|
|
13753
|
+
this.sanitizers = sinksOrSanitizers;
|
|
13754
|
+
const cfg = sanitizersOrConfig;
|
|
13755
|
+
this.config = {
|
|
13756
|
+
maxPathLength: cfg?.maxPathLength ?? 50,
|
|
13757
|
+
maxPathsPerSink: cfg?.maxPathsPerSink ?? 10,
|
|
13758
|
+
includeCode: cfg?.includeCode ?? false,
|
|
13759
|
+
sourceLines: cfg?.sourceLines ?? []
|
|
13760
|
+
};
|
|
13761
|
+
} else {
|
|
13762
|
+
const dfg = graphOrDfg;
|
|
13763
|
+
const calls = callsOrSources;
|
|
13764
|
+
const sources = sourcesOrSinks;
|
|
13765
|
+
const sinks = sinksOrSanitizers;
|
|
13766
|
+
const sanitizers = sanitizersOrConfig ?? [];
|
|
13767
|
+
this.graph = new CodeGraph({
|
|
13768
|
+
meta: { circle_ir: "3.0", file: "", language: "java", loc: 0, hash: "" },
|
|
13769
|
+
types: [],
|
|
13770
|
+
calls,
|
|
13771
|
+
cfg: { blocks: [], edges: [] },
|
|
13772
|
+
dfg,
|
|
13773
|
+
taint: { sources: [], sinks: [], sanitizers },
|
|
13774
|
+
imports: [],
|
|
13775
|
+
exports: [],
|
|
13776
|
+
unresolved: [],
|
|
13777
|
+
enriched: {}
|
|
13778
|
+
});
|
|
13779
|
+
this.sources = sources;
|
|
13780
|
+
this.sinks = sinks;
|
|
13781
|
+
this.sanitizers = sanitizers;
|
|
13782
|
+
this.config = {
|
|
13783
|
+
maxPathLength: config.maxPathLength ?? 50,
|
|
13784
|
+
maxPathsPerSink: config.maxPathsPerSink ?? 10,
|
|
13785
|
+
includeCode: config.includeCode ?? false,
|
|
13786
|
+
sourceLines: config.sourceLines ?? []
|
|
13787
|
+
};
|
|
13533
13788
|
}
|
|
13789
|
+
this.sanitizerLines = new Set(this.sanitizers.map((s) => s.line));
|
|
13534
13790
|
}
|
|
13535
13791
|
/**
|
|
13536
13792
|
* Find all taint paths from sources to sinks
|
|
@@ -13539,7 +13795,7 @@ var PathFinder = class {
|
|
|
13539
13795
|
const paths = [];
|
|
13540
13796
|
let pathId = 1;
|
|
13541
13797
|
for (const source of this.sources) {
|
|
13542
|
-
const sourceDefs = this.
|
|
13798
|
+
const sourceDefs = this.graph.defsAtLine(source.line);
|
|
13543
13799
|
for (const sourceDef of sourceDefs) {
|
|
13544
13800
|
const pathsFromSource = this.findPathsFromSource(source, sourceDef, pathId);
|
|
13545
13801
|
paths.push(...pathsFromSource);
|
|
@@ -13595,7 +13851,7 @@ var PathFinder = class {
|
|
|
13595
13851
|
description: `Flows into ${sink.type} sink`,
|
|
13596
13852
|
code: this.getCodeAtLine(sink.line)
|
|
13597
13853
|
};
|
|
13598
|
-
const call = this.
|
|
13854
|
+
const call = this.graph.callsAtLine(sink.line)[0];
|
|
13599
13855
|
paths.push({
|
|
13600
13856
|
id: `path-${startPathId + paths.length}`,
|
|
13601
13857
|
source: {
|
|
@@ -13619,7 +13875,7 @@ var PathFinder = class {
|
|
|
13619
13875
|
pathsPerSink.set(sink.line, sinkCount + 1);
|
|
13620
13876
|
}
|
|
13621
13877
|
}
|
|
13622
|
-
const uses = this.
|
|
13878
|
+
const uses = this.graph.usesOfDef(state.currentDef.id);
|
|
13623
13879
|
for (const use of uses) {
|
|
13624
13880
|
let sanitizer = state.sanitizer;
|
|
13625
13881
|
if (this.sanitizerLines.has(use.line) && !sanitizer) {
|
|
@@ -13628,7 +13884,7 @@ var PathFinder = class {
|
|
|
13628
13884
|
sanitizer = { line: san.line, method: san.method };
|
|
13629
13885
|
}
|
|
13630
13886
|
}
|
|
13631
|
-
const nextDefs = this.
|
|
13887
|
+
const nextDefs = this.graph.defsAtLine(use.line);
|
|
13632
13888
|
for (const nextDef of nextDefs) {
|
|
13633
13889
|
if (state.visited.has(nextDef.id)) continue;
|
|
13634
13890
|
const hop = this.createHop(state.currentDef, nextDef, use);
|
|
@@ -13641,7 +13897,7 @@ var PathFinder = class {
|
|
|
13641
13897
|
sanitizer
|
|
13642
13898
|
});
|
|
13643
13899
|
}
|
|
13644
|
-
const laterDefs = (this.defsByVar.get(use.variable) ?? []).filter((d) => d.line > use.line && !state.visited.has(d.id));
|
|
13900
|
+
const laterDefs = (this.graph.defsByVar.get(use.variable) ?? []).filter((d) => d.line > use.line && !state.visited.has(d.id));
|
|
13645
13901
|
for (const laterDef of laterDefs.slice(0, 3)) {
|
|
13646
13902
|
const hop = {
|
|
13647
13903
|
line: laterDef.line,
|
|
@@ -13667,14 +13923,12 @@ var PathFinder = class {
|
|
|
13667
13923
|
* Check if a definition reaches a sink
|
|
13668
13924
|
*/
|
|
13669
13925
|
reachesSink(def, sink) {
|
|
13670
|
-
const
|
|
13671
|
-
for (const use of uses) {
|
|
13926
|
+
for (const use of this.graph.usesAtLine(sink.line)) {
|
|
13672
13927
|
if (use.variable === def.variable || use.def_id === def.id) {
|
|
13673
13928
|
return true;
|
|
13674
13929
|
}
|
|
13675
13930
|
}
|
|
13676
|
-
const
|
|
13677
|
-
for (const call of calls) {
|
|
13931
|
+
for (const call of this.graph.callsAtLine(sink.line)) {
|
|
13678
13932
|
for (const arg of call.arguments) {
|
|
13679
13933
|
if (arg.variable === def.variable) {
|
|
13680
13934
|
return true;
|
|
@@ -13687,7 +13941,7 @@ var PathFinder = class {
|
|
|
13687
13941
|
* Create a hop description between two definitions
|
|
13688
13942
|
*/
|
|
13689
13943
|
createHop(fromDef, toDef, use) {
|
|
13690
|
-
const call = this.
|
|
13944
|
+
const call = this.graph.callsAtLine(toDef.line)[0];
|
|
13691
13945
|
let operation = "assign";
|
|
13692
13946
|
let description = `Assigned to ${toDef.variable}`;
|
|
13693
13947
|
if (call) {
|
|
@@ -80,6 +80,24 @@ function extractJavaScriptImports(tree) {
|
|
|
80
80
|
const importInfos = extractJSImportInfo(importStmt);
|
|
81
81
|
imports.push(...importInfos);
|
|
82
82
|
}
|
|
83
|
+
// Find re-export statements: `export { X } from './file'`
|
|
84
|
+
// These create an implicit import dependency that must be tracked by ImportGraph.
|
|
85
|
+
const exportStatements = findNodes(tree.rootNode, 'export_statement');
|
|
86
|
+
for (const exportStmt of exportStatements) {
|
|
87
|
+
const sourceNode = exportStmt.childForFieldName('source');
|
|
88
|
+
if (!sourceNode)
|
|
89
|
+
continue; // no `from '...'` clause — not a re-export
|
|
90
|
+
const fromPackage = getNodeText(sourceNode).replace(/['"]/g, '');
|
|
91
|
+
if (!fromPackage)
|
|
92
|
+
continue;
|
|
93
|
+
imports.push({
|
|
94
|
+
imported_name: '*',
|
|
95
|
+
from_package: fromPackage,
|
|
96
|
+
alias: null,
|
|
97
|
+
is_wildcard: true,
|
|
98
|
+
line_number: exportStmt.startPosition.row + 1,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
83
101
|
// Find CommonJS require calls
|
|
84
102
|
const requireCalls = findRequireCalls(tree);
|
|
85
103
|
imports.push(...requireCalls);
|