coretrace-python-analyzer 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- coretrace_python/__init__.py +1 -1
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +5 -2
- coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +1 -1
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +32 -7
- coretrace_python/cache.py +37 -8
- coretrace_python/dependency/advisories.py +22 -9
- coretrace_python/dependency/correlation.py +81 -13
- coretrace_python/dependency/graph.py +7 -2
- coretrace_python/engine.py +9 -4
- coretrace_python/interprocedural/__init__.py +4 -0
- coretrace_python/interprocedural/callgraph.py +62 -9
- coretrace_python/interprocedural/summaries.py +8 -1
- coretrace_python/plugins/api.py +18 -4
- coretrace_python/taint/__init__.py +2 -0
- coretrace_python/taint/engine.py +76 -14
- {coretrace_python_analyzer-0.4.0.dist-info → coretrace_python_analyzer-0.5.0.dist-info}/METADATA +1 -1
- {coretrace_python_analyzer-0.4.0.dist-info → coretrace_python_analyzer-0.5.0.dist-info}/RECORD +21 -21
- {coretrace_python_analyzer-0.4.0.dist-info → coretrace_python_analyzer-0.5.0.dist-info}/WHEEL +0 -0
- {coretrace_python_analyzer-0.4.0.dist-info → coretrace_python_analyzer-0.5.0.dist-info}/entry_points.txt +0 -0
- {coretrace_python_analyzer-0.4.0.dist-info → coretrace_python_analyzer-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {coretrace_python_analyzer-0.4.0.dist-info → coretrace_python_analyzer-0.5.0.dist-info}/licenses/NOTICE +0 -0
coretrace_python/__init__.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import ClassVar
|
|
|
7
7
|
|
|
8
8
|
from coretrace_python.analysis import AnyAnalysis
|
|
9
9
|
from coretrace_python.dependency import DependencyAnalysis
|
|
10
|
-
from coretrace_python.dependency.correlation import affected_symbols, evidence
|
|
10
|
+
from coretrace_python.dependency.correlation import affected_symbols, check_conditions, evidence
|
|
11
11
|
from coretrace_python.findings import Confidence, Finding
|
|
12
12
|
from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
|
|
13
13
|
from coretrace_python.plugins import ProjectContext, ProjectPlugin
|
|
@@ -29,6 +29,9 @@ class ReachableVulnerabilityPlugin(ProjectPlugin):
|
|
|
29
29
|
if not isinstance(site.target, ExternalSymbol):
|
|
30
30
|
continue
|
|
31
31
|
for advisory in affected.get(site.target.symbol, ()):
|
|
32
|
+
check = check_conditions(advisory.entry_point(site.target.symbol), site.arguments)
|
|
33
|
+
if check.contradicted is not None:
|
|
34
|
+
continue
|
|
32
35
|
findings.append(
|
|
33
36
|
Finding(
|
|
34
37
|
rule_id="reachable-vulnerability",
|
|
@@ -40,7 +43,7 @@ class ReachableVulnerabilityPlugin(ProjectPlugin):
|
|
|
40
43
|
confidence=Confidence.HIGH,
|
|
41
44
|
span=site.location,
|
|
42
45
|
function=function,
|
|
43
|
-
metadata=evidence(advisory, site.target.symbol, "reachable"),
|
|
46
|
+
metadata=evidence(advisory, site.target.symbol, "reachable", check),
|
|
44
47
|
)
|
|
45
48
|
)
|
|
46
49
|
return findings
|
|
@@ -7,23 +7,34 @@ from typing import ClassVar
|
|
|
7
7
|
|
|
8
8
|
from coretrace_python.analysis import AnyAnalysis
|
|
9
9
|
from coretrace_python.dependency import DependencyAnalysis
|
|
10
|
+
from coretrace_python.dependency.correlation import affected_symbols, check_conditions, ruled_out
|
|
10
11
|
from coretrace_python.findings import Confidence, Finding
|
|
12
|
+
from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
|
|
11
13
|
from coretrace_python.plugins import ProjectContext, ProjectPlugin
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class VulnerableDependencyPlugin(ProjectPlugin):
|
|
15
17
|
name: ClassVar[str] = "vulnerable-dependency"
|
|
16
|
-
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis})
|
|
18
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis, CallGraphAnalysis})
|
|
17
19
|
|
|
18
20
|
def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
|
|
19
21
|
findings: list[Finding] = []
|
|
20
22
|
imported = [s for module in ctx.modules for s in ctx.imports(module).all_symbols()]
|
|
23
|
+
excluded = _ruled_out(ctx)
|
|
21
24
|
for requirement in ctx.dependencies.requirements:
|
|
22
25
|
for advisory in ctx.advisories:
|
|
23
26
|
if not advisory.affects(requirement):
|
|
24
27
|
continue
|
|
25
28
|
pinned = requirement.pinned is not None
|
|
26
29
|
level = "imported" if advisory.imported_by(imported) else "declared"
|
|
30
|
+
metadata = {
|
|
31
|
+
"advisory": advisory.id,
|
|
32
|
+
"package": advisory.package,
|
|
33
|
+
"specifier": requirement.specifier,
|
|
34
|
+
"level": level,
|
|
35
|
+
}
|
|
36
|
+
if excluded.get(advisory.id):
|
|
37
|
+
metadata["ruled_out"] = "; ".join(excluded[advisory.id])
|
|
27
38
|
findings.append(
|
|
28
39
|
Finding(
|
|
29
40
|
rule_id="vulnerable-dependency",
|
|
@@ -35,12 +46,26 @@ class VulnerableDependencyPlugin(ProjectPlugin):
|
|
|
35
46
|
severity=advisory.severity,
|
|
36
47
|
confidence=Confidence.HIGH if pinned else Confidence.MEDIUM,
|
|
37
48
|
span=requirement.span,
|
|
38
|
-
metadata=
|
|
39
|
-
"advisory": advisory.id,
|
|
40
|
-
"package": advisory.package,
|
|
41
|
-
"specifier": requirement.specifier,
|
|
42
|
-
"level": level,
|
|
43
|
-
},
|
|
49
|
+
metadata=metadata,
|
|
44
50
|
)
|
|
45
51
|
)
|
|
46
52
|
return findings
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _ruled_out(ctx: ProjectContext) -> dict[str, list[str]]:
|
|
56
|
+
"""By advisory, the calls to its entry points whose arguments contradict one of its
|
|
57
|
+
conditions: the evidence that the requirement, imported, is not reached there."""
|
|
58
|
+
|
|
59
|
+
affected = affected_symbols(ctx.dependencies, ctx.advisories)
|
|
60
|
+
excluded: dict[str, list[str]] = {}
|
|
61
|
+
for module in sorted(ctx.modules):
|
|
62
|
+
graph = ctx.call_graph(module)
|
|
63
|
+
for function in graph.functions:
|
|
64
|
+
for site in graph.sites(function):
|
|
65
|
+
if not isinstance(site.target, ExternalSymbol):
|
|
66
|
+
continue
|
|
67
|
+
for advisory in affected.get(site.target.symbol, ()):
|
|
68
|
+
check = check_conditions(advisory.entry_point(site.target.symbol), site.arguments)
|
|
69
|
+
if check.contradicted is not None:
|
|
70
|
+
excluded.setdefault(advisory.id, []).append(ruled_out(module, site, check))
|
|
71
|
+
return excluded
|
coretrace_python/cache.py
CHANGED
|
@@ -22,11 +22,13 @@ from typing import Any
|
|
|
22
22
|
|
|
23
23
|
from coretrace_python.findings import Confidence, Finding, Severity
|
|
24
24
|
from coretrace_python.interprocedural import (
|
|
25
|
+
Arguments,
|
|
25
26
|
CallSite,
|
|
26
27
|
ExternalCall,
|
|
27
28
|
ExternalSymbol,
|
|
28
29
|
FunctionSummary,
|
|
29
30
|
KnownFunction,
|
|
31
|
+
ModuleFunction,
|
|
30
32
|
ModuleGraph,
|
|
31
33
|
Mutation,
|
|
32
34
|
NonlocalWrite,
|
|
@@ -37,14 +39,14 @@ from coretrace_python.interprocedural import (
|
|
|
37
39
|
from coretrace_python.semantic.symbols import SymbolId
|
|
38
40
|
from coretrace_python.source import SourceId, SourceSpan
|
|
39
41
|
|
|
40
|
-
CACHE_FORMAT =
|
|
42
|
+
CACHE_FORMAT = 7
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
@dataclass(frozen=True)
|
|
44
46
|
class CachedModule:
|
|
45
47
|
"""Everything a later run needs from one module without lowering it again."""
|
|
46
48
|
|
|
47
|
-
functions: tuple[
|
|
49
|
+
functions: tuple[ModuleFunction, ...]
|
|
48
50
|
summaries: Mapping[str, FunctionSummary]
|
|
49
51
|
sites: tuple[CallSite, ...]
|
|
50
52
|
findings: tuple[Finding, ...]
|
|
@@ -128,7 +130,7 @@ class ProjectCache:
|
|
|
128
130
|
def encode(module: CachedModule) -> dict[str, Any]:
|
|
129
131
|
return {
|
|
130
132
|
"format": CACHE_FORMAT,
|
|
131
|
-
"functions":
|
|
133
|
+
"functions": [[f.name, _encode_span(f.span), f.entry_point] for f in module.functions],
|
|
132
134
|
"summaries": {name: _encode_summary(s) for name, s in module.summaries.items()},
|
|
133
135
|
"sites": [_encode_site(site) for site in module.sites],
|
|
134
136
|
"findings": [_encode_finding(finding) for finding in module.findings],
|
|
@@ -139,7 +141,7 @@ def decode(data: Mapping[str, Any]) -> CachedModule:
|
|
|
139
141
|
if data["format"] != CACHE_FORMAT:
|
|
140
142
|
raise ValueError(f"unsupported cache format {data['format']!r}")
|
|
141
143
|
return CachedModule(
|
|
142
|
-
tuple(
|
|
144
|
+
tuple(_decode_function(function) for function in data["functions"]),
|
|
143
145
|
{_string(name): _decode_summary(s) for name, s in data["summaries"].items()},
|
|
144
146
|
tuple(_decode_site(site) for site in data["sites"]),
|
|
145
147
|
tuple(_decode_finding(finding) for finding in data["findings"]),
|
|
@@ -187,6 +189,11 @@ def _decode_span(data: Any) -> SourceSpan:
|
|
|
187
189
|
)
|
|
188
190
|
|
|
189
191
|
|
|
192
|
+
def _decode_function(data: Any) -> ModuleFunction:
|
|
193
|
+
name, span, entry_point = data
|
|
194
|
+
return ModuleFunction(_string(name), _decode_span(span), None if entry_point is None else _string(entry_point))
|
|
195
|
+
|
|
196
|
+
|
|
190
197
|
def _encode_finding(finding: Finding) -> dict[str, Any]:
|
|
191
198
|
return {
|
|
192
199
|
"rule": finding.rule_id,
|
|
@@ -219,6 +226,7 @@ def _encode_call(call: ExternalCall) -> dict[str, Any]:
|
|
|
219
226
|
"keywords": sorted(call.keyword_dependencies),
|
|
220
227
|
"location": _encode_span(call.location),
|
|
221
228
|
"call_site": None if call.call_site is None else _encode_span(call.call_site),
|
|
229
|
+
"given": _encode_arguments(call.arguments),
|
|
222
230
|
}
|
|
223
231
|
|
|
224
232
|
|
|
@@ -230,6 +238,7 @@ def _decode_call(data: Mapping[str, Any]) -> ExternalCall:
|
|
|
230
238
|
_indices(data["keywords"]),
|
|
231
239
|
_decode_span(data["location"]),
|
|
232
240
|
None if site is None else _decode_span(site),
|
|
241
|
+
_decode_arguments(data["given"]),
|
|
233
242
|
)
|
|
234
243
|
|
|
235
244
|
|
|
@@ -311,8 +320,7 @@ def _encode_site(site: CallSite) -> dict[str, Any]:
|
|
|
311
320
|
"caller": site.caller,
|
|
312
321
|
"location": _encode_span(site.location),
|
|
313
322
|
"target": _encode_target(site.target),
|
|
314
|
-
"arguments": site.arguments,
|
|
315
|
-
"keywords": site.keywords,
|
|
323
|
+
"arguments": _encode_arguments(site.arguments),
|
|
316
324
|
}
|
|
317
325
|
|
|
318
326
|
|
|
@@ -321,6 +329,27 @@ def _decode_site(data: Mapping[str, Any]) -> CallSite:
|
|
|
321
329
|
_string(data["caller"]),
|
|
322
330
|
_decode_span(data["location"]),
|
|
323
331
|
_decode_target(data["target"]),
|
|
324
|
-
|
|
325
|
-
|
|
332
|
+
_decode_arguments(data["arguments"]),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _encode_arguments(arguments: Arguments) -> dict[str, Any]:
|
|
337
|
+
return {
|
|
338
|
+
"positional": list(arguments.positional),
|
|
339
|
+
"keywords": [[name, value] for name, value in arguments.keywords],
|
|
340
|
+
"unpacked": arguments.unpacked,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _decode_arguments(data: Mapping[str, Any]) -> Arguments:
|
|
345
|
+
def denoted(value: Any) -> str | None:
|
|
346
|
+
return None if value is None else _string(value)
|
|
347
|
+
|
|
348
|
+
unpacked = data["unpacked"]
|
|
349
|
+
if not isinstance(unpacked, bool):
|
|
350
|
+
raise TypeError(f"expected a boolean, got {unpacked!r}")
|
|
351
|
+
return Arguments(
|
|
352
|
+
tuple(denoted(value) for value in data["positional"]),
|
|
353
|
+
tuple((_string(name), denoted(value)) for name, value in data["keywords"]),
|
|
354
|
+
unpacked,
|
|
326
355
|
)
|
|
@@ -160,6 +160,10 @@ def _condition_entry(condition: Condition) -> dict[str, Any]:
|
|
|
160
160
|
entry["argument"] = condition.argument
|
|
161
161
|
if condition.values:
|
|
162
162
|
entry["values"] = list(condition.values)
|
|
163
|
+
if condition.position is not None:
|
|
164
|
+
entry["position"] = condition.position
|
|
165
|
+
if condition.default:
|
|
166
|
+
entry["default"] = True
|
|
163
167
|
return entry
|
|
164
168
|
|
|
165
169
|
|
|
@@ -193,13 +197,22 @@ def _entry_point(entry: Mapping[str, Any]) -> AdvisoryEntryPoint:
|
|
|
193
197
|
return AdvisoryEntryPoint(
|
|
194
198
|
SymbolId(str(entry["symbol"])),
|
|
195
199
|
str(entry["justification"]),
|
|
196
|
-
tuple(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
tuple(_condition(c) for c in entry.get("conditions") or []),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _condition(entry: Mapping[str, Any]) -> Condition:
|
|
205
|
+
position = entry.get("position")
|
|
206
|
+
if position is not None and (not isinstance(position, int) or isinstance(position, bool) or position < 0):
|
|
207
|
+
raise TypeError(f"condition position must be a non-negative integer, got {position!r}")
|
|
208
|
+
default = entry.get("default", False)
|
|
209
|
+
if not isinstance(default, bool):
|
|
210
|
+
raise TypeError(f"condition default must be true or false, got {default!r}")
|
|
211
|
+
return Condition(
|
|
212
|
+
str(entry["kind"]),
|
|
213
|
+
str(entry["text"]),
|
|
214
|
+
None if entry.get("argument") is None else str(entry["argument"]),
|
|
215
|
+
tuple(str(v) for v in entry.get("values") or []),
|
|
216
|
+
position,
|
|
217
|
+
default,
|
|
205
218
|
)
|
|
@@ -10,10 +10,18 @@ are correlated here into one high-confidence ``exploitable-vulnerability`` findi
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
from collections.abc import Iterable, Mapping
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from coretrace_python.dependency.graph import (
|
|
16
|
+
DIRECT,
|
|
17
|
+
Advisory,
|
|
18
|
+
AdvisoryEntryPoint,
|
|
19
|
+
Condition,
|
|
20
|
+
DependencyGraph,
|
|
21
|
+
)
|
|
15
22
|
from coretrace_python.findings import Confidence, Finding, Severity
|
|
16
23
|
from coretrace_python.findings.refutation import Status, Verdict, Verdicts
|
|
24
|
+
from coretrace_python.interprocedural import Arguments, CallSite, ExternalSymbol
|
|
17
25
|
from coretrace_python.semantic.symbols import SymbolId
|
|
18
26
|
from coretrace_python.taint import Sink, TaintFlow, TaintKind
|
|
19
27
|
|
|
@@ -40,10 +48,68 @@ def advisory_sinks(affected: Affected) -> tuple[Sink, ...]:
|
|
|
40
48
|
return tuple(Sink(symbol, TaintKind.ADVISORY) for symbol in affected)
|
|
41
49
|
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class ConditionCheck:
|
|
53
|
+
"""What one call tells of an entry point's conditions: those it meets, those left to
|
|
54
|
+
review (every semantic one, and every argument one the call does not decide), and
|
|
55
|
+
the argument condition it contradicts with what it passes instead (None: absent),
|
|
56
|
+
in which case the call does not reach the vulnerability."""
|
|
57
|
+
|
|
58
|
+
met: tuple[Condition, ...] = ()
|
|
59
|
+
pending: tuple[Condition, ...] = ()
|
|
60
|
+
contradicted: Condition | None = None
|
|
61
|
+
passed: str | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_conditions(entry: AdvisoryEntryPoint | None, arguments: Arguments | None) -> ConditionCheck:
|
|
65
|
+
"""Decide ``entry``'s conditions against what the call's ``arguments`` denote."""
|
|
66
|
+
|
|
67
|
+
if entry is None:
|
|
68
|
+
return ConditionCheck()
|
|
69
|
+
met: list[Condition] = []
|
|
70
|
+
pending: list[Condition] = []
|
|
71
|
+
for condition in entry.conditions:
|
|
72
|
+
given = _given(condition, arguments) if condition.checkable and arguments is not None else None
|
|
73
|
+
if given is None:
|
|
74
|
+
pending.append(condition)
|
|
75
|
+
continue
|
|
76
|
+
explicit, value = given
|
|
77
|
+
if explicit and value is None:
|
|
78
|
+
pending.append(condition)
|
|
79
|
+
continue
|
|
80
|
+
affected = value in condition.values if explicit else condition.default
|
|
81
|
+
if not affected:
|
|
82
|
+
return ConditionCheck(tuple(met), tuple(pending), condition, value)
|
|
83
|
+
met.append(condition)
|
|
84
|
+
return ConditionCheck(tuple(met), tuple(pending))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _given(condition: Condition, arguments: Arguments) -> tuple[bool, str | None] | None:
|
|
88
|
+
"""Whether the call gives the condition's argument, and what it denotes: ``(True,
|
|
89
|
+
value)`` when given, ``(False, None)`` when surely absent, None when it cannot tell."""
|
|
90
|
+
|
|
91
|
+
for name, value in arguments.keywords:
|
|
92
|
+
if name == condition.argument:
|
|
93
|
+
return True, value
|
|
94
|
+
if condition.position is not None and condition.position < len(arguments.positional):
|
|
95
|
+
return True, arguments.positional[condition.position]
|
|
96
|
+
return None if arguments.unpacked else (False, None)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def ruled_out(module: str, site: CallSite, check: ConditionCheck) -> str:
|
|
100
|
+
"""Which call a contradicted condition rules out, and why: ``app:12
|
|
101
|
+
python.yaml.load(Loader=python.yaml.SafeLoader)``."""
|
|
102
|
+
|
|
103
|
+
assert check.contradicted is not None and isinstance(site.target, ExternalSymbol)
|
|
104
|
+
argument = check.contradicted.argument or f"#{check.contradicted.position}"
|
|
105
|
+
passed = f"{argument} absent" if check.passed is None else f"{argument}={check.passed}"
|
|
106
|
+
return f"{module}:{site.location.start_line} {site.target.symbol}({passed})"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def evidence(advisory: Advisory, symbol: SymbolId, level: str, check: ConditionCheck) -> dict[str, str]:
|
|
44
110
|
"""What a finding keeps of the advisory for ``symbol``: the level of evidence
|
|
45
|
-
established, how the symbol relates to the vulnerability and
|
|
46
|
-
|
|
111
|
+
established, how the symbol relates to the vulnerability, and its conditions — those
|
|
112
|
+
the call meets and those left to review."""
|
|
47
113
|
|
|
48
114
|
metadata = {"advisory": advisory.id, "package": advisory.package, "symbol": str(symbol), "level": level}
|
|
49
115
|
entry = advisory.entry_point(symbol)
|
|
@@ -54,9 +120,10 @@ def evidence(advisory: Advisory, symbol: SymbolId, level: str) -> dict[str, str]
|
|
|
54
120
|
metadata["justification"] = entry.justification
|
|
55
121
|
if entry.conditions:
|
|
56
122
|
metadata["conditions"] = "; ".join(c.text for c in entry.conditions)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
123
|
+
if check.met:
|
|
124
|
+
metadata["conditions_met"] = "; ".join(c.text for c in check.met)
|
|
125
|
+
if check.pending:
|
|
126
|
+
metadata["conditions_pending_review"] = "; ".join(c.text for c in check.pending)
|
|
60
127
|
return metadata
|
|
61
128
|
|
|
62
129
|
|
|
@@ -77,21 +144,22 @@ def correlate(
|
|
|
77
144
|
if verdict is not None and verdict.status is Status.REFUTED:
|
|
78
145
|
continue
|
|
79
146
|
hotspot = verdict is not None and verdict.status is Status.HOTSPOT
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
147
|
+
for advisory in affected.get(flow.sink.symbol, ()):
|
|
148
|
+
check = check_conditions(advisory.entry_point(flow.sink.symbol), flow.sink_arguments)
|
|
149
|
+
if check.contradicted is None:
|
|
150
|
+
findings.append(_exploitable(function, flow, advisory, verdict, hotspot, check))
|
|
83
151
|
return tuple(findings)
|
|
84
152
|
|
|
85
153
|
|
|
86
154
|
def _exploitable(
|
|
87
|
-
function: str, flow: TaintFlow, advisory: Advisory, verdict: Verdict | None, hotspot: bool
|
|
155
|
+
function: str, flow: TaintFlow, advisory: Advisory, verdict: Verdict | None, hotspot: bool, check: ConditionCheck
|
|
88
156
|
) -> Finding:
|
|
89
157
|
message = (
|
|
90
158
|
f"{advisory.id}: {flow.source.label} input reaches {flow.sink.symbol}, affected in "
|
|
91
159
|
f"the required {advisory.package} {advisory.vulnerable}: {advisory.summary}"
|
|
92
160
|
)
|
|
93
161
|
metadata = {
|
|
94
|
-
**evidence(advisory, flow.sink.symbol, "exploitable"),
|
|
162
|
+
**evidence(advisory, flow.sink.symbol, "exploitable", check),
|
|
95
163
|
"source": str(flow.source.symbol),
|
|
96
164
|
"source_label": flow.source.label,
|
|
97
165
|
"verdict": "hotspot" if hotspot else "vulnerability",
|
|
@@ -147,13 +147,18 @@ class Condition:
|
|
|
147
147
|
"""What must hold for an entry point to reach, or exploit, the affected code: the
|
|
148
148
|
value of an argument, the loader or algorithm used, the format decoded, the kind of
|
|
149
149
|
input, a configuration. ``kind`` says whether the engine can check it: an
|
|
150
|
-
``argument`` condition names the argument and
|
|
151
|
-
|
|
150
|
+
``argument`` condition names the argument by keyword, and by ``position`` when it may
|
|
151
|
+
be passed positionally, and lists the ``values`` that satisfy it — symbols
|
|
152
|
+
(``python.yaml.FullLoader``) or constants as Python writes them (``True``);
|
|
153
|
+
``default`` says an absent argument means a vulnerable value. A ``semantic``
|
|
154
|
+
condition cannot be checked and is reported as pending review."""
|
|
152
155
|
|
|
153
156
|
kind: str
|
|
154
157
|
text: str
|
|
155
158
|
argument: str | None = None
|
|
156
159
|
values: tuple[str, ...] = ()
|
|
160
|
+
position: int | None = None
|
|
161
|
+
default: bool = False
|
|
157
162
|
|
|
158
163
|
@property
|
|
159
164
|
def checkable(self) -> bool:
|
coretrace_python/engine.py
CHANGED
|
@@ -71,6 +71,7 @@ from coretrace_python.interprocedural import (
|
|
|
71
71
|
CallGraphAnalysis,
|
|
72
72
|
CallSite,
|
|
73
73
|
FunctionSummary,
|
|
74
|
+
ModuleFunction,
|
|
74
75
|
ModuleGraph,
|
|
75
76
|
ProjectSummaries,
|
|
76
77
|
SummaryAnalysis,
|
|
@@ -106,6 +107,7 @@ from coretrace_python.semantic.symbols import SymbolAnalysis, SymbolId
|
|
|
106
107
|
from coretrace_python.source import SourceFile, SourceId, SourceManager, SourceSpan, decode_text
|
|
107
108
|
from coretrace_python.taint import (
|
|
108
109
|
EntryPoint,
|
|
110
|
+
EntryPointAnalysis,
|
|
109
111
|
ModelTable,
|
|
110
112
|
RegisteredRoutes,
|
|
111
113
|
Routes,
|
|
@@ -139,6 +141,7 @@ ALL_ANALYSES: tuple[AnyAnalysis, ...] = (
|
|
|
139
141
|
ProjectSummaries,
|
|
140
142
|
SecurityModelAnalysis,
|
|
141
143
|
TaintAnalysis,
|
|
144
|
+
EntryPointAnalysis,
|
|
142
145
|
RefutationAnalysis,
|
|
143
146
|
DependencyAnalysis,
|
|
144
147
|
RegisteredRoutes,
|
|
@@ -147,7 +150,7 @@ ALL_ANALYSES: tuple[AnyAnalysis, ...] = (
|
|
|
147
150
|
|
|
148
151
|
# Named so the set stays right whichever analyses a build registers.
|
|
149
152
|
_PROJECT_DEPENDANT_NAMES = frozenset(
|
|
150
|
-
{"interprocedural.project", "interprocedural.summaries", "taint.flows", "findings.refutation"}
|
|
153
|
+
{"interprocedural.project", "interprocedural.summaries", "taint.flows", "taint.entry_points", "findings.refutation"}
|
|
151
154
|
)
|
|
152
155
|
_PROJECT_DEPENDANTS: frozenset[AnyAnalysis] = frozenset(
|
|
153
156
|
a for a in ALL_ANALYSES if a.name in _PROJECT_DEPENDANT_NAMES
|
|
@@ -451,6 +454,7 @@ def analyze_project(
|
|
|
451
454
|
|
|
452
455
|
index = _seed(results, graph, frozenset())
|
|
453
456
|
call_graphs: dict[str, CallGraph] = {}
|
|
457
|
+
functions: dict[str, tuple[ModuleFunction, ...]] = {}
|
|
454
458
|
for name in sorted(analysable):
|
|
455
459
|
entry = results[name]
|
|
456
460
|
findings.extend(entry.findings)
|
|
@@ -458,11 +462,12 @@ def analyze_project(
|
|
|
458
462
|
coverage.append(
|
|
459
463
|
FileCoverage(str(files[name].source_id), "analysed", len(entry.functions), len(entry.functions) - unsupported)
|
|
460
464
|
)
|
|
461
|
-
|
|
465
|
+
functions[name] = entry.functions
|
|
466
|
+
sites: dict[str, list[CallSite]] = {function.name: [] for function in entry.functions}
|
|
462
467
|
for site in entry.sites:
|
|
463
468
|
sites.setdefault(site.caller, []).append(site)
|
|
464
469
|
call_graphs[name] = CallGraph({}, {f: tuple(s) for f, s in sites.items()}, frozenset())
|
|
465
|
-
context = ProjectContext(graph, dependencies, advisories, analysable, call_graphs, policy, root)
|
|
470
|
+
context = ProjectContext(graph, dependencies, advisories, analysable, call_graphs, policy, root, functions)
|
|
466
471
|
for plugin in all_plugins:
|
|
467
472
|
if isinstance(plugin, ProjectPlugin):
|
|
468
473
|
findings.extend(plugin.analyze_project(context))
|
|
@@ -662,7 +667,7 @@ def _analyse_module(
|
|
|
662
667
|
)
|
|
663
668
|
)
|
|
664
669
|
return CachedModule(
|
|
665
|
-
|
|
670
|
+
manager.get(EntryPointAnalysis),
|
|
666
671
|
_summaries_of(manager),
|
|
667
672
|
tuple(site for function in graph.functions for site in graph.sites(function)),
|
|
668
673
|
(*findings, *correlated),
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Call graph and function summaries (architecture §19, §20)."""
|
|
2
2
|
|
|
3
3
|
from coretrace_python.interprocedural.callgraph import (
|
|
4
|
+
Arguments,
|
|
4
5
|
CallGraph,
|
|
5
6
|
CallGraphAnalysis,
|
|
6
7
|
CallSite,
|
|
7
8
|
ExternalSymbol,
|
|
8
9
|
KnownFunction,
|
|
10
|
+
ModuleFunction,
|
|
9
11
|
Target,
|
|
10
12
|
UnknownTarget,
|
|
11
13
|
)
|
|
@@ -28,6 +30,7 @@ from coretrace_python.interprocedural.summaries import (
|
|
|
28
30
|
)
|
|
29
31
|
|
|
30
32
|
__all__ = [
|
|
33
|
+
"Arguments",
|
|
31
34
|
"CallGraph",
|
|
32
35
|
"CallGraphAnalysis",
|
|
33
36
|
"CallSite",
|
|
@@ -35,6 +38,7 @@ __all__ = [
|
|
|
35
38
|
"ExternalSymbol",
|
|
36
39
|
"FunctionSummary",
|
|
37
40
|
"KnownFunction",
|
|
41
|
+
"ModuleFunction",
|
|
38
42
|
"ModuleGraph",
|
|
39
43
|
"Mutation",
|
|
40
44
|
"NonlocalWrite",
|
|
@@ -9,7 +9,7 @@ and framework models narrow it down.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from collections.abc import Mapping
|
|
12
|
-
from dataclasses import dataclass
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
13
|
from types import MappingProxyType
|
|
14
14
|
from typing import ClassVar
|
|
15
15
|
|
|
@@ -25,10 +25,12 @@ from coretrace_python.ir.lowering import (
|
|
|
25
25
|
from coretrace_python.ir.model import (
|
|
26
26
|
Await,
|
|
27
27
|
Call,
|
|
28
|
+
Constant,
|
|
28
29
|
FunctionIR,
|
|
29
30
|
GetAttr,
|
|
30
31
|
GetItem,
|
|
31
32
|
Global,
|
|
33
|
+
Instruction,
|
|
32
34
|
MakeFunction,
|
|
33
35
|
Symbol,
|
|
34
36
|
Value,
|
|
@@ -58,13 +60,36 @@ class UnknownTarget:
|
|
|
58
60
|
Target = KnownFunction | ExternalSymbol | UnknownTarget
|
|
59
61
|
|
|
60
62
|
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class Arguments:
|
|
65
|
+
"""What the arguments of a call denote: a symbol's canonical name
|
|
66
|
+
(``python.yaml.FullLoader``), a constant as Python writes it (``True``, ``'/static'``),
|
|
67
|
+
or None for anything else (a parameter, an expression). ``unpacked`` says the call
|
|
68
|
+
unpacks ``*args`` or ``**kwargs``, so an argument it does not give explicitly may
|
|
69
|
+
still be given; after ``*args`` no position is known, and ``positional`` is empty."""
|
|
70
|
+
|
|
71
|
+
positional: tuple[str | None, ...] = ()
|
|
72
|
+
keywords: tuple[tuple[str, str | None], ...] = ()
|
|
73
|
+
unpacked: bool = False
|
|
74
|
+
|
|
75
|
+
|
|
61
76
|
@dataclass(frozen=True)
|
|
62
77
|
class CallSite:
|
|
63
78
|
caller: str
|
|
64
79
|
location: SourceSpan
|
|
65
80
|
target: Target
|
|
66
|
-
arguments:
|
|
67
|
-
|
|
81
|
+
arguments: Arguments = field(default_factory=Arguments)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class ModuleFunction:
|
|
86
|
+
"""One function of a module as a project plugin sees it: the name the call graph
|
|
87
|
+
gives it (``Class.method``, ``outer.inner``, ``<module>``), where it is, and the label
|
|
88
|
+
of the entry point it is (``http`` for a route, ``argv`` for a command), if any."""
|
|
89
|
+
|
|
90
|
+
name: str
|
|
91
|
+
span: SourceSpan
|
|
92
|
+
entry_point: str | None = None
|
|
68
93
|
|
|
69
94
|
|
|
70
95
|
class CallGraph:
|
|
@@ -82,9 +107,7 @@ class CallGraph:
|
|
|
82
107
|
self._names = {function.span: name for name, function in definitions.items()}
|
|
83
108
|
self.unsupported = unsupported
|
|
84
109
|
self._sites = MappingProxyType(dict(sites))
|
|
85
|
-
self.
|
|
86
|
-
(site.caller, site.location): site.target for found in sites.values() for site in found
|
|
87
|
-
}
|
|
110
|
+
self._at = {(site.caller, site.location): site for found in sites.values() for site in found}
|
|
88
111
|
callers: dict[str, set[str]] = {name: set() for name in definitions}
|
|
89
112
|
for found in sites.values():
|
|
90
113
|
for site in found:
|
|
@@ -104,7 +127,14 @@ class CallGraph:
|
|
|
104
127
|
return self._sites.get(caller, ())
|
|
105
128
|
|
|
106
129
|
def target_at(self, caller: str, location: SourceSpan) -> Target:
|
|
107
|
-
|
|
130
|
+
site = self._at.get((caller, location))
|
|
131
|
+
return site.target if site is not None else UnknownTarget()
|
|
132
|
+
|
|
133
|
+
def arguments_at(self, caller: str, location: SourceSpan) -> Arguments:
|
|
134
|
+
"""What the arguments of the call at ``location`` in ``caller`` denote."""
|
|
135
|
+
|
|
136
|
+
site = self._at.get((caller, location))
|
|
137
|
+
return site.arguments if site is not None else Arguments()
|
|
108
138
|
|
|
109
139
|
def callees(self, caller: str) -> frozenset[str]:
|
|
110
140
|
return frozenset(
|
|
@@ -154,6 +184,29 @@ def derive_symbols(
|
|
|
154
184
|
return symbols
|
|
155
185
|
|
|
156
186
|
|
|
187
|
+
# Conditions name short constants (``True``, ``None``, a mode); a log message or a query
|
|
188
|
+
# passed as an argument is not recorded, so call sites stay small in the cache.
|
|
189
|
+
MAX_CONSTANT_LENGTH = 64
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _arguments(call: Call, symbols: Mapping[Value, SymbolId], defs: Mapping[Value, Instruction]) -> Arguments:
|
|
193
|
+
def denoted(value: Value) -> str | None:
|
|
194
|
+
symbol = symbols.get(value)
|
|
195
|
+
if symbol is not None:
|
|
196
|
+
return str(symbol)
|
|
197
|
+
made = defs.get(value)
|
|
198
|
+
if not isinstance(made, Constant):
|
|
199
|
+
return None
|
|
200
|
+
written = repr(made.value)
|
|
201
|
+
return written if len(written) <= MAX_CONSTANT_LENGTH else None
|
|
202
|
+
|
|
203
|
+
return Arguments(
|
|
204
|
+
() if call.starred else tuple(denoted(value) for value in call.arguments),
|
|
205
|
+
tuple((name, denoted(value)) for name, value in call.keywords if name is not None),
|
|
206
|
+
bool(call.starred) or any(name is None for name, _ in call.keywords),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
157
210
|
def resolve_targets(
|
|
158
211
|
function: FunctionIR,
|
|
159
212
|
scopes: ScopeTable,
|
|
@@ -328,6 +381,7 @@ class CallGraphAnalysis(Analysis[CallGraph]):
|
|
|
328
381
|
_typed_with_module_classes(function, ssa, classes),
|
|
329
382
|
bases,
|
|
330
383
|
)
|
|
384
|
+
defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
|
|
331
385
|
found: list[CallSite] = []
|
|
332
386
|
for block in ssa.blocks:
|
|
333
387
|
for instruction in block.instructions:
|
|
@@ -337,8 +391,7 @@ class CallGraphAnalysis(Analysis[CallGraph]):
|
|
|
337
391
|
name,
|
|
338
392
|
instruction.location,
|
|
339
393
|
targets.get(instruction.callee, UnknownTarget()),
|
|
340
|
-
|
|
341
|
-
len(instruction.keywords),
|
|
394
|
+
_arguments(instruction, symbols[name], defs),
|
|
342
395
|
)
|
|
343
396
|
)
|
|
344
397
|
sites[name] = tuple(found)
|
|
@@ -9,6 +9,7 @@ security knowledge: the taint engine decides which external symbols matter.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import dataclasses
|
|
12
13
|
from collections.abc import Mapping
|
|
13
14
|
from dataclasses import dataclass
|
|
14
15
|
from types import MappingProxyType
|
|
@@ -26,6 +27,7 @@ from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
|
|
|
26
27
|
from coretrace_python.cfg import CFG, BlockId, CFGAnalysis
|
|
27
28
|
from coretrace_python.dataflow import DataflowProblem, Direction, solve
|
|
28
29
|
from coretrace_python.interprocedural.callgraph import (
|
|
30
|
+
Arguments,
|
|
29
31
|
CallGraph,
|
|
30
32
|
CallGraphAnalysis,
|
|
31
33
|
ExternalSymbol,
|
|
@@ -92,6 +94,8 @@ class ExternalCall:
|
|
|
92
94
|
keyword_dependencies: Dependencies
|
|
93
95
|
location: SourceSpan
|
|
94
96
|
call_site: SourceSpan | None
|
|
97
|
+
# What the arguments of the call at ``location`` denote, where it is made.
|
|
98
|
+
arguments: Arguments = dataclasses.field(default_factory=Arguments)
|
|
95
99
|
|
|
96
100
|
|
|
97
101
|
@dataclass(frozen=True)
|
|
@@ -334,6 +338,7 @@ class _DependenceProblem(DataflowProblem[State]):
|
|
|
334
338
|
keywords.parameters,
|
|
335
339
|
call.location,
|
|
336
340
|
None,
|
|
341
|
+
self.graph.arguments_at(self.name, call.location),
|
|
337
342
|
)
|
|
338
343
|
return everything | Dep(externals=frozenset({target.symbol}))
|
|
339
344
|
if isinstance(target, KnownFunction):
|
|
@@ -416,6 +421,7 @@ class _DependenceProblem(DataflowProblem[State]):
|
|
|
416
421
|
mapped(reached.keyword_dependencies).parameters,
|
|
417
422
|
reached.location,
|
|
418
423
|
call.location,
|
|
424
|
+
reached.arguments,
|
|
419
425
|
)
|
|
420
426
|
made = self.defs.get(call.callee)
|
|
421
427
|
values = (*receiver, *call.arguments, *(made.captured if isinstance(made, MakeFunction) else ()))
|
|
@@ -432,6 +438,7 @@ class _DependenceProblem(DataflowProblem[State]):
|
|
|
432
438
|
keywords: Dependencies,
|
|
433
439
|
location: SourceSpan,
|
|
434
440
|
call_site: SourceSpan | None,
|
|
441
|
+
given: Arguments,
|
|
435
442
|
) -> None:
|
|
436
443
|
key = (symbol, location, call_site)
|
|
437
444
|
previous = self.external.get(key)
|
|
@@ -440,7 +447,7 @@ class _DependenceProblem(DataflowProblem[State]):
|
|
|
440
447
|
a | b for a, b in zip(previous.argument_dependencies, arguments, strict=False)
|
|
441
448
|
)
|
|
442
449
|
keywords |= previous.keyword_dependencies
|
|
443
|
-
self.external[key] = ExternalCall(symbol, arguments, keywords, location, call_site)
|
|
450
|
+
self.external[key] = ExternalCall(symbol, arguments, keywords, location, call_site, given)
|
|
444
451
|
|
|
445
452
|
|
|
446
453
|
def summarize(
|
coretrace_python/plugins/api.py
CHANGED
|
@@ -25,10 +25,15 @@ from coretrace_python.analysis.provider import R
|
|
|
25
25
|
from coretrace_python.dependency import Advisory, DependencyGraph, Policy
|
|
26
26
|
from coretrace_python.findings import Finding
|
|
27
27
|
from coretrace_python.hir import nodes
|
|
28
|
-
from coretrace_python.interprocedural import
|
|
28
|
+
from coretrace_python.interprocedural import (
|
|
29
|
+
CallGraph,
|
|
30
|
+
CallGraphAnalysis,
|
|
31
|
+
ModuleFunction,
|
|
32
|
+
ModuleGraph,
|
|
33
|
+
)
|
|
29
34
|
from coretrace_python.ir.lowering import analyzable_functions
|
|
30
35
|
from coretrace_python.semantic.imports import ImportAnalysis, ImportTable
|
|
31
|
-
from coretrace_python.taint import Model
|
|
36
|
+
from coretrace_python.taint import EntryPointAnalysis, Model
|
|
32
37
|
|
|
33
38
|
PLUGIN_API_VERSION = 1
|
|
34
39
|
|
|
@@ -57,8 +62,8 @@ class ModelPlugin(Plugin):
|
|
|
57
62
|
class ProjectContext:
|
|
58
63
|
"""What a project-scoped plugin sees: the module graph, the dependency graph, the
|
|
59
64
|
advisories every plugin and advisory file contributed, the dependency policy, and
|
|
60
|
-
each module's imports
|
|
61
|
-
it served from its cache."""
|
|
65
|
+
each module's imports, call graph and functions. The engine passes the call graphs
|
|
66
|
+
and functions of modules it served from its cache."""
|
|
62
67
|
|
|
63
68
|
def __init__(
|
|
64
69
|
self,
|
|
@@ -69,6 +74,7 @@ class ProjectContext:
|
|
|
69
74
|
call_graphs: Mapping[str, CallGraph] | None = None,
|
|
70
75
|
policy: Policy | None = None,
|
|
71
76
|
root: Path | None = None,
|
|
77
|
+
functions: Mapping[str, tuple[ModuleFunction, ...]] | None = None,
|
|
72
78
|
) -> None:
|
|
73
79
|
self.root = root
|
|
74
80
|
self.graph = graph
|
|
@@ -77,6 +83,7 @@ class ProjectContext:
|
|
|
77
83
|
self.policy = policy or Policy()
|
|
78
84
|
self._managers = managers
|
|
79
85
|
self._call_graphs = dict(call_graphs or {})
|
|
86
|
+
self._functions = dict(functions or {})
|
|
80
87
|
|
|
81
88
|
@property
|
|
82
89
|
def modules(self) -> tuple[str, ...]:
|
|
@@ -89,6 +96,13 @@ class ProjectContext:
|
|
|
89
96
|
cached = self._call_graphs.get(module)
|
|
90
97
|
return cached if cached is not None else self._managers[module].get(CallGraphAnalysis)
|
|
91
98
|
|
|
99
|
+
def functions(self, module: str) -> tuple[ModuleFunction, ...]:
|
|
100
|
+
"""The module's functions as its call graph names them, each with its span, so a
|
|
101
|
+
finding can be placed in one, and the label of the entry point it is, if any."""
|
|
102
|
+
|
|
103
|
+
cached = self._functions.get(module)
|
|
104
|
+
return cached if cached is not None else self._managers[module].get(EntryPointAnalysis)
|
|
105
|
+
|
|
92
106
|
|
|
93
107
|
class ProjectPlugin(Plugin):
|
|
94
108
|
"""A plugin that runs once per project rather than once per module (§26)."""
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Security models and the global multi-kind taint engine (architecture §16, §17)."""
|
|
2
2
|
|
|
3
3
|
from coretrace_python.taint.engine import (
|
|
4
|
+
EntryPointAnalysis,
|
|
4
5
|
Taint,
|
|
5
6
|
TaintAnalysis,
|
|
6
7
|
TaintFacts,
|
|
@@ -30,6 +31,7 @@ from coretrace_python.taint.routes import RegisteredRoutes, Routes, registered_r
|
|
|
30
31
|
__all__ = [
|
|
31
32
|
"AuthorizationGuard",
|
|
32
33
|
"EntryPoint",
|
|
34
|
+
"EntryPointAnalysis",
|
|
33
35
|
"Model",
|
|
34
36
|
"ModelError",
|
|
35
37
|
"ModelTable",
|
coretrace_python/taint/engine.py
CHANGED
|
@@ -22,16 +22,18 @@ from coretrace_python.abstract import (
|
|
|
22
22
|
HeapLocation,
|
|
23
23
|
mutated_by,
|
|
24
24
|
)
|
|
25
|
-
from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
|
|
25
|
+
from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis, FunctionAnalysis
|
|
26
26
|
from coretrace_python.cfg import CFG, BlockId, CFGAnalysis
|
|
27
27
|
from coretrace_python.dataflow import DataflowProblem, Direction, solve
|
|
28
28
|
from coretrace_python.hir import nodes
|
|
29
29
|
from coretrace_python.interprocedural import (
|
|
30
|
+
Arguments,
|
|
30
31
|
CallGraph,
|
|
31
32
|
CallGraphAnalysis,
|
|
32
33
|
ExternalSymbol,
|
|
33
34
|
FunctionSummary,
|
|
34
35
|
KnownFunction,
|
|
36
|
+
ModuleFunction,
|
|
35
37
|
ProjectSummaries,
|
|
36
38
|
SummaryAnalysis,
|
|
37
39
|
SummaryIndex,
|
|
@@ -111,6 +113,8 @@ class TaintFlow:
|
|
|
111
113
|
location: SourceSpan
|
|
112
114
|
through: str | None = None
|
|
113
115
|
sink_location: SourceSpan | None = None
|
|
116
|
+
# What the arguments of the sink call denote, to decide the conditions on them.
|
|
117
|
+
sink_arguments: Arguments | None = None
|
|
114
118
|
|
|
115
119
|
|
|
116
120
|
Key = Value | HeapLocation
|
|
@@ -394,10 +398,11 @@ class _TaintProblem(DataflowProblem[State]):
|
|
|
394
398
|
|
|
395
399
|
sink = self.models.sink(symbol)
|
|
396
400
|
if sink is not None:
|
|
401
|
+
given = self.graph.arguments_at(self.name, call.location)
|
|
397
402
|
for position, argument in enumerate(call.arguments):
|
|
398
|
-
self.report(flows, sink, self.rendered(argument, state), argument, call, None, None, position)
|
|
403
|
+
self.report(flows, sink, self.rendered(argument, state), argument, call, None, None, position, given)
|
|
399
404
|
for argument in (*call.starred, *(value for _, value in call.keywords)):
|
|
400
|
-
self.report(flows, sink, self.rendered(argument, state), argument, call, None, None, None)
|
|
405
|
+
self.report(flows, sink, self.rendered(argument, state), argument, call, None, None, None, given)
|
|
401
406
|
sanitizer = self.models.sanitizer(symbol)
|
|
402
407
|
if sanitizer is not None:
|
|
403
408
|
return everything.without(sanitizer.kinds)
|
|
@@ -483,7 +488,7 @@ class _TaintProblem(DataflowProblem[State]):
|
|
|
483
488
|
for position, deps in zip(positions, (*reached.argument_dependencies, reached.keyword_dependencies), strict=True):
|
|
484
489
|
taint, witness = mapped(deps)
|
|
485
490
|
if witness is not None:
|
|
486
|
-
self.report(flows, sink, taint, witness, call, through, reached.location, position)
|
|
491
|
+
self.report(flows, sink, taint, witness, call, through, reached.location, position, reached.arguments)
|
|
487
492
|
for mutation in summary.mutations:
|
|
488
493
|
if mutation.parameter < len(values):
|
|
489
494
|
stored = mapped(mutation.dependencies)[0]
|
|
@@ -509,6 +514,7 @@ class _TaintProblem(DataflowProblem[State]):
|
|
|
509
514
|
through: str | None,
|
|
510
515
|
sink_location: SourceSpan | None,
|
|
511
516
|
position: int | None = None,
|
|
517
|
+
arguments: Arguments | None = None,
|
|
512
518
|
) -> None:
|
|
513
519
|
reaching = taint.kinds & sink.kinds_at(position)
|
|
514
520
|
if not reaching:
|
|
@@ -523,6 +529,7 @@ class _TaintProblem(DataflowProblem[State]):
|
|
|
523
529
|
call.location,
|
|
524
530
|
through,
|
|
525
531
|
call.location if sink_location is None else sink_location,
|
|
532
|
+
arguments,
|
|
526
533
|
)
|
|
527
534
|
)
|
|
528
535
|
|
|
@@ -684,7 +691,7 @@ def entry_point_of(
|
|
|
684
691
|
return None
|
|
685
692
|
|
|
686
693
|
|
|
687
|
-
def
|
|
694
|
+
def function_entry_point(
|
|
688
695
|
function: nodes.Function,
|
|
689
696
|
module: nodes.Module,
|
|
690
697
|
models: ModelTable,
|
|
@@ -692,16 +699,11 @@ def parameter_sources(
|
|
|
692
699
|
symbols: SymbolTable,
|
|
693
700
|
instances: Instances | None = None,
|
|
694
701
|
routes: Routes | None = None,
|
|
695
|
-
) ->
|
|
696
|
-
"""The
|
|
697
|
-
|
|
698
|
-
(``routes``), and every parameter annotated with a typed-parameter symbol."""
|
|
702
|
+
) -> EntryPoint | None:
|
|
703
|
+
"""The entry point ``function`` is: one of its decorators or, for a method, one of its
|
|
704
|
+
class's bases matching an entry-point model, or a registration elsewhere (``routes``)."""
|
|
699
705
|
|
|
700
|
-
owner =
|
|
701
|
-
(s for s in module.body if isinstance(s, nodes.Class) and any(f is function for f in s.body)),
|
|
702
|
-
None,
|
|
703
|
-
)
|
|
704
|
-
sources: dict[int, Source] = {}
|
|
706
|
+
owner = _owner(module, function)
|
|
705
707
|
enclosing_function = _enclosing(module, function)
|
|
706
708
|
if enclosing_function is not None:
|
|
707
709
|
# ``app = Flask(__name__)`` inside ``create_app``: routes defined there resolve.
|
|
@@ -712,6 +714,34 @@ def parameter_sources(
|
|
|
712
714
|
entry = routes.get(project_symbol(module.name, qualified))
|
|
713
715
|
if entry is None and owner is not None:
|
|
714
716
|
entry = routes.get(project_symbol(module.name, owner.name))
|
|
717
|
+
return entry
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _owner(module: nodes.Module, function: nodes.Function) -> nodes.Class | None:
|
|
721
|
+
"""The top-level class ``function`` is a method of, if any."""
|
|
722
|
+
|
|
723
|
+
return next(
|
|
724
|
+
(s for s in module.body if isinstance(s, nodes.Class) and any(f is function for f in s.body)),
|
|
725
|
+
None,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def parameter_sources(
|
|
730
|
+
function: nodes.Function,
|
|
731
|
+
module: nodes.Module,
|
|
732
|
+
models: ModelTable,
|
|
733
|
+
scopes: ScopeTable,
|
|
734
|
+
symbols: SymbolTable,
|
|
735
|
+
instances: Instances | None = None,
|
|
736
|
+
routes: Routes | None = None,
|
|
737
|
+
) -> Mapping[int, Source]:
|
|
738
|
+
"""The attacker-controlled parameters of ``function``, by index: every parameter of an
|
|
739
|
+
entry point (``self`` excepted for a method), including one registered elsewhere
|
|
740
|
+
(``routes``), and every parameter annotated with a typed-parameter symbol."""
|
|
741
|
+
|
|
742
|
+
owner = _owner(module, function)
|
|
743
|
+
sources: dict[int, Source] = {}
|
|
744
|
+
entry = function_entry_point(function, module, models, scopes, symbols, instances, routes)
|
|
715
745
|
if entry is not None:
|
|
716
746
|
first = 1 if owner is not None else 0
|
|
717
747
|
for index in range(first, len(function.parameters)):
|
|
@@ -817,6 +847,38 @@ class TaintAnalysis(FunctionAnalysis[TaintFacts]):
|
|
|
817
847
|
)
|
|
818
848
|
|
|
819
849
|
|
|
850
|
+
class EntryPointAnalysis(Analysis[tuple[ModuleFunction, ...]]):
|
|
851
|
+
"""The module's functions as the call graph names them, each with its span and the
|
|
852
|
+
label of the entry point it is, decided as the taint engine decides which parameters
|
|
853
|
+
are attacker-controlled. What a project plugin sees of a module, cached with it."""
|
|
854
|
+
|
|
855
|
+
name: ClassVar[str] = "taint.entry_points"
|
|
856
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset(
|
|
857
|
+
{
|
|
858
|
+
CallGraphAnalysis,
|
|
859
|
+
SecurityModelAnalysis,
|
|
860
|
+
ScopeAnalysis,
|
|
861
|
+
SymbolAnalysis,
|
|
862
|
+
SummaryAnalysis,
|
|
863
|
+
ProjectSummaries,
|
|
864
|
+
RegisteredRoutes,
|
|
865
|
+
}
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
@classmethod
|
|
869
|
+
def compute(cls, ctx: AnalysisContext) -> tuple[ModuleFunction, ...]:
|
|
870
|
+
graph = ctx.get(CallGraphAnalysis)
|
|
871
|
+
models = ctx.get(SecurityModelAnalysis)
|
|
872
|
+
scopes, symbols = ctx.get(ScopeAnalysis), ctx.get(SymbolAnalysis)
|
|
873
|
+
instances = factory_instances(ctx.module, scopes, symbols, ctx.get(SummaryAnalysis), ctx.get(ProjectSummaries))
|
|
874
|
+
routes = ctx.get(RegisteredRoutes)
|
|
875
|
+
functions: list[ModuleFunction] = []
|
|
876
|
+
for name, function in graph.definitions.items():
|
|
877
|
+
entry = function_entry_point(function, ctx.module, models, scopes, symbols, instances, routes)
|
|
878
|
+
functions.append(ModuleFunction(name, function.span, entry.label if entry is not None else None))
|
|
879
|
+
return tuple(functions)
|
|
880
|
+
|
|
881
|
+
|
|
820
882
|
def self_seeds(
|
|
821
883
|
function: nodes.Function,
|
|
822
884
|
module: nodes.Module,
|
{coretrace_python_analyzer-0.4.0.dist-info → coretrace_python_analyzer-0.5.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: coretrace-python-analyzer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Static security analysis infrastructure for Python code
|
|
5
5
|
Project-URL: Homepage, https://github.com/CoreTrace/coretrace-python-analyzer
|
|
6
6
|
Project-URL: Repository, https://github.com/CoreTrace/coretrace-python-analyzer
|
{coretrace_python_analyzer-0.4.0.dist-info → coretrace_python_analyzer-0.5.0.dist-info}/RECORD
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
coretrace_python/__init__.py,sha256=
|
|
1
|
+
coretrace_python/__init__.py,sha256=utnxu8MbeZzYTN5zz5v2YJUPEiq63GzimvSef3LaKIo,75
|
|
2
2
|
coretrace_python/__main__.py,sha256=V8DnYtRboISK2VCKm7DGHLREKvqLbqApyWlKBOs26wU,65
|
|
3
|
-
coretrace_python/cache.py,sha256=
|
|
3
|
+
coretrace_python/cache.py,sha256=D14p9uzdise9HtkIEJVLZXICt5jUPcDYUMxo2iuh9g0,12427
|
|
4
4
|
coretrace_python/cli.py,sha256=ZE6F8wqNcE-ogfldii-UgUq4nOcEMnHuMCzz0l--yuw,9792
|
|
5
|
-
coretrace_python/engine.py,sha256=
|
|
5
|
+
coretrace_python/engine.py,sha256=ZXEPnio1cRSMme9HP2kmHJaYpychCRDYpZSyFosEZx0,27625
|
|
6
6
|
coretrace_python/abstract/__init__.py,sha256=n5FNkV8Z4LmwG0r06KWdLj4N2CPSgAKwY3896tdmRgM,999
|
|
7
7
|
coretrace_python/abstract/constants.py,sha256=z4-e7IzysxzGi58wFFff1MlEoV8l7AZsB9EL-q6b7vY,9071
|
|
8
8
|
coretrace_python/abstract/heap.py,sha256=OaESXFGC0qzyhAr76xrCu4skeDVzZM4wlenQqfMTkdk,9642
|
|
@@ -14,11 +14,11 @@ coretrace_python/analysis/provider.py,sha256=Q_OlEcoLzBAsNdAGWijbj_n3E2Ep5UQ4Z8_
|
|
|
14
14
|
coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py,sha256=_CJYHijv4DTgfdumcCqAVV5_MPWjfZOadxeM5pQdw_k,1845
|
|
15
15
|
coretrace_python/bundled/dependency/dependency_policy/plugin.toml,sha256=4w1TA-UKsa82WTnd1xy4bJuU3_G2RNCAWNICLPcZLMk,210
|
|
16
16
|
coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml,sha256=LuF9jQqZfplN_GBky69Xjq2pksaX1Fs8PYEir-7-vY8,275
|
|
17
|
-
coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py,sha256=
|
|
17
|
+
coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py,sha256=FwEqdDbkyYOUzF2piVteFfSzEm75tCnk2qRe8RvRNxA,2414
|
|
18
18
|
coretrace_python/bundled/dependency/sample_advisories/plugin.toml,sha256=cME4pSFzkFsxmIJGGBSrmXqynAd0dg34X-rgEoZ_u6E,184
|
|
19
19
|
coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py,sha256=jw98GK6hh5RAx05t8QPcUzsFMfGigAL8418vZLprKlc,3710
|
|
20
|
-
coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml,sha256=
|
|
21
|
-
coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py,sha256=
|
|
20
|
+
coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml,sha256=4-OnbEb3lYVAoTfuz8mDPH4L9fHOPQ2KW1dDFAbClFk,267
|
|
21
|
+
coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py,sha256=THXqpuffQZ8nuv1b8zQrgkA7eTI1PxhODATMzqhWeyY,3393
|
|
22
22
|
coretrace_python/bundled/models/aiohttp/aiohttp_models.py,sha256=EraiXpZih8yPRk5t0OKe2TMJO9iATv4Kj_lKLp_KpfA,2946
|
|
23
23
|
coretrace_python/bundled/models/aiohttp/plugin.toml,sha256=YvU60RUBexYBtIxbXdmUhO83BoijmOe3MeQKwY8QDaM,220
|
|
24
24
|
coretrace_python/bundled/models/bottle/bottle_models.py,sha256=0yt8-7g-Pxtas3qQcIdORkWLiqJu4O3JzCrSnhzbgHs,1885
|
|
@@ -89,9 +89,9 @@ coretrace_python/dataflow/__init__.py,sha256=N5uY8VTNjEk6-ARCgDkxr6uGqQIz2NbF-U9
|
|
|
89
89
|
coretrace_python/dataflow/lattice.py,sha256=HgYGVcKVUlexvEc0yT47aQCudLxU_SWLpHhr17igkiE,1929
|
|
90
90
|
coretrace_python/dataflow/solver.py,sha256=iDsW0wujhM2DXplu2JMKC46BRsroXlNETHPEoikPeG4,3475
|
|
91
91
|
coretrace_python/dependency/__init__.py,sha256=sZdigKRbM4NkV0Dr86FDF3rFRQz35pICVX6obgO2Rxo,1065
|
|
92
|
-
coretrace_python/dependency/advisories.py,sha256=
|
|
93
|
-
coretrace_python/dependency/correlation.py,sha256=
|
|
94
|
-
coretrace_python/dependency/graph.py,sha256=
|
|
92
|
+
coretrace_python/dependency/advisories.py,sha256=9g9teb2bzkHmnR9tB86AmtLLlVPhfedP8wITFwmXEXI,8381
|
|
93
|
+
coretrace_python/dependency/correlation.py,sha256=7a-i-1HbemzIfHFvdx0GcVuZ7gyI0o-NOT7-6uYoR_w,7797
|
|
94
|
+
coretrace_python/dependency/graph.py,sha256=0w6bWq7YNb_qcF7H-S-YGClhjS7JsK9Fh1i-w1ONpBQ,13910
|
|
95
95
|
coretrace_python/dependency/policy.py,sha256=u6nUw2nX-kTuqmw_vooxN8eXZoC31ZMoMaxC9pCcwec,2317
|
|
96
96
|
coretrace_python/dependency/sbom.py,sha256=h4K3Gb9O_D6uZyfpwpC5rPcNjH_Ib-3uvFxix6OKlfs,2520
|
|
97
97
|
coretrace_python/findings/__init__.py,sha256=gnJI-uB2CkgZ8I5aFYP_N5yZb7FGNIy7LirVDh4NLwo,372
|
|
@@ -106,10 +106,10 @@ coretrace_python/frontend/parser.py,sha256=6-CG7rWxUYeOGeDP206SJWy4BMQUP9Mr6Jf-t
|
|
|
106
106
|
coretrace_python/hir/__init__.py,sha256=xMpvj-RSp6bd78a_hhkqbe99-snjERkH-_Lv5o9ASbw,180
|
|
107
107
|
coretrace_python/hir/nodes.py,sha256=4BI0f3UhKbnz9ZIxMoKXM7ujav0BmRpD0OXaoblRxIk,12364
|
|
108
108
|
coretrace_python/hir/visitors.py,sha256=JFnX09hK6trLyRcWgZ5ehOG2ywX3VvkqQ1h82nahYNg,885
|
|
109
|
-
coretrace_python/interprocedural/__init__.py,sha256=
|
|
110
|
-
coretrace_python/interprocedural/callgraph.py,sha256=
|
|
109
|
+
coretrace_python/interprocedural/__init__.py,sha256=TCX98DRbO0rXFcohwixpDZUVo_9Mdnkbn8wtlBeiu7Q,1128
|
|
110
|
+
coretrace_python/interprocedural/callgraph.py,sha256=NuXogZcdcJwRB_UYiGHGsIss0REjfiQueel7Tfo_4fI,17565
|
|
111
111
|
coretrace_python/interprocedural/modulegraph.py,sha256=4sB7OKjBYfxtm7aMtHm2jRlqFVUNUsdnLhnv9UZ1BWA,8846
|
|
112
|
-
coretrace_python/interprocedural/summaries.py,sha256=
|
|
112
|
+
coretrace_python/interprocedural/summaries.py,sha256=q--xpp1euaf5utxbK4CZtS8CSyLJ6p3MLel5Iu80X3w,20747
|
|
113
113
|
coretrace_python/ir/__init__.py,sha256=ShBPYYqoQFePP-mgBjGezQ2acel0-ReOHF9vCC-G_4Q,232
|
|
114
114
|
coretrace_python/ir/defuse.py,sha256=CZEPnlgntRp4MqwR7kLldd2ZlIlBnr-V9VJAxXxwm0c,2616
|
|
115
115
|
coretrace_python/ir/lowering.py,sha256=V7r_4tEOi801NBOsPdG6pwrRHZIeVTahygYENneLQ4U,31428
|
|
@@ -117,7 +117,7 @@ coretrace_python/ir/model.py,sha256=i7LvHEnl5pmWYUcSui17ZGS1rdPVavMpnjt7Tog7P4c,
|
|
|
117
117
|
coretrace_python/ir/printer.py,sha256=otBIhZUvB_UfB68gid_FbPcSjBUSmxH3v7vBMZZQVkg,9125
|
|
118
118
|
coretrace_python/ir/ssa.py,sha256=cIYbcv90PI6AOeMrw2ZWp88fy7E2ZO4P3tqjtPHra7g,11116
|
|
119
119
|
coretrace_python/plugins/__init__.py,sha256=xRKqkvkvQMmfQACHHoBioYhIyyK4HoMAVogy6EZG8tg,1398
|
|
120
|
-
coretrace_python/plugins/api.py,sha256=
|
|
120
|
+
coretrace_python/plugins/api.py,sha256=NjeDRRwy2K6esSEkg1Ofmv09mqa0El6MlTmRDCWiOmQ,8182
|
|
121
121
|
coretrace_python/plugins/detectors.py,sha256=ej5Z_MS0CBqrR4b9xycjTVUIo5arpArpdbr7nL732iI,5700
|
|
122
122
|
coretrace_python/plugins/loader.py,sha256=Use-lHTL4-IJYQBZmODC89zhJbYKGvDSLZHWjZa5NaI,3885
|
|
123
123
|
coretrace_python/plugins/manifest.py,sha256=F24pgQSwApRUL6ZihRSLv9wL_M03A_pZjrj9Zi4Ku3Q,3438
|
|
@@ -136,13 +136,13 @@ coretrace_python/semantic/symbols.py,sha256=3JptnnX5_KBlSd00L_wcGlYZQdnmBvSBAhfi
|
|
|
136
136
|
coretrace_python/source/__init__.py,sha256=0Y8fvtbkETnNCJR9Zq7RS5TSgF9c22pz3SSk1KdAXF0,301
|
|
137
137
|
coretrace_python/source/manager.py,sha256=JRmMOcWfS1nocbVrdcvfQTqEfIkmnb5JP857TpGywN4,2736
|
|
138
138
|
coretrace_python/source/model.py,sha256=FZnN5mlir3tzbIpWoxnn6YtRi9UcJ_y0sZM60tKr8U4,1727
|
|
139
|
-
coretrace_python/taint/__init__.py,sha256=
|
|
140
|
-
coretrace_python/taint/engine.py,sha256=
|
|
139
|
+
coretrace_python/taint/__init__.py,sha256=BuZVlVE4h7Kr873dW3PZ7miBfHQvlRRcqvEaJVvJAas,1161
|
|
140
|
+
coretrace_python/taint/engine.py,sha256=4IFD5uFXG6QhPRBM9lY87hVCpfPLTAd7oTJZbveY5ZI,38180
|
|
141
141
|
coretrace_python/taint/models.py,sha256=H-Q_nChzwqm-MEGfbfw5jMegpXlIfwUIunFiRXbE90I,12005
|
|
142
142
|
coretrace_python/taint/routes.py,sha256=UAJKXcw_sM9shf58FfDroxVO9bNSm4qrQdZknwUB0lo,6110
|
|
143
|
-
coretrace_python_analyzer-0.
|
|
144
|
-
coretrace_python_analyzer-0.
|
|
145
|
-
coretrace_python_analyzer-0.
|
|
146
|
-
coretrace_python_analyzer-0.
|
|
147
|
-
coretrace_python_analyzer-0.
|
|
148
|
-
coretrace_python_analyzer-0.
|
|
143
|
+
coretrace_python_analyzer-0.5.0.dist-info/METADATA,sha256=v8A0jSjku33W6bQ9nd7aWtBRxGh0GQziPYycy8G4Igc,3216
|
|
144
|
+
coretrace_python_analyzer-0.5.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
145
|
+
coretrace_python_analyzer-0.5.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
|
|
146
|
+
coretrace_python_analyzer-0.5.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
147
|
+
coretrace_python_analyzer-0.5.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
|
|
148
|
+
coretrace_python_analyzer-0.5.0.dist-info/RECORD,,
|
{coretrace_python_analyzer-0.4.0.dist-info → coretrace_python_analyzer-0.5.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|