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,463 @@
1
+ """Function summaries (architecture §19).
2
+
3
+ A summary says which parameters a function's return value depends on and which external
4
+ symbols its parameters reach, directly or through calls to other known functions. It is
5
+ computed as a dependence data-flow problem over the SSA form and iterated to a fixpoint
6
+ over the call graph, so recursion converges to the least solution. Summaries carry no
7
+ security knowledge: the taint engine decides which external symbols matter.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Mapping
13
+ from dataclasses import dataclass
14
+ from types import MappingProxyType
15
+ from typing import ClassVar
16
+
17
+ from coretrace_python.abstract import (
18
+ ATTRIBUTES,
19
+ ELEMENTS,
20
+ HeapAnalysis,
21
+ HeapFacts,
22
+ HeapLocation,
23
+ mutated_by,
24
+ )
25
+ from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
26
+ from coretrace_python.cfg import CFG, BlockId, CFGAnalysis
27
+ from coretrace_python.dataflow import DataflowProblem, Direction, solve
28
+ from coretrace_python.interprocedural.callgraph import (
29
+ CallGraph,
30
+ CallGraphAnalysis,
31
+ ExternalSymbol,
32
+ KnownFunction,
33
+ UnknownTarget,
34
+ )
35
+ from coretrace_python.ir.model import (
36
+ BasicBlock,
37
+ Branch,
38
+ BuildDict,
39
+ BuildList,
40
+ BuildSet,
41
+ BuildTuple,
42
+ Call,
43
+ Constant,
44
+ ForNext,
45
+ FunctionIR,
46
+ GetAttr,
47
+ GetItem,
48
+ GetIter,
49
+ Global,
50
+ Instruction,
51
+ Jump,
52
+ MakeFunction,
53
+ Phi,
54
+ Return,
55
+ SetAttr,
56
+ SetItem,
57
+ Symbol,
58
+ Undefined,
59
+ Value,
60
+ )
61
+ from coretrace_python.ir.ssa import SSAAnalysis
62
+ from coretrace_python.semantic.symbols import SymbolId
63
+ from coretrace_python.source import SourceSpan
64
+
65
+ Dependencies = frozenset[int]
66
+ NONE: Dependencies = frozenset()
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class Dep:
71
+ """What a value depends on: parameter indices and results of external symbols."""
72
+
73
+ parameters: Dependencies = NONE
74
+ externals: frozenset[SymbolId] = frozenset()
75
+
76
+ def __or__(self, other: Dep) -> Dep:
77
+ return Dep(self.parameters | other.parameters, self.externals | other.externals)
78
+
79
+
80
+ EMPTY = Dep()
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class ExternalCall:
85
+ """An external symbol reached from this function; dependencies are parameter indices."""
86
+
87
+ symbol: SymbolId
88
+ argument_dependencies: tuple[Dependencies, ...]
89
+ keyword_dependencies: Dependencies
90
+ location: SourceSpan
91
+ call_site: SourceSpan | None
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class Mutation:
96
+ """What the function stores into ``field`` of its ``parameter``: data of other
97
+ parameters, and results of external symbols (§22)."""
98
+
99
+ parameter: int
100
+ field: str
101
+ dependencies: Dependencies
102
+ externals: frozenset[SymbolId]
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class FunctionSummary:
107
+ name: str
108
+ parameters: int
109
+ return_dependencies: Dependencies
110
+ external_calls: tuple[ExternalCall, ...]
111
+ unsupported: bool = False
112
+ return_externals: frozenset[SymbolId] = frozenset()
113
+ mutations: tuple[Mutation, ...] = ()
114
+ side_effects: frozenset[str] = frozenset()
115
+
116
+
117
+ class SummaryIndex:
118
+ """Summaries of every project function, keyed by project symbol (§21)."""
119
+
120
+ def __init__(self, summaries: Mapping[SymbolId, FunctionSummary] | None = None) -> None:
121
+ self.summaries: Mapping[SymbolId, FunctionSummary] = MappingProxyType(
122
+ dict(summaries or {})
123
+ )
124
+ self.symbols = tuple(sorted(self.summaries, key=str))
125
+
126
+ def summary(self, symbol: SymbolId) -> FunctionSummary | None:
127
+ return self.summaries.get(symbol)
128
+
129
+ def __eq__(self, other: object) -> bool:
130
+ return isinstance(other, SummaryIndex) and dict(self.summaries) == dict(other.summaries)
131
+
132
+ def __hash__(self) -> int:
133
+ return hash(self.symbols)
134
+
135
+
136
+ class ProjectSummaries(Analysis[SummaryIndex]):
137
+ """The project-wide summary index. The engine provides it for multi-file analysis;
138
+ on its own a module sees an empty index, which is the single-file behaviour."""
139
+
140
+ name: ClassVar[str] = "interprocedural.project"
141
+
142
+ @classmethod
143
+ def compute(cls, ctx: AnalysisContext) -> SummaryIndex:
144
+ return SummaryIndex()
145
+
146
+
147
+ class SummaryTable:
148
+ def __init__(self, summaries: Mapping[str, FunctionSummary]) -> None:
149
+ self._summaries = MappingProxyType(dict(summaries))
150
+ self.names = tuple(summaries)
151
+
152
+ def summary(self, name: str) -> FunctionSummary:
153
+ return self._summaries[name]
154
+
155
+
156
+ Key = Value | HeapLocation
157
+ State = Mapping[Key, Dep]
158
+ _CallKey = tuple[SymbolId, SourceSpan, SourceSpan | None]
159
+
160
+
161
+ class _DependenceProblem(DataflowProblem[State]):
162
+ direction: ClassVar[Direction] = Direction.FORWARD
163
+
164
+ def __init__(
165
+ self,
166
+ name: str,
167
+ function: FunctionIR,
168
+ graph: CallGraph,
169
+ table: Mapping[str, FunctionSummary],
170
+ project: Mapping[SymbolId, FunctionSummary] | None = None,
171
+ heap: HeapFacts | None = None,
172
+ ) -> None:
173
+ self.name = name
174
+ self.function = function
175
+ self.graph = graph
176
+ self.table = table
177
+ self.project = project or {}
178
+ self.heap = heap or HeapFacts({})
179
+ self.blocks = {block.id: block for block in function.blocks}
180
+ self.defs: dict[Value, Instruction] = {
181
+ i.result: i for block in function.blocks for i in block.instructions if i.result
182
+ }
183
+ self.external: dict[_CallKey, ExternalCall] = {}
184
+ self.returns: Dep = EMPTY
185
+ self.stores: dict[HeapLocation, Dep] = {}
186
+ self.mutated_globals: set[str] = set()
187
+
188
+ # ------------------------------------------------------------------ heap
189
+
190
+ def deep(self, value: Value, state: Mapping[Key, Dep]) -> Dep:
191
+ """What a value depends on, contents of the objects it points to included."""
192
+
193
+ deps = state.get(value, EMPTY)
194
+ for field in (ELEMENTS, ATTRIBUTES):
195
+ for location in self.heap.locations(value, field):
196
+ deps |= state.get(location, EMPTY)
197
+ return deps
198
+
199
+ def store(self, state: dict[Key, Dep], receiver: Value, field: str, deps: Dep) -> None:
200
+ for location in self.heap.locations(receiver, field):
201
+ state[location] = state.get(location, EMPTY) | deps
202
+ self.stores[location] = self.stores.get(location, EMPTY) | deps
203
+ if location.object.site.kind == "global":
204
+ self.mutated_globals.add(location.object.site.name)
205
+
206
+ def initial(self) -> State:
207
+ return MappingProxyType(
208
+ {p: Dep(frozenset({i})) for i, p in enumerate(self.function.parameters)}
209
+ )
210
+
211
+ def join(self, a: State, b: State) -> State:
212
+ merged = dict(a)
213
+ for value, deps in b.items():
214
+ merged[value] = merged[value] | deps if value in merged else deps
215
+ return MappingProxyType(merged)
216
+
217
+ def evaluate(self, block: BasicBlock, incoming: Mapping[BlockId, State]) -> State:
218
+ states = list(incoming.values())
219
+ state: dict[Key, Dep] = dict(states[0]) if states else {}
220
+ for other in states[1:]:
221
+ state = dict(self.join(state, other))
222
+ for instruction in block.instructions:
223
+ if isinstance(instruction, SetAttr):
224
+ self.store(state, instruction.object, ATTRIBUTES, state.get(instruction.value, EMPTY))
225
+ elif isinstance(instruction, SetItem):
226
+ self.store(state, instruction.object, ELEMENTS, state.get(instruction.value, EMPTY))
227
+ if instruction.result is None:
228
+ continue
229
+ if isinstance(instruction, Phi):
230
+ deps = EMPTY
231
+ for value, predecessor in instruction.incoming:
232
+ if predecessor in incoming:
233
+ deps |= incoming[predecessor].get(value, EMPTY)
234
+ elif isinstance(instruction, Call):
235
+ deps = self.call(instruction, state)
236
+ else:
237
+ deps = self.instruction(instruction, state)
238
+ if isinstance(instruction, GetAttr | GetItem | GetIter):
239
+ deps |= self.loaded(instruction, state)
240
+ state[instruction.result] = deps
241
+ terminator = block.terminator
242
+ if isinstance(terminator, ForNext) and terminator.result is not None:
243
+ state[terminator.result] = self.deep(terminator.iterator, state)
244
+ if isinstance(terminator, Return) and terminator.value is not None:
245
+ self.returns |= state.get(terminator.value, EMPTY)
246
+ return MappingProxyType(state)
247
+
248
+ def flow(self, cfg: CFG, block_id: BlockId, incoming: Mapping[BlockId, State]) -> Mapping[BlockId, State]:
249
+ block = self.blocks[block_id]
250
+ state = self.evaluate(block, incoming)
251
+ exits = {target: state for target in block.exception_targets}
252
+ terminator = block.terminator
253
+ if isinstance(terminator, Branch):
254
+ return {**exits, terminator.then_block: state, terminator.else_block: state}
255
+ if isinstance(terminator, Jump):
256
+ return {**exits, terminator.target: state}
257
+ if isinstance(terminator, ForNext):
258
+ return {**exits, terminator.body: state, terminator.exit: state}
259
+ return exits
260
+
261
+ # ------------------------------------------------------------------ transfer
262
+
263
+ def instruction(self, instruction: Instruction, state: Mapping[Key, Dep]) -> Dep:
264
+ if isinstance(instruction, Constant | Global | Symbol | Undefined):
265
+ return EMPTY
266
+ deps = EMPTY
267
+ for operand in instruction.operands():
268
+ deps |= state.get(operand, EMPTY)
269
+ if isinstance(instruction, BuildList | BuildTuple | BuildDict | BuildSet):
270
+ for unpacked in instruction.unpacked:
271
+ deps |= self.deep(unpacked, state)
272
+ return deps
273
+
274
+ def loaded(self, instruction: GetAttr | GetItem | GetIter, state: Mapping[Key, Dep]) -> Dep:
275
+ """The contents a load reads from the objects it reads from."""
276
+
277
+ if isinstance(instruction, GetAttr):
278
+ receiver, field = instruction.object, ATTRIBUTES
279
+ elif isinstance(instruction, GetItem):
280
+ receiver, field = instruction.object, ELEMENTS
281
+ else:
282
+ receiver, field = instruction.iterable, ELEMENTS
283
+ deps = EMPTY
284
+ for location in self.heap.locations(receiver, field):
285
+ deps |= state.get(location, EMPTY)
286
+ return deps
287
+
288
+ def call(self, call: Call, state: dict[Key, Dep]) -> Dep:
289
+ arguments = tuple(self.deep(a, state) for a in call.arguments)
290
+ keywords = EMPTY
291
+ for value in (*call.starred, *(v for _, v in call.keywords)):
292
+ keywords |= self.deep(value, state)
293
+ everything = keywords
294
+ for deps in arguments:
295
+ everything |= deps
296
+ receiver = mutated_by(call, self.defs)
297
+ if receiver is not None:
298
+ self.store(state, receiver, ELEMENTS, everything)
299
+ target = self.graph.target_at(self.name, call.location)
300
+
301
+ if isinstance(target, ExternalSymbol):
302
+ project = self.project.get(target.symbol)
303
+ if project is None:
304
+ project = self.project.get(target.symbol.attribute("__init__"))
305
+ if project is not None:
306
+ bound = self.receiver(call, project.name)
307
+ arguments = (*(self.deep(r, state) for r in bound), *arguments)
308
+ return self.known(project, arguments, keywords, everything, call, state, bound)
309
+ self.record(
310
+ target.symbol,
311
+ tuple(a.parameters for a in arguments),
312
+ keywords.parameters,
313
+ call.location,
314
+ None,
315
+ )
316
+ return everything | Dep(externals=frozenset({target.symbol}))
317
+ if isinstance(target, KnownFunction):
318
+ made = self.defs.get(call.callee)
319
+ if isinstance(made, MakeFunction):
320
+ arguments = (*arguments, *(self.deep(v, state) for v in made.captured))
321
+ bound = self.receiver(call, target.name)
322
+ arguments = (*(self.deep(r, state) for r in bound), *arguments)
323
+ return self.known(self.table[target.name], arguments, keywords, everything, call, state, bound)
324
+ assert isinstance(target, UnknownTarget)
325
+ return everything | state.get(call.callee, EMPTY)
326
+
327
+ def receiver(self, call: Call, name: str) -> tuple[Value, ...]:
328
+ """The object a method call runs on: the receiver of ``obj.method(...)`` or the
329
+ new object of ``Class(...)``, the method's implicit first parameter."""
330
+
331
+ if "." not in name:
332
+ return ()
333
+ callee = self.defs.get(call.callee)
334
+ if isinstance(callee, GetAttr):
335
+ return (callee.object,)
336
+ if name.endswith(".__init__") and isinstance(callee, Global | Symbol):
337
+ return (call.result,)
338
+ return ()
339
+
340
+ def known(
341
+ self,
342
+ callee: FunctionSummary,
343
+ arguments: tuple[Dep, ...],
344
+ keywords: Dep,
345
+ everything: Dep,
346
+ call: Call,
347
+ state: dict[Key, Dep],
348
+ receiver: tuple[Value, ...] = (),
349
+ ) -> Dep:
350
+ """Dependencies of a call to a function whose summary is known."""
351
+
352
+ if callee.unsupported:
353
+ return everything
354
+
355
+ def mapped(indices: Dependencies) -> Dep:
356
+ result = EMPTY
357
+ for index in indices:
358
+ result |= arguments[index] if index < len(arguments) else keywords
359
+ return result
360
+
361
+ for reached in callee.external_calls:
362
+ self.record(
363
+ reached.symbol,
364
+ tuple(mapped(d).parameters for d in reached.argument_dependencies),
365
+ mapped(reached.keyword_dependencies).parameters,
366
+ reached.location,
367
+ call.location,
368
+ )
369
+ made = self.defs.get(call.callee)
370
+ values = (*receiver, *call.arguments, *(made.captured if isinstance(made, MakeFunction) else ()))
371
+ for mutation in callee.mutations:
372
+ if mutation.parameter < len(values):
373
+ deps = mapped(mutation.dependencies) | Dep(externals=mutation.externals)
374
+ self.store(state, values[mutation.parameter], mutation.field, deps)
375
+ return mapped(callee.return_dependencies) | Dep(externals=callee.return_externals)
376
+
377
+ def record(
378
+ self,
379
+ symbol: SymbolId,
380
+ arguments: tuple[Dependencies, ...],
381
+ keywords: Dependencies,
382
+ location: SourceSpan,
383
+ call_site: SourceSpan | None,
384
+ ) -> None:
385
+ key = (symbol, location, call_site)
386
+ previous = self.external.get(key)
387
+ if previous is not None:
388
+ arguments = tuple(
389
+ a | b for a, b in zip(previous.argument_dependencies, arguments, strict=False)
390
+ )
391
+ keywords |= previous.keyword_dependencies
392
+ self.external[key] = ExternalCall(symbol, arguments, keywords, location, call_site)
393
+
394
+
395
+ def summarize(
396
+ name: str,
397
+ function: FunctionIR,
398
+ cfg: CFG,
399
+ graph: CallGraph,
400
+ table: Mapping[str, FunctionSummary],
401
+ project: Mapping[SymbolId, FunctionSummary] | None = None,
402
+ heap: HeapFacts | None = None,
403
+ ) -> FunctionSummary:
404
+ problem = _DependenceProblem(name, function, graph, table, project, heap)
405
+ solution = solve(problem, cfg)
406
+ problem.external = {}
407
+ problem.returns = EMPTY
408
+ problem.stores = {}
409
+ problem.mutated_globals = set()
410
+ for block in function.blocks:
411
+ if solution.reached(block.id):
412
+ problem.evaluate(block, solution.incoming(block.id))
413
+ mutations: list[Mutation] = []
414
+ for index, parameter in enumerate(function.parameters):
415
+ for field in (ELEMENTS, ATTRIBUTES):
416
+ deps = EMPTY
417
+ for location in problem.heap.locations(parameter, field):
418
+ deps |= problem.stores.get(location, EMPTY)
419
+ if deps.parameters or deps.externals:
420
+ mutations.append(Mutation(index, field, deps.parameters, deps.externals))
421
+ return FunctionSummary(
422
+ name,
423
+ len(function.parameters),
424
+ problem.returns.parameters,
425
+ tuple(problem.external.values()),
426
+ return_externals=problem.returns.externals,
427
+ mutations=tuple(mutations),
428
+ side_effects=frozenset(problem.mutated_globals),
429
+ )
430
+
431
+
432
+ class SummaryAnalysis(Analysis[SummaryTable]):
433
+ name: ClassVar[str] = "interprocedural.summaries"
434
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset(
435
+ {CallGraphAnalysis, SSAAnalysis, CFGAnalysis, ProjectSummaries, HeapAnalysis}
436
+ )
437
+
438
+ @classmethod
439
+ def compute(cls, ctx: AnalysisContext) -> SummaryTable:
440
+ graph = ctx.get(CallGraphAnalysis)
441
+ project = ctx.get(ProjectSummaries).summaries
442
+ table: dict[str, FunctionSummary] = {}
443
+ supported: dict[str, tuple[FunctionIR, CFG, HeapFacts]] = {}
444
+ for name, function in graph.definitions.items():
445
+ if name in graph.unsupported:
446
+ count = len(function.parameters)
447
+ table[name] = FunctionSummary(name, count, frozenset(range(count)), (), True)
448
+ else:
449
+ supported[name] = (
450
+ ctx.get(SSAAnalysis, function),
451
+ ctx.get(CFGAnalysis, function),
452
+ ctx.get(HeapAnalysis, function),
453
+ )
454
+ table[name] = FunctionSummary(name, len(function.parameters), NONE, ())
455
+ changed = True
456
+ while changed:
457
+ changed = False
458
+ for name, (ssa, cfg, heap) in supported.items():
459
+ updated = summarize(name, ssa, cfg, graph, table, project, heap)
460
+ if updated != table[name]:
461
+ table[name] = updated
462
+ changed = True
463
+ return SummaryTable(table)
@@ -0,0 +1,5 @@
1
+ """Python-aware intermediate representation."""
2
+
3
+ from coretrace_python.ir.model import PYIR_SCHEMA_VERSION, BasicBlock, FunctionIR, ModuleIR, Value
4
+
5
+ __all__ = ["PYIR_SCHEMA_VERSION", "BasicBlock", "FunctionIR", "ModuleIR", "Value"]
@@ -0,0 +1,74 @@
1
+ """Def-use and use-def chains over SSA values (architecture §7)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from types import MappingProxyType
8
+ from typing import ClassVar
9
+
10
+ from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
11
+ from coretrace_python.cfg import BlockId
12
+ from coretrace_python.hir import nodes
13
+ from coretrace_python.ir.model import ForNext, FunctionIR, Value
14
+ from coretrace_python.ir.ssa import SSAAnalysis
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Definition:
19
+ """Where a value is defined: a parameter (no block), an instruction, or a terminator."""
20
+
21
+ block: BlockId | None
22
+ index: int | None
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Use:
27
+ """Where a value is used: an instruction index, or ``None`` for the terminator."""
28
+
29
+ block: BlockId
30
+ index: int | None
31
+
32
+
33
+ class DefUse:
34
+ def __init__(
35
+ self,
36
+ definitions: Mapping[Value, Definition],
37
+ uses: Mapping[Value, tuple[Use, ...]],
38
+ ) -> None:
39
+ self._definitions = MappingProxyType(dict(definitions))
40
+ self._uses = MappingProxyType(dict(uses))
41
+
42
+ def definition(self, value: Value) -> Definition:
43
+ return self._definitions[value]
44
+
45
+ def uses(self, value: Value) -> tuple[Use, ...]:
46
+ return self._uses.get(value, ())
47
+
48
+
49
+ def def_use(function: FunctionIR) -> DefUse:
50
+ definitions: dict[Value, Definition] = {}
51
+ uses: dict[Value, list[Use]] = {}
52
+ for parameter in function.parameters:
53
+ definitions[parameter] = Definition(None, None)
54
+ for block in function.blocks:
55
+ for index, instruction in enumerate(block.instructions):
56
+ if instruction.result is not None:
57
+ definitions[instruction.result] = Definition(block.id, index)
58
+ for operand in instruction.operands():
59
+ uses.setdefault(operand, []).append(Use(block.id, index))
60
+ terminator = block.terminator
61
+ if isinstance(terminator, ForNext) and terminator.result is not None:
62
+ definitions[terminator.result] = Definition(block.id, None)
63
+ for operand in terminator.operands():
64
+ uses.setdefault(operand, []).append(Use(block.id, None))
65
+ return DefUse(definitions, {value: tuple(found) for value, found in uses.items()})
66
+
67
+
68
+ class DefUseAnalysis(FunctionAnalysis[DefUse]):
69
+ name: ClassVar[str] = "ir.defuse"
70
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis})
71
+
72
+ @classmethod
73
+ def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> DefUse:
74
+ return def_use(ctx.get(SSAAnalysis, function))