coretrace-python-analyzer 0.7.0__py3-none-any.whl → 0.8.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.7.0"
3
+ __version__ = "0.8.0"
4
4
 
coretrace_python/cache.py CHANGED
@@ -25,6 +25,7 @@ from coretrace_python.findings import Confidence, Finding, Severity
25
25
  from coretrace_python.interprocedural import (
26
26
  Arguments,
27
27
  CallSite,
28
+ Cleared,
28
29
  ExternalCall,
29
30
  ExternalSymbol,
30
31
  FunctionSummary,
@@ -41,7 +42,7 @@ from coretrace_python.interprocedural import (
41
42
  from coretrace_python.semantic.symbols import SymbolId
42
43
  from coretrace_python.source import SourceId, SourceSpan
43
44
 
44
- CACHE_FORMAT = 9
45
+ CACHE_FORMAT = 10
45
46
 
46
47
 
47
48
  @dataclass(frozen=True)
@@ -237,6 +238,8 @@ def _encode_call(call: ExternalCall) -> dict[str, Any]:
237
238
  "location": _encode_span(call.location),
238
239
  "call_site": None if call.call_site is None else _encode_span(call.call_site),
239
240
  "given": _encode_arguments(call.arguments),
241
+ "argument_cleared": [_encode_cleared(c) for c in call.argument_cleared],
242
+ "keyword_cleared": [_encode_cleared(c) for c in call.keyword_cleared],
240
243
  }
241
244
 
242
245
 
@@ -249,9 +252,19 @@ def _decode_call(data: Mapping[str, Any]) -> ExternalCall:
249
252
  _decode_span(data["location"]),
250
253
  None if site is None else _decode_span(site),
251
254
  _decode_arguments(data["given"]),
255
+ tuple(_decode_cleared(c) for c in data["argument_cleared"]),
256
+ tuple(_decode_cleared(c) for c in data["keyword_cleared"]),
252
257
  )
253
258
 
254
259
 
260
+ def _encode_cleared(cleared: Cleared) -> list[list[int]]:
261
+ return [[parameter, bits] for parameter, bits in cleared]
262
+
263
+
264
+ def _decode_cleared(data: Any) -> Cleared:
265
+ return tuple((_integer(parameter), _integer(bits)) for parameter, bits in data)
266
+
267
+
255
268
  def _encode_summary(summary: FunctionSummary) -> dict[str, Any]:
256
269
  return {
257
270
  "name": summary.name,
@@ -275,6 +288,7 @@ def _encode_summary(summary: FunctionSummary) -> dict[str, Any]:
275
288
  for w in summary.nonlocal_writes
276
289
  ],
277
290
  "static": summary.static,
291
+ "return_cleared": _encode_cleared(summary.return_cleared),
278
292
  }
279
293
 
280
294
 
@@ -303,6 +317,7 @@ def _decode_summary(data: Mapping[str, Any]) -> FunctionSummary:
303
317
  for w in data["nonlocal_writes"]
304
318
  ),
305
319
  bool(data["static"]),
320
+ _decode_cleared(data["return_cleared"]),
306
321
  )
307
322
 
308
323
 
coretrace_python/cli.py CHANGED
@@ -27,6 +27,7 @@ from coretrace_python.reporters import FORMATS, render
27
27
  from coretrace_python.semantic.imports import ImportResolutionError
28
28
  from coretrace_python.semantic.scopes import ScopeError
29
29
  from coretrace_python.source import SourceManager
30
+ from coretrace_python.taint import ModelError
30
31
 
31
32
  EXIT_CLEAN = 0
32
33
  EXIT_FINDINGS = 1
@@ -45,6 +46,7 @@ _ANALYSIS_ERRORS = (
45
46
  AnalysisError,
46
47
  ManifestError,
47
48
  IncompatiblePluginError,
49
+ ModelError,
48
50
  )
49
51
 
50
52
 
@@ -70,6 +70,7 @@ from coretrace_python.interprocedural import (
70
70
  CallGraph,
71
71
  CallGraphAnalysis,
72
72
  CallSite,
73
+ ClearingAnalysis,
73
74
  FunctionSummary,
74
75
  ModuleFunction,
75
76
  ModuleGraph,
@@ -149,6 +150,7 @@ ALL_ANALYSES: tuple[AnyAnalysis, ...] = (
149
150
  DependencyAnalysis,
150
151
  RegisteredRoutes,
151
152
  EscapedTemplates,
153
+ ClearingAnalysis,
152
154
  )
153
155
 
154
156
 
@@ -188,6 +190,7 @@ class ResultsEvicted(TransformationPass):
188
190
  DependencyAnalysis,
189
191
  RegisteredRoutes,
190
192
  EscapedTemplates,
193
+ ClearingAnalysis,
191
194
  }
192
195
  )
193
196
 
@@ -246,7 +249,9 @@ def build_manager(
246
249
  """A manager with every engine analysis registered and the engine inputs provided."""
247
250
 
248
251
  manager = _register_all(module)
249
- manager.provide(SecurityModelAnalysis, (models or SecurityModelRegistry()).freeze())
252
+ table = (models or SecurityModelRegistry()).freeze()
253
+ manager.provide(SecurityModelAnalysis, table)
254
+ manager.provide(ClearingAnalysis, table.clearing())
250
255
  manager.provide(ProjectSummaries, SummaryIndex())
251
256
  manager.provide(RegisteredRoutes, _routes_of(manager))
252
257
  return manager
@@ -276,10 +281,14 @@ def load_plugins(plugin_roots: Sequence[Path], manager: AnalysisManager) -> Plug
276
281
  return registry
277
282
 
278
283
 
279
- def plugin_models(plugins: Iterable[Plugin]) -> ModelTable:
284
+ def plugin_models(plugins: Iterable[Plugin], root: Path | None = None) -> ModelTable:
285
+ """The models of ``plugins``, with those they read from the project at ``root``."""
286
+
280
287
  models = SecurityModelRegistry()
281
288
  for plugin in plugins:
282
289
  models.register(*plugin.models, origin=plugin.name)
290
+ if root is not None:
291
+ models.register(*plugin.project_models(root), origin=plugin.name)
283
292
  return models.freeze()
284
293
 
285
294
 
@@ -294,7 +303,9 @@ def analyze_file(source: SourceFile, plugin_roots: Sequence[Path]) -> FileAnalys
294
303
 
295
304
  manager = _register_all(build_hir(source))
296
305
  registry = load_plugins(plugin_roots, manager)
297
- manager.provide(SecurityModelAnalysis, plugin_models(loaded.plugin for loaded in registry))
306
+ table = plugin_models(loaded.plugin for loaded in registry)
307
+ manager.provide(SecurityModelAnalysis, table)
308
+ manager.provide(ClearingAnalysis, table.clearing())
298
309
  manager.provide(ProjectSummaries, SummaryIndex())
299
310
  manager.provide(RegisteredRoutes, _routes_of(manager))
300
311
  findings, supported = _check_module(manager, tuple(loaded.plugin for loaded in registry))
@@ -385,7 +396,7 @@ def analyze_project(
385
396
  (a for plugin in all_plugins for a in plugin.advisories), file_advisories
386
397
  )
387
398
  affected = affected_symbols(dependencies, advisories)
388
- models = plugin_models(all_plugins).extended(*advisory_sinks(affected))
399
+ models = plugin_models(all_plugins, root).extended(*advisory_sinks(affected))
389
400
  for manager in managers.values():
390
401
  manager.provide(SecurityModelAnalysis, models)
391
402
  manager.provide(DependencyAnalysis, dependencies)
@@ -408,9 +419,11 @@ def analyze_project(
408
419
  for symbol, registered in _routes_of(analysable[name]).items():
409
420
  routes.setdefault(symbol, registered)
410
421
  escaped = escaped_templates(root)
422
+ clearing = models.clearing(escaped)
411
423
  for manager in analysable.values():
412
424
  manager.provide(RegisteredRoutes, routes)
413
425
  manager.provide(EscapedTemplates, escaped)
426
+ manager.provide(ClearingAnalysis, clearing)
414
427
 
415
428
  configuration = _configuration_key(registry, plugins, models, advisories, dependencies, routes, escaped)
416
429
  keys = module_keys(
@@ -620,13 +633,15 @@ def _analyse_batch(batch: _Batch) -> dict[str, dict[str, Any]]:
620
633
  (a for plugin in all_plugins for a in plugin.advisories), file_advisories
621
634
  )
622
635
  affected = affected_symbols(dependencies, advisories)
623
- models = plugin_models(all_plugins).extended(*advisory_sinks(affected))
636
+ models = plugin_models(all_plugins, batch.root).extended(*advisory_sinks(affected))
624
637
  routes = _decode_routes(batch.routes)
638
+ escaped = frozenset(batch.escaped)
625
639
  for manager in managers.values():
626
640
  manager.provide(SecurityModelAnalysis, models)
627
641
  manager.provide(DependencyAnalysis, dependencies)
628
642
  manager.provide(RegisteredRoutes, routes)
629
- manager.provide(EscapedTemplates, frozenset(batch.escaped))
643
+ manager.provide(EscapedTemplates, escaped)
644
+ manager.provide(ClearingAnalysis, models.clearing(escaped))
630
645
  module_plugins = tuple(p for p in all_plugins if not isinstance(p, ProjectPlugin))
631
646
  results = _analyse_managers(managers, decode_index(batch.seed), module_plugins, affected)
632
647
  return {name: encode(entry) for name, entry in results.items()}
@@ -29,7 +29,12 @@ from coretrace_python.abstract import ConstantPropagation, RangeAnalysis, RangeF
29
29
  from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
30
30
  from coretrace_python.cfg import CFG, BlockId, CFGAnalysis, DominanceAnalysis, DominatorTree
31
31
  from coretrace_python.hir import nodes
32
- from coretrace_python.interprocedural import CallGraphAnalysis
32
+ from coretrace_python.interprocedural import (
33
+ CallGraph,
34
+ CallGraphAnalysis,
35
+ KnownFunction,
36
+ project_symbol,
37
+ )
33
38
  from coretrace_python.ir.model import (
34
39
  BoolOp,
35
40
  Branch,
@@ -55,6 +60,7 @@ from coretrace_python.taint import (
55
60
  TaintAnalysis,
56
61
  TaintFacts,
57
62
  TaintFlow,
63
+ TaintKind,
58
64
  )
59
65
 
60
66
  VALIDATORS = frozenset(
@@ -214,15 +220,16 @@ class _Judge:
214
220
 
215
221
  def interpret(
216
222
  self, condition: Value, truth: bool | None
217
- ) -> tuple[dict[Value, str], set[Value]]:
218
- """Values the condition proves safe (with the reason), and values it mentions
219
- without proving anything. ``truth`` is ``None`` when it is not fixed at the sink.
223
+ ) -> tuple[dict[Value, tuple[str, TaintKind]], set[Value]]:
224
+ """Values the condition proves safe (with the reason and the kinds it proves them
225
+ safe for), and values it mentions without proving anything. ``truth`` is ``None``
226
+ when it is not fixed at the sink.
220
227
 
221
228
  A recognised check evaluated the wrong way (``isdigit()`` known false) yields
222
229
  nothing: it is neither a proof nor a reassuring guard."""
223
230
 
224
231
  definition = self.defs.get(condition)
225
- proven: dict[Value, str] = {}
232
+ proven: dict[Value, tuple[str, TaintKind]] = {}
226
233
  mentioned: set[Value] = set()
227
234
  if isinstance(definition, UnaryOp) and definition.operator == "not":
228
235
  return self.interpret(definition.operand, None if truth is None else not truth)
@@ -236,27 +243,29 @@ class _Judge:
236
243
  recognised = self.recognise(definition)
237
244
  if recognised is None:
238
245
  return proven, set(self.closure(condition)) | {condition}
239
- tested, reason, when_true = recognised
246
+ tested, reason, when_true, kinds = recognised
240
247
  if truth is None:
241
248
  mentioned = set(self.closure(condition)) | {condition}
242
249
  elif truth == when_true:
243
250
  # The check proves ``tested``; whatever else it reads is merely mentioned.
244
- proven[tested] = reason
251
+ proven[tested] = (reason, kinds)
245
252
  mentioned = set(self.closure(condition)) | {condition}
246
253
  return proven, mentioned
247
254
 
248
- def recognise(self, definition: Instruction | None) -> tuple[Value, str, bool] | None:
249
- """``(validated value, reason, truth that validates)`` for known check shapes."""
255
+ def recognise(self, definition: Instruction | None) -> tuple[Value, str, bool, TaintKind] | None:
256
+ """``(validated value, reason, truth that validates, kinds it proves)`` for known
257
+ check shapes. A string check, a constant allowlist or an equality with a constant
258
+ proves every kind; a ``Validator`` only the kinds it declares."""
250
259
 
251
260
  if isinstance(definition, Call) and not definition.arguments:
252
261
  callee = self.defs.get(definition.callee)
253
262
  if isinstance(callee, GetAttr) and callee.attribute in VALIDATORS:
254
- return callee.object, f"guarded by {callee.attribute}()", True
263
+ return callee.object, f"guarded by {callee.attribute}()", True, TaintKind.ALL
255
264
  if isinstance(definition, Call):
256
265
  symbol = self.symbols.get(definition.callee)
257
266
  validator = self.models.validator(symbol) if symbol is not None else None
258
267
  if validator is not None and validator.argument < len(definition.arguments):
259
- return definition.arguments[validator.argument], f"validated by {symbol}", True
268
+ return definition.arguments[validator.argument], f"validated by {symbol}", True, validator.kinds
260
269
  if isinstance(definition, Compare):
261
270
  left, right = definition.left, definition.right
262
271
  if definition.operator in ("in", "not_in") and self.is_constant_collection(right):
@@ -264,11 +273,12 @@ class _Judge:
264
273
  left,
265
274
  "allowlisted by a membership check on constants",
266
275
  definition.operator == "in",
276
+ TaintKind.ALL,
267
277
  )
268
278
  if definition.operator in ("eq", "not_eq"):
269
279
  for tested, other in ((left, right), (right, left)):
270
280
  if isinstance(self.defs.get(other), Constant):
271
- return tested, "equals a constant", definition.operator == "eq"
281
+ return tested, "equals a constant", definition.operator == "eq", TaintKind.ALL
272
282
  return None
273
283
 
274
284
  def examined(
@@ -387,8 +397,9 @@ class _Judge:
387
397
  )
388
398
  for guard in self.guards(sink):
389
399
  found, mentioned = self.interpret(guard.condition, guard.truth)
390
- for value, reason in found.items():
391
- if value in chain:
400
+ for value, (reason, kinds) in found.items():
401
+ # A check proving some kinds only leaves the others reaching the sink.
402
+ if value in chain and not flow.kinds & ~kinds:
392
403
  proven.setdefault(value, reason)
393
404
  examined = self.examined(guard.condition, mentioned, flow.argument, chain)
394
405
  for origin in origins:
@@ -478,6 +489,7 @@ class RefutationAnalysis(FunctionAnalysis[Verdicts]):
478
489
  constants = ctx.get(ConstantPropagation, function)
479
490
  graph = ctx.get(CallGraphAnalysis)
480
491
  models = ctx.get(SecurityModelAnalysis)
492
+ name = graph.name_of(function)
481
493
  return judge_flows(
482
494
  ssa,
483
495
  ctx.get(CFGAnalysis, function),
@@ -486,7 +498,21 @@ class RefutationAnalysis(FunctionAnalysis[Verdicts]):
486
498
  frozenset(b.id for b in ssa.blocks if constants.reachable(b.id)),
487
499
  ctx.get(RangeAnalysis, function),
488
500
  models,
489
- graph.symbols(graph.name_of(function)),
501
+ _with_project_callees(ssa, graph, name, ctx.module.name),
490
502
  authorization_of(function, models, ctx.get(ScopeAnalysis), ctx.get(SymbolAnalysis)),
491
503
  )
492
504
 
505
+
506
+ def _with_project_callees(function: FunctionIR, graph: CallGraph, name: str, module: str) -> dict[Value, SymbolId]:
507
+ """The symbols of ``name``'s values, with each call to a function of its module named
508
+ by its project symbol, so a model declared on it (``Validator``) applies there too."""
509
+
510
+ symbols = dict(graph.symbols(name))
511
+ for block in function.blocks:
512
+ for instruction in block.instructions:
513
+ if isinstance(instruction, Call):
514
+ target = graph.target_at(name, instruction.location)
515
+ if isinstance(target, KnownFunction):
516
+ symbols.setdefault(instruction.callee, project_symbol(module, target.name))
517
+ return symbols
518
+
@@ -20,6 +20,9 @@ from coretrace_python.interprocedural.modulegraph import (
20
20
  project_symbol,
21
21
  )
22
22
  from coretrace_python.interprocedural.summaries import (
23
+ Cleared,
24
+ Clearing,
25
+ ClearingAnalysis,
23
26
  ExternalCall,
24
27
  FunctionSummary,
25
28
  Mutation,
@@ -28,6 +31,7 @@ from coretrace_python.interprocedural.summaries import (
28
31
  SummaryAnalysis,
29
32
  SummaryIndex,
30
33
  SummaryTable,
34
+ cleared_by,
31
35
  )
32
36
 
33
37
  __all__ = [
@@ -35,6 +39,9 @@ __all__ = [
35
39
  "CallGraph",
36
40
  "CallGraphAnalysis",
37
41
  "CallSite",
42
+ "Cleared",
43
+ "Clearing",
44
+ "ClearingAnalysis",
38
45
  "ExternalCall",
39
46
  "ExternalSymbol",
40
47
  "FunctionSummary",
@@ -51,6 +58,7 @@ __all__ = [
51
58
  "Target",
52
59
  "UnknownTarget",
53
60
  "build_module_graph",
61
+ "cleared_by",
54
62
  "discover_files",
55
63
  "discover_sources",
56
64
  "project_symbol",
@@ -4,16 +4,19 @@ A summary says which parameters a function's return value depends on and which e
4
4
  symbols its parameters reach, directly or through calls to other known functions. It is
5
5
  computed as a dependence data-flow problem over the SSA form and iterated to a fixpoint
6
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.
7
+ security knowledge of their own: the taint engine decides which external symbols
8
+ matter. They only keep, per parameter, the bits a ``Clearing`` the engine provides says
9
+ the calls on the way cleared, on every path, so a caller's data reaches a sink or comes
10
+ back without the kinds a sanitizer inside the function removed.
8
11
  """
9
12
 
10
13
  from __future__ import annotations
11
14
 
12
15
  import dataclasses
13
- from collections.abc import Mapping
16
+ from collections.abc import Iterable, Mapping
14
17
  from dataclasses import dataclass
15
18
  from types import MappingProxyType
16
- from typing import ClassVar
19
+ from typing import ClassVar, TypeVar
17
20
 
18
21
  from coretrace_python.abstract import (
19
22
  ATTRIBUTES,
@@ -69,22 +72,90 @@ from coretrace_python.source import SourceSpan
69
72
 
70
73
  Dependencies = frozenset[int]
71
74
  NONE: Dependencies = frozenset()
75
+ _T = TypeVar("_T")
76
+ # Per parameter, the taint kind bits cleared on every path from it, sorted, none empty.
77
+ Cleared = tuple[tuple[int, int], ...]
72
78
 
73
79
 
74
80
  @dataclass(frozen=True)
75
81
  class Dep:
76
- """What a value depends on: parameter indices and results of external symbols."""
82
+ """What a value depends on: parameter indices, results of external symbols, and for
83
+ each parameter the bits cleared on every path from it to the value."""
77
84
 
78
85
  parameters: Dependencies = NONE
79
86
  externals: frozenset[SymbolId] = frozenset()
87
+ cleared: Cleared = ()
80
88
 
81
89
  def __or__(self, other: Dep) -> Dep:
82
- return Dep(self.parameters | other.parameters, self.externals | other.externals)
90
+ parameters = self.parameters | other.parameters
91
+ if not self.cleared and not other.cleared:
92
+ return Dep(parameters, self.externals | other.externals)
93
+ own, theirs = dict(self.cleared), dict(other.cleared)
94
+ # A parameter both sides depend on keeps what both paths cleared.
95
+ cleared = {
96
+ p: (own.get(p, 0) & theirs.get(p, 0))
97
+ if p in self.parameters and p in other.parameters
98
+ else own.get(p, 0) | theirs.get(p, 0)
99
+ for p in parameters
100
+ }
101
+ return Dep(parameters, self.externals | other.externals, _cleared(cleared))
102
+
103
+ def clearing(self, bits: int) -> Dep:
104
+ """This dependence after a call clearing ``bits`` from what it returns."""
105
+
106
+ if not bits or not self.parameters:
107
+ return self
108
+ own = dict(self.cleared)
109
+ return Dep(self.parameters, self.externals, _cleared({p: own.get(p, 0) | bits for p in self.parameters}))
110
+
111
+
112
+ def _cleared(bits: Mapping[int, int]) -> Cleared:
113
+ return tuple(sorted((p, b) for p, b in bits.items() if b))
114
+
115
+
116
+ def cleared_by(cleared: Cleared, parameter: int) -> int:
117
+ """The bits ``cleared`` records for ``parameter``, none when it has no entry."""
118
+
119
+ return next((bits for p, bits in cleared if p == parameter), 0)
83
120
 
84
121
 
85
122
  EMPTY = Dep()
86
123
 
87
124
 
125
+ @dataclass(frozen=True)
126
+ class Clearing:
127
+ """What an external call clears from the data it returns, as taint kind bits the
128
+ summaries carry without reading them. The engine derives it from the security models:
129
+ a sanitizer clears its kinds; a template render clears the bits of its entry when the
130
+ template it names is one the project shows escaping (``escaped`` holds the names the
131
+ way call sites record a constant)."""
132
+
133
+ sanitizers: Mapping[SymbolId, int] = dataclasses.field(default_factory=dict)
134
+ renders: Mapping[SymbolId, tuple[int, str, int]] = dataclasses.field(default_factory=dict)
135
+ escaped: frozenset[str] = frozenset()
136
+
137
+ def clears(self, symbol: SymbolId, arguments: Arguments) -> int:
138
+ bits = self.sanitizers.get(symbol, 0)
139
+ render = self.renders.get(symbol)
140
+ if render is not None:
141
+ position, keyword, rendered = render
142
+ named = arguments.given(keyword, position)
143
+ if named is not None and named[0] and named[1] in self.escaped:
144
+ bits |= rendered
145
+ return bits
146
+
147
+
148
+ class ClearingAnalysis(Analysis[Clearing]):
149
+ """What calls clear, provided by the engine from the security models; nothing on its
150
+ own, so a summary then keeps every kind."""
151
+
152
+ name: ClassVar[str] = "interprocedural.clearing"
153
+
154
+ @classmethod
155
+ def compute(cls, ctx: AnalysisContext) -> Clearing:
156
+ return Clearing()
157
+
158
+
88
159
  @dataclass(frozen=True)
89
160
  class ExternalCall:
90
161
  """An external symbol reached from this function; dependencies are parameter indices.
@@ -98,6 +169,19 @@ class ExternalCall:
98
169
  call_site: SourceSpan | None
99
170
  # What the arguments of the call at ``location`` denote, where it is made.
100
171
  arguments: Arguments = dataclasses.field(default_factory=Arguments)
172
+ # What was cleared on the way to each argument and keyword, in the same order.
173
+ argument_cleared: tuple[Cleared, ...] = ()
174
+ keyword_cleared: tuple[Cleared, ...] = ()
175
+
176
+ def positional(self) -> Iterable[tuple[Dependencies, Cleared]]:
177
+ """Each positional argument's dependencies, with what was cleared on their way."""
178
+
179
+ return _aligned(self.argument_dependencies, self.argument_cleared)
180
+
181
+ def named(self) -> Iterable[tuple[str | None, Dependencies, Cleared]]:
182
+ """Each keyword's name and dependencies, with what was cleared on their way."""
183
+
184
+ return ((name, deps, c) for (name, deps), c in _aligned(self.keyword_dependencies, self.keyword_cleared))
101
185
 
102
186
 
103
187
  @dataclass(frozen=True)
@@ -134,6 +218,8 @@ class FunctionSummary:
134
218
  nonlocal_writes: tuple[NonlocalWrite, ...] = ()
135
219
  # A ``@staticmethod``: ``Class.method(...)`` passes no receiver.
136
220
  static: bool = False
221
+ # What was cleared on the way from each parameter to the return value.
222
+ return_cleared: Cleared = ()
137
223
 
138
224
 
139
225
  class SummaryIndex:
@@ -191,6 +277,7 @@ class _DependenceProblem(DataflowProblem[State]):
191
277
  table: Mapping[str, FunctionSummary],
192
278
  project: Mapping[SymbolId, FunctionSummary] | None = None,
193
279
  heap: HeapFacts | None = None,
280
+ clearing: Clearing | None = None,
194
281
  ) -> None:
195
282
  self.name = name
196
283
  self.function = function
@@ -198,6 +285,7 @@ class _DependenceProblem(DataflowProblem[State]):
198
285
  self.table = table
199
286
  self.project = project or {}
200
287
  self.heap = heap or HeapFacts({})
288
+ self.clearing = clearing or Clearing()
201
289
  self.blocks = {block.id: block for block in function.blocks}
202
290
  self.defs: dict[Value, Instruction] = {
203
291
  i.result: i for block in function.blocks for i in block.instructions if i.result
@@ -338,15 +426,10 @@ class _DependenceProblem(DataflowProblem[State]):
338
426
  bound = () if project.static else self.receiver(call, project.name)
339
427
  arguments = (*(self.deep(r, state) for r in bound), *arguments)
340
428
  return self.known(project, arguments, keywords, everything, call, state, bound)
341
- self.record(
342
- target.symbol,
343
- tuple(a.parameters for a in arguments),
344
- tuple((name, deps.parameters) for name, deps in named),
345
- call.location,
346
- None,
347
- self.graph.arguments_at(self.name, call.location),
348
- )
349
- return everything | Dep(externals=frozenset({target.symbol}))
429
+ given = self.graph.arguments_at(self.name, call.location)
430
+ self.record(target.symbol, arguments, named, call.location, None, given)
431
+ cleared = everything.clearing(self.clearing.clears(target.symbol, given))
432
+ return cleared | Dep(externals=frozenset({target.symbol}))
350
433
  if isinstance(target, KnownFunction):
351
434
  made = self.defs.get(call.callee)
352
435
  if isinstance(made, MakeFunction):
@@ -414,17 +497,18 @@ class _DependenceProblem(DataflowProblem[State]):
414
497
  if callee.unsupported:
415
498
  return everything
416
499
 
417
- def mapped(indices: Dependencies) -> Dep:
500
+ def mapped(indices: Dependencies, cleared: Cleared = ()) -> Dep:
418
501
  result = EMPTY
419
502
  for index in indices:
420
- result |= arguments[index] if index < len(arguments) else keywords
503
+ part = arguments[index] if index < len(arguments) else keywords
504
+ result |= part.clearing(cleared_by(cleared, index))
421
505
  return result
422
506
 
423
507
  for reached in callee.external_calls:
424
508
  self.record(
425
509
  reached.symbol,
426
- tuple(mapped(d).parameters for d in reached.argument_dependencies),
427
- tuple((name, mapped(d).parameters) for name, d in reached.keyword_dependencies),
510
+ tuple(mapped(d, c) for d, c in reached.positional()),
511
+ tuple((name, mapped(d, c)) for name, d, c in reached.named()),
428
512
  reached.location,
429
513
  call.location,
430
514
  reached.arguments,
@@ -435,13 +519,13 @@ class _DependenceProblem(DataflowProblem[State]):
435
519
  if mutation.parameter < len(values):
436
520
  deps = mapped(mutation.dependencies) | Dep(externals=mutation.externals)
437
521
  self.store(state, values[mutation.parameter], mutation.field, deps)
438
- return mapped(callee.return_dependencies) | Dep(externals=callee.return_externals)
522
+ return mapped(callee.return_dependencies, callee.return_cleared) | Dep(externals=callee.return_externals)
439
523
 
440
524
  def record(
441
525
  self,
442
526
  symbol: SymbolId,
443
- arguments: tuple[Dependencies, ...],
444
- keywords: tuple[tuple[str | None, Dependencies], ...],
527
+ arguments: tuple[Dep, ...],
528
+ keywords: tuple[tuple[str | None, Dep], ...],
445
529
  location: SourceSpan,
446
530
  call_site: SourceSpan | None,
447
531
  given: Arguments,
@@ -450,14 +534,30 @@ class _DependenceProblem(DataflowProblem[State]):
450
534
  previous = self.external.get(key)
451
535
  if previous is not None:
452
536
  arguments = tuple(
453
- a | b for a, b in zip(previous.argument_dependencies, arguments, strict=False)
537
+ a | Dep(b, cleared=c) for a, (b, c) in zip(arguments, previous.positional(), strict=False)
454
538
  )
455
539
  # The same call records the same keywords, in the same order.
456
540
  keywords = tuple(
457
- (name, deps | old)
458
- for (name, deps), (_, old) in zip(keywords, previous.keyword_dependencies, strict=False)
541
+ (name, deps | Dep(old, cleared=c))
542
+ for (name, deps), (_, old, c) in zip(keywords, previous.named(), strict=False)
459
543
  )
460
- self.external[key] = ExternalCall(symbol, arguments, keywords, location, call_site, given)
544
+ self.external[key] = ExternalCall(
545
+ symbol,
546
+ tuple(a.parameters for a in arguments),
547
+ tuple((name, deps.parameters) for name, deps in keywords),
548
+ location,
549
+ call_site,
550
+ given,
551
+ tuple(a.cleared for a in arguments),
552
+ tuple(deps.cleared for _, deps in keywords),
553
+ )
554
+
555
+
556
+ def _aligned(dependencies: tuple[_T, ...], cleared: tuple[Cleared, ...]) -> Iterable[tuple[_T, Cleared]]:
557
+ """Each dependency with what was cleared on its way; nothing when not recorded."""
558
+
559
+ missing = len(dependencies) - len(cleared)
560
+ return zip(dependencies, (*cleared, *(() for _ in range(missing))), strict=True)
461
561
 
462
562
 
463
563
  def summarize(
@@ -469,8 +569,9 @@ def summarize(
469
569
  project: Mapping[SymbolId, FunctionSummary] | None = None,
470
570
  heap: HeapFacts | None = None,
471
571
  static: bool = False,
572
+ clearing: Clearing | None = None,
472
573
  ) -> FunctionSummary:
473
- problem = _DependenceProblem(name, function, graph, table, project, heap)
574
+ problem = _DependenceProblem(name, function, graph, table, project, heap, clearing)
474
575
  solution = solve(problem, cfg)
475
576
  problem.external = {}
476
577
  problem.returns = EMPTY
@@ -502,19 +603,21 @@ def summarize(
502
603
  if deps.parameters or deps.externals
503
604
  ),
504
605
  static=static,
606
+ return_cleared=problem.returns.cleared,
505
607
  )
506
608
 
507
609
 
508
610
  class SummaryAnalysis(Analysis[SummaryTable]):
509
611
  name: ClassVar[str] = "interprocedural.summaries"
510
612
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset(
511
- {CallGraphAnalysis, SSAAnalysis, CFGAnalysis, ProjectSummaries, HeapAnalysis}
613
+ {CallGraphAnalysis, SSAAnalysis, CFGAnalysis, ProjectSummaries, HeapAnalysis, ClearingAnalysis}
512
614
  )
513
615
 
514
616
  @classmethod
515
617
  def compute(cls, ctx: AnalysisContext) -> SummaryTable:
516
618
  graph = ctx.get(CallGraphAnalysis)
517
619
  project = ctx.get(ProjectSummaries).summaries
620
+ clearing = ctx.get(ClearingAnalysis)
518
621
  table: dict[str, FunctionSummary] = {}
519
622
  supported: dict[str, tuple[FunctionIR, CFG, HeapFacts]] = {}
520
623
  for name, function in graph.definitions.items():
@@ -533,7 +636,7 @@ class SummaryAnalysis(Analysis[SummaryTable]):
533
636
  changed = False
534
637
  for name, (ssa, cfg, heap) in supported.items():
535
638
  updated = summarize(
536
- name, ssa, cfg, graph, table, project, heap, _is_staticmethod(graph.definitions[name])
639
+ name, ssa, cfg, graph, table, project, heap, _is_staticmethod(graph.definitions[name]), clearing
537
640
  )
538
641
  if updated != table[name]:
539
642
  table[name] = updated
@@ -51,6 +51,14 @@ class Plugin(ABC):
51
51
  def analyze(self, ctx: PluginContext) -> Sequence[Finding]:
52
52
  raise NotImplementedError
53
53
 
54
+ def project_models(self, root: Path) -> Sequence[Model]:
55
+ """Models read from the project under ``root``, such as validators it declares
56
+ in a file there. Called once per directory check, in every process analysing
57
+ it, before any module; a single-file check reads no project. A model declared
58
+ wrongly raises ``ModelError``, which stops the check with its message."""
59
+
60
+ return ()
61
+
54
62
 
55
63
  class ModelPlugin(Plugin):
56
64
  """A plugin that only contributes security models or advisories (§15 providers)."""
@@ -30,6 +30,8 @@ from coretrace_python.interprocedural import (
30
30
  Arguments,
31
31
  CallGraph,
32
32
  CallGraphAnalysis,
33
+ Cleared,
34
+ Clearing,
33
35
  ExternalSymbol,
34
36
  FunctionSummary,
35
37
  KnownFunction,
@@ -38,6 +40,7 @@ from coretrace_python.interprocedural import (
38
40
  SummaryAnalysis,
39
41
  SummaryIndex,
40
42
  SummaryTable,
43
+ cleared_by,
41
44
  project_symbol,
42
45
  )
43
46
  from coretrace_python.ir.lowering import analyzable_functions, is_body_function
@@ -153,12 +156,11 @@ class _TaintProblem(DataflowProblem[State]):
153
156
  heap: HeapFacts | None = None,
154
157
  seeds: Mapping[HeapLocation, Taint] | None = None,
155
158
  module: str = "",
156
- escaped: frozenset[str] = frozenset(),
159
+ clearing: Clearing | None = None,
157
160
  ) -> None:
158
161
  self.name = name
159
162
  self.module = module
160
- # As call sites record a constant argument: the way Python writes it.
161
- self.escaped = frozenset(repr(name) for name in escaped)
163
+ self.clearing = clearing or Clearing()
162
164
  self.function = function
163
165
  self.models = models
164
166
  self.graph = graph
@@ -411,11 +413,9 @@ class _TaintProblem(DataflowProblem[State]):
411
413
  self.report(
412
414
  flows, sink, self.rendered(argument, state), argument, call, None, None, None, given, keyword=name
413
415
  )
414
- sanitizer = self.models.sanitizer(symbol)
415
- if sanitizer is not None:
416
- return everything.without(sanitizer.kinds)
417
- if self.renders_escaped(symbol, given):
418
- return everything.without(TaintKind.HTML)
416
+ cleared = self.clearing.clears(symbol, given)
417
+ if cleared:
418
+ return everything.without(TaintKind(cleared))
419
419
  # A method on a tainted object returns tainted data (``request.args.get``).
420
420
  everything = everything.join(state.get(call.callee, Taint.none()))
421
421
  source = self.models.source_covering(symbol)
@@ -423,14 +423,6 @@ class _TaintProblem(DataflowProblem[State]):
423
423
  return everything.join(Taint(source.kinds, frozenset({source})))
424
424
  return everything
425
425
 
426
- def renders_escaped(self, symbol: SymbolId, given: Arguments) -> bool:
427
- """Whether the call renders, by a name written in the call, a template the
428
- project shows escaping everything it renders."""
429
-
430
- render = self.models.template_render(symbol)
431
- named = given.given(render.keyword, render.position) if render is not None else None
432
- return named is not None and named[0] and named[1] in self.escaped
433
-
434
426
  def guarded(self, sink: Sink | None, arguments: Arguments) -> Sink | None:
435
427
  """The sink as this call makes it: an argument declared safe (``yaml.load`` with
436
428
  ``Loader=SafeLoader``) takes off the kinds it covers, and no kind left, no sink."""
@@ -501,11 +493,11 @@ class _TaintProblem(DataflowProblem[State]):
501
493
  spread = (*call.starred, *(value for _, value in call.keywords))
502
494
  values = (*receiver, *call.arguments, *captured)
503
495
 
504
- def mapped(deps: frozenset[int]) -> tuple[Taint, Value | None]:
496
+ def mapped(deps: frozenset[int], cleared: Cleared = ()) -> tuple[Taint, Value | None]:
505
497
  taint, witness = Taint.none(), None
506
498
  for index in sorted(deps):
507
499
  positional = index < len(arguments)
508
- part = arguments[index] if positional else keywords
500
+ part = (arguments[index] if positional else keywords).without(TaintKind(cleared_by(cleared, index)))
509
501
  if part and witness is None and (positional or spread):
510
502
  witness = values[index] if positional else spread[0]
511
503
  taint = taint.join(part)
@@ -515,12 +507,12 @@ class _TaintProblem(DataflowProblem[State]):
515
507
  sink = self.guarded(self.models.sink(reached.symbol), reached.arguments)
516
508
  if sink is None:
517
509
  continue
518
- for position, deps in enumerate(reached.argument_dependencies):
519
- taint, witness = mapped(deps)
510
+ for position, (deps, cleared) in enumerate(reached.positional()):
511
+ taint, witness = mapped(deps, cleared)
520
512
  if witness is not None:
521
513
  self.report(flows, sink, taint, witness, call, through, reached.location, position, reached.arguments)
522
- for name, deps in reached.keyword_dependencies:
523
- taint, witness = mapped(deps)
514
+ for name, deps, cleared in reached.named():
515
+ taint, witness = mapped(deps, cleared)
524
516
  if witness is not None:
525
517
  self.report(
526
518
  flows, sink, taint, witness, call, through, reached.location, None, reached.arguments, keyword=name
@@ -533,7 +525,7 @@ class _TaintProblem(DataflowProblem[State]):
533
525
  if stored_source is not None:
534
526
  stored = stored.join(Taint(stored_source.kinds, frozenset({stored_source})))
535
527
  self.store(state, values[mutation.parameter], mutation.field, stored)
536
- result = mapped(summary.return_dependencies)[0]
528
+ result = mapped(summary.return_dependencies, summary.return_cleared)[0]
537
529
  for symbol in sorted(summary.return_externals, key=str):
538
530
  returned_source = self.models.source(symbol)
539
531
  if returned_source is not None:
@@ -816,9 +808,9 @@ def propagate_taint(
816
808
  heap: HeapFacts | None = None,
817
809
  seeds: Mapping[HeapLocation, Taint] | None = None,
818
810
  module: str = "",
819
- escaped: frozenset[str] = frozenset(),
811
+ clearing: Clearing | None = None,
820
812
  ) -> TaintFacts:
821
- problem = _TaintProblem(name, function, models, graph, summaries, parameters, project, heap, seeds, module, escaped)
813
+ problem = _TaintProblem(name, function, models, graph, summaries, parameters, project, heap, seeds, module, clearing)
822
814
  solution = solve(problem, cfg)
823
815
  taints: dict[Key, Taint] = {}
824
816
  flows: list[TaintFlow] = []
@@ -883,7 +875,7 @@ class TaintAnalysis(FunctionAnalysis[TaintFacts]):
883
875
  heap,
884
876
  seeds,
885
877
  ctx.module.name,
886
- ctx.get(EscapedTemplates),
878
+ models.clearing(ctx.get(EscapedTemplates)),
887
879
  )
888
880
 
889
881
 
@@ -16,6 +16,7 @@ from types import MappingProxyType
16
16
  from typing import ClassVar
17
17
 
18
18
  from coretrace_python.analysis import Analysis, AnalysisContext, MissingInputError
19
+ from coretrace_python.interprocedural import Clearing
19
20
  from coretrace_python.semantic.symbols import SymbolId
20
21
 
21
22
 
@@ -334,6 +335,17 @@ class ModelTable:
334
335
  found = self._by_symbol[TemplateRender].get(symbol)
335
336
  return found if isinstance(found, TemplateRender) else None
336
337
 
338
+ def clearing(self, escaped: frozenset[str] = frozenset()) -> Clearing:
339
+ """What calls clear from the data they return: every sanitizer its kinds, every
340
+ template render ``HTML`` when it names one of the ``escaped`` templates."""
341
+
342
+ return Clearing(
343
+ MappingProxyType({s.symbol: s.kinds.value for s in self.sanitizers}),
344
+ MappingProxyType({r.symbol: (r.position, r.keyword, TaintKind.HTML.value) for r in self.template_renders}),
345
+ # As call sites record a constant argument: the way Python writes it.
346
+ frozenset(repr(name) for name in escaped),
347
+ )
348
+
337
349
 
338
350
  class SecurityModelRegistry:
339
351
  """Mutable collection point for the models plugins register."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coretrace-python-analyzer
3
- Version: 0.7.0
3
+ Version: 0.8.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=kEx6lE06VX-mfQ6o7DrayGZNlR2uTGSdGItAWsSIeHI,75
1
+ coretrace_python/__init__.py,sha256=u9j9n2ZvaBeVUS33TRJ3MIoLUWwaBdpCAainRMXI4ok,75
2
2
  coretrace_python/__main__.py,sha256=V8DnYtRboISK2VCKm7DGHLREKvqLbqApyWlKBOs26wU,65
3
- coretrace_python/cache.py,sha256=tt-l1jOibCx0lNKGd7XF_xTlcCeBLewPWOPpS-e3Ynk,12929
4
- coretrace_python/cli.py,sha256=SwXYUR8DIgppkqom6kKNM701pb4p2omEy-hRXIM5yZE,10946
5
- coretrace_python/engine.py,sha256=3NxNKzkNTWxa7a4NVkZ0fyh5KitNpDHuTsCrdBa4VxU,28696
3
+ coretrace_python/cache.py,sha256=-pZv-ELm4jMCTXQWSZIobHaT9UHIjFu7IlMkOM0i4LI,13607
4
+ coretrace_python/cli.py,sha256=86rTb3Kcezwk9DBTRg-G3G38ypx8zH4lZbCR467xvpE,11008
5
+ coretrace_python/engine.py,sha256=VWTUTgm9k0DrPYhYrgJQHzXi_L00pxnH8fGfo8insMc,29340
6
6
  coretrace_python/abstract/__init__.py,sha256=n5FNkV8Z4LmwG0r06KWdLj4N2CPSgAKwY3896tdmRgM,999
7
7
  coretrace_python/abstract/constants.py,sha256=z4-e7IzysxzGi58wFFff1MlEoV8l7AZsB9EL-q6b7vY,9071
8
8
  coretrace_python/abstract/heap.py,sha256=OaESXFGC0qzyhAr76xrCu4skeDVzZM4wlenQqfMTkdk,9642
@@ -99,7 +99,7 @@ coretrace_python/findings/__init__.py,sha256=gnJI-uB2CkgZ8I5aFYP_N5yZb7FGNIy7Lir
99
99
  coretrace_python/findings/baseline.py,sha256=fbGQuULRsCjhxSNdwuLfwsvql9ZUfF3jqCXrRmIgOoQ,3468
100
100
  coretrace_python/findings/coverage.py,sha256=U8URLEz6EN4B_TTQlLFCOtEmUbINt7a0Bexb-6OUFlI,1203
101
101
  coretrace_python/findings/model.py,sha256=zRK1FDuLgiVY4ch6H_AN9rDxC1orNW4wc8YN3cfVFfc,1424
102
- coretrace_python/findings/refutation.py,sha256=MmFANBozz0sodIhGUhrgr7BCEwhw5s80kRPRMDRDIyU,20707
102
+ coretrace_python/findings/refutation.py,sha256=UeYCz70LwyL8AMimhs3ys9cH0j1_YbdHVu7y3VvmmUg,22026
103
103
  coretrace_python/findings/suppressions.py,sha256=kN4q0CA1tzeAubeYe4rUv_aRXda4PAsMZY0r8M5cuQI,2633
104
104
  coretrace_python/frontend/__init__.py,sha256=ULI2tf_IRTMboaKM6uzTkGGh4Agy3cFjtw2H1IPrz4g,627
105
105
  coretrace_python/frontend/ast_adapter.py,sha256=VL2CgbQqDGSr2Wh-z0EpR0gBB7T29s0xHvfT-BPx08c,21833
@@ -107,10 +107,10 @@ coretrace_python/frontend/parser.py,sha256=6-CG7rWxUYeOGeDP206SJWy4BMQUP9Mr6Jf-t
107
107
  coretrace_python/hir/__init__.py,sha256=xMpvj-RSp6bd78a_hhkqbe99-snjERkH-_Lv5o9ASbw,180
108
108
  coretrace_python/hir/nodes.py,sha256=4BI0f3UhKbnz9ZIxMoKXM7ujav0BmRpD0OXaoblRxIk,12364
109
109
  coretrace_python/hir/visitors.py,sha256=JFnX09hK6trLyRcWgZ5ehOG2ywX3VvkqQ1h82nahYNg,885
110
- coretrace_python/interprocedural/__init__.py,sha256=SXtIGE3DD4fADIXLDuEvyT_3oIsh7Vio3BL4U6oUm28,1162
110
+ coretrace_python/interprocedural/__init__.py,sha256=rBASF3aAvl5CFAmIISu2gID9mqY_iyi-4Hx0GzDVZsg,1300
111
111
  coretrace_python/interprocedural/callgraph.py,sha256=E6enTawqcl4-TObdjvoFe6l1zEedvsWbnjEBD6omzy8,19827
112
112
  coretrace_python/interprocedural/modulegraph.py,sha256=4sB7OKjBYfxtm7aMtHm2jRlqFVUNUsdnLhnv9UZ1BWA,8846
113
- coretrace_python/interprocedural/summaries.py,sha256=9-WGS-X37KJY1ynoXXA5HNfYFLeCyiKVMAIjeWgaVLI,21333
113
+ coretrace_python/interprocedural/summaries.py,sha256=Z_KT3ggH7fgPz1mJL6TBz6V5STka1koIuUBAuYMti7o,26113
114
114
  coretrace_python/ir/__init__.py,sha256=ShBPYYqoQFePP-mgBjGezQ2acel0-ReOHF9vCC-G_4Q,232
115
115
  coretrace_python/ir/defuse.py,sha256=CZEPnlgntRp4MqwR7kLldd2ZlIlBnr-V9VJAxXxwm0c,2616
116
116
  coretrace_python/ir/lowering.py,sha256=V7r_4tEOi801NBOsPdG6pwrRHZIeVTahygYENneLQ4U,31428
@@ -118,7 +118,7 @@ coretrace_python/ir/model.py,sha256=i7LvHEnl5pmWYUcSui17ZGS1rdPVavMpnjt7Tog7P4c,
118
118
  coretrace_python/ir/printer.py,sha256=otBIhZUvB_UfB68gid_FbPcSjBUSmxH3v7vBMZZQVkg,9125
119
119
  coretrace_python/ir/ssa.py,sha256=cIYbcv90PI6AOeMrw2ZWp88fy7E2ZO4P3tqjtPHra7g,11116
120
120
  coretrace_python/plugins/__init__.py,sha256=xRKqkvkvQMmfQACHHoBioYhIyyK4HoMAVogy6EZG8tg,1398
121
- coretrace_python/plugins/api.py,sha256=NjeDRRwy2K6esSEkg1Ofmv09mqa0El6MlTmRDCWiOmQ,8182
121
+ coretrace_python/plugins/api.py,sha256=FcIe2c4sFB-PAPtYcJkidvdZJkr_xFKT8SvlBP5l38o,8603
122
122
  coretrace_python/plugins/detectors.py,sha256=ej5Z_MS0CBqrR4b9xycjTVUIo5arpArpdbr7nL732iI,5700
123
123
  coretrace_python/plugins/loader.py,sha256=Use-lHTL4-IJYQBZmODC89zhJbYKGvDSLZHWjZa5NaI,3885
124
124
  coretrace_python/plugins/manifest.py,sha256=F24pgQSwApRUL6ZihRSLv9wL_M03A_pZjrj9Zi4Ku3Q,3438
@@ -138,13 +138,13 @@ coretrace_python/source/__init__.py,sha256=0Y8fvtbkETnNCJR9Zq7RS5TSgF9c22pz3SSk1
138
138
  coretrace_python/source/manager.py,sha256=JRmMOcWfS1nocbVrdcvfQTqEfIkmnb5JP857TpGywN4,2736
139
139
  coretrace_python/source/model.py,sha256=FZnN5mlir3tzbIpWoxnn6YtRi9UcJ_y0sZM60tKr8U4,1727
140
140
  coretrace_python/taint/__init__.py,sha256=kfqCEXE5wKFLwz2XrT42_yEv9pW4vPfgg5yYBVVSr2I,1371
141
- coretrace_python/taint/engine.py,sha256=uB0Mga1xeKF6LVHx601pvojb0NLAPG0DqF8fy3dM8oU,40222
142
- coretrace_python/taint/models.py,sha256=i3_Sfwdqf_BGXV47jylfF3vuX6nYJcyAPiwC3xerh6g,14592
141
+ coretrace_python/taint/engine.py,sha256=lB6ysdzYNuYhIiRUaxfI4F3cAnaUK55d4lF9AzPWfrU,39742
142
+ coretrace_python/taint/models.py,sha256=B84caPxmQvTdB9H_80nZOqcFlpNd9ofpKI2YL5Uc0UA,15269
143
143
  coretrace_python/taint/routes.py,sha256=UAJKXcw_sM9shf58FfDroxVO9bNSm4qrQdZknwUB0lo,6110
144
144
  coretrace_python/taint/templates.py,sha256=8Ojb43VXuWxOVifeXRpi3_2mjZfnvIC7GQ-AW80GUYk,11148
145
- coretrace_python_analyzer-0.7.0.dist-info/METADATA,sha256=lak3Dd-Dpbv_TEo-PAhnBg5r6iwrJmsDM5aa6wogg1w,3216
146
- coretrace_python_analyzer-0.7.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
147
- coretrace_python_analyzer-0.7.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
148
- coretrace_python_analyzer-0.7.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
149
- coretrace_python_analyzer-0.7.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
150
- coretrace_python_analyzer-0.7.0.dist-info/RECORD,,
145
+ coretrace_python_analyzer-0.8.0.dist-info/METADATA,sha256=6iUDBdRu9mMtAf4EaeD4sVOr4iEdxzT7tkfOBlaNNT8,3216
146
+ coretrace_python_analyzer-0.8.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
147
+ coretrace_python_analyzer-0.8.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
148
+ coretrace_python_analyzer-0.8.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
149
+ coretrace_python_analyzer-0.8.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
150
+ coretrace_python_analyzer-0.8.0.dist-info/RECORD,,