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.
@@ -1,4 +1,4 @@
1
1
  """CoreTrace's Python static analysis frontend."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
4
4
 
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 Iterable, Mapping
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, suffixes: Iterable[str] = (".py", ".toml")) -> str:
68
- """A digest of the source files under ``directory``, so edited plugin code misses."""
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
- if path.is_file() and path.suffix in wanted:
74
- parts.append(str(path.relative_to(directory)))
75
- parts.append(path.read_text(encoding="utf-8", errors="replace"))
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
 
@@ -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
- module_path = directory / f"{manifest.entrypoint.module}.py"
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
- spec = importlib.util.spec_from_file_location(f"coretrace_plugin_{unique}", module_path)
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)
@@ -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
- return self.known(
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
- return self.known(
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
 
@@ -30,10 +30,14 @@ class TaintKind(Flag):
30
30
  ADVISORY = auto()
31
31
  DESERIALIZATION = auto()
32
32
  REDIRECT = auto()
33
- ALL = SQL | COMMAND | HTML | PATH | SSRF | CODE | ADVISORY | DESERIALIZATION | REDIRECT
34
- # Outside ALL on purpose: only credential-named parameters carry it, so a database
35
- # write reached by ordinary input is not a plaintext credential.
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
- if key in self._models:
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} is already registered"
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())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coretrace-python-analyzer
3
- Version: 0.2.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
@@ -1,8 +1,8 @@
1
- coretrace_python/__init__.py,sha256=Aitn8kEWC3OxAz_1TliVDkeY0-Dz9mZf-S09r7oSD4M,75
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=267JaeUore7nmYwCRQ5dv4VIESyeFkQg4MqLPXOyUcc,11194
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=RDuwxwMOV1pT7fX2T9WU9EfoBn_9dNXOcY-7ztNpb5U,27154
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=e7S6l3VhMpku0HpnAl5qbBRFiOlsSf66P7low7RsRvU,3174
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=QedazdxW-5jwiYbZWFI7AyQycSQWNDVOVbkPIKkI0i8,34667
141
- coretrace_python/taint/models.py,sha256=s7luO5vMaowxPcWoThkLTEdYuyw0JZZVkaKYW5o_xOI,10930
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.2.0.dist-info/METADATA,sha256=Cztxh-bWrr4a_Ttxusa8jsRqA1i5knoRD9gbF_X4jlU,3216
144
- coretrace_python_analyzer-0.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
145
- coretrace_python_analyzer-0.2.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
146
- coretrace_python_analyzer-0.2.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
147
- coretrace_python_analyzer-0.2.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
148
- coretrace_python_analyzer-0.2.0.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any