coretrace-python-analyzer 0.1.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 +4 -0
- coretrace_python/__main__.py +4 -0
- coretrace_python/abstract/__init__.py +45 -0
- coretrace_python/abstract/constants.py +226 -0
- coretrace_python/abstract/heap.py +252 -0
- coretrace_python/abstract/ranges.py +285 -0
- coretrace_python/abstract/values.py +56 -0
- coretrace_python/analysis/__init__.py +31 -0
- coretrace_python/analysis/manager.py +165 -0
- coretrace_python/analysis/provider.py +73 -0
- coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
- coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
- coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
- coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
- coretrace_python/bundled/models/cli/cli_models.py +35 -0
- coretrace_python/bundled/models/cli/plugin.toml +9 -0
- coretrace_python/bundled/models/credentials/credential_models.py +43 -0
- coretrace_python/bundled/models/credentials/plugin.toml +9 -0
- coretrace_python/bundled/models/django/django_models.py +123 -0
- coretrace_python/bundled/models/django/plugin.toml +9 -0
- coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
- coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
- coretrace_python/bundled/models/flask/flask_models.py +53 -0
- coretrace_python/bundled/models/flask/plugin.toml +9 -0
- coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
- coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
- coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
- coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
- coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
- coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
- coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
- coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
- coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
- coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
- coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
- coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
- coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
- coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
- coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
- coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
- coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
- coretrace_python/bundled/security/xss/plugin.toml +9 -0
- coretrace_python/bundled/security/xss/xss.py +17 -0
- coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
- coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
- coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
- coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
- coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
- coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
- coretrace_python/cache.py +310 -0
- coretrace_python/cfg/__init__.py +46 -0
- coretrace_python/cfg/builder.py +589 -0
- coretrace_python/cfg/dominance.py +183 -0
- coretrace_python/cfg/model.py +166 -0
- coretrace_python/cli.py +216 -0
- coretrace_python/dataflow/__init__.py +29 -0
- coretrace_python/dataflow/lattice.py +78 -0
- coretrace_python/dataflow/solver.py +96 -0
- coretrace_python/dependency/__init__.py +44 -0
- coretrace_python/dependency/advisories.py +168 -0
- coretrace_python/dependency/correlation.py +87 -0
- coretrace_python/dependency/graph.py +274 -0
- coretrace_python/dependency/policy.py +65 -0
- coretrace_python/dependency/sbom.py +65 -0
- coretrace_python/engine.py +688 -0
- coretrace_python/findings/__init__.py +13 -0
- coretrace_python/findings/coverage.py +45 -0
- coretrace_python/findings/model.py +49 -0
- coretrace_python/findings/refutation.py +427 -0
- coretrace_python/frontend/__init__.py +18 -0
- coretrace_python/frontend/ast_adapter.py +447 -0
- coretrace_python/frontend/parser.py +23 -0
- coretrace_python/hir/__init__.py +5 -0
- coretrace_python/hir/nodes.py +584 -0
- coretrace_python/hir/visitors.py +36 -0
- coretrace_python/interprocedural/__init__.py +49 -0
- coretrace_python/interprocedural/callgraph.py +291 -0
- coretrace_python/interprocedural/modulegraph.py +218 -0
- coretrace_python/interprocedural/summaries.py +463 -0
- coretrace_python/ir/__init__.py +5 -0
- coretrace_python/ir/defuse.py +74 -0
- coretrace_python/ir/lowering.py +575 -0
- coretrace_python/ir/model.py +481 -0
- coretrace_python/ir/printer.py +201 -0
- coretrace_python/ir/ssa.py +277 -0
- coretrace_python/plugins/__init__.py +58 -0
- coretrace_python/plugins/api.py +153 -0
- coretrace_python/plugins/detectors.py +114 -0
- coretrace_python/plugins/loader.py +84 -0
- coretrace_python/plugins/manifest.py +112 -0
- coretrace_python/plugins/registry.py +34 -0
- coretrace_python/plugins/secrets.py +330 -0
- coretrace_python/reporters/__init__.py +22 -0
- coretrace_python/reporters/json_format.py +48 -0
- coretrace_python/reporters/report.py +23 -0
- coretrace_python/reporters/sarif.py +70 -0
- coretrace_python/reporters/text.py +24 -0
- coretrace_python/semantic/__init__.py +9 -0
- coretrace_python/semantic/identity.py +39 -0
- coretrace_python/semantic/imports.py +131 -0
- coretrace_python/semantic/scopes.py +473 -0
- coretrace_python/semantic/symbols.py +87 -0
- coretrace_python/source/__init__.py +13 -0
- coretrace_python/source/manager.py +81 -0
- coretrace_python/source/model.py +57 -0
- coretrace_python/taint/__init__.py +55 -0
- coretrace_python/taint/engine.py +800 -0
- coretrace_python/taint/models.py +317 -0
- coretrace_python/taint/routes.py +99 -0
- coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
- coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
- coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
- coretrace_python_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Analysis coverage: which files and functions a run actually looked at.
|
|
2
|
+
|
|
3
|
+
"No findings" only means something when the reader knows what was analysed. Each file
|
|
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.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class FileCoverage:
|
|
15
|
+
path: str
|
|
16
|
+
status: str
|
|
17
|
+
functions: int
|
|
18
|
+
analysed: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Coverage:
|
|
23
|
+
details: tuple[FileCoverage, ...] = ()
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def files(self) -> int:
|
|
27
|
+
return len(self.details)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def files_analysed(self) -> int:
|
|
31
|
+
return sum(1 for d in self.details if d.status == "analysed")
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def functions(self) -> int:
|
|
35
|
+
return sum(d.functions for d in self.details)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def functions_analysed(self) -> int:
|
|
39
|
+
return sum(d.analysed for d in self.details)
|
|
40
|
+
|
|
41
|
+
def summary(self) -> str:
|
|
42
|
+
return (
|
|
43
|
+
f"coverage: {self.files_analysed}/{self.files} files, "
|
|
44
|
+
f"{self.functions_analysed}/{self.functions} functions"
|
|
45
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Normalized findings (architecture §23).
|
|
2
|
+
|
|
3
|
+
A finding references lightweight data only: a rule, a message, a source span and the
|
|
4
|
+
name of the enclosing function. It never carries CFG or taint-path copies; those are
|
|
5
|
+
reconstructed at report time.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
|
|
15
|
+
from coretrace_python.source import SourceSpan
|
|
16
|
+
|
|
17
|
+
FINDING_SCHEMA_VERSION = 1
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Severity(Enum):
|
|
21
|
+
INFO = "info"
|
|
22
|
+
LOW = "low"
|
|
23
|
+
MEDIUM = "medium"
|
|
24
|
+
HIGH = "high"
|
|
25
|
+
CRITICAL = "critical"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Confidence(Enum):
|
|
29
|
+
LOW = "low"
|
|
30
|
+
MEDIUM = "medium"
|
|
31
|
+
HIGH = "high"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _no_metadata() -> Mapping[str, str]:
|
|
35
|
+
return MappingProxyType({})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Finding:
|
|
40
|
+
rule_id: str
|
|
41
|
+
message: str
|
|
42
|
+
severity: Severity
|
|
43
|
+
confidence: Confidence
|
|
44
|
+
span: SourceSpan
|
|
45
|
+
function: str | None = None
|
|
46
|
+
metadata: Mapping[str, str] = field(default_factory=_no_metadata)
|
|
47
|
+
|
|
48
|
+
def __post_init__(self) -> None:
|
|
49
|
+
object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Proof / refutation engine (architecture §24).
|
|
2
|
+
|
|
3
|
+
Every taint flow gets a verdict. Walking the dominators of the sink block, each branch
|
|
4
|
+
whose one side alone reaches the sink fixes the truth of its condition there; the
|
|
5
|
+
condition is then interpreted: string validators (``isdigit()`` and friends), membership
|
|
6
|
+
in a constant allowlist and equality with a constant prove a value safe, ``and`` / ``or``
|
|
7
|
+
and ``not`` combine as expected, a ``Validator`` model names a callable whose truth proves
|
|
8
|
+
one of its arguments, a numeric value (``abstract.ranges``) cannot inject, and anything
|
|
9
|
+
else that mentions the value is a guard that does not prove it. A proof counts for an
|
|
10
|
+
origin when every dependence path from that origin to the sink argument goes through a
|
|
11
|
+
proven value. A flow is refuted when every tainted origin is proven safe or the sink is
|
|
12
|
+
unreachable, a hotspot when it sits behind an ``AuthorizationGuard`` (a decorator or a
|
|
13
|
+
dominating condition) or when an unproven guard mentions it, and a vulnerability
|
|
14
|
+
otherwise.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Mapping
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from types import MappingProxyType
|
|
23
|
+
from typing import ClassVar
|
|
24
|
+
|
|
25
|
+
from coretrace_python.abstract import ConstantPropagation, RangeAnalysis, RangeFacts
|
|
26
|
+
from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
|
|
27
|
+
from coretrace_python.cfg import CFG, BlockId, CFGAnalysis, DominanceAnalysis, DominatorTree
|
|
28
|
+
from coretrace_python.hir import nodes
|
|
29
|
+
from coretrace_python.interprocedural import CallGraphAnalysis
|
|
30
|
+
from coretrace_python.ir.model import (
|
|
31
|
+
BoolOp,
|
|
32
|
+
Branch,
|
|
33
|
+
BuildList,
|
|
34
|
+
BuildTuple,
|
|
35
|
+
Call,
|
|
36
|
+
Compare,
|
|
37
|
+
Constant,
|
|
38
|
+
FunctionIR,
|
|
39
|
+
GetAttr,
|
|
40
|
+
Instruction,
|
|
41
|
+
UnaryOp,
|
|
42
|
+
Value,
|
|
43
|
+
)
|
|
44
|
+
from coretrace_python.ir.ssa import SSAAnalysis
|
|
45
|
+
from coretrace_python.semantic.scopes import ScopeAnalysis, ScopeTable
|
|
46
|
+
from coretrace_python.semantic.symbols import SymbolAnalysis, SymbolId, SymbolTable
|
|
47
|
+
from coretrace_python.taint import (
|
|
48
|
+
AuthorizationGuard,
|
|
49
|
+
ModelTable,
|
|
50
|
+
SecurityModelAnalysis,
|
|
51
|
+
TaintAnalysis,
|
|
52
|
+
TaintFacts,
|
|
53
|
+
TaintFlow,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
VALIDATORS = frozenset(
|
|
57
|
+
{"isdigit", "isnumeric", "isdecimal", "isalnum", "isalpha", "isidentifier", "isascii"}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Status(Enum):
|
|
62
|
+
VULNERABILITY = "vulnerability"
|
|
63
|
+
HOTSPOT = "hotspot"
|
|
64
|
+
REFUTED = "refuted"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class Verdict:
|
|
69
|
+
flow: TaintFlow
|
|
70
|
+
status: Status
|
|
71
|
+
evidence: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Verdicts:
|
|
75
|
+
def __init__(self, verdicts: tuple[Verdict, ...]) -> None:
|
|
76
|
+
self._all = verdicts
|
|
77
|
+
self._by_flow: Mapping[TaintFlow, Verdict] = MappingProxyType(
|
|
78
|
+
{verdict.flow: verdict for verdict in verdicts}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def all(self) -> tuple[Verdict, ...]:
|
|
82
|
+
return self._all
|
|
83
|
+
|
|
84
|
+
def verdict(self, flow: TaintFlow) -> Verdict:
|
|
85
|
+
return self._by_flow[flow]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class _Guard:
|
|
90
|
+
"""A dominating branch condition with its truth at the sink."""
|
|
91
|
+
|
|
92
|
+
condition: Value
|
|
93
|
+
truth: bool
|
|
94
|
+
line: int
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _Judge:
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
function: FunctionIR,
|
|
101
|
+
cfg: CFG,
|
|
102
|
+
tree: DominatorTree,
|
|
103
|
+
taint: TaintFacts,
|
|
104
|
+
ranges: RangeFacts | None = None,
|
|
105
|
+
models: ModelTable | None = None,
|
|
106
|
+
symbols: Mapping[Value, SymbolId] | None = None,
|
|
107
|
+
authorization: AuthorizationGuard | None = None,
|
|
108
|
+
) -> None:
|
|
109
|
+
self.function = function
|
|
110
|
+
self.cfg = cfg
|
|
111
|
+
self.tree = tree
|
|
112
|
+
self.taint = taint
|
|
113
|
+
self.ranges = ranges or RangeFacts({})
|
|
114
|
+
self.models = models or ModelTable((), (), ())
|
|
115
|
+
self.symbols = symbols or {}
|
|
116
|
+
self.authorization = authorization
|
|
117
|
+
self.blocks = {block.id: block for block in function.blocks}
|
|
118
|
+
self.defs: dict[Value, Instruction] = {
|
|
119
|
+
i.result: i for block in function.blocks for i in block.instructions if i.result
|
|
120
|
+
}
|
|
121
|
+
self._closures: dict[Value, frozenset[Value]] = {}
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------ dependencies
|
|
124
|
+
|
|
125
|
+
def closure(self, value: Value) -> frozenset[Value]:
|
|
126
|
+
"""Every value ``value`` transitively depends on, excluding itself."""
|
|
127
|
+
|
|
128
|
+
if value in self._closures:
|
|
129
|
+
return self._closures[value]
|
|
130
|
+
found: set[Value] = set()
|
|
131
|
+
pending = [value]
|
|
132
|
+
while pending:
|
|
133
|
+
definition = self.defs.get(pending.pop())
|
|
134
|
+
if definition is None:
|
|
135
|
+
continue
|
|
136
|
+
for operand in definition.operands():
|
|
137
|
+
if operand not in found:
|
|
138
|
+
found.add(operand)
|
|
139
|
+
pending.append(operand)
|
|
140
|
+
self._closures[value] = frozenset(found)
|
|
141
|
+
return self._closures[value]
|
|
142
|
+
|
|
143
|
+
def origins(self, value: Value) -> frozenset[Value]:
|
|
144
|
+
"""Tainted values the argument depends on whose own operands are untainted."""
|
|
145
|
+
|
|
146
|
+
candidates = self.closure(value) | {value}
|
|
147
|
+
return frozenset(
|
|
148
|
+
v
|
|
149
|
+
for v in candidates
|
|
150
|
+
if self.taint.taint(v)
|
|
151
|
+
and not any(self.taint.taint(o) for o in self.closure(v))
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def covered(self, origin: Value, argument: Value, proven: Mapping[Value, str]) -> bool:
|
|
155
|
+
"""Whether every dependence path from ``origin`` to ``argument`` goes through a
|
|
156
|
+
proven value, so nothing of the origin reaches the sink unproven."""
|
|
157
|
+
|
|
158
|
+
if origin in proven:
|
|
159
|
+
return True
|
|
160
|
+
seen: set[Value] = set()
|
|
161
|
+
pending = [argument]
|
|
162
|
+
while pending:
|
|
163
|
+
value = pending.pop()
|
|
164
|
+
if value in proven or value in seen:
|
|
165
|
+
continue
|
|
166
|
+
if value == origin:
|
|
167
|
+
return False
|
|
168
|
+
seen.add(value)
|
|
169
|
+
definition = self.defs.get(value)
|
|
170
|
+
if definition is not None:
|
|
171
|
+
pending.extend(definition.operands())
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
# ------------------------------------------------------------------ guards
|
|
175
|
+
|
|
176
|
+
def sink_block(self, flow: TaintFlow) -> BlockId | None:
|
|
177
|
+
for block in self.function.blocks:
|
|
178
|
+
for instruction in block.instructions:
|
|
179
|
+
if isinstance(instruction, Call) and instruction.location == flow.location:
|
|
180
|
+
return block.id
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
def reaches(self, start: BlockId, target: BlockId, avoiding: BlockId) -> bool:
|
|
184
|
+
seen: set[BlockId] = set()
|
|
185
|
+
pending = [start]
|
|
186
|
+
while pending:
|
|
187
|
+
block = pending.pop()
|
|
188
|
+
if block == target:
|
|
189
|
+
return True
|
|
190
|
+
if block in seen or block == avoiding:
|
|
191
|
+
continue
|
|
192
|
+
seen.add(block)
|
|
193
|
+
pending.extend(self.cfg.successors(block))
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def guards(self, sink: BlockId) -> list[_Guard]:
|
|
197
|
+
found: list[_Guard] = []
|
|
198
|
+
dominator = self.tree.idom(sink)
|
|
199
|
+
while dominator is not None:
|
|
200
|
+
terminator = self.blocks[dominator].terminator
|
|
201
|
+
if isinstance(terminator, Branch):
|
|
202
|
+
then_reaches = self.reaches(terminator.then_block, sink, dominator)
|
|
203
|
+
else_reaches = self.reaches(terminator.else_block, sink, dominator)
|
|
204
|
+
if then_reaches != else_reaches:
|
|
205
|
+
found.append(
|
|
206
|
+
_Guard(terminator.condition, then_reaches, terminator.location.start_line)
|
|
207
|
+
)
|
|
208
|
+
dominator = self.tree.idom(dominator)
|
|
209
|
+
return found
|
|
210
|
+
|
|
211
|
+
def interpret(
|
|
212
|
+
self, condition: Value, truth: bool | None
|
|
213
|
+
) -> tuple[dict[Value, str], set[Value]]:
|
|
214
|
+
"""Values the condition proves safe (with the reason), and values it mentions
|
|
215
|
+
without proving anything. ``truth`` is ``None`` when it is not fixed at the sink.
|
|
216
|
+
|
|
217
|
+
A recognised check evaluated the wrong way (``isdigit()`` known false) yields
|
|
218
|
+
nothing: it is neither a proof nor a reassuring guard."""
|
|
219
|
+
|
|
220
|
+
definition = self.defs.get(condition)
|
|
221
|
+
proven: dict[Value, str] = {}
|
|
222
|
+
mentioned: set[Value] = set()
|
|
223
|
+
if isinstance(definition, UnaryOp) and definition.operator == "not":
|
|
224
|
+
return self.interpret(definition.operand, None if truth is None else not truth)
|
|
225
|
+
if isinstance(definition, BoolOp):
|
|
226
|
+
fixed = truth is not None and (definition.operator == "and") == truth
|
|
227
|
+
for value in definition.values:
|
|
228
|
+
found, seen = self.interpret(value, truth if fixed else None)
|
|
229
|
+
proven.update(found)
|
|
230
|
+
mentioned |= seen
|
|
231
|
+
return proven, mentioned
|
|
232
|
+
recognised = self.recognise(definition)
|
|
233
|
+
if recognised is None:
|
|
234
|
+
return proven, set(self.closure(condition)) | {condition}
|
|
235
|
+
tested, reason, when_true = recognised
|
|
236
|
+
if truth is None:
|
|
237
|
+
mentioned = set(self.closure(condition)) | {condition}
|
|
238
|
+
elif truth == when_true:
|
|
239
|
+
# The check proves ``tested``; whatever else it reads is merely mentioned.
|
|
240
|
+
proven[tested] = reason
|
|
241
|
+
mentioned = set(self.closure(condition)) | {condition}
|
|
242
|
+
return proven, mentioned
|
|
243
|
+
|
|
244
|
+
def recognise(self, definition: Instruction | None) -> tuple[Value, str, bool] | None:
|
|
245
|
+
"""``(validated value, reason, truth that validates)`` for known check shapes."""
|
|
246
|
+
|
|
247
|
+
if isinstance(definition, Call) and not definition.arguments:
|
|
248
|
+
callee = self.defs.get(definition.callee)
|
|
249
|
+
if isinstance(callee, GetAttr) and callee.attribute in VALIDATORS:
|
|
250
|
+
return callee.object, f"guarded by {callee.attribute}()", True
|
|
251
|
+
if isinstance(definition, Call):
|
|
252
|
+
symbol = self.symbols.get(definition.callee)
|
|
253
|
+
validator = self.models.validator(symbol) if symbol is not None else None
|
|
254
|
+
if validator is not None and validator.argument < len(definition.arguments):
|
|
255
|
+
return definition.arguments[validator.argument], f"validated by {symbol}", True
|
|
256
|
+
if isinstance(definition, Compare):
|
|
257
|
+
left, right = definition.left, definition.right
|
|
258
|
+
if definition.operator in ("in", "not_in") and self.is_constant_collection(right):
|
|
259
|
+
return (
|
|
260
|
+
left,
|
|
261
|
+
"allowlisted by a membership check on constants",
|
|
262
|
+
definition.operator == "in",
|
|
263
|
+
)
|
|
264
|
+
if definition.operator in ("eq", "not_eq"):
|
|
265
|
+
for tested, other in ((left, right), (right, left)):
|
|
266
|
+
if isinstance(self.defs.get(other), Constant):
|
|
267
|
+
return tested, "equals a constant", definition.operator == "eq"
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
def is_constant_collection(self, value: Value) -> bool:
|
|
271
|
+
definition = self.defs.get(value)
|
|
272
|
+
if isinstance(definition, Constant):
|
|
273
|
+
return isinstance(definition.value, str | bytes)
|
|
274
|
+
if isinstance(definition, BuildList | BuildTuple):
|
|
275
|
+
return all(isinstance(self.defs.get(e), Constant) for e in definition.elements)
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
# ------------------------------------------------------------------ authorization
|
|
279
|
+
|
|
280
|
+
def authorized_by(self, condition: Value, truth: bool) -> AuthorizationGuard | None:
|
|
281
|
+
"""The authorization guard this condition enforces when it is ``truth``."""
|
|
282
|
+
|
|
283
|
+
definition = self.defs.get(condition)
|
|
284
|
+
if isinstance(definition, UnaryOp) and definition.operator == "not":
|
|
285
|
+
return self.authorized_by(definition.operand, not truth)
|
|
286
|
+
if isinstance(definition, BoolOp) and (definition.operator == "and") == truth:
|
|
287
|
+
for value in definition.values:
|
|
288
|
+
found = self.authorized_by(value, truth)
|
|
289
|
+
if found is not None:
|
|
290
|
+
return found
|
|
291
|
+
return None
|
|
292
|
+
if isinstance(definition, Compare) and definition.operator in ("in", "not_in"):
|
|
293
|
+
# ``'logged_in' in session``: membership in an authorization store.
|
|
294
|
+
if truth != (definition.operator == "in"):
|
|
295
|
+
return None
|
|
296
|
+
container = self.symbols.get(definition.right)
|
|
297
|
+
return self.models.authorization(container) if container is not None else None
|
|
298
|
+
if not truth:
|
|
299
|
+
return None
|
|
300
|
+
symbol = self.symbols.get(condition)
|
|
301
|
+
if symbol is None and isinstance(definition, Call):
|
|
302
|
+
symbol = self.symbols.get(definition.callee)
|
|
303
|
+
return self.models.authorization(symbol) if symbol is not None else None
|
|
304
|
+
|
|
305
|
+
# ------------------------------------------------------------------ verdicts
|
|
306
|
+
|
|
307
|
+
def judge(self, flow: TaintFlow, reachable: bool) -> Verdict:
|
|
308
|
+
sink = self.sink_block(flow)
|
|
309
|
+
if sink is None or not reachable:
|
|
310
|
+
return Verdict(flow, Status.REFUTED, "sink unreachable by constant propagation")
|
|
311
|
+
origins = self.origins(flow.argument)
|
|
312
|
+
chain = self.closure(flow.argument) | {flow.argument}
|
|
313
|
+
proven: dict[Value, str] = {
|
|
314
|
+
value: f"numeric value within {interval}"
|
|
315
|
+
for value, interval in self.ranges.at(sink).items()
|
|
316
|
+
if value in chain
|
|
317
|
+
}
|
|
318
|
+
mentions: dict[Value, int] = {}
|
|
319
|
+
authorization: str | None = (
|
|
320
|
+
f"behind authorization ({self.authorization.label}) by decorator"
|
|
321
|
+
if self.authorization is not None
|
|
322
|
+
else None
|
|
323
|
+
)
|
|
324
|
+
for guard in self.guards(sink):
|
|
325
|
+
found, mentioned = self.interpret(guard.condition, guard.truth)
|
|
326
|
+
for value, reason in found.items():
|
|
327
|
+
if value in chain:
|
|
328
|
+
proven.setdefault(value, reason)
|
|
329
|
+
for origin in origins:
|
|
330
|
+
if origin not in mentions and mentioned & (
|
|
331
|
+
{origin} | {w for w in chain if origin in self.closure(w)}
|
|
332
|
+
):
|
|
333
|
+
mentions[origin] = guard.line
|
|
334
|
+
if authorization is None:
|
|
335
|
+
guard_model = self.authorized_by(guard.condition, guard.truth)
|
|
336
|
+
if guard_model is not None:
|
|
337
|
+
authorization = f"behind authorization ({guard_model.label}) at line {guard.line}"
|
|
338
|
+
proofs = {
|
|
339
|
+
origin: sorted(
|
|
340
|
+
{reason for value, reason in proven.items() if value == origin or origin in self.closure(value)}
|
|
341
|
+
)
|
|
342
|
+
for origin in origins
|
|
343
|
+
if self.covered(origin, flow.argument, proven)
|
|
344
|
+
}
|
|
345
|
+
if origins and all(origin in proofs for origin in origins):
|
|
346
|
+
reasons = sorted({reason for found in proofs.values() for reason in found})
|
|
347
|
+
return Verdict(flow, Status.REFUTED, "; ".join(reasons))
|
|
348
|
+
if authorization is not None:
|
|
349
|
+
return Verdict(flow, Status.HOTSPOT, authorization)
|
|
350
|
+
unguarded = [origin for origin in origins if origin not in proofs and origin not in mentions]
|
|
351
|
+
if not origins or unguarded:
|
|
352
|
+
return Verdict(flow, Status.VULNERABILITY, "no guard on the path to the sink")
|
|
353
|
+
line = min(mentions.values())
|
|
354
|
+
return Verdict(
|
|
355
|
+
flow, Status.HOTSPOT, f"guard at line {line} does not prove the value safe"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def judge_flows(
|
|
360
|
+
function: FunctionIR,
|
|
361
|
+
cfg: CFG,
|
|
362
|
+
tree: DominatorTree,
|
|
363
|
+
taint: TaintFacts,
|
|
364
|
+
reachable: frozenset[BlockId],
|
|
365
|
+
ranges: RangeFacts | None = None,
|
|
366
|
+
models: ModelTable | None = None,
|
|
367
|
+
symbols: Mapping[Value, SymbolId] | None = None,
|
|
368
|
+
authorization: AuthorizationGuard | None = None,
|
|
369
|
+
) -> Verdicts:
|
|
370
|
+
judge = _Judge(function, cfg, tree, taint, ranges, models, symbols, authorization)
|
|
371
|
+
verdicts = []
|
|
372
|
+
for flow in taint.flows:
|
|
373
|
+
sink = judge.sink_block(flow)
|
|
374
|
+
verdicts.append(judge.judge(flow, sink is not None and sink in reachable))
|
|
375
|
+
return Verdicts(tuple(verdicts))
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def authorization_of(
|
|
379
|
+
function: nodes.Function, models: ModelTable, scopes: ScopeTable, symbols: SymbolTable
|
|
380
|
+
) -> AuthorizationGuard | None:
|
|
381
|
+
"""The authorization guard among the function's decorators, if any."""
|
|
382
|
+
|
|
383
|
+
scope = scopes.scope_for(function)
|
|
384
|
+
enclosing = scope.parent if scope.parent is not None else scope.id
|
|
385
|
+
for decorator in function.decorators:
|
|
386
|
+
symbol = symbols.resolve_expression(enclosing, decorator)
|
|
387
|
+
guard = models.authorization(symbol) if symbol is not None else None
|
|
388
|
+
if guard is not None:
|
|
389
|
+
return guard
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class RefutationAnalysis(FunctionAnalysis[Verdicts]):
|
|
394
|
+
name: ClassVar[str] = "findings.refutation"
|
|
395
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset(
|
|
396
|
+
{
|
|
397
|
+
TaintAnalysis,
|
|
398
|
+
DominanceAnalysis,
|
|
399
|
+
ConstantPropagation,
|
|
400
|
+
RangeAnalysis,
|
|
401
|
+
SSAAnalysis,
|
|
402
|
+
CFGAnalysis,
|
|
403
|
+
SecurityModelAnalysis,
|
|
404
|
+
CallGraphAnalysis,
|
|
405
|
+
ScopeAnalysis,
|
|
406
|
+
SymbolAnalysis,
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
@classmethod
|
|
411
|
+
def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> Verdicts:
|
|
412
|
+
ssa = ctx.get(SSAAnalysis, function)
|
|
413
|
+
constants = ctx.get(ConstantPropagation, function)
|
|
414
|
+
graph = ctx.get(CallGraphAnalysis)
|
|
415
|
+
models = ctx.get(SecurityModelAnalysis)
|
|
416
|
+
return judge_flows(
|
|
417
|
+
ssa,
|
|
418
|
+
ctx.get(CFGAnalysis, function),
|
|
419
|
+
ctx.get(DominanceAnalysis, function),
|
|
420
|
+
ctx.get(TaintAnalysis, function),
|
|
421
|
+
frozenset(b.id for b in ssa.blocks if constants.reachable(b.id)),
|
|
422
|
+
ctx.get(RangeAnalysis, function),
|
|
423
|
+
models,
|
|
424
|
+
graph.symbols(graph.name_of(function)),
|
|
425
|
+
authorization_of(function, models, ctx.get(ScopeAnalysis), ctx.get(SymbolAnalysis)),
|
|
426
|
+
)
|
|
427
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Parsing and adaptation of Python source into parser-independent PyHIR.
|
|
2
|
+
|
|
3
|
+
Parser-specific objects never leave this package: callers receive a PyHIR module.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from coretrace_python.frontend.ast_adapter import HIRBuildError, build_module
|
|
7
|
+
from coretrace_python.frontend.parser import ParseError, parse_source_file
|
|
8
|
+
from coretrace_python.hir import nodes
|
|
9
|
+
from coretrace_python.source import SourceFile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_hir(source: SourceFile) -> nodes.Module:
|
|
13
|
+
"""Parse ``source`` and return its PyHIR module."""
|
|
14
|
+
|
|
15
|
+
return build_module(source, parse_source_file(source))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["HIRBuildError", "ParseError", "build_hir"]
|