coretrace-python-analyzer 0.1.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 +4 -0
- coretrace_python/__main__.py +4 -0
- coretrace_python/abstract/__init__.py +45 -0
- coretrace_python/abstract/constants.py +226 -0
- coretrace_python/abstract/heap.py +252 -0
- coretrace_python/abstract/ranges.py +285 -0
- coretrace_python/abstract/values.py +56 -0
- coretrace_python/analysis/__init__.py +31 -0
- coretrace_python/analysis/manager.py +165 -0
- coretrace_python/analysis/provider.py +73 -0
- coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
- coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
- coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
- coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
- coretrace_python/bundled/models/cli/cli_models.py +35 -0
- coretrace_python/bundled/models/cli/plugin.toml +9 -0
- coretrace_python/bundled/models/credentials/credential_models.py +43 -0
- coretrace_python/bundled/models/credentials/plugin.toml +9 -0
- coretrace_python/bundled/models/django/django_models.py +123 -0
- coretrace_python/bundled/models/django/plugin.toml +9 -0
- coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
- coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
- coretrace_python/bundled/models/flask/flask_models.py +53 -0
- coretrace_python/bundled/models/flask/plugin.toml +9 -0
- coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
- coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
- coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
- coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
- coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
- coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
- coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
- coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
- coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
- coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
- coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
- coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
- coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
- coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
- coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
- coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
- coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
- coretrace_python/bundled/security/xss/plugin.toml +9 -0
- coretrace_python/bundled/security/xss/xss.py +17 -0
- coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
- coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
- coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
- coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
- coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
- coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
- coretrace_python/cache.py +310 -0
- coretrace_python/cfg/__init__.py +46 -0
- coretrace_python/cfg/builder.py +589 -0
- coretrace_python/cfg/dominance.py +183 -0
- coretrace_python/cfg/model.py +166 -0
- coretrace_python/cli.py +216 -0
- coretrace_python/dataflow/__init__.py +29 -0
- coretrace_python/dataflow/lattice.py +78 -0
- coretrace_python/dataflow/solver.py +96 -0
- coretrace_python/dependency/__init__.py +44 -0
- coretrace_python/dependency/advisories.py +168 -0
- coretrace_python/dependency/correlation.py +87 -0
- coretrace_python/dependency/graph.py +274 -0
- coretrace_python/dependency/policy.py +65 -0
- coretrace_python/dependency/sbom.py +65 -0
- coretrace_python/engine.py +688 -0
- coretrace_python/findings/__init__.py +13 -0
- coretrace_python/findings/coverage.py +45 -0
- coretrace_python/findings/model.py +49 -0
- coretrace_python/findings/refutation.py +427 -0
- coretrace_python/frontend/__init__.py +18 -0
- coretrace_python/frontend/ast_adapter.py +447 -0
- coretrace_python/frontend/parser.py +23 -0
- coretrace_python/hir/__init__.py +5 -0
- coretrace_python/hir/nodes.py +584 -0
- coretrace_python/hir/visitors.py +36 -0
- coretrace_python/interprocedural/__init__.py +49 -0
- coretrace_python/interprocedural/callgraph.py +291 -0
- coretrace_python/interprocedural/modulegraph.py +218 -0
- coretrace_python/interprocedural/summaries.py +463 -0
- coretrace_python/ir/__init__.py +5 -0
- coretrace_python/ir/defuse.py +74 -0
- coretrace_python/ir/lowering.py +575 -0
- coretrace_python/ir/model.py +481 -0
- coretrace_python/ir/printer.py +201 -0
- coretrace_python/ir/ssa.py +277 -0
- coretrace_python/plugins/__init__.py +58 -0
- coretrace_python/plugins/api.py +153 -0
- coretrace_python/plugins/detectors.py +114 -0
- coretrace_python/plugins/loader.py +84 -0
- coretrace_python/plugins/manifest.py +112 -0
- coretrace_python/plugins/registry.py +34 -0
- coretrace_python/plugins/secrets.py +330 -0
- coretrace_python/reporters/__init__.py +22 -0
- coretrace_python/reporters/json_format.py +48 -0
- coretrace_python/reporters/report.py +23 -0
- coretrace_python/reporters/sarif.py +70 -0
- coretrace_python/reporters/text.py +24 -0
- coretrace_python/semantic/__init__.py +9 -0
- coretrace_python/semantic/identity.py +39 -0
- coretrace_python/semantic/imports.py +131 -0
- coretrace_python/semantic/scopes.py +473 -0
- coretrace_python/semantic/symbols.py +87 -0
- coretrace_python/source/__init__.py +13 -0
- coretrace_python/source/manager.py +81 -0
- coretrace_python/source/model.py +57 -0
- coretrace_python/taint/__init__.py +55 -0
- coretrace_python/taint/engine.py +800 -0
- coretrace_python/taint/models.py +317 -0
- coretrace_python/taint/routes.py +99 -0
- coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
- coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
- coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
- coretrace_python_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Dominance and post-dominance over control-flow graphs (architecture §5).
|
|
2
|
+
|
|
3
|
+
Both trees come from the same iterative algorithm (Cooper, Harvey and Kennedy) run on
|
|
4
|
+
the forward graph from the entry, or on the reversed graph from a virtual ``EXIT``
|
|
5
|
+
that follows every returning or raising block. The post-dominance frontier is the
|
|
6
|
+
control-dependence relation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
from typing import ClassVar
|
|
15
|
+
|
|
16
|
+
from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
|
|
17
|
+
from coretrace_python.cfg.builder import CFGAnalysis
|
|
18
|
+
from coretrace_python.cfg.model import CFG, BlockId
|
|
19
|
+
from coretrace_python.hir import nodes
|
|
20
|
+
|
|
21
|
+
EXIT = BlockId("<exit>")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class DominatorTree:
|
|
26
|
+
root: BlockId
|
|
27
|
+
blocks: tuple[BlockId, ...]
|
|
28
|
+
_idom: Mapping[BlockId, BlockId | None]
|
|
29
|
+
_children: Mapping[BlockId, tuple[BlockId, ...]]
|
|
30
|
+
_frontier: Mapping[BlockId, frozenset[BlockId]]
|
|
31
|
+
_depth: Mapping[BlockId, int] = field(init=False, repr=False, compare=False)
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
depth: dict[BlockId, int] = {}
|
|
35
|
+
for block in self.blocks:
|
|
36
|
+
parent = self._idom[block]
|
|
37
|
+
depth[block] = 0 if parent is None else depth[parent] + 1
|
|
38
|
+
object.__setattr__(self, "_depth", MappingProxyType(depth))
|
|
39
|
+
|
|
40
|
+
def idom(self, block: BlockId) -> BlockId | None:
|
|
41
|
+
return self._idom[block]
|
|
42
|
+
|
|
43
|
+
def children(self, block: BlockId) -> tuple[BlockId, ...]:
|
|
44
|
+
return self._children[block]
|
|
45
|
+
|
|
46
|
+
def frontier(self, block: BlockId) -> frozenset[BlockId]:
|
|
47
|
+
return self._frontier[block]
|
|
48
|
+
|
|
49
|
+
def dominates(self, dominator: BlockId, block: BlockId) -> bool:
|
|
50
|
+
if dominator not in self._idom or block not in self._idom:
|
|
51
|
+
return False
|
|
52
|
+
current: BlockId | None = block
|
|
53
|
+
while current is not None and self._depth[current] >= self._depth[dominator]:
|
|
54
|
+
if current == dominator:
|
|
55
|
+
return True
|
|
56
|
+
current = self._idom[current]
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def iterated_frontier(self, blocks: Iterable[BlockId]) -> frozenset[BlockId]:
|
|
60
|
+
result: set[BlockId] = set()
|
|
61
|
+
pending = list(blocks)
|
|
62
|
+
while pending:
|
|
63
|
+
for candidate in self._frontier[pending.pop()]:
|
|
64
|
+
if candidate not in result:
|
|
65
|
+
result.add(candidate)
|
|
66
|
+
pending.append(candidate)
|
|
67
|
+
return frozenset(result)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _build(
|
|
71
|
+
root: BlockId,
|
|
72
|
+
successors: Callable[[BlockId], tuple[BlockId, ...]],
|
|
73
|
+
order: Callable[[BlockId], int],
|
|
74
|
+
) -> DominatorTree:
|
|
75
|
+
# Reverse postorder over the reachable nodes.
|
|
76
|
+
postorder: list[BlockId] = []
|
|
77
|
+
seen: set[BlockId] = set()
|
|
78
|
+
|
|
79
|
+
def visit(node: BlockId) -> None:
|
|
80
|
+
seen.add(node)
|
|
81
|
+
for successor in successors(node):
|
|
82
|
+
if successor not in seen:
|
|
83
|
+
visit(successor)
|
|
84
|
+
postorder.append(node)
|
|
85
|
+
|
|
86
|
+
visit(root)
|
|
87
|
+
rpo = list(reversed(postorder))
|
|
88
|
+
index = {node: position for position, node in enumerate(rpo)}
|
|
89
|
+
predecessors: dict[BlockId, list[BlockId]] = {node: [] for node in rpo}
|
|
90
|
+
for node in rpo:
|
|
91
|
+
for successor in successors(node):
|
|
92
|
+
predecessors[successor].append(node)
|
|
93
|
+
|
|
94
|
+
idom: dict[BlockId, BlockId | None] = {root: None}
|
|
95
|
+
|
|
96
|
+
def intersect(a: BlockId, b: BlockId) -> BlockId:
|
|
97
|
+
while a != b:
|
|
98
|
+
while index[a] > index[b]:
|
|
99
|
+
a = idom[a] # type: ignore[assignment]
|
|
100
|
+
while index[b] > index[a]:
|
|
101
|
+
b = idom[b] # type: ignore[assignment]
|
|
102
|
+
return a
|
|
103
|
+
|
|
104
|
+
changed = True
|
|
105
|
+
while changed:
|
|
106
|
+
changed = False
|
|
107
|
+
for node in rpo[1:]:
|
|
108
|
+
candidates = [p for p in predecessors[node] if p in idom]
|
|
109
|
+
new_idom = candidates[0]
|
|
110
|
+
for other in candidates[1:]:
|
|
111
|
+
new_idom = intersect(other, new_idom)
|
|
112
|
+
if idom.get(node) != new_idom:
|
|
113
|
+
idom[node] = new_idom
|
|
114
|
+
changed = True
|
|
115
|
+
|
|
116
|
+
children: dict[BlockId, list[BlockId]] = {node: [] for node in rpo}
|
|
117
|
+
for node in sorted(rpo, key=order):
|
|
118
|
+
parent = idom[node]
|
|
119
|
+
if parent is not None:
|
|
120
|
+
children[parent].append(node)
|
|
121
|
+
|
|
122
|
+
frontier: dict[BlockId, set[BlockId]] = {node: set() for node in rpo}
|
|
123
|
+
for node in rpo:
|
|
124
|
+
if node == root or len(predecessors[node]) < 2:
|
|
125
|
+
continue
|
|
126
|
+
for predecessor in predecessors[node]:
|
|
127
|
+
runner: BlockId | None = predecessor
|
|
128
|
+
while runner is not None and runner != idom[node]:
|
|
129
|
+
frontier[runner].add(node)
|
|
130
|
+
runner = idom[runner]
|
|
131
|
+
|
|
132
|
+
preorder: list[BlockId] = []
|
|
133
|
+
|
|
134
|
+
def walk(node: BlockId) -> None:
|
|
135
|
+
preorder.append(node)
|
|
136
|
+
for child in children[node]:
|
|
137
|
+
walk(child)
|
|
138
|
+
|
|
139
|
+
walk(root)
|
|
140
|
+
return DominatorTree(
|
|
141
|
+
root=root,
|
|
142
|
+
blocks=tuple(preorder),
|
|
143
|
+
_idom=MappingProxyType(dict(idom)),
|
|
144
|
+
_children=MappingProxyType({k: tuple(v) for k, v in children.items()}),
|
|
145
|
+
_frontier=MappingProxyType({k: frozenset(v) for k, v in frontier.items()}),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def dominator_tree(cfg: CFG) -> DominatorTree:
|
|
150
|
+
order = {block_id: position for position, block_id in enumerate(cfg.blocks)}
|
|
151
|
+
return _build(cfg.entry, cfg.successors, lambda block: order[block])
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def post_dominator_tree(cfg: CFG) -> DominatorTree:
|
|
155
|
+
reachable = cfg.reachable()
|
|
156
|
+
order = {block_id: position for position, block_id in enumerate(cfg.blocks)}
|
|
157
|
+
order[EXIT] = -1
|
|
158
|
+
leaves = tuple(b for b in cfg.blocks if b in reachable and not cfg.successors(b))
|
|
159
|
+
|
|
160
|
+
def reversed_successors(block: BlockId) -> tuple[BlockId, ...]:
|
|
161
|
+
if block == EXIT:
|
|
162
|
+
return leaves
|
|
163
|
+
return tuple(p for p in cfg.predecessors(block) if p in reachable)
|
|
164
|
+
|
|
165
|
+
return _build(EXIT, reversed_successors, lambda block: order[block])
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class DominanceAnalysis(FunctionAnalysis[DominatorTree]):
|
|
169
|
+
name: ClassVar[str] = "cfg.dominance"
|
|
170
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({CFGAnalysis})
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> DominatorTree:
|
|
174
|
+
return dominator_tree(ctx.get(CFGAnalysis, function))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class PostDominanceAnalysis(FunctionAnalysis[DominatorTree]):
|
|
178
|
+
name: ClassVar[str] = "cfg.post_dominance"
|
|
179
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({CFGAnalysis})
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> DominatorTree:
|
|
183
|
+
return post_dominator_tree(ctx.get(CFGAnalysis, function))
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Control-flow graph model (architecture §5).
|
|
2
|
+
|
|
3
|
+
A block holds straight-line PyHIR statements and ends in exactly one terminator.
|
|
4
|
+
Python's iteration protocol stays explicit as ``ForEach`` instead of being encoded as
|
|
5
|
+
a generic switch; the CFG must remain semantically faithful to Python.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from types import MappingProxyType
|
|
13
|
+
from typing import TypeAlias
|
|
14
|
+
|
|
15
|
+
from coretrace_python.hir import nodes
|
|
16
|
+
from coretrace_python.source import SourceSpan
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CFGError(Exception):
|
|
20
|
+
"""A malformed graph or a control statement used outside its construct."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, order=True)
|
|
24
|
+
class BlockId:
|
|
25
|
+
value: str
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
return self.value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Branch:
|
|
33
|
+
condition: nodes.Expression
|
|
34
|
+
then_block: BlockId
|
|
35
|
+
else_block: BlockId
|
|
36
|
+
span: SourceSpan
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Jump:
|
|
41
|
+
target: BlockId
|
|
42
|
+
span: SourceSpan
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class Return:
|
|
47
|
+
value: nodes.Expression | None
|
|
48
|
+
span: SourceSpan
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class Raise:
|
|
53
|
+
exception: nodes.Expression | None
|
|
54
|
+
span: SourceSpan
|
|
55
|
+
cause: nodes.Expression | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class ForEach:
|
|
60
|
+
"""Bind the next item of ``iterable`` to ``target`` and enter ``body``, or ``exit``."""
|
|
61
|
+
|
|
62
|
+
target: nodes.Name
|
|
63
|
+
iterable: nodes.Expression
|
|
64
|
+
body: BlockId
|
|
65
|
+
exit: BlockId
|
|
66
|
+
span: SourceSpan
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
Terminator: TypeAlias = Branch | Jump | Return | Raise | ForEach
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def targets(terminator: Terminator) -> tuple[BlockId, ...]:
|
|
73
|
+
if isinstance(terminator, Branch):
|
|
74
|
+
return (terminator.then_block, terminator.else_block)
|
|
75
|
+
if isinstance(terminator, Jump):
|
|
76
|
+
return (terminator.target,)
|
|
77
|
+
if isinstance(terminator, ForEach):
|
|
78
|
+
return (terminator.body, terminator.exit)
|
|
79
|
+
return ()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class BasicBlock:
|
|
84
|
+
id: BlockId
|
|
85
|
+
statements: tuple[nodes.Statement, ...]
|
|
86
|
+
terminator: Terminator
|
|
87
|
+
exception_targets: tuple[BlockId, ...] = ()
|
|
88
|
+
"""Handler blocks control may reach if a statement of this block raises."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class CFG:
|
|
93
|
+
entry: BlockId
|
|
94
|
+
blocks: Mapping[BlockId, BasicBlock]
|
|
95
|
+
# Locals the builder introduced while laying out expression-level control flow
|
|
96
|
+
# (conditional expressions, comprehensions); the lowering treats them as locals.
|
|
97
|
+
synthetic_locals: frozenset[str] = frozenset()
|
|
98
|
+
_successors: Mapping[BlockId, tuple[BlockId, ...]] = field(
|
|
99
|
+
init=False, repr=False, compare=False
|
|
100
|
+
)
|
|
101
|
+
_predecessors: Mapping[BlockId, tuple[BlockId, ...]] = field(
|
|
102
|
+
init=False, repr=False, compare=False
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def __post_init__(self) -> None:
|
|
106
|
+
blocks = MappingProxyType(dict(self.blocks))
|
|
107
|
+
if self.entry not in blocks:
|
|
108
|
+
raise CFGError(f"entry block {self.entry} does not exist")
|
|
109
|
+
predecessors: dict[BlockId, list[BlockId]] = {block_id: [] for block_id in blocks}
|
|
110
|
+
successors: dict[BlockId, tuple[BlockId, ...]] = {}
|
|
111
|
+
for block_id, block in blocks.items():
|
|
112
|
+
if block.id != block_id:
|
|
113
|
+
raise CFGError(f"block {block.id} is stored under {block_id}")
|
|
114
|
+
found = list(targets(block.terminator))
|
|
115
|
+
found.extend(t for t in block.exception_targets if t not in found)
|
|
116
|
+
successors[block_id] = tuple(found)
|
|
117
|
+
for target in successors[block_id]:
|
|
118
|
+
if target not in blocks:
|
|
119
|
+
raise CFGError(f"block {block_id} targets unknown block {target}")
|
|
120
|
+
predecessors[target].append(block_id)
|
|
121
|
+
object.__setattr__(self, "blocks", blocks)
|
|
122
|
+
object.__setattr__(self, "_successors", MappingProxyType(successors))
|
|
123
|
+
object.__setattr__(
|
|
124
|
+
self,
|
|
125
|
+
"_predecessors",
|
|
126
|
+
MappingProxyType({k: tuple(v) for k, v in predecessors.items()}),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def block(self, block_id: BlockId) -> BasicBlock:
|
|
130
|
+
return self.blocks[block_id]
|
|
131
|
+
|
|
132
|
+
def successors(self, block_id: BlockId) -> tuple[BlockId, ...]:
|
|
133
|
+
return self._successors[block_id]
|
|
134
|
+
|
|
135
|
+
def predecessors(self, block_id: BlockId) -> tuple[BlockId, ...]:
|
|
136
|
+
return self._predecessors[block_id]
|
|
137
|
+
|
|
138
|
+
def reachable(self) -> frozenset[BlockId]:
|
|
139
|
+
seen: set[BlockId] = set()
|
|
140
|
+
pending = [self.entry]
|
|
141
|
+
while pending:
|
|
142
|
+
block_id = pending.pop()
|
|
143
|
+
if block_id not in seen:
|
|
144
|
+
seen.add(block_id)
|
|
145
|
+
pending.extend(self.successors(block_id))
|
|
146
|
+
return frozenset(seen)
|
|
147
|
+
|
|
148
|
+
def back_edges(self) -> frozenset[tuple[BlockId, BlockId]]:
|
|
149
|
+
"""Edges whose target is on the depth-first path from the entry to their source."""
|
|
150
|
+
|
|
151
|
+
found: set[tuple[BlockId, BlockId]] = set()
|
|
152
|
+
on_path: set[BlockId] = set()
|
|
153
|
+
finished: set[BlockId] = set()
|
|
154
|
+
|
|
155
|
+
def visit(block_id: BlockId) -> None:
|
|
156
|
+
on_path.add(block_id)
|
|
157
|
+
for successor in self.successors(block_id):
|
|
158
|
+
if successor in on_path:
|
|
159
|
+
found.add((block_id, successor))
|
|
160
|
+
elif successor not in finished:
|
|
161
|
+
visit(successor)
|
|
162
|
+
on_path.remove(block_id)
|
|
163
|
+
finished.add(block_id)
|
|
164
|
+
|
|
165
|
+
visit(self.entry)
|
|
166
|
+
return frozenset(found)
|
coretrace_python/cli.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
import zipfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from coretrace_python import __version__, engine
|
|
9
|
+
from coretrace_python.analysis import AnalysisError
|
|
10
|
+
from coretrace_python.cache import ProjectCache
|
|
11
|
+
from coretrace_python.cfg import CFGError
|
|
12
|
+
from coretrace_python.dependency import dump_advisories, import_osv, read_osv, render_sbom
|
|
13
|
+
from coretrace_python.frontend import HIRBuildError, ParseError, build_hir
|
|
14
|
+
from coretrace_python.ir.lowering import LoweringError, lower_module
|
|
15
|
+
from coretrace_python.ir.printer import format_module
|
|
16
|
+
from coretrace_python.plugins import IncompatiblePluginError, ManifestError
|
|
17
|
+
from coretrace_python.reporters import FORMATS, render
|
|
18
|
+
from coretrace_python.semantic.imports import ImportResolutionError
|
|
19
|
+
from coretrace_python.semantic.scopes import ScopeError
|
|
20
|
+
from coretrace_python.source import SourceManager
|
|
21
|
+
|
|
22
|
+
EXIT_CLEAN = 0
|
|
23
|
+
EXIT_FINDINGS = 1
|
|
24
|
+
EXIT_ERROR = 2
|
|
25
|
+
|
|
26
|
+
_ANALYSIS_ERRORS = (
|
|
27
|
+
OSError,
|
|
28
|
+
UnicodeError,
|
|
29
|
+
ParseError,
|
|
30
|
+
HIRBuildError,
|
|
31
|
+
ImportResolutionError,
|
|
32
|
+
ScopeError,
|
|
33
|
+
CFGError,
|
|
34
|
+
LoweringError,
|
|
35
|
+
AnalysisError,
|
|
36
|
+
ManifestError,
|
|
37
|
+
IncompatiblePluginError,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
42
|
+
parser = argparse.ArgumentParser(
|
|
43
|
+
prog=engine.TOOL_NAME,
|
|
44
|
+
description="Analyze Python source with CoreTrace.",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"path", type=Path, nargs="?", help="Python source file, or a directory for --check"
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--emit-ir",
|
|
51
|
+
action="store_true",
|
|
52
|
+
help="print the lowered Python intermediate representation",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--ssa",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="with --emit-ir, print the static single assignment form",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--check",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help="run the loaded plugins and report their findings",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--plugins",
|
|
66
|
+
action="append",
|
|
67
|
+
type=Path,
|
|
68
|
+
default=[],
|
|
69
|
+
metavar="DIR",
|
|
70
|
+
help="with --check, a directory searched recursively for plugin.toml, loaded on "
|
|
71
|
+
"top of the bundled plugins (repeatable)",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--no-bundled-plugins",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="with --check, do not load the plugins shipped with the package",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--format",
|
|
80
|
+
choices=sorted(FORMATS),
|
|
81
|
+
default=None,
|
|
82
|
+
help="with --check, the report format (default: text)",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--cache",
|
|
86
|
+
type=Path,
|
|
87
|
+
default=None,
|
|
88
|
+
metavar="DIR",
|
|
89
|
+
help="with --check on a directory, keep per-module results under DIR and reuse "
|
|
90
|
+
"them for modules unchanged since the previous run",
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--jobs",
|
|
94
|
+
type=int,
|
|
95
|
+
default=None,
|
|
96
|
+
metavar="N",
|
|
97
|
+
help="with --check on a directory, analyse independent modules in N processes",
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--sbom",
|
|
101
|
+
type=Path,
|
|
102
|
+
default=None,
|
|
103
|
+
metavar="PATH",
|
|
104
|
+
help="with --check on a directory, write a CycloneDX bill of materials to PATH",
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--advisories",
|
|
108
|
+
action="append",
|
|
109
|
+
type=Path,
|
|
110
|
+
default=[],
|
|
111
|
+
metavar="FILE",
|
|
112
|
+
help="with --check on a directory, a local advisory file to read in addition to "
|
|
113
|
+
"advisories.json at the root (repeatable)",
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--policy",
|
|
117
|
+
type=Path,
|
|
118
|
+
default=None,
|
|
119
|
+
metavar="FILE",
|
|
120
|
+
help="with --check on a directory, the dependency policy to apply instead of "
|
|
121
|
+
"coretrace-policy.toml at the root",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--import-advisories",
|
|
125
|
+
nargs=2,
|
|
126
|
+
type=Path,
|
|
127
|
+
default=None,
|
|
128
|
+
metavar=("SRC", "OUT"),
|
|
129
|
+
help="convert an OSV dump (a JSON file, a directory of them or a zip archive) "
|
|
130
|
+
"into the local advisory file OUT, then exit",
|
|
131
|
+
)
|
|
132
|
+
return parser
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main(argv: list[str] | None = None) -> int:
|
|
136
|
+
args = build_parser().parse_args(argv)
|
|
137
|
+
if args.import_advisories is not None:
|
|
138
|
+
source, out = args.import_advisories
|
|
139
|
+
try:
|
|
140
|
+
out.write_text(dump_advisories(import_osv(read_osv(source))), encoding="utf-8")
|
|
141
|
+
except (OSError, ValueError, zipfile.BadZipFile) as error:
|
|
142
|
+
print(f"error: {error}", file=sys.stderr)
|
|
143
|
+
return EXIT_ERROR
|
|
144
|
+
return EXIT_CLEAN
|
|
145
|
+
if not (args.emit_ir or args.check):
|
|
146
|
+
print("error: no action selected; pass --emit-ir or --check", file=sys.stderr)
|
|
147
|
+
return EXIT_ERROR
|
|
148
|
+
if args.path is None:
|
|
149
|
+
print("error: a path is required", file=sys.stderr)
|
|
150
|
+
return EXIT_ERROR
|
|
151
|
+
if args.format is not None and not args.check:
|
|
152
|
+
print("error: --format only applies to --check", file=sys.stderr)
|
|
153
|
+
return EXIT_ERROR
|
|
154
|
+
if args.cache is not None and not (args.check and args.path.is_dir()):
|
|
155
|
+
print("error: --cache only applies to --check on a directory", file=sys.stderr)
|
|
156
|
+
return EXIT_ERROR
|
|
157
|
+
if args.jobs is not None and not (args.check and args.path.is_dir()):
|
|
158
|
+
print("error: --jobs only applies to --check on a directory", file=sys.stderr)
|
|
159
|
+
return EXIT_ERROR
|
|
160
|
+
if args.jobs is not None and args.jobs < 1:
|
|
161
|
+
print("error: --jobs must be at least 1", file=sys.stderr)
|
|
162
|
+
return EXIT_ERROR
|
|
163
|
+
if args.sbom is not None and not (args.check and args.path.is_dir()):
|
|
164
|
+
print("error: --sbom only applies to --check on a directory", file=sys.stderr)
|
|
165
|
+
return EXIT_ERROR
|
|
166
|
+
if args.advisories and not (args.check and args.path.is_dir()):
|
|
167
|
+
print("error: --advisories only applies to --check on a directory", file=sys.stderr)
|
|
168
|
+
return EXIT_ERROR
|
|
169
|
+
if args.policy is not None and not (args.check and args.path.is_dir()):
|
|
170
|
+
print("error: --policy only applies to --check on a directory", file=sys.stderr)
|
|
171
|
+
return EXIT_ERROR
|
|
172
|
+
|
|
173
|
+
if args.no_bundled_plugins and not args.check:
|
|
174
|
+
print("error: --no-bundled-plugins only applies to --check", file=sys.stderr)
|
|
175
|
+
return EXIT_ERROR
|
|
176
|
+
plugin_roots = ([] if args.no_bundled_plugins else [engine.BUNDLED_PLUGINS]) + list(args.plugins)
|
|
177
|
+
|
|
178
|
+
if args.path.is_dir() and args.emit_ir:
|
|
179
|
+
print(f"error: {args.path} is a directory; --emit-ir needs a file", file=sys.stderr)
|
|
180
|
+
return EXIT_ERROR
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
if args.emit_ir:
|
|
184
|
+
source = SourceManager().load_file(args.path)
|
|
185
|
+
print(format_module(lower_module(build_hir(source), ssa=args.ssa)))
|
|
186
|
+
if args.check:
|
|
187
|
+
if args.path.is_dir():
|
|
188
|
+
cache = None if args.cache is None else ProjectCache(args.cache)
|
|
189
|
+
analysis = engine.analyze_project(
|
|
190
|
+
args.path,
|
|
191
|
+
plugin_roots,
|
|
192
|
+
cache=cache,
|
|
193
|
+
jobs=args.jobs or 1,
|
|
194
|
+
advisory_files=args.advisories,
|
|
195
|
+
policy_file=args.policy,
|
|
196
|
+
)
|
|
197
|
+
findings = analysis.findings
|
|
198
|
+
coverage = analysis.coverage
|
|
199
|
+
if args.sbom is not None:
|
|
200
|
+
args.sbom.write_text(
|
|
201
|
+
render_sbom(analysis.dependencies, analysis.advisories, engine.TOOL_NAME, __version__),
|
|
202
|
+
encoding="utf-8",
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
file_analysis = engine.analyze_file(SourceManager().load_file(args.path), plugin_roots)
|
|
206
|
+
findings, coverage = file_analysis.findings, file_analysis.coverage
|
|
207
|
+
print(render(args.format or "text", engine.report(findings, coverage)), end="")
|
|
208
|
+
return EXIT_FINDINGS if findings else EXIT_CLEAN
|
|
209
|
+
except _ANALYSIS_ERRORS as error:
|
|
210
|
+
print(f"error: {error}", file=sys.stderr)
|
|
211
|
+
return EXIT_ERROR
|
|
212
|
+
return EXIT_CLEAN
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Generic data-flow framework: lattices and a bidirectional worklist solver."""
|
|
2
|
+
|
|
3
|
+
from coretrace_python.dataflow.lattice import (
|
|
4
|
+
BOTTOM,
|
|
5
|
+
TOP,
|
|
6
|
+
Bottom,
|
|
7
|
+
Element,
|
|
8
|
+
FlatLattice,
|
|
9
|
+
Lattice,
|
|
10
|
+
Top,
|
|
11
|
+
same_element,
|
|
12
|
+
)
|
|
13
|
+
from coretrace_python.dataflow.solver import ENTRY, DataflowProblem, Direction, Solution, solve
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BOTTOM",
|
|
17
|
+
"ENTRY",
|
|
18
|
+
"TOP",
|
|
19
|
+
"Bottom",
|
|
20
|
+
"DataflowProblem",
|
|
21
|
+
"Direction",
|
|
22
|
+
"Element",
|
|
23
|
+
"FlatLattice",
|
|
24
|
+
"Lattice",
|
|
25
|
+
"Solution",
|
|
26
|
+
"Top",
|
|
27
|
+
"same_element",
|
|
28
|
+
"solve",
|
|
29
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Lattice vocabulary shared by data-flow problems (architecture §37 dataflow/lattice).
|
|
2
|
+
|
|
3
|
+
``BOTTOM`` means "no information yet" and ``TOP`` means "anything". A ``FlatLattice``
|
|
4
|
+
places every other element between the two: equal elements join to themselves, different
|
|
5
|
+
ones to ``TOP``. Elements are compared by type as well as value, so ``1`` and ``True``
|
|
6
|
+
are different constants.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Final, Generic, Protocol, TypeAlias, TypeVar
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Bottom:
|
|
15
|
+
__slots__ = ()
|
|
16
|
+
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
return "BOTTOM"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Top:
|
|
22
|
+
__slots__ = ()
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
return "TOP"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
BOTTOM: Final = Bottom()
|
|
29
|
+
TOP: Final = Top()
|
|
30
|
+
|
|
31
|
+
T = TypeVar("T")
|
|
32
|
+
Element: TypeAlias = T | Bottom | Top
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def same_element(a: object, b: object) -> bool:
|
|
36
|
+
"""Equality that also requires equal types, so ``1 == True`` does not conflate."""
|
|
37
|
+
|
|
38
|
+
return type(a) is type(b) and bool(a == b)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Lattice(Protocol[T]):
|
|
42
|
+
@property
|
|
43
|
+
def bottom(self) -> Element[T]: ...
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def top(self) -> Element[T]: ...
|
|
47
|
+
|
|
48
|
+
def join(self, a: Element[T], b: Element[T]) -> Element[T]: ...
|
|
49
|
+
|
|
50
|
+
def leq(self, a: Element[T], b: Element[T]) -> bool: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class FlatLattice(Generic[T]):
|
|
54
|
+
"""Bottom < every element < Top, with no order between elements."""
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def bottom(self) -> Element[T]:
|
|
58
|
+
return BOTTOM
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def top(self) -> Element[T]:
|
|
62
|
+
return TOP
|
|
63
|
+
|
|
64
|
+
def join(self, a: Element[T], b: Element[T]) -> Element[T]:
|
|
65
|
+
if a is BOTTOM:
|
|
66
|
+
return b
|
|
67
|
+
if b is BOTTOM:
|
|
68
|
+
return a
|
|
69
|
+
if a is TOP or b is TOP:
|
|
70
|
+
return TOP
|
|
71
|
+
return a if same_element(a, b) else TOP
|
|
72
|
+
|
|
73
|
+
def leq(self, a: Element[T], b: Element[T]) -> bool:
|
|
74
|
+
if a is BOTTOM or b is TOP:
|
|
75
|
+
return True
|
|
76
|
+
if a is TOP or b is BOTTOM:
|
|
77
|
+
return False
|
|
78
|
+
return same_element(a, b)
|