coretrace-python-analyzer 0.20.0__py3-none-any.whl → 0.21.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.
@@ -1,4 +1,4 @@
1
1
  """CoreTrace's Python static analysis frontend."""
2
2
 
3
- __version__ = "0.20.0"
3
+ __version__ = "0.21.0"
4
4
 
@@ -8,12 +8,14 @@ from collections.abc import Iterator, Sequence
8
8
  from typing import ClassVar
9
9
 
10
10
  from coretrace_python.analysis import AnyAnalysis
11
- from coretrace_python.dependency import Advisory, DependencyAnalysis
11
+ from coretrace_python.dependency import Advisory, AdvisoryEntryPoint, DependencyAnalysis
12
12
  from coretrace_python.dependency.correlation import (
13
13
  ConditionCheck,
14
+ affected_entries,
14
15
  affected_symbols,
15
16
  check_conditions,
16
17
  evidence,
18
+ suffix_index,
17
19
  )
18
20
  from coretrace_python.findings import Confidence, Finding
19
21
  from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
@@ -30,6 +32,7 @@ class ReachableVulnerabilityPlugin(ProjectPlugin):
30
32
  affected = affected_symbols(ctx.dependencies, ctx.advisories)
31
33
  if not affected:
32
34
  return ()
35
+ suffixes = suffix_index(affected, ctx.modules)
33
36
  findings: list[Finding] = []
34
37
  for module in ctx.modules:
35
38
  graph = ctx.call_graph(module)
@@ -37,30 +40,28 @@ class ReachableVulnerabilityPlugin(ProjectPlugin):
37
40
  for site in graph.sites(function):
38
41
  if not isinstance(site.target, ExternalSymbol):
39
42
  continue
40
- for advisory in affected.get(site.target.symbol, ()):
41
- entry = advisory.entry_point(site.target.symbol)
43
+ for advisory, entry in affected_entries(site.target.symbol, affected, suffixes):
42
44
  if entry is not None and entry.read:
43
45
  continue # a call reads its callee first: reported with the reads
44
- check = check_conditions(entry, site.arguments)
46
+ check = check_conditions(entry, site.arguments, prior_calls=site.prior_calls)
45
47
  if check.contradicted is not None:
46
48
  continue
47
- findings.append(_reached(advisory, site.target.symbol, site.location, function, check))
49
+ findings.append(_reached(advisory, site.target.symbol, site.location, function, check, entry))
48
50
  # Reading ``request.form.get`` reads ``request.form``: one finding per entry
49
51
  # point and function, where the function first reads it.
50
52
  reported: set[tuple[Advisory, SymbolId]] = set()
51
53
  for read in graph.reads(function):
52
54
  for symbol in _read_through(read.symbol):
53
- for advisory in affected.get(symbol, ()):
54
- entry = advisory.entry_point(symbol)
55
+ for advisory, entry in affected_entries(symbol, affected, suffixes):
55
56
  if entry is None or not entry.read or (advisory, symbol) in reported:
56
57
  continue
57
58
  reported.add((advisory, symbol))
58
59
  check = check_conditions(entry, None)
59
- findings.append(_reached(advisory, symbol, read.location, function, check))
60
+ findings.append(_reached(advisory, symbol, read.location, function, check, entry))
60
61
  for call in ctx.templates.calls:
61
- for advisory in affected.get(call.symbol, ()):
62
- check = check_conditions(advisory.entry_point(call.symbol), None)
63
- findings.append(_reached(advisory, call.symbol, call.span, None, check))
62
+ for advisory, entry in affected_entries(call.symbol, affected, suffixes):
63
+ check = check_conditions(entry, None)
64
+ findings.append(_reached(advisory, call.symbol, call.span, None, check, entry))
64
65
  return findings
65
66
 
66
67
 
@@ -73,7 +74,12 @@ def _read_through(symbol: SymbolId) -> Iterator[SymbolId]:
73
74
 
74
75
 
75
76
  def _reached(
76
- advisory: Advisory, symbol: SymbolId, span: SourceSpan, function: str | None, check: ConditionCheck
77
+ advisory: Advisory,
78
+ symbol: SymbolId,
79
+ span: SourceSpan,
80
+ function: str | None,
81
+ check: ConditionCheck,
82
+ entry: AdvisoryEntryPoint | None,
77
83
  ) -> Finding:
78
84
  return Finding(
79
85
  rule_id="reachable-vulnerability",
@@ -85,5 +91,5 @@ def _reached(
85
91
  confidence=Confidence.HIGH,
86
92
  span=span,
87
93
  function=function,
88
- metadata=evidence(advisory, symbol, "reachable", check),
94
+ metadata=evidence(advisory, symbol, "reachable", check, entry),
89
95
  )
@@ -7,7 +7,13 @@ from typing import ClassVar
7
7
 
8
8
  from coretrace_python.analysis import AnyAnalysis
9
9
  from coretrace_python.dependency import DependencyAnalysis
10
- from coretrace_python.dependency.correlation import affected_symbols, check_conditions, ruled_out
10
+ from coretrace_python.dependency.correlation import (
11
+ affected_entries,
12
+ affected_symbols,
13
+ check_conditions,
14
+ ruled_out,
15
+ suffix_index,
16
+ )
11
17
  from coretrace_python.findings import Confidence, Finding
12
18
  from coretrace_python.interprocedural import CallGraphAnalysis, ExternalSymbol
13
19
  from coretrace_python.plugins import ProjectContext, ProjectPlugin
@@ -57,6 +63,7 @@ def _ruled_out(ctx: ProjectContext) -> dict[str, list[str]]:
57
63
  conditions: the evidence that the requirement, imported, is not reached there."""
58
64
 
59
65
  affected = affected_symbols(ctx.dependencies, ctx.advisories)
66
+ suffixes = suffix_index(affected, ctx.modules)
60
67
  excluded: dict[str, list[str]] = {}
61
68
  for module in sorted(ctx.modules):
62
69
  graph = ctx.call_graph(module)
@@ -64,8 +71,8 @@ def _ruled_out(ctx: ProjectContext) -> dict[str, list[str]]:
64
71
  for site in graph.sites(function):
65
72
  if not isinstance(site.target, ExternalSymbol):
66
73
  continue
67
- for advisory in affected.get(site.target.symbol, ()):
68
- check = check_conditions(advisory.entry_point(site.target.symbol), site.arguments)
74
+ for advisory, entry in affected_entries(site.target.symbol, affected, suffixes):
75
+ check = check_conditions(entry, site.arguments, prior_calls=site.prior_calls)
69
76
  if check.contradicted is not None:
70
77
  excluded.setdefault(advisory.id, []).append(ruled_out(module, site, check))
71
78
  return excluded
coretrace_python/cache.py CHANGED
@@ -34,6 +34,7 @@ from coretrace_python.interprocedural import (
34
34
  ModuleGraph,
35
35
  Mutation,
36
36
  NonlocalWrite,
37
+ PriorCall,
37
38
  SummaryIndex,
38
39
  SymbolRead,
39
40
  Target,
@@ -42,7 +43,7 @@ from coretrace_python.interprocedural import (
42
43
  from coretrace_python.semantic.symbols import SymbolId
43
44
  from coretrace_python.source import SourceId, SourceSpan
44
45
 
45
- CACHE_FORMAT = 12
46
+ CACHE_FORMAT = 13
46
47
 
47
48
 
48
49
  @dataclass(frozen=True)
@@ -351,18 +352,33 @@ def _encode_site(site: CallSite) -> dict[str, Any]:
351
352
  "location": _encode_span(site.location),
352
353
  "target": _encode_target(site.target),
353
354
  "arguments": _encode_arguments(site.arguments),
355
+ "prior": None
356
+ if site.prior_calls is None
357
+ else [
358
+ [str(p.symbol), _encode_span(p.location), _encode_arguments(p.arguments), p.dominates]
359
+ for p in site.prior_calls
360
+ ],
354
361
  }
355
362
 
356
363
 
357
364
  def _decode_site(data: Mapping[str, Any]) -> CallSite:
365
+ prior = data["prior"]
358
366
  return CallSite(
359
367
  _string(data["caller"]),
360
368
  _decode_span(data["location"]),
361
369
  _decode_target(data["target"]),
362
370
  _decode_arguments(data["arguments"]),
371
+ None if prior is None else tuple(_decode_prior(entry) for entry in prior),
363
372
  )
364
373
 
365
374
 
375
+ def _decode_prior(data: Any) -> PriorCall:
376
+ symbol, span, arguments, dominates = data
377
+ if not isinstance(dominates, bool):
378
+ raise TypeError(f"expected a boolean, got {dominates!r}")
379
+ return PriorCall(SymbolId(_string(symbol)), _decode_span(span), _decode_arguments(arguments), dominates)
380
+
381
+
366
382
  def _encode_arguments(arguments: Arguments) -> dict[str, Any]:
367
383
  return {
368
384
  "positional": list(arguments.positional),
@@ -6,7 +6,7 @@ events into a version specifier; ``dump_advisories`` writes them as a small JSON
6
6
  that a project keeps at its root as ``advisories.json`` or passes with ``--advisories``.
7
7
  OSV records name no affected APIs, so imported advisories feed the requirement checks
8
8
  and the SBOM; a file completed by hand with ``affected_symbols``, ``entry_points`` and
9
- their ``conditions`` also feeds the reachability and correlation checks.
9
+ their ``conditions`` and ``suffixes`` also feeds the reachability and correlation checks.
10
10
  """
11
11
 
12
12
  from __future__ import annotations
@@ -161,6 +161,8 @@ def _entry_point_entry(entry_point: AdvisoryEntryPoint) -> dict[str, Any]:
161
161
  }
162
162
  if entry_point.read:
163
163
  entry["read"] = True
164
+ if entry_point.suffixes:
165
+ entry["suffixes"] = list(entry_point.suffixes)
164
166
  if entry_point.attacker_arguments:
165
167
  entry["attacker_arguments"] = [_attacker_argument_entry(a) for a in entry_point.attacker_arguments]
166
168
  return entry
@@ -187,6 +189,10 @@ def _condition_entry(condition: Condition) -> dict[str, Any]:
187
189
  entry["position"] = condition.position
188
190
  if condition.default:
189
191
  entry["default"] = True
192
+ if condition.present:
193
+ entry["present"] = True
194
+ if condition.method is not None:
195
+ entry["method"] = condition.method
190
196
  return entry
191
197
 
192
198
 
@@ -226,9 +232,20 @@ def _entry_point(entry: Mapping[str, Any]) -> AdvisoryEntryPoint:
226
232
  tuple(_condition(c) for c in entry.get("conditions") or []),
227
233
  read,
228
234
  tuple(_attacker_argument(a) for a in entry.get("attacker_arguments") or []),
235
+ tuple(_suffix(s) for s in entry.get("suffixes") or []),
229
236
  )
230
237
 
231
238
 
239
+ def _suffix(value: Any) -> str:
240
+ if not isinstance(value, str):
241
+ raise TypeError(f"an entry point suffix must be a string, got {value!r}")
242
+ parts = value.split(".")
243
+ # A single name (``annotate``) would match every project method of that name.
244
+ if len(parts) < 2 or not all(p.isidentifier() for p in parts):
245
+ raise ValueError(f"an entry point suffix is at least two dot-separated names, got {value!r}")
246
+ return value
247
+
248
+
232
249
  def _attacker_argument(entry: Mapping[str, Any]) -> AttackerArgument:
233
250
  argument, position, variadic = entry.get("argument"), entry.get("position"), entry.get("variadic", False)
234
251
  if argument is not None and not isinstance(argument, str):
@@ -251,6 +268,9 @@ def _condition(entry: Mapping[str, Any]) -> Condition:
251
268
  default = entry.get("default", False)
252
269
  if not isinstance(default, bool):
253
270
  raise TypeError(f"condition default must be true or false, got {default!r}")
271
+ present = entry.get("present", False)
272
+ if not isinstance(present, bool):
273
+ raise TypeError(f"condition present must be true or false, got {present!r}")
254
274
  condition = Condition(
255
275
  str(entry["kind"]),
256
276
  str(entry["text"]),
@@ -258,7 +278,22 @@ def _condition(entry: Mapping[str, Any]) -> Condition:
258
278
  tuple(str(v) for v in entry.get("values") or []),
259
279
  position,
260
280
  default,
281
+ present,
282
+ None if entry.get("method") is None else str(entry["method"]),
261
283
  )
284
+ if condition.present and (
285
+ condition.kind != "argument"
286
+ or (condition.argument is None and condition.position is None)
287
+ or condition.values
288
+ or condition.default
289
+ ):
290
+ raise ValueError("a present condition names the argument that must be passed, by keyword or position, and nothing else")
291
+ if condition.kind == "sequence" and not condition.method:
292
+ raise ValueError("a sequence condition names the method of the prior call")
293
+ if condition.kind == "sequence" and condition.values and condition.argument is None:
294
+ raise ValueError("the values of a sequence condition need the argument they constrain")
295
+ if condition.kind != "sequence" and condition.method is not None:
296
+ raise ValueError(f"only a sequence condition names a method, not {condition.kind!r}")
262
297
  if condition.kind == "host" and (
263
298
  (condition.argument is None and condition.position is None) or condition.values or condition.default
264
299
  ):
@@ -21,9 +21,9 @@ from coretrace_python.dependency.graph import (
21
21
  )
22
22
  from coretrace_python.findings import Confidence, Finding, Severity
23
23
  from coretrace_python.findings.refutation import Status, Verdict, Verdicts
24
- from coretrace_python.interprocedural import Arguments, CallSite, ExternalSymbol
24
+ from coretrace_python.interprocedural import Arguments, CallSite, ExternalSymbol, PriorCall
25
25
  from coretrace_python.semantic.symbols import SymbolId
26
- from coretrace_python.taint import KEYWORD_NAMES, Sink, TaintFlow, TaintKind
26
+ from coretrace_python.taint import KEYWORD_NAMES, Sink, SuffixSink, TaintFlow, TaintKind
27
27
  from coretrace_python.taint.urls import UrlProof, redirects_disabled
28
28
 
29
29
  Affected = Mapping[SymbolId, tuple[Advisory, ...]]
@@ -49,6 +49,56 @@ def advisory_sinks(affected: Affected) -> tuple[Sink, ...]:
49
49
  return tuple(Sink(symbol, TaintKind.ADVISORY) for symbol in affected)
50
50
 
51
51
 
52
+ @dataclass(frozen=True)
53
+ class SuffixIndex:
54
+ """The curated entry-point suffixes of the matching advisories, and the project's
55
+ module prefixes: a derived symbol matches a suffix on a dot boundary, as a
56
+ ``SuffixSink`` does, and only when it belongs to a project module — another
57
+ package's ``objects.annotate`` stays out."""
58
+
59
+ entries: tuple[tuple[str, Advisory, AdvisoryEntryPoint], ...] = ()
60
+ prefixes: tuple[str, ...] = ()
61
+
62
+ def matches(self, symbol: SymbolId) -> tuple[tuple[Advisory, AdvisoryEntryPoint], ...]:
63
+ name = symbol.canonical_name
64
+ found = tuple((a, e) for tail, a, e in self.entries if name.endswith(tail))
65
+ return found if found and name.startswith(self.prefixes) else ()
66
+
67
+
68
+ def suffix_index(affected: Affected, modules: Iterable[str]) -> SuffixIndex:
69
+ advisories = {a: None for found in affected.values() for a in found}
70
+ entries = tuple((f".{s}", a, e) for a in advisories for e in a.entry_points for s in e.suffixes)
71
+ return SuffixIndex(entries, tuple(f"python.{m}." for m in sorted(set(modules))))
72
+
73
+
74
+ def affected_entries(
75
+ symbol: SymbolId, affected: Affected, suffixes: SuffixIndex | None = None
76
+ ) -> tuple[tuple[Advisory, AdvisoryEntryPoint | None], ...]:
77
+ """The advisories reached through ``symbol``, each with the entry point it is reached
78
+ by: the advisories affecting the exact symbol, then those a suffix matches."""
79
+
80
+ found: list[tuple[Advisory, AdvisoryEntryPoint | None]] = [
81
+ (a, a.entry_point(symbol)) for a in affected.get(symbol, ())
82
+ ]
83
+ if suffixes is not None:
84
+ seen = {(a.id, a.package, None if e is None else e.symbol) for a, e in found}
85
+ for advisory, entry in suffixes.matches(symbol):
86
+ key = (advisory.id, advisory.package, entry.symbol)
87
+ if key not in seen:
88
+ seen.add(key)
89
+ found.append((advisory, entry))
90
+ return tuple(found)
91
+
92
+
93
+ def advisory_suffix_sinks(affected: Affected) -> tuple[SuffixSink, ...]:
94
+ """A suffix sink per curated entry-point suffix, so the taint engine tracks flows
95
+ into the derived symbols; ``correlate`` then keeps only the project's."""
96
+
97
+ advisories = {a: None for found in affected.values() for a in found}
98
+ suffixes = {s: None for a in advisories for e in a.entry_points for s in e.suffixes}
99
+ return tuple(SuffixSink(s, TaintKind.ADVISORY) for s in suffixes)
100
+
101
+
52
102
  @dataclass(frozen=True)
53
103
  class ConditionCheck:
54
104
  """What one call tells of an entry point's conditions: those it meets, those left to
@@ -67,6 +117,7 @@ def check_conditions(
67
117
  arguments: Arguments | None,
68
118
  url: UrlProof | None = None,
69
119
  passed_as: frozenset[tuple[int | None, str | None]] = frozenset(),
120
+ prior_calls: tuple[PriorCall, ...] | None = None,
70
121
  ) -> ConditionCheck:
71
122
  """Decide ``entry``'s conditions against what the call's ``arguments`` denote and, for
72
123
  a ``host`` condition, what ``url`` proves of the value the attacker's input is passed
@@ -75,7 +126,15 @@ def check_conditions(
75
126
  controls, and the condition stays pending with that uncertainty. A ``keyword_name``
76
127
  condition is met when the attacker's input is passed as the keys of a ``**`` mapping,
77
128
  pending when the call expands a mapping whose keys are not established, and
78
- contradicted by a call writing every keyword name."""
129
+ contradicted by a call writing every keyword name. An ``argument`` condition with
130
+ ``present`` is met when the call gives the argument, whatever it denotes, and
131
+ contradicted when the call surely does not give it. A ``sequence`` condition is met
132
+ when one of the ``prior_calls`` on the call's receiver dominates it — surely executes
133
+ before it — matches the condition's method and passes what it requires; contradicted
134
+ when the receiver is followed whole and no matching call could do so, the call's own
135
+ ``arguments`` included, since across the iterations of a loop the call is its own
136
+ prior; pending otherwise — an unfollowed receiver (``prior_calls`` is None), an
137
+ undecided or non-dominating matching call, or an undecidable self-check."""
79
138
 
80
139
  if entry is None:
81
140
  return ConditionCheck()
@@ -101,10 +160,40 @@ def check_conditions(
101
160
  text = f"the host is fixed by {origin!r}, but a redirect may lead to a host the attacker controls"
102
161
  pending.append(replace(condition, text=text))
103
162
  continue
163
+ if condition.kind == "sequence":
164
+ candidates = [
165
+ p
166
+ for p in prior_calls or ()
167
+ if condition.method is not None and _named(p.symbol, condition.method)
168
+ ]
169
+ satisfied = [_first_call_satisfies(condition, p.arguments) for p in candidates]
170
+ hit = next(
171
+ (p for p, s in zip(candidates, satisfied, strict=True) if s is True and p.dominates), None
172
+ )
173
+ if hit is not None:
174
+ text = f"{condition.text} (prior {condition.method} at line {hit.location.start_line})"
175
+ met.append(replace(condition, text=text))
176
+ elif (
177
+ prior_calls is None
178
+ or condition.method is None
179
+ or None in satisfied
180
+ or True in satisfied
181
+ or _first_call_satisfies(condition, arguments) is not False
182
+ ):
183
+ pending.append(condition)
184
+ else:
185
+ return ConditionCheck(tuple(met), tuple(pending), condition, None)
186
+ continue
104
187
  given = arguments.given(condition.argument, condition.position) if condition.checkable and arguments is not None else None
105
188
  if given is None:
106
189
  pending.append(condition)
107
190
  continue
191
+ if condition.present:
192
+ if given[0]:
193
+ met.append(condition)
194
+ else:
195
+ return ConditionCheck(tuple(met), tuple(pending), condition, None)
196
+ continue
108
197
  explicit, value = given
109
198
  if explicit and value is None:
110
199
  pending.append(condition)
@@ -116,6 +205,32 @@ def check_conditions(
116
205
  return ConditionCheck(tuple(met), tuple(pending))
117
206
 
118
207
 
208
+ def _named(symbol: SymbolId, method: str) -> bool:
209
+ """Whether ``symbol`` is the ``method`` a sequence condition names, by exact
210
+ canonical name or by ``.``-suffix (``Session.get``)."""
211
+
212
+ name = symbol.canonical_name
213
+ return name == method or name.endswith("." + method)
214
+
215
+
216
+ def _first_call_satisfies(condition: Condition, arguments: Arguments | None) -> bool | None:
217
+ """Whether ``arguments`` carry what a sequence ``condition`` requires of the first
218
+ call: trivially when it names no argument, else as an argument condition reads
219
+ ``given()``; None when they cannot tell."""
220
+
221
+ if condition.argument is None and condition.position is None:
222
+ return True
223
+ if arguments is None:
224
+ return None
225
+ given = arguments.given(condition.argument, condition.position)
226
+ if given is None:
227
+ return None
228
+ explicit, value = given
229
+ if explicit and value is None:
230
+ return None
231
+ return value in condition.values if explicit else condition.default
232
+
233
+
119
234
  def _passed_in(condition: Condition, passed_as: frozenset[tuple[int | None, str | None]]) -> bool:
120
235
  """Whether a value passed as ``passed_as`` is the argument ``condition`` names."""
121
236
 
@@ -132,18 +247,31 @@ def ruled_out(module: str, site: CallSite, check: ConditionCheck) -> str:
132
247
 
133
248
  assert check.contradicted is not None and isinstance(site.target, ExternalSymbol)
134
249
  contradicted = check.contradicted
250
+ if contradicted.kind == "sequence":
251
+ return (
252
+ f"{module}:{site.location.start_line} {site.target.symbol}"
253
+ f"(no prior {contradicted.method} on the receiver)"
254
+ )
135
255
  argument = contradicted.argument or ("keywords" if contradicted.kind == "keyword_name" else f"#{contradicted.position}")
136
256
  passed = f"{argument} absent" if check.passed is None else f"{argument}={check.passed}"
137
257
  return f"{module}:{site.location.start_line} {site.target.symbol}({passed})"
138
258
 
139
259
 
140
- def evidence(advisory: Advisory, symbol: SymbolId, level: str, check: ConditionCheck) -> dict[str, str]:
260
+ def evidence(
261
+ advisory: Advisory,
262
+ symbol: SymbolId,
263
+ level: str,
264
+ check: ConditionCheck,
265
+ entry: AdvisoryEntryPoint | None = None,
266
+ ) -> dict[str, str]:
141
267
  """What a finding keeps of the advisory for ``symbol``: the level of evidence
142
268
  established, how the symbol relates to the vulnerability, and its conditions — those
143
- the call meets and those left to review."""
269
+ the call meets and those left to review. ``entry`` names the entry point ``symbol``
270
+ was matched by when it is not the advisory's own symbol, as a suffix match derives
271
+ it from the project; the metadata then keeps both."""
144
272
 
145
273
  metadata = {"advisory": advisory.id, "package": advisory.package, "symbol": str(symbol), "level": level}
146
- entry = advisory.entry_point(symbol)
274
+ entry = entry if entry is not None else advisory.entry_point(symbol)
147
275
  if entry is None:
148
276
  metadata["justification"] = DIRECT
149
277
  return metadata
@@ -164,9 +292,12 @@ def correlate(
164
292
  verdicts: Verdicts | None,
165
293
  affected: Affected,
166
294
  urls: Mapping[TaintFlow, UrlProof] | None = None,
295
+ suffixes: SuffixIndex | None = None,
296
+ prior: Mapping[TaintFlow, tuple[PriorCall, ...] | None] | None = None,
167
297
  ) -> tuple[Finding, ...]:
168
298
  """Exploitable-vulnerability findings for the non-refuted ADVISORY flows of a function,
169
- one per advisory the sink is affected by; ``urls`` holds what each flow's URL proves."""
299
+ one per advisory the sink is affected by; ``urls`` holds what each flow's URL proves
300
+ and ``prior`` the other calls on each flow's receiver, for sequence conditions."""
170
301
 
171
302
  findings: list[Finding] = []
172
303
  for flow in flows:
@@ -176,25 +307,32 @@ def correlate(
176
307
  if verdict is not None and verdict.status is Status.REFUTED:
177
308
  continue
178
309
  hotspot = verdict is not None and verdict.status is Status.HOTSPOT
179
- for advisory in affected.get(flow.sink.symbol, ()):
180
- entry = advisory.entry_point(flow.sink.symbol)
310
+ for advisory, entry in affected_entries(flow.sink.symbol, affected, suffixes):
181
311
  if entry is not None and not any(entry.exploitable_through(p, k) for p, k in flow.passed_as):
182
312
  continue
183
- check = check_conditions(entry, flow.sink_arguments, (urls or {}).get(flow), flow.passed_as)
313
+ check = check_conditions(
314
+ entry, flow.sink_arguments, (urls or {}).get(flow), flow.passed_as, (prior or {}).get(flow)
315
+ )
184
316
  if check.contradicted is None:
185
- findings.append(_exploitable(function, flow, advisory, verdict, hotspot, check))
317
+ findings.append(_exploitable(function, flow, advisory, verdict, hotspot, check, entry))
186
318
  return tuple(findings)
187
319
 
188
320
 
189
321
  def _exploitable(
190
- function: str, flow: TaintFlow, advisory: Advisory, verdict: Verdict | None, hotspot: bool, check: ConditionCheck
322
+ function: str,
323
+ flow: TaintFlow,
324
+ advisory: Advisory,
325
+ verdict: Verdict | None,
326
+ hotspot: bool,
327
+ check: ConditionCheck,
328
+ entry: AdvisoryEntryPoint | None,
191
329
  ) -> Finding:
192
330
  message = (
193
331
  f"{advisory.id}: {flow.source.label} input reaches {flow.sink.symbol}, affected in "
194
332
  f"the required {advisory.package} {advisory.vulnerable}: {advisory.summary}"
195
333
  )
196
334
  metadata = {
197
- **evidence(advisory, flow.sink.symbol, "exploitable", check),
335
+ **evidence(advisory, flow.sink.symbol, "exploitable", check, entry),
198
336
  "source": str(flow.source.symbol),
199
337
  "source_label": flow.source.label,
200
338
  "verdict": "hotspot" if hotspot else "vulnerability",
@@ -151,13 +151,21 @@ class Condition:
151
151
  ``argument`` condition names the argument by keyword, and by ``position`` when it may
152
152
  be passed positionally, and lists the ``values`` that satisfy it — symbols
153
153
  (``python.yaml.FullLoader``) or constants as Python writes them (``True``);
154
- ``default`` says an absent argument means a vulnerable value. A ``host`` condition
154
+ ``default`` says an absent argument means a vulnerable value; ``present`` says the
155
+ argument being passed at all, whatever its value, is the condition, so it excludes
156
+ ``values`` and ``default``. A ``host`` condition
155
157
  says the attacker must choose the host of the URL passed as ``argument`` (or at
156
158
  ``position``); the engine decides it from what the URL's constant text proves. A
157
159
  ``keyword_name`` condition says the attacker must choose a keyword name of the call,
158
160
  as an ORM alias expanded from a mapping; the engine decides it from the keys of what
159
- the call expands with ``**``. A ``semantic`` condition cannot be checked and is
160
- reported as pending review."""
161
+ the call expands with ``**``. A ``sequence`` condition says the flaw needs a prior
162
+ call on the same receiver: it names the ``method`` that call must be, by canonical
163
+ name or ``.``-suffix, and, when ``argument`` is named, what it must pass, as an
164
+ argument condition does. It is met when such a call dominates the entry call —
165
+ surely executes before it — matches ``method`` and passes a value in ``values``
166
+ (``default`` for an absent argument); contradicted when the receiver is fully
167
+ followed and no such prior call can precede; pending otherwise. A ``semantic``
168
+ condition cannot be checked and is reported as pending review."""
161
169
 
162
170
  kind: str
163
171
  text: str
@@ -165,6 +173,8 @@ class Condition:
165
173
  values: tuple[str, ...] = ()
166
174
  position: int | None = None
167
175
  default: bool = False
176
+ present: bool = False
177
+ method: str | None = None
168
178
 
169
179
  @property
170
180
  def checkable(self) -> bool:
@@ -204,13 +214,19 @@ class AdvisoryEntryPoint:
204
214
  An entry point is reached by a call, or — when ``read`` is set, for an attribute whose
205
215
  getter runs the affected code, such as a lazily parsed request body — by any read of
206
216
  it or of an attribute of it. When it names ``attacker_arguments``, attacker input
207
- exploits it only through one of them; otherwise through any argument."""
217
+ exploits it only through one of them; otherwise through any argument.
218
+
219
+ A suffix names the tail of a derived project symbol through which the entry point is
220
+ reached when the engine cannot type the receiver: ``Item.objects.annotate`` derives
221
+ ``python.app.models.Item.objects.annotate``, which ``objects.annotate`` matches.
222
+ Suffixes match on dot boundaries, and only symbols of the project's own modules."""
208
223
 
209
224
  symbol: SymbolId
210
225
  justification: str
211
226
  conditions: tuple[Condition, ...] = ()
212
227
  read: bool = False
213
228
  attacker_arguments: tuple[AttackerArgument, ...] = ()
229
+ suffixes: tuple[str, ...] = ()
214
230
 
215
231
  def exploitable_through(self, position: int | None, keyword: str | None) -> bool:
216
232
  """Whether attacker input in the argument at ``position`` or ``keyword`` can
@@ -53,9 +53,12 @@ from coretrace_python.dependency import (
53
53
  )
54
54
  from coretrace_python.dependency.correlation import (
55
55
  Affected,
56
+ SuffixIndex,
56
57
  advisory_sinks,
58
+ advisory_suffix_sinks,
57
59
  affected_symbols,
58
60
  correlate,
61
+ suffix_index,
59
62
  )
60
63
  from coretrace_python.findings import (
61
64
  ADVISORIES,
@@ -424,7 +427,9 @@ def analyze_project(
424
427
  components = _components(registry, root, (path for path, _ in file_advisories))
425
428
  advisories, origins = _merge_advisories(_contributions(registry, plugins, root, file_advisories))
426
429
  affected = affected_symbols(dependencies, advisories)
427
- models = plugin_models(all_plugins, root).extended(*advisory_sinks(affected))
430
+ models = plugin_models(all_plugins, root).extended(
431
+ *advisory_sinks(affected), suffixes=advisory_suffix_sinks(affected)
432
+ )
428
433
  members = models.members_by_class()
429
434
  for manager in managers.values():
430
435
  manager.provide(SecurityModelAnalysis, models)
@@ -480,6 +485,7 @@ def analyze_project(
480
485
  reused = tuple(sorted(results))
481
486
 
482
487
  module_plugins = tuple(p for p in all_plugins if not isinstance(p, ProjectPlugin))
488
+ suffixes = suffix_index(affected, analysable)
483
489
  pool = None
484
490
  if jobs > 1:
485
491
  pool = concurrent.futures.ProcessPoolExecutor(
@@ -492,7 +498,9 @@ def analyze_project(
492
498
  if pool is None:
493
499
  for component in pending:
494
500
  batch = {name: analysable[name] for name in sorted(component)}
495
- computed.update(_analyse_managers(batch, _seed(results, graph, component), module_plugins, affected))
501
+ computed.update(
502
+ _analyse_managers(batch, _seed(results, graph, component), module_plugins, affected, suffixes)
503
+ )
496
504
  for manager in batch.values():
497
505
  manager.run(ResultsEvicted)
498
506
  else:
@@ -509,6 +517,7 @@ def analyze_project(
509
517
  _encode_routes(routes),
510
518
  tuple(sorted(escaped)),
511
519
  template_calls,
520
+ project=tuple(sorted(analysable)),
512
521
  ),
513
522
  )
514
523
  for component in pending
@@ -734,6 +743,7 @@ def _analyse_managers(
734
743
  seed: SummaryIndex,
735
744
  plugins: tuple[Plugin, ...],
736
745
  affected: Affected,
746
+ suffixes: SuffixIndex,
737
747
  ) -> dict[str, CachedModule]:
738
748
  """Analyse one component: iterate its summaries to a fixpoint over ``seed`` (§21),
739
749
  then extract what the rest of the run needs from each module. Each member registers
@@ -762,7 +772,7 @@ def _analyse_managers(
762
772
  for manager in managers.values():
763
773
  manager.run(ProjectSummariesUpdated)
764
774
  manager.provide(ProjectSummaries, index)
765
- return {name: _analyse_module(manager, plugins, affected) for name, manager in managers.items()}
775
+ return {name: _analyse_module(manager, plugins, affected, suffixes) for name, manager in managers.items()}
766
776
 
767
777
 
768
778
  @dataclass(frozen=True)
@@ -780,6 +790,9 @@ class _Batch:
780
790
  routes: tuple[tuple[str, str, str, int], ...] = ()
781
791
  escaped: tuple[str, ...] = ()
782
792
  template_calls: TemplateCalls = field(default_factory=TemplateCalls)
793
+ # Every analysable module of the project, not only the component's: a curated
794
+ # entry-point suffix matches derived symbols of any project module.
795
+ project: tuple[str, ...] = ()
783
796
 
784
797
 
785
798
  def _analyse_batch(batch: _Batch) -> dict[str, dict[str, Any]]:
@@ -796,7 +809,9 @@ def _analyse_batch(batch: _Batch) -> dict[str, dict[str, Any]]:
796
809
  continue
797
810
  advisories, _ = _merge_advisories(_contributions(registry, batch.plugins, batch.root, file_advisories))
798
811
  affected = affected_symbols(dependencies, advisories)
799
- models = plugin_models(all_plugins, batch.root).extended(*advisory_sinks(affected))
812
+ models = plugin_models(all_plugins, batch.root).extended(
813
+ *advisory_sinks(affected), suffixes=advisory_suffix_sinks(affected)
814
+ )
800
815
  routes = _decode_routes(batch.routes)
801
816
  escaped = frozenset(batch.escaped)
802
817
  members = models.members_by_class()
@@ -810,7 +825,8 @@ def _analyse_batch(batch: _Batch) -> dict[str, dict[str, Any]]:
810
825
  manager.provide(ClearingAnalysis, models.clearing(escaped))
811
826
  manager.provide(TemplateCallsAnalysis, batch.template_calls)
812
827
  module_plugins = tuple(p for p in all_plugins if not isinstance(p, ProjectPlugin))
813
- results = _analyse_managers(managers, decode_index(batch.seed), module_plugins, affected)
828
+ suffixes = suffix_index(affected, batch.project)
829
+ results = _analyse_managers(managers, decode_index(batch.seed), module_plugins, affected, suffixes)
814
830
  return {name: encode(entry) for name, entry in results.items()}
815
831
 
816
832
 
@@ -860,7 +876,7 @@ def _configuration_key(
860
876
 
861
877
 
862
878
  def _analyse_module(
863
- manager: AnalysisManager, plugins: tuple[Plugin, ...], affected: Affected
879
+ manager: AnalysisManager, plugins: tuple[Plugin, ...], affected: Affected, suffixes: SuffixIndex
864
880
  ) -> CachedModule:
865
881
  """One module's findings, summaries, call sites and symbol reads: what the cache
866
882
  keeps (§11)."""
@@ -875,8 +891,17 @@ def _analyse_module(
875
891
  ssa = manager.get(SSAAnalysis, function)
876
892
  defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
877
893
  urls = {flow: flow_url(flow, defs, strings) for flow in flows if flow.kinds & TaintKind.ADVISORY}
894
+ name = graph.name_of(function)
895
+ # A flow through a callee reaches the sink at another call: no receiver here.
896
+ prior = {
897
+ flow: graph.prior_calls_at(name, flow.location)
898
+ for flow in flows
899
+ if flow.kinds & TaintKind.ADVISORY and flow.through is None
900
+ }
878
901
  correlated.extend(
879
- correlate(graph.name_of(function), flows, manager.get(RefutationAnalysis, function), affected, urls)
902
+ correlate(
903
+ name, flows, manager.get(RefutationAnalysis, function), affected, urls, suffixes, prior
904
+ )
880
905
  )
881
906
  return CachedModule(
882
907
  manager.get(EntryPointAnalysis),
@@ -8,6 +8,7 @@ from coretrace_python.interprocedural.callgraph import (
8
8
  ExternalSymbol,
9
9
  KnownFunction,
10
10
  ModuleFunction,
11
+ PriorCall,
11
12
  Signature,
12
13
  SignaturesAnalysis,
13
14
  SymbolRead,
@@ -58,6 +59,7 @@ __all__ = [
58
59
  "ModuleGraph",
59
60
  "Mutation",
60
61
  "NonlocalWrite",
62
+ "PriorCall",
61
63
  "ProjectSummaries",
62
64
  "Signature",
63
65
  "SignaturesAnalysis",
@@ -11,12 +11,13 @@ from __future__ import annotations
11
11
 
12
12
  import re
13
13
  from collections.abc import Mapping
14
- from dataclasses import dataclass, field
14
+ from dataclasses import dataclass, field, replace
15
15
  from types import MappingProxyType
16
16
  from typing import ClassVar
17
17
 
18
18
  from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
19
- from coretrace_python.cfg import CFGError
19
+ from coretrace_python.cfg import BlockId, CFGError
20
+ from coretrace_python.cfg.dominance import DominanceAnalysis, DominatorTree
20
21
  from coretrace_python.hir import nodes
21
22
  from coretrace_python.ir.defuse import DefUse, def_use
22
23
  from coretrace_python.ir.lowering import (
@@ -27,6 +28,7 @@ from coretrace_python.ir.lowering import (
27
28
  )
28
29
  from coretrace_python.ir.model import (
29
30
  Await,
31
+ BasicBlock,
30
32
  BuildDict,
31
33
  Call,
32
34
  Constant,
@@ -93,12 +95,26 @@ class Arguments:
93
95
  return None if self.unpacked else (False, None)
94
96
 
95
97
 
98
+ @dataclass(frozen=True)
99
+ class PriorCall:
100
+ """Another call on the same receiver in this function; ``dominates`` says it surely
101
+ executes before this one."""
102
+
103
+ symbol: SymbolId
104
+ location: SourceSpan
105
+ arguments: Arguments
106
+ dominates: bool
107
+
108
+
96
109
  @dataclass(frozen=True)
97
110
  class CallSite:
98
111
  caller: str
99
112
  location: SourceSpan
100
113
  target: Target
101
114
  arguments: Arguments = field(default_factory=Arguments)
115
+ # None: no receiver, or one the engine cannot follow; a tuple is the complete set
116
+ # of other external calls on the receiver, in source order.
117
+ prior_calls: tuple[PriorCall, ...] | None = None
102
118
 
103
119
 
104
120
  @dataclass(frozen=True)
@@ -201,6 +217,13 @@ class CallGraph:
201
217
  site = self._at.get((caller, location))
202
218
  return site.arguments if site is not None else Arguments()
203
219
 
220
+ def prior_calls_at(self, caller: str, location: SourceSpan) -> tuple[PriorCall, ...] | None:
221
+ """The other calls on the receiver of the call at ``location`` in ``caller``;
222
+ None when there is no receiver, or one the engine cannot follow."""
223
+
224
+ site = self._at.get((caller, location))
225
+ return site.prior_calls if site is not None else None
226
+
204
227
  def callees(self, caller: str) -> frozenset[str]:
205
228
  return frozenset(
206
229
  site.target.name for site in self.sites(caller) if isinstance(site.target, KnownFunction)
@@ -335,6 +358,105 @@ def _position(span: SourceSpan) -> tuple[int, int]:
335
358
  return span.start_line, span.start_column
336
359
 
337
360
 
361
+ # A receiver carrying more calls than this is left unfollowed: the sibling pairing
362
+ # stays bounded, and such code is generated, not a client bound and used twice.
363
+ MAX_RECEIVER_CALLS = 12
364
+
365
+
366
+ def _relate_receivers(
367
+ sites: list[CallSite],
368
+ calls: list[tuple[Call, BlockId, int]],
369
+ function: FunctionIR,
370
+ defs: Mapping[Value, Instruction],
371
+ uses: DefUse,
372
+ symbols: Mapping[Value, SymbolId],
373
+ tree: DominatorTree,
374
+ ) -> tuple[CallSite, ...]:
375
+ """The ``sites`` with ``prior_calls`` filled wherever the receiver's whole lifetime
376
+ is visible, so the set of its other calls is complete and program order is decided
377
+ by dominance; any other receiver leaves ``None``. ``calls`` gives each site's
378
+ instruction with its block and index, aligned with ``sites``."""
379
+
380
+ groups: dict[Value, list[int]] = {}
381
+ for index, (call, _, _) in enumerate(calls):
382
+ callee = defs.get(call.callee)
383
+ if isinstance(callee, GetAttr):
384
+ groups.setdefault(callee.object, []).append(index)
385
+ related = list(sites)
386
+ blocks = {block.id: block for block in function.blocks}
387
+ for receiver, group in groups.items():
388
+ if not _followed(receiver, group, sites, blocks, defs, uses, symbols):
389
+ continue
390
+ ordered = sorted(group, key=lambda member: _position(sites[member].location))
391
+ for index in group:
392
+ _, block, at = calls[index]
393
+ related[index] = replace(
394
+ sites[index],
395
+ prior_calls=tuple(
396
+ PriorCall(
397
+ target.symbol,
398
+ sites[other].location,
399
+ sites[other].arguments,
400
+ _executes_before(calls[other], block, at, tree),
401
+ )
402
+ for other in ordered
403
+ if other != index and isinstance(target := sites[other].target, ExternalSymbol)
404
+ ),
405
+ )
406
+ return tuple(related)
407
+
408
+
409
+ def _followed(
410
+ receiver: Value,
411
+ group: list[int],
412
+ sites: list[CallSite],
413
+ blocks: Mapping[BlockId, BasicBlock],
414
+ defs: Mapping[Value, Instruction],
415
+ uses: DefUse,
416
+ symbols: Mapping[Value, SymbolId],
417
+ ) -> bool:
418
+ """Whether the receiver's whole lifetime is visible: bound once to the result of a
419
+ call or a ``with`` denoting a symbol, and read only to call external methods. Only
420
+ then is ``group`` complete enough to claim a prior call surely missing; a receiver
421
+ returned, passed to another call, mutated through an attribute or merged by a
422
+ ``Phi`` may be called elsewhere, and stays unfollowed."""
423
+
424
+ if len(group) > MAX_RECEIVER_CALLS or receiver not in symbols:
425
+ return False
426
+ if any(not isinstance(sites[index].target, ExternalSymbol) for index in group):
427
+ return False
428
+ made = defs.get(receiver)
429
+ while isinstance(made, Await):
430
+ made = defs.get(made.value)
431
+ if not isinstance(made, Call | WithEnter):
432
+ return False
433
+ for use in uses.uses(receiver):
434
+ if use.index is None: # a terminator use, as a return, escapes
435
+ return False
436
+ read = blocks[use.block].instructions[use.index]
437
+ if not isinstance(read, GetAttr):
438
+ return False
439
+ for called in uses.uses(read.result):
440
+ if called.index is None:
441
+ return False
442
+ call = blocks[called.block].instructions[called.index]
443
+ if not isinstance(call, Call) or call.callee != read.result or read.result in call.argument_values():
444
+ return False
445
+ return True
446
+
447
+
448
+ def _executes_before(
449
+ other: tuple[Call, BlockId, int], block: BlockId, index: int, tree: DominatorTree
450
+ ) -> bool:
451
+ """Whether ``other`` surely executes before the call at ``index`` in ``block``: it
452
+ comes earlier in the same block, or its block dominates this one."""
453
+
454
+ _, other_block, other_index = other
455
+ if other_block == block:
456
+ return other_index < index
457
+ return tree.dominates(other_block, block)
458
+
459
+
338
460
  def resolve_targets(
339
461
  function: FunctionIR,
340
462
  scopes: ScopeTable,
@@ -471,7 +593,7 @@ def _typed_with_module_classes(
471
593
  class CallGraphAnalysis(Analysis[CallGraph]):
472
594
  name: ClassVar[str] = "interprocedural.callgraph"
473
595
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset(
474
- {SSAAnalysis, ScopeAnalysis, SymbolAnalysis, SignaturesAnalysis}
596
+ {SSAAnalysis, ScopeAnalysis, SymbolAnalysis, SignaturesAnalysis, DominanceAnalysis}
475
597
  )
476
598
 
477
599
  @classmethod
@@ -537,8 +659,9 @@ class CallGraphAnalysis(Analysis[CallGraph]):
537
659
  defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
538
660
  uses = def_use(ssa)
539
661
  found: list[CallSite] = []
662
+ calls: list[tuple[Call, BlockId, int]] = []
540
663
  for block in ssa.blocks:
541
- for instruction in block.instructions:
664
+ for index, instruction in enumerate(block.instructions):
542
665
  if isinstance(instruction, Call):
543
666
  found.append(
544
667
  CallSite(
@@ -548,6 +671,9 @@ class CallGraphAnalysis(Analysis[CallGraph]):
548
671
  _arguments(instruction, symbols[name], defs, uses),
549
672
  )
550
673
  )
551
- sites[name] = tuple(found)
674
+ calls.append((instruction, block.id, index))
675
+ sites[name] = _relate_receivers(
676
+ found, calls, ssa, defs, uses, symbols[name], ctx.get(DominanceAnalysis, function)
677
+ )
552
678
  reads[name] = _reads(name, ssa, symbols[name])
553
679
  return CallGraph(definitions, sites, frozenset(unsupported), symbols, reads)
@@ -356,20 +356,23 @@ class ModelTable:
356
356
  return None
357
357
 
358
358
  def sink(self, symbol: SymbolId) -> Sink | None:
359
+ # An exact sink and the matching suffix sinks compose: an advisory sink on a
360
+ # derived symbol must not shadow ``objects.extra``'s SQL kind, nor conversely.
359
361
  found = self._by_symbol[Sink].get(symbol)
360
- if isinstance(found, Sink):
361
- return found
362
+ merged = found if isinstance(found, Sink) else None
362
363
  for suffix in self.suffix_sinks:
363
364
  if symbol.canonical_name.endswith(f".{suffix.suffix}"):
364
- return Sink(symbol, suffix.kinds, suffix.positions)
365
- return None
365
+ matched = Sink(symbol, suffix.kinds, suffix.positions)
366
+ merged = matched if merged is None else merged.merged(matched)
367
+ return merged
366
368
 
367
369
  def route_registrar(self, symbol: SymbolId) -> RouteRegistrar | None:
368
370
  found = self._by_symbol[RouteRegistrar].get(symbol)
369
371
  return found if isinstance(found, RouteRegistrar) else None
370
372
 
371
- def extended(self, *sinks: Sink) -> ModelTable:
372
- """A table with extra sinks; a sink already present gains the new kinds."""
373
+ def extended(self, *sinks: Sink, suffixes: tuple[SuffixSink, ...] = ()) -> ModelTable:
374
+ """A table with extra sinks and suffix sinks; a sink already present gains the
375
+ new kinds, a suffix sink already present is kept once."""
373
376
 
374
377
  merged = {sink.symbol: sink for sink in self.sinks}
375
378
  for sink in sinks:
@@ -385,7 +388,7 @@ class ModelTable:
385
388
  self.authorizations,
386
389
  self.named_parameters,
387
390
  self.route_registrars,
388
- self.suffix_sinks,
391
+ self.suffix_sinks + tuple(s for s in suffixes if s not in self.suffix_sinks),
389
392
  self.safe_arguments,
390
393
  self.template_renders,
391
394
  self.members,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coretrace-python-analyzer
3
- Version: 0.20.0
3
+ Version: 0.21.0
4
4
  Summary: Static security analysis infrastructure for Python code
5
5
  Project-URL: Homepage, https://github.com/CoreTrace/coretrace-python-analyzer
6
6
  Project-URL: Repository, https://github.com/CoreTrace/coretrace-python-analyzer
@@ -1,8 +1,8 @@
1
- coretrace_python/__init__.py,sha256=CxDlOASpblTY0nlvktbHjBs1Ntjlqj8ulKKg0N6LV6c,76
1
+ coretrace_python/__init__.py,sha256=qxOYJ7AnuZXOGjD8kgEwDnqkaV4dOwzK_MuofIgTfc4,76
2
2
  coretrace_python/__main__.py,sha256=V8DnYtRboISK2VCKm7DGHLREKvqLbqApyWlKBOs26wU,65
3
- coretrace_python/cache.py,sha256=cXDJvi0GqEGwC9lK6TH_nMYIH64sgbZUToQMnf5lAA0,13908
3
+ coretrace_python/cache.py,sha256=yrwI4echBNdOOJpfRMiZMeiUrnnP-dN4AHLwjUu6uSo,14559
4
4
  coretrace_python/cli.py,sha256=G5jhb6g6RgSRSjjS7DBgziIjUM1vTFwalJGvFHVKxrg,11225
5
- coretrace_python/engine.py,sha256=i_YckOVHZa_Mcwxgi2jsMkw8O49zHFQZ313mA_Xtl-4,37643
5
+ coretrace_python/engine.py,sha256=NJZDfoEfPis0U4wm06vCqLFptdAv1s7V0IAvGdC6ujU,38676
6
6
  coretrace_python/abstract/__init__.py,sha256=UK1yHTU4fldnW0W-e-nyPEOsqCnXqGPVv7LkxfyBMII,1021
7
7
  coretrace_python/abstract/constants.py,sha256=z4-e7IzysxzGi58wFFff1MlEoV8l7AZsB9EL-q6b7vY,9071
8
8
  coretrace_python/abstract/heap.py,sha256=uJAA0bYnN3-4fdESo6o4CzlYJSIpwlFLoyccacN6Uk8,9831
@@ -15,11 +15,11 @@ coretrace_python/analysis/provider.py,sha256=Q_OlEcoLzBAsNdAGWijbj_n3E2Ep5UQ4Z8_
15
15
  coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py,sha256=_CJYHijv4DTgfdumcCqAVV5_MPWjfZOadxeM5pQdw_k,1845
16
16
  coretrace_python/bundled/dependency/dependency_policy/plugin.toml,sha256=4w1TA-UKsa82WTnd1xy4bJuU3_G2RNCAWNICLPcZLMk,210
17
17
  coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml,sha256=LuF9jQqZfplN_GBky69Xjq2pksaX1Fs8PYEir-7-vY8,275
18
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py,sha256=G6JfE-7gipA_0etgno0XZ-PeRdzdcGzZ3OiKD0P8nyA,4219
18
+ coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py,sha256=5hNA3UoCULDXgKC3gBIQOKD1oeEe_qZ3V-2y5vkmPsI,4362
19
19
  coretrace_python/bundled/dependency/sample_advisories/plugin.toml,sha256=cME4pSFzkFsxmIJGGBSrmXqynAd0dg34X-rgEoZ_u6E,184
20
20
  coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py,sha256=jw98GK6hh5RAx05t8QPcUzsFMfGigAL8418vZLprKlc,3710
21
21
  coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml,sha256=4-OnbEb3lYVAoTfuz8mDPH4L9fHOPQ2KW1dDFAbClFk,267
22
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py,sha256=THXqpuffQZ8nuv1b8zQrgkA7eTI1PxhODATMzqhWeyY,3393
22
+ coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py,sha256=KT9L5GHUV6EsOb9sEnbKpwPx5hwpzxp2hC5RJ9G1ldc,3523
23
23
  coretrace_python/bundled/models/aiohttp/aiohttp_models.py,sha256=X3ZYgbrezHbsP3D9rVt-qM1_Iecu6nT_Yz9H7-XGl4M,3422
24
24
  coretrace_python/bundled/models/aiohttp/plugin.toml,sha256=YvU60RUBexYBtIxbXdmUhO83BoijmOe3MeQKwY8QDaM,220
25
25
  coretrace_python/bundled/models/bottle/bottle_models.py,sha256=Y0SUMCNGiGhzcMj0uxBLhCbLFgM7sBc4XBB-C5LEuDw,2200
@@ -94,9 +94,9 @@ coretrace_python/dataflow/__init__.py,sha256=N5uY8VTNjEk6-ARCgDkxr6uGqQIz2NbF-U9
94
94
  coretrace_python/dataflow/lattice.py,sha256=HgYGVcKVUlexvEc0yT47aQCudLxU_SWLpHhr17igkiE,1929
95
95
  coretrace_python/dataflow/solver.py,sha256=iDsW0wujhM2DXplu2JMKC46BRsroXlNETHPEoikPeG4,3475
96
96
  coretrace_python/dependency/__init__.py,sha256=BxkxnNwIEpAYtwbshCQovG-YXJ80QbwyzkmyJdbgsVg,1184
97
- coretrace_python/dependency/advisories.py,sha256=t1BKlfGx6GkPd0BNAWjli8-bIJj347A5zvfuWPO4rlw,10818
98
- coretrace_python/dependency/correlation.py,sha256=HsERzPR9A5NceRNh1dnNUO5B7saJFdXz0TrhI3c8IzY,9855
99
- coretrace_python/dependency/graph.py,sha256=ZaDSy1h3H4AS8kwGHV768wYf_R4KcqDnxzDQMsoIa88,18847
97
+ coretrace_python/dependency/advisories.py,sha256=ktGkMWJifjCQotP3ulJ5xK2IasDmMoox2gsne-AwwHU,12649
98
+ coretrace_python/dependency/correlation.py,sha256=Z6GNsDK7Ji7E5xyZcJtinNdEQQToO9FbmV6ojDKwov0,16124
99
+ coretrace_python/dependency/graph.py,sha256=939KKNugO9ipdWlShOquwEefC5qif5JbaRNstj50XkE,19971
100
100
  coretrace_python/dependency/policy.py,sha256=bsxZXL9xC0jnAzkzDh6ncnW8oKLG2H13jYmXF_q99Ng,2454
101
101
  coretrace_python/dependency/sbom.py,sha256=rzJ-t0Mwoghej8nk0N63Dpjr_wYXm6JzuOPXjD8Kqs4,3234
102
102
  coretrace_python/dependency/vex.py,sha256=bafWyziWWQP-Fjw1bvyOcOPsdFMwKy-sjMGWL7IPY3w,8048
@@ -113,8 +113,8 @@ coretrace_python/frontend/parser.py,sha256=6-CG7rWxUYeOGeDP206SJWy4BMQUP9Mr6Jf-t
113
113
  coretrace_python/hir/__init__.py,sha256=xMpvj-RSp6bd78a_hhkqbe99-snjERkH-_Lv5o9ASbw,180
114
114
  coretrace_python/hir/nodes.py,sha256=4BI0f3UhKbnz9ZIxMoKXM7ujav0BmRpD0OXaoblRxIk,12364
115
115
  coretrace_python/hir/visitors.py,sha256=JFnX09hK6trLyRcWgZ5ehOG2ywX3VvkqQ1h82nahYNg,885
116
- coretrace_python/interprocedural/__init__.py,sha256=q8u1eIpM55wqVtn46cxuymOn5D5DWU7K35i8Nzzbc3w,1604
117
- coretrace_python/interprocedural/callgraph.py,sha256=a4LNbp8ZboIgwkNgWIzYHipS1yHwXvgQy33ariECRcE,24452
116
+ coretrace_python/interprocedural/__init__.py,sha256=Wq0ipLA3pqoKoHgCSqYJPMJtLKkJdhrQQGhEVbBG5jE,1636
117
+ coretrace_python/interprocedural/callgraph.py,sha256=1vpzDscTI2mq0l3Xq7Cqgv7QEoZ49ILdPpDkaO_EJBE,29667
118
118
  coretrace_python/interprocedural/modulegraph.py,sha256=xKlO_L2mcZi0RoG9h2ulacYecqUhM5BG1RrdC6_HUk8,13036
119
119
  coretrace_python/interprocedural/summaries.py,sha256=6lybEynjtptZGGBw_3WKXx9ZZZqN96xL-r-tzo8fbMI,33862
120
120
  coretrace_python/ir/__init__.py,sha256=ShBPYYqoQFePP-mgBjGezQ2acel0-ReOHF9vCC-G_4Q,232
@@ -145,13 +145,13 @@ coretrace_python/source/manager.py,sha256=JRmMOcWfS1nocbVrdcvfQTqEfIkmnb5JP857Tp
145
145
  coretrace_python/source/model.py,sha256=FZnN5mlir3tzbIpWoxnn6YtRi9UcJ_y0sZM60tKr8U4,1727
146
146
  coretrace_python/taint/__init__.py,sha256=Da0nmvrxm9uKsmQogtHdc-FRrWo_xxpnLBDCSQ-xp9g,1867
147
147
  coretrace_python/taint/engine.py,sha256=NvGhkBpBviCKE0BGHlgEgKacKC55fSxjwUwkaBz7pkk,54415
148
- coretrace_python/taint/models.py,sha256=pCvE-6dYKYSLK7pylxhLWpeUcMfMyXlh_1VUJbzieo8,20405
148
+ coretrace_python/taint/models.py,sha256=Rzi6fSVEfNC913NDB9y5sR4kraZPKR7D4qRLAURMm_c,20827
149
149
  coretrace_python/taint/routes.py,sha256=UAJKXcw_sM9shf58FfDroxVO9bNSm4qrQdZknwUB0lo,6110
150
150
  coretrace_python/taint/templates.py,sha256=gUpUvVk_UIVY2a3T5ENm2TfBuPCDhnu2lu8h50srZ4Q,43963
151
151
  coretrace_python/taint/urls.py,sha256=kb0-uOT12ww7R3Jzs_lPKg9lFs74JI-bpa9pXX105xM,3166
152
- coretrace_python_analyzer-0.20.0.dist-info/METADATA,sha256=iC4jLQhKfefmmGcBpJSgaVFWY-SassLvisc6EoUtdz4,3217
153
- coretrace_python_analyzer-0.20.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
154
- coretrace_python_analyzer-0.20.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
155
- coretrace_python_analyzer-0.20.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
156
- coretrace_python_analyzer-0.20.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
157
- coretrace_python_analyzer-0.20.0.dist-info/RECORD,,
152
+ coretrace_python_analyzer-0.21.0.dist-info/METADATA,sha256=znaywXU-dm6Y2uyWG9vgT4CGZFC-V25w9k5fIxzsFDE,3217
153
+ coretrace_python_analyzer-0.21.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
154
+ coretrace_python_analyzer-0.21.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
155
+ coretrace_python_analyzer-0.21.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
156
+ coretrace_python_analyzer-0.21.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
157
+ coretrace_python_analyzer-0.21.0.dist-info/RECORD,,