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,277 @@
|
|
|
1
|
+
"""SSA construction (architecture §7).
|
|
2
|
+
|
|
3
|
+
``to_ssa`` derives a new immutable ``FunctionIR`` from the non-SSA PyIR of a function:
|
|
4
|
+
local loads and stores disappear, merges get ``Phi`` instructions placed on the iterated
|
|
5
|
+
dominance frontier of the definitions and pruned by liveness, a ``for_next`` terminator
|
|
6
|
+
defines the loop variable, and a local read before any definition on some path reads
|
|
7
|
+
an explicit ``Undefined`` value created once in the entry block. Only reachable blocks
|
|
8
|
+
are kept, and values are renumbered densely in block order.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from dataclasses import replace
|
|
15
|
+
from typing import ClassVar
|
|
16
|
+
|
|
17
|
+
from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
|
|
18
|
+
from coretrace_python.cfg import BlockId
|
|
19
|
+
from coretrace_python.cfg.dominance import DominanceAnalysis, DominatorTree
|
|
20
|
+
from coretrace_python.hir import nodes
|
|
21
|
+
from coretrace_python.ir.lowering import PyIRAnalysis
|
|
22
|
+
from coretrace_python.ir.model import (
|
|
23
|
+
BasicBlock,
|
|
24
|
+
Branch,
|
|
25
|
+
ForNext,
|
|
26
|
+
FunctionIR,
|
|
27
|
+
Instruction,
|
|
28
|
+
Jump,
|
|
29
|
+
LoadLocal,
|
|
30
|
+
Phi,
|
|
31
|
+
StoreLocal,
|
|
32
|
+
Terminator,
|
|
33
|
+
Undefined,
|
|
34
|
+
Value,
|
|
35
|
+
substitute,
|
|
36
|
+
successors,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _Renamer:
|
|
41
|
+
def __init__(self, function: FunctionIR, tree: DominatorTree) -> None:
|
|
42
|
+
self.function = function
|
|
43
|
+
self.tree = tree
|
|
44
|
+
self.blocks = {block.id: block for block in function.blocks if block.id in tree.blocks}
|
|
45
|
+
self.next_id = max((v.id for v in _all_values(function)), default=-1) + 1
|
|
46
|
+
self.stacks: dict[str, list[Value]] = {}
|
|
47
|
+
self.aliases: dict[Value, Value] = {}
|
|
48
|
+
self.undefined: dict[str, Undefined] = {}
|
|
49
|
+
self.phis: dict[BlockId, dict[str, Phi]] = {}
|
|
50
|
+
self.instructions: dict[BlockId, list[Instruction]] = {}
|
|
51
|
+
self.terminators: dict[BlockId, Terminator] = {}
|
|
52
|
+
self.predecessors: dict[BlockId, list[BlockId]] = {b: [] for b in self.blocks}
|
|
53
|
+
for block in self.blocks.values():
|
|
54
|
+
for successor in successors(block):
|
|
55
|
+
self.predecessors[successor].append(block.id)
|
|
56
|
+
self.loop_variables: dict[BlockId, tuple[str, BlockId]] = {}
|
|
57
|
+
for block in self.blocks.values():
|
|
58
|
+
if isinstance(block.terminator, ForNext):
|
|
59
|
+
self.loop_variables[block.terminator.body] = (block.terminator.target, block.id)
|
|
60
|
+
|
|
61
|
+
# ------------------------------------------------------------------ helpers
|
|
62
|
+
|
|
63
|
+
def new_value(self) -> Value:
|
|
64
|
+
value = Value(self.next_id)
|
|
65
|
+
self.next_id += 1
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
def current(self, name: str) -> Value:
|
|
69
|
+
stack = self.stacks.get(name)
|
|
70
|
+
if stack:
|
|
71
|
+
return stack[-1]
|
|
72
|
+
if name not in self.undefined:
|
|
73
|
+
self.undefined[name] = Undefined(self.new_value(), self.function.location, name)
|
|
74
|
+
return self.undefined[name].result
|
|
75
|
+
|
|
76
|
+
def resolve(self, value: Value) -> Value:
|
|
77
|
+
return self.aliases.get(value, value)
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------ phi placement
|
|
80
|
+
|
|
81
|
+
def place_phis(self) -> None:
|
|
82
|
+
definitions: dict[str, set[BlockId]] = {}
|
|
83
|
+
for block in self.blocks.values():
|
|
84
|
+
for instruction in block.instructions:
|
|
85
|
+
if isinstance(instruction, StoreLocal):
|
|
86
|
+
definitions.setdefault(instruction.name, set()).add(block.id)
|
|
87
|
+
if isinstance(block.terminator, ForNext):
|
|
88
|
+
definitions.setdefault(block.terminator.target, set()).add(block.terminator.body)
|
|
89
|
+
live_in = _live_in(self.blocks, self.predecessors, self.loop_variables)
|
|
90
|
+
for name, blocks in definitions.items():
|
|
91
|
+
for block_id in sorted(self.tree.iterated_frontier(blocks), key=self.order):
|
|
92
|
+
if name in live_in[block_id]:
|
|
93
|
+
self.phis.setdefault(block_id, {})[name] = Phi(
|
|
94
|
+
self.new_value(), self.blocks[block_id].terminator.location, name, ()
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def order(self, block_id: BlockId) -> int:
|
|
98
|
+
return self.tree.blocks.index(block_id)
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------ renaming
|
|
101
|
+
|
|
102
|
+
def rename(self, block_id: BlockId) -> None:
|
|
103
|
+
pushed: list[str] = []
|
|
104
|
+
|
|
105
|
+
def push(name: str, value: Value) -> None:
|
|
106
|
+
self.stacks.setdefault(name, []).append(value)
|
|
107
|
+
pushed.append(name)
|
|
108
|
+
|
|
109
|
+
if block_id in self.loop_variables:
|
|
110
|
+
name, header = self.loop_variables[block_id]
|
|
111
|
+
header_terminator = self.terminators[header]
|
|
112
|
+
assert isinstance(header_terminator, ForNext) and header_terminator.result is not None
|
|
113
|
+
push(name, header_terminator.result)
|
|
114
|
+
for name, phi in self.phis.get(block_id, {}).items():
|
|
115
|
+
push(name, phi.result)
|
|
116
|
+
|
|
117
|
+
kept: list[Instruction] = []
|
|
118
|
+
for instruction in self.blocks[block_id].instructions:
|
|
119
|
+
if isinstance(instruction, LoadLocal):
|
|
120
|
+
self.aliases[instruction.result] = self.current(instruction.name)
|
|
121
|
+
elif isinstance(instruction, StoreLocal):
|
|
122
|
+
push(instruction.name, self.resolve(instruction.value))
|
|
123
|
+
else:
|
|
124
|
+
kept.append(substitute(instruction, self.resolve))
|
|
125
|
+
self.instructions[block_id] = kept
|
|
126
|
+
|
|
127
|
+
terminator = substitute(self.blocks[block_id].terminator, self.resolve)
|
|
128
|
+
if isinstance(terminator, ForNext):
|
|
129
|
+
terminator = replace(terminator, result=self.new_value())
|
|
130
|
+
self.terminators[block_id] = terminator
|
|
131
|
+
|
|
132
|
+
for successor in (*_targets(terminator), *self.blocks[block_id].exception_targets):
|
|
133
|
+
for name, phi in self.phis.get(successor, {}).items():
|
|
134
|
+
incoming = (*phi.incoming, (self.current(name), block_id))
|
|
135
|
+
self.phis[successor][name] = replace(phi, incoming=incoming)
|
|
136
|
+
|
|
137
|
+
for child in self.tree.children(block_id):
|
|
138
|
+
self.rename(child)
|
|
139
|
+
for name in reversed(pushed):
|
|
140
|
+
self.stacks[name].pop()
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------ assembly
|
|
143
|
+
|
|
144
|
+
def build(self) -> FunctionIR:
|
|
145
|
+
self.place_phis()
|
|
146
|
+
self.rename(self.tree.root)
|
|
147
|
+
blocks: list[BasicBlock] = []
|
|
148
|
+
for block in self.function.blocks:
|
|
149
|
+
if block.id not in self.blocks:
|
|
150
|
+
continue
|
|
151
|
+
instructions: list[Instruction] = []
|
|
152
|
+
if block.id == self.function.entry:
|
|
153
|
+
instructions.extend(self.undefined.values())
|
|
154
|
+
instructions.extend(self.ordered_phis(block.id))
|
|
155
|
+
instructions.extend(self.instructions[block.id])
|
|
156
|
+
blocks.append(
|
|
157
|
+
BasicBlock(
|
|
158
|
+
block.id, tuple(instructions), self.terminators[block.id], block.exception_targets
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
return _renumber(replace(self.function, blocks=tuple(blocks)))
|
|
162
|
+
|
|
163
|
+
def ordered_phis(self, block_id: BlockId) -> list[Phi]:
|
|
164
|
+
phis = self.phis.get(block_id, {})
|
|
165
|
+
ordered_incoming = []
|
|
166
|
+
for phi in phis.values():
|
|
167
|
+
by_block = {b: v for v, b in phi.incoming}
|
|
168
|
+
incoming = tuple((by_block[p], p) for p in self.predecessors[block_id])
|
|
169
|
+
ordered_incoming.append(replace(phi, incoming=incoming))
|
|
170
|
+
return ordered_incoming
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _targets(terminator: Terminator) -> tuple[BlockId, ...]:
|
|
174
|
+
if isinstance(terminator, Branch):
|
|
175
|
+
return (terminator.then_block, terminator.else_block)
|
|
176
|
+
if isinstance(terminator, Jump):
|
|
177
|
+
return (terminator.target,)
|
|
178
|
+
if isinstance(terminator, ForNext):
|
|
179
|
+
return (terminator.body, terminator.exit)
|
|
180
|
+
return ()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _all_values(function: FunctionIR) -> list[Value]:
|
|
184
|
+
values = list(function.parameters)
|
|
185
|
+
for block in function.blocks:
|
|
186
|
+
for instruction in block.instructions:
|
|
187
|
+
if instruction.result is not None:
|
|
188
|
+
values.append(instruction.result)
|
|
189
|
+
return values
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _live_in(
|
|
193
|
+
blocks: Mapping[BlockId, BasicBlock],
|
|
194
|
+
predecessors: Mapping[BlockId, list[BlockId]],
|
|
195
|
+
loop_variables: Mapping[BlockId, tuple[str, BlockId]],
|
|
196
|
+
) -> dict[BlockId, frozenset[str]]:
|
|
197
|
+
"""Backward liveness of local names over the non-SSA blocks."""
|
|
198
|
+
|
|
199
|
+
uses: dict[BlockId, set[str]] = {}
|
|
200
|
+
defs: dict[BlockId, set[str]] = {}
|
|
201
|
+
for block_id, block in blocks.items():
|
|
202
|
+
used: set[str] = set()
|
|
203
|
+
defined: set[str] = set()
|
|
204
|
+
if block_id in loop_variables:
|
|
205
|
+
defined.add(loop_variables[block_id][0])
|
|
206
|
+
for instruction in block.instructions:
|
|
207
|
+
if isinstance(instruction, LoadLocal) and instruction.name not in defined:
|
|
208
|
+
used.add(instruction.name)
|
|
209
|
+
elif isinstance(instruction, StoreLocal):
|
|
210
|
+
defined.add(instruction.name)
|
|
211
|
+
uses[block_id], defs[block_id] = used, defined
|
|
212
|
+
|
|
213
|
+
successors: dict[BlockId, list[BlockId]] = {b: [] for b in blocks}
|
|
214
|
+
for block_id, preds in predecessors.items():
|
|
215
|
+
for predecessor in preds:
|
|
216
|
+
successors[predecessor].append(block_id)
|
|
217
|
+
|
|
218
|
+
live_in: dict[BlockId, frozenset[str]] = {b: frozenset() for b in blocks}
|
|
219
|
+
changed = True
|
|
220
|
+
while changed:
|
|
221
|
+
changed = False
|
|
222
|
+
for block_id in blocks:
|
|
223
|
+
live_out: set[str] = set()
|
|
224
|
+
for successor in successors[block_id]:
|
|
225
|
+
live_out |= live_in[successor]
|
|
226
|
+
new = frozenset(uses[block_id] | (live_out - defs[block_id]))
|
|
227
|
+
if new != live_in[block_id]:
|
|
228
|
+
live_in[block_id] = new
|
|
229
|
+
changed = True
|
|
230
|
+
return live_in
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _renumber(function: FunctionIR) -> FunctionIR:
|
|
234
|
+
mapping: dict[Value, Value] = {}
|
|
235
|
+
|
|
236
|
+
def fresh(value: Value) -> None:
|
|
237
|
+
mapping[value] = Value(len(mapping))
|
|
238
|
+
|
|
239
|
+
for parameter in function.parameters:
|
|
240
|
+
fresh(parameter)
|
|
241
|
+
for block in function.blocks:
|
|
242
|
+
for instruction in block.instructions:
|
|
243
|
+
if instruction.result is not None:
|
|
244
|
+
fresh(instruction.result)
|
|
245
|
+
if isinstance(block.terminator, ForNext) and block.terminator.result is not None:
|
|
246
|
+
fresh(block.terminator.result)
|
|
247
|
+
|
|
248
|
+
def renamed(value: Value) -> Value:
|
|
249
|
+
return mapping[value]
|
|
250
|
+
|
|
251
|
+
blocks = tuple(
|
|
252
|
+
BasicBlock(
|
|
253
|
+
block.id,
|
|
254
|
+
tuple(substitute(i, renamed, include_result=True) for i in block.instructions),
|
|
255
|
+
substitute(block.terminator, renamed, include_result=True),
|
|
256
|
+
block.exception_targets,
|
|
257
|
+
)
|
|
258
|
+
for block in function.blocks
|
|
259
|
+
)
|
|
260
|
+
return replace(
|
|
261
|
+
function, parameters=tuple(renamed(p) for p in function.parameters), blocks=blocks
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def to_ssa(function: FunctionIR, dominance: DominatorTree) -> FunctionIR:
|
|
266
|
+
return _Renamer(function, dominance).build()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class SSAAnalysis(FunctionAnalysis[FunctionIR]):
|
|
270
|
+
"""SSA form of a function, derived from its PyIR and dominator tree."""
|
|
271
|
+
|
|
272
|
+
name: ClassVar[str] = "ir.ssa"
|
|
273
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({PyIRAnalysis, DominanceAnalysis})
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> FunctionIR:
|
|
277
|
+
return to_ssa(ctx.get(PyIRAnalysis, function), ctx.get(DominanceAnalysis, function))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Plugin API, manifests, loader and registry (architecture §13, §14, §32–§34)."""
|
|
2
|
+
|
|
3
|
+
from coretrace_python.plugins.api import (
|
|
4
|
+
PLUGIN_API_VERSION,
|
|
5
|
+
ModelPlugin,
|
|
6
|
+
Plugin,
|
|
7
|
+
PluginContext,
|
|
8
|
+
ProjectContext,
|
|
9
|
+
ProjectPlugin,
|
|
10
|
+
run_plugins,
|
|
11
|
+
)
|
|
12
|
+
from coretrace_python.plugins.detectors import SymbolCallDetector, TaintDetector
|
|
13
|
+
from coretrace_python.plugins.loader import (
|
|
14
|
+
IncompatiblePluginError,
|
|
15
|
+
LoadedPlugin,
|
|
16
|
+
discover_plugins,
|
|
17
|
+
load_plugin,
|
|
18
|
+
)
|
|
19
|
+
from coretrace_python.plugins.manifest import (
|
|
20
|
+
Entrypoint,
|
|
21
|
+
ManifestError,
|
|
22
|
+
PluginManifest,
|
|
23
|
+
VersionRange,
|
|
24
|
+
load_manifest,
|
|
25
|
+
)
|
|
26
|
+
from coretrace_python.plugins.registry import PluginRegistry
|
|
27
|
+
from coretrace_python.plugins.secrets import (
|
|
28
|
+
SecretDetector,
|
|
29
|
+
SecretPattern,
|
|
30
|
+
literals,
|
|
31
|
+
shannon_entropy,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"PLUGIN_API_VERSION",
|
|
36
|
+
"Entrypoint",
|
|
37
|
+
"IncompatiblePluginError",
|
|
38
|
+
"LoadedPlugin",
|
|
39
|
+
"ManifestError",
|
|
40
|
+
"ModelPlugin",
|
|
41
|
+
"Plugin",
|
|
42
|
+
"PluginContext",
|
|
43
|
+
"PluginManifest",
|
|
44
|
+
"PluginRegistry",
|
|
45
|
+
"ProjectContext",
|
|
46
|
+
"ProjectPlugin",
|
|
47
|
+
"SecretDetector",
|
|
48
|
+
"SecretPattern",
|
|
49
|
+
"SymbolCallDetector",
|
|
50
|
+
"TaintDetector",
|
|
51
|
+
"VersionRange",
|
|
52
|
+
"discover_plugins",
|
|
53
|
+
"literals",
|
|
54
|
+
"load_manifest",
|
|
55
|
+
"load_plugin",
|
|
56
|
+
"run_plugins",
|
|
57
|
+
"shannon_entropy",
|
|
58
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""The typed plugin contract (architecture §13, §32, §33, §34).
|
|
2
|
+
|
|
3
|
+
A plugin declares the analyses it needs and consumes them through a ``PluginContext``,
|
|
4
|
+
a deliberately narrow view of the engine: the module, its functions and the declared
|
|
5
|
+
analyses. Nothing else is reachable, which keeps plugins on the stable API and leaves
|
|
6
|
+
room for out-of-process isolation later.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, ClassVar, overload
|
|
15
|
+
|
|
16
|
+
from coretrace_python.analysis import (
|
|
17
|
+
Analysis,
|
|
18
|
+
AnalysisManager,
|
|
19
|
+
AnyAnalysis,
|
|
20
|
+
FunctionAnalysis,
|
|
21
|
+
UndeclaredDependencyError,
|
|
22
|
+
)
|
|
23
|
+
from coretrace_python.analysis.provider import R
|
|
24
|
+
from coretrace_python.dependency import Advisory, DependencyGraph, Policy
|
|
25
|
+
from coretrace_python.findings import Finding
|
|
26
|
+
from coretrace_python.hir import nodes
|
|
27
|
+
from coretrace_python.interprocedural import CallGraph, CallGraphAnalysis, ModuleGraph
|
|
28
|
+
from coretrace_python.ir.lowering import analyzable_functions
|
|
29
|
+
from coretrace_python.semantic.imports import ImportAnalysis, ImportTable
|
|
30
|
+
from coretrace_python.taint import Model
|
|
31
|
+
|
|
32
|
+
PLUGIN_API_VERSION = 1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Plugin(ABC):
|
|
36
|
+
"""Declares the analyses it needs and the security models it contributes,
|
|
37
|
+
then turns analysis results into findings."""
|
|
38
|
+
|
|
39
|
+
name: ClassVar[str]
|
|
40
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset()
|
|
41
|
+
models: ClassVar[tuple[Model, ...]] = ()
|
|
42
|
+
advisories: ClassVar[tuple[Advisory, ...]] = ()
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ModelPlugin(Plugin):
|
|
50
|
+
"""A plugin that only contributes security models or advisories (§15 providers)."""
|
|
51
|
+
|
|
52
|
+
def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
|
|
53
|
+
return ()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ProjectContext:
|
|
57
|
+
"""What a project-scoped plugin sees: the module graph, the dependency graph, the
|
|
58
|
+
advisories every plugin and advisory file contributed, the dependency policy, and
|
|
59
|
+
each module's imports and call graph. The engine passes the call graphs of modules
|
|
60
|
+
it served from its cache."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
graph: ModuleGraph,
|
|
65
|
+
dependencies: DependencyGraph,
|
|
66
|
+
advisories: tuple[Advisory, ...],
|
|
67
|
+
managers: Mapping[str, AnalysisManager],
|
|
68
|
+
call_graphs: Mapping[str, CallGraph] | None = None,
|
|
69
|
+
policy: Policy | None = None,
|
|
70
|
+
root: Path | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
self.root = root
|
|
73
|
+
self.graph = graph
|
|
74
|
+
self.dependencies = dependencies
|
|
75
|
+
self.advisories = advisories
|
|
76
|
+
self.policy = policy or Policy()
|
|
77
|
+
self._managers = managers
|
|
78
|
+
self._call_graphs = dict(call_graphs or {})
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def modules(self) -> tuple[str, ...]:
|
|
82
|
+
return tuple(self._managers)
|
|
83
|
+
|
|
84
|
+
def imports(self, module: str) -> ImportTable:
|
|
85
|
+
return self._managers[module].get(ImportAnalysis)
|
|
86
|
+
|
|
87
|
+
def call_graph(self, module: str) -> CallGraph:
|
|
88
|
+
cached = self._call_graphs.get(module)
|
|
89
|
+
return cached if cached is not None else self._managers[module].get(CallGraphAnalysis)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ProjectPlugin(Plugin):
|
|
93
|
+
"""A plugin that runs once per project rather than once per module (§26)."""
|
|
94
|
+
|
|
95
|
+
def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
|
|
96
|
+
return ()
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def analyze_project(self, ctx: ProjectContext) -> Sequence[Finding]:
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class PluginContext:
|
|
104
|
+
"""What one plugin may see while analysing one module."""
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
manager: AnalysisManager,
|
|
109
|
+
plugin: Plugin,
|
|
110
|
+
functions: tuple[nodes.Function, ...] | None = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
self._manager = manager
|
|
113
|
+
self._plugin = plugin
|
|
114
|
+
self._functions = analyzable_functions(manager.module) if functions is None else functions
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def module(self) -> nodes.Module:
|
|
118
|
+
return self._manager.module
|
|
119
|
+
|
|
120
|
+
def functions(self) -> tuple[nodes.Function, ...]:
|
|
121
|
+
"""Top-level functions and methods the engine can analyse."""
|
|
122
|
+
|
|
123
|
+
return self._functions
|
|
124
|
+
|
|
125
|
+
@overload
|
|
126
|
+
def get(self, analysis: type[Analysis[R]], function: None = None) -> R: ...
|
|
127
|
+
|
|
128
|
+
@overload
|
|
129
|
+
def get(self, analysis: type[FunctionAnalysis[R]], function: nodes.Function) -> R: ...
|
|
130
|
+
|
|
131
|
+
def get(self, analysis: AnyAnalysis, function: nodes.Function | None = None) -> Any:
|
|
132
|
+
if analysis not in self._plugin.requires:
|
|
133
|
+
raise UndeclaredDependencyError(
|
|
134
|
+
f"{self._plugin.name} requests {analysis.name} without declaring it in requires"
|
|
135
|
+
)
|
|
136
|
+
if function is None:
|
|
137
|
+
assert issubclass(analysis, Analysis)
|
|
138
|
+
return self._manager.get(analysis)
|
|
139
|
+
assert issubclass(analysis, FunctionAnalysis)
|
|
140
|
+
return self._manager.get(analysis, function)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def run_plugins(
|
|
144
|
+
manager: AnalysisManager,
|
|
145
|
+
plugins: Iterable[Plugin],
|
|
146
|
+
functions: tuple[nodes.Function, ...] | None = None,
|
|
147
|
+
) -> tuple[Finding, ...]:
|
|
148
|
+
"""Run every plugin against one shared manager and concatenate their findings."""
|
|
149
|
+
|
|
150
|
+
findings: list[Finding] = []
|
|
151
|
+
for plugin in plugins:
|
|
152
|
+
findings.extend(plugin.analyze(PluginContext(manager, plugin, functions)))
|
|
153
|
+
return tuple(findings)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Reusable detector bases (architecture §15).
|
|
2
|
+
|
|
3
|
+
``TaintDetector`` turns the flows of one taint kind into findings; every security
|
|
4
|
+
detector is a few class attributes on top of it. ``SymbolCallDetector`` reports calls
|
|
5
|
+
whose callee resolves to a canonical symbol, through imports and local aliases alike,
|
|
6
|
+
by reading the SSA form.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from typing import ClassVar
|
|
13
|
+
|
|
14
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
15
|
+
from coretrace_python.findings import Confidence, Finding, Severity
|
|
16
|
+
from coretrace_python.findings.refutation import RefutationAnalysis, Status
|
|
17
|
+
from coretrace_python.ir.model import Call, Symbol
|
|
18
|
+
from coretrace_python.ir.ssa import SSAAnalysis
|
|
19
|
+
from coretrace_python.plugins.api import Plugin, PluginContext
|
|
20
|
+
from coretrace_python.semantic.symbols import SymbolId
|
|
21
|
+
from coretrace_python.taint import TaintAnalysis, TaintKind
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TaintDetector(Plugin):
|
|
25
|
+
"""Report every taint flow carrying ``kind`` into a sink."""
|
|
26
|
+
|
|
27
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({TaintAnalysis, RefutationAnalysis})
|
|
28
|
+
rule_id: ClassVar[str]
|
|
29
|
+
kind: ClassVar[TaintKind]
|
|
30
|
+
severity: ClassVar[Severity]
|
|
31
|
+
title: ClassVar[str]
|
|
32
|
+
confidence: ClassVar[Confidence] = Confidence.HIGH
|
|
33
|
+
|
|
34
|
+
def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
|
|
35
|
+
findings: list[Finding] = []
|
|
36
|
+
for function in ctx.functions():
|
|
37
|
+
verdicts = ctx.get(RefutationAnalysis, function)
|
|
38
|
+
for flow in ctx.get(TaintAnalysis, function).flows:
|
|
39
|
+
if not flow.kinds & self.kind:
|
|
40
|
+
continue
|
|
41
|
+
verdict = verdicts.verdict(flow)
|
|
42
|
+
if verdict.status is Status.REFUTED:
|
|
43
|
+
continue
|
|
44
|
+
message = f"{self.title}: {flow.source.label} input reaches {flow.sink.symbol}"
|
|
45
|
+
metadata = {
|
|
46
|
+
"source": str(flow.source.symbol),
|
|
47
|
+
"source_label": flow.source.label,
|
|
48
|
+
"sink": str(flow.sink.symbol),
|
|
49
|
+
"verdict": verdict.status.value,
|
|
50
|
+
"evidence": verdict.evidence,
|
|
51
|
+
}
|
|
52
|
+
if flow.through is not None and flow.sink_location is not None:
|
|
53
|
+
message += f" through {flow.through}"
|
|
54
|
+
metadata["through"] = flow.through
|
|
55
|
+
metadata["sink_line"] = str(flow.sink_location.start_line)
|
|
56
|
+
confidence = (
|
|
57
|
+
self.confidence if verdict.status is Status.VULNERABILITY else _lower(self.confidence)
|
|
58
|
+
)
|
|
59
|
+
findings.append(
|
|
60
|
+
Finding(
|
|
61
|
+
rule_id=self.rule_id,
|
|
62
|
+
message=message,
|
|
63
|
+
severity=self.severity,
|
|
64
|
+
confidence=confidence,
|
|
65
|
+
span=flow.location,
|
|
66
|
+
function=function.name,
|
|
67
|
+
metadata=metadata,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
return findings
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _lower(confidence: Confidence) -> Confidence:
|
|
74
|
+
return {Confidence.HIGH: Confidence.MEDIUM}.get(confidence, Confidence.LOW)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SymbolCallDetector(Plugin):
|
|
78
|
+
"""Report calls whose callee is one of ``symbols``, whatever name the file uses."""
|
|
79
|
+
|
|
80
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis})
|
|
81
|
+
rule_id: ClassVar[str]
|
|
82
|
+
symbols: ClassVar[frozenset[SymbolId]]
|
|
83
|
+
severity: ClassVar[Severity]
|
|
84
|
+
message_template: ClassVar[str]
|
|
85
|
+
|
|
86
|
+
def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
|
|
87
|
+
findings: list[Finding] = []
|
|
88
|
+
for function in ctx.functions():
|
|
89
|
+
ssa = ctx.get(SSAAnalysis, function)
|
|
90
|
+
defined = {
|
|
91
|
+
i.result: i.symbol_id
|
|
92
|
+
for block in ssa.blocks
|
|
93
|
+
for i in block.instructions
|
|
94
|
+
if isinstance(i, Symbol)
|
|
95
|
+
}
|
|
96
|
+
for block in ssa.blocks:
|
|
97
|
+
for instruction in block.instructions:
|
|
98
|
+
if not isinstance(instruction, Call):
|
|
99
|
+
continue
|
|
100
|
+
symbol = defined.get(instruction.callee)
|
|
101
|
+
if symbol is None or symbol not in self.symbols:
|
|
102
|
+
continue
|
|
103
|
+
findings.append(
|
|
104
|
+
Finding(
|
|
105
|
+
rule_id=self.rule_id,
|
|
106
|
+
message=self.message_template.format(symbol=symbol),
|
|
107
|
+
severity=self.severity,
|
|
108
|
+
confidence=Confidence.HIGH,
|
|
109
|
+
span=instruction.location,
|
|
110
|
+
function=function.name,
|
|
111
|
+
metadata={"symbol": str(symbol)},
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
return findings
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""In-process plugin loading (architecture §14, §34)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from coretrace_python.analysis import AnalysisManager
|
|
11
|
+
from coretrace_python.plugins.api import PLUGIN_API_VERSION, Plugin
|
|
12
|
+
from coretrace_python.plugins.manifest import ManifestError, PluginManifest, load_manifest
|
|
13
|
+
|
|
14
|
+
MANIFEST_FILENAME = "plugin.toml"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IncompatiblePluginError(Exception):
|
|
18
|
+
"""The plugin targets a plugin API version this engine does not provide."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class LoadedPlugin:
|
|
23
|
+
manifest: PluginManifest
|
|
24
|
+
plugin: Plugin
|
|
25
|
+
directory: Path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_plugin(directory: Path, manager: AnalysisManager) -> LoadedPlugin:
|
|
29
|
+
"""Load the plugin in ``directory`` and check it against its manifest."""
|
|
30
|
+
|
|
31
|
+
manifest_path = directory / MANIFEST_FILENAME
|
|
32
|
+
manifest = load_manifest(manifest_path)
|
|
33
|
+
if not manifest.plugin_api.contains(PLUGIN_API_VERSION):
|
|
34
|
+
raise IncompatiblePluginError(
|
|
35
|
+
f"plugin {manifest.name!r} requires plugin_api {manifest.plugin_api}"
|
|
36
|
+
f" but this engine provides {PLUGIN_API_VERSION}"
|
|
37
|
+
)
|
|
38
|
+
for analysis_name in manifest.requires:
|
|
39
|
+
try:
|
|
40
|
+
manager.analysis(analysis_name)
|
|
41
|
+
except KeyError:
|
|
42
|
+
raise ManifestError(
|
|
43
|
+
f"{manifest_path}: unknown analysis {analysis_name!r} in requires"
|
|
44
|
+
) from None
|
|
45
|
+
|
|
46
|
+
plugin_class = _import_entrypoint(directory, manifest)
|
|
47
|
+
declared = {analysis.name for analysis in plugin_class.requires}
|
|
48
|
+
if declared != set(manifest.requires):
|
|
49
|
+
raise ManifestError(
|
|
50
|
+
f"{manifest_path}: manifest requires {sorted(manifest.requires)}"
|
|
51
|
+
f" but {manifest.entrypoint.class_name} declares {sorted(declared)}"
|
|
52
|
+
)
|
|
53
|
+
return LoadedPlugin(manifest, plugin_class(), directory)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def discover_plugins(root: Path, manager: AnalysisManager) -> tuple[LoadedPlugin, ...]:
|
|
57
|
+
"""Load every plugin below ``root``, in path order."""
|
|
58
|
+
|
|
59
|
+
return tuple(
|
|
60
|
+
load_plugin(manifest_path.parent, manager)
|
|
61
|
+
for manifest_path in sorted(root.rglob(MANIFEST_FILENAME))
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _import_entrypoint(directory: Path, manifest: PluginManifest) -> type[Plugin]:
|
|
66
|
+
module_path = directory / f"{manifest.entrypoint.module}.py"
|
|
67
|
+
if not module_path.is_file():
|
|
68
|
+
raise ManifestError(
|
|
69
|
+
f"{directory / MANIFEST_FILENAME}: entrypoint module"
|
|
70
|
+
f" {manifest.entrypoint.module!r} not found"
|
|
71
|
+
)
|
|
72
|
+
unique = hashlib.sha1(str(module_path.resolve()).encode()).hexdigest()[:12]
|
|
73
|
+
spec = importlib.util.spec_from_file_location(f"coretrace_plugin_{unique}", module_path)
|
|
74
|
+
assert spec is not None and spec.loader is not None
|
|
75
|
+
module = importlib.util.module_from_spec(spec)
|
|
76
|
+
spec.loader.exec_module(module)
|
|
77
|
+
|
|
78
|
+
candidate = getattr(module, manifest.entrypoint.class_name, None)
|
|
79
|
+
if not (isinstance(candidate, type) and issubclass(candidate, Plugin)):
|
|
80
|
+
raise ManifestError(
|
|
81
|
+
f"{directory / MANIFEST_FILENAME}: entrypoint class"
|
|
82
|
+
f" {manifest.entrypoint.class_name!r} is not a Plugin"
|
|
83
|
+
)
|
|
84
|
+
return candidate
|