coretrace-python-analyzer 0.2.0__py3-none-any.whl → 0.3.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 +10 -7
- coretrace_python/engine.py +1 -1
- coretrace_python/plugins/loader.py +18 -2
- coretrace_python/taint/engine.py +16 -3
- coretrace_python/taint/models.py +33 -12
- {coretrace_python_analyzer-0.2.0.dist-info → coretrace_python_analyzer-0.3.0.dist-info}/METADATA +1 -1
- {coretrace_python_analyzer-0.2.0.dist-info → coretrace_python_analyzer-0.3.0.dist-info}/RECORD +12 -12
- {coretrace_python_analyzer-0.2.0.dist-info → coretrace_python_analyzer-0.3.0.dist-info}/WHEEL +1 -1
- {coretrace_python_analyzer-0.2.0.dist-info → coretrace_python_analyzer-0.3.0.dist-info}/entry_points.txt +0 -0
- {coretrace_python_analyzer-0.2.0.dist-info → coretrace_python_analyzer-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {coretrace_python_analyzer-0.2.0.dist-info → coretrace_python_analyzer-0.3.0.dist-info}/licenses/NOTICE +0 -0
coretrace_python/__init__.py
CHANGED
coretrace_python/cache.py
CHANGED
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import hashlib
|
|
15
15
|
import json
|
|
16
16
|
import os
|
|
17
|
-
from collections.abc import
|
|
17
|
+
from collections.abc import Mapping
|
|
18
18
|
from dataclasses import dataclass
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from types import MappingProxyType
|
|
@@ -64,15 +64,18 @@ def fingerprint(*parts: str) -> str:
|
|
|
64
64
|
return digest.hexdigest()
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
def directory_fingerprint(directory: Path
|
|
68
|
-
"""A digest of
|
|
67
|
+
def directory_fingerprint(directory: Path) -> str:
|
|
68
|
+
"""A digest of every file under ``directory``, so an edited plugin misses: its code,
|
|
69
|
+
its manifest and the rule or advisory data it ships in any format. Bytecode and
|
|
70
|
+
hidden files are not part of what the plugin does."""
|
|
69
71
|
|
|
70
|
-
wanted = tuple(suffixes)
|
|
71
72
|
parts: list[str] = []
|
|
72
73
|
for path in sorted(directory.rglob("*")):
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
relative = path.relative_to(directory)
|
|
75
|
+
if not path.is_file() or any(p == "__pycache__" or p.startswith(".") for p in relative.parts):
|
|
76
|
+
continue
|
|
77
|
+
parts.append(str(relative))
|
|
78
|
+
parts.append(path.read_bytes().hex())
|
|
76
79
|
return fingerprint(*parts)
|
|
77
80
|
|
|
78
81
|
|
coretrace_python/engine.py
CHANGED
|
@@ -257,7 +257,7 @@ def load_plugins(plugin_roots: Sequence[Path], manager: AnalysisManager) -> Plug
|
|
|
257
257
|
def plugin_models(plugins: Iterable[Plugin]) -> ModelTable:
|
|
258
258
|
models = SecurityModelRegistry()
|
|
259
259
|
for plugin in plugins:
|
|
260
|
-
models.register(*plugin.models)
|
|
260
|
+
models.register(*plugin.models, origin=plugin.name)
|
|
261
261
|
return models.freeze()
|
|
262
262
|
|
|
263
263
|
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import importlib.util
|
|
7
|
+
import sys
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
@@ -63,16 +64,31 @@ def discover_plugins(root: Path, manager: AnalysisManager) -> tuple[LoadedPlugin
|
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
def _import_entrypoint(directory: Path, manifest: PluginManifest) -> type[Plugin]:
|
|
66
|
-
|
|
67
|
+
"""Import the entrypoint, a module ``<name>.py`` or a package ``<name>/__init__.py``.
|
|
68
|
+
|
|
69
|
+
The module name derives from the path, so two plugins may both call their module
|
|
70
|
+
``models``; a package is registered in ``sys.modules`` under that name before it
|
|
71
|
+
runs, which is what its relative imports resolve through."""
|
|
72
|
+
|
|
73
|
+
package_path = directory / manifest.entrypoint.module / "__init__.py"
|
|
74
|
+
is_package = package_path.is_file()
|
|
75
|
+
module_path = package_path if is_package else directory / f"{manifest.entrypoint.module}.py"
|
|
67
76
|
if not module_path.is_file():
|
|
68
77
|
raise ManifestError(
|
|
69
78
|
f"{directory / MANIFEST_FILENAME}: entrypoint module"
|
|
70
79
|
f" {manifest.entrypoint.module!r} not found"
|
|
71
80
|
)
|
|
72
81
|
unique = hashlib.sha1(str(module_path.resolve()).encode()).hexdigest()[:12]
|
|
73
|
-
|
|
82
|
+
name = f"coretrace_plugin_{unique}"
|
|
83
|
+
spec = importlib.util.spec_from_file_location(
|
|
84
|
+
name,
|
|
85
|
+
module_path,
|
|
86
|
+
submodule_search_locations=[str(module_path.parent)] if is_package else None,
|
|
87
|
+
)
|
|
74
88
|
assert spec is not None and spec.loader is not None
|
|
75
89
|
module = importlib.util.module_from_spec(spec)
|
|
90
|
+
if is_package:
|
|
91
|
+
sys.modules[name] = module
|
|
76
92
|
spec.loader.exec_module(module)
|
|
77
93
|
|
|
78
94
|
candidate = getattr(module, manifest.entrypoint.class_name, None)
|
coretrace_python/taint/engine.py
CHANGED
|
@@ -147,8 +147,10 @@ class _TaintProblem(DataflowProblem[State]):
|
|
|
147
147
|
project: SummaryIndex | None = None,
|
|
148
148
|
heap: HeapFacts | None = None,
|
|
149
149
|
seeds: Mapping[HeapLocation, Taint] | None = None,
|
|
150
|
+
module: str = "",
|
|
150
151
|
) -> None:
|
|
151
152
|
self.name = name
|
|
153
|
+
self.module = module
|
|
152
154
|
self.function = function
|
|
153
155
|
self.models = models
|
|
154
156
|
self.graph = graph
|
|
@@ -294,9 +296,10 @@ class _TaintProblem(DataflowProblem[State]):
|
|
|
294
296
|
through = target.symbol.canonical_name.removeprefix("python.")
|
|
295
297
|
bound = () if project.static else self.receiver(call, project.name)
|
|
296
298
|
arguments = (*(self.deep(r, state) for r in bound), *arguments)
|
|
297
|
-
|
|
299
|
+
result = self.known(
|
|
298
300
|
project, through, arguments, keywords, everything, call, flows, state, (), bound
|
|
299
301
|
)
|
|
302
|
+
return self.declared_clean(target.symbol, result)
|
|
300
303
|
symbol = target.symbol
|
|
301
304
|
if not self.modelled(symbol):
|
|
302
305
|
# ``get_conn().execute`` derived ``app.database.get_conn.execute``; what
|
|
@@ -312,9 +315,10 @@ class _TaintProblem(DataflowProblem[State]):
|
|
|
312
315
|
*arguments,
|
|
313
316
|
*(self.deep(value, state) for value in captured),
|
|
314
317
|
)
|
|
315
|
-
|
|
318
|
+
result = self.known(
|
|
316
319
|
summary, target.name, arguments, keywords, everything, call, flows, state, captured, bound
|
|
317
320
|
)
|
|
321
|
+
return self.declared_clean(project_symbol(self.module, target.name), result)
|
|
318
322
|
returned = self.returned_symbol(call)
|
|
319
323
|
if returned is not None:
|
|
320
324
|
return self.external(returned, everything, call, state, flows)
|
|
@@ -369,6 +373,13 @@ class _TaintProblem(DataflowProblem[State]):
|
|
|
369
373
|
made = self.defs.get(call.callee)
|
|
370
374
|
return made.captured if isinstance(made, MakeFunction) else ()
|
|
371
375
|
|
|
376
|
+
def declared_clean(self, symbol: SymbolId, result: Taint) -> Taint:
|
|
377
|
+
"""A ``Sanitizer`` declared on a project function is the project's own knowledge
|
|
378
|
+
of what its result carries; it wins over what the summary derived from the body."""
|
|
379
|
+
|
|
380
|
+
sanitizer = self.models.sanitizer(symbol)
|
|
381
|
+
return result if sanitizer is None else result.without(sanitizer.kinds)
|
|
382
|
+
|
|
372
383
|
def modelled(self, symbol: SymbolId) -> bool:
|
|
373
384
|
return (
|
|
374
385
|
self.models.sink(symbol) is not None
|
|
@@ -730,8 +741,9 @@ def propagate_taint(
|
|
|
730
741
|
project: SummaryIndex | None = None,
|
|
731
742
|
heap: HeapFacts | None = None,
|
|
732
743
|
seeds: Mapping[HeapLocation, Taint] | None = None,
|
|
744
|
+
module: str = "",
|
|
733
745
|
) -> TaintFacts:
|
|
734
|
-
problem = _TaintProblem(name, function, models, graph, summaries, parameters, project, heap, seeds)
|
|
746
|
+
problem = _TaintProblem(name, function, models, graph, summaries, parameters, project, heap, seeds, module)
|
|
735
747
|
solution = solve(problem, cfg)
|
|
736
748
|
taints: dict[Key, Taint] = {}
|
|
737
749
|
flows: list[TaintFlow] = []
|
|
@@ -794,6 +806,7 @@ class TaintAnalysis(FunctionAnalysis[TaintFacts]):
|
|
|
794
806
|
ctx.get(ProjectSummaries),
|
|
795
807
|
heap,
|
|
796
808
|
seeds,
|
|
809
|
+
ctx.module.name,
|
|
797
810
|
)
|
|
798
811
|
|
|
799
812
|
|
coretrace_python/taint/models.py
CHANGED
|
@@ -30,10 +30,14 @@ class TaintKind(Flag):
|
|
|
30
30
|
ADVISORY = auto()
|
|
31
31
|
DESERIALIZATION = auto()
|
|
32
32
|
REDIRECT = auto()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
#
|
|
33
|
+
NOSQL = auto()
|
|
34
|
+
ALL = SQL | COMMAND | HTML | PATH | SSRF | CODE | ADVISORY | DESERIALIZATION | REDIRECT | NOSQL
|
|
35
|
+
# Outside ALL on purpose: only the values a model marks carry them, so a database
|
|
36
|
+
# write reached by ordinary input is not a plaintext credential, and ordinary input
|
|
37
|
+
# reaching a logger is neither log forging nor a personal-data leak.
|
|
36
38
|
CREDENTIAL = auto()
|
|
39
|
+
LOG = auto()
|
|
40
|
+
PII = auto()
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
class ModelError(Exception):
|
|
@@ -69,6 +73,11 @@ class Sink:
|
|
|
69
73
|
kinds &= ~restricted
|
|
70
74
|
return kinds
|
|
71
75
|
|
|
76
|
+
def merged(self, other: Sink) -> Sink:
|
|
77
|
+
"""This sink with ``other``'s kinds and positions added."""
|
|
78
|
+
|
|
79
|
+
return Sink(self.symbol, self.kinds | other.kinds, self.positions + other.positions)
|
|
80
|
+
|
|
72
81
|
|
|
73
82
|
@dataclass(frozen=True)
|
|
74
83
|
class Sanitizer:
|
|
@@ -251,11 +260,7 @@ class ModelTable:
|
|
|
251
260
|
merged = {sink.symbol: sink for sink in self.sinks}
|
|
252
261
|
for sink in sinks:
|
|
253
262
|
current = merged.get(sink.symbol)
|
|
254
|
-
merged[sink.symbol] = (
|
|
255
|
-
Sink(sink.symbol, current.kinds | sink.kinds, current.positions + sink.positions)
|
|
256
|
-
if current is not None
|
|
257
|
-
else sink
|
|
258
|
-
)
|
|
263
|
+
merged[sink.symbol] = sink if current is None else current.merged(sink)
|
|
259
264
|
return ModelTable(
|
|
260
265
|
self.sources,
|
|
261
266
|
tuple(merged.values()),
|
|
@@ -279,15 +284,31 @@ class SecurityModelRegistry:
|
|
|
279
284
|
|
|
280
285
|
def __init__(self) -> None:
|
|
281
286
|
self._models: dict[tuple[type[Model], SymbolId], Model] = {}
|
|
287
|
+
self._origins: dict[tuple[type[Model], SymbolId], str | None] = {}
|
|
288
|
+
|
|
289
|
+
def register(self, *models: Model, origin: str | None = None) -> None:
|
|
290
|
+
"""Add models; ``origin`` names the plugin so a conflict can name both sides.
|
|
291
|
+
|
|
292
|
+
Two plugins may describe the same symbol: an identical model is ignored, sinks
|
|
293
|
+
merge their kinds and positions, any other difference is a conflict."""
|
|
282
294
|
|
|
283
|
-
def register(self, *models: Model) -> None:
|
|
284
295
|
for model in models:
|
|
285
296
|
key = (type(model), model.symbol)
|
|
286
|
-
|
|
297
|
+
current = self._models.get(key)
|
|
298
|
+
if current is None:
|
|
299
|
+
self._models[key] = model
|
|
300
|
+
self._origins[key] = origin
|
|
301
|
+
elif current == model:
|
|
302
|
+
continue
|
|
303
|
+
elif isinstance(model, Sink) and isinstance(current, Sink):
|
|
304
|
+
self._models[key] = current.merged(model)
|
|
305
|
+
else:
|
|
306
|
+
registered = self._origins[key]
|
|
307
|
+
sides = f" by {registered!r} and {origin!r}" if registered and origin else ""
|
|
287
308
|
raise ModelError(
|
|
288
|
-
f"{type(model).__name__.lower()} model for {model.symbol}
|
|
309
|
+
f"{type(model).__name__.lower()} model for {model.symbol}"
|
|
310
|
+
f" is registered twice{sides} with different values"
|
|
289
311
|
)
|
|
290
|
-
self._models[key] = model
|
|
291
312
|
|
|
292
313
|
def freeze(self) -> ModelTable:
|
|
293
314
|
models = list(self._models.values())
|
{coretrace_python_analyzer-0.2.0.dist-info → coretrace_python_analyzer-0.3.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.3.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.2.0.dist-info → coretrace_python_analyzer-0.3.0.dist-info}/RECORD
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
coretrace_python/__init__.py,sha256=
|
|
1
|
+
coretrace_python/__init__.py,sha256=o079IGFBNi5YGVym-lPxQ5BTW75JafwRhu2pSI-MMjE,75
|
|
2
2
|
coretrace_python/__main__.py,sha256=V8DnYtRboISK2VCKm7DGHLREKvqLbqApyWlKBOs26wU,65
|
|
3
|
-
coretrace_python/cache.py,sha256=
|
|
3
|
+
coretrace_python/cache.py,sha256=3NoJIz_lHC6gKrq72dJA89ZDyfXJ4jqI646631uPbDM,11316
|
|
4
4
|
coretrace_python/cli.py,sha256=ZE6F8wqNcE-ogfldii-UgUq4nOcEMnHuMCzz0l--yuw,9792
|
|
5
|
-
coretrace_python/engine.py,sha256=
|
|
5
|
+
coretrace_python/engine.py,sha256=TWEnRs-QdBMzI2UYzj2IacwPfJAVj7M7PAXs5RuLiRY,27174
|
|
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
|
|
@@ -119,7 +119,7 @@ coretrace_python/ir/ssa.py,sha256=cIYbcv90PI6AOeMrw2ZWp88fy7E2ZO4P3tqjtPHra7g,11
|
|
|
119
119
|
coretrace_python/plugins/__init__.py,sha256=Jh5oLVI_Rc7ZzibsMjJzg69LGeNcRRjLcY5XaVOhjgA,1308
|
|
120
120
|
coretrace_python/plugins/api.py,sha256=y1bicTdPypstWs7aJ4JDIOSVKX-MwFq5NSjGntTZhDw,5309
|
|
121
121
|
coretrace_python/plugins/detectors.py,sha256=ej5Z_MS0CBqrR4b9xycjTVUIo5arpArpdbr7nL732iI,5700
|
|
122
|
-
coretrace_python/plugins/loader.py,sha256=
|
|
122
|
+
coretrace_python/plugins/loader.py,sha256=Ozj7NELI1ib3uoAPMsaImYmCJN3HKAjU0BZtQdFs5IQ,3837
|
|
123
123
|
coretrace_python/plugins/manifest.py,sha256=F24pgQSwApRUL6ZihRSLv9wL_M03A_pZjrj9Zi4Ku3Q,3438
|
|
124
124
|
coretrace_python/plugins/registry.py,sha256=ixKcT2YDky96TWCFckdzKnGRmFn1dbBVMSZLPvdxvY8,1177
|
|
125
125
|
coretrace_python/plugins/secrets.py,sha256=fpCxXpz3u7a-p1mAabC99FK_DpC1RSg62lLPe96pGy8,15162
|
|
@@ -137,12 +137,12 @@ coretrace_python/source/__init__.py,sha256=0Y8fvtbkETnNCJR9Zq7RS5TSgF9c22pz3SSk1
|
|
|
137
137
|
coretrace_python/source/manager.py,sha256=JRmMOcWfS1nocbVrdcvfQTqEfIkmnb5JP857TpGywN4,2736
|
|
138
138
|
coretrace_python/source/model.py,sha256=FZnN5mlir3tzbIpWoxnn6YtRi9UcJ_y0sZM60tKr8U4,1727
|
|
139
139
|
coretrace_python/taint/__init__.py,sha256=D-NH3kx9wKTNeWASewawGAx7ChOkGMGhmjf-E4crHl0,1111
|
|
140
|
-
coretrace_python/taint/engine.py,sha256=
|
|
141
|
-
coretrace_python/taint/models.py,sha256=
|
|
140
|
+
coretrace_python/taint/engine.py,sha256=XJGv7UBeKV4k1oUpDlYPotxDPrDo45-TIIt_vw9XBhM,35325
|
|
141
|
+
coretrace_python/taint/models.py,sha256=H-Q_nChzwqm-MEGfbfw5jMegpXlIfwUIunFiRXbE90I,12005
|
|
142
142
|
coretrace_python/taint/routes.py,sha256=UAJKXcw_sM9shf58FfDroxVO9bNSm4qrQdZknwUB0lo,6110
|
|
143
|
-
coretrace_python_analyzer-0.
|
|
144
|
-
coretrace_python_analyzer-0.
|
|
145
|
-
coretrace_python_analyzer-0.
|
|
146
|
-
coretrace_python_analyzer-0.
|
|
147
|
-
coretrace_python_analyzer-0.
|
|
148
|
-
coretrace_python_analyzer-0.
|
|
143
|
+
coretrace_python_analyzer-0.3.0.dist-info/METADATA,sha256=NmKBjMkxToUaG9ZAPRnnGShune4UJtnzvgWQVx7qC4k,3216
|
|
144
|
+
coretrace_python_analyzer-0.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
145
|
+
coretrace_python_analyzer-0.3.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
|
|
146
|
+
coretrace_python_analyzer-0.3.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
147
|
+
coretrace_python_analyzer-0.3.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
|
|
148
|
+
coretrace_python_analyzer-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|