coretrace-python-analyzer 0.5.0__py3-none-any.whl → 0.6.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 +52 -19
- coretrace_python/bundled/models/python_stdlib/python_stdlib.py +25 -1
- coretrace_python/cache.py +14 -4
- coretrace_python/cli.py +33 -1
- coretrace_python/dependency/__init__.py +2 -0
- coretrace_python/dependency/advisories.py +16 -8
- coretrace_python/dependency/correlation.py +1 -13
- coretrace_python/dependency/graph.py +76 -9
- coretrace_python/dependency/policy.py +6 -2
- coretrace_python/dependency/vex.py +182 -0
- coretrace_python/engine.py +17 -2
- coretrace_python/interprocedural/__init__.py +2 -0
- coretrace_python/interprocedural/callgraph.py +50 -2
- coretrace_python/taint/__init__.py +2 -0
- coretrace_python/taint/engine.py +16 -3
- coretrace_python/taint/models.py +24 -0
- {coretrace_python_analyzer-0.5.0.dist-info → coretrace_python_analyzer-0.6.0.dist-info}/METADATA +1 -1
- {coretrace_python_analyzer-0.5.0.dist-info → coretrace_python_analyzer-0.6.0.dist-info}/RECORD +23 -22
- {coretrace_python_analyzer-0.5.0.dist-info → coretrace_python_analyzer-0.6.0.dist-info}/WHEEL +0 -0
- {coretrace_python_analyzer-0.5.0.dist-info → coretrace_python_analyzer-0.6.0.dist-info}/entry_points.txt +0 -0
- {coretrace_python_analyzer-0.5.0.dist-info → coretrace_python_analyzer-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {coretrace_python_analyzer-0.5.0.dist-info → coretrace_python_analyzer-0.6.0.dist-info}/licenses/NOTICE +0 -0
coretrace_python/__init__.py
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
"""Calls, anywhere in the project, to an API affected by a vulnerable requirement
|
|
1
|
+
"""Calls, anywhere in the project, to an API affected by a vulnerable requirement, and
|
|
2
|
+
reads of an affected attribute whose getter runs the vulnerable code."""
|
|
2
3
|
|
|
3
4
|
from __future__ import annotations
|
|
4
5
|
|
|
5
|
-
from collections.abc import Sequence
|
|
6
|
+
from collections.abc import Iterator, Sequence
|
|
6
7
|
from typing import ClassVar
|
|
7
8
|
|
|
8
9
|
from coretrace_python.analysis import AnyAnalysis
|
|
9
|
-
from coretrace_python.dependency import DependencyAnalysis
|
|
10
|
-
from coretrace_python.dependency.correlation import
|
|
10
|
+
from coretrace_python.dependency import Advisory, DependencyAnalysis
|
|
11
|
+
from coretrace_python.dependency.correlation import (
|
|
12
|
+
ConditionCheck,
|
|
13
|
+
affected_symbols,
|
|
14
|
+
check_conditions,
|
|
15
|
+
evidence,
|
|
16
|
+
)
|
|
11
17
|
from coretrace_python.findings import Confidence, Finding
|
|
12
18
|
from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
|
|
13
19
|
from coretrace_python.plugins import ProjectContext, ProjectPlugin
|
|
20
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
21
|
+
from coretrace_python.source import SourceSpan
|
|
14
22
|
|
|
15
23
|
|
|
16
24
|
class ReachableVulnerabilityPlugin(ProjectPlugin):
|
|
@@ -29,21 +37,46 @@ class ReachableVulnerabilityPlugin(ProjectPlugin):
|
|
|
29
37
|
if not isinstance(site.target, ExternalSymbol):
|
|
30
38
|
continue
|
|
31
39
|
for advisory in affected.get(site.target.symbol, ()):
|
|
32
|
-
|
|
40
|
+
entry = advisory.entry_point(site.target.symbol)
|
|
41
|
+
if entry is not None and entry.read:
|
|
42
|
+
continue # a call reads its callee first: reported with the reads
|
|
43
|
+
check = check_conditions(entry, site.arguments)
|
|
33
44
|
if check.contradicted is not None:
|
|
34
45
|
continue
|
|
35
|
-
findings.append(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
48
|
-
)
|
|
46
|
+
findings.append(_reached(advisory, site.target.symbol, site.location, function, check))
|
|
47
|
+
# Reading ``request.form.get`` reads ``request.form``: one finding per entry
|
|
48
|
+
# point and function, where the function first reads it.
|
|
49
|
+
reported: set[tuple[Advisory, SymbolId]] = set()
|
|
50
|
+
for read in graph.reads(function):
|
|
51
|
+
for symbol in _read_through(read.symbol):
|
|
52
|
+
for advisory in affected.get(symbol, ()):
|
|
53
|
+
entry = advisory.entry_point(symbol)
|
|
54
|
+
if entry is None or not entry.read or (advisory, symbol) in reported:
|
|
55
|
+
continue
|
|
56
|
+
reported.add((advisory, symbol))
|
|
57
|
+
check = check_conditions(entry, None)
|
|
58
|
+
findings.append(_reached(advisory, symbol, read.location, function, check))
|
|
49
59
|
return findings
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _read_through(symbol: SymbolId) -> Iterator[SymbolId]:
|
|
63
|
+
"""``symbol`` and every symbol it is an attribute of, as reading it reads them all."""
|
|
64
|
+
|
|
65
|
+
components = symbol.canonical_name.split(".")
|
|
66
|
+
for end in range(len(components), 1, -1):
|
|
67
|
+
yield SymbolId(".".join(components[:end]))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _reached(advisory: Advisory, symbol: SymbolId, span: SourceSpan, function: str, check: ConditionCheck) -> Finding:
|
|
71
|
+
return Finding(
|
|
72
|
+
rule_id="reachable-vulnerability",
|
|
73
|
+
message=(
|
|
74
|
+
f"{advisory.id}: {symbol} is affected in the required "
|
|
75
|
+
f"{advisory.package} {advisory.vulnerable}: {advisory.summary}"
|
|
76
|
+
),
|
|
77
|
+
severity=advisory.severity,
|
|
78
|
+
confidence=Confidence.HIGH,
|
|
79
|
+
span=span,
|
|
80
|
+
function=function,
|
|
81
|
+
metadata=evidence(advisory, symbol, "reachable", check),
|
|
82
|
+
)
|
|
@@ -6,9 +6,28 @@ from typing import ClassVar
|
|
|
6
6
|
|
|
7
7
|
from coretrace_python.plugins import ModelPlugin
|
|
8
8
|
from coretrace_python.semantic.symbols import SymbolId
|
|
9
|
-
from coretrace_python.taint import
|
|
9
|
+
from coretrace_python.taint import (
|
|
10
|
+
Model,
|
|
11
|
+
SafeArgument,
|
|
12
|
+
Sanitizer,
|
|
13
|
+
Sink,
|
|
14
|
+
Source,
|
|
15
|
+
TaintKind,
|
|
16
|
+
Validator,
|
|
17
|
+
)
|
|
10
18
|
|
|
11
19
|
_ENVIRONMENT_KINDS = TaintKind.ALL & ~(TaintKind.COMMAND | TaintKind.PATH)
|
|
20
|
+
# SafeLoader and BaseLoader build plain data only, in Python and in C, under every spelling.
|
|
21
|
+
_SAFE_YAML_LOADERS = (
|
|
22
|
+
"python.yaml.SafeLoader",
|
|
23
|
+
"python.yaml.loader.SafeLoader",
|
|
24
|
+
"python.yaml.BaseLoader",
|
|
25
|
+
"python.yaml.loader.BaseLoader",
|
|
26
|
+
"python.yaml.CSafeLoader",
|
|
27
|
+
"python.yaml.cyaml.CSafeLoader",
|
|
28
|
+
"python.yaml.CBaseLoader",
|
|
29
|
+
"python.yaml.cyaml.CBaseLoader",
|
|
30
|
+
)
|
|
12
31
|
_PROCESS_OUTPUT_KINDS = TaintKind.ALL & ~TaintKind.PATH
|
|
13
32
|
|
|
14
33
|
|
|
@@ -66,6 +85,11 @@ class PythonStdlibModels(ModelPlugin):
|
|
|
66
85
|
Sink(_sym("dill.loads"), TaintKind.DESERIALIZATION),
|
|
67
86
|
Sink(_sym("jsonpickle.decode"), TaintKind.DESERIALIZATION),
|
|
68
87
|
Sink(_sym("yaml.load"), TaintKind.DESERIALIZATION),
|
|
88
|
+
Sink(_sym("yaml.load_all"), TaintKind.DESERIALIZATION),
|
|
89
|
+
Sink(_sym("yaml.full_load_all"), TaintKind.DESERIALIZATION),
|
|
90
|
+
Sink(_sym("yaml.unsafe_load_all"), TaintKind.DESERIALIZATION),
|
|
91
|
+
SafeArgument(_sym("yaml.load"), "Loader", _SAFE_YAML_LOADERS, position=1, kinds=TaintKind.DESERIALIZATION),
|
|
92
|
+
SafeArgument(_sym("yaml.load_all"), "Loader", _SAFE_YAML_LOADERS, position=1, kinds=TaintKind.DESERIALIZATION),
|
|
69
93
|
Sink(_sym("yaml.unsafe_load"), TaintKind.DESERIALIZATION),
|
|
70
94
|
Sink(_sym("yaml.full_load"), TaintKind.DESERIALIZATION),
|
|
71
95
|
Sanitizer(_sym("os.path.basename"), TaintKind.PATH),
|
coretrace_python/cache.py
CHANGED
|
@@ -4,9 +4,10 @@ A module's results are stored under a key derived from everything they depend on
|
|
|
4
4
|
source text and identity, the engine, schema and plugin API versions, the plugins and
|
|
5
5
|
their code, the security models, the advisories, the dependency graph, and the keys of
|
|
6
6
|
the project modules it imports transitively. A module whose key is unchanged on a later
|
|
7
|
-
run is served from the cache: its summaries seed the project index, its call sites
|
|
8
|
-
the project plugins and its findings are reported as they were.
|
|
9
|
-
tampered or foreign file can never execute anything; an
|
|
7
|
+
run is served from the cache: its summaries seed the project index, its call sites and
|
|
8
|
+
symbol reads serve the project plugins and its findings are reported as they were.
|
|
9
|
+
Entries are JSON, so a tampered or foreign file can never execute anything; an
|
|
10
|
+
unreadable entry is a miss.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from __future__ import annotations
|
|
@@ -33,13 +34,14 @@ from coretrace_python.interprocedural import (
|
|
|
33
34
|
Mutation,
|
|
34
35
|
NonlocalWrite,
|
|
35
36
|
SummaryIndex,
|
|
37
|
+
SymbolRead,
|
|
36
38
|
Target,
|
|
37
39
|
UnknownTarget,
|
|
38
40
|
)
|
|
39
41
|
from coretrace_python.semantic.symbols import SymbolId
|
|
40
42
|
from coretrace_python.source import SourceId, SourceSpan
|
|
41
43
|
|
|
42
|
-
CACHE_FORMAT =
|
|
44
|
+
CACHE_FORMAT = 8
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
@dataclass(frozen=True)
|
|
@@ -50,6 +52,7 @@ class CachedModule:
|
|
|
50
52
|
summaries: Mapping[str, FunctionSummary]
|
|
51
53
|
sites: tuple[CallSite, ...]
|
|
52
54
|
findings: tuple[Finding, ...]
|
|
55
|
+
reads: tuple[SymbolRead, ...] = ()
|
|
53
56
|
|
|
54
57
|
def __post_init__(self) -> None:
|
|
55
58
|
object.__setattr__(self, "summaries", MappingProxyType(dict(self.summaries)))
|
|
@@ -134,6 +137,7 @@ def encode(module: CachedModule) -> dict[str, Any]:
|
|
|
134
137
|
"summaries": {name: _encode_summary(s) for name, s in module.summaries.items()},
|
|
135
138
|
"sites": [_encode_site(site) for site in module.sites],
|
|
136
139
|
"findings": [_encode_finding(finding) for finding in module.findings],
|
|
140
|
+
"reads": [[r.function, _encode_span(r.location), str(r.symbol)] for r in module.reads],
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
|
|
@@ -145,6 +149,7 @@ def decode(data: Mapping[str, Any]) -> CachedModule:
|
|
|
145
149
|
{_string(name): _decode_summary(s) for name, s in data["summaries"].items()},
|
|
146
150
|
tuple(_decode_site(site) for site in data["sites"]),
|
|
147
151
|
tuple(_decode_finding(finding) for finding in data["findings"]),
|
|
152
|
+
tuple(_decode_read(read) for read in data["reads"]),
|
|
148
153
|
)
|
|
149
154
|
|
|
150
155
|
|
|
@@ -194,6 +199,11 @@ def _decode_function(data: Any) -> ModuleFunction:
|
|
|
194
199
|
return ModuleFunction(_string(name), _decode_span(span), None if entry_point is None else _string(entry_point))
|
|
195
200
|
|
|
196
201
|
|
|
202
|
+
def _decode_read(data: Any) -> SymbolRead:
|
|
203
|
+
function, span, symbol = data
|
|
204
|
+
return SymbolRead(_string(function), _decode_span(span), SymbolId(_string(symbol)))
|
|
205
|
+
|
|
206
|
+
|
|
197
207
|
def _encode_finding(finding: Finding) -> dict[str, Any]:
|
|
198
208
|
return {
|
|
199
209
|
"rule": finding.rule_id,
|
coretrace_python/cli.py
CHANGED
|
@@ -3,13 +3,20 @@ from __future__ import annotations
|
|
|
3
3
|
import argparse
|
|
4
4
|
import sys
|
|
5
5
|
import zipfile
|
|
6
|
+
from datetime import UTC, datetime
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
from coretrace_python import __version__, engine
|
|
9
10
|
from coretrace_python.analysis import AnalysisError
|
|
10
11
|
from coretrace_python.cache import ProjectCache
|
|
11
12
|
from coretrace_python.cfg import CFGError
|
|
12
|
-
from coretrace_python.dependency import
|
|
13
|
+
from coretrace_python.dependency import (
|
|
14
|
+
dump_advisories,
|
|
15
|
+
import_osv,
|
|
16
|
+
read_osv,
|
|
17
|
+
render_sbom,
|
|
18
|
+
render_vex,
|
|
19
|
+
)
|
|
13
20
|
from coretrace_python.findings import Severity
|
|
14
21
|
from coretrace_python.findings.baseline import Baseline, BaselineError
|
|
15
22
|
from coretrace_python.frontend import HIRBuildError, ParseError, build_hir
|
|
@@ -106,6 +113,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
106
113
|
metavar="PATH",
|
|
107
114
|
help="with --check on a directory, write a CycloneDX bill of materials to PATH",
|
|
108
115
|
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--vex",
|
|
118
|
+
type=Path,
|
|
119
|
+
default=None,
|
|
120
|
+
metavar="PATH",
|
|
121
|
+
help="with --check on a directory, write to PATH an OpenVEX document saying whether "
|
|
122
|
+
"each advisory affecting a requirement affects the project",
|
|
123
|
+
)
|
|
109
124
|
parser.add_argument(
|
|
110
125
|
"--advisories",
|
|
111
126
|
action="append",
|
|
@@ -182,6 +197,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
182
197
|
if args.sbom is not None and not (args.check and args.path.is_dir()):
|
|
183
198
|
print("error: --sbom only applies to --check on a directory", file=sys.stderr)
|
|
184
199
|
return EXIT_ERROR
|
|
200
|
+
if args.vex is not None and not (args.check and args.path.is_dir()):
|
|
201
|
+
print("error: --vex only applies to --check on a directory", file=sys.stderr)
|
|
202
|
+
return EXIT_ERROR
|
|
185
203
|
if args.advisories and not (args.check and args.path.is_dir()):
|
|
186
204
|
print("error: --advisories only applies to --check on a directory", file=sys.stderr)
|
|
187
205
|
return EXIT_ERROR
|
|
@@ -227,6 +245,20 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
227
245
|
render_sbom(analysis.dependencies, analysis.advisories, engine.TOOL_NAME, __version__),
|
|
228
246
|
encoding="utf-8",
|
|
229
247
|
)
|
|
248
|
+
if args.vex is not None:
|
|
249
|
+
args.vex.write_text(
|
|
250
|
+
render_vex(
|
|
251
|
+
analysis.dependencies,
|
|
252
|
+
analysis.advisories,
|
|
253
|
+
(*analysis.findings, *analysis.suppressed, *analysis.accepted),
|
|
254
|
+
analysis.coverage,
|
|
255
|
+
args.path,
|
|
256
|
+
engine.TOOL_NAME,
|
|
257
|
+
__version__,
|
|
258
|
+
datetime.now(UTC),
|
|
259
|
+
),
|
|
260
|
+
encoding="utf-8",
|
|
261
|
+
)
|
|
230
262
|
else:
|
|
231
263
|
file_analysis = engine.analyze_file(SourceManager().load_file(args.path), plugin_roots)
|
|
232
264
|
findings, coverage = file_analysis.findings, file_analysis.coverage
|
|
@@ -22,6 +22,7 @@ from coretrace_python.dependency.graph import (
|
|
|
22
22
|
)
|
|
23
23
|
from coretrace_python.dependency.policy import POLICY_FILE, Policy, apply_policy, load_policy
|
|
24
24
|
from coretrace_python.dependency.sbom import render_sbom
|
|
25
|
+
from coretrace_python.dependency.vex import render_vex
|
|
25
26
|
|
|
26
27
|
__all__ = [
|
|
27
28
|
"ADVISORY_FILE",
|
|
@@ -45,4 +46,5 @@ __all__ = [
|
|
|
45
46
|
"parse_dependencies",
|
|
46
47
|
"read_osv",
|
|
47
48
|
"render_sbom",
|
|
49
|
+
"render_vex",
|
|
48
50
|
]
|
|
@@ -138,14 +138,7 @@ def dump_advisories(advisories: Iterable[Advisory]) -> str:
|
|
|
138
138
|
"severity": a.severity.value,
|
|
139
139
|
"affected_symbols": [str(s) for s in a.affected_symbols],
|
|
140
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
|
-
],
|
|
141
|
+
"entry_points": [_entry_point_entry(e) for e in a.entry_points],
|
|
149
142
|
"modules": list(a.modules),
|
|
150
143
|
}
|
|
151
144
|
for a in advisories
|
|
@@ -154,6 +147,17 @@ def dump_advisories(advisories: Iterable[Advisory]) -> str:
|
|
|
154
147
|
return json.dumps(document, indent=2) + "\n"
|
|
155
148
|
|
|
156
149
|
|
|
150
|
+
def _entry_point_entry(entry_point: AdvisoryEntryPoint) -> dict[str, Any]:
|
|
151
|
+
entry: dict[str, Any] = {
|
|
152
|
+
"symbol": str(entry_point.symbol),
|
|
153
|
+
"justification": entry_point.justification,
|
|
154
|
+
"conditions": [_condition_entry(c) for c in entry_point.conditions],
|
|
155
|
+
}
|
|
156
|
+
if entry_point.read:
|
|
157
|
+
entry["read"] = True
|
|
158
|
+
return entry
|
|
159
|
+
|
|
160
|
+
|
|
157
161
|
def _condition_entry(condition: Condition) -> dict[str, Any]:
|
|
158
162
|
entry: dict[str, Any] = {"kind": condition.kind, "text": condition.text}
|
|
159
163
|
if condition.argument is not None:
|
|
@@ -194,10 +198,14 @@ def _advisory(entry: Mapping[str, Any]) -> Advisory:
|
|
|
194
198
|
|
|
195
199
|
|
|
196
200
|
def _entry_point(entry: Mapping[str, Any]) -> AdvisoryEntryPoint:
|
|
201
|
+
read = entry.get("read", False)
|
|
202
|
+
if not isinstance(read, bool):
|
|
203
|
+
raise TypeError(f"entry point read must be true or false, got {read!r}")
|
|
197
204
|
return AdvisoryEntryPoint(
|
|
198
205
|
SymbolId(str(entry["symbol"])),
|
|
199
206
|
str(entry["justification"]),
|
|
200
207
|
tuple(_condition(c) for c in entry.get("conditions") or []),
|
|
208
|
+
read,
|
|
201
209
|
)
|
|
202
210
|
|
|
203
211
|
|
|
@@ -69,7 +69,7 @@ def check_conditions(entry: AdvisoryEntryPoint | None, arguments: Arguments | No
|
|
|
69
69
|
met: list[Condition] = []
|
|
70
70
|
pending: list[Condition] = []
|
|
71
71
|
for condition in entry.conditions:
|
|
72
|
-
given =
|
|
72
|
+
given = arguments.given(condition.argument, condition.position) if condition.checkable and arguments is not None else None
|
|
73
73
|
if given is None:
|
|
74
74
|
pending.append(condition)
|
|
75
75
|
continue
|
|
@@ -84,18 +84,6 @@ def check_conditions(entry: AdvisoryEntryPoint | None, arguments: Arguments | No
|
|
|
84
84
|
return ConditionCheck(tuple(met), tuple(pending))
|
|
85
85
|
|
|
86
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
87
|
def ruled_out(module: str, site: CallSite, check: ConditionCheck) -> str:
|
|
100
88
|
"""Which call a contradicted condition rules out, and why: ``app:12
|
|
101
89
|
python.yaml.load(Loader=python.yaml.SafeLoader)``."""
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Dependency resolution (architecture §26).
|
|
2
2
|
|
|
3
3
|
Requirements declared in ``requirements.txt``, ``pyproject.toml`` (PEP 621 and Poetry)
|
|
4
|
-
and pinned in ``poetry.lock`` or ``uv.lock`` become a ``DependencyGraph
|
|
4
|
+
and pinned in ``poetry.lock`` or ``uv.lock`` become a ``DependencyGraph``; a lock file
|
|
5
|
+
also tells which other packages require each package. Versions are
|
|
5
6
|
compared with a small PEP 440 subset (``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``,
|
|
6
7
|
``~=`` and Poetry's ``^``), enough to decide whether a requirement may allow a version an
|
|
7
8
|
advisory marks as vulnerable. Nothing is downloaded.
|
|
@@ -11,9 +12,9 @@ from __future__ import annotations
|
|
|
11
12
|
|
|
12
13
|
import re
|
|
13
14
|
import tomllib
|
|
14
|
-
from collections.abc import Iterable, Mapping
|
|
15
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
15
16
|
from dataclasses import dataclass
|
|
16
|
-
from pathlib import PurePath
|
|
17
|
+
from pathlib import PurePath, PurePosixPath
|
|
17
18
|
from types import MappingProxyType
|
|
18
19
|
from typing import Any, ClassVar
|
|
19
20
|
|
|
@@ -168,11 +169,16 @@ class Condition:
|
|
|
168
169
|
@dataclass(frozen=True)
|
|
169
170
|
class AdvisoryEntryPoint:
|
|
170
171
|
"""A public API through which a project reaches an affected symbol, justified by the
|
|
171
|
-
fixing commit or by a call path, with the conditions under which it is affected.
|
|
172
|
+
fixing commit or by a call path, with the conditions under which it is affected.
|
|
173
|
+
|
|
174
|
+
An entry point is reached by a call, or — when ``read`` is set, for an attribute whose
|
|
175
|
+
getter runs the affected code, such as a lazily parsed request body — by any read of
|
|
176
|
+
it or of an attribute of it."""
|
|
172
177
|
|
|
173
178
|
symbol: SymbolId
|
|
174
179
|
justification: str
|
|
175
180
|
conditions: tuple[Condition, ...] = ()
|
|
181
|
+
read: bool = False
|
|
176
182
|
|
|
177
183
|
|
|
178
184
|
DIRECT = "affected symbol, changed by the fix"
|
|
@@ -219,9 +225,15 @@ class Advisory:
|
|
|
219
225
|
|
|
220
226
|
|
|
221
227
|
class DependencyGraph:
|
|
222
|
-
def __init__(
|
|
228
|
+
def __init__(
|
|
229
|
+
self,
|
|
230
|
+
requirements: Mapping[str, Requirement] | None = None,
|
|
231
|
+
errors: tuple[str, ...] = (),
|
|
232
|
+
dependents: Mapping[str, frozenset[str]] | None = None,
|
|
233
|
+
) -> None:
|
|
223
234
|
self._requirements = MappingProxyType(dict(sorted((requirements or {}).items())))
|
|
224
235
|
self.errors = errors
|
|
236
|
+
self._dependents = MappingProxyType(dict(dependents or {}))
|
|
225
237
|
|
|
226
238
|
@property
|
|
227
239
|
def names(self) -> tuple[str, ...]:
|
|
@@ -234,6 +246,12 @@ class DependencyGraph:
|
|
|
234
246
|
def requirement(self, name: str) -> Requirement | None:
|
|
235
247
|
return self._requirements.get(normalize(name))
|
|
236
248
|
|
|
249
|
+
def required_by(self, name: str) -> frozenset[str] | None:
|
|
250
|
+
"""The other packages a lock file shows requiring ``name`` — an empty set when
|
|
251
|
+
only the project does — or None when no lock file mentions it."""
|
|
252
|
+
|
|
253
|
+
return self._dependents.get(normalize(name))
|
|
254
|
+
|
|
237
255
|
def merge(self, other: DependencyGraph) -> DependencyGraph:
|
|
238
256
|
merged = dict(self._requirements)
|
|
239
257
|
for name, requirement in other._requirements.items():
|
|
@@ -248,7 +266,10 @@ class DependencyGraph:
|
|
|
248
266
|
requirement.pinned or current.pinned,
|
|
249
267
|
current.optional and requirement.optional,
|
|
250
268
|
)
|
|
251
|
-
|
|
269
|
+
dependents = dict(self._dependents)
|
|
270
|
+
for name, found in other._dependents.items():
|
|
271
|
+
dependents[name] = dependents.get(name, frozenset()) | found
|
|
272
|
+
return DependencyGraph(merged, self.errors + other.errors, dependents)
|
|
252
273
|
|
|
253
274
|
|
|
254
275
|
def parse_dependencies(source: SourceFile) -> DependencyGraph:
|
|
@@ -260,7 +281,7 @@ def parse_dependencies(source: SourceFile) -> DependencyGraph:
|
|
|
260
281
|
if name == "pyproject.toml":
|
|
261
282
|
return _parse_toml(source, _pyproject_requirements)
|
|
262
283
|
if name in ("poetry.lock", "uv.lock"):
|
|
263
|
-
return _parse_toml(source, _lock_requirements)
|
|
284
|
+
return _parse_toml(source, _lock_requirements, _lock_dependents)
|
|
264
285
|
return DependencyGraph()
|
|
265
286
|
|
|
266
287
|
|
|
@@ -276,7 +297,7 @@ def _parse_requirements_txt(source: SourceFile) -> DependencyGraph:
|
|
|
276
297
|
return DependencyGraph(found)
|
|
277
298
|
|
|
278
299
|
|
|
279
|
-
def _parse_toml(source: SourceFile, extract: Any) -> DependencyGraph:
|
|
300
|
+
def _parse_toml(source: SourceFile, extract: Any, dependents: Any = None) -> DependencyGraph:
|
|
280
301
|
try:
|
|
281
302
|
data = tomllib.loads(source.text)
|
|
282
303
|
except tomllib.TOMLDecodeError as error:
|
|
@@ -284,7 +305,7 @@ def _parse_toml(source: SourceFile, extract: Any) -> DependencyGraph:
|
|
|
284
305
|
found: dict[str, Requirement] = {}
|
|
285
306
|
for requirement in extract(data, source):
|
|
286
307
|
found[requirement.name] = requirement
|
|
287
|
-
return DependencyGraph(found)
|
|
308
|
+
return DependencyGraph(found, dependents=None if dependents is None else dependents(data))
|
|
288
309
|
|
|
289
310
|
|
|
290
311
|
def _line_of(source: SourceFile, key: str, default: int = 1) -> int:
|
|
@@ -335,6 +356,52 @@ def _lock_requirements(data: Mapping[str, Any], source: SourceFile) -> list[Requ
|
|
|
335
356
|
return found
|
|
336
357
|
|
|
337
358
|
|
|
359
|
+
def _lock_dependents(data: Mapping[str, Any]) -> dict[str, frozenset[str]]:
|
|
360
|
+
"""For every package a lock file lists or requires, the other locked packages that
|
|
361
|
+
require it, optional extras and development groups included. The project's own
|
|
362
|
+
packages — uv's editable or virtual sources inside the project — are not others."""
|
|
363
|
+
|
|
364
|
+
dependents: dict[str, set[str]] = {}
|
|
365
|
+
for package in data.get("package", []) or []:
|
|
366
|
+
name = package.get("name")
|
|
367
|
+
if not isinstance(name, str):
|
|
368
|
+
continue
|
|
369
|
+
dependents.setdefault(normalize(name), set())
|
|
370
|
+
own = _is_project_package(package)
|
|
371
|
+
for dependency in _locked_dependencies(package):
|
|
372
|
+
found = dependents.setdefault(dependency, set())
|
|
373
|
+
if not own:
|
|
374
|
+
found.add(normalize(name))
|
|
375
|
+
return {name: frozenset(found) for name, found in dependents.items()}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _locked_dependencies(package: Mapping[str, Any]) -> Iterator[str]:
|
|
379
|
+
declared = package.get("dependencies") or []
|
|
380
|
+
if isinstance(declared, Mapping): # poetry.lock: ``name = specifier``, optional ones too
|
|
381
|
+
yield from (normalize(str(name)) for name in declared)
|
|
382
|
+
return
|
|
383
|
+
groups = (
|
|
384
|
+
declared,
|
|
385
|
+
*(package.get("optional-dependencies") or {}).values(),
|
|
386
|
+
*(package.get("dev-dependencies") or {}).values(),
|
|
387
|
+
)
|
|
388
|
+
for group in groups:
|
|
389
|
+
for entry in group or []:
|
|
390
|
+
if isinstance(entry, Mapping) and isinstance(entry.get("name"), str):
|
|
391
|
+
yield normalize(entry["name"])
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _is_project_package(package: Mapping[str, Any]) -> bool:
|
|
395
|
+
source = package.get("source")
|
|
396
|
+
if not isinstance(source, Mapping):
|
|
397
|
+
return False
|
|
398
|
+
path = source.get("editable", source.get("virtual"))
|
|
399
|
+
if not isinstance(path, str):
|
|
400
|
+
return False
|
|
401
|
+
location = PurePosixPath(path)
|
|
402
|
+
return not location.is_absolute() and ".." not in location.parts
|
|
403
|
+
|
|
404
|
+
|
|
338
405
|
class DependencyAnalysis(Analysis[DependencyGraph]):
|
|
339
406
|
"""The project's dependency graph, provided by the engine; empty for a lone file."""
|
|
340
407
|
|
|
@@ -29,6 +29,11 @@ class Policy:
|
|
|
29
29
|
def denies(self, package: str) -> bool:
|
|
30
30
|
return normalize(package) in {normalize(name) for name in self.deny}
|
|
31
31
|
|
|
32
|
+
def accepts(self, finding: Finding) -> bool:
|
|
33
|
+
"""Whether ``finding`` is about an advisory the project accepts."""
|
|
34
|
+
|
|
35
|
+
return finding.metadata.get("advisory") in self.ignore
|
|
36
|
+
|
|
32
37
|
|
|
33
38
|
def load_policy(path: Path) -> Policy:
|
|
34
39
|
try:
|
|
@@ -61,5 +66,4 @@ def _boolean(value: Any, key: str) -> bool:
|
|
|
61
66
|
def apply_policy(policy: Policy, findings: Iterable[Finding]) -> tuple[Finding, ...]:
|
|
62
67
|
"""The findings minus those about an advisory the policy accepts."""
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
return tuple(f for f in findings if f.metadata.get("advisory") not in ignored)
|
|
69
|
+
return tuple(f for f in findings if not policy.accepts(f))
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""OpenVEX statements from the evidence of a check (architecture §26).
|
|
2
|
+
|
|
3
|
+
One statement per advisory affecting a requirement, with the required package as the
|
|
4
|
+
subcomponent of the project:
|
|
5
|
+
|
|
6
|
+
- ``affected`` when the project's code reaches the vulnerable code: a reachable or
|
|
7
|
+
exploitable finding, whatever the policy or a suppression did with it;
|
|
8
|
+
- ``not_affected``, as ``vulnerable_code_not_in_execute_path``, only when the advisory is
|
|
9
|
+
curated (it names entry points), no code of the project reaches them, every file and
|
|
10
|
+
function was analysed, and a lock file shows that no other package requires the
|
|
11
|
+
vulnerable one, since the code of installed packages is not analysed;
|
|
12
|
+
- ``under_investigation`` otherwise, with the reason in its notes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
from collections.abc import Iterable, Sequence
|
|
20
|
+
from datetime import UTC, datetime
|
|
21
|
+
from pathlib import Path, PurePosixPath
|
|
22
|
+
from urllib.parse import quote
|
|
23
|
+
|
|
24
|
+
from coretrace_python.dependency.graph import Advisory, DependencyGraph, Requirement
|
|
25
|
+
from coretrace_python.dependency.sbom import purl
|
|
26
|
+
from coretrace_python.findings import Finding
|
|
27
|
+
from coretrace_python.findings.coverage import Coverage
|
|
28
|
+
|
|
29
|
+
CONTEXT = "https://openvex.dev/ns/v0.2.0"
|
|
30
|
+
# OpenVEX's shared namespace for documents without an IRI of their own, and the author
|
|
31
|
+
# OpenVEX tools such as govulncheck write when none is known.
|
|
32
|
+
NAMESPACE = "https://openvex.dev/docs/public/vex-"
|
|
33
|
+
AUTHOR = "Unknown Author"
|
|
34
|
+
# Evidence levels that put the vulnerable code in the project's execution path, lowest first.
|
|
35
|
+
REACHED = ("reachable", "exploitable")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render_vex(
|
|
39
|
+
dependencies: DependencyGraph,
|
|
40
|
+
advisories: Iterable[Advisory],
|
|
41
|
+
evidence: Iterable[Finding],
|
|
42
|
+
coverage: Coverage,
|
|
43
|
+
root: Path,
|
|
44
|
+
tool_name: str,
|
|
45
|
+
tool_version: str,
|
|
46
|
+
timestamp: datetime,
|
|
47
|
+
) -> str:
|
|
48
|
+
"""The OpenVEX document of the project at ``root``. ``evidence`` is every finding of
|
|
49
|
+
the check, those suppressed or accepted by the policy included. The document's
|
|
50
|
+
identifier derives from its statements, not from ``timestamp``."""
|
|
51
|
+
|
|
52
|
+
root = root.resolve()
|
|
53
|
+
findings = tuple(evidence)
|
|
54
|
+
# The product must be an IRI; the check knows the project by its directory only.
|
|
55
|
+
product = f"pkg:generic/{quote(root.name, safe='')}"
|
|
56
|
+
statements: list[dict[str, object]] = []
|
|
57
|
+
for advisory in sorted(advisories, key=lambda a: (a.id, a.package)):
|
|
58
|
+
about = [
|
|
59
|
+
f
|
|
60
|
+
for f in findings
|
|
61
|
+
if f.metadata.get("advisory") == advisory.id and f.metadata.get("package") == advisory.package
|
|
62
|
+
]
|
|
63
|
+
for requirement in dependencies.requirements:
|
|
64
|
+
if advisory.affects(requirement):
|
|
65
|
+
statements.append(
|
|
66
|
+
{
|
|
67
|
+
"vulnerability": _vulnerability(advisory),
|
|
68
|
+
"products": [{"@id": product, "subcomponents": [{"@id": purl(requirement)}]}],
|
|
69
|
+
**_status(advisory, requirement, about, dependencies, coverage, root),
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
tooling = f"{tool_name} {tool_version}"
|
|
73
|
+
content = {
|
|
74
|
+
"@context": CONTEXT,
|
|
75
|
+
"author": AUTHOR,
|
|
76
|
+
"version": 1,
|
|
77
|
+
"tooling": tooling,
|
|
78
|
+
"statements": statements,
|
|
79
|
+
}
|
|
80
|
+
digest = hashlib.sha256(json.dumps(content, sort_keys=True).encode("utf-8")).hexdigest()
|
|
81
|
+
document = {
|
|
82
|
+
"@context": CONTEXT,
|
|
83
|
+
"@id": NAMESPACE + digest,
|
|
84
|
+
"author": AUTHOR,
|
|
85
|
+
"timestamp": timestamp.astimezone(UTC).isoformat(timespec="seconds").replace("+00:00", "Z"),
|
|
86
|
+
"version": 1,
|
|
87
|
+
"tooling": tooling,
|
|
88
|
+
"statements": statements,
|
|
89
|
+
}
|
|
90
|
+
return json.dumps(document, indent=2) + "\n"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _vulnerability(advisory: Advisory) -> dict[str, object]:
|
|
94
|
+
vulnerability: dict[str, object] = {"name": advisory.id, "description": advisory.summary}
|
|
95
|
+
if advisory.aliases:
|
|
96
|
+
vulnerability["aliases"] = list(advisory.aliases)
|
|
97
|
+
return vulnerability
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _status(
|
|
101
|
+
advisory: Advisory,
|
|
102
|
+
requirement: Requirement,
|
|
103
|
+
findings: Sequence[Finding],
|
|
104
|
+
dependencies: DependencyGraph,
|
|
105
|
+
coverage: Coverage,
|
|
106
|
+
root: Path,
|
|
107
|
+
) -> dict[str, str]:
|
|
108
|
+
package = requirement.name
|
|
109
|
+
reached = _reached(findings, root)
|
|
110
|
+
if reached:
|
|
111
|
+
return {
|
|
112
|
+
"status": "affected",
|
|
113
|
+
"action_statement": (
|
|
114
|
+
f"Update {package} to a version outside the vulnerable range {advisory.vulnerable}."
|
|
115
|
+
),
|
|
116
|
+
"status_notes": f"Reached by the project's code at {reached}.",
|
|
117
|
+
}
|
|
118
|
+
if not advisory.entry_points:
|
|
119
|
+
return _investigating(
|
|
120
|
+
"The advisory names no entry point, so whether the project reaches the vulnerable "
|
|
121
|
+
"code is not known."
|
|
122
|
+
)
|
|
123
|
+
symbols = ", ".join(sorted(str(e.symbol) for e in advisory.entry_points))
|
|
124
|
+
entries = f"the entry points of {advisory.id} ({symbols})"
|
|
125
|
+
ruled_out = "; ".join(str(f.metadata["ruled_out"]) for f in findings if "ruled_out" in f.metadata)
|
|
126
|
+
ruled = f" Calls ruled out by their arguments: {ruled_out}." if ruled_out else ""
|
|
127
|
+
partial = sorted(
|
|
128
|
+
_relative(d.path, root)
|
|
129
|
+
for d in coverage.details
|
|
130
|
+
if d.status != "analysed" or d.analysed < d.functions
|
|
131
|
+
)
|
|
132
|
+
if partial:
|
|
133
|
+
return _investigating(
|
|
134
|
+
f"No analysed code reaches {entries}, but {', '.join(partial)} could not be fully "
|
|
135
|
+
f"analysed.{ruled}"
|
|
136
|
+
)
|
|
137
|
+
required_by = dependencies.required_by(package)
|
|
138
|
+
if required_by is None:
|
|
139
|
+
return _investigating(
|
|
140
|
+
f"No code of the project reaches {entries}, but no lock file shows which installed "
|
|
141
|
+
f"packages require {package}, and their code is not analysed.{ruled}"
|
|
142
|
+
)
|
|
143
|
+
if required_by:
|
|
144
|
+
return _investigating(
|
|
145
|
+
f"No code of the project reaches {entries}, but {package} is required by "
|
|
146
|
+
f"{', '.join(sorted(required_by))}, whose code is not analysed.{ruled}"
|
|
147
|
+
)
|
|
148
|
+
return {
|
|
149
|
+
"status": "not_affected",
|
|
150
|
+
"justification": "vulnerable_code_not_in_execute_path",
|
|
151
|
+
"impact_statement": (
|
|
152
|
+
f"No code of the project reaches {entries}, and no other locked package requires "
|
|
153
|
+
f"{package}.{ruled}"
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _reached(findings: Sequence[Finding], root: Path) -> str:
|
|
159
|
+
"""Where the project's code reaches the vulnerable code, each place at its highest
|
|
160
|
+
level of evidence; empty when nothing reaches it."""
|
|
161
|
+
|
|
162
|
+
levels: dict[tuple[str, int, str], str] = {}
|
|
163
|
+
for finding in findings:
|
|
164
|
+
level = finding.metadata.get("level")
|
|
165
|
+
if level in REACHED:
|
|
166
|
+
path = _relative(str(finding.span.source_id), root)
|
|
167
|
+
place = (path, finding.span.start_line, str(finding.metadata.get("symbol")))
|
|
168
|
+
levels[place] = max(level, levels.get(place, level), key=REACHED.index)
|
|
169
|
+
return "; ".join(
|
|
170
|
+
f"{path}:{line} {symbol} ({level})" for (path, line, symbol), level in sorted(levels.items())
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _investigating(notes: str) -> dict[str, str]:
|
|
175
|
+
return {"status": "under_investigation", "status_notes": notes}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _relative(path: str, root: Path) -> str:
|
|
179
|
+
try:
|
|
180
|
+
return PurePosixPath(Path(path).relative_to(root)).as_posix()
|
|
181
|
+
except ValueError:
|
|
182
|
+
return path
|
coretrace_python/engine.py
CHANGED
|
@@ -76,6 +76,7 @@ from coretrace_python.interprocedural import (
|
|
|
76
76
|
ProjectSummaries,
|
|
77
77
|
SummaryAnalysis,
|
|
78
78
|
SummaryIndex,
|
|
79
|
+
SymbolRead,
|
|
79
80
|
build_module_graph,
|
|
80
81
|
discover_sources,
|
|
81
82
|
project_symbol,
|
|
@@ -201,6 +202,8 @@ class ProjectAnalysis:
|
|
|
201
202
|
coverage: Coverage = field(default_factory=Coverage)
|
|
202
203
|
# Findings silenced by an inline ``# coretrace: ignore`` comment.
|
|
203
204
|
suppressed: tuple[Finding, ...] = ()
|
|
205
|
+
# Findings about an advisory the policy accepts, still evidence of what is reached.
|
|
206
|
+
accepted: tuple[Finding, ...] = ()
|
|
204
207
|
|
|
205
208
|
|
|
206
209
|
@dataclass(frozen=True)
|
|
@@ -466,7 +469,15 @@ def analyze_project(
|
|
|
466
469
|
sites: dict[str, list[CallSite]] = {function.name: [] for function in entry.functions}
|
|
467
470
|
for site in entry.sites:
|
|
468
471
|
sites.setdefault(site.caller, []).append(site)
|
|
469
|
-
|
|
472
|
+
reads: dict[str, list[SymbolRead]] = {}
|
|
473
|
+
for read in entry.reads:
|
|
474
|
+
reads.setdefault(read.function, []).append(read)
|
|
475
|
+
call_graphs[name] = CallGraph(
|
|
476
|
+
{},
|
|
477
|
+
{f: tuple(s) for f, s in sites.items()},
|
|
478
|
+
frozenset(),
|
|
479
|
+
reads={f: tuple(r) for f, r in reads.items()},
|
|
480
|
+
)
|
|
470
481
|
context = ProjectContext(graph, dependencies, advisories, analysable, call_graphs, policy, root, functions)
|
|
471
482
|
for plugin in all_plugins:
|
|
472
483
|
if isinstance(plugin, ProjectPlugin):
|
|
@@ -474,6 +485,7 @@ def analyze_project(
|
|
|
474
485
|
for plugin in all_plugins:
|
|
475
486
|
if isinstance(plugin, ProjectPlugin):
|
|
476
487
|
findings = list(apply_refinement(plugin, findings, plugin.refine(context, tuple(findings))))
|
|
488
|
+
accepted = tuple(f for f in findings if policy.accepts(f))
|
|
477
489
|
kept, suppressed = partition(apply_policy(policy, findings), _text_of(sources))
|
|
478
490
|
return ProjectAnalysis(
|
|
479
491
|
graph,
|
|
@@ -485,6 +497,7 @@ def analyze_project(
|
|
|
485
497
|
advisories,
|
|
486
498
|
Coverage(tuple(sorted(coverage, key=lambda c: c.path))),
|
|
487
499
|
suppressed,
|
|
500
|
+
accepted,
|
|
488
501
|
)
|
|
489
502
|
|
|
490
503
|
|
|
@@ -651,7 +664,8 @@ def _configuration_key(
|
|
|
651
664
|
def _analyse_module(
|
|
652
665
|
manager: AnalysisManager, plugins: tuple[Plugin, ...], affected: Affected
|
|
653
666
|
) -> CachedModule:
|
|
654
|
-
"""One module's findings, summaries and
|
|
667
|
+
"""One module's findings, summaries, call sites and symbol reads: what the cache
|
|
668
|
+
keeps (§11)."""
|
|
655
669
|
|
|
656
670
|
findings, supported = _check_module(manager, plugins)
|
|
657
671
|
graph = manager.get(CallGraphAnalysis)
|
|
@@ -671,6 +685,7 @@ def _analyse_module(
|
|
|
671
685
|
_summaries_of(manager),
|
|
672
686
|
tuple(site for function in graph.functions for site in graph.sites(function)),
|
|
673
687
|
(*findings, *correlated),
|
|
688
|
+
tuple(read for function in graph.functions for read in graph.reads(function)),
|
|
674
689
|
)
|
|
675
690
|
|
|
676
691
|
|
|
@@ -8,6 +8,7 @@ from coretrace_python.interprocedural.callgraph import (
|
|
|
8
8
|
ExternalSymbol,
|
|
9
9
|
KnownFunction,
|
|
10
10
|
ModuleFunction,
|
|
11
|
+
SymbolRead,
|
|
11
12
|
Target,
|
|
12
13
|
UnknownTarget,
|
|
13
14
|
)
|
|
@@ -46,6 +47,7 @@ __all__ = [
|
|
|
46
47
|
"SummaryAnalysis",
|
|
47
48
|
"SummaryIndex",
|
|
48
49
|
"SummaryTable",
|
|
50
|
+
"SymbolRead",
|
|
49
51
|
"Target",
|
|
50
52
|
"UnknownTarget",
|
|
51
53
|
"build_module_graph",
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Every call site of every analysable function resolves, through the SSA form, to a
|
|
4
4
|
``KnownFunction`` defined at module level, an ``ExternalSymbol`` reached through imports
|
|
5
5
|
or builtins, or ``UnknownTarget`` (parameters, attributes, methods) until type inference
|
|
6
|
-
and framework models narrow it down.
|
|
6
|
+
and framework models narrow it down. The graph also keeps the symbols each function
|
|
7
|
+
reads, called or not: reading ``request.form`` runs its getter.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
@@ -72,6 +73,18 @@ class Arguments:
|
|
|
72
73
|
keywords: tuple[tuple[str, str | None], ...] = ()
|
|
73
74
|
unpacked: bool = False
|
|
74
75
|
|
|
76
|
+
def given(self, keyword: str | None, position: int | None = None) -> tuple[bool, str | None] | None:
|
|
77
|
+
"""Whether the call gives an argument, by ``keyword`` or at ``position``, and what it
|
|
78
|
+
denotes: ``(True, value)`` when given, ``(False, None)`` when surely absent, None
|
|
79
|
+
when the call cannot tell because it unpacks arguments."""
|
|
80
|
+
|
|
81
|
+
for name, value in self.keywords:
|
|
82
|
+
if name == keyword:
|
|
83
|
+
return True, value
|
|
84
|
+
if position is not None and position < len(self.positional):
|
|
85
|
+
return True, self.positional[position]
|
|
86
|
+
return None if self.unpacked else (False, None)
|
|
87
|
+
|
|
75
88
|
|
|
76
89
|
@dataclass(frozen=True)
|
|
77
90
|
class CallSite:
|
|
@@ -81,6 +94,16 @@ class CallSite:
|
|
|
81
94
|
arguments: Arguments = field(default_factory=Arguments)
|
|
82
95
|
|
|
83
96
|
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class SymbolRead:
|
|
99
|
+
"""Where ``function`` first reads ``symbol``: an imported name or an attribute of a
|
|
100
|
+
value denoting a symbol (``request.form`` in ``request.form['name']``)."""
|
|
101
|
+
|
|
102
|
+
function: str
|
|
103
|
+
location: SourceSpan
|
|
104
|
+
symbol: SymbolId
|
|
105
|
+
|
|
106
|
+
|
|
84
107
|
@dataclass(frozen=True)
|
|
85
108
|
class ModuleFunction:
|
|
86
109
|
"""One function of a module as a project plugin sees it: the name the call graph
|
|
@@ -99,8 +122,10 @@ class CallGraph:
|
|
|
99
122
|
sites: Mapping[str, tuple[CallSite, ...]],
|
|
100
123
|
unsupported: frozenset[str],
|
|
101
124
|
symbols: Mapping[str, Mapping[Value, SymbolId]] | None = None,
|
|
125
|
+
reads: Mapping[str, tuple[SymbolRead, ...]] | None = None,
|
|
102
126
|
) -> None:
|
|
103
127
|
self._symbols = {name: MappingProxyType(dict(found)) for name, found in (symbols or {}).items()}
|
|
128
|
+
self._reads = MappingProxyType(dict(reads or {}))
|
|
104
129
|
self.definitions: Mapping[str, nodes.Function] = MappingProxyType(dict(definitions))
|
|
105
130
|
# A graph rebuilt from cached call sites has sites but no definitions.
|
|
106
131
|
self.functions = tuple(dict.fromkeys((*definitions, *sites)))
|
|
@@ -126,6 +151,11 @@ class CallGraph:
|
|
|
126
151
|
def sites(self, caller: str) -> tuple[CallSite, ...]:
|
|
127
152
|
return self._sites.get(caller, ())
|
|
128
153
|
|
|
154
|
+
def reads(self, function: str) -> tuple[SymbolRead, ...]:
|
|
155
|
+
"""The symbols ``function`` reads, each where it first reads it."""
|
|
156
|
+
|
|
157
|
+
return self._reads.get(function, ())
|
|
158
|
+
|
|
129
159
|
def target_at(self, caller: str, location: SourceSpan) -> Target:
|
|
130
160
|
site = self._at.get((caller, location))
|
|
131
161
|
return site.target if site is not None else UnknownTarget()
|
|
@@ -207,6 +237,22 @@ def _arguments(call: Call, symbols: Mapping[Value, SymbolId], defs: Mapping[Valu
|
|
|
207
237
|
)
|
|
208
238
|
|
|
209
239
|
|
|
240
|
+
def _reads(name: str, function: FunctionIR, symbols: Mapping[Value, SymbolId]) -> tuple[SymbolRead, ...]:
|
|
241
|
+
first: dict[SymbolId, SymbolRead] = {}
|
|
242
|
+
for block in function.blocks:
|
|
243
|
+
for instruction in block.instructions:
|
|
244
|
+
if isinstance(instruction, Symbol | GetAttr) and instruction.result in symbols:
|
|
245
|
+
read = SymbolRead(name, instruction.location, symbols[instruction.result])
|
|
246
|
+
known = first.get(read.symbol)
|
|
247
|
+
if known is None or _position(read.location) < _position(known.location):
|
|
248
|
+
first[read.symbol] = read
|
|
249
|
+
return tuple(sorted(first.values(), key=lambda read: (_position(read.location), str(read.symbol))))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _position(span: SourceSpan) -> tuple[int, int]:
|
|
253
|
+
return span.start_line, span.start_column
|
|
254
|
+
|
|
255
|
+
|
|
210
256
|
def resolve_targets(
|
|
211
257
|
function: FunctionIR,
|
|
212
258
|
scopes: ScopeTable,
|
|
@@ -359,6 +405,7 @@ class CallGraphAnalysis(Analysis[CallGraph]):
|
|
|
359
405
|
break
|
|
360
406
|
sites: dict[str, tuple[CallSite, ...]] = {}
|
|
361
407
|
symbols: dict[str, Mapping[Value, SymbolId]] = {}
|
|
408
|
+
reads: dict[str, tuple[SymbolRead, ...]] = {}
|
|
362
409
|
unsupported: set[str] = set()
|
|
363
410
|
for name, function in definitions.items():
|
|
364
411
|
try:
|
|
@@ -395,4 +442,5 @@ class CallGraphAnalysis(Analysis[CallGraph]):
|
|
|
395
442
|
)
|
|
396
443
|
)
|
|
397
444
|
sites[name] = tuple(found)
|
|
398
|
-
|
|
445
|
+
reads[name] = _reads(name, ssa, symbols[name])
|
|
446
|
+
return CallGraph(definitions, sites, frozenset(unsupported), symbols, reads)
|
|
@@ -16,6 +16,7 @@ from coretrace_python.taint.models import (
|
|
|
16
16
|
ModelTable,
|
|
17
17
|
NamedParameter,
|
|
18
18
|
RouteRegistrar,
|
|
19
|
+
SafeArgument,
|
|
19
20
|
Sanitizer,
|
|
20
21
|
SecurityModelAnalysis,
|
|
21
22
|
SecurityModelRegistry,
|
|
@@ -39,6 +40,7 @@ __all__ = [
|
|
|
39
40
|
"RegisteredRoutes",
|
|
40
41
|
"RouteRegistrar",
|
|
41
42
|
"Routes",
|
|
43
|
+
"SafeArgument",
|
|
42
44
|
"Sanitizer",
|
|
43
45
|
"SecurityModelAnalysis",
|
|
44
46
|
"SecurityModelRegistry",
|
coretrace_python/taint/engine.py
CHANGED
|
@@ -396,9 +396,9 @@ class _TaintProblem(DataflowProblem[State]):
|
|
|
396
396
|
) -> Taint:
|
|
397
397
|
"""Sinks, sanitizers and sources of a call to an external symbol."""
|
|
398
398
|
|
|
399
|
-
|
|
399
|
+
given = self.graph.arguments_at(self.name, call.location)
|
|
400
|
+
sink = self.guarded(self.models.sink(symbol), given)
|
|
400
401
|
if sink is not None:
|
|
401
|
-
given = self.graph.arguments_at(self.name, call.location)
|
|
402
402
|
for position, argument in enumerate(call.arguments):
|
|
403
403
|
self.report(flows, sink, self.rendered(argument, state), argument, call, None, None, position, given)
|
|
404
404
|
for argument in (*call.starred, *(value for _, value in call.keywords)):
|
|
@@ -413,6 +413,19 @@ class _TaintProblem(DataflowProblem[State]):
|
|
|
413
413
|
return everything.join(Taint(source.kinds, frozenset({source})))
|
|
414
414
|
return everything
|
|
415
415
|
|
|
416
|
+
def guarded(self, sink: Sink | None, arguments: Arguments) -> Sink | None:
|
|
417
|
+
"""The sink as this call makes it: an argument declared safe (``yaml.load`` with
|
|
418
|
+
``Loader=SafeLoader``) takes off the kinds it covers, and no kind left, no sink."""
|
|
419
|
+
|
|
420
|
+
if sink is None:
|
|
421
|
+
return None
|
|
422
|
+
safe = self.models.safe_argument(sink.symbol)
|
|
423
|
+
given = arguments.given(safe.argument, safe.position) if safe is not None else None
|
|
424
|
+
if safe is None or given is None or not given[0] or given[1] not in safe.values:
|
|
425
|
+
return sink
|
|
426
|
+
kinds = sink.kinds & ~safe.kinds
|
|
427
|
+
return Sink(sink.symbol, kinds, sink.positions) if kinds else None
|
|
428
|
+
|
|
416
429
|
def rendered(self, argument: Value, state: Mapping[Key, Taint]) -> Taint:
|
|
417
430
|
"""The taint an argument brings to a sink. A container literal is serialised by
|
|
418
431
|
every framework (``self.write({...})`` answers JSON), never rendered as markup, so
|
|
@@ -481,7 +494,7 @@ class _TaintProblem(DataflowProblem[State]):
|
|
|
481
494
|
return taint, witness
|
|
482
495
|
|
|
483
496
|
for reached in summary.external_calls:
|
|
484
|
-
sink = self.models.sink(reached.symbol)
|
|
497
|
+
sink = self.guarded(self.models.sink(reached.symbol), reached.arguments)
|
|
485
498
|
if sink is None:
|
|
486
499
|
continue
|
|
487
500
|
positions: list[int | None] = [*range(len(reached.argument_dependencies)), None]
|
coretrace_python/taint/models.py
CHANGED
|
@@ -149,6 +149,21 @@ class SuffixSink:
|
|
|
149
149
|
return SymbolId(f"python.suffix.{self.suffix.replace('.', '_')}")
|
|
150
150
|
|
|
151
151
|
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class SafeArgument:
|
|
154
|
+
"""A call to the sink ``symbol`` whose ``argument`` (by keyword, or at ``position``)
|
|
155
|
+
denotes one of ``values`` is not a sink for ``kinds``: ``yaml.load`` with
|
|
156
|
+
``Loader=SafeLoader``. Values are written as call sites record them: a symbol by its
|
|
157
|
+
canonical name, a constant as Python writes it. Only a value given explicitly makes
|
|
158
|
+
the call safe; an absent argument, a variable or unpacked arguments leave the sink."""
|
|
159
|
+
|
|
160
|
+
symbol: SymbolId
|
|
161
|
+
argument: str
|
|
162
|
+
values: tuple[str, ...]
|
|
163
|
+
position: int | None = None
|
|
164
|
+
kinds: TaintKind = TaintKind.ALL
|
|
165
|
+
|
|
166
|
+
|
|
152
167
|
@dataclass(frozen=True)
|
|
153
168
|
class Validator:
|
|
154
169
|
"""A callable whose truth proves its ``argument`` safe (refutation evidence, §24)."""
|
|
@@ -178,6 +193,7 @@ Model = (
|
|
|
178
193
|
| NamedParameter
|
|
179
194
|
| RouteRegistrar
|
|
180
195
|
| SuffixSink
|
|
196
|
+
| SafeArgument
|
|
181
197
|
)
|
|
182
198
|
|
|
183
199
|
|
|
@@ -193,6 +209,7 @@ class ModelTable:
|
|
|
193
209
|
named_parameters: tuple[NamedParameter, ...] = ()
|
|
194
210
|
route_registrars: tuple[RouteRegistrar, ...] = ()
|
|
195
211
|
suffix_sinks: tuple[SuffixSink, ...] = ()
|
|
212
|
+
safe_arguments: tuple[SafeArgument, ...] = ()
|
|
196
213
|
_by_symbol: dict[type[Model], dict[SymbolId, Model]] = field(
|
|
197
214
|
init=False, repr=False, compare=False
|
|
198
215
|
)
|
|
@@ -207,6 +224,7 @@ class ModelTable:
|
|
|
207
224
|
Validator: {m.symbol: m for m in self.validators},
|
|
208
225
|
AuthorizationGuard: {m.symbol: m for m in self.authorizations},
|
|
209
226
|
RouteRegistrar: {m.symbol: m for m in self.route_registrars},
|
|
227
|
+
SafeArgument: {m.symbol: m for m in self.safe_arguments},
|
|
210
228
|
}
|
|
211
229
|
object.__setattr__(self, "_by_symbol", MappingProxyType(index))
|
|
212
230
|
|
|
@@ -272,12 +290,17 @@ class ModelTable:
|
|
|
272
290
|
self.named_parameters,
|
|
273
291
|
self.route_registrars,
|
|
274
292
|
self.suffix_sinks,
|
|
293
|
+
self.safe_arguments,
|
|
275
294
|
)
|
|
276
295
|
|
|
277
296
|
def sanitizer(self, symbol: SymbolId) -> Sanitizer | None:
|
|
278
297
|
found = self._by_symbol[Sanitizer].get(symbol)
|
|
279
298
|
return found if isinstance(found, Sanitizer) else None
|
|
280
299
|
|
|
300
|
+
def safe_argument(self, symbol: SymbolId) -> SafeArgument | None:
|
|
301
|
+
found = self._by_symbol[SafeArgument].get(symbol)
|
|
302
|
+
return found if isinstance(found, SafeArgument) else None
|
|
303
|
+
|
|
281
304
|
|
|
282
305
|
class SecurityModelRegistry:
|
|
283
306
|
"""Mutable collection point for the models plugins register."""
|
|
@@ -323,6 +346,7 @@ class SecurityModelRegistry:
|
|
|
323
346
|
named_parameters=tuple(m for m in models if isinstance(m, NamedParameter)),
|
|
324
347
|
route_registrars=tuple(m for m in models if isinstance(m, RouteRegistrar)),
|
|
325
348
|
suffix_sinks=tuple(m for m in models if isinstance(m, SuffixSink)),
|
|
349
|
+
safe_arguments=tuple(m for m in models if isinstance(m, SafeArgument)),
|
|
326
350
|
)
|
|
327
351
|
|
|
328
352
|
|
{coretrace_python_analyzer-0.5.0.dist-info → coretrace_python_analyzer-0.6.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.6.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.5.0.dist-info → coretrace_python_analyzer-0.6.0.dist-info}/RECORD
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
coretrace_python/__init__.py,sha256=
|
|
1
|
+
coretrace_python/__init__.py,sha256=MPB7k3sSK3YPPbcgkTbDkNy8vJRChFlClF8INwVFbTw,75
|
|
2
2
|
coretrace_python/__main__.py,sha256=V8DnYtRboISK2VCKm7DGHLREKvqLbqApyWlKBOs26wU,65
|
|
3
|
-
coretrace_python/cache.py,sha256=
|
|
4
|
-
coretrace_python/cli.py,sha256=
|
|
5
|
-
coretrace_python/engine.py,sha256=
|
|
3
|
+
coretrace_python/cache.py,sha256=0FgPpr5frJrZ3HndMMNJwSmXtkdWYv1yvkvCDMENt5E,12823
|
|
4
|
+
coretrace_python/cli.py,sha256=SwXYUR8DIgppkqom6kKNM701pb4p2omEy-hRXIM5yZE,10946
|
|
5
|
+
coretrace_python/engine.py,sha256=Ou1gEoHThoi7eAqv12kIV7Wy16t6hCNVqzAEplUXCCI,28202
|
|
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,7 +14,7 @@ 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=QFI8fj-k1xMtRDNEej9HvxEB9c40IME-ZTPzVwoCxKk,3838
|
|
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
20
|
coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml,sha256=4-OnbEb3lYVAoTfuz8mDPH4L9fHOPQ2KW1dDFAbClFk,267
|
|
@@ -38,7 +38,7 @@ coretrace_python/bundled/models/flask/plugin.toml,sha256=kI53Xwf5aZKd2n3fEQEbBzN
|
|
|
38
38
|
coretrace_python/bundled/models/http_clients/http_client_models.py,sha256=TmVIg66zRRQxx3C9RT2XX6xLhemm037hIIBFRSyKzU0,1273
|
|
39
39
|
coretrace_python/bundled/models/http_clients/plugin.toml,sha256=vq9tWHgy8-m8LiU1StGhm9Lsy7vnj3w90P4AKDaKfc0,216
|
|
40
40
|
coretrace_python/bundled/models/python_stdlib/plugin.toml,sha256=DWqjG68rx91HCJi_VEp6tUs4ujkzlR7Lq7saMvtK2HM,187
|
|
41
|
-
coretrace_python/bundled/models/python_stdlib/python_stdlib.py,sha256=
|
|
41
|
+
coretrace_python/bundled/models/python_stdlib/python_stdlib.py,sha256=3LmjcFx3rUhM3Mpw86q8bS5Xj6gRJs_p6M-8yBGiK2k,4632
|
|
42
42
|
coretrace_python/bundled/models/sqlalchemy/plugin.toml,sha256=2gRbglnMK5U2QWnzs2U2pDvmTeWmrlW7XpJ_8dRmIoY,182
|
|
43
43
|
coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py,sha256=Gm2qjyXUrjzALy-MigUhPQyyDxYdfWlQKDZLp1C6Jqk,1697
|
|
44
44
|
coretrace_python/bundled/models/tornado/plugin.toml,sha256=u_XjJK2uKBR11zg4yGxsCDHcx2pKEQ1MBvvB6Mm0siA,222
|
|
@@ -88,12 +88,13 @@ 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=
|
|
95
|
-
coretrace_python/dependency/policy.py,sha256=
|
|
91
|
+
coretrace_python/dependency/__init__.py,sha256=CXE36X2d5rio90NWlpjLo8Hly-tmzDCvu05FZAj_WXQ,1138
|
|
92
|
+
coretrace_python/dependency/advisories.py,sha256=tfR-5BmjV4GYcsF3CSGA4jSXNwOVG6mS8AqPfy2fWms,8653
|
|
93
|
+
coretrace_python/dependency/correlation.py,sha256=pR9S0tcdumdk2DMfUXxz6Wdul_RZE8FBfVmCUEhcA7w,7231
|
|
94
|
+
coretrace_python/dependency/graph.py,sha256=6UpAsqbtr5EoVGp6qkaQ7Gi1iUbGtulyh0hfpK_RqLg,16875
|
|
95
|
+
coretrace_python/dependency/policy.py,sha256=bsxZXL9xC0jnAzkzDh6ncnW8oKLG2H13jYmXF_q99Ng,2454
|
|
96
96
|
coretrace_python/dependency/sbom.py,sha256=h4K3Gb9O_D6uZyfpwpC5rPcNjH_Ib-3uvFxix6OKlfs,2520
|
|
97
|
+
coretrace_python/dependency/vex.py,sha256=ZPASwL6Var269WifrU7g1NRlDf-jxKKs8LzMo8dUkxI,7237
|
|
97
98
|
coretrace_python/findings/__init__.py,sha256=gnJI-uB2CkgZ8I5aFYP_N5yZb7FGNIy7LirVDh4NLwo,372
|
|
98
99
|
coretrace_python/findings/baseline.py,sha256=fbGQuULRsCjhxSNdwuLfwsvql9ZUfF3jqCXrRmIgOoQ,3468
|
|
99
100
|
coretrace_python/findings/coverage.py,sha256=U8URLEz6EN4B_TTQlLFCOtEmUbINt7a0Bexb-6OUFlI,1203
|
|
@@ -106,8 +107,8 @@ coretrace_python/frontend/parser.py,sha256=6-CG7rWxUYeOGeDP206SJWy4BMQUP9Mr6Jf-t
|
|
|
106
107
|
coretrace_python/hir/__init__.py,sha256=xMpvj-RSp6bd78a_hhkqbe99-snjERkH-_Lv5o9ASbw,180
|
|
107
108
|
coretrace_python/hir/nodes.py,sha256=4BI0f3UhKbnz9ZIxMoKXM7ujav0BmRpD0OXaoblRxIk,12364
|
|
108
109
|
coretrace_python/hir/visitors.py,sha256=JFnX09hK6trLyRcWgZ5ehOG2ywX3VvkqQ1h82nahYNg,885
|
|
109
|
-
coretrace_python/interprocedural/__init__.py,sha256=
|
|
110
|
-
coretrace_python/interprocedural/callgraph.py,sha256=
|
|
110
|
+
coretrace_python/interprocedural/__init__.py,sha256=SXtIGE3DD4fADIXLDuEvyT_3oIsh7Vio3BL4U6oUm28,1162
|
|
111
|
+
coretrace_python/interprocedural/callgraph.py,sha256=E6enTawqcl4-TObdjvoFe6l1zEedvsWbnjEBD6omzy8,19827
|
|
111
112
|
coretrace_python/interprocedural/modulegraph.py,sha256=4sB7OKjBYfxtm7aMtHm2jRlqFVUNUsdnLhnv9UZ1BWA,8846
|
|
112
113
|
coretrace_python/interprocedural/summaries.py,sha256=q--xpp1euaf5utxbK4CZtS8CSyLJ6p3MLel5Iu80X3w,20747
|
|
113
114
|
coretrace_python/ir/__init__.py,sha256=ShBPYYqoQFePP-mgBjGezQ2acel0-ReOHF9vCC-G_4Q,232
|
|
@@ -136,13 +137,13 @@ coretrace_python/semantic/symbols.py,sha256=3JptnnX5_KBlSd00L_wcGlYZQdnmBvSBAhfi
|
|
|
136
137
|
coretrace_python/source/__init__.py,sha256=0Y8fvtbkETnNCJR9Zq7RS5TSgF9c22pz3SSk1KdAXF0,301
|
|
137
138
|
coretrace_python/source/manager.py,sha256=JRmMOcWfS1nocbVrdcvfQTqEfIkmnb5JP857TpGywN4,2736
|
|
138
139
|
coretrace_python/source/model.py,sha256=FZnN5mlir3tzbIpWoxnn6YtRi9UcJ_y0sZM60tKr8U4,1727
|
|
139
|
-
coretrace_python/taint/__init__.py,sha256=
|
|
140
|
-
coretrace_python/taint/engine.py,sha256=
|
|
141
|
-
coretrace_python/taint/models.py,sha256=
|
|
140
|
+
coretrace_python/taint/__init__.py,sha256=UIJwnPnyYODsvnpe6-cmF8tOvKoGvWE7yVH3yIfthcM,1199
|
|
141
|
+
coretrace_python/taint/engine.py,sha256=UY59sb-sUWct3jzFHr6PBfJ_VFhZgb13tSyvD9nKqaM,38916
|
|
142
|
+
coretrace_python/taint/models.py,sha256=23TMzT149dCpRULO3RAyzbZWmayXWw4-a5iRjrqC0yo,13074
|
|
142
143
|
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.
|
|
144
|
+
coretrace_python_analyzer-0.6.0.dist-info/METADATA,sha256=WW3JCb3v2qbdWowN5kzDA1NRREuGErf-QysFU7k2kIU,3216
|
|
145
|
+
coretrace_python_analyzer-0.6.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
146
|
+
coretrace_python_analyzer-0.6.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
|
|
147
|
+
coretrace_python_analyzer-0.6.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
148
|
+
coretrace_python_analyzer-0.6.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
|
|
149
|
+
coretrace_python_analyzer-0.6.0.dist-info/RECORD,,
|
{coretrace_python_analyzer-0.5.0.dist-info → coretrace_python_analyzer-0.6.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|