cobraphp 0.1.0.dev1__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.
- cobraphp/__init__.py +60 -0
- cobraphp/_backend.py +47 -0
- cobraphp/_placeholder.py +10 -0
- cobraphp/analysis/__init__.py +60 -0
- cobraphp/analysis/_ast_types.py +17 -0
- cobraphp/analysis/_call_builder.py +135 -0
- cobraphp/analysis/_call_graph.py +211 -0
- cobraphp/analysis/_call_graph_nodes.py +195 -0
- cobraphp/analysis/_callee_index.py +112 -0
- cobraphp/analysis/_defuse_builder.py +402 -0
- cobraphp/analysis/_errors.py +17 -0
- cobraphp/analysis/_file_filter.py +172 -0
- cobraphp/analysis/_project_root.py +29 -0
- cobraphp/analysis/_records.py +140 -0
- cobraphp/analysis/_semgrep_runner.py +322 -0
- cobraphp/analysis/_source_kinds.py +6 -0
- cobraphp/analysis/_taint_graph.py +200 -0
- cobraphp/analysis/_taint_propagator.py +295 -0
- cobraphp/analysis/_taint_rules.py +123 -0
- cobraphp/analysis/frameworks.toml +38 -0
- cobraphp/analysis/php_ast_types.toml +29 -0
- cobraphp/analysis/rules/database_taint.yaml +94 -0
- cobraphp/analysis/rules/user_input_taint.yaml +186 -0
- cobraphp/config.py +50 -0
- cobraphp/entries.py +39 -0
- cobraphp/errors.py +25 -0
- cobraphp/graphs.py +63 -0
- cobraphp/project.py +49 -0
- cobraphp/py.typed +0 -0
- cobraphp/results.py +116 -0
- cobraphp-0.1.0.dev1.dist-info/METADATA +105 -0
- cobraphp-0.1.0.dev1.dist-info/RECORD +35 -0
- cobraphp-0.1.0.dev1.dist-info/WHEEL +5 -0
- cobraphp-0.1.0.dev1.dist-info/licenses/LICENSE +339 -0
- cobraphp-0.1.0.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Call graph node and edge data types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from cpg2py import AbcEdgeQuerier, AbcNodeQuerier, Storage
|
|
8
|
+
|
|
9
|
+
from ._errors import StaticAnalysisError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CallNode(AbcNodeQuerier): # type: ignore[misc]
|
|
13
|
+
"""Abstract base for call graph node metadata.
|
|
14
|
+
|
|
15
|
+
Extends AbcNodeQuerier to access call graph node properties
|
|
16
|
+
from Storage. Do NOT instantiate directly — use FuncNode or
|
|
17
|
+
SiteNode subclasses.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
node_id: AST node ID (readonly, inherited).
|
|
21
|
+
node_type: "function_declaration" or "call_site" (readonly).
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, storage: Storage, node_id: str, node_type: str) -> None:
|
|
26
|
+
"""Initialize CallNode via AbcNodeQuerier.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
storage: Storage instance containing node data.
|
|
30
|
+
node_id: AST node ID.
|
|
31
|
+
node_type: "function_declaration" or "call_site".
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
NodeNotFoundError: If node doesn't exist in storage.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(storage, node_id)
|
|
38
|
+
self._storage_ref = storage
|
|
39
|
+
storage.set_node_prop(node_id, "node_type", node_type)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def node_type(self) -> str:
|
|
43
|
+
"""Node type: 'function_declaration' or 'call_site'."""
|
|
44
|
+
return self.get_property("node_type") or ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FuncNode(CallNode):
|
|
48
|
+
"""Storage wrapper for function declaration or file-level container.
|
|
49
|
+
|
|
50
|
+
Represents function declarations or file-level containers in
|
|
51
|
+
call graph. When is_file=True, represents a file-level container
|
|
52
|
+
for top-level code.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
name: Function name or file path (mutable).
|
|
56
|
+
parameter_ids: AST node IDs of parameter declarations (mutable).
|
|
57
|
+
return_type_hint: Return type annotation if present (mutable).
|
|
58
|
+
is_file: Whether this is a file-level container (readonly).
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, storage: Storage, node_id: str) -> None:
|
|
63
|
+
"""Initialize FuncNode.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
storage: Storage instance containing node data.
|
|
67
|
+
node_id: AST node ID of function declaration or file ID.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
NodeNotFoundError: If node doesn't exist in storage.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
super().__init__(storage, node_id, "function_declaration")
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def is_file(self) -> bool:
|
|
77
|
+
"""Whether this node represents a file-level container."""
|
|
78
|
+
value = self.get_property("is_file")
|
|
79
|
+
return bool(value) if value is not None else False
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def name(self) -> str:
|
|
83
|
+
"""Function name."""
|
|
84
|
+
return self.get_property("name") or ""
|
|
85
|
+
|
|
86
|
+
@name.setter
|
|
87
|
+
def name(self, value: str) -> None:
|
|
88
|
+
"""Set function name."""
|
|
89
|
+
self._storage_ref.set_node_prop(self.node_id, "name", value)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def parameter_ids(self) -> list[str]:
|
|
93
|
+
"""AST node IDs of parameter declarations."""
|
|
94
|
+
return self.get_property("parameter_ids") or []
|
|
95
|
+
|
|
96
|
+
@parameter_ids.setter
|
|
97
|
+
def parameter_ids(self, value: list[str]) -> None:
|
|
98
|
+
"""Set parameter IDs."""
|
|
99
|
+
self._storage_ref.set_node_prop(self.node_id, "parameter_ids", value)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def return_type_hint(self) -> str | None:
|
|
103
|
+
"""Type hint for return value, or None if not specified."""
|
|
104
|
+
value = self.get_property("return_type_hint")
|
|
105
|
+
return cast(str, value) if value else None
|
|
106
|
+
|
|
107
|
+
@return_type_hint.setter
|
|
108
|
+
def return_type_hint(self, value: str) -> None:
|
|
109
|
+
"""Set return type hint."""
|
|
110
|
+
self._storage_ref.set_node_prop(self.node_id, "return_type_hint", value)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SiteNode(CallNode):
|
|
114
|
+
"""Storage wrapper for call site node metadata.
|
|
115
|
+
|
|
116
|
+
Attributes:
|
|
117
|
+
argument_ids: AST node IDs of argument expressions (mutable).
|
|
118
|
+
caller_id: AST node ID of containing function or file (mutable).
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, storage: Storage, node_id: str) -> None:
|
|
123
|
+
"""Initialize SiteNode.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
storage: Storage instance containing node data.
|
|
127
|
+
node_id: AST node ID of call expression.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
NodeNotFoundError: If node doesn't exist in storage.
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
super().__init__(storage, node_id, "call_site")
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def argument_ids(self) -> list[str]:
|
|
137
|
+
"""AST node IDs of argument expressions."""
|
|
138
|
+
return self.get_property("argument_ids") or []
|
|
139
|
+
|
|
140
|
+
@argument_ids.setter
|
|
141
|
+
def argument_ids(self, value: list[str]) -> None:
|
|
142
|
+
"""Set argument IDs."""
|
|
143
|
+
self._storage_ref.set_node_prop(self.node_id, "argument_ids", value)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def caller_id(self) -> str:
|
|
147
|
+
"""AST node ID of the function or file containing this call site.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
StaticAnalysisError: If caller_id has not been set.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
value = self.get_property("caller_id")
|
|
154
|
+
if value is None:
|
|
155
|
+
raise StaticAnalysisError(f"Property 'caller_id' not found for node {self.node_id}")
|
|
156
|
+
return cast(str, value)
|
|
157
|
+
|
|
158
|
+
@caller_id.setter
|
|
159
|
+
def caller_id(self, value: str) -> None:
|
|
160
|
+
"""Set caller function ID."""
|
|
161
|
+
self._storage_ref.set_node_prop(self.node_id, "caller_id", value)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class CallEdge(AbcEdgeQuerier): # type: ignore[misc]
|
|
165
|
+
"""Storage wrapper for call graph edge (call site to callee).
|
|
166
|
+
|
|
167
|
+
Attributes:
|
|
168
|
+
edge_id: Edge ID tuple (from, to, type) (readonly, inherited).
|
|
169
|
+
callsite_node_id: Alias for from_nid (readonly).
|
|
170
|
+
callee_node_id: Alias for to_nid (readonly).
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(self, storage: Storage, f_nid: str, t_nid: str, e_type: str) -> None:
|
|
175
|
+
"""Initialize CallEdge.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
storage: Storage instance containing edge data.
|
|
179
|
+
f_nid: Source node ID (callsite).
|
|
180
|
+
t_nid: Target node ID (callee).
|
|
181
|
+
e_type: Edge type.
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
super().__init__(storage, f_nid, t_nid, e_type)
|
|
185
|
+
self._storage_ref = storage
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def callsite_node_id(self) -> str:
|
|
189
|
+
"""AST node ID of call expression (alias for from_nid)."""
|
|
190
|
+
return cast(str, self.from_nid)
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def callee_node_id(self) -> str:
|
|
194
|
+
"""AST node ID of function declaration (alias for to_nid)."""
|
|
195
|
+
return cast(str, self.to_nid)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Resolve PHP call sites to named callable declarations using the receiver class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from php_parser_py import AST, Node
|
|
6
|
+
|
|
7
|
+
# PHP AST node types of the declarations and calls callee resolution relates.
|
|
8
|
+
FUNCTION_DECL_TYPE = "Stmt_Function"
|
|
9
|
+
METHOD_DECL_TYPE = "Stmt_ClassMethod"
|
|
10
|
+
NAMED_CALLABLE_TYPES = (FUNCTION_DECL_TYPE, METHOD_DECL_TYPE)
|
|
11
|
+
FUNCTION_CALL_TYPE = "Expr_FuncCall"
|
|
12
|
+
METHOD_CALL_TYPES = ("Expr_MethodCall", "Expr_StaticCall")
|
|
13
|
+
CALL_SITE_TYPES = (FUNCTION_CALL_TYPE, *METHOD_CALL_TYPES)
|
|
14
|
+
# Node types php-parser emits for a name written literally in source.
|
|
15
|
+
_NAME_TYPES = frozenset({"Name", "Name_FullyQualified", "Name_Relative"})
|
|
16
|
+
_IDENTIFIER_TYPES = frozenset({"Identifier"})
|
|
17
|
+
_VARIABLE_TYPE = "Expr_Variable"
|
|
18
|
+
# Declarations that own methods; $this, self, and static refer to the nearest one.
|
|
19
|
+
_CLASS_LIKE_TYPES = frozenset({"Stmt_Class", "Stmt_Trait", "Stmt_Interface", "Stmt_Enum"})
|
|
20
|
+
# PHP's receiver variable and class keywords bound to the enclosing class (keywords are case-insensitive).
|
|
21
|
+
_THIS_VARIABLE = "this"
|
|
22
|
+
_ENCLOSING_CLASS_KEYWORDS = frozenset({"self", "static"})
|
|
23
|
+
NAMESPACE_SEPARATOR = "\\"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CalleeIndex:
|
|
27
|
+
"""Named function and method declarations indexed for callee resolution.
|
|
28
|
+
|
|
29
|
+
A function call resolves to a free function only: its exact name, else
|
|
30
|
+
the short name after the last namespace separator. A method call
|
|
31
|
+
resolves to its receiver class's method of that name, where $this,
|
|
32
|
+
self, and static denote the enclosing class and a literal class name
|
|
33
|
+
denotes that class by short name. With no receiver class (parent, a
|
|
34
|
+
variable, an expression) or no such method in it, the call resolves to
|
|
35
|
+
the first method of that name in AST order. A call whose name is an
|
|
36
|
+
expression has no callee. For every key, the first declaration wins.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, ast: AST) -> None:
|
|
40
|
+
self._ast = ast
|
|
41
|
+
self._functions: dict[str, Node] = {}
|
|
42
|
+
self._class_methods: dict[tuple[str, str], Node] = {}
|
|
43
|
+
self._methods: dict[str, Node] = {}
|
|
44
|
+
|
|
45
|
+
def add(self, decl: Node, name: str) -> None:
|
|
46
|
+
"""Index a Stmt_Function or Stmt_ClassMethod declaration under its name."""
|
|
47
|
+
if decl.node_type == FUNCTION_DECL_TYPE:
|
|
48
|
+
self._functions.setdefault(name, decl)
|
|
49
|
+
return
|
|
50
|
+
class_name = _enclosing_class_name(self._ast, decl)
|
|
51
|
+
if class_name is not None:
|
|
52
|
+
self._class_methods.setdefault((class_name, name), decl)
|
|
53
|
+
self._methods.setdefault(name, decl)
|
|
54
|
+
|
|
55
|
+
def resolve(self, call_site: Node) -> Node | None:
|
|
56
|
+
"""Return the declaration a call site targets, or None when unresolved."""
|
|
57
|
+
if call_site.node_type == FUNCTION_CALL_TYPE:
|
|
58
|
+
return self._resolve_function(call_site)
|
|
59
|
+
return self._resolve_method(call_site)
|
|
60
|
+
|
|
61
|
+
# Free function by exact name, else by namespace short name.
|
|
62
|
+
def _resolve_function(self, call_site: Node) -> Node | None:
|
|
63
|
+
name = _child_name(self._ast, call_site, _NAME_TYPES)
|
|
64
|
+
if name is None:
|
|
65
|
+
return None
|
|
66
|
+
exact = self._functions.get(name)
|
|
67
|
+
if exact is not None or NAMESPACE_SEPARATOR not in name:
|
|
68
|
+
return exact
|
|
69
|
+
return self._functions.get(_short_name(name))
|
|
70
|
+
|
|
71
|
+
# Receiver class's method, else the first method of that name.
|
|
72
|
+
def _resolve_method(self, call_site: Node) -> Node | None:
|
|
73
|
+
name = _child_name(self._ast, call_site, _IDENTIFIER_TYPES)
|
|
74
|
+
if name is None:
|
|
75
|
+
return None
|
|
76
|
+
class_name = self._receiver_class_name(call_site)
|
|
77
|
+
own = None if class_name is None else self._class_methods.get((class_name, name))
|
|
78
|
+
return own if own is not None else self._methods.get(name)
|
|
79
|
+
|
|
80
|
+
# Class a method call's receiver (its first child) statically denotes, or None.
|
|
81
|
+
def _receiver_class_name(self, call_site: Node) -> str | None:
|
|
82
|
+
receiver = next(iter(self._ast.succ(call_site)))
|
|
83
|
+
name = str(receiver.all_properties.get("name", ""))
|
|
84
|
+
if receiver.node_type == _VARIABLE_TYPE:
|
|
85
|
+
return _enclosing_class_name(self._ast, call_site) if name == _THIS_VARIABLE else None
|
|
86
|
+
if receiver.node_type not in _NAME_TYPES or name.lower() == "parent":
|
|
87
|
+
return None
|
|
88
|
+
if name.lower() in _ENCLOSING_CLASS_KEYWORDS:
|
|
89
|
+
return _enclosing_class_name(self._ast, call_site)
|
|
90
|
+
return _short_name(name)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Name of the nearest enclosing class-like declaration; None when absent or anonymous.
|
|
94
|
+
def _enclosing_class_name(ast: AST, node: Node) -> str | None:
|
|
95
|
+
enclosing = next((a for a in ast.ancestors(node) if a.node_type in _CLASS_LIKE_TYPES), None)
|
|
96
|
+
if enclosing is None:
|
|
97
|
+
return None
|
|
98
|
+
return _child_name(ast, enclosing, _IDENTIFIER_TYPES)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Non-empty name property of the first direct child of the given types, else None.
|
|
102
|
+
def _child_name(ast: AST, node: Node, child_types: frozenset[str]) -> str | None:
|
|
103
|
+
child = next((c for c in ast.succ(node) if c.node_type in child_types), None)
|
|
104
|
+
if child is None:
|
|
105
|
+
return None
|
|
106
|
+
name = child.all_properties.get("name", "")
|
|
107
|
+
return str(name) if name else None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Segment after the last namespace separator.
|
|
111
|
+
def _short_name(name: str) -> str:
|
|
112
|
+
return name.rsplit(NAMESPACE_SEPARATOR, 1)[-1]
|