coretrace-python-analyzer 0.14.0__py3-none-any.whl → 0.16.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/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +9 -2
- coretrace_python/bundled/models/django/django_models.py +4 -1
- coretrace_python/bundled/security/open_redirect/open_redirect.py +33 -3
- coretrace_python/bundled/security/open_redirect/plugin.toml +1 -1
- coretrace_python/bundled/security/ssrf/ssrf.py +14 -29
- coretrace_python/dependency/advisories.py +6 -1
- coretrace_python/dependency/correlation.py +36 -5
- coretrace_python/dependency/graph.py +4 -2
- coretrace_python/dependency/vex.py +10 -1
- coretrace_python/engine.py +34 -9
- coretrace_python/findings/coverage.py +4 -1
- coretrace_python/interprocedural/__init__.py +8 -0
- coretrace_python/interprocedural/summaries.py +173 -7
- coretrace_python/plugins/api.py +7 -4
- coretrace_python/taint/__init__.py +16 -1
- coretrace_python/taint/engine.py +63 -4
- coretrace_python/taint/models.py +20 -2
- coretrace_python/taint/templates.py +458 -18
- coretrace_python/taint/urls.py +70 -0
- {coretrace_python_analyzer-0.14.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/METADATA +1 -1
- {coretrace_python_analyzer-0.14.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/RECORD +26 -25
- {coretrace_python_analyzer-0.14.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/WHEEL +0 -0
- {coretrace_python_analyzer-0.14.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/entry_points.txt +0 -0
- {coretrace_python_analyzer-0.14.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {coretrace_python_analyzer-0.14.0.dist-info → coretrace_python_analyzer-0.16.0.dist-info}/licenses/NOTICE +0 -0
coretrace_python/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Calls, anywhere in the project, to an API affected by a vulnerable requirement, and
|
|
2
|
-
reads of an affected attribute whose getter runs the vulnerable code.
|
|
2
|
+
reads of an affected attribute whose getter runs the vulnerable code. A filter a project
|
|
3
|
+
template applies is a call to the function behind it, at the template's line."""
|
|
3
4
|
|
|
4
5
|
from __future__ import annotations
|
|
5
6
|
|
|
@@ -56,6 +57,10 @@ class ReachableVulnerabilityPlugin(ProjectPlugin):
|
|
|
56
57
|
reported.add((advisory, symbol))
|
|
57
58
|
check = check_conditions(entry, None)
|
|
58
59
|
findings.append(_reached(advisory, symbol, read.location, function, check))
|
|
60
|
+
for call in ctx.templates.calls:
|
|
61
|
+
for advisory in affected.get(call.symbol, ()):
|
|
62
|
+
check = check_conditions(advisory.entry_point(call.symbol), None)
|
|
63
|
+
findings.append(_reached(advisory, call.symbol, call.span, None, check))
|
|
59
64
|
return findings
|
|
60
65
|
|
|
61
66
|
|
|
@@ -67,7 +72,9 @@ def _read_through(symbol: SymbolId) -> Iterator[SymbolId]:
|
|
|
67
72
|
yield SymbolId(".".join(components[:end]))
|
|
68
73
|
|
|
69
74
|
|
|
70
|
-
def _reached(
|
|
75
|
+
def _reached(
|
|
76
|
+
advisory: Advisory, symbol: SymbolId, span: SourceSpan, function: str | None, check: ConditionCheck
|
|
77
|
+
) -> Finding:
|
|
71
78
|
return Finding(
|
|
72
79
|
rule_id="reachable-vulnerability",
|
|
73
80
|
message=(
|
|
@@ -139,7 +139,10 @@ class DjangoModels(ModelPlugin):
|
|
|
139
139
|
Sink(_sym("django.http.HttpResponseRedirect"), TaintKind.REDIRECT, _TARGET_ONLY),
|
|
140
140
|
Sink(_sym("django.http.HttpResponsePermanentRedirect"), TaintKind.REDIRECT, _TARGET_ONLY),
|
|
141
141
|
Sanitizer(_sym("django.utils.html.escape"), TaintKind.HTML),
|
|
142
|
-
TemplateRender(_sym("django.template.loader.render_to_string")),
|
|
142
|
+
TemplateRender(_sym("django.template.loader.render_to_string"), request=2),
|
|
143
|
+
TemplateRender(_sym("django.shortcuts.render"), 1, context=2, request=0),
|
|
144
|
+
TemplateRender(_sym("django.template.response.TemplateResponse"), 1, "template", 2, 0),
|
|
145
|
+
TemplateRender(_sym("django.template.response.SimpleTemplateResponse"), 0, "template"),
|
|
143
146
|
Sanitizer(_sym("django.utils.html.conditional_escape"), TaintKind.HTML),
|
|
144
147
|
# The masked CSRF secret: ASCII letters and digits only, a malformed cookie is
|
|
145
148
|
# replaced before it is used.
|
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
"""Open redirect: attacker-controlled input reaching a open redirect sink.
|
|
1
|
+
"""Open redirect: attacker-controlled input reaching a open redirect sink.
|
|
2
|
+
|
|
3
|
+
The rule draws its verdict from what the target's constant text proves (``taint.urls``),
|
|
4
|
+
as a browser resolves it. A reference on the current site (``/profile/``, ``?page=``,
|
|
5
|
+
``profile/``) keeps the browser on the site, where any further redirect is the project's
|
|
6
|
+
own code, analysed on its own: refuted. A fixed absolute host sends the browser there,
|
|
7
|
+
but the input chooses the path, where that host may redirect again: a hotspot. Anything
|
|
8
|
+
unproven, ``"/" + next`` included, stays a vulnerability.
|
|
9
|
+
"""
|
|
2
10
|
|
|
3
11
|
from __future__ import annotations
|
|
4
12
|
|
|
5
13
|
from typing import ClassVar
|
|
6
14
|
|
|
15
|
+
from coretrace_python.abstract.strings import ModuleStringsAnalysis
|
|
16
|
+
from coretrace_python.analysis import AnyAnalysis
|
|
7
17
|
from coretrace_python.findings import Severity
|
|
8
|
-
from coretrace_python.
|
|
9
|
-
from coretrace_python.
|
|
18
|
+
from coretrace_python.findings.refutation import Status, Verdict
|
|
19
|
+
from coretrace_python.hir import nodes
|
|
20
|
+
from coretrace_python.ir.ssa import SSAAnalysis
|
|
21
|
+
from coretrace_python.plugins import PluginContext, TaintDetector
|
|
22
|
+
from coretrace_python.taint import TaintFlow, TaintKind
|
|
23
|
+
from coretrace_python.taint.urls import flow_url
|
|
10
24
|
|
|
11
25
|
|
|
12
26
|
class OpenRedirectPlugin(TaintDetector):
|
|
@@ -15,3 +29,19 @@ class OpenRedirectPlugin(TaintDetector):
|
|
|
15
29
|
kind: ClassVar[TaintKind] = TaintKind.REDIRECT
|
|
16
30
|
severity: ClassVar[Severity] = Severity.MEDIUM
|
|
17
31
|
title: ClassVar[str] = "Open redirect"
|
|
32
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
|
|
33
|
+
|
|
34
|
+
def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
|
|
35
|
+
ssa = ctx.get(SSAAnalysis, function)
|
|
36
|
+
defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
|
|
37
|
+
target = flow_url(flow, defs, ctx.get(ModuleStringsAnalysis))
|
|
38
|
+
if target.relative:
|
|
39
|
+
return Verdict(flow, Status.REFUTED, f"the browser stays on the site: the target starts with {target.text!r}")
|
|
40
|
+
if target.origin is not None and verdict.status is Status.VULNERABILITY:
|
|
41
|
+
return Verdict(
|
|
42
|
+
flow,
|
|
43
|
+
Status.HOTSPOT,
|
|
44
|
+
f"the browser goes to the host fixed by {target.origin!r}, but the input chooses the path, "
|
|
45
|
+
"where that host may redirect again",
|
|
46
|
+
)
|
|
47
|
+
return verdict
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name = "open-redirect"
|
|
2
2
|
version = "1.0.0"
|
|
3
3
|
plugin_api = ">=1,<2"
|
|
4
|
-
requires = ["taint.flows", "findings.refutation"]
|
|
4
|
+
requires = ["taint.flows", "findings.refutation", "ir.ssa", "abstract.module_strings"]
|
|
5
5
|
provides = ["vulnerability.open-redirect"]
|
|
6
6
|
|
|
7
7
|
[entrypoint]
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
"""Server-side request forgery: attacker-controlled input reaching a SSRF sink.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
refuted. Anything unproven, a base of unknown value or a URL built in a helper function,
|
|
10
|
-
stays a vulnerability.
|
|
3
|
+
The rule draws its verdict from what the URL's constant text proves (``taint.urls``).
|
|
4
|
+
A fixed host keeps the input from choosing it, but the input still chooses the path,
|
|
5
|
+
which may reach a sensitive resource of that host, and a redirect may lead elsewhere: a
|
|
6
|
+
hotspot. With the path fixed too, the input reaching the query or fragment only, and
|
|
7
|
+
redirects disabled in the call, the destination is fixed: refuted. Anything unproven,
|
|
8
|
+
a base of unknown value or a URL built in a helper function, stays a vulnerability.
|
|
11
9
|
"""
|
|
12
10
|
|
|
13
11
|
from __future__ import annotations
|
|
14
12
|
|
|
15
|
-
import re
|
|
16
13
|
from typing import ClassVar
|
|
17
14
|
|
|
18
|
-
from coretrace_python.abstract.strings import ModuleStringsAnalysis
|
|
15
|
+
from coretrace_python.abstract.strings import ModuleStringsAnalysis
|
|
19
16
|
from coretrace_python.analysis import AnyAnalysis
|
|
20
17
|
from coretrace_python.findings import Severity
|
|
21
18
|
from coretrace_python.findings.refutation import Status, Verdict
|
|
@@ -23,15 +20,7 @@ from coretrace_python.hir import nodes
|
|
|
23
20
|
from coretrace_python.ir.ssa import SSAAnalysis
|
|
24
21
|
from coretrace_python.plugins import PluginContext, TaintDetector
|
|
25
22
|
from coretrace_python.taint import TaintFlow, TaintKind
|
|
26
|
-
|
|
27
|
-
# ``scheme://`` and an authority ended by ``/``, ``?`` or ``#``. A backslash, which some
|
|
28
|
-
# URL parsers take for a slash, does not end it here.
|
|
29
|
-
_FIXED_HOST = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+[/?#]")
|
|
30
|
-
# The same with the path fixed too: what follows is query or fragment.
|
|
31
|
-
_FIXED_PATH = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/?#\\]+(?:/[^?#]*)?[?#]")
|
|
32
|
-
# A client honours the one of these keywords it knows and rejects the other with a
|
|
33
|
-
# ``TypeError``: either way, the call follows no redirect.
|
|
34
|
-
_NO_REDIRECTS = ("allow_redirects", "follow_redirects")
|
|
23
|
+
from coretrace_python.taint.urls import flow_url, redirects_disabled
|
|
35
24
|
|
|
36
25
|
|
|
37
26
|
class SsrfPlugin(TaintDetector):
|
|
@@ -43,22 +32,18 @@ class SsrfPlugin(TaintDetector):
|
|
|
43
32
|
requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
|
|
44
33
|
|
|
45
34
|
def judge(self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict) -> Verdict:
|
|
46
|
-
if flow.through is not None:
|
|
47
|
-
return verdict # the URL is built in the callee
|
|
48
35
|
ssa = ctx.get(SSAAnalysis, function)
|
|
49
36
|
defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if host is None:
|
|
37
|
+
url = flow_url(flow, defs, ctx.get(ModuleStringsAnalysis))
|
|
38
|
+
if url.origin is None:
|
|
53
39
|
return verdict
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if _FIXED_PATH.match(text) is not None:
|
|
40
|
+
no_redirects = redirects_disabled(flow.sink_arguments)
|
|
41
|
+
if url.path_fixed:
|
|
57
42
|
if no_redirects:
|
|
58
|
-
return Verdict(flow, Status.REFUTED, f"the destination is fixed by {text!r} and redirects are disabled")
|
|
43
|
+
return Verdict(flow, Status.REFUTED, f"the destination is fixed by {url.text!r} and redirects are disabled")
|
|
59
44
|
left = "a redirect may lead elsewhere"
|
|
60
45
|
else:
|
|
61
46
|
left = "the input chooses the path" + ("" if no_redirects else ", and a redirect may lead elsewhere")
|
|
62
47
|
if verdict.status is not Status.VULNERABILITY:
|
|
63
48
|
return verdict
|
|
64
|
-
return Verdict(flow, Status.HOTSPOT, f"the host is fixed by {
|
|
49
|
+
return Verdict(flow, Status.HOTSPOT, f"the host is fixed by {url.origin!r}, but {left}")
|
|
@@ -251,7 +251,7 @@ def _condition(entry: Mapping[str, Any]) -> Condition:
|
|
|
251
251
|
default = entry.get("default", False)
|
|
252
252
|
if not isinstance(default, bool):
|
|
253
253
|
raise TypeError(f"condition default must be true or false, got {default!r}")
|
|
254
|
-
|
|
254
|
+
condition = Condition(
|
|
255
255
|
str(entry["kind"]),
|
|
256
256
|
str(entry["text"]),
|
|
257
257
|
None if entry.get("argument") is None else str(entry["argument"]),
|
|
@@ -259,3 +259,8 @@ def _condition(entry: Mapping[str, Any]) -> Condition:
|
|
|
259
259
|
position,
|
|
260
260
|
default,
|
|
261
261
|
)
|
|
262
|
+
if condition.kind == "host" and (
|
|
263
|
+
(condition.argument is None and condition.position is None) or condition.values or condition.default
|
|
264
|
+
):
|
|
265
|
+
raise ValueError("a host condition names the URL argument, by keyword or position, and nothing else")
|
|
266
|
+
return condition
|
|
@@ -10,7 +10,7 @@ are correlated here into one high-confidence ``exploitable-vulnerability`` findi
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
from collections.abc import Iterable, Mapping
|
|
13
|
-
from dataclasses import dataclass
|
|
13
|
+
from dataclasses import dataclass, replace
|
|
14
14
|
|
|
15
15
|
from coretrace_python.dependency.graph import (
|
|
16
16
|
DIRECT,
|
|
@@ -24,6 +24,7 @@ from coretrace_python.findings.refutation import Status, Verdict, Verdicts
|
|
|
24
24
|
from coretrace_python.interprocedural import Arguments, CallSite, ExternalSymbol
|
|
25
25
|
from coretrace_python.semantic.symbols import SymbolId
|
|
26
26
|
from coretrace_python.taint import Sink, TaintFlow, TaintKind
|
|
27
|
+
from coretrace_python.taint.urls import UrlProof, redirects_disabled
|
|
27
28
|
|
|
28
29
|
Affected = Mapping[SymbolId, tuple[Advisory, ...]]
|
|
29
30
|
|
|
@@ -61,14 +62,33 @@ class ConditionCheck:
|
|
|
61
62
|
passed: str | None = None
|
|
62
63
|
|
|
63
64
|
|
|
64
|
-
def check_conditions(
|
|
65
|
-
|
|
65
|
+
def check_conditions(
|
|
66
|
+
entry: AdvisoryEntryPoint | None,
|
|
67
|
+
arguments: Arguments | None,
|
|
68
|
+
url: UrlProof | None = None,
|
|
69
|
+
passed_as: frozenset[tuple[int | None, str | None]] = frozenset(),
|
|
70
|
+
) -> ConditionCheck:
|
|
71
|
+
"""Decide ``entry``'s conditions against what the call's ``arguments`` denote and, for
|
|
72
|
+
a ``host`` condition, what ``url`` proves of the value the attacker's input is passed
|
|
73
|
+
in (``passed_as``). A host the URL's constant text fixes rules the call out when the
|
|
74
|
+
call disables redirects; otherwise a redirect may still lead to a host the attacker
|
|
75
|
+
controls, and the condition stays pending with that uncertainty."""
|
|
66
76
|
|
|
67
77
|
if entry is None:
|
|
68
78
|
return ConditionCheck()
|
|
69
79
|
met: list[Condition] = []
|
|
70
80
|
pending: list[Condition] = []
|
|
71
81
|
for condition in entry.conditions:
|
|
82
|
+
if condition.kind == "host":
|
|
83
|
+
origin = url.origin if url is not None and _passed_in(condition, passed_as) else None
|
|
84
|
+
if origin is None:
|
|
85
|
+
pending.append(condition)
|
|
86
|
+
elif redirects_disabled(arguments):
|
|
87
|
+
return ConditionCheck(tuple(met), tuple(pending), condition, repr(origin))
|
|
88
|
+
else:
|
|
89
|
+
text = f"the host is fixed by {origin!r}, but a redirect may lead to a host the attacker controls"
|
|
90
|
+
pending.append(replace(condition, text=text))
|
|
91
|
+
continue
|
|
72
92
|
given = arguments.given(condition.argument, condition.position) if condition.checkable and arguments is not None else None
|
|
73
93
|
if given is None:
|
|
74
94
|
pending.append(condition)
|
|
@@ -84,6 +104,16 @@ def check_conditions(entry: AdvisoryEntryPoint | None, arguments: Arguments | No
|
|
|
84
104
|
return ConditionCheck(tuple(met), tuple(pending))
|
|
85
105
|
|
|
86
106
|
|
|
107
|
+
def _passed_in(condition: Condition, passed_as: frozenset[tuple[int | None, str | None]]) -> bool:
|
|
108
|
+
"""Whether a value passed as ``passed_as`` is the argument ``condition`` names."""
|
|
109
|
+
|
|
110
|
+
return any(
|
|
111
|
+
(keyword is not None and keyword == condition.argument)
|
|
112
|
+
or (keyword is None and position is not None and position == condition.position)
|
|
113
|
+
for position, keyword in passed_as
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
87
117
|
def ruled_out(module: str, site: CallSite, check: ConditionCheck) -> str:
|
|
88
118
|
"""Which call a contradicted condition rules out, and why: ``app:12
|
|
89
119
|
python.yaml.load(Loader=python.yaml.SafeLoader)``."""
|
|
@@ -120,9 +150,10 @@ def correlate(
|
|
|
120
150
|
flows: Iterable[TaintFlow],
|
|
121
151
|
verdicts: Verdicts | None,
|
|
122
152
|
affected: Affected,
|
|
153
|
+
urls: Mapping[TaintFlow, UrlProof] | None = None,
|
|
123
154
|
) -> tuple[Finding, ...]:
|
|
124
155
|
"""Exploitable-vulnerability findings for the non-refuted ADVISORY flows of a function,
|
|
125
|
-
one per advisory the sink is affected by."""
|
|
156
|
+
one per advisory the sink is affected by; ``urls`` holds what each flow's URL proves."""
|
|
126
157
|
|
|
127
158
|
findings: list[Finding] = []
|
|
128
159
|
for flow in flows:
|
|
@@ -136,7 +167,7 @@ def correlate(
|
|
|
136
167
|
entry = advisory.entry_point(flow.sink.symbol)
|
|
137
168
|
if entry is not None and not any(entry.exploitable_through(p, k) for p, k in flow.passed_as):
|
|
138
169
|
continue
|
|
139
|
-
check = check_conditions(entry, flow.sink_arguments)
|
|
170
|
+
check = check_conditions(entry, flow.sink_arguments, (urls or {}).get(flow), flow.passed_as)
|
|
140
171
|
if check.contradicted is None:
|
|
141
172
|
findings.append(_exploitable(function, flow, advisory, verdict, hotspot, check))
|
|
142
173
|
return tuple(findings)
|
|
@@ -151,8 +151,10 @@ class Condition:
|
|
|
151
151
|
``argument`` condition names the argument by keyword, and by ``position`` when it may
|
|
152
152
|
be passed positionally, and lists the ``values`` that satisfy it — symbols
|
|
153
153
|
(``python.yaml.FullLoader``) or constants as Python writes them (``True``);
|
|
154
|
-
``default`` says an absent argument means a vulnerable value. A ``
|
|
155
|
-
|
|
154
|
+
``default`` says an absent argument means a vulnerable value. A ``host`` condition
|
|
155
|
+
says the attacker must choose the host of the URL passed as ``argument`` (or at
|
|
156
|
+
``position``); the engine decides it from what the URL's constant text proves. A
|
|
157
|
+
``semantic`` condition cannot be checked and is reported as pending review."""
|
|
156
158
|
|
|
157
159
|
kind: str
|
|
158
160
|
text: str
|
|
@@ -7,7 +7,8 @@ subcomponent of the project:
|
|
|
7
7
|
exploitable finding, whatever the policy or a suppression did with it;
|
|
8
8
|
- ``not_affected``, as ``vulnerable_code_not_in_execute_path``, only when the advisory is
|
|
9
9
|
curated (it names entry points), no code of the project reaches them, every file and
|
|
10
|
-
function was analysed,
|
|
10
|
+
function was analysed, every template the project names was read when a template
|
|
11
|
+
filter can reach them, and a lock file shows that no other package requires the
|
|
11
12
|
vulnerable one, since the code of installed packages is not analysed;
|
|
12
13
|
- ``under_investigation`` otherwise, with the reason in its notes.
|
|
13
14
|
"""
|
|
@@ -25,6 +26,7 @@ from coretrace_python.dependency.graph import Advisory, DependencyGraph, Require
|
|
|
25
26
|
from coretrace_python.dependency.sbom import purl
|
|
26
27
|
from coretrace_python.findings import Component, Finding
|
|
27
28
|
from coretrace_python.findings.coverage import Coverage
|
|
29
|
+
from coretrace_python.taint import FILTER_FUNCTIONS
|
|
28
30
|
|
|
29
31
|
CONTEXT = "https://openvex.dev/ns/v0.2.0"
|
|
30
32
|
# OpenVEX's shared namespace for documents without an IRI of their own, and the author
|
|
@@ -33,6 +35,8 @@ NAMESPACE = "https://openvex.dev/docs/public/vex-"
|
|
|
33
35
|
AUTHOR = "Unknown Author"
|
|
34
36
|
# Evidence levels that put the vulnerable code in the project's execution path, lowest first.
|
|
35
37
|
REACHED = ("reachable", "exploitable")
|
|
38
|
+
# What a template calls: a template the engine cannot read may reach these.
|
|
39
|
+
_TEMPLATE_CALLS = frozenset(FILTER_FUNCTIONS.values())
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
def render_vex(
|
|
@@ -140,6 +144,11 @@ def _status(
|
|
|
140
144
|
f"No analysed code reaches {entries}, but {', '.join(partial)} could not be fully "
|
|
141
145
|
f"analysed.{ruled}"
|
|
142
146
|
)
|
|
147
|
+
if coverage.unread_templates and _TEMPLATE_CALLS.intersection(advisory.reachable_symbols):
|
|
148
|
+
return _investigating(
|
|
149
|
+
f"No analysed code reaches {entries}, but a template can reach them, and the engine could "
|
|
150
|
+
f"not read every template the project names: {'; '.join(coverage.unread_templates)}.{ruled}"
|
|
151
|
+
)
|
|
143
152
|
required_by = dependencies.required_by(package)
|
|
144
153
|
if required_by is None:
|
|
145
154
|
return _investigating(
|
coretrace_python/engine.py
CHANGED
|
@@ -83,6 +83,8 @@ from coretrace_python.interprocedural import (
|
|
|
83
83
|
SummaryAnalysis,
|
|
84
84
|
SummaryIndex,
|
|
85
85
|
SymbolRead,
|
|
86
|
+
TemplateCalls,
|
|
87
|
+
TemplateCallsAnalysis,
|
|
86
88
|
build_module_graph,
|
|
87
89
|
discover_sources,
|
|
88
90
|
project_symbol,
|
|
@@ -124,8 +126,12 @@ from coretrace_python.taint import (
|
|
|
124
126
|
TaintAnalysis,
|
|
125
127
|
TaintKind,
|
|
126
128
|
escaped_templates,
|
|
129
|
+
project_templates,
|
|
127
130
|
registered_routes,
|
|
131
|
+
request_processor,
|
|
132
|
+
unread_renders,
|
|
128
133
|
)
|
|
134
|
+
from coretrace_python.taint.urls import flow_url
|
|
129
135
|
|
|
130
136
|
TOOL_NAME = "coretrace-python-analyzer"
|
|
131
137
|
|
|
@@ -157,6 +163,7 @@ ALL_ANALYSES: tuple[AnyAnalysis, ...] = (
|
|
|
157
163
|
RegisteredRoutes,
|
|
158
164
|
EscapedTemplates,
|
|
159
165
|
ClearingAnalysis,
|
|
166
|
+
TemplateCallsAnalysis,
|
|
160
167
|
)
|
|
161
168
|
|
|
162
169
|
|
|
@@ -197,6 +204,7 @@ class ResultsEvicted(TransformationPass):
|
|
|
197
204
|
RegisteredRoutes,
|
|
198
205
|
EscapedTemplates,
|
|
199
206
|
ClearingAnalysis,
|
|
207
|
+
TemplateCallsAnalysis,
|
|
200
208
|
}
|
|
201
209
|
)
|
|
202
210
|
|
|
@@ -430,13 +438,20 @@ def analyze_project(
|
|
|
430
438
|
for symbol, registered in _routes_of(analysable[name]).items():
|
|
431
439
|
routes.setdefault(symbol, registered)
|
|
432
440
|
escaped = escaped_templates(root)
|
|
441
|
+
templates = project_templates(root)
|
|
433
442
|
clearing = models.clearing(escaped)
|
|
443
|
+
template_calls = models.template_calls(
|
|
444
|
+
templates.filters, request_processor(modules[name] for name in sorted(analysable))
|
|
445
|
+
)
|
|
434
446
|
for manager in analysable.values():
|
|
435
447
|
manager.provide(RegisteredRoutes, routes)
|
|
436
448
|
manager.provide(EscapedTemplates, escaped)
|
|
437
449
|
manager.provide(ClearingAnalysis, clearing)
|
|
450
|
+
manager.provide(TemplateCallsAnalysis, template_calls)
|
|
438
451
|
|
|
439
|
-
configuration = _configuration_key(
|
|
452
|
+
configuration = _configuration_key(
|
|
453
|
+
components, plugins, models, advisories, dependencies, routes, escaped, template_calls
|
|
454
|
+
)
|
|
440
455
|
keys = module_keys(
|
|
441
456
|
graph,
|
|
442
457
|
{name: fingerprint(configuration, str(files[name].source_id), name, files[name].text) for name in analysable},
|
|
@@ -478,6 +493,7 @@ def analyze_project(
|
|
|
478
493
|
advisory_paths,
|
|
479
494
|
_encode_routes(routes),
|
|
480
495
|
tuple(sorted(escaped)),
|
|
496
|
+
template_calls,
|
|
481
497
|
),
|
|
482
498
|
)
|
|
483
499
|
for component in pending
|
|
@@ -515,7 +531,9 @@ def analyze_project(
|
|
|
515
531
|
frozenset(),
|
|
516
532
|
reads={f: tuple(r) for f, r in reads.items()},
|
|
517
533
|
)
|
|
518
|
-
context = ProjectContext(
|
|
534
|
+
context = ProjectContext(
|
|
535
|
+
graph, dependencies, advisories, analysable, call_graphs, policy, root, functions, templates
|
|
536
|
+
)
|
|
519
537
|
for plugin in all_plugins:
|
|
520
538
|
if isinstance(plugin, ProjectPlugin):
|
|
521
539
|
findings.extend(plugin.analyze_project(context))
|
|
@@ -525,6 +543,9 @@ def analyze_project(
|
|
|
525
543
|
findings = [_sourced(finding, origins) for finding in findings]
|
|
526
544
|
accepted = tuple(f for f in findings if policy.accepts(f))
|
|
527
545
|
kept, suppressed = partition(apply_policy(policy, findings), _text_of(sources))
|
|
546
|
+
unread = list(templates.unread)
|
|
547
|
+
for name in sorted(call_graphs):
|
|
548
|
+
unread.extend(unread_renders(name, call_graphs[name], models, templates.names))
|
|
528
549
|
return ProjectAnalysis(
|
|
529
550
|
graph,
|
|
530
551
|
index,
|
|
@@ -533,7 +554,7 @@ def analyze_project(
|
|
|
533
554
|
MappingProxyType(keys),
|
|
534
555
|
reused,
|
|
535
556
|
advisories,
|
|
536
|
-
Coverage(tuple(sorted(coverage, key=lambda c: c.path))),
|
|
557
|
+
Coverage(tuple(sorted(coverage, key=lambda c: c.path)), tuple(unread)),
|
|
537
558
|
suppressed,
|
|
538
559
|
accepted,
|
|
539
560
|
components,
|
|
@@ -685,6 +706,7 @@ class _Batch:
|
|
|
685
706
|
advisory_paths: tuple[Path, ...] = ()
|
|
686
707
|
routes: tuple[tuple[str, str, str, int], ...] = ()
|
|
687
708
|
escaped: tuple[str, ...] = ()
|
|
709
|
+
template_calls: TemplateCalls = field(default_factory=TemplateCalls)
|
|
688
710
|
|
|
689
711
|
|
|
690
712
|
def _analyse_batch(batch: _Batch) -> dict[str, dict[str, Any]]:
|
|
@@ -712,6 +734,7 @@ def _analyse_batch(batch: _Batch) -> dict[str, dict[str, Any]]:
|
|
|
712
734
|
manager.provide(RegisteredRoutes, routes)
|
|
713
735
|
manager.provide(EscapedTemplates, escaped)
|
|
714
736
|
manager.provide(ClearingAnalysis, models.clearing(escaped))
|
|
737
|
+
manager.provide(TemplateCallsAnalysis, batch.template_calls)
|
|
715
738
|
module_plugins = tuple(p for p in all_plugins if not isinstance(p, ProjectPlugin))
|
|
716
739
|
results = _analyse_managers(managers, decode_index(batch.seed), module_plugins, affected)
|
|
717
740
|
return {name: encode(entry) for name, entry in results.items()}
|
|
@@ -740,6 +763,7 @@ def _configuration_key(
|
|
|
740
763
|
dependencies: DependencyGraph,
|
|
741
764
|
routes: Routes | None = None,
|
|
742
765
|
escaped: frozenset[str] = frozenset(),
|
|
766
|
+
template_calls: TemplateCalls | None = None,
|
|
743
767
|
) -> str:
|
|
744
768
|
"""Everything a module's results depend on besides the project sources (§11)."""
|
|
745
769
|
|
|
@@ -757,6 +781,7 @@ def _configuration_key(
|
|
|
757
781
|
repr(dependencies.errors),
|
|
758
782
|
repr(_encode_routes(routes or {})),
|
|
759
783
|
repr(sorted(escaped)),
|
|
784
|
+
repr(template_calls),
|
|
760
785
|
)
|
|
761
786
|
|
|
762
787
|
|
|
@@ -770,14 +795,14 @@ def _analyse_module(
|
|
|
770
795
|
graph = manager.get(CallGraphAnalysis)
|
|
771
796
|
correlated: list[Finding] = []
|
|
772
797
|
if affected:
|
|
798
|
+
strings = manager.get(ModuleStringsAnalysis)
|
|
773
799
|
for function in supported:
|
|
800
|
+
flows = manager.get(TaintAnalysis, function).flows
|
|
801
|
+
ssa = manager.get(SSAAnalysis, function)
|
|
802
|
+
defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
|
|
803
|
+
urls = {flow: flow_url(flow, defs, strings) for flow in flows if flow.kinds & TaintKind.ADVISORY}
|
|
774
804
|
correlated.extend(
|
|
775
|
-
correlate(
|
|
776
|
-
graph.name_of(function),
|
|
777
|
-
manager.get(TaintAnalysis, function).flows,
|
|
778
|
-
manager.get(RefutationAnalysis, function),
|
|
779
|
-
affected,
|
|
780
|
-
)
|
|
805
|
+
correlate(graph.name_of(function), flows, manager.get(RefutationAnalysis, function), affected, urls)
|
|
781
806
|
)
|
|
782
807
|
return CachedModule(
|
|
783
808
|
manager.get(EntryPointAnalysis),
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
"No findings" only means something when the reader knows what was analysed. Each file
|
|
4
4
|
is ``analysed``, a ``syntax-error`` (the frontend rejected it) or ``unreadable`` (it could
|
|
5
|
-
not be decoded); analysed files count their functions and how many lowered.
|
|
5
|
+
not be decoded); analysed files count their functions and how many lowered. The
|
|
6
|
+
templates the project names but the run could not read are listed, each where the
|
|
7
|
+
project names it: they may call any template filter.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from __future__ import annotations
|
|
@@ -21,6 +23,7 @@ class FileCoverage:
|
|
|
21
23
|
@dataclass(frozen=True)
|
|
22
24
|
class Coverage:
|
|
23
25
|
details: tuple[FileCoverage, ...] = ()
|
|
26
|
+
unread_templates: tuple[str, ...] = ()
|
|
24
27
|
|
|
25
28
|
@property
|
|
26
29
|
def files(self) -> int:
|
|
@@ -20,6 +20,7 @@ from coretrace_python.interprocedural.modulegraph import (
|
|
|
20
20
|
project_symbol,
|
|
21
21
|
)
|
|
22
22
|
from coretrace_python.interprocedural.summaries import (
|
|
23
|
+
FILTER_ARGUMENTS,
|
|
23
24
|
Cleared,
|
|
24
25
|
Clearing,
|
|
25
26
|
ClearingAnalysis,
|
|
@@ -31,10 +32,14 @@ from coretrace_python.interprocedural.summaries import (
|
|
|
31
32
|
SummaryAnalysis,
|
|
32
33
|
SummaryIndex,
|
|
33
34
|
SummaryTable,
|
|
35
|
+
TemplateCalls,
|
|
36
|
+
TemplateCallsAnalysis,
|
|
37
|
+
TemplateFilter,
|
|
34
38
|
cleared_by,
|
|
35
39
|
)
|
|
36
40
|
|
|
37
41
|
__all__ = [
|
|
42
|
+
"FILTER_ARGUMENTS",
|
|
38
43
|
"Arguments",
|
|
39
44
|
"CallGraph",
|
|
40
45
|
"CallGraphAnalysis",
|
|
@@ -56,6 +61,9 @@ __all__ = [
|
|
|
56
61
|
"SummaryTable",
|
|
57
62
|
"SymbolRead",
|
|
58
63
|
"Target",
|
|
64
|
+
"TemplateCalls",
|
|
65
|
+
"TemplateCallsAnalysis",
|
|
66
|
+
"TemplateFilter",
|
|
59
67
|
"UnknownTarget",
|
|
60
68
|
"build_module_graph",
|
|
61
69
|
"cleared_by",
|