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.
Files changed (126) hide show
  1. coretrace_python/__init__.py +4 -0
  2. coretrace_python/__main__.py +4 -0
  3. coretrace_python/abstract/__init__.py +45 -0
  4. coretrace_python/abstract/constants.py +226 -0
  5. coretrace_python/abstract/heap.py +252 -0
  6. coretrace_python/abstract/ranges.py +285 -0
  7. coretrace_python/abstract/values.py +56 -0
  8. coretrace_python/analysis/__init__.py +31 -0
  9. coretrace_python/analysis/manager.py +165 -0
  10. coretrace_python/analysis/provider.py +73 -0
  11. coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
  12. coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
  13. coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
  14. coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
  15. coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
  16. coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
  17. coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
  18. coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
  19. coretrace_python/bundled/models/cli/cli_models.py +35 -0
  20. coretrace_python/bundled/models/cli/plugin.toml +9 -0
  21. coretrace_python/bundled/models/credentials/credential_models.py +43 -0
  22. coretrace_python/bundled/models/credentials/plugin.toml +9 -0
  23. coretrace_python/bundled/models/django/django_models.py +123 -0
  24. coretrace_python/bundled/models/django/plugin.toml +9 -0
  25. coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
  26. coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
  27. coretrace_python/bundled/models/flask/flask_models.py +53 -0
  28. coretrace_python/bundled/models/flask/plugin.toml +9 -0
  29. coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
  30. coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
  31. coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
  32. coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
  33. coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
  34. coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
  35. coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
  36. coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
  37. coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
  38. coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
  39. coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
  40. coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
  41. coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
  42. coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
  43. coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
  44. coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
  45. coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
  46. coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
  47. coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
  48. coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
  49. coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
  50. coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
  51. coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
  52. coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
  53. coretrace_python/bundled/security/xss/plugin.toml +9 -0
  54. coretrace_python/bundled/security/xss/xss.py +17 -0
  55. coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
  56. coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
  57. coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
  58. coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
  59. coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
  60. coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
  61. coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
  62. coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
  63. coretrace_python/cache.py +310 -0
  64. coretrace_python/cfg/__init__.py +46 -0
  65. coretrace_python/cfg/builder.py +589 -0
  66. coretrace_python/cfg/dominance.py +183 -0
  67. coretrace_python/cfg/model.py +166 -0
  68. coretrace_python/cli.py +216 -0
  69. coretrace_python/dataflow/__init__.py +29 -0
  70. coretrace_python/dataflow/lattice.py +78 -0
  71. coretrace_python/dataflow/solver.py +96 -0
  72. coretrace_python/dependency/__init__.py +44 -0
  73. coretrace_python/dependency/advisories.py +168 -0
  74. coretrace_python/dependency/correlation.py +87 -0
  75. coretrace_python/dependency/graph.py +274 -0
  76. coretrace_python/dependency/policy.py +65 -0
  77. coretrace_python/dependency/sbom.py +65 -0
  78. coretrace_python/engine.py +688 -0
  79. coretrace_python/findings/__init__.py +13 -0
  80. coretrace_python/findings/coverage.py +45 -0
  81. coretrace_python/findings/model.py +49 -0
  82. coretrace_python/findings/refutation.py +427 -0
  83. coretrace_python/frontend/__init__.py +18 -0
  84. coretrace_python/frontend/ast_adapter.py +447 -0
  85. coretrace_python/frontend/parser.py +23 -0
  86. coretrace_python/hir/__init__.py +5 -0
  87. coretrace_python/hir/nodes.py +584 -0
  88. coretrace_python/hir/visitors.py +36 -0
  89. coretrace_python/interprocedural/__init__.py +49 -0
  90. coretrace_python/interprocedural/callgraph.py +291 -0
  91. coretrace_python/interprocedural/modulegraph.py +218 -0
  92. coretrace_python/interprocedural/summaries.py +463 -0
  93. coretrace_python/ir/__init__.py +5 -0
  94. coretrace_python/ir/defuse.py +74 -0
  95. coretrace_python/ir/lowering.py +575 -0
  96. coretrace_python/ir/model.py +481 -0
  97. coretrace_python/ir/printer.py +201 -0
  98. coretrace_python/ir/ssa.py +277 -0
  99. coretrace_python/plugins/__init__.py +58 -0
  100. coretrace_python/plugins/api.py +153 -0
  101. coretrace_python/plugins/detectors.py +114 -0
  102. coretrace_python/plugins/loader.py +84 -0
  103. coretrace_python/plugins/manifest.py +112 -0
  104. coretrace_python/plugins/registry.py +34 -0
  105. coretrace_python/plugins/secrets.py +330 -0
  106. coretrace_python/reporters/__init__.py +22 -0
  107. coretrace_python/reporters/json_format.py +48 -0
  108. coretrace_python/reporters/report.py +23 -0
  109. coretrace_python/reporters/sarif.py +70 -0
  110. coretrace_python/reporters/text.py +24 -0
  111. coretrace_python/semantic/__init__.py +9 -0
  112. coretrace_python/semantic/identity.py +39 -0
  113. coretrace_python/semantic/imports.py +131 -0
  114. coretrace_python/semantic/scopes.py +473 -0
  115. coretrace_python/semantic/symbols.py +87 -0
  116. coretrace_python/source/__init__.py +13 -0
  117. coretrace_python/source/manager.py +81 -0
  118. coretrace_python/source/model.py +57 -0
  119. coretrace_python/taint/__init__.py +55 -0
  120. coretrace_python/taint/engine.py +800 -0
  121. coretrace_python/taint/models.py +317 -0
  122. coretrace_python/taint/routes.py +99 -0
  123. coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
  124. coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
  125. coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
  126. coretrace_python_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,310 @@
1
+ """Persistent per-module cache (architecture §11, §38 Phase 10).
2
+
3
+ A module's results are stored under a key derived from everything they depend on: its
4
+ source text and identity, the engine, schema and plugin API versions, the plugins and
5
+ their code, the security models, the advisories, the dependency graph, and the keys of
6
+ the project modules it imports transitively. A module whose key is unchanged on a later
7
+ run is served from the cache: its summaries seed the project index, its call sites serve
8
+ the project plugins and its findings are reported as they were. Entries are JSON, so a
9
+ tampered or foreign file can never execute anything; an unreadable entry is a miss.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import json
16
+ import os
17
+ from collections.abc import Iterable, Mapping
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from types import MappingProxyType
21
+ from typing import Any
22
+
23
+ from coretrace_python.findings import Confidence, Finding, Severity
24
+ from coretrace_python.interprocedural import (
25
+ CallSite,
26
+ ExternalCall,
27
+ ExternalSymbol,
28
+ FunctionSummary,
29
+ KnownFunction,
30
+ ModuleGraph,
31
+ Mutation,
32
+ SummaryIndex,
33
+ Target,
34
+ UnknownTarget,
35
+ )
36
+ from coretrace_python.semantic.symbols import SymbolId
37
+ from coretrace_python.source import SourceId, SourceSpan
38
+
39
+ CACHE_FORMAT = 2
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class CachedModule:
44
+ """Everything a later run needs from one module without lowering it again."""
45
+
46
+ functions: tuple[str, ...]
47
+ summaries: Mapping[str, FunctionSummary]
48
+ sites: tuple[CallSite, ...]
49
+ findings: tuple[Finding, ...]
50
+
51
+ def __post_init__(self) -> None:
52
+ object.__setattr__(self, "summaries", MappingProxyType(dict(self.summaries)))
53
+
54
+
55
+ # --------------------------------------------------------------------------- keys
56
+
57
+
58
+ def fingerprint(*parts: str) -> str:
59
+ digest = hashlib.sha256()
60
+ for part in parts:
61
+ digest.update(part.encode("utf-8"))
62
+ digest.update(b"\0")
63
+ return digest.hexdigest()
64
+
65
+
66
+ def directory_fingerprint(directory: Path, suffixes: Iterable[str] = (".py", ".toml")) -> str:
67
+ """A digest of the source files under ``directory``, so edited plugin code misses."""
68
+
69
+ wanted = tuple(suffixes)
70
+ parts: list[str] = []
71
+ for path in sorted(directory.rglob("*")):
72
+ if path.is_file() and path.suffix in wanted:
73
+ parts.append(str(path.relative_to(directory)))
74
+ parts.append(path.read_text(encoding="utf-8", errors="replace"))
75
+ return fingerprint(*parts)
76
+
77
+
78
+ def module_keys(graph: ModuleGraph, own: Mapping[str, str]) -> dict[str, str]:
79
+ """The key of each module: its own key plus those of every module it imports,
80
+ transitively, so a change anywhere below a module misses for that module too."""
81
+
82
+ keys: dict[str, str] = {}
83
+ for name in graph.modules:
84
+ closure: set[str] = set()
85
+ pending = [name]
86
+ while pending:
87
+ current = pending.pop()
88
+ for imported in graph.imports(current):
89
+ if imported in own and imported not in closure and imported != name:
90
+ closure.add(imported)
91
+ pending.append(imported)
92
+ keys[name] = fingerprint(own[name], *(own[m] for m in sorted(closure)))
93
+ return keys
94
+
95
+
96
+ # --------------------------------------------------------------------------- store
97
+
98
+
99
+ class ProjectCache:
100
+ """A directory of JSON entries, one per module key."""
101
+
102
+ def __init__(self, directory: Path) -> None:
103
+ self.directory = directory
104
+
105
+ def load(self, key: str) -> CachedModule | None:
106
+ path = self.directory / f"{key}.json"
107
+ try:
108
+ with path.open(encoding="utf-8") as handle:
109
+ return decode(json.load(handle))
110
+ except (OSError, ValueError, KeyError, TypeError, IndexError, AttributeError):
111
+ return None
112
+
113
+ def store(self, key: str, module: CachedModule) -> None:
114
+ self.directory.mkdir(parents=True, exist_ok=True)
115
+ path = self.directory / f"{key}.json"
116
+ temporary = path.with_name(f"{path.name}.{os.getpid()}.tmp")
117
+ temporary.write_text(json.dumps(encode(module)), encoding="utf-8")
118
+ temporary.replace(path)
119
+
120
+
121
+ # --------------------------------------------------------------------------- codec
122
+
123
+
124
+ def encode(module: CachedModule) -> dict[str, Any]:
125
+ return {
126
+ "format": CACHE_FORMAT,
127
+ "functions": list(module.functions),
128
+ "summaries": {name: _encode_summary(s) for name, s in module.summaries.items()},
129
+ "sites": [_encode_site(site) for site in module.sites],
130
+ "findings": [_encode_finding(finding) for finding in module.findings],
131
+ }
132
+
133
+
134
+ def decode(data: Mapping[str, Any]) -> CachedModule:
135
+ if data["format"] != CACHE_FORMAT:
136
+ raise ValueError(f"unsupported cache format {data['format']!r}")
137
+ return CachedModule(
138
+ tuple(_string(name) for name in data["functions"]),
139
+ {_string(name): _decode_summary(s) for name, s in data["summaries"].items()},
140
+ tuple(_decode_site(site) for site in data["sites"]),
141
+ tuple(_decode_finding(finding) for finding in data["findings"]),
142
+ )
143
+
144
+
145
+ def encode_index(index: SummaryIndex) -> dict[str, Any]:
146
+ """A summary index as plain data, to hand a worker process what it imports."""
147
+
148
+ return {str(symbol): _encode_summary(index.summaries[symbol]) for symbol in index.symbols}
149
+
150
+
151
+ def decode_index(data: Mapping[str, Any]) -> SummaryIndex:
152
+ return SummaryIndex({SymbolId(_string(k)): _decode_summary(v) for k, v in data.items()})
153
+
154
+
155
+ def _string(value: Any) -> str:
156
+ if not isinstance(value, str):
157
+ raise TypeError(f"expected a string, got {value!r}")
158
+ return value
159
+
160
+
161
+ def _integer(value: Any) -> int:
162
+ if not isinstance(value, int) or isinstance(value, bool):
163
+ raise TypeError(f"expected an integer, got {value!r}")
164
+ return value
165
+
166
+
167
+ def _indices(values: Any) -> frozenset[int]:
168
+ return frozenset(_integer(v) for v in values)
169
+
170
+
171
+ def _encode_span(span: SourceSpan) -> list[Any]:
172
+ return [str(span.source_id), span.start_line, span.start_column, span.end_line, span.end_column]
173
+
174
+
175
+ def _decode_span(data: Any) -> SourceSpan:
176
+ file, line, column, end_line, end_column = data
177
+ return SourceSpan(
178
+ SourceId(_string(file)),
179
+ _integer(line),
180
+ _integer(column),
181
+ None if end_line is None else _integer(end_line),
182
+ None if end_column is None else _integer(end_column),
183
+ )
184
+
185
+
186
+ def _encode_finding(finding: Finding) -> dict[str, Any]:
187
+ return {
188
+ "rule": finding.rule_id,
189
+ "message": finding.message,
190
+ "severity": finding.severity.value,
191
+ "confidence": finding.confidence.value,
192
+ "span": _encode_span(finding.span),
193
+ "function": finding.function,
194
+ "metadata": dict(finding.metadata),
195
+ }
196
+
197
+
198
+ def _decode_finding(data: Mapping[str, Any]) -> Finding:
199
+ function = data["function"]
200
+ return Finding(
201
+ _string(data["rule"]),
202
+ _string(data["message"]),
203
+ Severity(data["severity"]),
204
+ Confidence(data["confidence"]),
205
+ _decode_span(data["span"]),
206
+ None if function is None else _string(function),
207
+ {_string(k): _string(v) for k, v in data["metadata"].items()},
208
+ )
209
+
210
+
211
+ def _encode_call(call: ExternalCall) -> dict[str, Any]:
212
+ return {
213
+ "symbol": str(call.symbol),
214
+ "arguments": [sorted(deps) for deps in call.argument_dependencies],
215
+ "keywords": sorted(call.keyword_dependencies),
216
+ "location": _encode_span(call.location),
217
+ "call_site": None if call.call_site is None else _encode_span(call.call_site),
218
+ }
219
+
220
+
221
+ def _decode_call(data: Mapping[str, Any]) -> ExternalCall:
222
+ site = data["call_site"]
223
+ return ExternalCall(
224
+ SymbolId(_string(data["symbol"])),
225
+ tuple(_indices(deps) for deps in data["arguments"]),
226
+ _indices(data["keywords"]),
227
+ _decode_span(data["location"]),
228
+ None if site is None else _decode_span(site),
229
+ )
230
+
231
+
232
+ def _encode_summary(summary: FunctionSummary) -> dict[str, Any]:
233
+ return {
234
+ "name": summary.name,
235
+ "parameters": summary.parameters,
236
+ "returns": sorted(summary.return_dependencies),
237
+ "external_calls": [_encode_call(call) for call in summary.external_calls],
238
+ "unsupported": summary.unsupported,
239
+ "return_externals": sorted(str(s) for s in summary.return_externals),
240
+ "mutations": [
241
+ {
242
+ "parameter": m.parameter,
243
+ "field": m.field,
244
+ "dependencies": sorted(m.dependencies),
245
+ "externals": sorted(str(s) for s in m.externals),
246
+ }
247
+ for m in summary.mutations
248
+ ],
249
+ "side_effects": sorted(summary.side_effects),
250
+ }
251
+
252
+
253
+ def _decode_summary(data: Mapping[str, Any]) -> FunctionSummary:
254
+ return FunctionSummary(
255
+ _string(data["name"]),
256
+ _integer(data["parameters"]),
257
+ _indices(data["returns"]),
258
+ tuple(_decode_call(call) for call in data["external_calls"]),
259
+ bool(data["unsupported"]),
260
+ frozenset(SymbolId(_string(s)) for s in data["return_externals"]),
261
+ tuple(
262
+ Mutation(
263
+ _integer(m["parameter"]),
264
+ _string(m["field"]),
265
+ _indices(m["dependencies"]),
266
+ frozenset(SymbolId(_string(s)) for s in m["externals"]),
267
+ )
268
+ for m in data["mutations"]
269
+ ),
270
+ frozenset(_string(name) for name in data["side_effects"]),
271
+ )
272
+
273
+
274
+ def _encode_target(target: Target) -> dict[str, Any]:
275
+ if isinstance(target, KnownFunction):
276
+ return {"kind": "known", "name": target.name}
277
+ if isinstance(target, ExternalSymbol):
278
+ return {"kind": "external", "symbol": str(target.symbol)}
279
+ return {"kind": "unknown"}
280
+
281
+
282
+ def _decode_target(data: Mapping[str, Any]) -> Target:
283
+ kind = data["kind"]
284
+ if kind == "known":
285
+ return KnownFunction(_string(data["name"]))
286
+ if kind == "external":
287
+ return ExternalSymbol(SymbolId(_string(data["symbol"])))
288
+ if kind == "unknown":
289
+ return UnknownTarget()
290
+ raise ValueError(f"unknown call target kind {kind!r}")
291
+
292
+
293
+ def _encode_site(site: CallSite) -> dict[str, Any]:
294
+ return {
295
+ "caller": site.caller,
296
+ "location": _encode_span(site.location),
297
+ "target": _encode_target(site.target),
298
+ "arguments": site.arguments,
299
+ "keywords": site.keywords,
300
+ }
301
+
302
+
303
+ def _decode_site(data: Mapping[str, Any]) -> CallSite:
304
+ return CallSite(
305
+ _string(data["caller"]),
306
+ _decode_span(data["location"]),
307
+ _decode_target(data["target"]),
308
+ _integer(data["arguments"]),
309
+ _integer(data["keywords"]),
310
+ )
@@ -0,0 +1,46 @@
1
+ """Per-function control-flow graphs over PyHIR (architecture §5)."""
2
+
3
+ from coretrace_python.cfg.builder import CFGAnalysis, build_cfg
4
+ from coretrace_python.cfg.dominance import (
5
+ EXIT,
6
+ DominanceAnalysis,
7
+ DominatorTree,
8
+ PostDominanceAnalysis,
9
+ dominator_tree,
10
+ post_dominator_tree,
11
+ )
12
+ from coretrace_python.cfg.model import (
13
+ CFG,
14
+ BasicBlock,
15
+ BlockId,
16
+ Branch,
17
+ CFGError,
18
+ ForEach,
19
+ Jump,
20
+ Raise,
21
+ Return,
22
+ Terminator,
23
+ targets,
24
+ )
25
+
26
+ __all__ = [
27
+ "CFG",
28
+ "EXIT",
29
+ "BasicBlock",
30
+ "BlockId",
31
+ "Branch",
32
+ "CFGAnalysis",
33
+ "CFGError",
34
+ "DominanceAnalysis",
35
+ "DominatorTree",
36
+ "ForEach",
37
+ "Jump",
38
+ "PostDominanceAnalysis",
39
+ "Raise",
40
+ "Return",
41
+ "Terminator",
42
+ "build_cfg",
43
+ "dominator_tree",
44
+ "post_dominator_tree",
45
+ "targets",
46
+ ]