circle-ir 3.9.10 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -34
- 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 +1069 -18
- package/dist/core/circle-ir-core.cjs +8 -5
- package/dist/core/circle-ir-core.js +8 -5
- package/dist/languages/plugins/java.d.ts +9 -0
- package/dist/languages/plugins/java.js +48 -6
- package/dist/languages/plugins/java.js.map +1 -1
- package/docs/SPEC.md +13 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# circle-ir
|
|
2
2
|
|
|
3
|
-
A high-performance Static Application Security Testing (SAST) library for detecting security vulnerabilities through taint analysis, and code quality findings through an extensible
|
|
3
|
+
A high-performance Static Application Security Testing (SAST) library for detecting security vulnerabilities through taint analysis, and code quality findings through an extensible 36-pass analysis pipeline. Works in Node.js and browsers.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Taint Analysis**: Track data flow from sources (user input) to sinks (dangerous operations)
|
|
8
8
|
- **Multi-language Support**: Java, JavaScript/TypeScript, Python, Rust, Bash/Shell
|
|
9
9
|
- **High Accuracy**: 100% on OWASP Benchmark, 100% on Juliet Test Suite, 97.7% TPR on SecuriBench Micro
|
|
10
|
-
- **
|
|
10
|
+
- **36-Pass Pipeline**: 19 security taint passes + 17 reliability/performance/maintainability/architecture quality passes
|
|
11
|
+
- **Metrics Engine**: 24 software quality metrics (cyclomatic complexity, Halstead, CBO, RFC, LCOM, DIT, and 4 composite scores)
|
|
11
12
|
- **Cross-File Analysis**: `analyzeProject()` surfaces taint flows that span multiple files
|
|
12
13
|
- **Universal**: Works in Node.js and browsers with environment-agnostic core
|
|
13
14
|
- **Zero External Dependencies**: Core analysis runs without network calls or external services
|
|
@@ -40,12 +41,20 @@ for (const flow of result.taint.flows || []) {
|
|
|
40
41
|
console.log(` Sink: line ${flow.sink_line}`);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
// Quality findings from analysis passes
|
|
44
|
+
// Quality findings from all 36 analysis passes
|
|
44
45
|
for (const finding of result.findings || []) {
|
|
45
46
|
console.log(`[${finding.severity}] ${finding.rule_id} at line ${finding.line}`);
|
|
46
47
|
console.log(` ${finding.message}`);
|
|
47
48
|
if (finding.fix) console.log(` Fix: ${finding.fix}`);
|
|
48
49
|
}
|
|
50
|
+
|
|
51
|
+
// Software quality metrics
|
|
52
|
+
const m = result.metrics;
|
|
53
|
+
if (m) {
|
|
54
|
+
console.log(`Cyclomatic complexity: ${m.cyclomatic_complexity}`);
|
|
55
|
+
console.log(`Maintainability index: ${m.maintainability_index}`);
|
|
56
|
+
console.log(`CBO (coupling): ${m.CBO}`);
|
|
57
|
+
}
|
|
49
58
|
```
|
|
50
59
|
|
|
51
60
|
### Browser
|
|
@@ -101,7 +110,8 @@ result.dfg // Data flow graph
|
|
|
101
110
|
result.taint // Taint sources, sinks, flows
|
|
102
111
|
result.imports // Import statements
|
|
103
112
|
result.exports // Exported symbols
|
|
104
|
-
result.findings // SastFinding[] from all
|
|
113
|
+
result.findings // SastFinding[] from all 36 analysis passes
|
|
114
|
+
result.metrics // FileMetrics — 24 software quality metrics (always populated)
|
|
105
115
|
```
|
|
106
116
|
|
|
107
117
|
### `analyzeProject(files, options?)`
|
|
@@ -181,20 +191,29 @@ const pyResult = await analyze(pyCode, 'app.py', 'python');
|
|
|
181
191
|
const rsResult = await analyze(rsCode, 'main.rs', 'rust');
|
|
182
192
|
```
|
|
183
193
|
|
|
184
|
-
## Detected Vulnerabilities
|
|
185
|
-
|
|
186
|
-
| Type | CWE | Description |
|
|
187
|
-
|
|
188
|
-
| SQL Injection | CWE-89 | User input in SQL queries |
|
|
189
|
-
| Command Injection | CWE-78 | User input in system commands |
|
|
190
|
-
|
|
|
191
|
-
|
|
|
192
|
-
|
|
|
193
|
-
|
|
|
194
|
-
|
|
|
195
|
-
| SSRF | CWE-918 | Server-side request forgery |
|
|
196
|
-
|
|
|
197
|
-
|
|
|
194
|
+
## Detected Security Vulnerabilities
|
|
195
|
+
|
|
196
|
+
| Type | CWE | Severity | Description |
|
|
197
|
+
|------|-----|----------|-------------|
|
|
198
|
+
| SQL Injection | CWE-89 | Critical | User input in SQL queries |
|
|
199
|
+
| Command Injection | CWE-78 | Critical | User input in system commands |
|
|
200
|
+
| Deserialization | CWE-502 | Critical | Untrusted deserialization |
|
|
201
|
+
| XXE | CWE-611 | Critical | XML external entity injection |
|
|
202
|
+
| Code Injection | CWE-94 | Critical | Dynamic code execution |
|
|
203
|
+
| XSS | CWE-79 | High | User input in HTML output |
|
|
204
|
+
| Path Traversal | CWE-22 | High | User input in file paths |
|
|
205
|
+
| SSRF | CWE-918 | High | Server-side request forgery |
|
|
206
|
+
| LDAP Injection | CWE-90 | High | User input in LDAP queries |
|
|
207
|
+
| XPath Injection | CWE-643 | High | User input in XPath queries |
|
|
208
|
+
| NoSQL Injection | CWE-943 | High | User input in NoSQL queries |
|
|
209
|
+
| Open Redirect | CWE-601 | Medium | User controls redirect destination |
|
|
210
|
+
| Log Injection | CWE-117 | Medium | User input in logs |
|
|
211
|
+
| Trust Boundary | CWE-501 | Medium | Data crosses trust boundary |
|
|
212
|
+
| External Taint | CWE-668 | Medium | External input reaches sensitive sink |
|
|
213
|
+
| Weak Random | CWE-330 | Low | Weak random number generator |
|
|
214
|
+
| Weak Hash | CWE-327 | Low | Weak hashing algorithm |
|
|
215
|
+
| Weak Crypto | CWE-327 | Low | Weak cryptographic algorithm |
|
|
216
|
+
| Insecure Cookie | CWE-614 | Low | Cookie without Secure/HttpOnly flags |
|
|
198
217
|
|
|
199
218
|
## Configuration
|
|
200
219
|
|
|
@@ -212,7 +231,7 @@ sources:
|
|
|
212
231
|
|
|
213
232
|
## SAST Findings & Quality Passes
|
|
214
233
|
|
|
215
|
-
The
|
|
234
|
+
The 36-pass pipeline emits `SastFinding[]` via `result.findings`. Each finding is SARIF 2.1.0-aligned:
|
|
216
235
|
|
|
217
236
|
```typescript
|
|
218
237
|
interface SastFinding {
|
|
@@ -230,21 +249,57 @@ interface SastFinding {
|
|
|
230
249
|
}
|
|
231
250
|
```
|
|
232
251
|
|
|
233
|
-
**
|
|
234
|
-
|
|
235
|
-
|
|
|
236
|
-
|
|
237
|
-
|
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
|
|
|
241
|
-
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
**Pass categories** (see [docs/PASSES.md](docs/PASSES.md) for the full registry with all 36 rule IDs and CWEs):
|
|
253
|
+
|
|
254
|
+
| Category | Passes | Example rule_ids |
|
|
255
|
+
|----------|--------|-----------------|
|
|
256
|
+
| `security` (19) | Taint matching, propagation, inter-procedural | _(produces `taint.flows`)_ |
|
|
257
|
+
| `reliability` (16) | Resource management, control flow, exception handling | `null-deref`, `resource-leak`, `infinite-loop`, `double-close`, `use-after-close`, `missing-guard-dom`, `cleanup-verify`, `unhandled-exception`, `broad-catch`, `swallowed-exception` |
|
|
258
|
+
| `performance` (5) | Loop efficiency, async patterns | `n-plus-one`, `redundant-loop-computation`, `unbounded-collection`, `serial-await`, `react-inline-jsx` |
|
|
259
|
+
| `maintainability` (3) | Documentation, markers | `missing-public-doc`, `todo-in-prod`, `stale-doc-ref` |
|
|
260
|
+
| `architecture` (6) | Coupling, inheritance, interface contracts | `circular-dependency`, `orphan-module`, `dependency-fan-out`, `deep-inheritance`, `missing-override`, `unused-interface-method` |
|
|
261
|
+
|
|
262
|
+
## Metrics Engine
|
|
263
|
+
|
|
264
|
+
`result.metrics` is always populated with 24 software quality metrics:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
interface FileMetrics {
|
|
268
|
+
// Complexity
|
|
269
|
+
cyclomatic_complexity: number; // v(G) per method average
|
|
270
|
+
WMC: number; // Weighted methods per class
|
|
271
|
+
halstead_volume: number; // Halstead volume
|
|
272
|
+
halstead_difficulty: number;
|
|
273
|
+
halstead_effort: number;
|
|
274
|
+
halstead_bugs: number;
|
|
275
|
+
|
|
276
|
+
// Size
|
|
277
|
+
LOC: number; // Lines of code
|
|
278
|
+
NLOC: number; // Non-blank lines
|
|
279
|
+
comment_density: number; // Comment lines / total lines
|
|
280
|
+
function_count: number;
|
|
281
|
+
|
|
282
|
+
// Coupling
|
|
283
|
+
CBO: number; // Coupling between objects
|
|
284
|
+
RFC: number; // Response for a class
|
|
285
|
+
|
|
286
|
+
// Inheritance
|
|
287
|
+
DIT: number; // Depth of inheritance tree
|
|
288
|
+
NOC: number; // Number of children
|
|
289
|
+
|
|
290
|
+
// Cohesion
|
|
291
|
+
LCOM: number; // Lack of cohesion in methods
|
|
292
|
+
|
|
293
|
+
// Documentation
|
|
294
|
+
doc_coverage: number; // Fraction of public APIs documented
|
|
295
|
+
|
|
296
|
+
// Composite scores (0–100)
|
|
297
|
+
maintainability_index: number;
|
|
298
|
+
code_quality_index: number;
|
|
299
|
+
bug_hotspot_score: number;
|
|
300
|
+
refactoring_roi: number;
|
|
301
|
+
}
|
|
302
|
+
```
|
|
248
303
|
|
|
249
304
|
## Key Analysis Features
|
|
250
305
|
|
|
@@ -253,6 +308,9 @@ interface SastFinding {
|
|
|
253
308
|
- **Inter-Procedural Analysis**: Tracks taint across method boundaries
|
|
254
309
|
- **Sanitizer Recognition**: Detects PreparedStatement, ESAPI, escapeHtml, and other sanitizers
|
|
255
310
|
- **Collection Tracking**: Precise taint tracking through List/Map operations with index shifting
|
|
311
|
+
- **Dominator Tree Analysis**: Powers `missing-guard-dom` (CWE-285) and `cleanup-verify` (CWE-772) via post-dominator computation
|
|
312
|
+
- **TypeHierarchy Resolution**: `PreparedStatement.executeQuery()` matches `Statement`-level sink configs — no duplicate config entries needed
|
|
313
|
+
- **Exception Flow Graph**: Tracks try/catch structure for `unhandled-exception`, `broad-catch`, `swallowed-exception`
|
|
256
314
|
|
|
257
315
|
## Benchmark Results
|
|
258
316
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pass: cleanup-verify (#54, CWE-772)
|
|
3
|
+
*
|
|
4
|
+
* Detects resources that have a close() call but that close() does not
|
|
5
|
+
* post-dominate the acquisition point — meaning some control-flow paths
|
|
6
|
+
* skip the cleanup entirely.
|
|
7
|
+
*
|
|
8
|
+
* Detection strategy:
|
|
9
|
+
* 1. Find resource-opening calls (same set as ResourceLeakPass).
|
|
10
|
+
* 2. Locate the corresponding close() call within the enclosing method.
|
|
11
|
+
* 3. Build a post-dominator graph by reversing all CFG edges and computing
|
|
12
|
+
* a DominatorGraph from the exit block.
|
|
13
|
+
* 4. If close() block does NOT post-dominate the open block → emit finding.
|
|
14
|
+
*
|
|
15
|
+
* Languages: Java, Python, JavaScript/TypeScript.
|
|
16
|
+
* Skips: Rust (RAII guarantees cleanup), Bash.
|
|
17
|
+
*
|
|
18
|
+
* Note: complements ResourceLeakPass, which handles the no-close() case.
|
|
19
|
+
*/
|
|
20
|
+
import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
|
|
21
|
+
export interface CleanupVerifyResult {
|
|
22
|
+
findings: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class CleanupVerifyPass implements AnalysisPass<CleanupVerifyResult> {
|
|
25
|
+
readonly name = "cleanup-verify";
|
|
26
|
+
readonly category: "reliability";
|
|
27
|
+
run(ctx: PassContext): CleanupVerifyResult;
|
|
28
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pass: cleanup-verify (#54, CWE-772)
|
|
3
|
+
*
|
|
4
|
+
* Detects resources that have a close() call but that close() does not
|
|
5
|
+
* post-dominate the acquisition point — meaning some control-flow paths
|
|
6
|
+
* skip the cleanup entirely.
|
|
7
|
+
*
|
|
8
|
+
* Detection strategy:
|
|
9
|
+
* 1. Find resource-opening calls (same set as ResourceLeakPass).
|
|
10
|
+
* 2. Locate the corresponding close() call within the enclosing method.
|
|
11
|
+
* 3. Build a post-dominator graph by reversing all CFG edges and computing
|
|
12
|
+
* a DominatorGraph from the exit block.
|
|
13
|
+
* 4. If close() block does NOT post-dominate the open block → emit finding.
|
|
14
|
+
*
|
|
15
|
+
* Languages: Java, Python, JavaScript/TypeScript.
|
|
16
|
+
* Skips: Rust (RAII guarantees cleanup), Bash.
|
|
17
|
+
*
|
|
18
|
+
* Note: complements ResourceLeakPass, which handles the no-close() case.
|
|
19
|
+
*/
|
|
20
|
+
import { DominatorGraph } from '../../graph/dominator-graph.js';
|
|
21
|
+
/** Resource-opening constructors (same set as ResourceLeakPass). */
|
|
22
|
+
const RESOURCE_CTORS = new Set([
|
|
23
|
+
'FileInputStream', 'FileOutputStream', 'FileReader', 'FileWriter',
|
|
24
|
+
'BufferedReader', 'BufferedWriter', 'PrintWriter', 'InputStreamReader',
|
|
25
|
+
'OutputStreamWriter', 'RandomAccessFile', 'DataInputStream', 'DataOutputStream',
|
|
26
|
+
'ObjectInputStream', 'ObjectOutputStream', 'ZipInputStream', 'ZipOutputStream',
|
|
27
|
+
'JarInputStream', 'JarOutputStream', 'GZIPInputStream', 'GZIPOutputStream',
|
|
28
|
+
'FileChannel', 'Socket', 'ServerSocket', 'DatagramSocket',
|
|
29
|
+
]);
|
|
30
|
+
/** Factory / open methods that return closeable resources. */
|
|
31
|
+
const RESOURCE_FACTORY_METHODS = new Set([
|
|
32
|
+
'openConnection', 'openStream', 'newInputStream', 'newOutputStream',
|
|
33
|
+
'newBufferedReader', 'newBufferedWriter', 'newByteChannel',
|
|
34
|
+
'open', 'createReadStream', 'createWriteStream', 'createConnection',
|
|
35
|
+
]);
|
|
36
|
+
/** Methods that release a resource. */
|
|
37
|
+
const CLOSE_METHODS = new Set([
|
|
38
|
+
'close', 'dispose', 'shutdown', 'disconnect', 'release', 'destroy', 'free',
|
|
39
|
+
'shutdownNow', 'terminate',
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Build a post-dominator graph by reversing all CFG edges and running
|
|
43
|
+
* the dominator algorithm from the exit block.
|
|
44
|
+
* `postDom.dominates(A, B)` means "A post-dominates B in the original CFG".
|
|
45
|
+
*/
|
|
46
|
+
function buildPostDomGraph(cfg) {
|
|
47
|
+
const exitBlock = cfg.blocks.find(b => b.type === 'exit') ??
|
|
48
|
+
cfg.blocks.find(b => !cfg.edges.some(e => e.from === b.id));
|
|
49
|
+
if (!exitBlock || cfg.blocks.length === 0) {
|
|
50
|
+
return new DominatorGraph({ blocks: [], edges: [] });
|
|
51
|
+
}
|
|
52
|
+
const reversed = {
|
|
53
|
+
blocks: cfg.blocks,
|
|
54
|
+
edges: cfg.edges.map(e => ({ from: e.to, to: e.from, type: e.type })),
|
|
55
|
+
};
|
|
56
|
+
return new DominatorGraph(reversed, exitBlock.id);
|
|
57
|
+
}
|
|
58
|
+
export class CleanupVerifyPass {
|
|
59
|
+
name = 'cleanup-verify';
|
|
60
|
+
category = 'reliability';
|
|
61
|
+
run(ctx) {
|
|
62
|
+
const { graph, language } = ctx;
|
|
63
|
+
// Rust RAII guarantees cleanup; Bash has no structured resource model
|
|
64
|
+
if (language === 'rust' || language === 'bash')
|
|
65
|
+
return { findings: 0 };
|
|
66
|
+
const { cfg, calls } = graph.ir;
|
|
67
|
+
const file = graph.ir.meta.file;
|
|
68
|
+
if (cfg.blocks.length === 0)
|
|
69
|
+
return { findings: 0 };
|
|
70
|
+
const postDom = buildPostDomGraph(cfg);
|
|
71
|
+
const blockContainingLine = (line) => cfg.blocks.find(b => b.start_line <= line && line <= b.end_line) ?? null;
|
|
72
|
+
let count = 0;
|
|
73
|
+
for (const call of calls) {
|
|
74
|
+
const name = call.method_name;
|
|
75
|
+
const isConstructor = call.is_constructor === true && RESOURCE_CTORS.has(name);
|
|
76
|
+
const isFactory = !call.is_constructor && RESOURCE_FACTORY_METHODS.has(name);
|
|
77
|
+
if (!isConstructor && !isFactory)
|
|
78
|
+
continue;
|
|
79
|
+
const openLine = call.location.line;
|
|
80
|
+
// Resource must be captured in a variable to be trackable
|
|
81
|
+
const defs = graph.defsAtLine(openLine);
|
|
82
|
+
if (defs.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
const resourceVar = defs[0].variable;
|
|
85
|
+
const methodInfo = graph.methodAtLine(openLine);
|
|
86
|
+
if (!methodInfo)
|
|
87
|
+
continue;
|
|
88
|
+
const methodEnd = methodInfo.method.end_line;
|
|
89
|
+
// Find the first close() call for this resource within the enclosing method
|
|
90
|
+
const closeCall = calls.find(c => CLOSE_METHODS.has(c.method_name) &&
|
|
91
|
+
c.receiver === resourceVar &&
|
|
92
|
+
c.location.line > openLine &&
|
|
93
|
+
c.location.line <= methodEnd);
|
|
94
|
+
// ResourceLeakPass handles the no-close() case; we only care about
|
|
95
|
+
// close() calls that may be skipped on some paths
|
|
96
|
+
if (!closeCall)
|
|
97
|
+
continue;
|
|
98
|
+
const openBlock = blockContainingLine(openLine);
|
|
99
|
+
const closeBlock = blockContainingLine(closeCall.location.line);
|
|
100
|
+
if (!openBlock || !closeBlock)
|
|
101
|
+
continue;
|
|
102
|
+
// If close post-dominates open, cleanup is guaranteed on every exit path
|
|
103
|
+
if (postDom.dominates(closeBlock.id, openBlock.id))
|
|
104
|
+
continue;
|
|
105
|
+
count++;
|
|
106
|
+
ctx.addFinding({
|
|
107
|
+
id: `cleanup-verify-${file}-${openLine}`,
|
|
108
|
+
pass: this.name,
|
|
109
|
+
category: this.category,
|
|
110
|
+
rule_id: 'cleanup-verify',
|
|
111
|
+
cwe: 'CWE-772',
|
|
112
|
+
severity: 'medium',
|
|
113
|
+
level: 'warning',
|
|
114
|
+
message: `Resource \`${resourceVar}\` opened at line ${openLine} may not close on all ` +
|
|
115
|
+
`paths — close() at line ${closeCall.location.line} does not post-dominate ` +
|
|
116
|
+
`the acquisition`,
|
|
117
|
+
file,
|
|
118
|
+
line: openLine,
|
|
119
|
+
fix: 'Use try-with-resources (Java) or a finally block to guarantee cleanup on all paths',
|
|
120
|
+
evidence: {
|
|
121
|
+
resource: name,
|
|
122
|
+
variable: resourceVar,
|
|
123
|
+
close_line: closeCall.location.line,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return { findings: count };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=cleanup-verify-pass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup-verify-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/cleanup-verify-pass.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pass: missing-guard-dom (#53, CWE-285)
|
|
3
|
+
*
|
|
4
|
+
* Detects sensitive operations that are not dominated by an authentication
|
|
5
|
+
* or authorization check on all control-flow paths within the same method.
|
|
6
|
+
*
|
|
7
|
+
* Detection strategy:
|
|
8
|
+
* 1. Identify calls to known authentication methods and sensitive operations.
|
|
9
|
+
* 2. Build a DominatorGraph from the file-level CFG.
|
|
10
|
+
* 3. For each sensitive operation, find the CFG block containing it and check
|
|
11
|
+
* whether any auth-check block in the same method dominates that block.
|
|
12
|
+
* 4. If no auth-check block dominates the sensitive-op block → emit finding.
|
|
13
|
+
*
|
|
14
|
+
* Language: Java only (other languages handled differently or not yet).
|
|
15
|
+
* Dedup: at most one finding per method.
|
|
16
|
+
*/
|
|
17
|
+
import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
|
|
18
|
+
export interface MissingGuardDomResult {
|
|
19
|
+
findings: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class MissingGuardDomPass implements AnalysisPass<MissingGuardDomResult> {
|
|
22
|
+
readonly name = "missing-guard-dom";
|
|
23
|
+
readonly category: "security";
|
|
24
|
+
run(ctx: PassContext): MissingGuardDomResult;
|
|
25
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pass: missing-guard-dom (#53, CWE-285)
|
|
3
|
+
*
|
|
4
|
+
* Detects sensitive operations that are not dominated by an authentication
|
|
5
|
+
* or authorization check on all control-flow paths within the same method.
|
|
6
|
+
*
|
|
7
|
+
* Detection strategy:
|
|
8
|
+
* 1. Identify calls to known authentication methods and sensitive operations.
|
|
9
|
+
* 2. Build a DominatorGraph from the file-level CFG.
|
|
10
|
+
* 3. For each sensitive operation, find the CFG block containing it and check
|
|
11
|
+
* whether any auth-check block in the same method dominates that block.
|
|
12
|
+
* 4. If no auth-check block dominates the sensitive-op block → emit finding.
|
|
13
|
+
*
|
|
14
|
+
* Language: Java only (other languages handled differently or not yet).
|
|
15
|
+
* Dedup: at most one finding per method.
|
|
16
|
+
*/
|
|
17
|
+
import { DominatorGraph } from '../../graph/dominator-graph.js';
|
|
18
|
+
const AUTH_METHODS = new Set([
|
|
19
|
+
'authenticate', 'isAuthenticated', 'isAuthorized', 'isAdmin',
|
|
20
|
+
'checkAuth', 'hasPermission', 'requiresAuth', 'verifyToken',
|
|
21
|
+
'validateToken', 'checkRole', 'authorize', 'isLoggedIn',
|
|
22
|
+
]);
|
|
23
|
+
const SENSITIVE_METHODS = new Set([
|
|
24
|
+
'delete', 'deleteById', 'drop', 'truncate', 'executeUpdate',
|
|
25
|
+
'createUser', 'createAdmin', 'modifyPermission', 'grantRole',
|
|
26
|
+
'setAdmin', 'elevatePrivilege',
|
|
27
|
+
]);
|
|
28
|
+
export class MissingGuardDomPass {
|
|
29
|
+
name = 'missing-guard-dom';
|
|
30
|
+
category = 'security';
|
|
31
|
+
run(ctx) {
|
|
32
|
+
const { graph, language } = ctx;
|
|
33
|
+
if (language !== 'java')
|
|
34
|
+
return { findings: 0 };
|
|
35
|
+
const { cfg, calls } = graph.ir;
|
|
36
|
+
if (cfg.blocks.length === 0 || cfg.edges.length === 0)
|
|
37
|
+
return { findings: 0 };
|
|
38
|
+
const dom = new DominatorGraph(cfg);
|
|
39
|
+
const file = graph.ir.meta.file;
|
|
40
|
+
// Collect auth-check and sensitive-op call lines from the IR
|
|
41
|
+
const authCallLines = [];
|
|
42
|
+
const sensitiveOps = [];
|
|
43
|
+
for (const call of calls) {
|
|
44
|
+
if (AUTH_METHODS.has(call.method_name)) {
|
|
45
|
+
authCallLines.push(call.location.line);
|
|
46
|
+
}
|
|
47
|
+
if (SENSITIVE_METHODS.has(call.method_name)) {
|
|
48
|
+
sensitiveOps.push({ line: call.location.line, method: call.method_name });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (sensitiveOps.length === 0)
|
|
52
|
+
return { findings: 0 };
|
|
53
|
+
// Helper: find the CFG block whose [start_line, end_line] contains a given line
|
|
54
|
+
const blockContainingLine = (line) => cfg.blocks.find(b => b.start_line <= line && line <= b.end_line) ?? null;
|
|
55
|
+
// Emit at most one finding per method to avoid noise
|
|
56
|
+
const reportedMethods = new Set();
|
|
57
|
+
let count = 0;
|
|
58
|
+
for (const op of sensitiveOps) {
|
|
59
|
+
const opBlock = blockContainingLine(op.line);
|
|
60
|
+
if (!opBlock)
|
|
61
|
+
continue;
|
|
62
|
+
const methodInfo = graph.methodAtLine(op.line);
|
|
63
|
+
if (!methodInfo)
|
|
64
|
+
continue;
|
|
65
|
+
const methodKey = `${methodInfo.type.name}::${methodInfo.method.name}`;
|
|
66
|
+
if (reportedMethods.has(methodKey))
|
|
67
|
+
continue;
|
|
68
|
+
const { start_line, end_line } = methodInfo.method;
|
|
69
|
+
// Restrict auth checks to those inside the same method
|
|
70
|
+
const authInMethod = authCallLines.filter(l => l >= start_line && l <= end_line);
|
|
71
|
+
// Check whether any auth-check block dominates the sensitive-op block
|
|
72
|
+
const dominated = authInMethod.some(authLine => {
|
|
73
|
+
const authBlock = blockContainingLine(authLine);
|
|
74
|
+
return authBlock !== null && dom.dominates(authBlock.id, opBlock.id);
|
|
75
|
+
});
|
|
76
|
+
if (!dominated) {
|
|
77
|
+
reportedMethods.add(methodKey);
|
|
78
|
+
count++;
|
|
79
|
+
ctx.addFinding({
|
|
80
|
+
id: `missing-guard-dom-${file}-${op.line}`,
|
|
81
|
+
pass: this.name,
|
|
82
|
+
category: this.category,
|
|
83
|
+
rule_id: 'missing-guard-dom',
|
|
84
|
+
cwe: 'CWE-285',
|
|
85
|
+
severity: 'high',
|
|
86
|
+
level: 'error',
|
|
87
|
+
message: `Sensitive operation \`${op.method}()\` at line ${op.line} is not dominated ` +
|
|
88
|
+
`by an authentication check`,
|
|
89
|
+
file,
|
|
90
|
+
line: op.line,
|
|
91
|
+
fix: `Add authentication/authorization check on all paths leading to line ${op.line}`,
|
|
92
|
+
evidence: { method: op.method },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { findings: count };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=missing-guard-dom-pass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-guard-dom-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/missing-guard-dom-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAEhE,MAAM,YAAY,GAAwB,IAAI,GAAG,CAAC;IAChD,cAAc,EAAE,iBAAiB,EAAE,cAAc,EAAE,SAAS;IAC5D,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa;IAC3D,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY;CACxD,CAAC,CAAC;AAEH,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC;IACrD,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe;IAC3D,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,WAAW;IAC5D,UAAU,EAAE,kBAAkB;CAC/B,CAAC,CAAC;AAMH,MAAM,OAAO,mBAAmB;IACrB,IAAI,GAAG,mBAAmB,CAAC;IAC3B,QAAQ,GAAG,UAAmB,CAAC;IAExC,GAAG,CAAC,GAAgB;QAClB,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC;QAEhC,IAAI,QAAQ,KAAK,MAAM;YAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QAEhD,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC;QAChC,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QAE9E,MAAM,GAAG,GAAG,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;QAEhC,6DAA6D;QAC7D,MAAM,aAAa,GAAa,EAAE,CAAC;QACnC,MAAM,YAAY,GAA4C,EAAE,CAAC;QAEjE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;gBACvC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YACzC,CAAC;YACD,IAAI,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;gBAC5C,YAAY,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;YAC5E,CAAC;QACH,CAAC;QAED,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QAEtD,gFAAgF;QAChF,MAAM,mBAAmB,GAAG,CAAC,IAAY,EAAE,EAAE,CAC3C,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC;QAE3E,qDAAqD;QACrD,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;QAC1C,IAAI,KAAK,GAAG,CAAC,CAAC;QAEd,KAAK,MAAM,EAAE,IAAI,YAAY,EAAE,CAAC;YAC9B,MAAM,OAAO,GAAG,mBAAmB,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YAC7C,IAAI,CAAC,OAAO;gBAAE,SAAS;YAEvB,MAAM,UAAU,GAAG,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YAC/C,IAAI,CAAC,UAAU;gBAAE,SAAS;YAE1B,MAAM,SAAS,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACvE,IAAI,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC;gBAAE,SAAS;YAE7C,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC;YAEnD,uDAAuD;YACvD,MAAM,YAAY,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,UAAU,IAAI,CAAC,IAAI,QAAQ,CAAC,CAAC;YAEjF,sEAAsE;YACtE,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAC7C,MAAM,SAAS,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;gBAChD,OAAO,SAAS,KAAK,IAAI,IAAI,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;YACvE,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC/B,KAAK,EAAE,CAAC;gBACR,GAAG,CAAC,UAAU,CAAC;oBACb,EAAE,EAAE,qBAAqB,IAAI,IAAI,EAAE,CAAC,IAAI,EAAE;oBAC1C,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,OAAO,EAAE,mBAAmB;oBAC5B,GAAG,EAAE,SAAS;oBACd,QAAQ,EAAE,MAAM;oBAChB,KAAK,EAAE,OAAO;oBACd,OAAO,EACL,yBAAyB,EAAE,CAAC,MAAM,gBAAgB,EAAE,CAAC,IAAI,oBAAoB;wBAC7E,4BAA4B;oBAC9B,IAAI;oBACJ,IAAI,EAAE,EAAE,CAAC,IAAI;oBACb,GAAG,EAAE,uEAAuE,EAAE,CAAC,IAAI,EAAE;oBACrF,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE;iBAChC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC7B,CAAC;CACF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pass: missing-override (#64)
|
|
3
|
+
*
|
|
4
|
+
* Detects Java methods that override a parent class method but lack the
|
|
5
|
+
* @Override annotation. Without @Override the compiler cannot catch signature
|
|
6
|
+
* mismatches introduced by a parent-class refactoring.
|
|
7
|
+
*
|
|
8
|
+
* Detection strategy:
|
|
9
|
+
* 1. Build a map of class → method names from all types in the IR.
|
|
10
|
+
* 2. Build a parent map: class name → direct parent class name (strip generics).
|
|
11
|
+
* 3. For each class that has a parent in the same file, walk the inheritance
|
|
12
|
+
* chain (max 10 hops, cycle guard) to collect all ancestor method names.
|
|
13
|
+
* 4. For each non-constructor, non-private, non-static, non-abstract method
|
|
14
|
+
* whose name appears in the ancestor set — if @Override is absent → finding.
|
|
15
|
+
*
|
|
16
|
+
* Language: Java only.
|
|
17
|
+
* Dedup: at most one finding per class:method pair.
|
|
18
|
+
*/
|
|
19
|
+
import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
|
|
20
|
+
export interface MissingOverrideResult {
|
|
21
|
+
findings: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class MissingOverridePass implements AnalysisPass<MissingOverrideResult> {
|
|
24
|
+
readonly name = "missing-override";
|
|
25
|
+
readonly category: "maintainability";
|
|
26
|
+
run(ctx: PassContext): MissingOverrideResult;
|
|
27
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pass: missing-override (#64)
|
|
3
|
+
*
|
|
4
|
+
* Detects Java methods that override a parent class method but lack the
|
|
5
|
+
* @Override annotation. Without @Override the compiler cannot catch signature
|
|
6
|
+
* mismatches introduced by a parent-class refactoring.
|
|
7
|
+
*
|
|
8
|
+
* Detection strategy:
|
|
9
|
+
* 1. Build a map of class → method names from all types in the IR.
|
|
10
|
+
* 2. Build a parent map: class name → direct parent class name (strip generics).
|
|
11
|
+
* 3. For each class that has a parent in the same file, walk the inheritance
|
|
12
|
+
* chain (max 10 hops, cycle guard) to collect all ancestor method names.
|
|
13
|
+
* 4. For each non-constructor, non-private, non-static, non-abstract method
|
|
14
|
+
* whose name appears in the ancestor set — if @Override is absent → finding.
|
|
15
|
+
*
|
|
16
|
+
* Language: Java only.
|
|
17
|
+
* Dedup: at most one finding per class:method pair.
|
|
18
|
+
*/
|
|
19
|
+
export class MissingOverridePass {
|
|
20
|
+
name = 'missing-override';
|
|
21
|
+
category = 'maintainability';
|
|
22
|
+
run(ctx) {
|
|
23
|
+
const { graph, language } = ctx;
|
|
24
|
+
if (language !== 'java')
|
|
25
|
+
return { findings: 0 };
|
|
26
|
+
const { types } = graph.ir;
|
|
27
|
+
const file = graph.ir.meta.file;
|
|
28
|
+
if (types.length === 0)
|
|
29
|
+
return { findings: 0 };
|
|
30
|
+
// Build map: class name → Set<method name>
|
|
31
|
+
const methodsByClass = new Map();
|
|
32
|
+
for (const type of types) {
|
|
33
|
+
methodsByClass.set(type.name, new Set(type.methods.map(m => m.name)));
|
|
34
|
+
}
|
|
35
|
+
// Build parent map: class name → direct parent class name (generics stripped)
|
|
36
|
+
const parentMap = new Map();
|
|
37
|
+
for (const type of types) {
|
|
38
|
+
if (type.extends) {
|
|
39
|
+
const parent = type.extends.replace(/<[^>]*>/g, '').trim();
|
|
40
|
+
parentMap.set(type.name, parent);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (parentMap.size === 0)
|
|
44
|
+
return { findings: 0 };
|
|
45
|
+
// Walk inheritance chain to collect all ancestor method names
|
|
46
|
+
const getAncestorMethods = (className) => {
|
|
47
|
+
const methods = new Set();
|
|
48
|
+
const visited = new Set();
|
|
49
|
+
let current = parentMap.get(className);
|
|
50
|
+
let hops = 0;
|
|
51
|
+
while (current && !visited.has(current) && hops < 10) {
|
|
52
|
+
visited.add(current);
|
|
53
|
+
const parentMethods = methodsByClass.get(current);
|
|
54
|
+
if (parentMethods) {
|
|
55
|
+
for (const m of parentMethods)
|
|
56
|
+
methods.add(m);
|
|
57
|
+
}
|
|
58
|
+
current = parentMap.get(current);
|
|
59
|
+
hops++;
|
|
60
|
+
}
|
|
61
|
+
return methods;
|
|
62
|
+
};
|
|
63
|
+
const dedup = new Set();
|
|
64
|
+
let count = 0;
|
|
65
|
+
for (const type of types) {
|
|
66
|
+
if (!parentMap.has(type.name))
|
|
67
|
+
continue;
|
|
68
|
+
const ancestorMethods = getAncestorMethods(type.name);
|
|
69
|
+
if (ancestorMethods.size === 0)
|
|
70
|
+
continue;
|
|
71
|
+
for (const method of type.methods) {
|
|
72
|
+
// Skip constructors (same name as class)
|
|
73
|
+
if (method.name === type.name)
|
|
74
|
+
continue;
|
|
75
|
+
// Skip private / static / abstract methods
|
|
76
|
+
if (method.modifiers.includes('private'))
|
|
77
|
+
continue;
|
|
78
|
+
if (method.modifiers.includes('static'))
|
|
79
|
+
continue;
|
|
80
|
+
if (method.modifiers.includes('abstract'))
|
|
81
|
+
continue;
|
|
82
|
+
if (!ancestorMethods.has(method.name))
|
|
83
|
+
continue;
|
|
84
|
+
if (method.annotations.includes('Override'))
|
|
85
|
+
continue;
|
|
86
|
+
const key = `${type.name}:${method.name}`;
|
|
87
|
+
if (dedup.has(key))
|
|
88
|
+
continue;
|
|
89
|
+
dedup.add(key);
|
|
90
|
+
count++;
|
|
91
|
+
ctx.addFinding({
|
|
92
|
+
id: `missing-override-${file}-${method.start_line}`,
|
|
93
|
+
pass: this.name,
|
|
94
|
+
category: this.category,
|
|
95
|
+
rule_id: 'missing-override',
|
|
96
|
+
severity: 'low',
|
|
97
|
+
level: 'warning',
|
|
98
|
+
message: `Method \`${method.name}()\` in \`${type.name}\` overrides a parent method ` +
|
|
99
|
+
`but lacks @Override`,
|
|
100
|
+
file,
|
|
101
|
+
line: method.start_line,
|
|
102
|
+
fix: 'Add @Override to make the intent explicit and catch signature mismatches at compile time',
|
|
103
|
+
evidence: { className: type.name, methodName: method.name },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { findings: count };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=missing-override-pass.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"missing-override-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/missing-override-pass.ts"],"names":[],"mappings":"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"}
|