coretrace-python-analyzer 0.8.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.
@@ -1,4 +1,4 @@
1
1
  """CoreTrace's Python static analysis frontend."""
2
2
 
3
- __version__ = "0.8.0"
3
+ __version__ = "0.9.0"
4
4
 
@@ -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 Advisory, AdvisoryEntryPoint, Condition, normalize
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
- check = check_conditions(advisory.entry_point(flow.sink.symbol), flow.sink_arguments)
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"
@@ -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
@@ -119,6 +119,9 @@ class TaintFlow:
119
119
  sink_location: SourceSpan | None = None
120
120
  # What the arguments of the sink call denote, to decide the conditions on them.
121
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()
122
125
 
123
126
 
124
127
  Key = Value | HeapLocation
@@ -559,6 +562,7 @@ class _TaintProblem(DataflowProblem[State]):
559
562
  through,
560
563
  call.location if sink_location is None else sink_location,
561
564
  arguments,
565
+ frozenset({(position, keyword)}),
562
566
  )
563
567
  )
564
568
 
@@ -819,7 +823,18 @@ def propagate_taint(
819
823
  state, found = problem.evaluate(block, solution.incoming(block.id))
820
824
  taints.update(state)
821
825
  flows.extend(found)
822
- return TaintFacts(taints, tuple(dict.fromkeys(flows)))
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())
823
838
 
824
839
 
825
840
  class TaintAnalysis(FunctionAnalysis[TaintFacts]):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coretrace-python-analyzer
3
- Version: 0.8.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
@@ -1,4 +1,4 @@
1
- coretrace_python/__init__.py,sha256=u9j9n2ZvaBeVUS33TRJ3MIoLUWwaBdpCAainRMXI4ok,75
1
+ coretrace_python/__init__.py,sha256=JoQcl29Ph_JPstj_FTAA1vDAxIylnVKy9661tvKlYIo,75
2
2
  coretrace_python/__main__.py,sha256=V8DnYtRboISK2VCKm7DGHLREKvqLbqApyWlKBOs26wU,65
3
3
  coretrace_python/cache.py,sha256=-pZv-ELm4jMCTXQWSZIobHaT9UHIjFu7IlMkOM0i4LI,13607
4
4
  coretrace_python/cli.py,sha256=86rTb3Kcezwk9DBTRg-G3G38ypx8zH4lZbCR467xvpE,11008
@@ -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=CXE36X2d5rio90NWlpjLo8Hly-tmzDCvu05FZAj_WXQ,1138
92
- coretrace_python/dependency/advisories.py,sha256=tfR-5BmjV4GYcsF3CSGA4jSXNwOVG6mS8AqPfy2fWms,8653
93
- coretrace_python/dependency/correlation.py,sha256=pR9S0tcdumdk2DMfUXxz6Wdul_RZE8FBfVmCUEhcA7w,7231
94
- coretrace_python/dependency/graph.py,sha256=6UpAsqbtr5EoVGp6qkaQ7Gi1iUbGtulyh0hfpK_RqLg,16875
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
@@ -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=lB6ysdzYNuYhIiRUaxfI4F3cAnaUK55d4lF9AzPWfrU,39742
141
+ coretrace_python/taint/engine.py,sha256=_uVUc2m29aljN_YR2iCQtEdvZFc-LJ5CDWiZF1Tz3WU,40524
142
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.8.0.dist-info/METADATA,sha256=6iUDBdRu9mMtAf4EaeD4sVOr4iEdxzT7tkfOBlaNNT8,3216
146
- coretrace_python_analyzer-0.8.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
147
- coretrace_python_analyzer-0.8.0.dist-info/entry_points.txt,sha256=wdozemiraOkbt65g_Y_zgspuZpoLxJdaCVMJt1Ft3c0,72
148
- coretrace_python_analyzer-0.8.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
149
- coretrace_python_analyzer-0.8.0.dist-info/licenses/NOTICE,sha256=s3VHj5ngV0ozjW_kZJpg_35ne6JwoBK0ysOjbqVtA0w,113
150
- coretrace_python_analyzer-0.8.0.dist-info/RECORD,,
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,,