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.
Files changed (126) hide show
  1. coretrace_python/__init__.py +4 -0
  2. coretrace_python/__main__.py +4 -0
  3. coretrace_python/abstract/__init__.py +45 -0
  4. coretrace_python/abstract/constants.py +226 -0
  5. coretrace_python/abstract/heap.py +252 -0
  6. coretrace_python/abstract/ranges.py +285 -0
  7. coretrace_python/abstract/values.py +56 -0
  8. coretrace_python/analysis/__init__.py +31 -0
  9. coretrace_python/analysis/manager.py +165 -0
  10. coretrace_python/analysis/provider.py +73 -0
  11. coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
  12. coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
  13. coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
  14. coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
  15. coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
  16. coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
  17. coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
  18. coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
  19. coretrace_python/bundled/models/cli/cli_models.py +35 -0
  20. coretrace_python/bundled/models/cli/plugin.toml +9 -0
  21. coretrace_python/bundled/models/credentials/credential_models.py +43 -0
  22. coretrace_python/bundled/models/credentials/plugin.toml +9 -0
  23. coretrace_python/bundled/models/django/django_models.py +123 -0
  24. coretrace_python/bundled/models/django/plugin.toml +9 -0
  25. coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
  26. coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
  27. coretrace_python/bundled/models/flask/flask_models.py +53 -0
  28. coretrace_python/bundled/models/flask/plugin.toml +9 -0
  29. coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
  30. coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
  31. coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
  32. coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
  33. coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
  34. coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
  35. coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
  36. coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
  37. coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
  38. coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
  39. coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
  40. coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
  41. coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
  42. coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
  43. coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
  44. coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
  45. coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
  46. coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
  47. coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
  48. coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
  49. coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
  50. coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
  51. coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
  52. coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
  53. coretrace_python/bundled/security/xss/plugin.toml +9 -0
  54. coretrace_python/bundled/security/xss/xss.py +17 -0
  55. coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
  56. coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
  57. coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
  58. coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
  59. coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
  60. coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
  61. coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
  62. coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
  63. coretrace_python/cache.py +310 -0
  64. coretrace_python/cfg/__init__.py +46 -0
  65. coretrace_python/cfg/builder.py +589 -0
  66. coretrace_python/cfg/dominance.py +183 -0
  67. coretrace_python/cfg/model.py +166 -0
  68. coretrace_python/cli.py +216 -0
  69. coretrace_python/dataflow/__init__.py +29 -0
  70. coretrace_python/dataflow/lattice.py +78 -0
  71. coretrace_python/dataflow/solver.py +96 -0
  72. coretrace_python/dependency/__init__.py +44 -0
  73. coretrace_python/dependency/advisories.py +168 -0
  74. coretrace_python/dependency/correlation.py +87 -0
  75. coretrace_python/dependency/graph.py +274 -0
  76. coretrace_python/dependency/policy.py +65 -0
  77. coretrace_python/dependency/sbom.py +65 -0
  78. coretrace_python/engine.py +688 -0
  79. coretrace_python/findings/__init__.py +13 -0
  80. coretrace_python/findings/coverage.py +45 -0
  81. coretrace_python/findings/model.py +49 -0
  82. coretrace_python/findings/refutation.py +427 -0
  83. coretrace_python/frontend/__init__.py +18 -0
  84. coretrace_python/frontend/ast_adapter.py +447 -0
  85. coretrace_python/frontend/parser.py +23 -0
  86. coretrace_python/hir/__init__.py +5 -0
  87. coretrace_python/hir/nodes.py +584 -0
  88. coretrace_python/hir/visitors.py +36 -0
  89. coretrace_python/interprocedural/__init__.py +49 -0
  90. coretrace_python/interprocedural/callgraph.py +291 -0
  91. coretrace_python/interprocedural/modulegraph.py +218 -0
  92. coretrace_python/interprocedural/summaries.py +463 -0
  93. coretrace_python/ir/__init__.py +5 -0
  94. coretrace_python/ir/defuse.py +74 -0
  95. coretrace_python/ir/lowering.py +575 -0
  96. coretrace_python/ir/model.py +481 -0
  97. coretrace_python/ir/printer.py +201 -0
  98. coretrace_python/ir/ssa.py +277 -0
  99. coretrace_python/plugins/__init__.py +58 -0
  100. coretrace_python/plugins/api.py +153 -0
  101. coretrace_python/plugins/detectors.py +114 -0
  102. coretrace_python/plugins/loader.py +84 -0
  103. coretrace_python/plugins/manifest.py +112 -0
  104. coretrace_python/plugins/registry.py +34 -0
  105. coretrace_python/plugins/secrets.py +330 -0
  106. coretrace_python/reporters/__init__.py +22 -0
  107. coretrace_python/reporters/json_format.py +48 -0
  108. coretrace_python/reporters/report.py +23 -0
  109. coretrace_python/reporters/sarif.py +70 -0
  110. coretrace_python/reporters/text.py +24 -0
  111. coretrace_python/semantic/__init__.py +9 -0
  112. coretrace_python/semantic/identity.py +39 -0
  113. coretrace_python/semantic/imports.py +131 -0
  114. coretrace_python/semantic/scopes.py +473 -0
  115. coretrace_python/semantic/symbols.py +87 -0
  116. coretrace_python/source/__init__.py +13 -0
  117. coretrace_python/source/manager.py +81 -0
  118. coretrace_python/source/model.py +57 -0
  119. coretrace_python/taint/__init__.py +55 -0
  120. coretrace_python/taint/engine.py +800 -0
  121. coretrace_python/taint/models.py +317 -0
  122. coretrace_python/taint/routes.py +99 -0
  123. coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
  124. coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
  125. coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
  126. 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