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,183 @@
1
+ """Dominance and post-dominance over control-flow graphs (architecture §5).
2
+
3
+ Both trees come from the same iterative algorithm (Cooper, Harvey and Kennedy) run on
4
+ the forward graph from the entry, or on the reversed graph from a virtual ``EXIT``
5
+ that follows every returning or raising block. The post-dominance frontier is the
6
+ control-dependence relation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable, Iterable, Mapping
12
+ from dataclasses import dataclass, field
13
+ from types import MappingProxyType
14
+ from typing import ClassVar
15
+
16
+ from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
17
+ from coretrace_python.cfg.builder import CFGAnalysis
18
+ from coretrace_python.cfg.model import CFG, BlockId
19
+ from coretrace_python.hir import nodes
20
+
21
+ EXIT = BlockId("<exit>")
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class DominatorTree:
26
+ root: BlockId
27
+ blocks: tuple[BlockId, ...]
28
+ _idom: Mapping[BlockId, BlockId | None]
29
+ _children: Mapping[BlockId, tuple[BlockId, ...]]
30
+ _frontier: Mapping[BlockId, frozenset[BlockId]]
31
+ _depth: Mapping[BlockId, int] = field(init=False, repr=False, compare=False)
32
+
33
+ def __post_init__(self) -> None:
34
+ depth: dict[BlockId, int] = {}
35
+ for block in self.blocks:
36
+ parent = self._idom[block]
37
+ depth[block] = 0 if parent is None else depth[parent] + 1
38
+ object.__setattr__(self, "_depth", MappingProxyType(depth))
39
+
40
+ def idom(self, block: BlockId) -> BlockId | None:
41
+ return self._idom[block]
42
+
43
+ def children(self, block: BlockId) -> tuple[BlockId, ...]:
44
+ return self._children[block]
45
+
46
+ def frontier(self, block: BlockId) -> frozenset[BlockId]:
47
+ return self._frontier[block]
48
+
49
+ def dominates(self, dominator: BlockId, block: BlockId) -> bool:
50
+ if dominator not in self._idom or block not in self._idom:
51
+ return False
52
+ current: BlockId | None = block
53
+ while current is not None and self._depth[current] >= self._depth[dominator]:
54
+ if current == dominator:
55
+ return True
56
+ current = self._idom[current]
57
+ return False
58
+
59
+ def iterated_frontier(self, blocks: Iterable[BlockId]) -> frozenset[BlockId]:
60
+ result: set[BlockId] = set()
61
+ pending = list(blocks)
62
+ while pending:
63
+ for candidate in self._frontier[pending.pop()]:
64
+ if candidate not in result:
65
+ result.add(candidate)
66
+ pending.append(candidate)
67
+ return frozenset(result)
68
+
69
+
70
+ def _build(
71
+ root: BlockId,
72
+ successors: Callable[[BlockId], tuple[BlockId, ...]],
73
+ order: Callable[[BlockId], int],
74
+ ) -> DominatorTree:
75
+ # Reverse postorder over the reachable nodes.
76
+ postorder: list[BlockId] = []
77
+ seen: set[BlockId] = set()
78
+
79
+ def visit(node: BlockId) -> None:
80
+ seen.add(node)
81
+ for successor in successors(node):
82
+ if successor not in seen:
83
+ visit(successor)
84
+ postorder.append(node)
85
+
86
+ visit(root)
87
+ rpo = list(reversed(postorder))
88
+ index = {node: position for position, node in enumerate(rpo)}
89
+ predecessors: dict[BlockId, list[BlockId]] = {node: [] for node in rpo}
90
+ for node in rpo:
91
+ for successor in successors(node):
92
+ predecessors[successor].append(node)
93
+
94
+ idom: dict[BlockId, BlockId | None] = {root: None}
95
+
96
+ def intersect(a: BlockId, b: BlockId) -> BlockId:
97
+ while a != b:
98
+ while index[a] > index[b]:
99
+ a = idom[a] # type: ignore[assignment]
100
+ while index[b] > index[a]:
101
+ b = idom[b] # type: ignore[assignment]
102
+ return a
103
+
104
+ changed = True
105
+ while changed:
106
+ changed = False
107
+ for node in rpo[1:]:
108
+ candidates = [p for p in predecessors[node] if p in idom]
109
+ new_idom = candidates[0]
110
+ for other in candidates[1:]:
111
+ new_idom = intersect(other, new_idom)
112
+ if idom.get(node) != new_idom:
113
+ idom[node] = new_idom
114
+ changed = True
115
+
116
+ children: dict[BlockId, list[BlockId]] = {node: [] for node in rpo}
117
+ for node in sorted(rpo, key=order):
118
+ parent = idom[node]
119
+ if parent is not None:
120
+ children[parent].append(node)
121
+
122
+ frontier: dict[BlockId, set[BlockId]] = {node: set() for node in rpo}
123
+ for node in rpo:
124
+ if node == root or len(predecessors[node]) < 2:
125
+ continue
126
+ for predecessor in predecessors[node]:
127
+ runner: BlockId | None = predecessor
128
+ while runner is not None and runner != idom[node]:
129
+ frontier[runner].add(node)
130
+ runner = idom[runner]
131
+
132
+ preorder: list[BlockId] = []
133
+
134
+ def walk(node: BlockId) -> None:
135
+ preorder.append(node)
136
+ for child in children[node]:
137
+ walk(child)
138
+
139
+ walk(root)
140
+ return DominatorTree(
141
+ root=root,
142
+ blocks=tuple(preorder),
143
+ _idom=MappingProxyType(dict(idom)),
144
+ _children=MappingProxyType({k: tuple(v) for k, v in children.items()}),
145
+ _frontier=MappingProxyType({k: frozenset(v) for k, v in frontier.items()}),
146
+ )
147
+
148
+
149
+ def dominator_tree(cfg: CFG) -> DominatorTree:
150
+ order = {block_id: position for position, block_id in enumerate(cfg.blocks)}
151
+ return _build(cfg.entry, cfg.successors, lambda block: order[block])
152
+
153
+
154
+ def post_dominator_tree(cfg: CFG) -> DominatorTree:
155
+ reachable = cfg.reachable()
156
+ order = {block_id: position for position, block_id in enumerate(cfg.blocks)}
157
+ order[EXIT] = -1
158
+ leaves = tuple(b for b in cfg.blocks if b in reachable and not cfg.successors(b))
159
+
160
+ def reversed_successors(block: BlockId) -> tuple[BlockId, ...]:
161
+ if block == EXIT:
162
+ return leaves
163
+ return tuple(p for p in cfg.predecessors(block) if p in reachable)
164
+
165
+ return _build(EXIT, reversed_successors, lambda block: order[block])
166
+
167
+
168
+ class DominanceAnalysis(FunctionAnalysis[DominatorTree]):
169
+ name: ClassVar[str] = "cfg.dominance"
170
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({CFGAnalysis})
171
+
172
+ @classmethod
173
+ def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> DominatorTree:
174
+ return dominator_tree(ctx.get(CFGAnalysis, function))
175
+
176
+
177
+ class PostDominanceAnalysis(FunctionAnalysis[DominatorTree]):
178
+ name: ClassVar[str] = "cfg.post_dominance"
179
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({CFGAnalysis})
180
+
181
+ @classmethod
182
+ def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> DominatorTree:
183
+ return post_dominator_tree(ctx.get(CFGAnalysis, function))
@@ -0,0 +1,166 @@
1
+ """Control-flow graph model (architecture §5).
2
+
3
+ A block holds straight-line PyHIR statements and ends in exactly one terminator.
4
+ Python's iteration protocol stays explicit as ``ForEach`` instead of being encoded as
5
+ a generic switch; the CFG must remain semantically faithful to Python.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping
11
+ from dataclasses import dataclass, field
12
+ from types import MappingProxyType
13
+ from typing import TypeAlias
14
+
15
+ from coretrace_python.hir import nodes
16
+ from coretrace_python.source import SourceSpan
17
+
18
+
19
+ class CFGError(Exception):
20
+ """A malformed graph or a control statement used outside its construct."""
21
+
22
+
23
+ @dataclass(frozen=True, order=True)
24
+ class BlockId:
25
+ value: str
26
+
27
+ def __str__(self) -> str:
28
+ return self.value
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class Branch:
33
+ condition: nodes.Expression
34
+ then_block: BlockId
35
+ else_block: BlockId
36
+ span: SourceSpan
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Jump:
41
+ target: BlockId
42
+ span: SourceSpan
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class Return:
47
+ value: nodes.Expression | None
48
+ span: SourceSpan
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class Raise:
53
+ exception: nodes.Expression | None
54
+ span: SourceSpan
55
+ cause: nodes.Expression | None = None
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class ForEach:
60
+ """Bind the next item of ``iterable`` to ``target`` and enter ``body``, or ``exit``."""
61
+
62
+ target: nodes.Name
63
+ iterable: nodes.Expression
64
+ body: BlockId
65
+ exit: BlockId
66
+ span: SourceSpan
67
+
68
+
69
+ Terminator: TypeAlias = Branch | Jump | Return | Raise | ForEach
70
+
71
+
72
+ def targets(terminator: Terminator) -> tuple[BlockId, ...]:
73
+ if isinstance(terminator, Branch):
74
+ return (terminator.then_block, terminator.else_block)
75
+ if isinstance(terminator, Jump):
76
+ return (terminator.target,)
77
+ if isinstance(terminator, ForEach):
78
+ return (terminator.body, terminator.exit)
79
+ return ()
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class BasicBlock:
84
+ id: BlockId
85
+ statements: tuple[nodes.Statement, ...]
86
+ terminator: Terminator
87
+ exception_targets: tuple[BlockId, ...] = ()
88
+ """Handler blocks control may reach if a statement of this block raises."""
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class CFG:
93
+ entry: BlockId
94
+ blocks: Mapping[BlockId, BasicBlock]
95
+ # Locals the builder introduced while laying out expression-level control flow
96
+ # (conditional expressions, comprehensions); the lowering treats them as locals.
97
+ synthetic_locals: frozenset[str] = frozenset()
98
+ _successors: Mapping[BlockId, tuple[BlockId, ...]] = field(
99
+ init=False, repr=False, compare=False
100
+ )
101
+ _predecessors: Mapping[BlockId, tuple[BlockId, ...]] = field(
102
+ init=False, repr=False, compare=False
103
+ )
104
+
105
+ def __post_init__(self) -> None:
106
+ blocks = MappingProxyType(dict(self.blocks))
107
+ if self.entry not in blocks:
108
+ raise CFGError(f"entry block {self.entry} does not exist")
109
+ predecessors: dict[BlockId, list[BlockId]] = {block_id: [] for block_id in blocks}
110
+ successors: dict[BlockId, tuple[BlockId, ...]] = {}
111
+ for block_id, block in blocks.items():
112
+ if block.id != block_id:
113
+ raise CFGError(f"block {block.id} is stored under {block_id}")
114
+ found = list(targets(block.terminator))
115
+ found.extend(t for t in block.exception_targets if t not in found)
116
+ successors[block_id] = tuple(found)
117
+ for target in successors[block_id]:
118
+ if target not in blocks:
119
+ raise CFGError(f"block {block_id} targets unknown block {target}")
120
+ predecessors[target].append(block_id)
121
+ object.__setattr__(self, "blocks", blocks)
122
+ object.__setattr__(self, "_successors", MappingProxyType(successors))
123
+ object.__setattr__(
124
+ self,
125
+ "_predecessors",
126
+ MappingProxyType({k: tuple(v) for k, v in predecessors.items()}),
127
+ )
128
+
129
+ def block(self, block_id: BlockId) -> BasicBlock:
130
+ return self.blocks[block_id]
131
+
132
+ def successors(self, block_id: BlockId) -> tuple[BlockId, ...]:
133
+ return self._successors[block_id]
134
+
135
+ def predecessors(self, block_id: BlockId) -> tuple[BlockId, ...]:
136
+ return self._predecessors[block_id]
137
+
138
+ def reachable(self) -> frozenset[BlockId]:
139
+ seen: set[BlockId] = set()
140
+ pending = [self.entry]
141
+ while pending:
142
+ block_id = pending.pop()
143
+ if block_id not in seen:
144
+ seen.add(block_id)
145
+ pending.extend(self.successors(block_id))
146
+ return frozenset(seen)
147
+
148
+ def back_edges(self) -> frozenset[tuple[BlockId, BlockId]]:
149
+ """Edges whose target is on the depth-first path from the entry to their source."""
150
+
151
+ found: set[tuple[BlockId, BlockId]] = set()
152
+ on_path: set[BlockId] = set()
153
+ finished: set[BlockId] = set()
154
+
155
+ def visit(block_id: BlockId) -> None:
156
+ on_path.add(block_id)
157
+ for successor in self.successors(block_id):
158
+ if successor in on_path:
159
+ found.add((block_id, successor))
160
+ elif successor not in finished:
161
+ visit(successor)
162
+ on_path.remove(block_id)
163
+ finished.add(block_id)
164
+
165
+ visit(self.entry)
166
+ return frozenset(found)
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ import zipfile
6
+ from pathlib import Path
7
+
8
+ from coretrace_python import __version__, engine
9
+ from coretrace_python.analysis import AnalysisError
10
+ from coretrace_python.cache import ProjectCache
11
+ from coretrace_python.cfg import CFGError
12
+ from coretrace_python.dependency import dump_advisories, import_osv, read_osv, render_sbom
13
+ from coretrace_python.frontend import HIRBuildError, ParseError, build_hir
14
+ from coretrace_python.ir.lowering import LoweringError, lower_module
15
+ from coretrace_python.ir.printer import format_module
16
+ from coretrace_python.plugins import IncompatiblePluginError, ManifestError
17
+ from coretrace_python.reporters import FORMATS, render
18
+ from coretrace_python.semantic.imports import ImportResolutionError
19
+ from coretrace_python.semantic.scopes import ScopeError
20
+ from coretrace_python.source import SourceManager
21
+
22
+ EXIT_CLEAN = 0
23
+ EXIT_FINDINGS = 1
24
+ EXIT_ERROR = 2
25
+
26
+ _ANALYSIS_ERRORS = (
27
+ OSError,
28
+ UnicodeError,
29
+ ParseError,
30
+ HIRBuildError,
31
+ ImportResolutionError,
32
+ ScopeError,
33
+ CFGError,
34
+ LoweringError,
35
+ AnalysisError,
36
+ ManifestError,
37
+ IncompatiblePluginError,
38
+ )
39
+
40
+
41
+ def build_parser() -> argparse.ArgumentParser:
42
+ parser = argparse.ArgumentParser(
43
+ prog=engine.TOOL_NAME,
44
+ description="Analyze Python source with CoreTrace.",
45
+ )
46
+ parser.add_argument(
47
+ "path", type=Path, nargs="?", help="Python source file, or a directory for --check"
48
+ )
49
+ parser.add_argument(
50
+ "--emit-ir",
51
+ action="store_true",
52
+ help="print the lowered Python intermediate representation",
53
+ )
54
+ parser.add_argument(
55
+ "--ssa",
56
+ action="store_true",
57
+ help="with --emit-ir, print the static single assignment form",
58
+ )
59
+ parser.add_argument(
60
+ "--check",
61
+ action="store_true",
62
+ help="run the loaded plugins and report their findings",
63
+ )
64
+ parser.add_argument(
65
+ "--plugins",
66
+ action="append",
67
+ type=Path,
68
+ default=[],
69
+ metavar="DIR",
70
+ help="with --check, a directory searched recursively for plugin.toml, loaded on "
71
+ "top of the bundled plugins (repeatable)",
72
+ )
73
+ parser.add_argument(
74
+ "--no-bundled-plugins",
75
+ action="store_true",
76
+ help="with --check, do not load the plugins shipped with the package",
77
+ )
78
+ parser.add_argument(
79
+ "--format",
80
+ choices=sorted(FORMATS),
81
+ default=None,
82
+ help="with --check, the report format (default: text)",
83
+ )
84
+ parser.add_argument(
85
+ "--cache",
86
+ type=Path,
87
+ default=None,
88
+ metavar="DIR",
89
+ help="with --check on a directory, keep per-module results under DIR and reuse "
90
+ "them for modules unchanged since the previous run",
91
+ )
92
+ parser.add_argument(
93
+ "--jobs",
94
+ type=int,
95
+ default=None,
96
+ metavar="N",
97
+ help="with --check on a directory, analyse independent modules in N processes",
98
+ )
99
+ parser.add_argument(
100
+ "--sbom",
101
+ type=Path,
102
+ default=None,
103
+ metavar="PATH",
104
+ help="with --check on a directory, write a CycloneDX bill of materials to PATH",
105
+ )
106
+ parser.add_argument(
107
+ "--advisories",
108
+ action="append",
109
+ type=Path,
110
+ default=[],
111
+ metavar="FILE",
112
+ help="with --check on a directory, a local advisory file to read in addition to "
113
+ "advisories.json at the root (repeatable)",
114
+ )
115
+ parser.add_argument(
116
+ "--policy",
117
+ type=Path,
118
+ default=None,
119
+ metavar="FILE",
120
+ help="with --check on a directory, the dependency policy to apply instead of "
121
+ "coretrace-policy.toml at the root",
122
+ )
123
+ parser.add_argument(
124
+ "--import-advisories",
125
+ nargs=2,
126
+ type=Path,
127
+ default=None,
128
+ metavar=("SRC", "OUT"),
129
+ help="convert an OSV dump (a JSON file, a directory of them or a zip archive) "
130
+ "into the local advisory file OUT, then exit",
131
+ )
132
+ return parser
133
+
134
+
135
+ def main(argv: list[str] | None = None) -> int:
136
+ args = build_parser().parse_args(argv)
137
+ if args.import_advisories is not None:
138
+ source, out = args.import_advisories
139
+ try:
140
+ out.write_text(dump_advisories(import_osv(read_osv(source))), encoding="utf-8")
141
+ except (OSError, ValueError, zipfile.BadZipFile) as error:
142
+ print(f"error: {error}", file=sys.stderr)
143
+ return EXIT_ERROR
144
+ return EXIT_CLEAN
145
+ if not (args.emit_ir or args.check):
146
+ print("error: no action selected; pass --emit-ir or --check", file=sys.stderr)
147
+ return EXIT_ERROR
148
+ if args.path is None:
149
+ print("error: a path is required", file=sys.stderr)
150
+ return EXIT_ERROR
151
+ if args.format is not None and not args.check:
152
+ print("error: --format only applies to --check", file=sys.stderr)
153
+ return EXIT_ERROR
154
+ if args.cache is not None and not (args.check and args.path.is_dir()):
155
+ print("error: --cache only applies to --check on a directory", file=sys.stderr)
156
+ return EXIT_ERROR
157
+ if args.jobs is not None and not (args.check and args.path.is_dir()):
158
+ print("error: --jobs only applies to --check on a directory", file=sys.stderr)
159
+ return EXIT_ERROR
160
+ if args.jobs is not None and args.jobs < 1:
161
+ print("error: --jobs must be at least 1", file=sys.stderr)
162
+ return EXIT_ERROR
163
+ if args.sbom is not None and not (args.check and args.path.is_dir()):
164
+ print("error: --sbom only applies to --check on a directory", file=sys.stderr)
165
+ return EXIT_ERROR
166
+ if args.advisories and not (args.check and args.path.is_dir()):
167
+ print("error: --advisories only applies to --check on a directory", file=sys.stderr)
168
+ return EXIT_ERROR
169
+ if args.policy is not None and not (args.check and args.path.is_dir()):
170
+ print("error: --policy only applies to --check on a directory", file=sys.stderr)
171
+ return EXIT_ERROR
172
+
173
+ if args.no_bundled_plugins and not args.check:
174
+ print("error: --no-bundled-plugins only applies to --check", file=sys.stderr)
175
+ return EXIT_ERROR
176
+ plugin_roots = ([] if args.no_bundled_plugins else [engine.BUNDLED_PLUGINS]) + list(args.plugins)
177
+
178
+ if args.path.is_dir() and args.emit_ir:
179
+ print(f"error: {args.path} is a directory; --emit-ir needs a file", file=sys.stderr)
180
+ return EXIT_ERROR
181
+
182
+ try:
183
+ if args.emit_ir:
184
+ source = SourceManager().load_file(args.path)
185
+ print(format_module(lower_module(build_hir(source), ssa=args.ssa)))
186
+ if args.check:
187
+ if args.path.is_dir():
188
+ cache = None if args.cache is None else ProjectCache(args.cache)
189
+ analysis = engine.analyze_project(
190
+ args.path,
191
+ plugin_roots,
192
+ cache=cache,
193
+ jobs=args.jobs or 1,
194
+ advisory_files=args.advisories,
195
+ policy_file=args.policy,
196
+ )
197
+ findings = analysis.findings
198
+ coverage = analysis.coverage
199
+ if args.sbom is not None:
200
+ args.sbom.write_text(
201
+ render_sbom(analysis.dependencies, analysis.advisories, engine.TOOL_NAME, __version__),
202
+ encoding="utf-8",
203
+ )
204
+ else:
205
+ file_analysis = engine.analyze_file(SourceManager().load_file(args.path), plugin_roots)
206
+ findings, coverage = file_analysis.findings, file_analysis.coverage
207
+ print(render(args.format or "text", engine.report(findings, coverage)), end="")
208
+ return EXIT_FINDINGS if findings else EXIT_CLEAN
209
+ except _ANALYSIS_ERRORS as error:
210
+ print(f"error: {error}", file=sys.stderr)
211
+ return EXIT_ERROR
212
+ return EXIT_CLEAN
213
+
214
+
215
+ if __name__ == "__main__":
216
+ raise SystemExit(main())
@@ -0,0 +1,29 @@
1
+ """Generic data-flow framework: lattices and a bidirectional worklist solver."""
2
+
3
+ from coretrace_python.dataflow.lattice import (
4
+ BOTTOM,
5
+ TOP,
6
+ Bottom,
7
+ Element,
8
+ FlatLattice,
9
+ Lattice,
10
+ Top,
11
+ same_element,
12
+ )
13
+ from coretrace_python.dataflow.solver import ENTRY, DataflowProblem, Direction, Solution, solve
14
+
15
+ __all__ = [
16
+ "BOTTOM",
17
+ "ENTRY",
18
+ "TOP",
19
+ "Bottom",
20
+ "DataflowProblem",
21
+ "Direction",
22
+ "Element",
23
+ "FlatLattice",
24
+ "Lattice",
25
+ "Solution",
26
+ "Top",
27
+ "same_element",
28
+ "solve",
29
+ ]
@@ -0,0 +1,78 @@
1
+ """Lattice vocabulary shared by data-flow problems (architecture §37 dataflow/lattice).
2
+
3
+ ``BOTTOM`` means "no information yet" and ``TOP`` means "anything". A ``FlatLattice``
4
+ places every other element between the two: equal elements join to themselves, different
5
+ ones to ``TOP``. Elements are compared by type as well as value, so ``1`` and ``True``
6
+ are different constants.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Final, Generic, Protocol, TypeAlias, TypeVar
12
+
13
+
14
+ class Bottom:
15
+ __slots__ = ()
16
+
17
+ def __repr__(self) -> str:
18
+ return "BOTTOM"
19
+
20
+
21
+ class Top:
22
+ __slots__ = ()
23
+
24
+ def __repr__(self) -> str:
25
+ return "TOP"
26
+
27
+
28
+ BOTTOM: Final = Bottom()
29
+ TOP: Final = Top()
30
+
31
+ T = TypeVar("T")
32
+ Element: TypeAlias = T | Bottom | Top
33
+
34
+
35
+ def same_element(a: object, b: object) -> bool:
36
+ """Equality that also requires equal types, so ``1 == True`` does not conflate."""
37
+
38
+ return type(a) is type(b) and bool(a == b)
39
+
40
+
41
+ class Lattice(Protocol[T]):
42
+ @property
43
+ def bottom(self) -> Element[T]: ...
44
+
45
+ @property
46
+ def top(self) -> Element[T]: ...
47
+
48
+ def join(self, a: Element[T], b: Element[T]) -> Element[T]: ...
49
+
50
+ def leq(self, a: Element[T], b: Element[T]) -> bool: ...
51
+
52
+
53
+ class FlatLattice(Generic[T]):
54
+ """Bottom < every element < Top, with no order between elements."""
55
+
56
+ @property
57
+ def bottom(self) -> Element[T]:
58
+ return BOTTOM
59
+
60
+ @property
61
+ def top(self) -> Element[T]:
62
+ return TOP
63
+
64
+ def join(self, a: Element[T], b: Element[T]) -> Element[T]:
65
+ if a is BOTTOM:
66
+ return b
67
+ if b is BOTTOM:
68
+ return a
69
+ if a is TOP or b is TOP:
70
+ return TOP
71
+ return a if same_element(a, b) else TOP
72
+
73
+ def leq(self, a: Element[T], b: Element[T]) -> bool:
74
+ if a is BOTTOM or b is TOP:
75
+ return True
76
+ if a is TOP or b is BOTTOM:
77
+ return False
78
+ return same_element(a, b)