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,45 @@
1
+ """Analysis coverage: which files and functions a run actually looked at.
2
+
3
+ "No findings" only means something when the reader knows what was analysed. Each file
4
+ is ``analysed``, a ``syntax-error`` (the frontend rejected it) or ``unreadable`` (it could
5
+ not be decoded); analysed files count their functions and how many lowered.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class FileCoverage:
15
+ path: str
16
+ status: str
17
+ functions: int
18
+ analysed: int
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Coverage:
23
+ details: tuple[FileCoverage, ...] = ()
24
+
25
+ @property
26
+ def files(self) -> int:
27
+ return len(self.details)
28
+
29
+ @property
30
+ def files_analysed(self) -> int:
31
+ return sum(1 for d in self.details if d.status == "analysed")
32
+
33
+ @property
34
+ def functions(self) -> int:
35
+ return sum(d.functions for d in self.details)
36
+
37
+ @property
38
+ def functions_analysed(self) -> int:
39
+ return sum(d.analysed for d in self.details)
40
+
41
+ def summary(self) -> str:
42
+ return (
43
+ f"coverage: {self.files_analysed}/{self.files} files, "
44
+ f"{self.functions_analysed}/{self.functions} functions"
45
+ )
@@ -0,0 +1,49 @@
1
+ """Normalized findings (architecture §23).
2
+
3
+ A finding references lightweight data only: a rule, a message, a source span and the
4
+ name of the enclosing function. It never carries CFG or taint-path copies; those are
5
+ reconstructed at report time.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from types import MappingProxyType
14
+
15
+ from coretrace_python.source import SourceSpan
16
+
17
+ FINDING_SCHEMA_VERSION = 1
18
+
19
+
20
+ class Severity(Enum):
21
+ INFO = "info"
22
+ LOW = "low"
23
+ MEDIUM = "medium"
24
+ HIGH = "high"
25
+ CRITICAL = "critical"
26
+
27
+
28
+ class Confidence(Enum):
29
+ LOW = "low"
30
+ MEDIUM = "medium"
31
+ HIGH = "high"
32
+
33
+
34
+ def _no_metadata() -> Mapping[str, str]:
35
+ return MappingProxyType({})
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class Finding:
40
+ rule_id: str
41
+ message: str
42
+ severity: Severity
43
+ confidence: Confidence
44
+ span: SourceSpan
45
+ function: str | None = None
46
+ metadata: Mapping[str, str] = field(default_factory=_no_metadata)
47
+
48
+ def __post_init__(self) -> None:
49
+ object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
@@ -0,0 +1,427 @@
1
+ """Proof / refutation engine (architecture §24).
2
+
3
+ Every taint flow gets a verdict. Walking the dominators of the sink block, each branch
4
+ whose one side alone reaches the sink fixes the truth of its condition there; the
5
+ condition is then interpreted: string validators (``isdigit()`` and friends), membership
6
+ in a constant allowlist and equality with a constant prove a value safe, ``and`` / ``or``
7
+ and ``not`` combine as expected, a ``Validator`` model names a callable whose truth proves
8
+ one of its arguments, a numeric value (``abstract.ranges``) cannot inject, and anything
9
+ else that mentions the value is a guard that does not prove it. A proof counts for an
10
+ origin when every dependence path from that origin to the sink argument goes through a
11
+ proven value. A flow is refuted when every tainted origin is proven safe or the sink is
12
+ unreachable, a hotspot when it sits behind an ``AuthorizationGuard`` (a decorator or a
13
+ dominating condition) or when an unproven guard mentions it, and a vulnerability
14
+ otherwise.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Mapping
20
+ from dataclasses import dataclass
21
+ from enum import Enum
22
+ from types import MappingProxyType
23
+ from typing import ClassVar
24
+
25
+ from coretrace_python.abstract import ConstantPropagation, RangeAnalysis, RangeFacts
26
+ from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
27
+ from coretrace_python.cfg import CFG, BlockId, CFGAnalysis, DominanceAnalysis, DominatorTree
28
+ from coretrace_python.hir import nodes
29
+ from coretrace_python.interprocedural import CallGraphAnalysis
30
+ from coretrace_python.ir.model import (
31
+ BoolOp,
32
+ Branch,
33
+ BuildList,
34
+ BuildTuple,
35
+ Call,
36
+ Compare,
37
+ Constant,
38
+ FunctionIR,
39
+ GetAttr,
40
+ Instruction,
41
+ UnaryOp,
42
+ Value,
43
+ )
44
+ from coretrace_python.ir.ssa import SSAAnalysis
45
+ from coretrace_python.semantic.scopes import ScopeAnalysis, ScopeTable
46
+ from coretrace_python.semantic.symbols import SymbolAnalysis, SymbolId, SymbolTable
47
+ from coretrace_python.taint import (
48
+ AuthorizationGuard,
49
+ ModelTable,
50
+ SecurityModelAnalysis,
51
+ TaintAnalysis,
52
+ TaintFacts,
53
+ TaintFlow,
54
+ )
55
+
56
+ VALIDATORS = frozenset(
57
+ {"isdigit", "isnumeric", "isdecimal", "isalnum", "isalpha", "isidentifier", "isascii"}
58
+ )
59
+
60
+
61
+ class Status(Enum):
62
+ VULNERABILITY = "vulnerability"
63
+ HOTSPOT = "hotspot"
64
+ REFUTED = "refuted"
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class Verdict:
69
+ flow: TaintFlow
70
+ status: Status
71
+ evidence: str
72
+
73
+
74
+ class Verdicts:
75
+ def __init__(self, verdicts: tuple[Verdict, ...]) -> None:
76
+ self._all = verdicts
77
+ self._by_flow: Mapping[TaintFlow, Verdict] = MappingProxyType(
78
+ {verdict.flow: verdict for verdict in verdicts}
79
+ )
80
+
81
+ def all(self) -> tuple[Verdict, ...]:
82
+ return self._all
83
+
84
+ def verdict(self, flow: TaintFlow) -> Verdict:
85
+ return self._by_flow[flow]
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class _Guard:
90
+ """A dominating branch condition with its truth at the sink."""
91
+
92
+ condition: Value
93
+ truth: bool
94
+ line: int
95
+
96
+
97
+ class _Judge:
98
+ def __init__(
99
+ self,
100
+ function: FunctionIR,
101
+ cfg: CFG,
102
+ tree: DominatorTree,
103
+ taint: TaintFacts,
104
+ ranges: RangeFacts | None = None,
105
+ models: ModelTable | None = None,
106
+ symbols: Mapping[Value, SymbolId] | None = None,
107
+ authorization: AuthorizationGuard | None = None,
108
+ ) -> None:
109
+ self.function = function
110
+ self.cfg = cfg
111
+ self.tree = tree
112
+ self.taint = taint
113
+ self.ranges = ranges or RangeFacts({})
114
+ self.models = models or ModelTable((), (), ())
115
+ self.symbols = symbols or {}
116
+ self.authorization = authorization
117
+ self.blocks = {block.id: block for block in function.blocks}
118
+ self.defs: dict[Value, Instruction] = {
119
+ i.result: i for block in function.blocks for i in block.instructions if i.result
120
+ }
121
+ self._closures: dict[Value, frozenset[Value]] = {}
122
+
123
+ # ------------------------------------------------------------------ dependencies
124
+
125
+ def closure(self, value: Value) -> frozenset[Value]:
126
+ """Every value ``value`` transitively depends on, excluding itself."""
127
+
128
+ if value in self._closures:
129
+ return self._closures[value]
130
+ found: set[Value] = set()
131
+ pending = [value]
132
+ while pending:
133
+ definition = self.defs.get(pending.pop())
134
+ if definition is None:
135
+ continue
136
+ for operand in definition.operands():
137
+ if operand not in found:
138
+ found.add(operand)
139
+ pending.append(operand)
140
+ self._closures[value] = frozenset(found)
141
+ return self._closures[value]
142
+
143
+ def origins(self, value: Value) -> frozenset[Value]:
144
+ """Tainted values the argument depends on whose own operands are untainted."""
145
+
146
+ candidates = self.closure(value) | {value}
147
+ return frozenset(
148
+ v
149
+ for v in candidates
150
+ if self.taint.taint(v)
151
+ and not any(self.taint.taint(o) for o in self.closure(v))
152
+ )
153
+
154
+ def covered(self, origin: Value, argument: Value, proven: Mapping[Value, str]) -> bool:
155
+ """Whether every dependence path from ``origin`` to ``argument`` goes through a
156
+ proven value, so nothing of the origin reaches the sink unproven."""
157
+
158
+ if origin in proven:
159
+ return True
160
+ seen: set[Value] = set()
161
+ pending = [argument]
162
+ while pending:
163
+ value = pending.pop()
164
+ if value in proven or value in seen:
165
+ continue
166
+ if value == origin:
167
+ return False
168
+ seen.add(value)
169
+ definition = self.defs.get(value)
170
+ if definition is not None:
171
+ pending.extend(definition.operands())
172
+ return True
173
+
174
+ # ------------------------------------------------------------------ guards
175
+
176
+ def sink_block(self, flow: TaintFlow) -> BlockId | None:
177
+ for block in self.function.blocks:
178
+ for instruction in block.instructions:
179
+ if isinstance(instruction, Call) and instruction.location == flow.location:
180
+ return block.id
181
+ return None
182
+
183
+ def reaches(self, start: BlockId, target: BlockId, avoiding: BlockId) -> bool:
184
+ seen: set[BlockId] = set()
185
+ pending = [start]
186
+ while pending:
187
+ block = pending.pop()
188
+ if block == target:
189
+ return True
190
+ if block in seen or block == avoiding:
191
+ continue
192
+ seen.add(block)
193
+ pending.extend(self.cfg.successors(block))
194
+ return False
195
+
196
+ def guards(self, sink: BlockId) -> list[_Guard]:
197
+ found: list[_Guard] = []
198
+ dominator = self.tree.idom(sink)
199
+ while dominator is not None:
200
+ terminator = self.blocks[dominator].terminator
201
+ if isinstance(terminator, Branch):
202
+ then_reaches = self.reaches(terminator.then_block, sink, dominator)
203
+ else_reaches = self.reaches(terminator.else_block, sink, dominator)
204
+ if then_reaches != else_reaches:
205
+ found.append(
206
+ _Guard(terminator.condition, then_reaches, terminator.location.start_line)
207
+ )
208
+ dominator = self.tree.idom(dominator)
209
+ return found
210
+
211
+ def interpret(
212
+ self, condition: Value, truth: bool | None
213
+ ) -> tuple[dict[Value, str], set[Value]]:
214
+ """Values the condition proves safe (with the reason), and values it mentions
215
+ without proving anything. ``truth`` is ``None`` when it is not fixed at the sink.
216
+
217
+ A recognised check evaluated the wrong way (``isdigit()`` known false) yields
218
+ nothing: it is neither a proof nor a reassuring guard."""
219
+
220
+ definition = self.defs.get(condition)
221
+ proven: dict[Value, str] = {}
222
+ mentioned: set[Value] = set()
223
+ if isinstance(definition, UnaryOp) and definition.operator == "not":
224
+ return self.interpret(definition.operand, None if truth is None else not truth)
225
+ if isinstance(definition, BoolOp):
226
+ fixed = truth is not None and (definition.operator == "and") == truth
227
+ for value in definition.values:
228
+ found, seen = self.interpret(value, truth if fixed else None)
229
+ proven.update(found)
230
+ mentioned |= seen
231
+ return proven, mentioned
232
+ recognised = self.recognise(definition)
233
+ if recognised is None:
234
+ return proven, set(self.closure(condition)) | {condition}
235
+ tested, reason, when_true = recognised
236
+ if truth is None:
237
+ mentioned = set(self.closure(condition)) | {condition}
238
+ elif truth == when_true:
239
+ # The check proves ``tested``; whatever else it reads is merely mentioned.
240
+ proven[tested] = reason
241
+ mentioned = set(self.closure(condition)) | {condition}
242
+ return proven, mentioned
243
+
244
+ def recognise(self, definition: Instruction | None) -> tuple[Value, str, bool] | None:
245
+ """``(validated value, reason, truth that validates)`` for known check shapes."""
246
+
247
+ if isinstance(definition, Call) and not definition.arguments:
248
+ callee = self.defs.get(definition.callee)
249
+ if isinstance(callee, GetAttr) and callee.attribute in VALIDATORS:
250
+ return callee.object, f"guarded by {callee.attribute}()", True
251
+ if isinstance(definition, Call):
252
+ symbol = self.symbols.get(definition.callee)
253
+ validator = self.models.validator(symbol) if symbol is not None else None
254
+ if validator is not None and validator.argument < len(definition.arguments):
255
+ return definition.arguments[validator.argument], f"validated by {symbol}", True
256
+ if isinstance(definition, Compare):
257
+ left, right = definition.left, definition.right
258
+ if definition.operator in ("in", "not_in") and self.is_constant_collection(right):
259
+ return (
260
+ left,
261
+ "allowlisted by a membership check on constants",
262
+ definition.operator == "in",
263
+ )
264
+ if definition.operator in ("eq", "not_eq"):
265
+ for tested, other in ((left, right), (right, left)):
266
+ if isinstance(self.defs.get(other), Constant):
267
+ return tested, "equals a constant", definition.operator == "eq"
268
+ return None
269
+
270
+ def is_constant_collection(self, value: Value) -> bool:
271
+ definition = self.defs.get(value)
272
+ if isinstance(definition, Constant):
273
+ return isinstance(definition.value, str | bytes)
274
+ if isinstance(definition, BuildList | BuildTuple):
275
+ return all(isinstance(self.defs.get(e), Constant) for e in definition.elements)
276
+ return False
277
+
278
+ # ------------------------------------------------------------------ authorization
279
+
280
+ def authorized_by(self, condition: Value, truth: bool) -> AuthorizationGuard | None:
281
+ """The authorization guard this condition enforces when it is ``truth``."""
282
+
283
+ definition = self.defs.get(condition)
284
+ if isinstance(definition, UnaryOp) and definition.operator == "not":
285
+ return self.authorized_by(definition.operand, not truth)
286
+ if isinstance(definition, BoolOp) and (definition.operator == "and") == truth:
287
+ for value in definition.values:
288
+ found = self.authorized_by(value, truth)
289
+ if found is not None:
290
+ return found
291
+ return None
292
+ if isinstance(definition, Compare) and definition.operator in ("in", "not_in"):
293
+ # ``'logged_in' in session``: membership in an authorization store.
294
+ if truth != (definition.operator == "in"):
295
+ return None
296
+ container = self.symbols.get(definition.right)
297
+ return self.models.authorization(container) if container is not None else None
298
+ if not truth:
299
+ return None
300
+ symbol = self.symbols.get(condition)
301
+ if symbol is None and isinstance(definition, Call):
302
+ symbol = self.symbols.get(definition.callee)
303
+ return self.models.authorization(symbol) if symbol is not None else None
304
+
305
+ # ------------------------------------------------------------------ verdicts
306
+
307
+ def judge(self, flow: TaintFlow, reachable: bool) -> Verdict:
308
+ sink = self.sink_block(flow)
309
+ if sink is None or not reachable:
310
+ return Verdict(flow, Status.REFUTED, "sink unreachable by constant propagation")
311
+ origins = self.origins(flow.argument)
312
+ chain = self.closure(flow.argument) | {flow.argument}
313
+ proven: dict[Value, str] = {
314
+ value: f"numeric value within {interval}"
315
+ for value, interval in self.ranges.at(sink).items()
316
+ if value in chain
317
+ }
318
+ mentions: dict[Value, int] = {}
319
+ authorization: str | None = (
320
+ f"behind authorization ({self.authorization.label}) by decorator"
321
+ if self.authorization is not None
322
+ else None
323
+ )
324
+ for guard in self.guards(sink):
325
+ found, mentioned = self.interpret(guard.condition, guard.truth)
326
+ for value, reason in found.items():
327
+ if value in chain:
328
+ proven.setdefault(value, reason)
329
+ for origin in origins:
330
+ if origin not in mentions and mentioned & (
331
+ {origin} | {w for w in chain if origin in self.closure(w)}
332
+ ):
333
+ mentions[origin] = guard.line
334
+ if authorization is None:
335
+ guard_model = self.authorized_by(guard.condition, guard.truth)
336
+ if guard_model is not None:
337
+ authorization = f"behind authorization ({guard_model.label}) at line {guard.line}"
338
+ proofs = {
339
+ origin: sorted(
340
+ {reason for value, reason in proven.items() if value == origin or origin in self.closure(value)}
341
+ )
342
+ for origin in origins
343
+ if self.covered(origin, flow.argument, proven)
344
+ }
345
+ if origins and all(origin in proofs for origin in origins):
346
+ reasons = sorted({reason for found in proofs.values() for reason in found})
347
+ return Verdict(flow, Status.REFUTED, "; ".join(reasons))
348
+ if authorization is not None:
349
+ return Verdict(flow, Status.HOTSPOT, authorization)
350
+ unguarded = [origin for origin in origins if origin not in proofs and origin not in mentions]
351
+ if not origins or unguarded:
352
+ return Verdict(flow, Status.VULNERABILITY, "no guard on the path to the sink")
353
+ line = min(mentions.values())
354
+ return Verdict(
355
+ flow, Status.HOTSPOT, f"guard at line {line} does not prove the value safe"
356
+ )
357
+
358
+
359
+ def judge_flows(
360
+ function: FunctionIR,
361
+ cfg: CFG,
362
+ tree: DominatorTree,
363
+ taint: TaintFacts,
364
+ reachable: frozenset[BlockId],
365
+ ranges: RangeFacts | None = None,
366
+ models: ModelTable | None = None,
367
+ symbols: Mapping[Value, SymbolId] | None = None,
368
+ authorization: AuthorizationGuard | None = None,
369
+ ) -> Verdicts:
370
+ judge = _Judge(function, cfg, tree, taint, ranges, models, symbols, authorization)
371
+ verdicts = []
372
+ for flow in taint.flows:
373
+ sink = judge.sink_block(flow)
374
+ verdicts.append(judge.judge(flow, sink is not None and sink in reachable))
375
+ return Verdicts(tuple(verdicts))
376
+
377
+
378
+ def authorization_of(
379
+ function: nodes.Function, models: ModelTable, scopes: ScopeTable, symbols: SymbolTable
380
+ ) -> AuthorizationGuard | None:
381
+ """The authorization guard among the function's decorators, if any."""
382
+
383
+ scope = scopes.scope_for(function)
384
+ enclosing = scope.parent if scope.parent is not None else scope.id
385
+ for decorator in function.decorators:
386
+ symbol = symbols.resolve_expression(enclosing, decorator)
387
+ guard = models.authorization(symbol) if symbol is not None else None
388
+ if guard is not None:
389
+ return guard
390
+ return None
391
+
392
+
393
+ class RefutationAnalysis(FunctionAnalysis[Verdicts]):
394
+ name: ClassVar[str] = "findings.refutation"
395
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset(
396
+ {
397
+ TaintAnalysis,
398
+ DominanceAnalysis,
399
+ ConstantPropagation,
400
+ RangeAnalysis,
401
+ SSAAnalysis,
402
+ CFGAnalysis,
403
+ SecurityModelAnalysis,
404
+ CallGraphAnalysis,
405
+ ScopeAnalysis,
406
+ SymbolAnalysis,
407
+ }
408
+ )
409
+
410
+ @classmethod
411
+ def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> Verdicts:
412
+ ssa = ctx.get(SSAAnalysis, function)
413
+ constants = ctx.get(ConstantPropagation, function)
414
+ graph = ctx.get(CallGraphAnalysis)
415
+ models = ctx.get(SecurityModelAnalysis)
416
+ return judge_flows(
417
+ ssa,
418
+ ctx.get(CFGAnalysis, function),
419
+ ctx.get(DominanceAnalysis, function),
420
+ ctx.get(TaintAnalysis, function),
421
+ frozenset(b.id for b in ssa.blocks if constants.reachable(b.id)),
422
+ ctx.get(RangeAnalysis, function),
423
+ models,
424
+ graph.symbols(graph.name_of(function)),
425
+ authorization_of(function, models, ctx.get(ScopeAnalysis), ctx.get(SymbolAnalysis)),
426
+ )
427
+
@@ -0,0 +1,18 @@
1
+ """Parsing and adaptation of Python source into parser-independent PyHIR.
2
+
3
+ Parser-specific objects never leave this package: callers receive a PyHIR module.
4
+ """
5
+
6
+ from coretrace_python.frontend.ast_adapter import HIRBuildError, build_module
7
+ from coretrace_python.frontend.parser import ParseError, parse_source_file
8
+ from coretrace_python.hir import nodes
9
+ from coretrace_python.source import SourceFile
10
+
11
+
12
+ def build_hir(source: SourceFile) -> nodes.Module:
13
+ """Parse ``source`` and return its PyHIR module."""
14
+
15
+ return build_module(source, parse_source_file(source))
16
+
17
+
18
+ __all__ = ["HIRBuildError", "ParseError", "build_hir"]