coretrace-python-analyzer 0.18.0__py3-none-any.whl → 0.19.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.
- coretrace_python/__init__.py +1 -1
- coretrace_python/cache.py +9 -4
- coretrace_python/engine.py +95 -28
- coretrace_python/findings/coverage.py +6 -4
- coretrace_python/interprocedural/__init__.py +2 -0
- coretrace_python/interprocedural/callgraph.py +6 -2
- coretrace_python/interprocedural/modulegraph.py +103 -9
- coretrace_python/semantic/scopes.py +2 -2
- coretrace_python/taint/engine.py +53 -8
- coretrace_python/taint/templates.py +279 -29
- {coretrace_python_analyzer-0.18.0.dist-info → coretrace_python_analyzer-0.19.0.dist-info}/METADATA +1 -1
- {coretrace_python_analyzer-0.18.0.dist-info → coretrace_python_analyzer-0.19.0.dist-info}/RECORD +16 -16
- {coretrace_python_analyzer-0.18.0.dist-info → coretrace_python_analyzer-0.19.0.dist-info}/WHEEL +0 -0
- {coretrace_python_analyzer-0.18.0.dist-info → coretrace_python_analyzer-0.19.0.dist-info}/entry_points.txt +0 -0
- {coretrace_python_analyzer-0.18.0.dist-info → coretrace_python_analyzer-0.19.0.dist-info}/licenses/LICENSE +0 -0
- {coretrace_python_analyzer-0.18.0.dist-info → coretrace_python_analyzer-0.19.0.dist-info}/licenses/NOTICE +0 -0
coretrace_python/__init__.py
CHANGED
coretrace_python/cache.py
CHANGED
|
@@ -42,7 +42,7 @@ from coretrace_python.interprocedural import (
|
|
|
42
42
|
from coretrace_python.semantic.symbols import SymbolId
|
|
43
43
|
from coretrace_python.source import SourceId, SourceSpan
|
|
44
44
|
|
|
45
|
-
CACHE_FORMAT =
|
|
45
|
+
CACHE_FORMAT = 11
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
@dataclass(frozen=True)
|
|
@@ -134,7 +134,7 @@ class ProjectCache:
|
|
|
134
134
|
def encode(module: CachedModule) -> dict[str, Any]:
|
|
135
135
|
return {
|
|
136
136
|
"format": CACHE_FORMAT,
|
|
137
|
-
"functions": [[f.name, _encode_span(f.span), f.entry_point] for f in module.functions],
|
|
137
|
+
"functions": [[f.name, _encode_span(f.span), f.entry_point, list(f.aliases)] for f in module.functions],
|
|
138
138
|
"summaries": {name: _encode_summary(s) for name, s in module.summaries.items()},
|
|
139
139
|
"sites": [_encode_site(site) for site in module.sites],
|
|
140
140
|
"findings": [_encode_finding(finding) for finding in module.findings],
|
|
@@ -196,8 +196,13 @@ def _decode_span(data: Any) -> SourceSpan:
|
|
|
196
196
|
|
|
197
197
|
|
|
198
198
|
def _decode_function(data: Any) -> ModuleFunction:
|
|
199
|
-
name, span, entry_point = data
|
|
200
|
-
return ModuleFunction(
|
|
199
|
+
name, span, entry_point, aliases = data
|
|
200
|
+
return ModuleFunction(
|
|
201
|
+
_string(name),
|
|
202
|
+
_decode_span(span),
|
|
203
|
+
None if entry_point is None else _string(entry_point),
|
|
204
|
+
tuple(_string(alias) for alias in aliases),
|
|
205
|
+
)
|
|
201
206
|
|
|
202
207
|
|
|
203
208
|
def _decode_read(data: Any) -> SymbolRead:
|
coretrace_python/engine.py
CHANGED
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import concurrent.futures
|
|
10
10
|
import hashlib
|
|
11
11
|
import multiprocessing
|
|
12
|
+
from collections import Counter
|
|
12
13
|
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
13
14
|
from dataclasses import dataclass, field, replace
|
|
14
15
|
from pathlib import Path
|
|
@@ -88,6 +89,7 @@ from coretrace_python.interprocedural import (
|
|
|
88
89
|
TemplateCallsAnalysis,
|
|
89
90
|
build_module_graph,
|
|
90
91
|
discover_sources,
|
|
92
|
+
name_modules,
|
|
91
93
|
project_symbol,
|
|
92
94
|
)
|
|
93
95
|
from coretrace_python.ir import PYIR_SCHEMA_VERSION
|
|
@@ -371,7 +373,11 @@ def analyze_project(
|
|
|
371
373
|
strongly connected components of the module graph, imports first; with ``jobs``
|
|
372
374
|
above one the components of a wave are analysed in that many processes (§29).
|
|
373
375
|
``advisory_files`` add to the ``advisories.json`` at ``root``; ``policy_file``
|
|
374
|
-
replaces the ``coretrace-policy.toml`` there.
|
|
376
|
+
replaces the ``coretrace-policy.toml`` there.
|
|
377
|
+
|
|
378
|
+
Every module is known by the name ``name_modules`` gives its file: its import name,
|
|
379
|
+
or its path from the root when another file has that import name. Such files carry
|
|
380
|
+
an ``ambiguous-module`` note and are covered as ``ambiguous``."""
|
|
375
381
|
|
|
376
382
|
if jobs < 1:
|
|
377
383
|
raise ValueError("jobs must be at least 1")
|
|
@@ -398,18 +404,18 @@ def analyze_project(
|
|
|
398
404
|
files: dict[str, SourceFile] = {}
|
|
399
405
|
unreadable: list[tuple[Path, str]] = []
|
|
400
406
|
coverage: list[FileCoverage] = []
|
|
401
|
-
|
|
407
|
+
named = name_modules(root, discover_sources(root, sources, unreadable))
|
|
402
408
|
for path, reason in unreadable:
|
|
403
409
|
findings.append(_note("syntax-error", f"{path}: {reason}", sources.add_source(str(path), ""), 1))
|
|
404
410
|
coverage.append(FileCoverage(str(path), "unreadable", 0, 0))
|
|
405
|
-
for source in
|
|
411
|
+
for name, source in named.items():
|
|
406
412
|
try:
|
|
407
|
-
modules[
|
|
413
|
+
modules[name] = build_hir(source)
|
|
408
414
|
except (ParseError, HIRBuildError) as error:
|
|
409
415
|
findings.append(_note("syntax-error", str(error), source, 1))
|
|
410
416
|
coverage.append(FileCoverage(str(source.source_id), "syntax-error", 0, 0))
|
|
411
417
|
continue
|
|
412
|
-
files[
|
|
418
|
+
files[name] = source
|
|
413
419
|
|
|
414
420
|
managers = {name: _register_all(module) for name, module in modules.items()}
|
|
415
421
|
probe = next(iter(managers.values()), None) or _register_all(build_hir(sources.add_source("<empty>", "")))
|
|
@@ -439,6 +445,8 @@ def analyze_project(
|
|
|
439
445
|
graph = build_module_graph(
|
|
440
446
|
{name: files[name] for name in analysable}, {name: modules[name] for name in analysable}, imports
|
|
441
447
|
)
|
|
448
|
+
shared = _shared_names(graph)
|
|
449
|
+
findings.extend(_ambiguity_notes(graph, shared, root))
|
|
442
450
|
routes: dict[SymbolId, EntryPoint] = {}
|
|
443
451
|
for name in sorted(analysable):
|
|
444
452
|
for symbol, registered in _routes_of(analysable[name]).items():
|
|
@@ -447,7 +455,8 @@ def analyze_project(
|
|
|
447
455
|
templates = project_templates(root)
|
|
448
456
|
clearing = models.clearing(escaped)
|
|
449
457
|
template_calls = models.template_calls(
|
|
450
|
-
templates.filters,
|
|
458
|
+
templates.filters,
|
|
459
|
+
request_processor((modules[name], analysable[name].get(ScopeAnalysis)) for name in sorted(analysable)),
|
|
451
460
|
)
|
|
452
461
|
for manager in analysable.values():
|
|
453
462
|
manager.provide(RegisteredRoutes, routes)
|
|
@@ -514,15 +523,16 @@ def analyze_project(
|
|
|
514
523
|
if pool is not None:
|
|
515
524
|
pool.shutdown()
|
|
516
525
|
|
|
517
|
-
index =
|
|
526
|
+
index = _index(results, {name: name for name in results})
|
|
518
527
|
call_graphs: dict[str, CallGraph] = {}
|
|
519
528
|
functions: dict[str, tuple[ModuleFunction, ...]] = {}
|
|
520
529
|
for name in sorted(analysable):
|
|
521
530
|
entry = results[name]
|
|
522
531
|
findings.extend(entry.findings)
|
|
523
532
|
unsupported = sum(1 for f in entry.findings if f.rule_id == "unsupported-syntax")
|
|
533
|
+
status = "ambiguous" if files[name].module_name in shared else "analysed"
|
|
524
534
|
coverage.append(
|
|
525
|
-
FileCoverage(str(files[name].source_id),
|
|
535
|
+
FileCoverage(str(files[name].source_id), status, len(entry.functions), len(entry.functions) - unsupported)
|
|
526
536
|
)
|
|
527
537
|
functions[name] = entry.functions
|
|
528
538
|
sites: dict[str, list[CallSite]] = {function.name: [] for function in entry.functions}
|
|
@@ -644,24 +654,76 @@ def _advisory_paths(root: Path, advisory_files: Sequence[Path]) -> tuple[Path, .
|
|
|
644
654
|
return (*([default] if default.is_file() else []), *advisory_files)
|
|
645
655
|
|
|
646
656
|
|
|
657
|
+
def _shared_names(graph: ModuleGraph) -> dict[str, list[str]]:
|
|
658
|
+
"""The import names several analysable files have, each with those files' modules."""
|
|
659
|
+
|
|
660
|
+
by_name: dict[str, list[str]] = {}
|
|
661
|
+
for name in graph.modules:
|
|
662
|
+
by_name.setdefault(graph.source(name).module_name, []).append(name)
|
|
663
|
+
return {import_name: names for import_name, names in by_name.items() if len(names) > 1}
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _ambiguity_notes(graph: ModuleGraph, shared: Mapping[str, list[str]], root: Path) -> list[Finding]:
|
|
667
|
+
"""One note on each file whose import name another file shares, since a symbol of
|
|
668
|
+
that name may denote a function of either, and one on each importer whose import of
|
|
669
|
+
such a name no root resolved, since calls through it are not followed."""
|
|
670
|
+
|
|
671
|
+
def located(name: str) -> str:
|
|
672
|
+
return _located(_path_of(graph.source(name)), root)
|
|
673
|
+
|
|
674
|
+
notes: list[Finding] = []
|
|
675
|
+
for import_name, names in sorted(shared.items()):
|
|
676
|
+
for name in names:
|
|
677
|
+
others = ", ".join(located(other) for other in names if other != name)
|
|
678
|
+
message = (
|
|
679
|
+
f"{located(name)} is the module '{import_name}' like {others}: a symbol "
|
|
680
|
+
f"python.{import_name}.* may name a function of either, and an import of "
|
|
681
|
+
f"'{import_name}' resolves only from its own root"
|
|
682
|
+
)
|
|
683
|
+
notes.append(_note("ambiguous-module", message, graph.source(name), 1))
|
|
684
|
+
for name in graph.modules:
|
|
685
|
+
for import_name in sorted(graph.unresolved(name)):
|
|
686
|
+
candidates = " or ".join(located(other) for other in shared[import_name])
|
|
687
|
+
message = f"'{import_name}' imported here is {candidates}: calls into it are not followed"
|
|
688
|
+
notes.append(_note("ambiguous-module", message, graph.source(name), 1))
|
|
689
|
+
return notes
|
|
690
|
+
|
|
691
|
+
|
|
647
692
|
def _seed(results: Mapping[str, CachedModule], graph: ModuleGraph, component: frozenset[str]) -> SummaryIndex:
|
|
648
693
|
"""The summaries a component starts from: those of every module it imports,
|
|
649
|
-
transitively, all final by the time its wave runs
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
694
|
+
transitively, all final by the time its wave runs, each under the import name the
|
|
695
|
+
component's code refers to it by. Of several files with one import name, only the
|
|
696
|
+
one a member imports directly, resolved in its own root, goes under that name: the
|
|
697
|
+
others are reached through summaries that inline them already, and a name no
|
|
698
|
+
member's root resolved is followed into neither file."""
|
|
699
|
+
|
|
700
|
+
wanted: set[str] = set()
|
|
701
|
+
pending = list(component)
|
|
702
|
+
while pending:
|
|
703
|
+
for imported in graph.imports(pending.pop()):
|
|
704
|
+
if imported in results and imported not in wanted:
|
|
705
|
+
wanted.add(imported)
|
|
706
|
+
pending.append(imported)
|
|
707
|
+
direct = {imported for member in component for imported in graph.imports(member)}
|
|
708
|
+
by_import: dict[str, list[str]] = {}
|
|
709
|
+
for name in wanted:
|
|
710
|
+
by_import.setdefault(graph.source(name).module_name, []).append(name)
|
|
711
|
+
names: dict[str, str] = {}
|
|
712
|
+
for import_name, modules in by_import.items():
|
|
713
|
+
chosen = modules if len(modules) == 1 else [name for name in modules if name in direct]
|
|
714
|
+
if len(chosen) == 1:
|
|
715
|
+
names[chosen[0]] = import_name
|
|
716
|
+
return _index(results, names)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _index(results: Mapping[str, CachedModule], names: Mapping[str, str]) -> SummaryIndex:
|
|
720
|
+
"""The summaries of the modules in ``names``, each function under the project symbol
|
|
721
|
+
of the module name given for its module."""
|
|
722
|
+
|
|
661
723
|
return SummaryIndex(
|
|
662
724
|
{
|
|
663
|
-
project_symbol(
|
|
664
|
-
for name in sorted(
|
|
725
|
+
project_symbol(module, function): summary
|
|
726
|
+
for name, module in sorted(names.items())
|
|
665
727
|
for function, summary in results[name].summaries.items()
|
|
666
728
|
}
|
|
667
729
|
)
|
|
@@ -674,8 +736,12 @@ def _analyse_managers(
|
|
|
674
736
|
affected: Affected,
|
|
675
737
|
) -> dict[str, CachedModule]:
|
|
676
738
|
"""Analyse one component: iterate its summaries to a fixpoint over ``seed`` (§21),
|
|
677
|
-
then extract what the rest of the run needs from each module.
|
|
739
|
+
then extract what the rest of the run needs from each module. Each member registers
|
|
740
|
+
its functions under its import name, what the others' code refers to it by; a name
|
|
741
|
+
two members share, a cycle across roots, is registered by neither."""
|
|
678
742
|
|
|
743
|
+
counts = Counter(manager.module.name for manager in managers.values())
|
|
744
|
+
names = {name: manager.module.name for name, manager in managers.items() if counts[manager.module.name] == 1}
|
|
679
745
|
index = seed
|
|
680
746
|
for manager in managers.values():
|
|
681
747
|
manager.provide(ProjectSummaries, index)
|
|
@@ -684,9 +750,9 @@ def _analyse_managers(
|
|
|
684
750
|
{
|
|
685
751
|
**seed.summaries,
|
|
686
752
|
**{
|
|
687
|
-
project_symbol(
|
|
688
|
-
for name,
|
|
689
|
-
for function, summary in _summaries_of(
|
|
753
|
+
project_symbol(module, function): summary
|
|
754
|
+
for name, module in names.items()
|
|
755
|
+
for function, summary in _summaries_of(managers[name]).items()
|
|
690
756
|
},
|
|
691
757
|
}
|
|
692
758
|
)
|
|
@@ -701,8 +767,9 @@ def _analyse_managers(
|
|
|
701
767
|
|
|
702
768
|
@dataclass(frozen=True)
|
|
703
769
|
class _Batch:
|
|
704
|
-
"""One component handed to a worker process
|
|
705
|
-
|
|
770
|
+
"""One component handed to a worker process, its files under the engine's module
|
|
771
|
+
names: it rebuilds the configuration from the project root and the plugin roots, so
|
|
772
|
+
only paths and the imported summaries travel."""
|
|
706
773
|
|
|
707
774
|
root: Path
|
|
708
775
|
plugin_roots: tuple[Path, ...]
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Analysis coverage: which files and functions a run actually looked at.
|
|
2
2
|
|
|
3
3
|
"No findings" only means something when the reader knows what was analysed. Each file
|
|
4
|
-
is ``analysed``,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
is ``analysed``, ``ambiguous`` (analysed, but another file has its Python module name,
|
|
5
|
+
so a symbol of that name may denote a function of either and an import of it resolves
|
|
6
|
+
only from its own root), a ``syntax-error`` (the frontend rejected it) or ``unreadable``
|
|
7
|
+
(it could not be decoded); analysed and ambiguous files count their functions and how
|
|
8
|
+
many lowered. The templates the project names but the run could not read are listed,
|
|
9
|
+
each where the project names it: they may call any template filter.
|
|
8
10
|
"""
|
|
9
11
|
|
|
10
12
|
from __future__ import annotations
|
|
@@ -19,6 +19,7 @@ from coretrace_python.interprocedural.modulegraph import (
|
|
|
19
19
|
build_module_graph,
|
|
20
20
|
discover_files,
|
|
21
21
|
discover_sources,
|
|
22
|
+
name_modules,
|
|
22
23
|
project_symbol,
|
|
23
24
|
)
|
|
24
25
|
from coretrace_python.interprocedural.summaries import (
|
|
@@ -73,5 +74,6 @@ __all__ = [
|
|
|
73
74
|
"cleared_by",
|
|
74
75
|
"discover_files",
|
|
75
76
|
"discover_sources",
|
|
77
|
+
"name_modules",
|
|
76
78
|
"project_symbol",
|
|
77
79
|
]
|
|
@@ -132,12 +132,16 @@ class SignaturesAnalysis(Analysis[tuple[Signature, ...]]):
|
|
|
132
132
|
@dataclass(frozen=True)
|
|
133
133
|
class ModuleFunction:
|
|
134
134
|
"""One function of a module as a project plugin sees it: the name the call graph
|
|
135
|
-
gives it (``Class.method``, ``outer.inner``, ``<module>``), where it is,
|
|
136
|
-
of the entry point it is (``http`` for a route, ``argv`` for a command), if any
|
|
135
|
+
gives it (``Class.method``, ``outer.inner``, ``<module>``), where it is, the label
|
|
136
|
+
of the entry point it is (``http`` for a route, ``argv`` for a command), if any, and
|
|
137
|
+
the module-level ``aliases`` bound to it by assigning one name to another
|
|
138
|
+
(``main = actual``), transitively and in source order; a method or a nested
|
|
139
|
+
function has none."""
|
|
137
140
|
|
|
138
141
|
name: str
|
|
139
142
|
span: SourceSpan
|
|
140
143
|
entry_point: str | None = None
|
|
144
|
+
aliases: tuple[str, ...] = ()
|
|
141
145
|
|
|
142
146
|
|
|
143
147
|
class CallGraph:
|
|
@@ -6,12 +6,18 @@ each module imports. Functions defined in the project get project symbols
|
|
|
6
6
|
(``python.app.helpers.run``) whose summaries live in a ``SummaryIndex`` that the engine
|
|
7
7
|
provides to every module's manager, so calls into other files are analysed through
|
|
8
8
|
summaries rather than by retaining every module's PyIR.
|
|
9
|
+
|
|
10
|
+
The identity of a module is its file. Two files may have the same import name
|
|
11
|
+
(``a/app.py`` and ``z/app.py`` outside any package are both ``app``, each from its own
|
|
12
|
+
directory): the engine then names them by their path from the root, ``a.app`` and
|
|
13
|
+
``z.app``, and an import of ``app`` resolves within the importer's own root only.
|
|
9
14
|
"""
|
|
10
15
|
|
|
11
16
|
from __future__ import annotations
|
|
12
17
|
|
|
18
|
+
from collections import Counter
|
|
13
19
|
from collections.abc import Iterable, Mapping
|
|
14
|
-
from dataclasses import dataclass
|
|
20
|
+
from dataclasses import dataclass, field
|
|
15
21
|
from pathlib import Path
|
|
16
22
|
from types import MappingProxyType
|
|
17
23
|
|
|
@@ -67,6 +73,66 @@ def _is_environment(directory: Path, known: dict[Path, bool]) -> bool:
|
|
|
67
73
|
return known[directory]
|
|
68
74
|
|
|
69
75
|
|
|
76
|
+
def name_modules(root: Path, sources: Iterable[SourceFile]) -> dict[str, SourceFile]:
|
|
77
|
+
"""One name per file, in the order given: its import name when no other file has it;
|
|
78
|
+
otherwise its path from ``root`` as a dotted name (``a/app.py`` and ``z/app.py`` are
|
|
79
|
+
``a.app`` and ``z.app``, a root-level ``manage.py`` stays ``manage``), or that path
|
|
80
|
+
itself when even the dotted names coincide (``x.y/app.py`` and ``x/y/app.py``).
|
|
81
|
+
Decided over the whole set, so the names do not depend on the order the files are
|
|
82
|
+
found in."""
|
|
83
|
+
|
|
84
|
+
found = tuple(sources)
|
|
85
|
+
counts = Counter(source.module_name for source in found)
|
|
86
|
+
shared = [source for source in found if counts[source.module_name] > 1]
|
|
87
|
+
paths = {source.source_id: _relative(root, source) for source in shared}
|
|
88
|
+
dotted = {source_id: _dotted(path) for source_id, path in paths.items()}
|
|
89
|
+
taken = (
|
|
90
|
+
{name for name, count in counts.items() if count == 1}
|
|
91
|
+
| {name for name, count in Counter(dotted.values()).items() if count > 1}
|
|
92
|
+
| {path.as_posix() for path in paths.values()}
|
|
93
|
+
)
|
|
94
|
+
named: dict[str, SourceFile] = {}
|
|
95
|
+
for source in found:
|
|
96
|
+
if counts[source.module_name] == 1:
|
|
97
|
+
named[source.module_name] = source
|
|
98
|
+
elif dotted[source.source_id] not in taken:
|
|
99
|
+
named[dotted[source.source_id]] = source
|
|
100
|
+
else:
|
|
101
|
+
named[paths[source.source_id].as_posix()] = source
|
|
102
|
+
return named
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _relative(root: Path, source: SourceFile) -> Path:
|
|
106
|
+
path = source.path if source.path is not None else Path(str(source.source_id))
|
|
107
|
+
try:
|
|
108
|
+
return path.relative_to(root.resolve())
|
|
109
|
+
except ValueError:
|
|
110
|
+
return path
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _dotted(path: Path) -> str:
|
|
114
|
+
"""``a/app.py`` as ``a.app`` and ``pkg/__init__.py`` as ``pkg``; a root-level
|
|
115
|
+
``__init__.py`` has no dotted name and keeps its path."""
|
|
116
|
+
|
|
117
|
+
parts = path.with_suffix("").parts
|
|
118
|
+
if parts and parts[-1] == "__init__":
|
|
119
|
+
parts = parts[:-1]
|
|
120
|
+
return ".".join(parts) or path.as_posix()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _import_root(source: SourceFile) -> Path | None:
|
|
124
|
+
"""The directory an import of ``source`` by its module name starts from: the parent
|
|
125
|
+
of its top-level package, or its own directory for a module outside any package,
|
|
126
|
+
where ``module_name_for`` stopped. ``None`` for a source not on disk."""
|
|
127
|
+
|
|
128
|
+
if source.path is None:
|
|
129
|
+
return None
|
|
130
|
+
directory = source.path.parent
|
|
131
|
+
while (directory / "__init__.py").is_file():
|
|
132
|
+
directory = directory.parent
|
|
133
|
+
return directory
|
|
134
|
+
|
|
135
|
+
|
|
70
136
|
def project_symbol(module_name: str, qualified_name: str) -> SymbolId:
|
|
71
137
|
"""The canonical symbol of a project function. A module whose name is not an
|
|
72
138
|
identifier cannot be imported by that name, but its functions still need stable
|
|
@@ -90,6 +156,7 @@ def _component(part: str) -> str:
|
|
|
90
156
|
class ModuleGraph:
|
|
91
157
|
_sources: Mapping[str, SourceFile]
|
|
92
158
|
_imports: Mapping[str, frozenset[str]]
|
|
159
|
+
_unresolved: Mapping[str, frozenset[str]] = field(default_factory=dict)
|
|
93
160
|
|
|
94
161
|
@property
|
|
95
162
|
def modules(self) -> tuple[str, ...]:
|
|
@@ -104,6 +171,12 @@ class ModuleGraph:
|
|
|
104
171
|
def importers(self, name: str) -> frozenset[str]:
|
|
105
172
|
return frozenset(m for m, imported in self._imports.items() if name in imported)
|
|
106
173
|
|
|
174
|
+
def unresolved(self, name: str) -> frozenset[str]:
|
|
175
|
+
"""The import names ``name`` imports that several project files have and not
|
|
176
|
+
exactly one in its own root: no edge records them."""
|
|
177
|
+
|
|
178
|
+
return self._unresolved.get(name, frozenset())
|
|
179
|
+
|
|
107
180
|
def schedule(self) -> tuple[tuple[frozenset[str], ...], ...]:
|
|
108
181
|
"""Strongly connected components in waves: every component of a wave imports,
|
|
109
182
|
outside itself, only components of earlier waves, so one wave can be analysed
|
|
@@ -167,10 +240,17 @@ def build_module_graph(
|
|
|
167
240
|
modules: Mapping[str, nodes.Module],
|
|
168
241
|
imports: Mapping[str, ImportTable],
|
|
169
242
|
) -> ModuleGraph:
|
|
170
|
-
"""Edges from each module to the project modules it imports, in any scope.
|
|
171
|
-
|
|
172
|
-
|
|
243
|
+
"""Edges from each module to the project modules it imports, in any scope. An import
|
|
244
|
+
names a module by its import name; when several files have that name, it resolves to
|
|
245
|
+
the one in the importer's own root and, without exactly one there, stays unresolved:
|
|
246
|
+
no edge, and the name is recorded on the graph."""
|
|
247
|
+
|
|
248
|
+
by_name: dict[str, list[str]] = {}
|
|
249
|
+
for name, source in sources.items():
|
|
250
|
+
by_name.setdefault(source.module_name, []).append(name)
|
|
251
|
+
roots = {name: _import_root(source) for name, source in sources.items()}
|
|
173
252
|
edges: dict[str, frozenset[str]] = {}
|
|
253
|
+
unresolved: dict[str, frozenset[str]] = {}
|
|
174
254
|
for name, module in modules.items():
|
|
175
255
|
candidates: set[str] = set()
|
|
176
256
|
dotted_tops: set[str] = set()
|
|
@@ -187,11 +267,24 @@ def build_module_graph(
|
|
|
187
267
|
candidates.add(path)
|
|
188
268
|
# The most specific project module each import names; ``app.helpers`` implies
|
|
189
269
|
# the ``app`` package, which is not an edge worth recording.
|
|
190
|
-
found =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
270
|
+
found: set[str] = set()
|
|
271
|
+
missing: set[str] = set()
|
|
272
|
+
for candidate in candidates:
|
|
273
|
+
for prefix in _prefixes(candidate):
|
|
274
|
+
matching = by_name.get(prefix)
|
|
275
|
+
if matching is None:
|
|
276
|
+
continue
|
|
277
|
+
if len(matching) > 1:
|
|
278
|
+
matching = [m for m in matching if roots[m] == roots[name]]
|
|
279
|
+
if len(matching) == 1:
|
|
280
|
+
found.add(matching[0])
|
|
281
|
+
else:
|
|
282
|
+
missing.add(prefix)
|
|
283
|
+
break
|
|
284
|
+
edges[name] = frozenset(found - {name})
|
|
285
|
+
if missing:
|
|
286
|
+
unresolved[name] = frozenset(missing)
|
|
287
|
+
return ModuleGraph(MappingProxyType(dict(sources)), MappingProxyType(edges), MappingProxyType(unresolved))
|
|
195
288
|
|
|
196
289
|
|
|
197
290
|
def _prefixes(dotted: str) -> list[str]:
|
|
@@ -217,5 +310,6 @@ __all__ = [
|
|
|
217
310
|
"SummaryIndex",
|
|
218
311
|
"build_module_graph",
|
|
219
312
|
"discover_sources",
|
|
313
|
+
"name_modules",
|
|
220
314
|
"project_symbol",
|
|
221
315
|
]
|
|
@@ -113,8 +113,8 @@ class ScopeTable:
|
|
|
113
113
|
def children(self, scope_id: ScopeId) -> tuple[Scope, ...]:
|
|
114
114
|
return self._children[scope_id]
|
|
115
115
|
|
|
116
|
-
def scope_for(self, node: nodes.Function | nodes.Class | nodes.Comprehension) -> Scope:
|
|
117
|
-
"""Return the scope introduced by a definition or comprehension node."""
|
|
116
|
+
def scope_for(self, node: nodes.Function | nodes.Class | nodes.Lambda | nodes.Comprehension) -> Scope:
|
|
117
|
+
"""Return the scope introduced by a definition, lambda or comprehension node."""
|
|
118
118
|
|
|
119
119
|
return self._scopes[self._spans[node.span]]
|
|
120
120
|
|
coretrace_python/taint/engine.py
CHANGED
|
@@ -836,7 +836,9 @@ def function_entry_point(
|
|
|
836
836
|
) -> EntryPoint | None:
|
|
837
837
|
"""The entry point ``function`` is: one of its decorators or, for a method, one of its
|
|
838
838
|
class's bases matching an entry-point model, a registration elsewhere (``routes``), or
|
|
839
|
-
a model naming the function, or its class, by its own project symbol
|
|
839
|
+
a model naming the function, or its class, by its own project symbol or by a
|
|
840
|
+
module-level alias of it (``main = actual`` names ``actual`` as ``main``). A model
|
|
841
|
+
naming the method wins over one naming its class, whichever name it uses."""
|
|
840
842
|
|
|
841
843
|
owner = _owner(module, function)
|
|
842
844
|
enclosing_function = _enclosing(module, function)
|
|
@@ -844,12 +846,16 @@ def function_entry_point(
|
|
|
844
846
|
# ``app = Flask(__name__)`` inside ``create_app``: routes defined there resolve.
|
|
845
847
|
instances = {**(instances or {}), **local_instances(enclosing_function, scopes, symbols)}
|
|
846
848
|
entry = entry_point_of(function, models, scopes, symbols, owner, instances, project)
|
|
847
|
-
if entry is None:
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
849
|
+
if entry is not None:
|
|
850
|
+
return entry
|
|
851
|
+
defined = function if owner is None else owner
|
|
852
|
+
names = (defined.name, *_aliases(module, defined.name))
|
|
853
|
+
qualified = names if owner is None else (*(f"{name}.{function.name}" for name in names), *names)
|
|
854
|
+
for name in qualified:
|
|
855
|
+
entry = _declared(project_symbol(module.name, name), models, routes)
|
|
856
|
+
if entry is not None:
|
|
857
|
+
return entry
|
|
858
|
+
return None
|
|
853
859
|
|
|
854
860
|
|
|
855
861
|
def _declared(symbol: SymbolId, models: ModelTable, routes: Routes | None) -> EntryPoint | None:
|
|
@@ -860,6 +866,43 @@ def _declared(symbol: SymbolId, models: ModelTable, routes: Routes | None) -> En
|
|
|
860
866
|
return registered if registered is not None else models.entry_point(symbol)
|
|
861
867
|
|
|
862
868
|
|
|
869
|
+
def _aliases(module: nodes.Module, name: str) -> Iterable[str]:
|
|
870
|
+
"""The module-level names bound to ``name`` by assigning one name to another,
|
|
871
|
+
transitively and in source order: ``handler = actual`` then ``main = handler`` make
|
|
872
|
+
both aliases of ``actual``. A name bound inside a function or class, or to anything
|
|
873
|
+
but a name (a call, an import), is no alias."""
|
|
874
|
+
|
|
875
|
+
aliases: dict[str, list[str]] = {}
|
|
876
|
+
for statement in _module_level(module.body):
|
|
877
|
+
if isinstance(statement, nodes.Assign) and isinstance(statement.target, nodes.Name) and isinstance(statement.value, nodes.Name):
|
|
878
|
+
aliases.setdefault(statement.value.identifier, []).append(statement.target.identifier)
|
|
879
|
+
found = {name}
|
|
880
|
+
pending = [name]
|
|
881
|
+
while pending:
|
|
882
|
+
for alias in aliases.get(pending.pop(0), ()):
|
|
883
|
+
if alias not in found:
|
|
884
|
+
found.add(alias)
|
|
885
|
+
pending.append(alias)
|
|
886
|
+
yield alias
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _module_level(body: Iterable[nodes.Statement]) -> Iterable[nodes.Statement]:
|
|
890
|
+
"""The statements executed at module level: the body and what its compound
|
|
891
|
+
statements hold, not what a function or class defines."""
|
|
892
|
+
|
|
893
|
+
for statement in body:
|
|
894
|
+
if isinstance(statement, nodes.Function | nodes.Class):
|
|
895
|
+
continue
|
|
896
|
+
yield statement
|
|
897
|
+
for attribute in ("body", "orelse", "finalbody"):
|
|
898
|
+
nested = getattr(statement, attribute, None)
|
|
899
|
+
if isinstance(nested, tuple):
|
|
900
|
+
yield from _module_level(nested)
|
|
901
|
+
if isinstance(statement, nodes.Try):
|
|
902
|
+
for handler in statement.handlers:
|
|
903
|
+
yield from _module_level(handler.body)
|
|
904
|
+
|
|
905
|
+
|
|
863
906
|
def _owner(module: nodes.Module, function: nodes.Function) -> nodes.Class | None:
|
|
864
907
|
"""The top-level class ``function`` is a method of, if any."""
|
|
865
908
|
|
|
@@ -1109,7 +1152,9 @@ class EntryPointAnalysis(Analysis[tuple[ModuleFunction, ...]]):
|
|
|
1109
1152
|
functions: list[ModuleFunction] = []
|
|
1110
1153
|
for name, function in graph.definitions.items():
|
|
1111
1154
|
entry = function_entry_point(function, ctx.module, models, scopes, symbols, instances, routes, project)
|
|
1112
|
-
|
|
1155
|
+
label = entry.label if entry is not None else None
|
|
1156
|
+
# A dotted name, a method's or a nested function's, is bound by no assignment.
|
|
1157
|
+
functions.append(ModuleFunction(name, function.span, label, tuple(_aliases(ctx.module, name))))
|
|
1113
1158
|
return tuple(functions)
|
|
1114
1159
|
|
|
1115
1160
|
|
|
@@ -24,18 +24,30 @@ calls ``django.template.defaultfilters.striptags``. Every template found counts,
|
|
|
24
24
|
or not a Python call names it, since a class-based view renders its ``template_name``
|
|
25
25
|
inside Django. A template the engine cannot read may call any filter; the engine says
|
|
26
26
|
where the project names one.
|
|
27
|
+
|
|
28
|
+
A project tag library, a module of a ``templatetags`` directory, replaces a filter of
|
|
29
|
+
that name in every template loading it before applying the filter, as Django's parser
|
|
30
|
+
does: ``{% load custom %}{{ bio|striptags }}`` calls the project's ``striptags`` when
|
|
31
|
+
``custom.py`` registers one, a later load overrides an earlier one, and
|
|
32
|
+
``{% load striptags from custom %}`` selects. The engine reads what a library registers
|
|
33
|
+
on its ``register`` with the semantic layers, and where it cannot tell — no such library,
|
|
34
|
+
a file it cannot read or understand, several apps giving the name and disagreeing — the
|
|
35
|
+
filter is neither the built-in nor certainly replaced: no call is claimed, and the load
|
|
36
|
+
is listed among what the engine could not read. ``OPTIONS["builtins"]`` and
|
|
37
|
+
``OPTIONS["libraries"]`` of the settings are not read.
|
|
27
38
|
"""
|
|
28
39
|
|
|
29
40
|
from __future__ import annotations
|
|
30
41
|
|
|
31
42
|
import re
|
|
32
|
-
from collections.abc import Iterable, Iterator, Mapping
|
|
43
|
+
from collections.abc import Container, Iterable, Iterator, Mapping
|
|
33
44
|
from dataclasses import dataclass, field
|
|
34
45
|
from pathlib import Path
|
|
35
46
|
from types import MappingProxyType
|
|
36
47
|
from typing import ClassVar
|
|
37
48
|
|
|
38
49
|
from coretrace_python.analysis import Analysis, AnalysisContext
|
|
50
|
+
from coretrace_python.frontend import HIRBuildError, ParseError, build_hir
|
|
39
51
|
from coretrace_python.hir import nodes
|
|
40
52
|
from coretrace_python.hir.visitors import Node, children
|
|
41
53
|
from coretrace_python.interprocedural import (
|
|
@@ -44,11 +56,21 @@ from coretrace_python.interprocedural import (
|
|
|
44
56
|
TemplateFilter,
|
|
45
57
|
discover_files,
|
|
46
58
|
)
|
|
47
|
-
from coretrace_python.semantic.
|
|
48
|
-
from coretrace_python.
|
|
59
|
+
from coretrace_python.semantic.imports import ImportResolutionError, analyze_imports
|
|
60
|
+
from coretrace_python.semantic.scopes import (
|
|
61
|
+
BindingKind,
|
|
62
|
+
ResolutionKind,
|
|
63
|
+
ScopeError,
|
|
64
|
+
ScopeId,
|
|
65
|
+
ScopeTable,
|
|
66
|
+
analyze_scopes,
|
|
67
|
+
)
|
|
68
|
+
from coretrace_python.semantic.symbols import SymbolId, analyze_symbols
|
|
69
|
+
from coretrace_python.source import SourceId, SourceManager, SourceSpan
|
|
49
70
|
from coretrace_python.taint.models import ModelTable
|
|
50
71
|
|
|
51
72
|
TEMPLATES_DIRECTORY = "templates"
|
|
73
|
+
TEMPLATETAGS_DIRECTORY = "templatetags"
|
|
52
74
|
|
|
53
75
|
_OUTPUT, _TAG, _URL = "\x00", "\x01", "/u"
|
|
54
76
|
_SYNTAX = re.compile(r"{{(.*?)}}|{%(.*?)%}|{#.*?#}", re.DOTALL)
|
|
@@ -102,6 +124,10 @@ FILTER_FUNCTIONS: Mapping[str, SymbolId] = MappingProxyType(
|
|
|
102
124
|
# Every filter but ``safe`` and ``safeseq`` escapes its input, or returns it for
|
|
103
125
|
# autoescaping to escape, when autoescaping is on.
|
|
104
126
|
_FILTERS = frozenset(FILTER_FUNCTIONS) - {"safe", "safeseq"}
|
|
127
|
+
# The class a project tag library binds ``register`` to, by either of its paths, and
|
|
128
|
+
# its methods registering tags, which register no filter.
|
|
129
|
+
_LIBRARY_CLASSES = frozenset(SymbolId(f"python.django.template.{p}") for p in ("Library", "library.Library"))
|
|
130
|
+
_TAG_REGISTRARS = frozenset({"tag", "tag_function", "simple_tag", "simple_block_tag", "inclusion_tag"})
|
|
105
131
|
|
|
106
132
|
_URL_ATTRIBUTES = frozenset(
|
|
107
133
|
{
|
|
@@ -131,6 +157,8 @@ _BINDING = re.compile(r"(\w+)=")
|
|
|
131
157
|
# the request context processor adds gives (``request.GET``).
|
|
132
158
|
_VARIABLE = re.compile(r"([A-Za-z_]\w*)(?:\.(\w+))?")
|
|
133
159
|
_LITERALS = frozenset({"True", "False", "None"})
|
|
160
|
+
# The variable a block renders to keep the parent's content, as ``_chain`` reads it.
|
|
161
|
+
_BLOCK_SUPER = "block.super"
|
|
134
162
|
|
|
135
163
|
REQUEST_PROCESSOR = "django.template.context_processors.request"
|
|
136
164
|
_DJANGO_ENGINE = "django.template.backends.django.DjangoTemplates"
|
|
@@ -314,10 +342,11 @@ class FilterCall:
|
|
|
314
342
|
@dataclass(frozen=True)
|
|
315
343
|
class ProjectTemplates:
|
|
316
344
|
"""The templates found under the project's ``templates`` directories: their
|
|
317
|
-
``names``, as a render call names them; the filters
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
``
|
|
345
|
+
``names``, as a render call names them; the filters of Django's own libraries they
|
|
346
|
+
apply, as ``calls``, unless a project library they load replaces the filter; where
|
|
347
|
+
they name a template the engine cannot read, or load a library it cannot read while
|
|
348
|
+
applying a filter of that name, as ``unread``; and, by name, the filter calls
|
|
349
|
+
rendering each template makes with the context keys reaching them, as ``filters``."""
|
|
321
350
|
|
|
322
351
|
names: frozenset[str] = frozenset()
|
|
323
352
|
calls: tuple[FilterCall, ...] = ()
|
|
@@ -363,13 +392,44 @@ class _Reference:
|
|
|
363
392
|
return frozenset(found)
|
|
364
393
|
|
|
365
394
|
|
|
395
|
+
@dataclass(frozen=True)
|
|
396
|
+
class _Load:
|
|
397
|
+
"""A ``{% load %}`` of ``library`` at ``offset``, of the ``names`` it selects
|
|
398
|
+
(``{% load a b from lib %}``), None for the whole library."""
|
|
399
|
+
|
|
400
|
+
library: str
|
|
401
|
+
names: frozenset[str] | None
|
|
402
|
+
offset: int
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@dataclass(frozen=True)
|
|
406
|
+
class _Library:
|
|
407
|
+
"""What the project's tag library files of one name register: the filter names they
|
|
408
|
+
surely register, ``certain``, and those they may register, ``uncertain`` — None when
|
|
409
|
+
they may register anything, as a file the engine cannot read or understand may."""
|
|
410
|
+
|
|
411
|
+
certain: frozenset[str] = frozenset()
|
|
412
|
+
uncertain: frozenset[str] | None = None
|
|
413
|
+
|
|
414
|
+
def overrides(self, name: str) -> bool | None:
|
|
415
|
+
"""Whether loading the library replaces the filter ``name``; None when it may."""
|
|
416
|
+
|
|
417
|
+
if name in self.certain:
|
|
418
|
+
return True
|
|
419
|
+
return None if self.uncertain is None or name in self.uncertain else False
|
|
420
|
+
|
|
421
|
+
|
|
366
422
|
@dataclass(frozen=True)
|
|
367
423
|
class _Template:
|
|
368
|
-
"""What one template file applies and references,
|
|
424
|
+
"""What one template file applies and references, the ``blocks`` it defines, and
|
|
425
|
+
among them the ``extended`` ones, which render ``{{ block.super }}``, and the loads
|
|
426
|
+
under which it applies a filter the engine could not resolve."""
|
|
369
427
|
|
|
370
428
|
applications: tuple[_Application, ...]
|
|
371
429
|
references: tuple[_Reference, ...]
|
|
372
430
|
blocks: frozenset[str]
|
|
431
|
+
extended: frozenset[str] = frozenset()
|
|
432
|
+
unresolved: tuple[_Load, ...] = ()
|
|
373
433
|
|
|
374
434
|
def rendered(self, blocks: tuple[str, ...], overridden: frozenset[str]) -> bool:
|
|
375
435
|
"""Whether what stands in ``blocks`` renders when a template extending this one
|
|
@@ -379,15 +439,25 @@ class _Template:
|
|
|
379
439
|
extends = any(reference.extends for reference in self.references)
|
|
380
440
|
return not overridden.intersection(blocks) and (bool(blocks) or not extends)
|
|
381
441
|
|
|
442
|
+
@property
|
|
443
|
+
def replaced(self) -> frozenset[str]:
|
|
444
|
+
"""The blocks this template replaces in what it extends: those it defines
|
|
445
|
+
without rendering ``{{ block.super }}``, which keeps the parent's content."""
|
|
446
|
+
|
|
447
|
+
return self.blocks - self.extended
|
|
448
|
+
|
|
382
449
|
|
|
383
450
|
def project_templates(root: Path) -> ProjectTemplates:
|
|
384
451
|
"""What the templates under ``root`` call, and the templates they include or extend
|
|
385
452
|
that the engine cannot read: named by an expression, or by a name found under no
|
|
386
|
-
``templates`` directory. A template file that cannot be read is unread too
|
|
453
|
+
``templates`` directory. A template file that cannot be read is unread too, and so
|
|
454
|
+
is a template loading a library the engine cannot read, or find, or finds in several
|
|
455
|
+
apps disagreeing, where it then applies a filter of that library's name. A name
|
|
387
456
|
found in several files gives no ``filters``: which one renders depends on the
|
|
388
457
|
loaders."""
|
|
389
458
|
|
|
390
459
|
found = {path: tuple(_names(path.relative_to(root).parts)) for path in discover_files(root)}
|
|
460
|
+
libraries = _libraries(found)
|
|
391
461
|
templates = sorted(path for path, names in found.items() if names)
|
|
392
462
|
files: dict[str, list[Path]] = {}
|
|
393
463
|
for path in templates:
|
|
@@ -401,13 +471,17 @@ def project_templates(root: Path) -> ProjectTemplates:
|
|
|
401
471
|
if text is None:
|
|
402
472
|
unread.append(f"{relative}, unreadable")
|
|
403
473
|
continue
|
|
404
|
-
read[path] = _read_scopes(text, SourceId(str(path)))
|
|
474
|
+
read[path] = _read_scopes(text, SourceId(str(path)), libraries)
|
|
405
475
|
for reference in read[path].references:
|
|
406
476
|
where = f"{relative}:{_position(text, reference.offset)[0]}"
|
|
407
477
|
if reference.name is None:
|
|
408
478
|
unread.append(f"{where} names a template by an expression")
|
|
409
479
|
elif reference.name not in files:
|
|
410
480
|
unread.append(f"{where} names {reference.name!r}, not found")
|
|
481
|
+
for load in read[path].unresolved:
|
|
482
|
+
library = libraries.get(load.library)
|
|
483
|
+
reason = "not found" if library is None else "not read" if library.uncertain is None else "found in several files"
|
|
484
|
+
unread.append(f"{relative}:{_position(text, load.offset)[0]} loads {load.library!r}, {reason}")
|
|
411
485
|
calls = tuple(FilterCall(a.symbol, a.span) for template in read.values() for a in template.applications)
|
|
412
486
|
filters = {
|
|
413
487
|
name: tuple(
|
|
@@ -444,7 +518,7 @@ def _reaching(
|
|
|
444
518
|
if reference.name is None:
|
|
445
519
|
continue
|
|
446
520
|
if reference.extends:
|
|
447
|
-
found = _reaching(reference.name, files, read, visiting | {name}, overridden | template.
|
|
521
|
+
found = _reaching(reference.name, files, read, visiting | {name}, overridden | template.replaced)
|
|
448
522
|
elif template.rendered(reference.blocks, overridden):
|
|
449
523
|
found = _reaching(reference.name, files, read, visiting | {name})
|
|
450
524
|
else:
|
|
@@ -453,18 +527,23 @@ def _reaching(
|
|
|
453
527
|
yield symbol, span, where, reference.maps(value), reference.maps(argument)
|
|
454
528
|
|
|
455
529
|
|
|
456
|
-
def _read_scopes(text: str, source: SourceId) -> _Template:
|
|
530
|
+
def _read_scopes(text: str, source: SourceId, libraries: Mapping[str, _Library]) -> _Template:
|
|
457
531
|
"""The filters a template applies and the templates it includes or extends, with the
|
|
458
532
|
names free in it that reach each: through ``{% for %}``, ``{% with %}`` and the
|
|
459
533
|
``as`` of other tags, which bind a name until their block ends, the arguments of
|
|
460
|
-
earlier filters, and the output of ``{% filter %}`` blocks.
|
|
534
|
+
earlier filters, and the output of ``{% filter %}`` blocks. A filter is the one of
|
|
535
|
+
Django's own libraries unless a project library, among ``libraries``, loaded before
|
|
536
|
+
it in the file replaces it, as Django's parser resolves filters."""
|
|
461
537
|
|
|
462
538
|
applications: list[_Application] = []
|
|
463
539
|
references: list[_Reference] = []
|
|
464
540
|
opened: list[str] = []
|
|
465
541
|
defined: set[str] = set()
|
|
542
|
+
extended: set[str] = set()
|
|
466
543
|
scopes: list[dict[str, frozenset[str]]] = [{}]
|
|
467
544
|
blocks: list[tuple[list[tuple[str, int, frozenset[str]]], set[str]]] = []
|
|
545
|
+
loaded: list[_Load] = []
|
|
546
|
+
unresolved: dict[_Load, None] = {}
|
|
468
547
|
|
|
469
548
|
def free(names: frozenset[str]) -> frozenset[str]:
|
|
470
549
|
found: set[str] = set()
|
|
@@ -474,14 +553,36 @@ def _read_scopes(text: str, source: SourceId) -> _Template:
|
|
|
474
553
|
found |= {name} if bound is None else _through(bound, name)
|
|
475
554
|
return frozenset(found)
|
|
476
555
|
|
|
556
|
+
def behind(name: str) -> SymbolId | _Load | None:
|
|
557
|
+
"""The function behind the filter ``name`` applied here: the one of Django's own
|
|
558
|
+
libraries, unless the latest load so far of a library registering ``name``
|
|
559
|
+
replaces it (None), or may (that load). Django's own libraries register no name
|
|
560
|
+
of another, so their loads change nothing."""
|
|
561
|
+
|
|
562
|
+
builtin = FILTER_FUNCTIONS.get(name)
|
|
563
|
+
if builtin is None:
|
|
564
|
+
return None
|
|
565
|
+
for load in reversed(loaded):
|
|
566
|
+
if (load.names is not None and name not in load.names) or load.library in _LIBRARIES:
|
|
567
|
+
continue
|
|
568
|
+
library = libraries.get(load.library)
|
|
569
|
+
verdict = library.overrides(name) if library is not None else None
|
|
570
|
+
if verdict is None:
|
|
571
|
+
return load
|
|
572
|
+
if verdict:
|
|
573
|
+
return None
|
|
574
|
+
return builtin
|
|
575
|
+
|
|
477
576
|
def apply(head: frozenset[str], chain: list[tuple[str, int, frozenset[str]]]) -> frozenset[str]:
|
|
478
577
|
"""Record the filters of ``chain`` applied to a value the free names ``head``
|
|
479
578
|
reach; the free names reaching the value of the whole expression."""
|
|
480
579
|
|
|
481
580
|
value = head
|
|
482
581
|
for name, offset, argument in chain:
|
|
483
|
-
symbol =
|
|
484
|
-
if symbol
|
|
582
|
+
symbol = behind(name)
|
|
583
|
+
if isinstance(symbol, _Load):
|
|
584
|
+
unresolved[symbol] = None
|
|
585
|
+
elif symbol is not None:
|
|
485
586
|
line, column = _position(text, offset)
|
|
486
587
|
span = SourceSpan(source, line, column)
|
|
487
588
|
applications.append(_Application(symbol, span, value, free(argument), tuple(opened)))
|
|
@@ -491,6 +592,8 @@ def _read_scopes(text: str, source: SourceId) -> _Template:
|
|
|
491
592
|
for offset, expression, tag in _expressions(text):
|
|
492
593
|
if not tag:
|
|
493
594
|
head, chain = _chain(expression, offset)
|
|
595
|
+
if opened and _BLOCK_SUPER in head:
|
|
596
|
+
extended.add(opened[-1])
|
|
494
597
|
printed = apply(free(head), chain)
|
|
495
598
|
for _, collected in blocks:
|
|
496
599
|
collected |= printed
|
|
@@ -543,10 +646,145 @@ def _read_scopes(text: str, source: SourceId) -> _Template:
|
|
|
543
646
|
else:
|
|
544
647
|
context = {k: v for scope in scopes for k, v in scope.items()}
|
|
545
648
|
references.append(_Reference(named, offset, bindings, "only" in words, context, tuple(opened)))
|
|
649
|
+
elif name == "load":
|
|
650
|
+
selected = frozenset(words[1 : words.index("from")]) if "from" in words else None
|
|
651
|
+
loaded.extend(_Load(library, selected, offset) for library in _loaded(words[1:]))
|
|
546
652
|
elif "as" in words[1:-1]:
|
|
547
653
|
target = words[words.index("as", 1) + 1]
|
|
548
654
|
scopes[-1][target] = values.get(words[1], frozenset()) if name == "regroup" else frozenset()
|
|
549
|
-
return _Template(
|
|
655
|
+
return _Template(
|
|
656
|
+
tuple(applications), tuple(references), frozenset(defined), frozenset(extended), tuple(unresolved)
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _libraries(paths: Iterable[Path]) -> dict[str, _Library]:
|
|
661
|
+
"""The project's tag libraries among ``paths``, by the name ``{% load %}`` gives
|
|
662
|
+
them: the modules of the ``templatetags`` directories. Of several files of one name,
|
|
663
|
+
in different apps, only what every file registers is certain, and one the engine
|
|
664
|
+
cannot read leaves everything uncertain."""
|
|
665
|
+
|
|
666
|
+
readings: dict[str, list[frozenset[str] | None]] = {}
|
|
667
|
+
for path in paths:
|
|
668
|
+
if path.suffix == ".py" and path.parent.name == TEMPLATETAGS_DIRECTORY and path.stem != "__init__":
|
|
669
|
+
readings.setdefault(path.stem, []).append(_registered_filters(path))
|
|
670
|
+
libraries: dict[str, _Library] = {}
|
|
671
|
+
for name, found in readings.items():
|
|
672
|
+
read = [names for names in found if names is not None]
|
|
673
|
+
if len(read) < len(found):
|
|
674
|
+
libraries[name] = _Library()
|
|
675
|
+
else:
|
|
676
|
+
certain = read[0].intersection(*read[1:])
|
|
677
|
+
libraries[name] = _Library(certain, read[0].union(*read[1:]) - certain)
|
|
678
|
+
return libraries
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _registered_filters(path: Path) -> frozenset[str] | None:
|
|
682
|
+
"""The names of the filters the tag library module ``path`` registers, or None when
|
|
683
|
+
the engine cannot tell them all: the file cannot be read or parsed, ``register`` is
|
|
684
|
+
not bound to a ``Library`` at module level, a registration's name is not written as
|
|
685
|
+
a constant, or the library is filled or passed around in a way the engine does not
|
|
686
|
+
read. The ``Library`` is known by its symbol, whatever the import spelling, and a
|
|
687
|
+
registration under ``if``, ``try`` or a function counts."""
|
|
688
|
+
|
|
689
|
+
try:
|
|
690
|
+
module = build_hir(SourceManager().load_file(path))
|
|
691
|
+
scopes = analyze_scopes(module)
|
|
692
|
+
symbols = analyze_symbols(scopes, analyze_imports(module, scopes), module)
|
|
693
|
+
except (OSError, UnicodeDecodeError, ParseError, HIRBuildError, ScopeError, ImportResolutionError):
|
|
694
|
+
return None
|
|
695
|
+
scope = scopes.module_scope.id
|
|
696
|
+
if symbols.resolve(scope, "register") not in _LIBRARY_CLASSES:
|
|
697
|
+
return None
|
|
698
|
+
functions = {name for name, bound in scopes.module_scope.bindings.items() if bound.kind is BindingKind.FUNCTION}
|
|
699
|
+
registered: set[str] = set()
|
|
700
|
+
|
|
701
|
+
def record(name: str | None) -> bool:
|
|
702
|
+
if name is not None:
|
|
703
|
+
registered.add(name)
|
|
704
|
+
return name is not None
|
|
705
|
+
|
|
706
|
+
def visit(node: Node) -> bool:
|
|
707
|
+
"""Read the registrations under ``node``; False when one cannot be read."""
|
|
708
|
+
|
|
709
|
+
if isinstance(node, nodes.Function):
|
|
710
|
+
for decorator in node.decorators:
|
|
711
|
+
if _library_attribute(symbols.resolve_expression(scope, decorator)) == "filter":
|
|
712
|
+
named = _decorated(decorator, node.name) if isinstance(decorator, nodes.Call) else node.name
|
|
713
|
+
if not record(named):
|
|
714
|
+
return False
|
|
715
|
+
elif not visit(decorator):
|
|
716
|
+
return False
|
|
717
|
+
return all(map(visit, (*node.parameters, *node.body)))
|
|
718
|
+
if isinstance(node, nodes.Call) and isinstance(node.callee, nodes.Name | nodes.Attribute):
|
|
719
|
+
callee = symbols.resolve_expression(scope, node.callee)
|
|
720
|
+
if callee in _LIBRARY_CLASSES:
|
|
721
|
+
return not (node.arguments or node.keywords) # ``Library(filters=...)`` fills it unseen
|
|
722
|
+
if _library_attribute(callee) == "filter":
|
|
723
|
+
return record(_registered(node, functions)) and all(map(visit, (*node.arguments, *node.keywords)))
|
|
724
|
+
if isinstance(node, nodes.Name | nodes.Attribute):
|
|
725
|
+
symbol = symbols.resolve_expression(scope, node)
|
|
726
|
+
if _library_attribute(symbol) in _TAG_REGISTRARS:
|
|
727
|
+
return True
|
|
728
|
+
if symbol in _LIBRARY_CLASSES or _library_attribute(symbol) is not None:
|
|
729
|
+
return False # the library, or its filters, in hands the engine does not follow
|
|
730
|
+
if isinstance(node, nodes.Assign) and isinstance(node.target, nodes.Name):
|
|
731
|
+
return visit(node.value)
|
|
732
|
+
return all(map(visit, children(node)))
|
|
733
|
+
|
|
734
|
+
return frozenset(registered) if all(map(visit, module.body)) else None
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _library_attribute(symbol: SymbolId | None) -> str | None:
|
|
738
|
+
"""The attribute of a ``Library`` that ``symbol`` names, ``filter`` for
|
|
739
|
+
``register.filter``; None for any other symbol."""
|
|
740
|
+
|
|
741
|
+
if symbol is None:
|
|
742
|
+
return None
|
|
743
|
+
owner, _, name = symbol.canonical_name.rpartition(".")
|
|
744
|
+
return name if "." in owner and SymbolId(owner) in _LIBRARY_CLASSES else None
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _decorated(call: nodes.Call, function: str) -> str | None:
|
|
748
|
+
"""What ``@register.filter(...)`` registers ``function`` as: the constant ``name`` it
|
|
749
|
+
gives, or ``function`` itself when it gives only flags; None when the engine cannot
|
|
750
|
+
tell."""
|
|
751
|
+
|
|
752
|
+
given = _arguments(call, "name")
|
|
753
|
+
if given is None:
|
|
754
|
+
return None
|
|
755
|
+
return function if given[0] is None else _string(given[0])
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _registered(call: nodes.Call, functions: Container[str]) -> str | None:
|
|
759
|
+
"""What ``register.filter(...)`` registers when called as a statement: the constant
|
|
760
|
+
name of ``register.filter("x", fn)`` and ``register.filter(name="x", filter_func=fn)``,
|
|
761
|
+
the function's own name for ``register.filter(fn)`` when the module defines
|
|
762
|
+
``fn``; None when the engine cannot tell."""
|
|
763
|
+
|
|
764
|
+
given = _arguments(call, "name", "filter_func")
|
|
765
|
+
if given is None:
|
|
766
|
+
return None
|
|
767
|
+
name, function = given
|
|
768
|
+
if function is None:
|
|
769
|
+
return name.identifier if isinstance(name, nodes.Name) and name.identifier in functions else None
|
|
770
|
+
return _string(name)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _arguments(call: nodes.Call, *parameters: str) -> list[nodes.Expression | None] | None:
|
|
774
|
+
"""The argument ``call`` gives each of ``parameters``, by position or keyword; None
|
|
775
|
+
when it unpacks any, which could give them too."""
|
|
776
|
+
|
|
777
|
+
if any(isinstance(a, nodes.Starred) for a in call.arguments) or any(k.name is None for k in call.keywords):
|
|
778
|
+
return None
|
|
779
|
+
keywords = {keyword.name: keyword.value for keyword in call.keywords}
|
|
780
|
+
return [
|
|
781
|
+
call.arguments[position] if position < len(call.arguments) else keywords.get(name)
|
|
782
|
+
for position, name in enumerate(parameters)
|
|
783
|
+
]
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _string(expression: nodes.Expression | None) -> str | None:
|
|
787
|
+
return expression.value if isinstance(expression, nodes.Constant) and isinstance(expression.value, str) else None
|
|
550
788
|
|
|
551
789
|
|
|
552
790
|
def _chain(expression: str, offset: int) -> tuple[frozenset[str], list[tuple[str, int, frozenset[str]]]]:
|
|
@@ -578,29 +816,41 @@ def _through(bound: frozenset[str], name: str) -> frozenset[str]:
|
|
|
578
816
|
return frozenset(".".join(f"{b}.{attribute}".split(".")[:2]) if attribute else b for b in bound)
|
|
579
817
|
|
|
580
818
|
|
|
581
|
-
def request_processor(modules: Iterable[nodes.Module]) -> bool:
|
|
819
|
+
def request_processor(modules: Iterable[tuple[nodes.Module, ScopeTable]]) -> bool:
|
|
582
820
|
"""Whether every Django template engine the project's settings configure runs the
|
|
583
|
-
``request`` context processor, for certain: each assignment to
|
|
584
|
-
literal list of literal engines, at least one of them
|
|
585
|
-
of those lists the processor in a literal
|
|
586
|
-
|
|
587
|
-
|
|
821
|
+
``request`` context processor, for certain: each module-level assignment to
|
|
822
|
+
``TEMPLATES`` is a literal list of literal engines, at least one of them
|
|
823
|
+
``DjangoTemplates``, and each of those lists the processor in a literal
|
|
824
|
+
``context_processors``; no other code names the module's ``TEMPLATES``, or a
|
|
825
|
+
``TEMPLATES`` it does not bind (a star import), which could change it. A
|
|
826
|
+
``TEMPLATES`` bound in a function or a class is another name, which configures
|
|
827
|
+
nothing and changes nothing. A mere mention of the processor proves nothing."""
|
|
588
828
|
|
|
589
829
|
assignments = mentions = 0
|
|
590
|
-
for module in modules:
|
|
591
|
-
|
|
592
|
-
while stack:
|
|
593
|
-
node = stack.pop()
|
|
594
|
-
stack.extend(children(node))
|
|
830
|
+
for module, scopes in modules:
|
|
831
|
+
for node, scope in _scoped(module, scopes):
|
|
595
832
|
if isinstance(node, nodes.Name) and node.identifier == _ENGINES_SETTING:
|
|
596
|
-
|
|
597
|
-
|
|
833
|
+
if scopes.resolve(scope, node.identifier).kind in (ResolutionKind.GLOBAL, ResolutionKind.UNBOUND):
|
|
834
|
+
mentions += 1
|
|
835
|
+
elif isinstance(node, nodes.Assign) and _names_engines(node.target) and scope == scopes.module_scope.id:
|
|
598
836
|
if not _runs_request_processor(node.value):
|
|
599
837
|
return False
|
|
600
838
|
assignments += 1
|
|
601
839
|
return assignments > 0 and mentions == assignments
|
|
602
840
|
|
|
603
841
|
|
|
842
|
+
def _scoped(module: nodes.Module, scopes: ScopeTable) -> Iterator[tuple[Node, ScopeId]]:
|
|
843
|
+
"""Every node of ``module`` with the scope it stands in."""
|
|
844
|
+
|
|
845
|
+
stack: list[tuple[Node, ScopeId]] = [(node, scopes.module_scope.id) for node in module.body]
|
|
846
|
+
while stack:
|
|
847
|
+
node, scope = stack.pop()
|
|
848
|
+
yield node, scope
|
|
849
|
+
if isinstance(node, nodes.Function | nodes.Lambda | nodes.Class | nodes.Comprehension):
|
|
850
|
+
scope = scopes.scope_for(node).id
|
|
851
|
+
stack.extend((child, scope) for child in children(node))
|
|
852
|
+
|
|
853
|
+
|
|
604
854
|
def _names_engines(target: nodes.Expression) -> bool:
|
|
605
855
|
return isinstance(target, nodes.Name) and target.identifier == _ENGINES_SETTING
|
|
606
856
|
|
{coretrace_python_analyzer-0.18.0.dist-info → coretrace_python_analyzer-0.19.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: coretrace-python-analyzer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.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
|
{coretrace_python_analyzer-0.18.0.dist-info → coretrace_python_analyzer-0.19.0.dist-info}/RECORD
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
coretrace_python/__init__.py,sha256=
|
|
1
|
+
coretrace_python/__init__.py,sha256=FWsRdqY0ZoJRj8rA--2DntUA3GU1JsrZdyrjQIAq5wE,76
|
|
2
2
|
coretrace_python/__main__.py,sha256=V8DnYtRboISK2VCKm7DGHLREKvqLbqApyWlKBOs26wU,65
|
|
3
|
-
coretrace_python/cache.py,sha256
|
|
3
|
+
coretrace_python/cache.py,sha256=XnUC677v8VQvlmLA_GQO376isHn6ZdSOOA0dRua0Foo,13716
|
|
4
4
|
coretrace_python/cli.py,sha256=G5jhb6g6RgSRSjjS7DBgziIjUM1vTFwalJGvFHVKxrg,11225
|
|
5
|
-
coretrace_python/engine.py,sha256=
|
|
5
|
+
coretrace_python/engine.py,sha256=i_YckOVHZa_Mcwxgi2jsMkw8O49zHFQZ313mA_Xtl-4,37643
|
|
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
|
|
@@ -102,7 +102,7 @@ coretrace_python/dependency/sbom.py,sha256=rzJ-t0Mwoghej8nk0N63Dpjr_wYXm6JzuOPXj
|
|
|
102
102
|
coretrace_python/dependency/vex.py,sha256=bafWyziWWQP-Fjw1bvyOcOPsdFMwKy-sjMGWL7IPY3w,8048
|
|
103
103
|
coretrace_python/findings/__init__.py,sha256=yFKXSFEsAnFSxF1E_ptF2OH4YlUcCaSeIlWSvqMgaHo,500
|
|
104
104
|
coretrace_python/findings/baseline.py,sha256=fbGQuULRsCjhxSNdwuLfwsvql9ZUfF3jqCXrRmIgOoQ,3468
|
|
105
|
-
coretrace_python/findings/coverage.py,sha256=
|
|
105
|
+
coretrace_python/findings/coverage.py,sha256=LT74vTEjnt_kAgWBEt60rM0sB6mFAyIVzhta0WNUkxE,1580
|
|
106
106
|
coretrace_python/findings/model.py,sha256=zRK1FDuLgiVY4ch6H_AN9rDxC1orNW4wc8YN3cfVFfc,1424
|
|
107
107
|
coretrace_python/findings/provenance.py,sha256=vP3nbyDPMlRlxVcVlurF0mSGNuxO3B7kzDDDd_BVws0,838
|
|
108
108
|
coretrace_python/findings/refutation.py,sha256=UeYCz70LwyL8AMimhs3ys9cH0j1_YbdHVu7y3VvmmUg,22026
|
|
@@ -113,9 +113,9 @@ coretrace_python/frontend/parser.py,sha256=6-CG7rWxUYeOGeDP206SJWy4BMQUP9Mr6Jf-t
|
|
|
113
113
|
coretrace_python/hir/__init__.py,sha256=xMpvj-RSp6bd78a_hhkqbe99-snjERkH-_Lv5o9ASbw,180
|
|
114
114
|
coretrace_python/hir/nodes.py,sha256=4BI0f3UhKbnz9ZIxMoKXM7ujav0BmRpD0OXaoblRxIk,12364
|
|
115
115
|
coretrace_python/hir/visitors.py,sha256=JFnX09hK6trLyRcWgZ5ehOG2ywX3VvkqQ1h82nahYNg,885
|
|
116
|
-
coretrace_python/interprocedural/__init__.py,sha256=
|
|
117
|
-
coretrace_python/interprocedural/callgraph.py,sha256=
|
|
118
|
-
coretrace_python/interprocedural/modulegraph.py,sha256=
|
|
116
|
+
coretrace_python/interprocedural/__init__.py,sha256=q8u1eIpM55wqVtn46cxuymOn5D5DWU7K35i8Nzzbc3w,1604
|
|
117
|
+
coretrace_python/interprocedural/callgraph.py,sha256=EEG8kh9yyFPZiiFcouR8wuIwicClSKHu-z7nLh1kKOQ,22163
|
|
118
|
+
coretrace_python/interprocedural/modulegraph.py,sha256=xKlO_L2mcZi0RoG9h2ulacYecqUhM5BG1RrdC6_HUk8,13036
|
|
119
119
|
coretrace_python/interprocedural/summaries.py,sha256=6lybEynjtptZGGBw_3WKXx9ZZZqN96xL-r-tzo8fbMI,33862
|
|
120
120
|
coretrace_python/ir/__init__.py,sha256=ShBPYYqoQFePP-mgBjGezQ2acel0-ReOHF9vCC-G_4Q,232
|
|
121
121
|
coretrace_python/ir/defuse.py,sha256=CZEPnlgntRp4MqwR7kLldd2ZlIlBnr-V9VJAxXxwm0c,2616
|
|
@@ -138,20 +138,20 @@ coretrace_python/reporters/text.py,sha256=bHsHkMheyL0AB1iWtxBO5H9S14k1AqJvKkNsFr
|
|
|
138
138
|
coretrace_python/semantic/__init__.py,sha256=tJOvNvMzJ9yjP2bgkLpPO29pl9gMQgICh6XVYJHrovw,464
|
|
139
139
|
coretrace_python/semantic/identity.py,sha256=uQBdqEXrOtYmGIiR9hZu0Pimv52iW1rOOOvIdG0InNY,1472
|
|
140
140
|
coretrace_python/semantic/imports.py,sha256=389MZs8iTUc3CyUHG0VAgWFsypd7VPFKkLHmFz9L-N0,5673
|
|
141
|
-
coretrace_python/semantic/scopes.py,sha256
|
|
141
|
+
coretrace_python/semantic/scopes.py,sha256=-ZP88ppAjSBUPB5TvsznuP8YmCXU-LzSq6acq8iM95w,20264
|
|
142
142
|
coretrace_python/semantic/symbols.py,sha256=G0jwsWFo2346Y1R2sztsB8HuZZm_aHfHSJXXeZKPQLo,7354
|
|
143
143
|
coretrace_python/source/__init__.py,sha256=0Y8fvtbkETnNCJR9Zq7RS5TSgF9c22pz3SSk1KdAXF0,301
|
|
144
144
|
coretrace_python/source/manager.py,sha256=JRmMOcWfS1nocbVrdcvfQTqEfIkmnb5JP857TpGywN4,2736
|
|
145
145
|
coretrace_python/source/model.py,sha256=FZnN5mlir3tzbIpWoxnn6YtRi9UcJ_y0sZM60tKr8U4,1727
|
|
146
146
|
coretrace_python/taint/__init__.py,sha256=nKpD33_4IRneAkd3Dt-j7Q-cHl47rDA_eLZFDHZVH28,1827
|
|
147
|
-
coretrace_python/taint/engine.py,sha256=
|
|
147
|
+
coretrace_python/taint/engine.py,sha256=InunQrFgPq5NmiNkn0BQTTb_7D4IY_wZ2sJZn_K4iMs,51533
|
|
148
148
|
coretrace_python/taint/models.py,sha256=pCvE-6dYKYSLK7pylxhLWpeUcMfMyXlh_1VUJbzieo8,20405
|
|
149
149
|
coretrace_python/taint/routes.py,sha256=UAJKXcw_sM9shf58FfDroxVO9bNSm4qrQdZknwUB0lo,6110
|
|
150
|
-
coretrace_python/taint/templates.py,sha256=
|
|
150
|
+
coretrace_python/taint/templates.py,sha256=gUpUvVk_UIVY2a3T5ENm2TfBuPCDhnu2lu8h50srZ4Q,43963
|
|
151
151
|
coretrace_python/taint/urls.py,sha256=kb0-uOT12ww7R3Jzs_lPKg9lFs74JI-bpa9pXX105xM,3166
|
|
152
|
-
coretrace_python_analyzer-0.
|
|
153
|
-
coretrace_python_analyzer-0.
|
|
154
|
-
coretrace_python_analyzer-0.
|
|
155
|
-
coretrace_python_analyzer-0.
|
|
156
|
-
coretrace_python_analyzer-0.
|
|
157
|
-
coretrace_python_analyzer-0.
|
|
152
|
+
coretrace_python_analyzer-0.19.0.dist-info/METADATA,sha256=QWl44X6mXg-aIRsdgMZIGyX-Ms3CvvpysboxggSRJi4,3217
|
|
153
|
+
coretrace_python_analyzer-0.19.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
154
|
+
coretrace_python_analyzer-0.19.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
|
|
155
|
+
coretrace_python_analyzer-0.19.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
156
|
+
coretrace_python_analyzer-0.19.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
|
|
157
|
+
coretrace_python_analyzer-0.19.0.dist-info/RECORD,,
|
{coretrace_python_analyzer-0.18.0.dist-info → coretrace_python_analyzer-0.19.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|