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