coretrace-python-analyzer 0.21.0__py3-none-any.whl → 0.23.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 (52) hide show
  1. coretrace_python/__init__.py +1 -1
  2. coretrace_python/abstract/module_constants.py +121 -0
  3. coretrace_python/abstract/strings.py +12 -42
  4. coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +23 -6
  5. coretrace_python/bundled/models/python_stdlib/python_stdlib.py +27 -0
  6. coretrace_python/bundled/secrets/config_secrets/config_secrets.py +2 -1
  7. coretrace_python/bundled/security/code_injection/code_injection.py +85 -4
  8. coretrace_python/bundled/security/code_injection/plugin.toml +1 -1
  9. coretrace_python/bundled/security/command_injection/command_injection.py +1028 -3
  10. coretrace_python/bundled/security/command_injection/plugin.toml +1 -1
  11. coretrace_python/bundled/syntax/unsafe_xml/plugin.toml +1 -1
  12. coretrace_python/bundled/syntax/unsafe_xml/unsafe_xml.py +92 -12
  13. coretrace_python/cache.py +35 -17
  14. coretrace_python/cli.py +84 -14
  15. coretrace_python/dependency/graph.py +51 -30
  16. coretrace_python/dependency/vex.py +5 -2
  17. coretrace_python/engine.py +203 -60
  18. coretrace_python/findings/baseline.py +268 -34
  19. coretrace_python/findings/context.py +57 -0
  20. coretrace_python/findings/model.py +3 -2
  21. coretrace_python/findings/refutation.py +210 -27
  22. coretrace_python/findings/suppressions.py +11 -3
  23. coretrace_python/frontend/ast_adapter.py +27 -1
  24. coretrace_python/frontend/parser.py +10 -1
  25. coretrace_python/interprocedural/callgraph.py +14 -1
  26. coretrace_python/interprocedural/summaries.py +91 -19
  27. coretrace_python/ir/defuse.py +26 -1
  28. coretrace_python/ir/lowering.py +6 -2
  29. coretrace_python/ir/model.py +13 -2
  30. coretrace_python/ir/printer.py +4 -1
  31. coretrace_python/plugins/__init__.py +8 -1
  32. coretrace_python/plugins/api.py +16 -3
  33. coretrace_python/plugins/detectors.py +51 -7
  34. coretrace_python/plugins/secrets.py +245 -44
  35. coretrace_python/reporters/json_format.py +14 -7
  36. coretrace_python/reporters/report.py +7 -2
  37. coretrace_python/reporters/sarif.py +31 -12
  38. coretrace_python/reporters/text.py +5 -4
  39. coretrace_python/semantic/symbols.py +2 -1
  40. coretrace_python/source/__init__.py +6 -2
  41. coretrace_python/source/manager.py +16 -0
  42. coretrace_python/source/model.py +20 -0
  43. coretrace_python/source/positions.py +420 -0
  44. coretrace_python/taint/commands.py +45 -0
  45. coretrace_python/taint/engine.py +114 -27
  46. coretrace_python/taint/models.py +27 -3
  47. {coretrace_python_analyzer-0.21.0.dist-info → coretrace_python_analyzer-0.23.0.dist-info}/METADATA +10 -1
  48. {coretrace_python_analyzer-0.21.0.dist-info → coretrace_python_analyzer-0.23.0.dist-info}/RECORD +52 -48
  49. {coretrace_python_analyzer-0.21.0.dist-info → coretrace_python_analyzer-0.23.0.dist-info}/WHEEL +0 -0
  50. {coretrace_python_analyzer-0.21.0.dist-info → coretrace_python_analyzer-0.23.0.dist-info}/entry_points.txt +0 -0
  51. {coretrace_python_analyzer-0.21.0.dist-info → coretrace_python_analyzer-0.23.0.dist-info}/licenses/LICENSE +0 -0
  52. {coretrace_python_analyzer-0.21.0.dist-info → coretrace_python_analyzer-0.23.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,4 +1,4 @@
1
1
  """CoreTrace's Python static analysis frontend."""
2
2
 
3
- __version__ = "0.21.0"
3
+ __version__ = "0.23.0"
4
4
 
@@ -0,0 +1,121 @@
1
+ """Module-level names whose value is known (architecture §18): the names bound once and
2
+ never rebound, and among them the ones bound to an immutable collection of constants,
3
+ which an allowlist reads."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Mapping
8
+ from types import MappingProxyType
9
+ from typing import ClassVar
10
+
11
+ from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
12
+ from coretrace_python.cfg import CFGError
13
+ from coretrace_python.hir import nodes
14
+ from coretrace_python.ir.lowering import (
15
+ MODULE_BODY,
16
+ LoweringError,
17
+ PyIRAnalysis,
18
+ analyzable_functions,
19
+ )
20
+ from coretrace_python.ir.model import (
21
+ BuildDict,
22
+ BuildList,
23
+ BuildSet,
24
+ BuildTuple,
25
+ Call,
26
+ Constant,
27
+ ForNext,
28
+ Instruction,
29
+ SetGlobal,
30
+ StoreLocal,
31
+ Symbol,
32
+ Value,
33
+ )
34
+ from coretrace_python.semantic.symbols import SymbolId
35
+
36
+ FROZENSET = SymbolId("python.builtins.frozenset")
37
+
38
+
39
+ def module_bindings(ctx: AnalysisContext) -> tuple[Mapping[str, Instruction], Mapping[Value, Instruction]]:
40
+ """The module-level names bound once and never rebound, by no other statement of the
41
+ module body, no function assigning them as ``global``, no definition of that name,
42
+ each with the instruction that makes its value; and the definitions of the module
43
+ body's values. A module that does not lower entirely has none."""
44
+
45
+ try:
46
+ functions = [ctx.get(PyIRAnalysis, function) for function in analyzable_functions(ctx.module)]
47
+ except (LoweringError, CFGError):
48
+ return MappingProxyType({}), MappingProxyType({})
49
+ body = next((function for function in functions if function.name == MODULE_BODY), None)
50
+ if body is None:
51
+ return MappingProxyType({}), MappingProxyType({})
52
+ defs = {i.result: i for block in body.blocks for i in block.instructions if i.result is not None}
53
+ stored: dict[str, list[Value | None]] = {}
54
+ for block in body.blocks:
55
+ for instruction in block.instructions:
56
+ if isinstance(instruction, StoreLocal):
57
+ stored.setdefault(instruction.name, []).append(instruction.value)
58
+ if isinstance(block.terminator, ForNext):
59
+ # A ``for`` binds its target to each item.
60
+ stored.setdefault(block.terminator.target, []).append(None)
61
+ rebound = {
62
+ i.name for function in functions for block in function.blocks for i in block.instructions
63
+ if isinstance(i, SetGlobal)
64
+ }
65
+ rebound |= {s.name for s in ctx.module.body if isinstance(s, nodes.Function | nodes.Class)}
66
+ bound: dict[str, Instruction] = {}
67
+ for name, values in stored.items():
68
+ value = values[0] if len(values) == 1 and name not in rebound else None
69
+ made = defs.get(value) if value is not None else None
70
+ if made is not None:
71
+ bound[name] = made
72
+ return MappingProxyType(bound), MappingProxyType(defs)
73
+
74
+
75
+ def constant_members(display: Instruction | None, defs: Mapping[Value, Instruction]) -> bool:
76
+ """Whether ``display`` is a tuple, list or set display whose items are all constants,
77
+ or a dict display whose keys are, with nothing unpacked into it: the members a
78
+ membership test reads."""
79
+
80
+ if isinstance(display, BuildList | BuildTuple | BuildSet):
81
+ return not display.unpacked and all(isinstance(defs.get(e), Constant) for e in display.elements)
82
+ if isinstance(display, BuildDict):
83
+ return not display.unpacked and all(isinstance(defs.get(key), Constant) for key, _ in display.items)
84
+ return False
85
+
86
+
87
+ def converts(call: Call, defs: Mapping[Value, Instruction], symbols: frozenset[SymbolId]) -> Value | None:
88
+ """The one positional argument of ``call`` when it calls one of ``symbols`` with
89
+ nothing else (``frozenset(x)``), otherwise ``None``."""
90
+
91
+ callee = defs.get(call.callee)
92
+ if not isinstance(callee, Symbol) or callee.symbol_id not in symbols:
93
+ return None
94
+ if len(call.arguments) != 1 or call.keywords or call.starred:
95
+ return None
96
+ return call.arguments[0]
97
+
98
+
99
+ class ModuleCollectionsAnalysis(Analysis[frozenset[str]]):
100
+ """The module-level names bound once and never rebound (``module_bindings``) to an
101
+ immutable collection of constants: a tuple display, or ``frozenset`` of a display
102
+ whose members are constants (``constant_members``). A list, a set or a dict can still
103
+ change after it is bound, so it is not one."""
104
+
105
+ name: ClassVar[str] = "abstract.module_collections"
106
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({PyIRAnalysis})
107
+
108
+ @classmethod
109
+ def compute(cls, ctx: AnalysisContext) -> frozenset[str]:
110
+ bound, defs = module_bindings(ctx)
111
+ found: set[str] = set()
112
+ for name, made in bound.items():
113
+ collection: Instruction | None = made
114
+ if isinstance(made, Call):
115
+ argument = converts(made, defs, frozenset({FROZENSET}))
116
+ collection = defs.get(argument) if argument is not None else None
117
+ elif not isinstance(made, BuildTuple):
118
+ continue
119
+ if constant_members(collection, defs):
120
+ found.add(name)
121
+ return frozenset(found)
@@ -9,24 +9,15 @@ from collections.abc import Mapping
9
9
  from types import MappingProxyType
10
10
  from typing import ClassVar
11
11
 
12
+ from coretrace_python.abstract.module_constants import module_bindings
12
13
  from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
13
- from coretrace_python.cfg import CFGError
14
- from coretrace_python.hir import nodes
15
- from coretrace_python.ir.lowering import (
16
- MODULE_BODY,
17
- LoweringError,
18
- PyIRAnalysis,
19
- analyzable_functions,
20
- )
14
+ from coretrace_python.ir.lowering import PyIRAnalysis
21
15
  from coretrace_python.ir.model import (
22
16
  BinaryOp,
23
17
  BuildString,
24
18
  Constant,
25
- ForNext,
26
19
  Global,
27
20
  Instruction,
28
- SetGlobal,
29
- StoreLocal,
30
21
  Value,
31
22
  )
32
23
 
@@ -59,40 +50,19 @@ def leading_text(value: Value, defs: Mapping[Value, Instruction], strings: Mappi
59
50
 
60
51
 
61
52
  class ModuleStringsAnalysis(Analysis[Mapping[str, str]]):
62
- """The module-level names bound once, to a string literal, and never rebound: by no
63
- other statement of the module body, no function assigning them as ``global``, no
64
- definition of that name. A module that does not lower entirely has none."""
53
+ """The module-level names bound once and never rebound (``module_bindings``) to a
54
+ string literal, with that literal."""
65
55
 
66
56
  name: ClassVar[str] = "abstract.module_strings"
67
57
  requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({PyIRAnalysis})
68
58
 
69
59
  @classmethod
70
60
  def compute(cls, ctx: AnalysisContext) -> Mapping[str, str]:
71
- try:
72
- functions = [ctx.get(PyIRAnalysis, function) for function in analyzable_functions(ctx.module)]
73
- except (LoweringError, CFGError):
74
- return MappingProxyType({})
75
- body = next((function for function in functions if function.name == MODULE_BODY), None)
76
- if body is None:
77
- return MappingProxyType({})
78
- defs = {i.result: i for block in body.blocks for i in block.instructions if i.result is not None}
79
- stored: dict[str, list[Value | None]] = {}
80
- for block in body.blocks:
81
- for instruction in block.instructions:
82
- if isinstance(instruction, StoreLocal):
83
- stored.setdefault(instruction.name, []).append(instruction.value)
84
- if isinstance(block.terminator, ForNext):
85
- # A ``for`` binds its target to each item.
86
- stored.setdefault(block.terminator.target, []).append(None)
87
- rebound = {
88
- i.name for function in functions for block in function.blocks for i in block.instructions
89
- if isinstance(i, SetGlobal)
90
- }
91
- rebound |= {s.name for s in ctx.module.body if isinstance(s, nodes.Function | nodes.Class)}
92
- found: dict[str, str] = {}
93
- for name, values in stored.items():
94
- value = values[0] if len(values) == 1 and name not in rebound else None
95
- made = defs.get(value) if value is not None else None
96
- if isinstance(made, Constant) and isinstance(made.value, str):
97
- found[name] = made.value
98
- return MappingProxyType(found)
61
+ bound, _ = module_bindings(ctx)
62
+ return MappingProxyType(
63
+ {
64
+ name: made.value
65
+ for name, made in bound.items()
66
+ if isinstance(made, Constant) and isinstance(made.value, str)
67
+ }
68
+ )
@@ -3,10 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Sequence
6
+ from pathlib import PurePath
6
7
  from typing import ClassVar
7
8
 
8
9
  from coretrace_python.analysis import AnyAnalysis
9
- from coretrace_python.dependency import DependencyAnalysis
10
+ from coretrace_python.dependency import Advisory, DependencyAnalysis, Requirement
10
11
  from coretrace_python.dependency.correlation import (
11
12
  affected_entries,
12
13
  affected_symbols,
@@ -44,11 +45,7 @@ class VulnerableDependencyPlugin(ProjectPlugin):
44
45
  findings.append(
45
46
  Finding(
46
47
  rule_id="vulnerable-dependency",
47
- message=(
48
- f"{advisory.id}: {advisory.package} {advisory.vulnerable} is "
49
- f"{'required' if pinned else 'allowed'} by {requirement.name}"
50
- f"{requirement.specifier or ''}: {advisory.summary}"
51
- ),
48
+ message=_message(ctx, advisory, requirement),
52
49
  severity=advisory.severity,
53
50
  confidence=Confidence.HIGH if pinned else Confidence.MEDIUM,
54
51
  span=requirement.span,
@@ -58,6 +55,26 @@ class VulnerableDependencyPlugin(ProjectPlugin):
58
55
  return findings
59
56
 
60
57
 
58
+ def _message(ctx: ProjectContext, advisory: Advisory, requirement: Requirement) -> str:
59
+ """The evidence: for a locked package, the version the lock file pins and the
60
+ packages that require it; for a declared requirement, the specifier that allows or
61
+ requires a vulnerable version."""
62
+
63
+ if requirement.locked is None:
64
+ verb = "required" if requirement.pinned is not None else "allowed"
65
+ return (
66
+ f"{advisory.id}: {advisory.package} {advisory.vulnerable} is {verb} by "
67
+ f"{requirement.name}{requirement.specifier or ''}: {advisory.summary}"
68
+ )
69
+ lock = PurePath(str(requirement.span.source_id)).name
70
+ dependents = sorted(ctx.dependencies.required_by(requirement.name) or ())
71
+ required = f" (required by {', '.join(dependents)})" if dependents else ""
72
+ return (
73
+ f"{advisory.id}: {advisory.package} {requirement.locked}, pinned in {lock}, "
74
+ f"is in {advisory.vulnerable}{required}: {advisory.summary}"
75
+ )
76
+
77
+
61
78
  def _ruled_out(ctx: ProjectContext) -> dict[str, list[str]]:
62
79
  """By advisory, the calls to its entry points whose arguments contradict one of its
63
80
  conditions: the evidence that the requirement, imported, is not reached there."""
@@ -62,6 +62,33 @@ class PythonStdlibModels(ModelPlugin):
62
62
  # The code is the first argument; the globals and locals it runs with are data.
63
63
  Sink(_sym("builtins.eval"), TaintKind.CODE, ((TaintKind.CODE, (0,)),)),
64
64
  Sink(_sym("builtins.exec"), TaintKind.CODE, ((TaintKind.CODE, (0,)),)),
65
+ # The module or the file the call imports or runs; its globals, its run name and
66
+ # the package of a relative name are data (#240).
67
+ *(
68
+ Sink(_sym(symbol), TaintKind.CODE, ((TaintKind.CODE, (0,)),), ((TaintKind.CODE, (keyword,)),), selects=True)
69
+ for symbol, keyword in (
70
+ ("importlib.import_module", "name"),
71
+ ("builtins.__import__", "name"),
72
+ ("importlib.__import__", "name"),
73
+ ("runpy.run_module", "mod_name"),
74
+ ("runpy.run_path", "path_name"),
75
+ )
76
+ ),
77
+ # The directory imports are looked up in, and the site directory whose ``.pth``
78
+ # files run; the index of ``insert`` is data (#240).
79
+ Sink(_sym("sys.path.insert"), TaintKind.CODE, ((TaintKind.CODE, (1,)),), selects=True),
80
+ Sink(_sym("sys.path.append"), TaintKind.CODE, ((TaintKind.CODE, (0,)),), selects=True),
81
+ Sink(_sym("sys.path.extend"), TaintKind.CODE, ((TaintKind.CODE, (0,)),), selects=True),
82
+ Sink(_sym("site.addsitedir"), TaintKind.CODE, ((TaintKind.CODE, (0,)),), ((TaintKind.CODE, ("sitedir",)),), selects=True),
83
+ # The name of the attribute ``getattr`` selects, when what it selects is called;
84
+ # the object and the default are data (#240).
85
+ Sink(
86
+ _sym("builtins.getattr"),
87
+ TaintKind.CODE,
88
+ ((TaintKind.CODE, (1,)),),
89
+ selects=True,
90
+ requires_called_result=TaintKind.CODE,
91
+ ),
65
92
  Sink(_sym("builtins.open"), TaintKind.PATH),
66
93
  Sink(_sym("os.remove"), TaintKind.PATH),
67
94
  Sink(_sym("os.unlink"), TaintKind.PATH),
@@ -9,6 +9,7 @@ from collections.abc import Sequence
9
9
  from typing import ClassVar
10
10
 
11
11
  from coretrace_python.findings import Finding
12
+ from coretrace_python.findings.context import path_in_project
12
13
  from coretrace_python.plugins import PluginContext, ProjectContext, ProjectPlugin
13
14
  from coretrace_python.plugins.secrets import (
14
15
  DEFAULT_CREDENTIAL_NAMES,
@@ -32,7 +33,7 @@ class ConfigSecrets(SecretDetector, ProjectPlugin):
32
33
  return ()
33
34
  findings: list[Finding] = []
34
35
  for value, name, span, function in config_literals(ctx.root):
35
- finding = self.judge(value, name, span, function)
36
+ finding = self.judge(value, name, span, function, path=path_in_project(span.source_id, ctx.root))
36
37
  if finding is not None:
37
38
  findings.append(finding)
38
39
  return findings
@@ -1,14 +1,43 @@
1
1
  """Code injection: attacker-controlled input reaching a CODE sink, such as the code
2
- ``eval`` or ``exec`` runs. ``dangerous-eval`` reports every such call; this rule reports
3
- the ones attacker input reaches."""
2
+ ``eval`` or ``exec`` runs, the module ``importlib.import_module``, ``__import__`` or
3
+ ``runpy.run_module`` imports, the file ``runpy.run_path`` runs, the directory
4
+ ``sys.path`` and ``site.addsitedir`` load code from, or the attribute ``getattr`` selects
5
+ when it is called. ``dangerous-eval`` reports every ``eval`` or ``exec``; this rule reports
6
+ the calls attacker input reaches.
7
+
8
+ An import runs the top-level code of a module the input chooses among those that can be
9
+ found, and a script path runs the file the input chooses: execution is possible, and
10
+ how far it goes depends on what the input can reach. Their findings are titled for
11
+ that. A constant package prefix (``"app.plugins." + name``) confines an import to the
12
+ modules of that package: a hotspot. A directory added to ``sys.path`` runs code only
13
+ when a later import resolves there: a hotspot. ``site.addsitedir`` runs the import lines
14
+ of the directory's ``.pth`` files during the call: a vulnerability, exploitable when the
15
+ directory holds files the attacker controls, as the finding states. The attribute
16
+ ``getattr`` selects runs at each call of its result, where the finding is placed: what
17
+ runs depends on what the object exposes, a hotspot."""
4
18
 
5
19
  from __future__ import annotations
6
20
 
7
21
  from typing import ClassVar
8
22
 
23
+ from coretrace_python.abstract.strings import ModuleStringsAnalysis, leading_text
24
+ from coretrace_python.analysis import AnyAnalysis
9
25
  from coretrace_python.findings import Severity
10
- from coretrace_python.plugins import TaintDetector
11
- from coretrace_python.taint import TaintKind
26
+ from coretrace_python.findings.refutation import Status, Verdict
27
+ from coretrace_python.hir import nodes
28
+ from coretrace_python.ir.ssa import SSAAnalysis
29
+ from coretrace_python.plugins import Assessment, PluginContext, TaintDetector
30
+ from coretrace_python.semantic.symbols import SymbolId
31
+ from coretrace_python.taint import TaintFlow, TaintKind
32
+
33
+ _IMPORTS = frozenset(
34
+ SymbolId(f"python.{name}")
35
+ for name in ("importlib.import_module", "builtins.__import__", "importlib.__import__", "runpy.run_module")
36
+ )
37
+ _RUN_PATH = SymbolId("python.runpy.run_path")
38
+ _IMPORT_PATHS = frozenset(SymbolId(f"python.sys.path.{name}") for name in ("insert", "append", "extend"))
39
+ _SITE_DIRECTORY = SymbolId("python.site.addsitedir")
40
+ _GETATTR = SymbolId("python.builtins.getattr")
12
41
 
13
42
 
14
43
  class CodeInjectionPlugin(TaintDetector):
@@ -17,3 +46,55 @@ class CodeInjectionPlugin(TaintDetector):
17
46
  kind: ClassVar[TaintKind] = TaintKind.CODE
18
47
  severity: ClassVar[Severity] = Severity.HIGH
19
48
  title: ClassVar[str] = "Code injection"
49
+ requires: ClassVar[frozenset[AnyAnalysis]] = TaintDetector.requires | {SSAAnalysis, ModuleStringsAnalysis}
50
+
51
+ def assess(
52
+ self, ctx: PluginContext, function: nodes.Function, flow: TaintFlow, verdict: Verdict
53
+ ) -> Assessment:
54
+ symbol = flow.sink.symbol
55
+ if symbol == _RUN_PATH:
56
+ return Assessment(verdict, self.severity, "Untrusted script execution")
57
+ if symbol in _IMPORT_PATHS:
58
+ if verdict.status is Status.VULNERABILITY:
59
+ verdict = Verdict(
60
+ flow,
61
+ Status.HOTSPOT,
62
+ "the input chooses a directory imports are looked up in: code runs only when a later "
63
+ "import resolves to a module the attacker placed there",
64
+ )
65
+ return Assessment(verdict, self.severity, "Untrusted import path")
66
+ if symbol == _GETATTR:
67
+ if verdict.status is Status.VULNERABILITY:
68
+ verdict = Verdict(
69
+ flow,
70
+ Status.HOTSPOT,
71
+ "the input chooses the attribute called: what runs depends on what the object exposes",
72
+ )
73
+ lines: dict[str, str] = {}
74
+ if flow.invoked_at is not None and flow.sink_location is not None:
75
+ lines = {
76
+ "selected_line": str(flow.sink_location.start_line),
77
+ "invoked_line": str(flow.invoked_at.start_line),
78
+ }
79
+ return Assessment(verdict, self.severity, "Untrusted attribute call", lines)
80
+ if symbol == _SITE_DIRECTORY:
81
+ if verdict.status is Status.VULNERABILITY:
82
+ verdict = Verdict(
83
+ flow,
84
+ Status.VULNERABILITY,
85
+ f"{verdict.evidence}; the import lines of the .pth files of the directory run during the "
86
+ "call: exploitable when the input chooses a directory holding files the attacker controls",
87
+ )
88
+ return Assessment(verdict, self.severity, "Untrusted site directory")
89
+ if symbol not in _IMPORTS:
90
+ return Assessment(verdict, self.severity)
91
+ if verdict.status is Status.VULNERABILITY and flow.through is None:
92
+ ssa = ctx.get(SSAAnalysis, function)
93
+ defs = {i.result: i for block in ssa.blocks for i in block.instructions if i.result is not None}
94
+ prefix, whole = leading_text(flow.argument, defs, ctx.get(ModuleStringsAnalysis))
95
+ package = prefix.rpartition(".")[0]
96
+ if not whole and package and not prefix.startswith("."):
97
+ verdict = Verdict(
98
+ flow, Status.HOTSPOT, f"the package is fixed by {prefix!r}, but the input chooses a module in it"
99
+ )
100
+ return Assessment(verdict, self.severity, "Untrusted module import")
@@ -1,7 +1,7 @@
1
1
  name = "code-injection"
2
2
  version = "1.0.0"
3
3
  plugin_api = ">=1,<2"
4
- requires = ["taint.flows", "findings.refutation"]
4
+ requires = ["taint.flows", "findings.refutation", "ir.ssa", "abstract.module_strings"]
5
5
  provides = ["vulnerability.code-injection"]
6
6
 
7
7
  [entrypoint]