circle-ir 3.1.0 → 3.1.1

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 CHANGED
@@ -194,6 +194,9 @@ sources:
194
194
 
195
195
  - [Circle-IR Specification](docs/SPEC.md) - IR format specification
196
196
  - [Architecture Guide](docs/ARCHITECTURE.md) - Detailed system architecture
197
+ - [Contributing Guide](CONTRIBUTING.md) - How to contribute
198
+ - [Changelog](CHANGELOG.md) - Version history
199
+ - [TODO](TODO.md) - Pending improvements and roadmap
197
200
 
198
201
  ## License
199
202
 
@@ -0,0 +1,131 @@
1
+ {
2
+ "sinks": [
3
+ {
4
+ "property": "innerHTML",
5
+ "type": "xss",
6
+ "cwe": "CWE-79",
7
+ "severity": "critical",
8
+ "note": "DOM XSS - innerHTML assignment with user input"
9
+ },
10
+ {
11
+ "property": "outerHTML",
12
+ "type": "xss",
13
+ "cwe": "CWE-79",
14
+ "severity": "critical",
15
+ "note": "DOM XSS - outerHTML assignment with user input"
16
+ },
17
+ {
18
+ "method": "write",
19
+ "class": "document",
20
+ "type": "xss",
21
+ "cwe": "CWE-79",
22
+ "severity": "critical",
23
+ "arg_positions": [0],
24
+ "note": "DOM XSS - document.write with user input"
25
+ },
26
+ {
27
+ "method": "writeln",
28
+ "class": "document",
29
+ "type": "xss",
30
+ "cwe": "CWE-79",
31
+ "severity": "critical",
32
+ "arg_positions": [0],
33
+ "note": "DOM XSS - document.writeln with user input"
34
+ },
35
+ {
36
+ "method": "insertAdjacentHTML",
37
+ "type": "xss",
38
+ "cwe": "CWE-79",
39
+ "severity": "critical",
40
+ "arg_positions": [1],
41
+ "note": "DOM XSS - insertAdjacentHTML with user input"
42
+ },
43
+ {
44
+ "property": "src",
45
+ "element": "script",
46
+ "type": "xss",
47
+ "cwe": "CWE-79",
48
+ "severity": "critical",
49
+ "note": "DOM XSS - script src manipulation"
50
+ },
51
+ {
52
+ "property": "href",
53
+ "element": "a",
54
+ "type": "xss",
55
+ "cwe": "CWE-79",
56
+ "severity": "high",
57
+ "note": "DOM XSS - anchor href with javascript: protocol"
58
+ },
59
+ {
60
+ "property": "action",
61
+ "element": "form",
62
+ "type": "xss",
63
+ "cwe": "CWE-79",
64
+ "severity": "high",
65
+ "note": "DOM XSS - form action manipulation"
66
+ },
67
+ {
68
+ "method": "setAttribute",
69
+ "type": "xss",
70
+ "cwe": "CWE-79",
71
+ "severity": "high",
72
+ "arg_positions": [1],
73
+ "note": "DOM XSS - setAttribute with event handler or dangerous attributes"
74
+ },
75
+ {
76
+ "method": "setAttributeNS",
77
+ "type": "xss",
78
+ "cwe": "CWE-79",
79
+ "severity": "high",
80
+ "arg_positions": [2],
81
+ "note": "DOM XSS - setAttributeNS"
82
+ },
83
+ {
84
+ "method": "createContextualFragment",
85
+ "class": "Range",
86
+ "type": "xss",
87
+ "cwe": "CWE-79",
88
+ "severity": "critical",
89
+ "arg_positions": [0],
90
+ "note": "DOM XSS - Range.createContextualFragment"
91
+ },
92
+ {
93
+ "method": "parseFromString",
94
+ "class": "DOMParser",
95
+ "type": "xss",
96
+ "cwe": "CWE-79",
97
+ "severity": "high",
98
+ "arg_positions": [0],
99
+ "note": "DOM parsing - may lead to XSS if content is rendered"
100
+ }
101
+ ],
102
+ "sanitizers": [
103
+ {
104
+ "method": "sanitize",
105
+ "class": "DOMPurify",
106
+ "removes": ["xss"],
107
+ "note": "DOMPurify sanitization"
108
+ },
109
+ {
110
+ "method": "createTextNode",
111
+ "class": "document",
112
+ "removes": ["xss"],
113
+ "note": "Text node creation - safe from XSS"
114
+ },
115
+ {
116
+ "property": "textContent",
117
+ "removes": ["xss"],
118
+ "note": "textContent assignment - safe from XSS"
119
+ },
120
+ {
121
+ "property": "innerText",
122
+ "removes": ["xss"],
123
+ "note": "innerText assignment - safe from XSS"
124
+ },
125
+ {
126
+ "method": "encodeURIComponent",
127
+ "removes": ["xss"],
128
+ "note": "URL encoding - safe for URL parameters"
129
+ }
130
+ ]
131
+ }
@@ -0,0 +1,296 @@
1
+ {
2
+ "sources": [
3
+ {
4
+ "property": "query",
5
+ "object": "req",
6
+ "type": "http_param",
7
+ "severity": "high",
8
+ "return_tainted": true,
9
+ "note": "Express.js query parameters (req.query.x)"
10
+ },
11
+ {
12
+ "property": "params",
13
+ "object": "req",
14
+ "type": "http_path",
15
+ "severity": "high",
16
+ "return_tainted": true,
17
+ "note": "Express.js route parameters (req.params.x)"
18
+ },
19
+ {
20
+ "property": "body",
21
+ "object": "req",
22
+ "type": "http_body",
23
+ "severity": "high",
24
+ "return_tainted": true,
25
+ "note": "Express.js request body (req.body.x)"
26
+ },
27
+ {
28
+ "property": "headers",
29
+ "object": "req",
30
+ "type": "http_header",
31
+ "severity": "high",
32
+ "return_tainted": true,
33
+ "note": "Express.js request headers (req.headers.x)"
34
+ },
35
+ {
36
+ "property": "cookies",
37
+ "object": "req",
38
+ "type": "http_cookie",
39
+ "severity": "high",
40
+ "return_tainted": true,
41
+ "note": "Express.js cookies (req.cookies.x)"
42
+ },
43
+ {
44
+ "method": "param",
45
+ "object": "req",
46
+ "type": "http_param",
47
+ "severity": "high",
48
+ "return_tainted": true,
49
+ "note": "Express.js req.param() method"
50
+ },
51
+ {
52
+ "method": "get",
53
+ "object": "req",
54
+ "type": "http_header",
55
+ "severity": "high",
56
+ "return_tainted": true,
57
+ "note": "Express.js req.get() for headers"
58
+ },
59
+ {
60
+ "method": "header",
61
+ "object": "req",
62
+ "type": "http_header",
63
+ "severity": "high",
64
+ "return_tainted": true,
65
+ "note": "Express.js req.header() for headers"
66
+ },
67
+ {
68
+ "property": "url",
69
+ "object": "req",
70
+ "type": "http_path",
71
+ "severity": "high",
72
+ "return_tainted": true,
73
+ "note": "Express.js request URL"
74
+ },
75
+ {
76
+ "property": "path",
77
+ "object": "req",
78
+ "type": "http_path",
79
+ "severity": "medium",
80
+ "return_tainted": true,
81
+ "note": "Express.js request path"
82
+ },
83
+ {
84
+ "property": "originalUrl",
85
+ "object": "req",
86
+ "type": "http_path",
87
+ "severity": "high",
88
+ "return_tainted": true,
89
+ "note": "Express.js original URL"
90
+ },
91
+ {
92
+ "property": "baseUrl",
93
+ "object": "req",
94
+ "type": "http_path",
95
+ "severity": "medium",
96
+ "return_tainted": true,
97
+ "note": "Express.js base URL"
98
+ },
99
+ {
100
+ "property": "hostname",
101
+ "object": "req",
102
+ "type": "http_header",
103
+ "severity": "medium",
104
+ "return_tainted": true,
105
+ "note": "Express.js hostname from Host header"
106
+ },
107
+ {
108
+ "property": "ip",
109
+ "object": "req",
110
+ "type": "http_header",
111
+ "severity": "low",
112
+ "return_tainted": true,
113
+ "note": "Express.js client IP (can be spoofed via X-Forwarded-For)"
114
+ },
115
+ {
116
+ "property": "protocol",
117
+ "object": "req",
118
+ "type": "http_header",
119
+ "severity": "low",
120
+ "return_tainted": true,
121
+ "note": "Express.js protocol"
122
+ },
123
+ {
124
+ "property": "searchParams",
125
+ "object": "URL",
126
+ "type": "http_param",
127
+ "severity": "high",
128
+ "return_tainted": true,
129
+ "note": "URL searchParams"
130
+ },
131
+ {
132
+ "method": "searchParams.get",
133
+ "type": "http_param",
134
+ "severity": "high",
135
+ "return_tainted": true,
136
+ "note": "URL searchParams.get()"
137
+ },
138
+ {
139
+ "property": "search",
140
+ "object": "location",
141
+ "type": "http_param",
142
+ "severity": "high",
143
+ "return_tainted": true,
144
+ "note": "Browser location.search"
145
+ },
146
+ {
147
+ "property": "hash",
148
+ "object": "location",
149
+ "type": "http_param",
150
+ "severity": "high",
151
+ "return_tainted": true,
152
+ "note": "Browser location.hash"
153
+ },
154
+ {
155
+ "property": "href",
156
+ "object": "location",
157
+ "type": "http_path",
158
+ "severity": "high",
159
+ "return_tainted": true,
160
+ "note": "Browser location.href"
161
+ },
162
+ {
163
+ "property": "pathname",
164
+ "object": "location",
165
+ "type": "http_path",
166
+ "severity": "medium",
167
+ "return_tainted": true,
168
+ "note": "Browser location.pathname"
169
+ },
170
+ {
171
+ "method": "getElementById",
172
+ "object": "document",
173
+ "type": "dom_input",
174
+ "severity": "high",
175
+ "return_tainted": true,
176
+ "note": "DOM element (may contain user input)"
177
+ },
178
+ {
179
+ "method": "querySelector",
180
+ "object": "document",
181
+ "type": "dom_input",
182
+ "severity": "high",
183
+ "return_tainted": true,
184
+ "note": "DOM element (may contain user input)"
185
+ },
186
+ {
187
+ "method": "querySelectorAll",
188
+ "object": "document",
189
+ "type": "dom_input",
190
+ "severity": "high",
191
+ "return_tainted": true,
192
+ "note": "DOM elements (may contain user input)"
193
+ },
194
+ {
195
+ "property": "value",
196
+ "type": "dom_input",
197
+ "severity": "high",
198
+ "return_tainted": true,
199
+ "note": "Form input value"
200
+ },
201
+ {
202
+ "property": "innerHTML",
203
+ "type": "dom_input",
204
+ "severity": "high",
205
+ "return_tainted": true,
206
+ "note": "DOM innerHTML (can be tainted)"
207
+ },
208
+ {
209
+ "property": "innerText",
210
+ "type": "dom_input",
211
+ "severity": "medium",
212
+ "return_tainted": true,
213
+ "note": "DOM innerText (can be tainted)"
214
+ },
215
+ {
216
+ "property": "textContent",
217
+ "type": "dom_input",
218
+ "severity": "medium",
219
+ "return_tainted": true,
220
+ "note": "DOM textContent (can be tainted)"
221
+ },
222
+ {
223
+ "method": "prompt",
224
+ "type": "user_input",
225
+ "severity": "high",
226
+ "return_tainted": true,
227
+ "note": "Browser prompt() dialog - user input"
228
+ },
229
+ {
230
+ "property": "request.query",
231
+ "type": "http_param",
232
+ "severity": "high",
233
+ "return_tainted": true,
234
+ "note": "Koa/Hapi request query"
235
+ },
236
+ {
237
+ "property": "request.params",
238
+ "type": "http_path",
239
+ "severity": "high",
240
+ "return_tainted": true,
241
+ "note": "Koa/Hapi request params"
242
+ },
243
+ {
244
+ "property": "request.body",
245
+ "type": "http_body",
246
+ "severity": "high",
247
+ "return_tainted": true,
248
+ "note": "Koa/Hapi request body"
249
+ },
250
+ {
251
+ "property": "ctx.query",
252
+ "type": "http_param",
253
+ "severity": "high",
254
+ "return_tainted": true,
255
+ "note": "Koa context query"
256
+ },
257
+ {
258
+ "property": "ctx.params",
259
+ "type": "http_path",
260
+ "severity": "high",
261
+ "return_tainted": true,
262
+ "note": "Koa context params"
263
+ },
264
+ {
265
+ "property": "ctx.request.body",
266
+ "type": "http_body",
267
+ "severity": "high",
268
+ "return_tainted": true,
269
+ "note": "Koa context request body"
270
+ },
271
+ {
272
+ "property": "input",
273
+ "object": "event",
274
+ "type": "user_input",
275
+ "severity": "high",
276
+ "return_tainted": true,
277
+ "note": "Event input (keyboard, etc.)"
278
+ },
279
+ {
280
+ "property": "data",
281
+ "object": "event",
282
+ "type": "user_input",
283
+ "severity": "high",
284
+ "return_tainted": true,
285
+ "note": "Event data (paste, drag, etc.)"
286
+ },
287
+ {
288
+ "property": "target.value",
289
+ "object": "event",
290
+ "type": "user_input",
291
+ "severity": "high",
292
+ "return_tainted": true,
293
+ "note": "Event target value"
294
+ }
295
+ ]
296
+ }
@@ -199,6 +199,48 @@
199
199
  "severity": "high",
200
200
  "return_tainted": true,
201
201
  "note": "socket.recvfrom() - network data with address"
202
+ },
203
+ {
204
+ "annotation": "Query",
205
+ "type": "http_param",
206
+ "severity": "high",
207
+ "note": "FastAPI Query() parameter"
208
+ },
209
+ {
210
+ "annotation": "Path",
211
+ "type": "http_param",
212
+ "severity": "high",
213
+ "note": "FastAPI Path() parameter"
214
+ },
215
+ {
216
+ "annotation": "Body",
217
+ "type": "http_body",
218
+ "severity": "high",
219
+ "note": "FastAPI Body() parameter"
220
+ },
221
+ {
222
+ "annotation": "Header",
223
+ "type": "http_header",
224
+ "severity": "high",
225
+ "note": "FastAPI Header() parameter"
226
+ },
227
+ {
228
+ "annotation": "Cookie",
229
+ "type": "http_cookie",
230
+ "severity": "high",
231
+ "note": "FastAPI Cookie() parameter"
232
+ },
233
+ {
234
+ "annotation": "Form",
235
+ "type": "http_param",
236
+ "severity": "high",
237
+ "note": "FastAPI Form() parameter"
238
+ },
239
+ {
240
+ "annotation": "File",
241
+ "type": "file_input",
242
+ "severity": "high",
243
+ "note": "FastAPI File() upload"
202
244
  }
203
245
  ],
204
246
  "annotations": [
@@ -225,6 +267,42 @@
225
267
  "type": "http_param",
226
268
  "severity": "high",
227
269
  "note": "Django REST framework @api_view() decorated function"
270
+ },
271
+ {
272
+ "annotation": "app.get",
273
+ "type": "http_param",
274
+ "severity": "high",
275
+ "note": "FastAPI @app.get() decorated function parameters"
276
+ },
277
+ {
278
+ "annotation": "app.post",
279
+ "type": "http_body",
280
+ "severity": "high",
281
+ "note": "FastAPI @app.post() decorated function parameters"
282
+ },
283
+ {
284
+ "annotation": "app.put",
285
+ "type": "http_body",
286
+ "severity": "high",
287
+ "note": "FastAPI @app.put() decorated function parameters"
288
+ },
289
+ {
290
+ "annotation": "app.delete",
291
+ "type": "http_param",
292
+ "severity": "high",
293
+ "note": "FastAPI @app.delete() decorated function parameters"
294
+ },
295
+ {
296
+ "annotation": "router.get",
297
+ "type": "http_param",
298
+ "severity": "high",
299
+ "note": "FastAPI APIRouter @router.get() decorated function"
300
+ },
301
+ {
302
+ "annotation": "router.post",
303
+ "type": "http_body",
304
+ "severity": "high",
305
+ "note": "FastAPI APIRouter @router.post() decorated function"
228
306
  }
229
307
  ]
230
308
  }
@@ -67,15 +67,20 @@ function findSources(calls, types, patterns) {
67
67
  continue;
68
68
  for (const param of method.parameters) {
69
69
  // Check if parameter type could carry tainted data
70
- if (param.type && isInterproceduralTaintableType(param.type)) {
70
+ // For typed languages (Java), check the type
71
+ // For untyped languages (JavaScript), treat all params as potentially tainted
72
+ const isTaintable = param.type
73
+ ? isInterproceduralTaintableType(param.type)
74
+ : true; // JavaScript/Python - no type means any value
75
+ if (isTaintable) {
71
76
  // Use parameter line if available, fallback to method start line
72
77
  const paramLine = param.line ?? method.start_line;
73
78
  sources.push({
74
79
  type: 'interprocedural_param',
75
- location: `${param.type} ${param.name} in ${method.name}`,
80
+ location: `${param.type || 'any'} ${param.name} in ${method.name}`,
76
81
  severity: 'medium',
77
82
  line: paramLine,
78
- confidence: 0.7, // Lower confidence since we don't know the call site
83
+ confidence: param.type ? 0.7 : 0.5, // Lower confidence for untyped params
79
84
  });
80
85
  }
81
86
  }
@@ -105,7 +110,16 @@ function findSources(calls, types, patterns) {
105
110
  }
106
111
  }
107
112
  }
108
- return sources;
113
+ // Deduplicate sources by line+type, keeping highest confidence
114
+ const sourceMap = new Map();
115
+ for (const source of sources) {
116
+ const key = `${source.line}:${source.type}`;
117
+ const existing = sourceMap.get(key);
118
+ if (!existing || source.confidence > existing.confidence) {
119
+ sourceMap.set(key, source);
120
+ }
121
+ }
122
+ return Array.from(sourceMap.values());
109
123
  }
110
124
  /**
111
125
  * Check if a parameter type could carry tainted data in inter-procedural analysis.
@@ -167,23 +181,31 @@ function isInterproceduralTaintableType(typeName) {
167
181
  }
168
182
  /**
169
183
  * Find taint sinks in method calls.
184
+ * Deduplicates sinks at the same location+line+cwe, keeping highest confidence.
170
185
  */
171
186
  function findSinks(calls, patterns) {
172
- const sinks = [];
187
+ // Use a map to deduplicate by location+line+cwe
188
+ const sinkMap = new Map();
173
189
  for (const call of calls) {
174
190
  for (const pattern of patterns) {
175
191
  if (matchesSinkPattern(call, pattern)) {
176
- sinks.push({
177
- type: pattern.type,
178
- cwe: pattern.cwe,
179
- location: formatCallLocation(call),
180
- line: call.location.line,
181
- confidence: calculateSinkConfidence(call, pattern),
182
- });
192
+ const location = formatCallLocation(call);
193
+ const key = `${location}:${call.location.line}:${pattern.cwe}`;
194
+ const confidence = calculateSinkConfidence(call, pattern);
195
+ const existing = sinkMap.get(key);
196
+ if (!existing || confidence > existing.confidence) {
197
+ sinkMap.set(key, {
198
+ type: pattern.type,
199
+ cwe: pattern.cwe,
200
+ location,
201
+ line: call.location.line,
202
+ confidence,
203
+ });
204
+ }
183
205
  }
184
206
  }
185
207
  }
186
- return sinks;
208
+ return Array.from(sinkMap.values());
187
209
  }
188
210
  /**
189
211
  * Check if a call matches a source pattern.