coretrace-python-analyzer 0.6.0__py3-none-any.whl → 0.7.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.6.0"
3
+ __version__ = "0.7.0"
4
4
 
@@ -23,6 +23,7 @@ from coretrace_python.taint import (
23
23
  Source,
24
24
  SuffixSink,
25
25
  TaintKind,
26
+ TemplateRender,
26
27
  TypedParameter,
27
28
  )
28
29
 
@@ -119,7 +120,15 @@ class DjangoModels(ModelPlugin):
119
120
  Sink(_sym("django.http.HttpResponseRedirect"), TaintKind.REDIRECT, _TARGET_ONLY),
120
121
  Sink(_sym("django.http.HttpResponsePermanentRedirect"), TaintKind.REDIRECT, _TARGET_ONLY),
121
122
  Sanitizer(_sym("django.utils.html.escape"), TaintKind.HTML),
123
+ TemplateRender(_sym("django.template.loader.render_to_string")),
122
124
  Sanitizer(_sym("django.utils.html.conditional_escape"), TaintKind.HTML),
125
+ # The masked CSRF secret: ASCII letters and digits only, a malformed cookie is
126
+ # replaced before it is used.
127
+ Sanitizer(_sym("django.middleware.csrf.get_token"), TaintKind.ALL),
128
+ # A path of the project's URL configuration, never scheme-relative: attacker data
129
+ # in its arguments or query reaches no other host through a redirect.
130
+ *(Sanitizer(_sym(f"django.urls.{name}"), TaintKind.REDIRECT) for name in ("reverse", "reverse_lazy")),
131
+ *(Sanitizer(_sym(f"django.urls.base.{name}"), TaintKind.REDIRECT) for name in ("reverse", "reverse_lazy")),
123
132
  *(AuthorizationGuard(_sym(decorator), label) for decorator, label in _AUTHORIZATION_DECORATORS),
124
133
  # ``urlpatterns = [path('login/', views.log_in)]``: the referenced view is an
125
134
  # entry point wherever it is defined; routers register viewsets.
@@ -1,5 +1,8 @@
1
- """Requests and httpx models: every request function is a SSRF sink, and what it returns
2
- is an ``http-response`` source, so data fetched from a remote server is untrusted."""
1
+ """Requests and httpx models: every request function is a SSRF sink for the argument that
2
+ chooses the destination, and what it returns is an ``http-response`` source, so data
3
+ fetched from a remote server is untrusted. The destination is the URL — first, or second
4
+ after the method, or ``url=`` — or the prepared request a ``send`` takes; the body, the
5
+ headers and the query parameters do not choose the host."""
3
6
 
4
7
  from __future__ import annotations
5
8
 
@@ -20,15 +23,18 @@ _CALLERS = (
20
23
  "httpx.AsyncClient",
21
24
  )
22
25
 
23
- _REQUEST_FUNCTIONS = (
24
- *(f"{caller}.{method}" for caller in _CALLERS for method in _METHODS),
25
- "requests.Session.send",
26
- "httpx.stream",
27
- "httpx.Client.stream",
28
- "httpx.AsyncClient.stream",
29
- "httpx.Client.send",
30
- "httpx.AsyncClient.send",
31
- )
26
+ # Where each function takes its destination, by position (the receiver of a method
27
+ # excluded) and by keyword, as its signature declares it.
28
+ _DESTINATIONS = {
29
+ **{f"{caller}.{method}": (0, "url") for caller in _CALLERS for method in _METHODS if method != "request"},
30
+ **{f"{caller}.request": (1, "url") for caller in _CALLERS},
31
+ "httpx.stream": (1, "url"),
32
+ "httpx.Client.stream": (1, "url"),
33
+ "httpx.AsyncClient.stream": (1, "url"),
34
+ "requests.Session.send": (0, "request"),
35
+ "httpx.Client.send": (0, "request"),
36
+ "httpx.AsyncClient.send": (0, "request"),
37
+ }
32
38
 
33
39
 
34
40
  def _sym(path: str) -> SymbolId:
@@ -38,6 +44,9 @@ def _sym(path: str) -> SymbolId:
38
44
  class HttpClientModels(ModelPlugin):
39
45
  name: ClassVar[str] = "http-client-models"
40
46
  models: ClassVar[tuple[Model, ...]] = (
41
- *(Sink(_sym(function), TaintKind.SSRF) for function in _REQUEST_FUNCTIONS),
42
- *(Source(_sym(function), "http-response") for function in _REQUEST_FUNCTIONS),
47
+ *(
48
+ Sink(_sym(function), TaintKind.SSRF, ((TaintKind.SSRF, (position,)),), ((TaintKind.SSRF, (keyword,)),))
49
+ for function, (position, keyword) in _DESTINATIONS.items()
50
+ ),
51
+ *(Source(_sym(function), "http-response") for function in _DESTINATIONS),
43
52
  )
coretrace_python/cache.py CHANGED
@@ -41,7 +41,7 @@ from coretrace_python.interprocedural import (
41
41
  from coretrace_python.semantic.symbols import SymbolId
42
42
  from coretrace_python.source import SourceId, SourceSpan
43
43
 
44
- CACHE_FORMAT = 8
44
+ CACHE_FORMAT = 9
45
45
 
46
46
 
47
47
  @dataclass(frozen=True)
@@ -233,7 +233,7 @@ def _encode_call(call: ExternalCall) -> dict[str, Any]:
233
233
  return {
234
234
  "symbol": str(call.symbol),
235
235
  "arguments": [sorted(deps) for deps in call.argument_dependencies],
236
- "keywords": sorted(call.keyword_dependencies),
236
+ "keywords": [[name, sorted(deps)] for name, deps in call.keyword_dependencies],
237
237
  "location": _encode_span(call.location),
238
238
  "call_site": None if call.call_site is None else _encode_span(call.call_site),
239
239
  "given": _encode_arguments(call.arguments),
@@ -245,7 +245,7 @@ def _decode_call(data: Mapping[str, Any]) -> ExternalCall:
245
245
  return ExternalCall(
246
246
  SymbolId(_string(data["symbol"])),
247
247
  tuple(_indices(deps) for deps in data["arguments"]),
248
- _indices(data["keywords"]),
248
+ tuple((None if name is None else _string(name), _indices(deps)) for name, deps in data["keywords"]),
249
249
  _decode_span(data["location"]),
250
250
  None if site is None else _decode_span(site),
251
251
  _decode_arguments(data["given"]),
@@ -109,6 +109,7 @@ from coretrace_python.source import SourceFile, SourceId, SourceManager, SourceS
109
109
  from coretrace_python.taint import (
110
110
  EntryPoint,
111
111
  EntryPointAnalysis,
112
+ EscapedTemplates,
112
113
  ModelTable,
113
114
  RegisteredRoutes,
114
115
  Routes,
@@ -116,6 +117,7 @@ from coretrace_python.taint import (
116
117
  SecurityModelRegistry,
117
118
  TaintAnalysis,
118
119
  TaintKind,
120
+ escaped_templates,
119
121
  registered_routes,
120
122
  )
121
123
 
@@ -146,6 +148,7 @@ ALL_ANALYSES: tuple[AnyAnalysis, ...] = (
146
148
  RefutationAnalysis,
147
149
  DependencyAnalysis,
148
150
  RegisteredRoutes,
151
+ EscapedTemplates,
149
152
  )
150
153
 
151
154
 
@@ -178,7 +181,14 @@ class ResultsEvicted(TransformationPass):
178
181
 
179
182
  name: ClassVar[str] = "project.results-evicted"
180
183
  preserves: ClassVar[frozenset[AnyAnalysis]] = frozenset(
181
- {*SEMANTIC_ANALYSES, SecurityModelAnalysis, ProjectSummaries, DependencyAnalysis, RegisteredRoutes}
184
+ {
185
+ *SEMANTIC_ANALYSES,
186
+ SecurityModelAnalysis,
187
+ ProjectSummaries,
188
+ DependencyAnalysis,
189
+ RegisteredRoutes,
190
+ EscapedTemplates,
191
+ }
182
192
  )
183
193
 
184
194
  @classmethod
@@ -397,10 +407,12 @@ def analyze_project(
397
407
  for name in sorted(analysable):
398
408
  for symbol, registered in _routes_of(analysable[name]).items():
399
409
  routes.setdefault(symbol, registered)
410
+ escaped = escaped_templates(root)
400
411
  for manager in analysable.values():
401
412
  manager.provide(RegisteredRoutes, routes)
413
+ manager.provide(EscapedTemplates, escaped)
402
414
 
403
- configuration = _configuration_key(registry, plugins, models, advisories, dependencies, routes)
415
+ configuration = _configuration_key(registry, plugins, models, advisories, dependencies, routes, escaped)
404
416
  keys = module_keys(
405
417
  graph,
406
418
  {name: fingerprint(configuration, str(files[name].source_id), name, files[name].text) for name in analysable},
@@ -441,6 +453,7 @@ def analyze_project(
441
453
  encode_index(_seed(results, graph, component)),
442
454
  advisory_paths,
443
455
  _encode_routes(routes),
456
+ tuple(sorted(escaped)),
444
457
  ),
445
458
  )
446
459
  for component in pending
@@ -588,6 +601,7 @@ class _Batch:
588
601
  seed: Mapping[str, Any]
589
602
  advisory_paths: tuple[Path, ...] = ()
590
603
  routes: tuple[tuple[str, str, str, int], ...] = ()
604
+ escaped: tuple[str, ...] = ()
591
605
 
592
606
 
593
607
  def _analyse_batch(batch: _Batch) -> dict[str, dict[str, Any]]:
@@ -612,6 +626,7 @@ def _analyse_batch(batch: _Batch) -> dict[str, dict[str, Any]]:
612
626
  manager.provide(SecurityModelAnalysis, models)
613
627
  manager.provide(DependencyAnalysis, dependencies)
614
628
  manager.provide(RegisteredRoutes, routes)
629
+ manager.provide(EscapedTemplates, frozenset(batch.escaped))
615
630
  module_plugins = tuple(p for p in all_plugins if not isinstance(p, ProjectPlugin))
616
631
  results = _analyse_managers(managers, decode_index(batch.seed), module_plugins, affected)
617
632
  return {name: encode(entry) for name, entry in results.items()}
@@ -639,6 +654,7 @@ def _configuration_key(
639
654
  advisories: tuple[Advisory, ...],
640
655
  dependencies: DependencyGraph,
641
656
  routes: Routes | None = None,
657
+ escaped: frozenset[str] = frozenset(),
642
658
  ) -> str:
643
659
  """Everything a module's results depend on besides the project sources (§11)."""
644
660
 
@@ -658,6 +674,7 @@ def _configuration_key(
658
674
  repr(dependencies.requirements),
659
675
  repr(dependencies.errors),
660
676
  repr(_encode_routes(routes or {})),
677
+ repr(sorted(escaped)),
661
678
  )
662
679
 
663
680
 
@@ -8,10 +8,13 @@ and ``not`` combine as expected, a ``Validator`` model names a callable whose tr
8
8
  one of its arguments, a numeric value (``abstract.ranges``) cannot inject, and anything
9
9
  else that mentions the value is a guard that does not prove it. A proof counts for an
10
10
  origin when every dependence path from that origin to the sink argument goes through a
11
- proven value. A flow is refuted when every tainted origin is proven safe or the sink is
12
- unreachable, a hotspot when it sits behind an ``AuthorizationGuard`` (a decorator or a
13
- dominating condition) or when an unproven guard mentions it, and a vulnerability
14
- otherwise.
11
+ proven value. A guard mentions an origin only when it examines what the sink receives:
12
+ reading another attribute or item of an object the flow reads (``request.method`` next
13
+ to ``request.POST``) examines nothing of it, while the same attribute, or a method called
14
+ on the object (``form.is_valid()``), does. A flow is refuted when every tainted origin is
15
+ proven safe or the sink is unreachable, a hotspot when it sits behind an
16
+ ``AuthorizationGuard`` (a decorator or a dominating condition) or when an unproven guard
17
+ mentions it, and a vulnerability otherwise.
15
18
  """
16
19
 
17
20
  from __future__ import annotations
@@ -37,6 +40,7 @@ from coretrace_python.ir.model import (
37
40
  Constant,
38
41
  FunctionIR,
39
42
  GetAttr,
43
+ GetItem,
40
44
  Instruction,
41
45
  UnaryOp,
42
46
  Value,
@@ -267,6 +271,66 @@ class _Judge:
267
271
  return tested, "equals a constant", definition.operator == "eq"
268
272
  return None
269
273
 
274
+ def examined(
275
+ self, condition: Value, mentioned: set[Value], argument: Value, chain: frozenset[Value]
276
+ ) -> set[Value]:
277
+ """What the guard examines of the values it mentions: everything its condition is
278
+ computed from, except an object it only reads another attribute or item of than
279
+ the flow reads (``request`` in ``request.method`` when the sink receives
280
+ ``request.POST['cmd']``). A method called on the object (``form.is_valid()``), the
281
+ same attribute or item, or any part of an object the flow takes whole (passes to
282
+ the sink or to a call), examines it."""
283
+
284
+ reads = {self.access(value) for value in chain} - {None}
285
+ whole = {argument} | {
286
+ operand
287
+ for value in chain
288
+ if (definition := self.defs.get(value)) is not None
289
+ for operand in definition.operands()
290
+ if not (isinstance(definition, GetAttr | GetItem) and definition.object == operand)
291
+ }
292
+ found: set[Value] = set()
293
+ called: set[Value] = set()
294
+ pending = [condition]
295
+ while pending:
296
+ value = pending.pop()
297
+ if value in found:
298
+ continue
299
+ found.add(value)
300
+ definition = self.defs.get(value)
301
+ if definition is None:
302
+ continue
303
+ if isinstance(definition, Call):
304
+ called.add(definition.callee)
305
+ access = None if value in called else self.access(value)
306
+ for operand in definition.operands():
307
+ if access is not None and access[0] == operand and operand not in whole and self.aside(access, reads):
308
+ continue
309
+ pending.append(operand)
310
+ return found & mentioned
311
+
312
+ @staticmethod
313
+ def aside(access: tuple[Value, str, object], reads: set[tuple[Value, str, object] | None]) -> bool:
314
+ """Whether reading ``access`` reads beside what the flow reads of the same object;
315
+ an item under a key unknown on either side may be the same one."""
316
+
317
+ owner, kind, name = access
318
+ if kind == "item" and (name is None or (owner, "item", None) in reads):
319
+ return False
320
+ return access not in reads
321
+
322
+ def access(self, value: Value) -> tuple[Value, str, object] | None:
323
+ """``(object, "attribute" or "item", name or constant key)`` for a read of an
324
+ attribute or an item; ``None`` for any other value, and as the key when unknown."""
325
+
326
+ definition = self.defs.get(value)
327
+ if isinstance(definition, GetAttr):
328
+ return definition.object, "attribute", definition.attribute
329
+ if isinstance(definition, GetItem):
330
+ key = self.defs.get(definition.key)
331
+ return definition.object, "item", key.value if isinstance(key, Constant) else None
332
+ return None
333
+
270
334
  def is_constant_collection(self, value: Value) -> bool:
271
335
  definition = self.defs.get(value)
272
336
  if isinstance(definition, Constant):
@@ -326,8 +390,9 @@ class _Judge:
326
390
  for value, reason in found.items():
327
391
  if value in chain:
328
392
  proven.setdefault(value, reason)
393
+ examined = self.examined(guard.condition, mentioned, flow.argument, chain)
329
394
  for origin in origins:
330
- if origin not in mentions and mentioned & (
395
+ if origin not in mentions and examined & (
331
396
  {origin} | {w for w in chain if origin in self.closure(w)}
332
397
  ):
333
398
  mentions[origin] = guard.line
@@ -87,11 +87,13 @@ EMPTY = Dep()
87
87
 
88
88
  @dataclass(frozen=True)
89
89
  class ExternalCall:
90
- """An external symbol reached from this function; dependencies are parameter indices."""
90
+ """An external symbol reached from this function; dependencies are parameter indices.
91
+ Keyword arguments keep their name, so a sink reading ``url=`` only can be decided;
92
+ ``*args`` and ``**kwargs`` have none."""
91
93
 
92
94
  symbol: SymbolId
93
95
  argument_dependencies: tuple[Dependencies, ...]
94
- keyword_dependencies: Dependencies
96
+ keyword_dependencies: tuple[tuple[str | None, Dependencies], ...]
95
97
  location: SourceSpan
96
98
  call_site: SourceSpan | None
97
99
  # What the arguments of the call at ``location`` denote, where it is made.
@@ -313,9 +315,13 @@ class _DependenceProblem(DataflowProblem[State]):
313
315
 
314
316
  def call(self, call: Call, state: dict[Key, Dep]) -> Dep:
315
317
  arguments = tuple(self.deep(a, state) for a in call.arguments)
318
+ named = (
319
+ *((name, self.deep(value, state)) for name, value in call.keywords),
320
+ *((None, self.deep(value, state)) for value in call.starred),
321
+ )
316
322
  keywords = EMPTY
317
- for value in (*call.starred, *(v for _, v in call.keywords)):
318
- keywords |= self.deep(value, state)
323
+ for _, deps in named:
324
+ keywords |= deps
319
325
  everything = keywords
320
326
  for deps in arguments:
321
327
  everything |= deps
@@ -335,7 +341,7 @@ class _DependenceProblem(DataflowProblem[State]):
335
341
  self.record(
336
342
  target.symbol,
337
343
  tuple(a.parameters for a in arguments),
338
- keywords.parameters,
344
+ tuple((name, deps.parameters) for name, deps in named),
339
345
  call.location,
340
346
  None,
341
347
  self.graph.arguments_at(self.name, call.location),
@@ -418,7 +424,7 @@ class _DependenceProblem(DataflowProblem[State]):
418
424
  self.record(
419
425
  reached.symbol,
420
426
  tuple(mapped(d).parameters for d in reached.argument_dependencies),
421
- mapped(reached.keyword_dependencies).parameters,
427
+ tuple((name, mapped(d).parameters) for name, d in reached.keyword_dependencies),
422
428
  reached.location,
423
429
  call.location,
424
430
  reached.arguments,
@@ -435,7 +441,7 @@ class _DependenceProblem(DataflowProblem[State]):
435
441
  self,
436
442
  symbol: SymbolId,
437
443
  arguments: tuple[Dependencies, ...],
438
- keywords: Dependencies,
444
+ keywords: tuple[tuple[str | None, Dependencies], ...],
439
445
  location: SourceSpan,
440
446
  call_site: SourceSpan | None,
441
447
  given: Arguments,
@@ -446,7 +452,11 @@ class _DependenceProblem(DataflowProblem[State]):
446
452
  arguments = tuple(
447
453
  a | b for a, b in zip(previous.argument_dependencies, arguments, strict=False)
448
454
  )
449
- keywords |= previous.keyword_dependencies
455
+ # The same call records the same keywords, in the same order.
456
+ keywords = tuple(
457
+ (name, deps | old)
458
+ for (name, deps), (_, old) in zip(keywords, previous.keyword_dependencies, strict=False)
459
+ )
450
460
  self.external[key] = ExternalCall(symbol, arguments, keywords, location, call_site, given)
451
461
 
452
462
 
@@ -24,15 +24,18 @@ from coretrace_python.taint.models import (
24
24
  Source,
25
25
  SuffixSink,
26
26
  TaintKind,
27
+ TemplateRender,
27
28
  TypedParameter,
28
29
  Validator,
29
30
  )
30
31
  from coretrace_python.taint.routes import RegisteredRoutes, Routes, registered_routes
32
+ from coretrace_python.taint.templates import EscapedTemplates, escaped_templates
31
33
 
32
34
  __all__ = [
33
35
  "AuthorizationGuard",
34
36
  "EntryPoint",
35
37
  "EntryPointAnalysis",
38
+ "EscapedTemplates",
36
39
  "Model",
37
40
  "ModelError",
38
41
  "ModelTable",
@@ -52,8 +55,10 @@ __all__ = [
52
55
  "TaintFacts",
53
56
  "TaintFlow",
54
57
  "TaintKind",
58
+ "TemplateRender",
55
59
  "TypedParameter",
56
60
  "Validator",
61
+ "escaped_templates",
57
62
  "propagate_taint",
58
63
  "registered_routes",
59
64
  ]
@@ -10,7 +10,7 @@ argument reaching a sink whose kinds it still carries is reported as a ``TaintFl
10
10
  from __future__ import annotations
11
11
 
12
12
  from collections.abc import Callable, Mapping
13
- from dataclasses import dataclass
13
+ from dataclasses import dataclass, replace
14
14
  from types import MappingProxyType
15
15
  from typing import ClassVar
16
16
 
@@ -79,6 +79,7 @@ from coretrace_python.taint.models import (
79
79
  TaintKind,
80
80
  )
81
81
  from coretrace_python.taint.routes import RegisteredRoutes, Routes
82
+ from coretrace_python.taint.templates import EscapedTemplates
82
83
 
83
84
 
84
85
  @dataclass(frozen=True)
@@ -152,9 +153,12 @@ class _TaintProblem(DataflowProblem[State]):
152
153
  heap: HeapFacts | None = None,
153
154
  seeds: Mapping[HeapLocation, Taint] | None = None,
154
155
  module: str = "",
156
+ escaped: frozenset[str] = frozenset(),
155
157
  ) -> None:
156
158
  self.name = name
157
159
  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)
158
162
  self.function = function
159
163
  self.models = models
160
164
  self.graph = graph
@@ -401,11 +405,17 @@ class _TaintProblem(DataflowProblem[State]):
401
405
  if sink is not None:
402
406
  for position, argument in enumerate(call.arguments):
403
407
  self.report(flows, sink, self.rendered(argument, state), argument, call, None, None, position, given)
404
- for argument in (*call.starred, *(value for _, value in call.keywords)):
408
+ for argument in call.starred:
405
409
  self.report(flows, sink, self.rendered(argument, state), argument, call, None, None, None, given)
410
+ for name, argument in call.keywords:
411
+ self.report(
412
+ flows, sink, self.rendered(argument, state), argument, call, None, None, None, given, keyword=name
413
+ )
406
414
  sanitizer = self.models.sanitizer(symbol)
407
415
  if sanitizer is not None:
408
416
  return everything.without(sanitizer.kinds)
417
+ if self.renders_escaped(symbol, given):
418
+ return everything.without(TaintKind.HTML)
409
419
  # A method on a tainted object returns tainted data (``request.args.get``).
410
420
  everything = everything.join(state.get(call.callee, Taint.none()))
411
421
  source = self.models.source_covering(symbol)
@@ -413,6 +423,14 @@ class _TaintProblem(DataflowProblem[State]):
413
423
  return everything.join(Taint(source.kinds, frozenset({source})))
414
424
  return everything
415
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
+
416
434
  def guarded(self, sink: Sink | None, arguments: Arguments) -> Sink | None:
417
435
  """The sink as this call makes it: an argument declared safe (``yaml.load`` with
418
436
  ``Loader=SafeLoader``) takes off the kinds it covers, and no kind left, no sink."""
@@ -424,7 +442,7 @@ class _TaintProblem(DataflowProblem[State]):
424
442
  if safe is None or given is None or not given[0] or given[1] not in safe.values:
425
443
  return sink
426
444
  kinds = sink.kinds & ~safe.kinds
427
- return Sink(sink.symbol, kinds, sink.positions) if kinds else None
445
+ return replace(sink, kinds=kinds) if kinds else None
428
446
 
429
447
  def rendered(self, argument: Value, state: Mapping[Key, Taint]) -> Taint:
430
448
  """The taint an argument brings to a sink. A container literal is serialised by
@@ -497,11 +515,16 @@ class _TaintProblem(DataflowProblem[State]):
497
515
  sink = self.guarded(self.models.sink(reached.symbol), reached.arguments)
498
516
  if sink is None:
499
517
  continue
500
- positions: list[int | None] = [*range(len(reached.argument_dependencies)), None]
501
- for position, deps in zip(positions, (*reached.argument_dependencies, reached.keyword_dependencies), strict=True):
518
+ for position, deps in enumerate(reached.argument_dependencies):
502
519
  taint, witness = mapped(deps)
503
520
  if witness is not None:
504
521
  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)
524
+ if witness is not None:
525
+ self.report(
526
+ flows, sink, taint, witness, call, through, reached.location, None, reached.arguments, keyword=name
527
+ )
505
528
  for mutation in summary.mutations:
506
529
  if mutation.parameter < len(values):
507
530
  stored = mapped(mutation.dependencies)[0]
@@ -528,8 +551,9 @@ class _TaintProblem(DataflowProblem[State]):
528
551
  sink_location: SourceSpan | None,
529
552
  position: int | None = None,
530
553
  arguments: Arguments | None = None,
554
+ keyword: str | None = None,
531
555
  ) -> None:
532
- reaching = taint.kinds & sink.kinds_at(position)
556
+ reaching = taint.kinds & sink.kinds_at(position, keyword)
533
557
  if not reaching:
534
558
  return
535
559
  for source in sorted(taint.sources, key=lambda s: str(s.symbol)):
@@ -792,8 +816,9 @@ def propagate_taint(
792
816
  heap: HeapFacts | None = None,
793
817
  seeds: Mapping[HeapLocation, Taint] | None = None,
794
818
  module: str = "",
819
+ escaped: frozenset[str] = frozenset(),
795
820
  ) -> TaintFacts:
796
- problem = _TaintProblem(name, function, models, graph, summaries, parameters, project, heap, seeds, module)
821
+ problem = _TaintProblem(name, function, models, graph, summaries, parameters, project, heap, seeds, module, escaped)
797
822
  solution = solve(problem, cfg)
798
823
  taints: dict[Key, Taint] = {}
799
824
  flows: list[TaintFlow] = []
@@ -821,6 +846,7 @@ class TaintAnalysis(FunctionAnalysis[TaintFacts]):
821
846
  ProjectSummaries,
822
847
  HeapAnalysis,
823
848
  RegisteredRoutes,
849
+ EscapedTemplates,
824
850
  }
825
851
  )
826
852
 
@@ -857,6 +883,7 @@ class TaintAnalysis(FunctionAnalysis[TaintFacts]):
857
883
  heap,
858
884
  seeds,
859
885
  ctx.module.name,
886
+ ctx.get(EscapedTemplates),
860
887
  )
861
888
 
862
889
 
@@ -55,28 +55,42 @@ class Source:
55
55
 
56
56
  @dataclass(frozen=True)
57
57
  class Sink:
58
- """A callable whose arguments must not carry the given taint kinds. ``positions``
59
- restricts some kinds to argument positions: a SQL statement is the first argument
60
- of ``execute``, its parameter tuple is not a statement."""
58
+ """A callable whose arguments must not carry the given taint kinds. ``positions`` and
59
+ ``keywords`` restrict some kinds to the arguments they list: a SQL statement is the
60
+ first argument of ``execute``, its parameter tuple is not a statement; the target of
61
+ ``requests.get`` is its first argument or ``url=``, its body is no destination."""
61
62
 
62
63
  symbol: SymbolId
63
64
  kinds: TaintKind
64
65
  positions: tuple[tuple[TaintKind, tuple[int, ...]], ...] = ()
65
-
66
- def kinds_at(self, position: int | None) -> TaintKind:
67
- """The kinds that must not reach the argument at ``position`` (``None`` for a
68
- keyword or starred argument)."""
69
-
70
- kinds = self.kinds
71
- for restricted, allowed in self.positions:
72
- if position is None or position not in allowed:
73
- kinds &= ~restricted
74
- return kinds
66
+ keywords: tuple[tuple[TaintKind, tuple[str, ...]], ...] = ()
67
+
68
+ def kinds_at(self, position: int | None, keyword: str | None = None) -> TaintKind:
69
+ """The kinds that must not reach the argument at ``position`` or passed as
70
+ ``keyword``; both are ``None`` for a starred argument or ``**kwargs``, which
71
+ reach no restricted kind. A restricted kind reaches any argument one of its
72
+ restrictions lists."""
73
+
74
+ restricted = allowed = TaintKind(0)
75
+ for kinds, positions in self.positions:
76
+ restricted |= kinds
77
+ if position in positions:
78
+ allowed |= kinds
79
+ for kinds, names in self.keywords:
80
+ restricted |= kinds
81
+ if keyword in names:
82
+ allowed |= kinds
83
+ return self.kinds & ~(restricted & ~allowed)
75
84
 
76
85
  def merged(self, other: Sink) -> Sink:
77
- """This sink with ``other``'s kinds and positions added."""
86
+ """This sink with ``other``'s kinds, positions and keywords added."""
78
87
 
79
- return Sink(self.symbol, self.kinds | other.kinds, self.positions + other.positions)
88
+ return Sink(
89
+ self.symbol,
90
+ self.kinds | other.kinds,
91
+ self.positions + other.positions,
92
+ self.keywords + other.keywords,
93
+ )
80
94
 
81
95
 
82
96
  @dataclass(frozen=True)
@@ -173,6 +187,17 @@ class Validator:
173
187
  argument: int = 0
174
188
 
175
189
 
190
+ @dataclass(frozen=True)
191
+ class TemplateRender:
192
+ """A call rendering the template it names (argument ``position``, or ``keyword``)
193
+ with autoescaping on, such as ``render_to_string``: what it returns carries no
194
+ ``HTML`` when the project shows that template escaping everything it renders."""
195
+
196
+ symbol: SymbolId
197
+ position: int = 0
198
+ keyword: str = "template_name"
199
+
200
+
176
201
  @dataclass(frozen=True)
177
202
  class AuthorizationGuard:
178
203
  """A decorator, or a condition, that restricts who reaches the code behind it; a
@@ -194,6 +219,7 @@ Model = (
194
219
  | RouteRegistrar
195
220
  | SuffixSink
196
221
  | SafeArgument
222
+ | TemplateRender
197
223
  )
198
224
 
199
225
 
@@ -210,6 +236,7 @@ class ModelTable:
210
236
  route_registrars: tuple[RouteRegistrar, ...] = ()
211
237
  suffix_sinks: tuple[SuffixSink, ...] = ()
212
238
  safe_arguments: tuple[SafeArgument, ...] = ()
239
+ template_renders: tuple[TemplateRender, ...] = ()
213
240
  _by_symbol: dict[type[Model], dict[SymbolId, Model]] = field(
214
241
  init=False, repr=False, compare=False
215
242
  )
@@ -225,6 +252,7 @@ class ModelTable:
225
252
  AuthorizationGuard: {m.symbol: m for m in self.authorizations},
226
253
  RouteRegistrar: {m.symbol: m for m in self.route_registrars},
227
254
  SafeArgument: {m.symbol: m for m in self.safe_arguments},
255
+ TemplateRender: {m.symbol: m for m in self.template_renders},
228
256
  }
229
257
  object.__setattr__(self, "_by_symbol", MappingProxyType(index))
230
258
 
@@ -291,6 +319,7 @@ class ModelTable:
291
319
  self.route_registrars,
292
320
  self.suffix_sinks,
293
321
  self.safe_arguments,
322
+ self.template_renders,
294
323
  )
295
324
 
296
325
  def sanitizer(self, symbol: SymbolId) -> Sanitizer | None:
@@ -301,6 +330,10 @@ class ModelTable:
301
330
  found = self._by_symbol[SafeArgument].get(symbol)
302
331
  return found if isinstance(found, SafeArgument) else None
303
332
 
333
+ def template_render(self, symbol: SymbolId) -> TemplateRender | None:
334
+ found = self._by_symbol[TemplateRender].get(symbol)
335
+ return found if isinstance(found, TemplateRender) else None
336
+
304
337
 
305
338
  class SecurityModelRegistry:
306
339
  """Mutable collection point for the models plugins register."""
@@ -347,6 +380,7 @@ class SecurityModelRegistry:
347
380
  route_registrars=tuple(m for m in models if isinstance(m, RouteRegistrar)),
348
381
  suffix_sinks=tuple(m for m in models if isinstance(m, SuffixSink)),
349
382
  safe_arguments=tuple(m for m in models if isinstance(m, SafeArgument)),
383
+ template_renders=tuple(m for m in models if isinstance(m, TemplateRender)),
350
384
  )
351
385
 
352
386
 
@@ -0,0 +1,250 @@
1
+ """Django templates that escape everything they render (architecture §17).
2
+
3
+ ``render_to_string(name, context)`` returns HTML in which autoescaping escaped every
4
+ variable, unless the template says otherwise. The engine does not render templates; it
5
+ reads them, and establishes that a template escapes only when nothing in it, or in what
6
+ it extends or includes, can output data unescaped:
7
+
8
+ - no ``|safe`` or ``|safeseq``, no ``{% autoescape off %}``, and no tag, filter or tag
9
+ library beyond Django's own, whose output a custom one could mark safe;
10
+ - no variable where HTML escaping does not protect it: in a ``<script>`` or ``<style>``
11
+ block, an event handler, a ``style`` or ``srcdoc`` attribute, a URL attribute whose
12
+ value does not start with a fixed relative or ``http(s)`` prefix, an unquoted
13
+ attribute, an attribute or a tag name;
14
+ - no Python file of the project turning autoescaping off.
15
+
16
+ Templates are found under the project's ``templates`` directories, by their path below
17
+ one. A template the engine cannot find — named by an expression, under a ``DIRS`` entry
18
+ of another name, shipped by an installed package — is never assumed to escape.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ from collections.abc import Iterator
25
+ from pathlib import Path
26
+ from typing import ClassVar
27
+
28
+ from coretrace_python.analysis import Analysis, AnalysisContext
29
+ from coretrace_python.interprocedural import discover_files
30
+
31
+ TEMPLATES_DIRECTORY = "templates"
32
+
33
+ _OUTPUT, _TAG, _URL = "\x00", "\x01", "/u"
34
+ _SYNTAX = re.compile(r"{{(.*?)}}|{%(.*?)%}|{#.*?#}", re.DOTALL)
35
+ _AUTOESCAPE_OFF = re.compile(r"""["']autoescape["']\s*:\s*False|\bautoescape\s*=\s*False""")
36
+
37
+ # Django's own libraries, tags and filters. Every filter but ``safe`` and ``safeseq``
38
+ # escapes its input, or returns it for autoescaping to escape, when autoescaping is on.
39
+ _LIBRARIES = frozenset({"static", "i18n", "l10n", "tz", "cache", "humanize"})
40
+ _TAGS = frozenset(
41
+ {
42
+ "load", "extends", "include", "block", "endblock", "if", "elif", "else", "endif",
43
+ "for", "empty", "endfor", "with", "endwith", "ifchanged", "endifchanged", "spaceless",
44
+ "endspaceless", "filter", "endfilter", "autoescape", "endautoescape", "csrf_token",
45
+ "now", "lorem", "widthratio", "templatetag", "regroup", "resetcycle", "url", "static",
46
+ "get_static_prefix", "get_media_prefix", "firstof", "cycle", "trans", "translate",
47
+ "blocktrans", "blocktranslate", "endblocktrans", "endblocktranslate", "plural",
48
+ "get_current_language", "get_current_language_bidi", "get_available_languages",
49
+ "get_language_info", "get_language_info_list", "language", "endlanguage", "localize",
50
+ "endlocalize", "localtime", "endlocaltime", "timezone", "endtimezone",
51
+ "get_current_timezone", "cache", "endcache",
52
+ }
53
+ )
54
+ # Tags that print a value: checked where they stand, like a variable.
55
+ _PRINTING = frozenset({"firstof", "cycle", "trans", "translate"})
56
+ _FILTERS = frozenset(
57
+ {
58
+ "add", "addslashes", "capfirst", "center", "cut", "date", "default", "default_if_none",
59
+ "dictsort", "dictsortreversed", "divisibleby", "escape", "escapejs", "escapeseq",
60
+ "filesizeformat", "first", "floatformat", "force_escape", "get_digit", "iriencode",
61
+ "join", "json_script", "last", "length", "length_is", "linebreaks", "linebreaksbr",
62
+ "linenumbers", "ljust", "lower", "make_list", "phone2numeric", "pluralize", "pprint",
63
+ "random", "rjust", "slice", "slugify", "stringformat", "striptags", "time", "timesince",
64
+ "timeuntil", "title", "truncatechars", "truncatechars_html", "truncatewords",
65
+ "truncatewords_html", "unordered_list", "upper", "urlencode", "urlize", "urlizetrunc",
66
+ "wordcount", "wordwrap", "yesno", "language_name", "language_name_local",
67
+ "language_bidi", "language_name_translated", "localize", "unlocalize", "localtime",
68
+ "utc", "timezone", "apnumber", "intcomma", "intword", "naturalday", "naturaltime",
69
+ "ordinal",
70
+ }
71
+ )
72
+
73
+ _URL_ATTRIBUTES = frozenset(
74
+ {
75
+ "href", "src", "action", "formaction", "poster", "background", "cite", "data",
76
+ "srcset", "ping", "manifest", "longdesc", "usemap", "codebase", "classid", "archive",
77
+ "profile", "lowsrc", "dynsrc", "icon", "xlink:href",
78
+ }
79
+ )
80
+ # A relative path (not ``//host``), a fragment, a query, or an http(s) URL whose host is
81
+ # fixed: no scheme such as ``javascript:`` can follow.
82
+ _SAFE_URL_PREFIX = re.compile(r"(?:/(?!/)|\.{1,2}/|[#?]|https?://[^/?#\x00\x01]+[/?#])")
83
+ _QUOTED = r"\"[^\"]*\"|'[^']*'"
84
+ _ATTRIBUTE = re.compile(rf"([^\s\"'>/=]+)(?:\s*=\s*({_QUOTED}|[^\s\"'=<>`]+))?")
85
+ _START_TAG = re.compile(rf"<([A-Za-z][^\s/>]*)((?:\s*[^\s\"'>/=]+(?:\s*=\s*(?:{_QUOTED}|[^\s\"'=<>`]+))?)*)\s*/?>")
86
+ _RAW_TEXT = re.compile(r"<(script|style)\b[^>]*>(.*?)</\1\s*>", re.DOTALL | re.IGNORECASE)
87
+ _COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
88
+ _END_TAG = re.compile(r"</[A-Za-z][^>]*>")
89
+
90
+
91
+ def escaped_templates(root: Path) -> frozenset[str]:
92
+ """The names of the templates under ``root`` that escape every variable they render,
93
+ as ``render_to_string`` names them (their path below a ``templates`` directory)."""
94
+
95
+ found: dict[str, list[Path]] = {}
96
+ for path in discover_files(root):
97
+ if path.suffix == ".py" and _AUTOESCAPE_OFF.search(_read(path)):
98
+ return frozenset()
99
+ for name in _names(path.relative_to(root).parts):
100
+ found.setdefault(name, []).append(path)
101
+ local: dict[str, bool] = {}
102
+ requires: dict[str, set[str]] = {}
103
+ for name, paths in found.items():
104
+ verdicts = [_inspect(_read(path)) for path in paths]
105
+ local[name] = all(safe for safe, _ in verdicts)
106
+ requires[name] = {needed for _, needs in verdicts for needed in needs}
107
+ escaped = {name for name, safe in local.items() if safe}
108
+ changed = True
109
+ while changed:
110
+ kept = {name for name in escaped if requires[name] <= escaped}
111
+ changed = kept != escaped
112
+ escaped = kept
113
+ return frozenset(escaped)
114
+
115
+
116
+ def _names(parts: tuple[str, ...]) -> Iterator[str]:
117
+ for index, part in enumerate(parts[:-1]):
118
+ if part == TEMPLATES_DIRECTORY:
119
+ yield "/".join(parts[index + 1 :])
120
+
121
+
122
+ def _read(path: Path) -> str:
123
+ try:
124
+ return path.read_text(encoding="utf-8", errors="replace")
125
+ except OSError:
126
+ return ""
127
+
128
+
129
+ def _inspect(text: str) -> tuple[bool, set[str]]:
130
+ """Whether the template escapes all it prints itself, and the templates it extends
131
+ or includes, which must escape too."""
132
+
133
+ requires: set[str] = set()
134
+ html: list[str] = []
135
+ skipping: str | None = None
136
+ position = 0
137
+ text = text.replace(_OUTPUT, "").replace(_TAG, "")
138
+ for match in _SYNTAX.finditer(text):
139
+ variable, tag = match.group(1), match.group(2)
140
+ words = tag.split() if tag is not None else []
141
+ if skipping is not None:
142
+ if words[:1] == [skipping]:
143
+ if skipping == "endverbatim":
144
+ html.append(text[position : match.start()])
145
+ skipping, position = None, match.end()
146
+ continue
147
+ html.append(text[position : match.start()])
148
+ position = match.end()
149
+ if variable is not None:
150
+ if not _escaping_filters(variable):
151
+ return False, requires
152
+ html.append(_OUTPUT)
153
+ elif words and words[0] in ("comment", "verbatim"):
154
+ skipping = f"end{words[0]}"
155
+ if words[0] == "verbatim":
156
+ position = match.end()
157
+ elif words:
158
+ name = words[0]
159
+ if name not in _TAGS:
160
+ return False, requires
161
+ if name == "load" and not set(_loaded(words[1:])) <= _LIBRARIES:
162
+ return False, requires
163
+ if name == "autoescape" and words[1:] != ["on"]:
164
+ return False, requires
165
+ if name == "filter" and not _escaping_filters(tag.split(None, 1)[1] if len(words) > 1 else ""):
166
+ return False, requires
167
+ if name in ("extends", "include"):
168
+ needed = _constant(words[1] if len(words) > 1 else "")
169
+ if needed is None:
170
+ return False, requires
171
+ requires.add(needed)
172
+ html.append(_OUTPUT if name in _PRINTING else _URL if name in ("url", "static") else _TAG)
173
+ if skipping is not None:
174
+ return False, requires
175
+ html.append(text[position:])
176
+ return _escapes_where_printed("".join(html)), requires
177
+
178
+
179
+ def _loaded(words: list[str]) -> list[str]:
180
+ """The libraries ``{% load a b %}`` or ``{% load x y from lib %}`` loads."""
181
+
182
+ return words[words.index("from") + 1 :] if "from" in words else words
183
+
184
+
185
+ def _escaping_filters(expression: str) -> bool:
186
+ filters = re.sub(_QUOTED, "", expression).split("|")[1:]
187
+ return all(f.split(":", 1)[0].strip() in _FILTERS for f in filters)
188
+
189
+
190
+ def _constant(word: str) -> str | None:
191
+ return word[1:-1] if len(word) >= 2 and word[0] == word[-1] and word[0] in "\"'" else None
192
+
193
+
194
+ def _escapes_where_printed(html: str) -> bool:
195
+ """Whether every output marker of ``html`` stands where HTML escaping protects it."""
196
+
197
+ position = 0
198
+ while (start := html.find("<", position)) != -1:
199
+ rest = html[start:]
200
+ comment = _COMMENT.match(rest)
201
+ raw = _RAW_TEXT.match(rest)
202
+ tag = _START_TAG.match(rest)
203
+ end = _END_TAG.match(rest)
204
+ if comment:
205
+ position = start + comment.end()
206
+ elif raw:
207
+ if _OUTPUT in raw.group(2) or not _attributes_escape(raw.group(0)[: raw.start(2)]):
208
+ return False
209
+ position = start + raw.end()
210
+ elif tag:
211
+ if _OUTPUT in tag.group(1) or not _attributes_escape(tag.group(2)):
212
+ return False
213
+ position = start + tag.end()
214
+ elif end:
215
+ if _OUTPUT in end.group(0):
216
+ return False
217
+ position = start + end.end()
218
+ else:
219
+ # ``<{{ name }}``: the variable would name a tag, escaping cannot stop it.
220
+ if rest[1:].lstrip("/ \t\n").startswith(_OUTPUT):
221
+ return False
222
+ position = start + 1
223
+ return True
224
+
225
+
226
+ def _attributes_escape(attributes: str) -> bool:
227
+ for match in _ATTRIBUTE.finditer(attributes):
228
+ name, value = match.group(1).lower(), match.group(2)
229
+ if _OUTPUT in name:
230
+ return False
231
+ if value is None or _OUTPUT not in value:
232
+ continue
233
+ if value[0] not in "\"'" or name.startswith("on") or name in ("style", "srcdoc"):
234
+ return False
235
+ if name in _URL_ATTRIBUTES:
236
+ prefix = value[1:].split(_OUTPUT, 1)[0]
237
+ if _TAG in prefix or not _SAFE_URL_PREFIX.match(prefix):
238
+ return False
239
+ return True
240
+
241
+
242
+ class EscapedTemplates(Analysis[frozenset[str]]):
243
+ """The names of the project templates that escape everything they render, provided
244
+ by the engine for a project; none on its own."""
245
+
246
+ name: ClassVar[str] = "taint.escaped_templates"
247
+
248
+ @classmethod
249
+ def compute(cls, ctx: AnalysisContext) -> frozenset[str]:
250
+ return frozenset()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coretrace-python-analyzer
3
- Version: 0.6.0
3
+ Version: 0.7.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=MPB7k3sSK3YPPbcgkTbDkNy8vJRChFlClF8INwVFbTw,75
1
+ coretrace_python/__init__.py,sha256=kEx6lE06VX-mfQ6o7DrayGZNlR2uTGSdGItAWsSIeHI,75
2
2
  coretrace_python/__main__.py,sha256=V8DnYtRboISK2VCKm7DGHLREKvqLbqApyWlKBOs26wU,65
3
- coretrace_python/cache.py,sha256=0FgPpr5frJrZ3HndMMNJwSmXtkdWYv1yvkvCDMENt5E,12823
3
+ coretrace_python/cache.py,sha256=tt-l1jOibCx0lNKGd7XF_xTlcCeBLewPWOPpS-e3Ynk,12929
4
4
  coretrace_python/cli.py,sha256=SwXYUR8DIgppkqom6kKNM701pb4p2omEy-hRXIM5yZE,10946
5
- coretrace_python/engine.py,sha256=Ou1gEoHThoi7eAqv12kIV7Wy16t6hCNVqzAEplUXCCI,28202
5
+ coretrace_python/engine.py,sha256=3NxNKzkNTWxa7a4NVkZ0fyh5KitNpDHuTsCrdBa4VxU,28696
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
@@ -29,13 +29,13 @@ coretrace_python/bundled/models/credentials/credential_models.py,sha256=Mw9ty8PD
29
29
  coretrace_python/bundled/models/credentials/plugin.toml,sha256=0ut7Jc7IdcvbsZKPu6ty-z3DBlMg2CUT7JX4y4nMybo,217
30
30
  coretrace_python/bundled/models/db_drivers/db_driver_models.py,sha256=x4SAwhcpuG5TLfhmVsuAQ1VV3lAXSNh5YHx7QuFBQyU,2339
31
31
  coretrace_python/bundled/models/db_drivers/plugin.toml,sha256=094-QIV92x-69uy2CgMulBXfGvt48qtefLyeW2q3SRs,178
32
- coretrace_python/bundled/models/django/django_models.py,sha256=xgczSQ98vyLgOHKTLb2nZL7VyxdoDUTJC8oKIh0kdRg,5756
32
+ coretrace_python/bundled/models/django/django_models.py,sha256=1ftZ8tEBZJXrAiZr0rQZBSIOu4VqwOkHz8WE8FE5UBI,6445
33
33
  coretrace_python/bundled/models/django/plugin.toml,sha256=_Qa2X_4IViORQkcLvLsAC5vI-2P5CxgG8VvnuNIWcFQ,214
34
34
  coretrace_python/bundled/models/fastapi/fastapi_models.py,sha256=K2Edlo1RzYKmAJAIChNVUDc8gorAS48MGEMKNlCIGrI,1251
35
35
  coretrace_python/bundled/models/fastapi/plugin.toml,sha256=V_R2WcEc-qkST0tTpL-cRf2FEbjK6Mk727WbJmVpWJM,200
36
36
  coretrace_python/bundled/models/flask/flask_models.py,sha256=XVZWH4u2SdbErk40L-JjTBXQI1hrVCDlMu5BeqE1Ibc,2319
37
37
  coretrace_python/bundled/models/flask/plugin.toml,sha256=kI53Xwf5aZKd2n3fEQEbBzNnOXRvuonqEazTOIXPJP4,192
38
- coretrace_python/bundled/models/http_clients/http_client_models.py,sha256=TmVIg66zRRQxx3C9RT2XX6xLhemm037hIIBFRSyKzU0,1273
38
+ coretrace_python/bundled/models/http_clients/http_client_models.py,sha256=9XhHWZKq3XWcD0WexucuedAiwxp3fhXxGmo9R6DNtSQ,1954
39
39
  coretrace_python/bundled/models/http_clients/plugin.toml,sha256=vq9tWHgy8-m8LiU1StGhm9Lsy7vnj3w90P4AKDaKfc0,216
40
40
  coretrace_python/bundled/models/python_stdlib/plugin.toml,sha256=DWqjG68rx91HCJi_VEp6tUs4ujkzlR7Lq7saMvtK2HM,187
41
41
  coretrace_python/bundled/models/python_stdlib/python_stdlib.py,sha256=3LmjcFx3rUhM3Mpw86q8bS5Xj6gRJs_p6M-8yBGiK2k,4632
@@ -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=wL26drcy0JB-qHlRpb4jaYWej209GxTi2tt5SWnfCFw,17465
102
+ coretrace_python/findings/refutation.py,sha256=MmFANBozz0sodIhGUhrgr7BCEwhw5s80kRPRMDRDIyU,20707
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
@@ -110,7 +110,7 @@ coretrace_python/hir/visitors.py,sha256=JFnX09hK6trLyRcWgZ5ehOG2ywX3VvkqQ1h82nah
110
110
  coretrace_python/interprocedural/__init__.py,sha256=SXtIGE3DD4fADIXLDuEvyT_3oIsh7Vio3BL4U6oUm28,1162
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=q--xpp1euaf5utxbK4CZtS8CSyLJ6p3MLel5Iu80X3w,20747
113
+ coretrace_python/interprocedural/summaries.py,sha256=9-WGS-X37KJY1ynoXXA5HNfYFLeCyiKVMAIjeWgaVLI,21333
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
@@ -137,13 +137,14 @@ coretrace_python/semantic/symbols.py,sha256=3JptnnX5_KBlSd00L_wcGlYZQdnmBvSBAhfi
137
137
  coretrace_python/source/__init__.py,sha256=0Y8fvtbkETnNCJR9Zq7RS5TSgF9c22pz3SSk1KdAXF0,301
138
138
  coretrace_python/source/manager.py,sha256=JRmMOcWfS1nocbVrdcvfQTqEfIkmnb5JP857TpGywN4,2736
139
139
  coretrace_python/source/model.py,sha256=FZnN5mlir3tzbIpWoxnn6YtRi9UcJ_y0sZM60tKr8U4,1727
140
- coretrace_python/taint/__init__.py,sha256=UIJwnPnyYODsvnpe6-cmF8tOvKoGvWE7yVH3yIfthcM,1199
141
- coretrace_python/taint/engine.py,sha256=UY59sb-sUWct3jzFHr6PBfJ_VFhZgb13tSyvD9nKqaM,38916
142
- coretrace_python/taint/models.py,sha256=23TMzT149dCpRULO3RAyzbZWmayXWw4-a5iRjrqC0yo,13074
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
143
143
  coretrace_python/taint/routes.py,sha256=UAJKXcw_sM9shf58FfDroxVO9bNSm4qrQdZknwUB0lo,6110
144
- coretrace_python_analyzer-0.6.0.dist-info/METADATA,sha256=WW3JCb3v2qbdWowN5kzDA1NRREuGErf-QysFU7k2kIU,3216
145
- coretrace_python_analyzer-0.6.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
146
- coretrace_python_analyzer-0.6.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
147
- coretrace_python_analyzer-0.6.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
148
- coretrace_python_analyzer-0.6.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
149
- coretrace_python_analyzer-0.6.0.dist-info/RECORD,,
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,,