coretrace-python-analyzer 0.3.0__py3-none-any.whl → 0.4.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 +16 -26
- coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +2 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +3 -0
- coretrace_python/cache.py +1 -1
- coretrace_python/dependency/__init__.py +4 -0
- coretrace_python/dependency/advisories.py +42 -5
- coretrace_python/dependency/correlation.py +70 -44
- coretrace_python/dependency/graph.py +74 -5
- coretrace_python/engine.py +17 -7
- coretrace_python/interprocedural/callgraph.py +9 -2
- coretrace_python/interprocedural/modulegraph.py +4 -1
- coretrace_python/ir/lowering.py +57 -3
- coretrace_python/plugins/__init__.py +4 -0
- coretrace_python/plugins/api.py +47 -1
- coretrace_python/plugins/loader.py +4 -4
- coretrace_python/semantic/scopes.py +3 -1
- coretrace_python/taint/engine.py +9 -2
- {coretrace_python_analyzer-0.3.0.dist-info → coretrace_python_analyzer-0.4.0.dist-info}/METADATA +1 -1
- {coretrace_python_analyzer-0.3.0.dist-info → coretrace_python_analyzer-0.4.0.dist-info}/RECORD +24 -24
- {coretrace_python_analyzer-0.3.0.dist-info → coretrace_python_analyzer-0.4.0.dist-info}/WHEEL +0 -0
- {coretrace_python_analyzer-0.3.0.dist-info → coretrace_python_analyzer-0.4.0.dist-info}/entry_points.txt +0 -0
- {coretrace_python_analyzer-0.3.0.dist-info → coretrace_python_analyzer-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {coretrace_python_analyzer-0.3.0.dist-info → coretrace_python_analyzer-0.4.0.dist-info}/licenses/NOTICE +0 -0
coretrace_python/__init__.py
CHANGED
|
@@ -7,6 +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
11
|
from coretrace_python.findings import Confidence, Finding
|
|
11
12
|
from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
|
|
12
13
|
from coretrace_python.plugins import ProjectContext, ProjectPlugin
|
|
@@ -17,12 +18,7 @@ class ReachableVulnerabilityPlugin(ProjectPlugin):
|
|
|
17
18
|
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({DependencyAnalysis, CallGraphAnalysis})
|
|
18
19
|
|
|
19
20
|
def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
|
|
20
|
-
affected =
|
|
21
|
-
for requirement in ctx.dependencies.requirements:
|
|
22
|
-
for advisory in ctx.advisories:
|
|
23
|
-
if advisory.affects(requirement):
|
|
24
|
-
for symbol in advisory.affected_symbols:
|
|
25
|
-
affected.setdefault(symbol, advisory)
|
|
21
|
+
affected = affected_symbols(ctx.dependencies, ctx.advisories)
|
|
26
22
|
if not affected:
|
|
27
23
|
return ()
|
|
28
24
|
findings: list[Finding] = []
|
|
@@ -32,25 +28,19 @@ class ReachableVulnerabilityPlugin(ProjectPlugin):
|
|
|
32
28
|
for site in graph.sites(function):
|
|
33
29
|
if not isinstance(site.target, ExternalSymbol):
|
|
34
30
|
continue
|
|
35
|
-
advisory
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
metadata={
|
|
50
|
-
"advisory": advisory.id,
|
|
51
|
-
"package": advisory.package,
|
|
52
|
-
"symbol": str(site.target.symbol),
|
|
53
|
-
},
|
|
31
|
+
for advisory in affected.get(site.target.symbol, ()):
|
|
32
|
+
findings.append(
|
|
33
|
+
Finding(
|
|
34
|
+
rule_id="reachable-vulnerability",
|
|
35
|
+
message=(
|
|
36
|
+
f"{advisory.id}: {site.target.symbol} is affected in the required "
|
|
37
|
+
f"{advisory.package} {advisory.vulnerable}: {advisory.summary}"
|
|
38
|
+
),
|
|
39
|
+
severity=advisory.severity,
|
|
40
|
+
confidence=Confidence.HIGH,
|
|
41
|
+
span=site.location,
|
|
42
|
+
function=function,
|
|
43
|
+
metadata=evidence(advisory, site.target.symbol, "reachable"),
|
|
44
|
+
)
|
|
54
45
|
)
|
|
55
|
-
)
|
|
56
46
|
return findings
|
|
@@ -28,6 +28,7 @@ class SampleAdvisories(ModelPlugin):
|
|
|
28
28
|
"yaml.load and full_load can execute arbitrary code from untrusted documents",
|
|
29
29
|
Severity.CRITICAL,
|
|
30
30
|
(_sym("yaml.load"), _sym("yaml.full_load"), _sym("yaml.unsafe_load")),
|
|
31
|
+
modules=("yaml",),
|
|
31
32
|
),
|
|
32
33
|
Advisory(
|
|
33
34
|
"CVE-2018-18074",
|
|
@@ -105,5 +106,6 @@ class SampleAdvisories(ModelPlugin):
|
|
|
105
106
|
Severity.HIGH,
|
|
106
107
|
(_sym("PIL.Image.open"),),
|
|
107
108
|
("GHSA-cfh3-3jmp-rvhc",),
|
|
109
|
+
modules=("PIL",),
|
|
108
110
|
),
|
|
109
111
|
)
|
|
@@ -17,11 +17,13 @@ class VulnerableDependencyPlugin(ProjectPlugin):
|
|
|
17
17
|
|
|
18
18
|
def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
|
|
19
19
|
findings: list[Finding] = []
|
|
20
|
+
imported = [s for module in ctx.modules for s in ctx.imports(module).all_symbols()]
|
|
20
21
|
for requirement in ctx.dependencies.requirements:
|
|
21
22
|
for advisory in ctx.advisories:
|
|
22
23
|
if not advisory.affects(requirement):
|
|
23
24
|
continue
|
|
24
25
|
pinned = requirement.pinned is not None
|
|
26
|
+
level = "imported" if advisory.imported_by(imported) else "declared"
|
|
25
27
|
findings.append(
|
|
26
28
|
Finding(
|
|
27
29
|
rule_id="vulnerable-dependency",
|
|
@@ -37,6 +39,7 @@ class VulnerableDependencyPlugin(ProjectPlugin):
|
|
|
37
39
|
"advisory": advisory.id,
|
|
38
40
|
"package": advisory.package,
|
|
39
41
|
"specifier": requirement.specifier,
|
|
42
|
+
"level": level,
|
|
40
43
|
},
|
|
41
44
|
)
|
|
42
45
|
)
|
coretrace_python/cache.py
CHANGED
|
@@ -11,6 +11,8 @@ from coretrace_python.dependency.advisories import (
|
|
|
11
11
|
from coretrace_python.dependency.graph import (
|
|
12
12
|
DEPENDENCY_FILES,
|
|
13
13
|
Advisory,
|
|
14
|
+
AdvisoryEntryPoint,
|
|
15
|
+
Condition,
|
|
14
16
|
DependencyAnalysis,
|
|
15
17
|
DependencyGraph,
|
|
16
18
|
Requirement,
|
|
@@ -26,7 +28,9 @@ __all__ = [
|
|
|
26
28
|
"DEPENDENCY_FILES",
|
|
27
29
|
"POLICY_FILE",
|
|
28
30
|
"Advisory",
|
|
31
|
+
"AdvisoryEntryPoint",
|
|
29
32
|
"AdvisoryFileError",
|
|
33
|
+
"Condition",
|
|
30
34
|
"DependencyAnalysis",
|
|
31
35
|
"DependencyGraph",
|
|
32
36
|
"Policy",
|
|
@@ -5,8 +5,8 @@ OSV dump into ``Advisory`` values, keeping the PyPI ecosystem and turning each r
|
|
|
5
5
|
events into a version specifier; ``dump_advisories`` writes them as a small JSON file
|
|
6
6
|
that a project keeps at its root as ``advisories.json`` or passes with ``--advisories``.
|
|
7
7
|
OSV records name no affected APIs, so imported advisories feed the requirement checks
|
|
8
|
-
and the SBOM; a file completed by hand with ``affected_symbols``
|
|
9
|
-
reachability and correlation checks.
|
|
8
|
+
and the SBOM; a file completed by hand with ``affected_symbols``, ``entry_points`` and
|
|
9
|
+
their ``conditions`` also feeds the reachability and correlation checks.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -17,7 +17,7 @@ from collections.abc import Iterable, Iterator, Mapping
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
|
-
from coretrace_python.dependency.graph import Advisory, normalize
|
|
20
|
+
from coretrace_python.dependency.graph import Advisory, AdvisoryEntryPoint, Condition, normalize
|
|
21
21
|
from coretrace_python.findings import Severity
|
|
22
22
|
from coretrace_python.semantic.symbols import SymbolId
|
|
23
23
|
|
|
@@ -84,8 +84,9 @@ def import_osv(records: Iterable[Mapping[str, Any]]) -> tuple[Advisory, ...]:
|
|
|
84
84
|
if str(package.get("ecosystem", "")).lower() != "pypi" or not package.get("name"):
|
|
85
85
|
continue
|
|
86
86
|
name = normalize(str(package["name"]))
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
specifiers = _specifiers(affected.get("ranges") or [])
|
|
88
|
+
if specifiers:
|
|
89
|
+
advisories.append(Advisory(identifier, name, " || ".join(specifiers), summary, severity, (), aliases))
|
|
89
90
|
return tuple(advisories)
|
|
90
91
|
|
|
91
92
|
|
|
@@ -137,6 +138,15 @@ def dump_advisories(advisories: Iterable[Advisory]) -> str:
|
|
|
137
138
|
"severity": a.severity.value,
|
|
138
139
|
"affected_symbols": [str(s) for s in a.affected_symbols],
|
|
139
140
|
"aliases": list(a.aliases),
|
|
141
|
+
"entry_points": [
|
|
142
|
+
{
|
|
143
|
+
"symbol": str(e.symbol),
|
|
144
|
+
"justification": e.justification,
|
|
145
|
+
"conditions": [_condition_entry(c) for c in e.conditions],
|
|
146
|
+
}
|
|
147
|
+
for e in a.entry_points
|
|
148
|
+
],
|
|
149
|
+
"modules": list(a.modules),
|
|
140
150
|
}
|
|
141
151
|
for a in advisories
|
|
142
152
|
],
|
|
@@ -144,6 +154,15 @@ def dump_advisories(advisories: Iterable[Advisory]) -> str:
|
|
|
144
154
|
return json.dumps(document, indent=2) + "\n"
|
|
145
155
|
|
|
146
156
|
|
|
157
|
+
def _condition_entry(condition: Condition) -> dict[str, Any]:
|
|
158
|
+
entry: dict[str, Any] = {"kind": condition.kind, "text": condition.text}
|
|
159
|
+
if condition.argument is not None:
|
|
160
|
+
entry["argument"] = condition.argument
|
|
161
|
+
if condition.values:
|
|
162
|
+
entry["values"] = list(condition.values)
|
|
163
|
+
return entry
|
|
164
|
+
|
|
165
|
+
|
|
147
166
|
def load_advisories(path: Path) -> tuple[Advisory, ...]:
|
|
148
167
|
try:
|
|
149
168
|
document = json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -165,4 +184,22 @@ def _advisory(entry: Mapping[str, Any]) -> Advisory:
|
|
|
165
184
|
Severity(entry["severity"]),
|
|
166
185
|
tuple(SymbolId(str(s)) for s in entry.get("affected_symbols") or []),
|
|
167
186
|
tuple(str(a) for a in entry.get("aliases") or []),
|
|
187
|
+
tuple(_entry_point(e) for e in entry.get("entry_points") or []),
|
|
188
|
+
tuple(str(m) for m in entry.get("modules") or []),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _entry_point(entry: Mapping[str, Any]) -> AdvisoryEntryPoint:
|
|
193
|
+
return AdvisoryEntryPoint(
|
|
194
|
+
SymbolId(str(entry["symbol"])),
|
|
195
|
+
str(entry["justification"]),
|
|
196
|
+
tuple(
|
|
197
|
+
Condition(
|
|
198
|
+
str(c["kind"]),
|
|
199
|
+
str(c["text"]),
|
|
200
|
+
None if c.get("argument") is None else str(c["argument"]),
|
|
201
|
+
tuple(str(v) for v in c.get("values") or []),
|
|
202
|
+
)
|
|
203
|
+
for c in entry.get("conditions") or []
|
|
204
|
+
),
|
|
168
205
|
)
|
|
@@ -11,77 +11,103 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from collections.abc import Iterable, Mapping
|
|
13
13
|
|
|
14
|
-
from coretrace_python.dependency.graph import Advisory, DependencyGraph
|
|
14
|
+
from coretrace_python.dependency.graph import DIRECT, Advisory, DependencyGraph
|
|
15
15
|
from coretrace_python.findings import Confidence, Finding, Severity
|
|
16
|
-
from coretrace_python.findings.refutation import Status, Verdicts
|
|
16
|
+
from coretrace_python.findings.refutation import Status, Verdict, Verdicts
|
|
17
17
|
from coretrace_python.semantic.symbols import SymbolId
|
|
18
18
|
from coretrace_python.taint import Sink, TaintFlow, TaintKind
|
|
19
19
|
|
|
20
|
+
Affected = Mapping[SymbolId, tuple[Advisory, ...]]
|
|
20
21
|
|
|
21
|
-
def affected_symbols(
|
|
22
|
-
dependencies: DependencyGraph, advisories: Iterable[Advisory]
|
|
23
|
-
) -> Mapping[SymbolId, Advisory]:
|
|
24
|
-
"""The APIs affected by advisories whose package is required in a vulnerable version."""
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
def affected_symbols(dependencies: DependencyGraph, advisories: Iterable[Advisory]) -> Affected:
|
|
24
|
+
"""The APIs affected by advisories whose package is required in a vulnerable version,
|
|
25
|
+
each with every such advisory: a call to ``yaml.load`` reaches every CVE of the
|
|
26
|
+
pinned release, not the first one listed."""
|
|
27
|
+
|
|
28
|
+
affected: dict[SymbolId, list[Advisory]] = {}
|
|
27
29
|
for requirement in dependencies.requirements:
|
|
28
30
|
for advisory in advisories:
|
|
29
31
|
if advisory.affects(requirement):
|
|
30
|
-
for symbol in advisory.
|
|
31
|
-
affected.setdefault(symbol,
|
|
32
|
-
|
|
32
|
+
for symbol in advisory.reachable_symbols:
|
|
33
|
+
found = affected.setdefault(symbol, [])
|
|
34
|
+
if advisory not in found:
|
|
35
|
+
found.append(advisory)
|
|
36
|
+
return {symbol: tuple(found) for symbol, found in affected.items()}
|
|
33
37
|
|
|
34
38
|
|
|
35
|
-
def advisory_sinks(affected:
|
|
39
|
+
def advisory_sinks(affected: Affected) -> tuple[Sink, ...]:
|
|
36
40
|
return tuple(Sink(symbol, TaintKind.ADVISORY) for symbol in affected)
|
|
37
41
|
|
|
38
42
|
|
|
43
|
+
def evidence(advisory: Advisory, symbol: SymbolId, level: str) -> dict[str, str]:
|
|
44
|
+
"""What a finding keeps of the advisory for ``symbol``: the level of evidence
|
|
45
|
+
established, how the symbol relates to the vulnerability and under which
|
|
46
|
+
conditions, the ones the engine could not check listed as pending review."""
|
|
47
|
+
|
|
48
|
+
metadata = {"advisory": advisory.id, "package": advisory.package, "symbol": str(symbol), "level": level}
|
|
49
|
+
entry = advisory.entry_point(symbol)
|
|
50
|
+
if entry is None:
|
|
51
|
+
metadata["justification"] = DIRECT
|
|
52
|
+
return metadata
|
|
53
|
+
metadata["entry_point"] = str(entry.symbol)
|
|
54
|
+
metadata["justification"] = entry.justification
|
|
55
|
+
if entry.conditions:
|
|
56
|
+
metadata["conditions"] = "; ".join(c.text for c in entry.conditions)
|
|
57
|
+
# ponytail: no condition is checked yet, so every one awaits review; argument
|
|
58
|
+
# conditions get checked at the call site once call sites carry argument symbols.
|
|
59
|
+
metadata["conditions_pending_review"] = "; ".join(c.text for c in entry.conditions)
|
|
60
|
+
return metadata
|
|
61
|
+
|
|
62
|
+
|
|
39
63
|
def correlate(
|
|
40
64
|
function: str,
|
|
41
65
|
flows: Iterable[TaintFlow],
|
|
42
66
|
verdicts: Verdicts | None,
|
|
43
|
-
affected:
|
|
67
|
+
affected: Affected,
|
|
44
68
|
) -> tuple[Finding, ...]:
|
|
45
|
-
"""Exploitable-vulnerability findings for the non-refuted ADVISORY flows of a function
|
|
69
|
+
"""Exploitable-vulnerability findings for the non-refuted ADVISORY flows of a function,
|
|
70
|
+
one per advisory the sink is affected by."""
|
|
46
71
|
|
|
47
72
|
findings: list[Finding] = []
|
|
48
73
|
for flow in flows:
|
|
49
74
|
if not flow.kinds & TaintKind.ADVISORY:
|
|
50
75
|
continue
|
|
51
|
-
advisory = affected.get(flow.sink.symbol)
|
|
52
|
-
if advisory is None:
|
|
53
|
-
continue
|
|
54
76
|
verdict = verdicts.verdict(flow) if verdicts is not None else None
|
|
55
77
|
if verdict is not None and verdict.status is Status.REFUTED:
|
|
56
78
|
continue
|
|
57
79
|
hotspot = verdict is not None and verdict.status is Status.HOTSPOT
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
f"the required {advisory.package} {advisory.vulnerable}: {advisory.summary}"
|
|
61
|
-
)
|
|
62
|
-
metadata = {
|
|
63
|
-
"advisory": advisory.id,
|
|
64
|
-
"package": advisory.package,
|
|
65
|
-
"symbol": str(flow.sink.symbol),
|
|
66
|
-
"source": str(flow.source.symbol),
|
|
67
|
-
"source_label": flow.source.label,
|
|
68
|
-
"verdict": "hotspot" if hotspot else "vulnerability",
|
|
69
|
-
}
|
|
70
|
-
if verdict is not None:
|
|
71
|
-
metadata["evidence"] = verdict.evidence
|
|
72
|
-
if flow.through is not None and flow.sink_location is not None:
|
|
73
|
-
message += f" through {flow.through}"
|
|
74
|
-
metadata["through"] = flow.through
|
|
75
|
-
metadata["sink_line"] = str(flow.sink_location.start_line)
|
|
76
|
-
findings.append(
|
|
77
|
-
Finding(
|
|
78
|
-
rule_id="exploitable-vulnerability",
|
|
79
|
-
message=message,
|
|
80
|
-
severity=Severity.CRITICAL,
|
|
81
|
-
confidence=Confidence.MEDIUM if hotspot else Confidence.HIGH,
|
|
82
|
-
span=flow.location,
|
|
83
|
-
function=function,
|
|
84
|
-
metadata=metadata,
|
|
85
|
-
)
|
|
80
|
+
findings.extend(
|
|
81
|
+
_exploitable(function, flow, advisory, verdict, hotspot) for advisory in affected.get(flow.sink.symbol, ())
|
|
86
82
|
)
|
|
87
83
|
return tuple(findings)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _exploitable(
|
|
87
|
+
function: str, flow: TaintFlow, advisory: Advisory, verdict: Verdict | None, hotspot: bool
|
|
88
|
+
) -> Finding:
|
|
89
|
+
message = (
|
|
90
|
+
f"{advisory.id}: {flow.source.label} input reaches {flow.sink.symbol}, affected in "
|
|
91
|
+
f"the required {advisory.package} {advisory.vulnerable}: {advisory.summary}"
|
|
92
|
+
)
|
|
93
|
+
metadata = {
|
|
94
|
+
**evidence(advisory, flow.sink.symbol, "exploitable"),
|
|
95
|
+
"source": str(flow.source.symbol),
|
|
96
|
+
"source_label": flow.source.label,
|
|
97
|
+
"verdict": "hotspot" if hotspot else "vulnerability",
|
|
98
|
+
}
|
|
99
|
+
if verdict is not None:
|
|
100
|
+
metadata["evidence"] = verdict.evidence
|
|
101
|
+
if flow.through is not None and flow.sink_location is not None:
|
|
102
|
+
message += f" through {flow.through}"
|
|
103
|
+
metadata["through"] = flow.through
|
|
104
|
+
metadata["sink_line"] = str(flow.sink_location.start_line)
|
|
105
|
+
return Finding(
|
|
106
|
+
rule_id="exploitable-vulnerability",
|
|
107
|
+
message=message,
|
|
108
|
+
severity=Severity.CRITICAL,
|
|
109
|
+
confidence=Confidence.MEDIUM if hotspot else Confidence.HIGH,
|
|
110
|
+
span=flow.location,
|
|
111
|
+
function=function,
|
|
112
|
+
metadata=metadata,
|
|
113
|
+
)
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
13
|
import tomllib
|
|
14
|
-
from collections.abc import Mapping
|
|
14
|
+
from collections.abc import Iterable, Mapping
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
from pathlib import PurePath
|
|
17
17
|
from types import MappingProxyType
|
|
@@ -50,6 +50,13 @@ class Version:
|
|
|
50
50
|
return self.parts + (0,) * (length - len(self.parts))
|
|
51
51
|
|
|
52
52
|
def satisfies(self, specifier: str) -> bool:
|
|
53
|
+
"""Whether this version is in ``specifier``: comma-separated clauses all hold, and
|
|
54
|
+
``||`` separates alternatives, as an advisory over several release series needs
|
|
55
|
+
(``>=4.2,<4.2.28 || >=5.2,<5.2.11``)."""
|
|
56
|
+
|
|
57
|
+
return any(self._satisfies_all(alternative) for alternative in _alternatives(specifier))
|
|
58
|
+
|
|
59
|
+
def _satisfies_all(self, specifier: str) -> bool:
|
|
53
60
|
for clause in [c.strip() for c in specifier.split(",") if c.strip()]:
|
|
54
61
|
match = _CLAUSE.match(clause)
|
|
55
62
|
if match is None:
|
|
@@ -91,12 +98,17 @@ class Version:
|
|
|
91
98
|
return True
|
|
92
99
|
|
|
93
100
|
|
|
101
|
+
def _alternatives(specifier: str) -> list[str]:
|
|
102
|
+
return [a for a in specifier.split("||") if a.strip()] or [""]
|
|
103
|
+
|
|
104
|
+
|
|
94
105
|
def _lower_bounds(specifier: str) -> list[Version]:
|
|
95
106
|
bounds: list[Version] = []
|
|
96
|
-
for
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
for alternative in _alternatives(specifier):
|
|
108
|
+
for clause in [c.strip() for c in alternative.split(",") if c.strip()]:
|
|
109
|
+
match = _CLAUSE.match(clause)
|
|
110
|
+
if match is not None and match.group(1) in (">=", ">", "~=", "^", "==", "==="):
|
|
111
|
+
bounds.append(Version.parse(match.group(2)))
|
|
100
112
|
return bounds
|
|
101
113
|
|
|
102
114
|
|
|
@@ -130,6 +142,37 @@ class Requirement:
|
|
|
130
142
|
return any(v.satisfies(self.specifier) and v.satisfies(vulnerable) for v in candidates)
|
|
131
143
|
|
|
132
144
|
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class Condition:
|
|
147
|
+
"""What must hold for an entry point to reach, or exploit, the affected code: the
|
|
148
|
+
value of an argument, the loader or algorithm used, the format decoded, the kind of
|
|
149
|
+
input, a configuration. ``kind`` says whether the engine can check it: an
|
|
150
|
+
``argument`` condition names the argument and the values that satisfy it; a
|
|
151
|
+
``semantic`` condition cannot be checked yet and is reported as pending review."""
|
|
152
|
+
|
|
153
|
+
kind: str
|
|
154
|
+
text: str
|
|
155
|
+
argument: str | None = None
|
|
156
|
+
values: tuple[str, ...] = ()
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def checkable(self) -> bool:
|
|
160
|
+
return self.kind == "argument"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class AdvisoryEntryPoint:
|
|
165
|
+
"""A public API through which a project reaches an affected symbol, justified by the
|
|
166
|
+
fixing commit or by a call path, with the conditions under which it is affected."""
|
|
167
|
+
|
|
168
|
+
symbol: SymbolId
|
|
169
|
+
justification: str
|
|
170
|
+
conditions: tuple[Condition, ...] = ()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
DIRECT = "affected symbol, changed by the fix"
|
|
174
|
+
|
|
175
|
+
|
|
133
176
|
@dataclass(frozen=True)
|
|
134
177
|
class Advisory:
|
|
135
178
|
id: str
|
|
@@ -139,10 +182,36 @@ class Advisory:
|
|
|
139
182
|
severity: Severity
|
|
140
183
|
affected_symbols: tuple[SymbolId, ...] = ()
|
|
141
184
|
aliases: tuple[str, ...] = ()
|
|
185
|
+
entry_points: tuple[AdvisoryEntryPoint, ...] = ()
|
|
186
|
+
# Top-level modules the package installs, to tell an imported package from a merely
|
|
187
|
+
# required one; defaults to the package name (``pyyaml`` installs ``yaml``: say so).
|
|
188
|
+
modules: tuple[str, ...] = ()
|
|
189
|
+
|
|
190
|
+
def __post_init__(self) -> None:
|
|
191
|
+
if not self.modules:
|
|
192
|
+
object.__setattr__(self, "modules", (normalize(self.package).replace("-", "_"),))
|
|
142
193
|
|
|
143
194
|
def affects(self, requirement: Requirement) -> bool:
|
|
144
195
|
return requirement.name == normalize(self.package) and requirement.may_match(self.vulnerable)
|
|
145
196
|
|
|
197
|
+
@property
|
|
198
|
+
def reachable_symbols(self) -> tuple[SymbolId, ...]:
|
|
199
|
+
"""Every symbol a call to which reaches the vulnerability: the entry points and
|
|
200
|
+
the affected symbols themselves."""
|
|
201
|
+
|
|
202
|
+
return (*(e.symbol for e in self.entry_points), *self.affected_symbols)
|
|
203
|
+
|
|
204
|
+
def entry_point(self, symbol: SymbolId) -> AdvisoryEntryPoint | None:
|
|
205
|
+
return next((e for e in self.entry_points if e.symbol == symbol), None)
|
|
206
|
+
|
|
207
|
+
def imported_by(self, symbols: Iterable[SymbolId]) -> bool:
|
|
208
|
+
"""Whether any of the ``symbols`` a project imports belongs to this package."""
|
|
209
|
+
|
|
210
|
+
prefixes = tuple(f"python.{m}" for m in self.modules)
|
|
211
|
+
return any(
|
|
212
|
+
s.canonical_name == p or s.canonical_name.startswith(p + ".") for s in symbols for p in prefixes
|
|
213
|
+
)
|
|
214
|
+
|
|
146
215
|
|
|
147
216
|
class DependencyGraph:
|
|
148
217
|
def __init__(self, requirements: Mapping[str, Requirement] | None = None, errors: tuple[str, ...] = ()) -> None:
|
coretrace_python/engine.py
CHANGED
|
@@ -48,7 +48,12 @@ from coretrace_python.dependency import (
|
|
|
48
48
|
load_policy,
|
|
49
49
|
parse_dependencies,
|
|
50
50
|
)
|
|
51
|
-
from coretrace_python.dependency.correlation import
|
|
51
|
+
from coretrace_python.dependency.correlation import (
|
|
52
|
+
Affected,
|
|
53
|
+
advisory_sinks,
|
|
54
|
+
affected_symbols,
|
|
55
|
+
correlate,
|
|
56
|
+
)
|
|
52
57
|
from coretrace_python.findings import (
|
|
53
58
|
FINDING_SCHEMA_VERSION,
|
|
54
59
|
Confidence,
|
|
@@ -89,6 +94,7 @@ from coretrace_python.plugins import (
|
|
|
89
94
|
PluginRegistry,
|
|
90
95
|
ProjectContext,
|
|
91
96
|
ProjectPlugin,
|
|
97
|
+
apply_refinement,
|
|
92
98
|
discover_plugins,
|
|
93
99
|
run_plugins,
|
|
94
100
|
)
|
|
@@ -460,6 +466,9 @@ def analyze_project(
|
|
|
460
466
|
for plugin in all_plugins:
|
|
461
467
|
if isinstance(plugin, ProjectPlugin):
|
|
462
468
|
findings.extend(plugin.analyze_project(context))
|
|
469
|
+
for plugin in all_plugins:
|
|
470
|
+
if isinstance(plugin, ProjectPlugin):
|
|
471
|
+
findings = list(apply_refinement(plugin, findings, plugin.refine(context, tuple(findings))))
|
|
463
472
|
kept, suppressed = partition(apply_policy(policy, findings), _text_of(sources))
|
|
464
473
|
return ProjectAnalysis(
|
|
465
474
|
graph,
|
|
@@ -477,12 +486,13 @@ def analyze_project(
|
|
|
477
486
|
def _merge_advisories(
|
|
478
487
|
from_plugins: Iterable[Advisory], from_files: Iterable[Advisory]
|
|
479
488
|
) -> tuple[Advisory, ...]:
|
|
480
|
-
"""One advisory per identifier
|
|
481
|
-
a plugin
|
|
489
|
+
"""One advisory per identifier and package: a later contributor replaces an earlier
|
|
490
|
+
one, so a curated plugin refines a bundled sample and a local file, the project's
|
|
491
|
+
own feed, wins over every plugin."""
|
|
482
492
|
|
|
483
|
-
merged: dict[tuple[str, str
|
|
493
|
+
merged: dict[tuple[str, str], Advisory] = {}
|
|
484
494
|
for advisory in (*from_plugins, *from_files):
|
|
485
|
-
merged[(advisory.id, advisory.package
|
|
495
|
+
merged[(advisory.id, advisory.package)] = advisory
|
|
486
496
|
return tuple(merged.values())
|
|
487
497
|
|
|
488
498
|
|
|
@@ -520,7 +530,7 @@ def _analyse_managers(
|
|
|
520
530
|
managers: Mapping[str, AnalysisManager],
|
|
521
531
|
seed: SummaryIndex,
|
|
522
532
|
plugins: tuple[Plugin, ...],
|
|
523
|
-
affected:
|
|
533
|
+
affected: Affected,
|
|
524
534
|
) -> dict[str, CachedModule]:
|
|
525
535
|
"""Analyse one component: iterate its summaries to a fixpoint over ``seed`` (§21),
|
|
526
536
|
then extract what the rest of the run needs from each module."""
|
|
@@ -634,7 +644,7 @@ def _configuration_key(
|
|
|
634
644
|
|
|
635
645
|
|
|
636
646
|
def _analyse_module(
|
|
637
|
-
manager: AnalysisManager, plugins: tuple[Plugin, ...], affected:
|
|
647
|
+
manager: AnalysisManager, plugins: tuple[Plugin, ...], affected: Affected
|
|
638
648
|
) -> CachedModule:
|
|
639
649
|
"""One module's findings, summaries and call sites: what the cache keeps (§11)."""
|
|
640
650
|
|
|
@@ -16,7 +16,12 @@ from typing import ClassVar
|
|
|
16
16
|
from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
|
|
17
17
|
from coretrace_python.cfg import CFGError
|
|
18
18
|
from coretrace_python.hir import nodes
|
|
19
|
-
from coretrace_python.ir.lowering import
|
|
19
|
+
from coretrace_python.ir.lowering import (
|
|
20
|
+
MODULE_BODY,
|
|
21
|
+
LoweringError,
|
|
22
|
+
analyzable_functions,
|
|
23
|
+
qualified_name,
|
|
24
|
+
)
|
|
20
25
|
from coretrace_python.ir.model import (
|
|
21
26
|
Await,
|
|
22
27
|
Call,
|
|
@@ -198,7 +203,9 @@ def resolve_targets(
|
|
|
198
203
|
for block in function.blocks:
|
|
199
204
|
for instruction in block.instructions:
|
|
200
205
|
if isinstance(instruction, MakeFunction):
|
|
201
|
-
|
|
206
|
+
# A lambda or def made by the module body is named without a prefix, as
|
|
207
|
+
# its scope's parent is the module.
|
|
208
|
+
qualified = instruction.name if function.name == MODULE_BODY else f"{function.name}.{instruction.name}"
|
|
202
209
|
if qualified in nested:
|
|
203
210
|
targets[instruction.result] = KnownFunction(qualified)
|
|
204
211
|
elif isinstance(instruction, Global):
|
|
@@ -73,7 +73,10 @@ def project_symbol(module_name: str, qualified_name: str) -> SymbolId:
|
|
|
73
73
|
symbols: invalid characters become underscores."""
|
|
74
74
|
|
|
75
75
|
module = ".".join(_component(part) for part in module_name.split("."))
|
|
76
|
-
|
|
76
|
+
# A synthetic body function is ``<module>`` or ``Cls.<body>``: valid as a name, not
|
|
77
|
+
# as a symbol component. Nothing calls a body, so its symbol only needs to be one.
|
|
78
|
+
qualified = ".".join(_component(part) for part in qualified_name.split("."))
|
|
79
|
+
return SymbolId(f"python.{module}.{qualified}")
|
|
77
80
|
|
|
78
81
|
|
|
79
82
|
def _component(part: str) -> str:
|
coretrace_python/ir/lowering.py
CHANGED
|
@@ -120,7 +120,19 @@ class _FunctionLowerer:
|
|
|
120
120
|
def resolve(self, name: str) -> Resolution:
|
|
121
121
|
if name in self.cfg.synthetic_locals:
|
|
122
122
|
return Resolution(ResolutionKind.LOCAL, self.scope.id)
|
|
123
|
-
|
|
123
|
+
resolution = self.scopes.resolve(self.scope.id, name)
|
|
124
|
+
# The module body runs as a script: the variables it assigns are its locals, so
|
|
125
|
+
# ``x = input(); os.system(x)`` connects in SSA and ``for x in ...`` has a target.
|
|
126
|
+
# Its functions, classes and imports stay globals, which is how the call graph
|
|
127
|
+
# and the symbol resolution find them.
|
|
128
|
+
if (
|
|
129
|
+
resolution.kind is ResolutionKind.GLOBAL
|
|
130
|
+
and self.scope.kind is ScopeKind.MODULE
|
|
131
|
+
and (binding := self.scope.bindings.get(name)) is not None
|
|
132
|
+
and binding.kind is BindingKind.LOCAL
|
|
133
|
+
):
|
|
134
|
+
return Resolution(ResolutionKind.LOCAL, self.scope.id)
|
|
135
|
+
return resolution
|
|
124
136
|
|
|
125
137
|
# ------------------------------------------------------------------ expressions
|
|
126
138
|
|
|
@@ -360,6 +372,9 @@ class _FunctionLowerer:
|
|
|
360
372
|
# records that the import runs here (§39 rule 3).
|
|
361
373
|
module = "." * node.level + (node.module or "") if isinstance(node, nodes.ImportFrom) else ""
|
|
362
374
|
for alias in node.names:
|
|
375
|
+
if alias.name == "*":
|
|
376
|
+
# ``from m import *`` binds through the scope's wildcards, not a name.
|
|
377
|
+
continue
|
|
363
378
|
bound = alias.as_name or alias.name.partition(".")[0]
|
|
364
379
|
symbol = self.symbols.resolve(self.scope.id, bound)
|
|
365
380
|
if symbol is None:
|
|
@@ -563,8 +578,9 @@ _FUNCTIONS_CACHE_SIZE = 32
|
|
|
563
578
|
|
|
564
579
|
|
|
565
580
|
def analyzable_functions(module: nodes.Module) -> tuple[nodes.Function, ...]:
|
|
566
|
-
"""
|
|
567
|
-
lambdas nested inside them, in source order. Computed once per
|
|
581
|
+
"""The module body, top-level functions, top-level classes' bodies and methods, and
|
|
582
|
+
the functions and lambdas nested inside them, in source order. Computed once per
|
|
583
|
+
module: the result is
|
|
568
584
|
memoised for the last few modules seen, keyed by identity, so every consumer shares
|
|
569
585
|
one tuple and the synthesized lambda functions it holds."""
|
|
570
586
|
|
|
@@ -581,16 +597,54 @@ def analyzable_functions(module: nodes.Module) -> tuple[nodes.Function, ...]:
|
|
|
581
597
|
|
|
582
598
|
def _analyzable_functions(module: nodes.Module) -> tuple[nodes.Function, ...]:
|
|
583
599
|
functions: list[nodes.Function] = []
|
|
600
|
+
body = module_function(module)
|
|
601
|
+
if body is not None:
|
|
602
|
+
_collect(body, functions)
|
|
584
603
|
for statement in module.body:
|
|
585
604
|
if isinstance(statement, nodes.Function):
|
|
586
605
|
_collect(statement, functions)
|
|
587
606
|
elif isinstance(statement, nodes.Class):
|
|
607
|
+
body = class_body_function(statement)
|
|
608
|
+
if body is not None:
|
|
609
|
+
_collect(body, functions)
|
|
588
610
|
for member in statement.body:
|
|
589
611
|
if isinstance(member, nodes.Function):
|
|
590
612
|
_collect(member, functions)
|
|
591
613
|
return tuple(functions)
|
|
592
614
|
|
|
593
615
|
|
|
616
|
+
MODULE_BODY = "<module>"
|
|
617
|
+
CLASS_BODY = ".<body>"
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def module_function(module: nodes.Module) -> nodes.Function | None:
|
|
621
|
+
"""The module's statements outside its definitions, as a function named ``<module>``
|
|
622
|
+
with the module's span, so its names resolve in the module scope; ``None`` when the
|
|
623
|
+
module only defines things."""
|
|
624
|
+
|
|
625
|
+
statements = tuple(s for s in module.body if not isinstance(s, nodes.Function | nodes.Class))
|
|
626
|
+
if not statements:
|
|
627
|
+
return None
|
|
628
|
+
return nodes.Function(MODULE_BODY, (), statements, False, module.span)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def class_body_function(cls: nodes.Class) -> nodes.Function | None:
|
|
632
|
+
"""A top-level class's statements outside its methods (``field = Field()``), as a
|
|
633
|
+
function named ``Cls.<body>`` with the class's span, so its names resolve in the
|
|
634
|
+
class scope."""
|
|
635
|
+
|
|
636
|
+
statements = tuple(s for s in cls.body if not isinstance(s, nodes.Function))
|
|
637
|
+
if not statements:
|
|
638
|
+
return None
|
|
639
|
+
return nodes.Function(f"{cls.name}{CLASS_BODY}", (), statements, False, cls.span)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def is_body_function(function: nodes.Function) -> bool:
|
|
643
|
+
"""Whether ``function`` is a synthetic module or class body rather than a definition."""
|
|
644
|
+
|
|
645
|
+
return function.name == MODULE_BODY or function.name.endswith(CLASS_BODY)
|
|
646
|
+
|
|
647
|
+
|
|
594
648
|
def _collect(function: nodes.Function, into: list[nodes.Function]) -> None:
|
|
595
649
|
into.append(function)
|
|
596
650
|
|
|
@@ -7,6 +7,8 @@ from coretrace_python.plugins.api import (
|
|
|
7
7
|
PluginContext,
|
|
8
8
|
ProjectContext,
|
|
9
9
|
ProjectPlugin,
|
|
10
|
+
RefinementError,
|
|
11
|
+
apply_refinement,
|
|
10
12
|
run_plugins,
|
|
11
13
|
)
|
|
12
14
|
from coretrace_python.plugins.detectors import SymbolCallDetector, TaintDetector
|
|
@@ -44,11 +46,13 @@ __all__ = [
|
|
|
44
46
|
"PluginRegistry",
|
|
45
47
|
"ProjectContext",
|
|
46
48
|
"ProjectPlugin",
|
|
49
|
+
"RefinementError",
|
|
47
50
|
"SecretDetector",
|
|
48
51
|
"SecretPattern",
|
|
49
52
|
"SymbolCallDetector",
|
|
50
53
|
"TaintDetector",
|
|
51
54
|
"VersionRange",
|
|
55
|
+
"apply_refinement",
|
|
52
56
|
"discover_plugins",
|
|
53
57
|
"literals",
|
|
54
58
|
"load_manifest",
|
coretrace_python/plugins/api.py
CHANGED
|
@@ -8,6 +8,7 @@ room for out-of-process isolation later.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import dataclasses
|
|
11
12
|
from abc import ABC, abstractmethod
|
|
12
13
|
from collections.abc import Iterable, Mapping, Sequence
|
|
13
14
|
from pathlib import Path
|
|
@@ -99,6 +100,50 @@ class ProjectPlugin(Plugin):
|
|
|
99
100
|
def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
|
|
100
101
|
raise NotImplementedError
|
|
101
102
|
|
|
103
|
+
def refine(self, ctx: ProjectContext, findings: tuple[Finding, ...]) -> Sequence[Finding]:
|
|
104
|
+
"""Reclassify the findings of the run, once every plugin has reported.
|
|
105
|
+
|
|
106
|
+
A refinement returns the same findings in the same order and may change only
|
|
107
|
+
their severity, their confidence and add metadata: what a finding is, where it
|
|
108
|
+
is and the evidence behind it are not the refiner's to touch. The engine records
|
|
109
|
+
who reclassified a finding and what it was before, and rejects anything else."""
|
|
110
|
+
|
|
111
|
+
return findings
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class RefinementError(Exception):
|
|
115
|
+
"""A refinement did more than reclassify a finding."""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
REFINABLE = ("severity", "confidence")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def apply_refinement(plugin: Plugin, original: Sequence[Finding], refined: Sequence[Finding]) -> tuple[Finding, ...]:
|
|
122
|
+
"""The refined findings, checked against the contract of ``ProjectPlugin.refine`` and
|
|
123
|
+
stamped with a trace of the reclassification."""
|
|
124
|
+
|
|
125
|
+
if len(refined) != len(original):
|
|
126
|
+
raise RefinementError(f"plugin {plugin.name!r} returned {len(refined)} findings for {len(original)}")
|
|
127
|
+
kept: list[Finding] = []
|
|
128
|
+
for before, after in zip(original, refined, strict=True):
|
|
129
|
+
for attribute in ("rule_id", "message", "span", "function"):
|
|
130
|
+
if getattr(before, attribute) != getattr(after, attribute):
|
|
131
|
+
raise RefinementError(f"plugin {plugin.name!r} changed the {attribute} of a finding")
|
|
132
|
+
lost = [k for k, v in before.metadata.items() if after.metadata.get(k) != v]
|
|
133
|
+
if lost:
|
|
134
|
+
raise RefinementError(f"plugin {plugin.name!r} changed the evidence {lost} of a finding")
|
|
135
|
+
changed = [a for a in REFINABLE if getattr(before, a) != getattr(after, a)]
|
|
136
|
+
if not changed:
|
|
137
|
+
kept.append(after)
|
|
138
|
+
continue
|
|
139
|
+
trace = dict(after.metadata)
|
|
140
|
+
for attribute in changed:
|
|
141
|
+
trace.setdefault(f"original_{attribute}", getattr(before, attribute).value)
|
|
142
|
+
refiners = trace.get("refined_by", "")
|
|
143
|
+
trace["refined_by"] = f"{refiners},{plugin.name}" if refiners else plugin.name
|
|
144
|
+
kept.append(dataclasses.replace(after, metadata=trace))
|
|
145
|
+
return tuple(kept)
|
|
146
|
+
|
|
102
147
|
|
|
103
148
|
class PluginContext:
|
|
104
149
|
"""What one plugin may see while analysing one module."""
|
|
@@ -118,7 +163,8 @@ class PluginContext:
|
|
|
118
163
|
return self._manager.module
|
|
119
164
|
|
|
120
165
|
def functions(self) -> tuple[nodes.Function, ...]:
|
|
121
|
-
"""
|
|
166
|
+
"""The functions the engine analyses: the module body (``<module>``), top-level
|
|
167
|
+
functions, class bodies (``Cls.<body>``) and methods."""
|
|
122
168
|
|
|
123
169
|
return self._functions
|
|
124
170
|
|
|
@@ -67,8 +67,9 @@ def _import_entrypoint(directory: Path, manifest: PluginManifest) -> type[Plugin
|
|
|
67
67
|
"""Import the entrypoint, a module ``<name>.py`` or a package ``<name>/__init__.py``.
|
|
68
68
|
|
|
69
69
|
The module name derives from the path, so two plugins may both call their module
|
|
70
|
-
``models
|
|
71
|
-
runs
|
|
70
|
+
``models``. The module is registered in ``sys.modules`` under that name before it
|
|
71
|
+
runs: a package's relative imports resolve through it, and so does ``dataclasses``
|
|
72
|
+
when it evaluates postponed annotations."""
|
|
72
73
|
|
|
73
74
|
package_path = directory / manifest.entrypoint.module / "__init__.py"
|
|
74
75
|
is_package = package_path.is_file()
|
|
@@ -87,8 +88,7 @@ def _import_entrypoint(directory: Path, manifest: PluginManifest) -> type[Plugin
|
|
|
87
88
|
)
|
|
88
89
|
assert spec is not None and spec.loader is not None
|
|
89
90
|
module = importlib.util.module_from_spec(spec)
|
|
90
|
-
|
|
91
|
-
sys.modules[name] = module
|
|
91
|
+
sys.modules[name] = module
|
|
92
92
|
spec.loader.exec_module(module)
|
|
93
93
|
|
|
94
94
|
candidate = getattr(module, manifest.entrypoint.class_name, None)
|
|
@@ -218,7 +218,9 @@ class _Collector:
|
|
|
218
218
|
ScopeId(str(module.span.source_id)), ScopeKind.MODULE, "<module>", None, module.span
|
|
219
219
|
)
|
|
220
220
|
self.builders: list[_ScopeBuilder] = [self.module]
|
|
221
|
-
|
|
221
|
+
# The module scope is found by the module's span, as the synthetic ``<module>``
|
|
222
|
+
# function carries it.
|
|
223
|
+
self.spans: dict[SourceSpan, ScopeId] = {module.span: self.module.id}
|
|
222
224
|
self.body(module.body, self.module)
|
|
223
225
|
|
|
224
226
|
def body(self, statements: tuple[nodes.Statement, ...], scope: _ScopeBuilder) -> None:
|
coretrace_python/taint/engine.py
CHANGED
|
@@ -38,7 +38,7 @@ from coretrace_python.interprocedural import (
|
|
|
38
38
|
SummaryTable,
|
|
39
39
|
project_symbol,
|
|
40
40
|
)
|
|
41
|
-
from coretrace_python.ir.lowering import analyzable_functions
|
|
41
|
+
from coretrace_python.ir.lowering import analyzable_functions, is_body_function
|
|
42
42
|
from coretrace_python.ir.model import (
|
|
43
43
|
BasicBlock,
|
|
44
44
|
Branch,
|
|
@@ -593,7 +593,12 @@ def local_instances(function: nodes.Function, scopes: ScopeTable, symbols: Symbo
|
|
|
593
593
|
|
|
594
594
|
|
|
595
595
|
def _enclosing(module: nodes.Module, function: nodes.Function) -> nodes.Function | None:
|
|
596
|
-
"""The function whose body defines ``function``, if it is nested.
|
|
596
|
+
"""The function whose body defines ``function``, if it is nested. A module or class
|
|
597
|
+
body is nested in nothing, and encloses nothing: a lambda at module level reads
|
|
598
|
+
globals, not the locals of a function."""
|
|
599
|
+
|
|
600
|
+
if is_body_function(function):
|
|
601
|
+
return None
|
|
597
602
|
|
|
598
603
|
def search(
|
|
599
604
|
body: tuple[nodes.Statement, ...], parent: nodes.Function | None
|
|
@@ -618,6 +623,8 @@ def _enclosing(module: nodes.Module, function: nodes.Function) -> nodes.Function
|
|
|
618
623
|
# analysable function whose span contains theirs.
|
|
619
624
|
innermost: nodes.Function | None = None
|
|
620
625
|
for candidate in analyzable_functions(module):
|
|
626
|
+
if is_body_function(candidate):
|
|
627
|
+
continue
|
|
621
628
|
if candidate.span != function.span and _contains(candidate.span, function.span):
|
|
622
629
|
innermost = candidate
|
|
623
630
|
return innermost
|
{coretrace_python_analyzer-0.3.0.dist-info → coretrace_python_analyzer-0.4.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.4.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.3.0.dist-info → coretrace_python_analyzer-0.4.0.dist-info}/RECORD
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
coretrace_python/__init__.py,sha256=
|
|
1
|
+
coretrace_python/__init__.py,sha256=uJFWBDpap961PWbZ01zYoAZ0LTF4lnI0UH11pEl6GEw,75
|
|
2
2
|
coretrace_python/__main__.py,sha256=V8DnYtRboISK2VCKm7DGHLREKvqLbqApyWlKBOs26wU,65
|
|
3
|
-
coretrace_python/cache.py,sha256=
|
|
3
|
+
coretrace_python/cache.py,sha256=Aj6o-PvOHoTjLRuD-rJqYZv-fnPnp7NmbNFmfWssOvk,11316
|
|
4
4
|
coretrace_python/cli.py,sha256=ZE6F8wqNcE-ogfldii-UgUq4nOcEMnHuMCzz0l--yuw,9792
|
|
5
|
-
coretrace_python/engine.py,sha256=
|
|
5
|
+
coretrace_python/engine.py,sha256=FGMR22fXmBnt_iafkM7-xSh9vYhw15OPKGvTTJu-xiQ,27403
|
|
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=nPwOlbebAhMNmk4BHldNe-UMrsK6qUUs1bL4z8fObvY,2186
|
|
18
18
|
coretrace_python/bundled/dependency/sample_advisories/plugin.toml,sha256=cME4pSFzkFsxmIJGGBSrmXqynAd0dg34X-rgEoZ_u6E,184
|
|
19
|
-
coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py,sha256=
|
|
19
|
+
coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py,sha256=jw98GK6hh5RAx05t8QPcUzsFMfGigAL8418vZLprKlc,3710
|
|
20
20
|
coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml,sha256=FbmiXLEqtPUZEsDc5q_UqCzn6AecL5HCSdGVy7_2dy0,238
|
|
21
|
-
coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py,sha256=
|
|
21
|
+
coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py,sha256=ruwruejQ_NqYy7osxfaiPFRlus1DsMEluJ2obhxATkM,2092
|
|
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
|
|
@@ -88,10 +88,10 @@ coretrace_python/cfg/model.py,sha256=6EJiXFAGrsMgCdyxMJ6TClY8tQhJF6Hq2bX802Gc0yg
|
|
|
88
88
|
coretrace_python/dataflow/__init__.py,sha256=N5uY8VTNjEk6-ARCgDkxr6uGqQIz2NbF-U9sC_v_lfA,547
|
|
89
89
|
coretrace_python/dataflow/lattice.py,sha256=HgYGVcKVUlexvEc0yT47aQCudLxU_SWLpHhr17igkiE,1929
|
|
90
90
|
coretrace_python/dataflow/solver.py,sha256=iDsW0wujhM2DXplu2JMKC46BRsroXlNETHPEoikPeG4,3475
|
|
91
|
-
coretrace_python/dependency/__init__.py,sha256=
|
|
92
|
-
coretrace_python/dependency/advisories.py,sha256=
|
|
93
|
-
coretrace_python/dependency/correlation.py,sha256=
|
|
94
|
-
coretrace_python/dependency/graph.py,sha256=
|
|
91
|
+
coretrace_python/dependency/__init__.py,sha256=sZdigKRbM4NkV0Dr86FDF3rFRQz35pICVX6obgO2Rxo,1065
|
|
92
|
+
coretrace_python/dependency/advisories.py,sha256=RMEQ_9bvZ7OThjBncM5CbKzsmySPvQYuNMZqQVcrVnc,7765
|
|
93
|
+
coretrace_python/dependency/correlation.py,sha256=nyysSoXPz9jg_X8zeweybDpJRXLmj4eH0Z0_WQw5rd0,4916
|
|
94
|
+
coretrace_python/dependency/graph.py,sha256=Z0_u4v0hAYMCAyXEWMh7NwcG2FesIHsXUd3Fl8C1MF8,13617
|
|
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
|
|
@@ -107,19 +107,19 @@ coretrace_python/hir/__init__.py,sha256=xMpvj-RSp6bd78a_hhkqbe99-snjERkH-_Lv5o9A
|
|
|
107
107
|
coretrace_python/hir/nodes.py,sha256=4BI0f3UhKbnz9ZIxMoKXM7ujav0BmRpD0OXaoblRxIk,12364
|
|
108
108
|
coretrace_python/hir/visitors.py,sha256=JFnX09hK6trLyRcWgZ5ehOG2ywX3VvkqQ1h82nahYNg,885
|
|
109
109
|
coretrace_python/interprocedural/__init__.py,sha256=57nLUicdsnX0uIub--EUcLt3TRazzZxy2LQ8TRwlOS0,1054
|
|
110
|
-
coretrace_python/interprocedural/callgraph.py,sha256=
|
|
111
|
-
coretrace_python/interprocedural/modulegraph.py,sha256=
|
|
110
|
+
coretrace_python/interprocedural/callgraph.py,sha256=5YJ3W5JsKf-uSHkwDvSPBRS1X1ZTyjPaz-wJ8aJV3zE,15232
|
|
111
|
+
coretrace_python/interprocedural/modulegraph.py,sha256=4sB7OKjBYfxtm7aMtHm2jRlqFVUNUsdnLhnv9UZ1BWA,8846
|
|
112
112
|
coretrace_python/interprocedural/summaries.py,sha256=6JFrTaMkG34g_nLhfmckjxKWvexCGvXuXozLliw6xQ8,20427
|
|
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
|
-
coretrace_python/ir/lowering.py,sha256=
|
|
115
|
+
coretrace_python/ir/lowering.py,sha256=V7r_4tEOi801NBOsPdG6pwrRHZIeVTahygYENneLQ4U,31428
|
|
116
116
|
coretrace_python/ir/model.py,sha256=i7LvHEnl5pmWYUcSui17ZGS1rdPVavMpnjt7Tog7P4c,11125
|
|
117
117
|
coretrace_python/ir/printer.py,sha256=otBIhZUvB_UfB68gid_FbPcSjBUSmxH3v7vBMZZQVkg,9125
|
|
118
118
|
coretrace_python/ir/ssa.py,sha256=cIYbcv90PI6AOeMrw2ZWp88fy7E2ZO4P3tqjtPHra7g,11116
|
|
119
|
-
coretrace_python/plugins/__init__.py,sha256=
|
|
120
|
-
coretrace_python/plugins/api.py,sha256=
|
|
119
|
+
coretrace_python/plugins/__init__.py,sha256=xRKqkvkvQMmfQACHHoBioYhIyyK4HoMAVogy6EZG8tg,1398
|
|
120
|
+
coretrace_python/plugins/api.py,sha256=Fjjy3FjnkS9OoQ55eLQZ-YJgykd4TgKNAYXySb0uSXY,7588
|
|
121
121
|
coretrace_python/plugins/detectors.py,sha256=ej5Z_MS0CBqrR4b9xycjTVUIo5arpArpdbr7nL732iI,5700
|
|
122
|
-
coretrace_python/plugins/loader.py,sha256=
|
|
122
|
+
coretrace_python/plugins/loader.py,sha256=Use-lHTL4-IJYQBZmODC89zhJbYKGvDSLZHWjZa5NaI,3885
|
|
123
123
|
coretrace_python/plugins/manifest.py,sha256=F24pgQSwApRUL6ZihRSLv9wL_M03A_pZjrj9Zi4Ku3Q,3438
|
|
124
124
|
coretrace_python/plugins/registry.py,sha256=ixKcT2YDky96TWCFckdzKnGRmFn1dbBVMSZLPvdxvY8,1177
|
|
125
125
|
coretrace_python/plugins/secrets.py,sha256=fpCxXpz3u7a-p1mAabC99FK_DpC1RSg62lLPe96pGy8,15162
|
|
@@ -131,18 +131,18 @@ coretrace_python/reporters/text.py,sha256=bHsHkMheyL0AB1iWtxBO5H9S14k1AqJvKkNsFr
|
|
|
131
131
|
coretrace_python/semantic/__init__.py,sha256=_tjT9CgXfPbOMs8EFguhWrLeSmpyrqOHU_OSoQPiWYs,411
|
|
132
132
|
coretrace_python/semantic/identity.py,sha256=uQBdqEXrOtYmGIiR9hZu0Pimv52iW1rOOOvIdG0InNY,1472
|
|
133
133
|
coretrace_python/semantic/imports.py,sha256=389MZs8iTUc3CyUHG0VAgWFsypd7VPFKkLHmFz9L-N0,5673
|
|
134
|
-
coretrace_python/semantic/scopes.py,sha256=
|
|
134
|
+
coretrace_python/semantic/scopes.py,sha256=zg8ClITSAUbOMvLOV5BsfdG1oC4cd0VweUb9S5Jc4js,20241
|
|
135
135
|
coretrace_python/semantic/symbols.py,sha256=3JptnnX5_KBlSd00L_wcGlYZQdnmBvSBAhfivBF6DLo,3513
|
|
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
139
|
coretrace_python/taint/__init__.py,sha256=D-NH3kx9wKTNeWASewawGAx7ChOkGMGhmjf-E4crHl0,1111
|
|
140
|
-
coretrace_python/taint/engine.py,sha256=
|
|
140
|
+
coretrace_python/taint/engine.py,sha256=tj28lfqWZSyddXsJ2c0Q7zbVSmk8ceZ-FccZDoBuhd4,35605
|
|
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.4.0.dist-info/METADATA,sha256=4CTPKMus5dgilu4I3lpROK-gLTL9HiQC5P75aZu8Kj8,3216
|
|
144
|
+
coretrace_python_analyzer-0.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
145
|
+
coretrace_python_analyzer-0.4.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
|
|
146
|
+
coretrace_python_analyzer-0.4.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
147
|
+
coretrace_python_analyzer-0.4.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
|
|
148
|
+
coretrace_python_analyzer-0.4.0.dist-info/RECORD,,
|
{coretrace_python_analyzer-0.3.0.dist-info → coretrace_python_analyzer-0.4.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|