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.
@@ -0,0 +1,295 @@
1
+ """Build DataFlowGraph via Semgrep source identification + AST def-use chains + CallGraph inter-proc edges.
2
+
3
+ Four-phase pipeline:
4
+ 1. Semgrep identifies tainted expressions per Source kind (intraprocedural)
5
+ 2. AST-based def-use chains create cross-statement edges within functions
6
+ 3. CallGraph creates cross-function edges (argument->parameter, return->callsite)
7
+ 4. BFS propagates taint from Semgrep seeds through the combined graph
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Iterator
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+ from php_parser_py import AST, Node
17
+
18
+ from ._errors import StaticAnalysisError
19
+ from ._call_graph import CallGraph
20
+ from ._defuse_builder import build_interproc_edges, build_intraproc_edges
21
+ from ._semgrep_runner import (
22
+ OffsetIndex,
23
+ SemgrepResult,
24
+ build_file_id_index,
25
+ build_offset_index,
26
+ resolve_file_node_id,
27
+ resolve_project_path,
28
+ run_semgrep,
29
+ )
30
+ from ._taint_graph import DataFlowGraph
31
+ from ._taint_rules import CONSTANT_NODE_TYPES, TAINT_RULES, select_seed_locations
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class _SeedExpansion:
36
+ """Shared context for the seed-expansion pass.
37
+
38
+ Bundles the AST, the dataflow graph being populated, and the accumulator
39
+ of newly discovered seeds so expansion helpers take one workspace rather
40
+ than threading four positional arguments each.
41
+ """
42
+
43
+ ast: AST
44
+ dataflow_graph: DataFlowGraph
45
+ expanded: dict[str, set[str]]
46
+
47
+ def record(self, node_id: str, source_types: set[str]) -> None:
48
+ """Ensure node exists and merge source_types into its expanded seeds."""
49
+ _ensure_node(self.dataflow_graph, node_id)
50
+ self.expanded.setdefault(node_id, set()).update(source_types)
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class _SeedCollection:
55
+ """Shared context for mapping Semgrep results into seed nodes.
56
+
57
+ Bundles the project path, file-id index, offset index, and graph so
58
+ result processing takes one workspace rather than threading positionally.
59
+ """
60
+
61
+ project_path: Path
62
+ file_id_by_path: dict[str, str]
63
+ offset_index: OffsetIndex
64
+ dataflow_graph: DataFlowGraph
65
+
66
+
67
+ class TaintPropagator:
68
+ """Combines Semgrep intraprocedural taint identification with AST def-use chains and CallGraph inter-proc edges.
69
+
70
+ Attributes:
71
+ _ast: PHP project AST.
72
+ _call_graph: Populated CallGraph for inter-procedural analysis.
73
+ _timeout: Maximum execution time in seconds.
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ ast: AST,
79
+ call_graph: CallGraph,
80
+ timeout: int | None,
81
+ target_files: set[str] | None = None,
82
+ ) -> None:
83
+ self._ast: AST = ast
84
+ self._call_graph: CallGraph = call_graph
85
+ self._timeout: int | None = timeout
86
+ self._target_files: set[str] | None = target_files
87
+
88
+ def propagate(self) -> DataFlowGraph:
89
+ """Run 4-phase taint analysis pipeline.
90
+
91
+ Returns:
92
+ Populated DataFlowGraph with connected taint paths.
93
+
94
+ Raises:
95
+ StaticAnalysisError: Semgrep execution failure or invalid AST structure.
96
+ """
97
+ dataflow_graph = DataFlowGraph()
98
+ project_path = resolve_project_path(self._ast)
99
+ if not project_path.is_dir():
100
+ raise StaticAnalysisError(f"Project path is not a directory: {project_path}")
101
+
102
+ collection = _SeedCollection(
103
+ project_path=project_path,
104
+ file_id_by_path=build_file_id_index(self._ast),
105
+ offset_index=build_offset_index(self._ast),
106
+ dataflow_graph=dataflow_graph,
107
+ )
108
+ seeds: dict[str, set[str]] = {}
109
+ for source_type in TAINT_RULES:
110
+ self._collect_semgrep_seeds(collection, source_type, seeds)
111
+
112
+ _expand_seeds_to_subexpressions(self._ast, seeds, dataflow_graph)
113
+ build_intraproc_edges(self._ast, dataflow_graph)
114
+ build_interproc_edges(self._ast, self._call_graph, dataflow_graph)
115
+ _propagate_taint(seeds, dataflow_graph)
116
+
117
+ return dataflow_graph
118
+
119
+ # ── Phase 1: Semgrep seed collection ─────────────────────────────
120
+
121
+ def _collect_semgrep_seeds(
122
+ self,
123
+ collection: _SeedCollection,
124
+ source_type: str,
125
+ seeds: dict[str, set[str]],
126
+ ) -> None:
127
+ """Run Semgrep for source_type, map results to AST nodes, populate seeds."""
128
+ rule_yaml = TAINT_RULES.get(source_type)
129
+ if not rule_yaml:
130
+ return
131
+
132
+ semgrep_output = run_semgrep(rule_yaml, collection.project_path, self._timeout, self._target_files)
133
+
134
+ for result in semgrep_output.get("results", []):
135
+ _process_semgrep_result(collection, result, source_type, seeds)
136
+
137
+
138
+ # ── Phase 1: Semgrep result processing ──────────────────────────────
139
+
140
+
141
+ def _process_semgrep_result(
142
+ collection: _SeedCollection,
143
+ result: SemgrepResult,
144
+ source_type: str,
145
+ seeds: dict[str, set[str]],
146
+ ) -> None:
147
+ """Map a single Semgrep result to an AST node and register as seed."""
148
+ locations = select_seed_locations(result, collection.project_path, collection.file_id_by_path)
149
+ for location in locations:
150
+ file_node_id = resolve_file_node_id(location.path, collection.project_path, collection.file_id_by_path)
151
+ if location.end_offset is None:
152
+ node_id = collection.offset_index.resolve(location.offset, file_node_id)
153
+ elif file_node_id is not None:
154
+ node_id = collection.offset_index.resolve_source_span(location.offset, location.end_offset, file_node_id)
155
+ else:
156
+ continue
157
+ if node_id:
158
+ seeds.setdefault(node_id, set()).add(source_type)
159
+ _ensure_node(collection.dataflow_graph, node_id, source_type)
160
+
161
+
162
+ # ── Phase 1b: Seed expansion ────────────────────────────────────────
163
+
164
+
165
+ def _expand_seeds_to_subexpressions(
166
+ ast: AST,
167
+ seeds: dict[str, set[str]],
168
+ dataflow_graph: DataFlowGraph,
169
+ ) -> None:
170
+ """Expand Semgrep seed nodes to their child expressions via AST edges.
171
+
172
+ Bridges statement-level Semgrep results to expression-level
173
+ nodes that Phase 2/3 edges connect at.
174
+ """
175
+ ctx = _SeedExpansion(ast=ast, dataflow_graph=dataflow_graph, expanded={})
176
+
177
+ for node_id, source_types in list(seeds.items()):
178
+ _expand_assignment_context(ctx, node_id, source_types)
179
+ _expand_node(ctx, node_id, source_types)
180
+
181
+ for node_id, source_types in ctx.expanded.items():
182
+ seeds.setdefault(node_id, set()).update(source_types)
183
+
184
+
185
+ def _expand_assignment_context(
186
+ ctx: _SeedExpansion,
187
+ node_id: str,
188
+ source_types: set[str],
189
+ ) -> None:
190
+ """If node is LHS of an assignment, also expand the assignment RHS as seed."""
191
+ node = ctx.ast.node(node_id)
192
+ if node is None:
193
+ return
194
+
195
+ for parent in ctx.ast.prev(node):
196
+ if parent.node_type != "Expr_Assign":
197
+ continue
198
+ children = list(ctx.ast.succ(parent))
199
+ if len(children) < 2:
200
+ continue
201
+ if children[0].id != node_id:
202
+ continue
203
+ _seed_assignment_and_rhs(ctx, parent, children[1], source_types)
204
+
205
+
206
+ def _seed_assignment_and_rhs(
207
+ ctx: _SeedExpansion,
208
+ parent: Node,
209
+ rhs: Node,
210
+ source_types: set[str],
211
+ ) -> None:
212
+ """Seed an assignment node and its RHS subexpressions."""
213
+ ctx.record(parent.id, source_types)
214
+
215
+ if rhs.node_type not in CONSTANT_NODE_TYPES:
216
+ ctx.record(rhs.id, source_types)
217
+ ctx.dataflow_graph.add_structural_edge(parent.id, rhs.id)
218
+ _expand_node(ctx, rhs.id, source_types)
219
+
220
+
221
+ def _expand_node(
222
+ ctx: _SeedExpansion,
223
+ node_id: str,
224
+ source_types: set[str],
225
+ ) -> None:
226
+ """Expand a node to its AST descendants in pre-order, creating structural edges.
227
+
228
+ Walks with an explicit stack of child iterators, so a deeply nested
229
+ expression (a long generated concatenation) does not exhaust the Python
230
+ call stack. Constant children are skipped together with their subtrees.
231
+ """
232
+ node = ctx.ast.node(node_id)
233
+ if node is None:
234
+ return
235
+
236
+ pending: list[tuple[str, Iterator[Node]]] = [(node_id, iter(ctx.ast.succ(node)))]
237
+ while pending:
238
+ parent_id, children = pending[-1]
239
+ child = next(children, None)
240
+ if child is None:
241
+ pending.pop()
242
+ continue
243
+ if child.node_type in CONSTANT_NODE_TYPES:
244
+ continue
245
+ ctx.record(child.id, source_types)
246
+ ctx.dataflow_graph.add_structural_edge(parent_id, child.id)
247
+ child_node = ctx.ast.node(child.id)
248
+ if child_node is not None:
249
+ pending.append((child.id, iter(ctx.ast.succ(child_node))))
250
+
251
+
252
+ # ── Phase 4: BFS taint propagation ──────────────────────────────────
253
+
254
+
255
+ def _propagate_taint(
256
+ seeds: dict[str, set[str]],
257
+ dataflow_graph: DataFlowGraph,
258
+ ) -> None:
259
+ """BFS from seed nodes, propagating taint through graph edges."""
260
+ for source_type in TAINT_RULES:
261
+ seed_ids = [nid for nid, types in seeds.items() if source_type in types]
262
+ for current in dataflow_graph.reachable_from(seed_ids):
263
+ _mark_taint(dataflow_graph, current, source_type)
264
+
265
+
266
+ def _mark_taint(
267
+ dataflow_graph: DataFlowGraph,
268
+ node_id: str,
269
+ source_type: str,
270
+ ) -> None:
271
+ """Mark a node with a taint source if not already present."""
272
+ df_node = dataflow_graph.get_dataflow_node(node_id)
273
+ if source_type not in df_node.taints:
274
+ current_taints = df_node.taints.copy()
275
+ current_taints.append(source_type)
276
+ df_node.taints = current_taints
277
+
278
+
279
+ # ── Node helpers ─────────────────────────────────────────────────────
280
+
281
+
282
+ def _ensure_node(
283
+ dataflow_graph: DataFlowGraph,
284
+ node_id: str,
285
+ source_type: str | None = None,
286
+ ) -> None:
287
+ """Ensure node exists in graph; optionally set initial taint."""
288
+ if not dataflow_graph.storage.contains_node(node_id):
289
+ df_node = dataflow_graph.add_dataflow_node(node_id)
290
+ if source_type is not None:
291
+ df_node.taints = [source_type]
292
+ return
293
+
294
+ if source_type is not None:
295
+ _mark_taint(dataflow_graph, node_id, source_type)
@@ -0,0 +1,123 @@
1
+ """Semgrep taint rule loading and AST node type constants.
2
+
3
+ Loads taint rules from YAML files in the rules/ directory.
4
+ Node type sets loaded from php_ast_types.toml.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import cast
12
+
13
+ from ._ast_types import load_php_ast_types
14
+ from ._semgrep_runner import SemgrepResult
15
+ from ._source_kinds import DATABASE, USER_INPUT
16
+
17
+ _RULES_DIR = Path(__file__).parent / "rules"
18
+
19
+ # Insertion order is the order of the Semgrep runs and of each node's taints list.
20
+ _RULE_FILES: dict[str, str] = {
21
+ USER_INPUT: "user_input_taint.yaml",
22
+ DATABASE: "database_taint.yaml",
23
+ }
24
+
25
+
26
+ def _load_rule(filename: str) -> str:
27
+ """Load a YAML rule file from the rules directory."""
28
+ path = _RULES_DIR / filename
29
+ return path.read_text(encoding="utf-8")
30
+
31
+
32
+ TAINT_RULES: dict[str, str] = {source: _load_rule(filename) for source, filename in _RULE_FILES.items()}
33
+
34
+ CONSTANT_NODE_TYPES, CALLABLE_TYPES, _ = load_php_ast_types()
35
+
36
+
37
+ # Identity and metadata are the contract of the call-argument rule in the YAML.
38
+ _CALL_ARGUMENT_RULE = "user-input-call-argument-reachability"
39
+ _INPUT_OUTPUT_RULE = "user-input-pollution-reachability"
40
+ _TRACE_SEED_METADATA = "seed_from_source_trace"
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class SeedLocation:
45
+ """An offset seed or an exact expression span from a source trace."""
46
+
47
+ path: str
48
+ offset: int
49
+ end_offset: int | None = None
50
+
51
+
52
+ def select_seed_locations(
53
+ result: SemgrepResult, project_path: Path, file_id_by_path: dict[str, str]
54
+ ) -> list[SeedLocation]:
55
+ """Keep the rule's seed and supplement input/output matches with a source."""
56
+ seed = select_seed_result(result, project_path, file_id_by_path)
57
+ path, offset = _field(seed, "path"), _field(_field(seed, "start"), "offset")
58
+ if not isinstance(path, str) or not isinstance(offset, int):
59
+ return []
60
+ selected = [SeedLocation(path, offset)]
61
+ check_id = result.get("check_id")
62
+ if isinstance(check_id, str) and check_id.rsplit(".", 1)[-1] == _INPUT_OUTPUT_RULE:
63
+ source = _trace_seed_location(result, project_path, file_id_by_path)
64
+ if source is not None:
65
+ selected.append(source)
66
+ return selected
67
+
68
+
69
+ def select_seed_result(
70
+ result: SemgrepResult, project_path: Path, file_id_by_path: dict[str, str]
71
+ ) -> SemgrepResult | None:
72
+ """Select a validated source location for the call-argument rule.
73
+
74
+ Existing rules retain their result location. Unsupported or missing source
75
+ traces on the call rule produce no seed, rather than a whole-argument seed.
76
+ """
77
+ check_id = result.get("check_id")
78
+ if not isinstance(check_id, str) or check_id.rsplit(".", 1)[-1] != _CALL_ARGUMENT_RULE:
79
+ return result
80
+ extra = result.get("extra")
81
+ if _field(_field(extra, "metadata"), _TRACE_SEED_METADATA) is not True:
82
+ return None
83
+ source = _trace_seed_location(result, project_path, file_id_by_path)
84
+ return {"path": source.path, "start": {"offset": source.offset}} if source is not None else None
85
+
86
+
87
+ def _trace_seed_location(
88
+ result: SemgrepResult, project_path: Path, file_id_by_path: dict[str, str]
89
+ ) -> SeedLocation | None:
90
+ extra = result.get("extra")
91
+ location = _source_location(_field(_field(extra, "dataflow_trace"), "taint_source"))
92
+ seed = _local_source_result(location, result, project_path)
93
+ if seed is None or str(seed["path"]) not in file_id_by_path:
94
+ return None
95
+ offset = cast(int, _field(seed["start"], "offset"))
96
+ end = cast(int, _field(_field(location, "end"), "offset"))
97
+ return SeedLocation(str(seed["path"]), offset, end)
98
+
99
+
100
+ def _field(value: object, name: str) -> object:
101
+ # Semgrep's optional trace payload is validated at this JSON boundary.
102
+ return cast(object, value.get(name)) if isinstance(value, dict) else None
103
+
104
+
105
+ def _source_location(source: object) -> object:
106
+ if not isinstance(source, list) or len(source) != 2 or source[0] != "CliLoc":
107
+ return None
108
+ payload = source[1]
109
+ return payload[0] if isinstance(payload, list) and len(payload) == 2 else None
110
+
111
+
112
+ def _local_source_result(location: object, result: SemgrepResult, project_path: Path) -> SemgrepResult | None:
113
+ source_path, sink_path = _field(location, "path"), result.get("path")
114
+ if not isinstance(source_path, str) or not isinstance(sink_path, str):
115
+ return None
116
+ source = (project_path / source_path).resolve()
117
+ if source != (project_path / sink_path).resolve() or not source.is_relative_to(project_path.resolve()):
118
+ return None
119
+ start = _field(_field(location, "start"), "offset")
120
+ end = _field(_field(location, "end"), "offset")
121
+ if type(start) is not int or type(end) is not int or not 0 <= start < end:
122
+ return None
123
+ return {"path": str(source), "start": {"offset": start}}
@@ -0,0 +1,38 @@
1
+ # Framework identity-marker registry for in-tree (vendored, no-vendor/no-namespace)
2
+ # framework detection. Each entry names ONE framework by a UNIQUE identity
3
+ # bootstrap file, never a role-generic name (e.g. Kernel.php). When the marker
4
+ # file is found AND its head contains the signature string, the marker's subtree
5
+ # root (or only its exclude_subpaths) is excluded from the parsed files and the Semgrep scan.
6
+ #
7
+ # Fields:
8
+ # name framework identifier (logged on exclusion).
9
+ # marker POSIX relative-path suffix of the identity bootstrap file. Its
10
+ # component count fixes how many path levels to strip to the root.
11
+ # contains substring that must appear in the marker file head; disambiguates
12
+ # name collisions. Empty match = no exclusion (conservative bias).
13
+ # exclude_subpaths (optional) subpaths under the detected root to exclude.
14
+ # Omitted/empty = exclude the whole subtree. Set it for a monorepo
15
+ # framework that also ships application code under the same root, so
16
+ # only the framework dirs drop and business code is kept.
17
+
18
+ [[framework]]
19
+ name = "yii1"
20
+ marker = "YiiBase.php"
21
+ contains = "class YiiBase"
22
+
23
+ # Magento 2 ships framework, tooling, and application code under one root. Drop
24
+ # the non-business dirs (dev tooling/tests, low-level lib internals, the setup
25
+ # installer) and keep app/code (e.g. Magento/Sales, Checkout, Quote).
26
+ [[framework]]
27
+ name = "magento2"
28
+ marker = "app/etc/NonComposerComponentRegistration.php"
29
+ contains = 'namespace Magento\NonComposerComponentRegistration;'
30
+ exclude_subpaths = ["dev", "lib/internal", "setup"]
31
+
32
+ # ThinkPHP deployments can write generated PHP below runtime/cache.
33
+ # Keep application and framework source under the same server root in scope.
34
+ [[framework]]
35
+ name = "thinkphp"
36
+ marker = "thinkphp/base.php"
37
+ contains = "namespace think;"
38
+ exclude_subpaths = ["runtime/cache"]
@@ -0,0 +1,29 @@
1
+ # PHP AST node type classifications read by the data-flow analysis.
2
+
3
+ [constant_node_types]
4
+ # Scalar literals and magic constants — not taintable
5
+ values = [
6
+ "Scalar_String", "Scalar_Int", "Scalar_Float",
7
+ "Scalar_MagicConst_Line", "Scalar_MagicConst_File",
8
+ "Scalar_MagicConst_Dir", "Scalar_MagicConst_Class",
9
+ "Scalar_MagicConst_Trait", "Scalar_MagicConst_Method",
10
+ "Scalar_MagicConst_Function", "Scalar_MagicConst_Namespace",
11
+ "Scalar_MagicConst_Property",
12
+ "Expr_ConstFetch", "Expr_ClassConstFetch",
13
+ ]
14
+
15
+ [callable_types]
16
+ values = [
17
+ "Stmt_Function", "Stmt_ClassMethod", "Expr_Closure", "Expr_ArrowFunction",
18
+ ]
19
+
20
+ [assign_node_types]
21
+ values = [
22
+ "Expr_Assign",
23
+ "Expr_AssignOp_Plus", "Expr_AssignOp_Minus",
24
+ "Expr_AssignOp_Mul", "Expr_AssignOp_Div", "Expr_AssignOp_Mod",
25
+ "Expr_AssignOp_Concat",
26
+ "Expr_AssignOp_BitwiseAnd", "Expr_AssignOp_BitwiseOr", "Expr_AssignOp_BitwiseXor",
27
+ "Expr_AssignOp_ShiftLeft", "Expr_AssignOp_ShiftRight",
28
+ "Expr_AssignOp_Pow", "Expr_AssignOp_Coalesce",
29
+ ]
@@ -0,0 +1,94 @@
1
+ rules:
2
+ - id: database-pollution-reachability
3
+ message: "Database pollution reachability analysis"
4
+ severity: WARNING
5
+ languages: [php]
6
+ mode: taint
7
+ pattern-sources:
8
+ # ── Bare PHP database functions ──
9
+ - pattern: mysqli_query(...)
10
+ - pattern: mysql_query(...)
11
+ - pattern: $PDO->query(...)
12
+ - pattern: $PDO->prepare(...)
13
+ - pattern: $STMT->execute(...)
14
+ - pattern: $PDO->exec(...)
15
+ - pattern: pg_query(...)
16
+ - pattern: pg_fetch_assoc(...)
17
+ - pattern: pg_fetch_row(...)
18
+ - pattern: pg_fetch_array(...)
19
+ # ── MySQLi fetch ──
20
+ - pattern: mysqli_fetch_assoc(...)
21
+ - pattern: mysqli_fetch_array(...)
22
+ - pattern: mysqli_fetch_row(...)
23
+ - pattern: $STMT->fetch(...)
24
+ - pattern: $STMT->fetchAll(...)
25
+ - pattern: $RESULT->fetch_assoc()
26
+ - pattern: $RESULT->fetch_array()
27
+ # ── SQLite3 ──
28
+ - pattern: $DB->querySingle(...)
29
+ - pattern: $RESULT->fetchArray(...)
30
+ # ── CodeIgniter query builder ──
31
+ - pattern: $this->db->query(...)
32
+ - pattern: $this->db->get(...)
33
+ - pattern: $this->db->get_where(...)
34
+ - pattern: $QUERY->result()
35
+ - pattern: $QUERY->result_array()
36
+ - pattern: $QUERY->row()
37
+ - pattern: $QUERY->row_array()
38
+ - pattern: $QUERY->num_rows()
39
+ # ── Laravel / Eloquent ──
40
+ - pattern: $MODEL::find(...)
41
+ - pattern: $MODEL::findOrFail(...)
42
+ - pattern: $MODEL::where(...)->get()
43
+ - pattern: $MODEL::where(...)->first()
44
+ - pattern: $MODEL::all()
45
+ - pattern: $QUERY->get()
46
+ - pattern: $QUERY->first()
47
+ - pattern: $QUERY->pluck(...)
48
+ - pattern: $QUERY->value(...)
49
+ - pattern: DB::table(...)->get()
50
+ - pattern: DB::table(...)->first()
51
+ - pattern: DB::select(...)
52
+ - pattern: DB::table(...)->where(...)->get()
53
+ - pattern: DB::table(...)->where(...)->first()
54
+ # ── WordPress ──
55
+ - pattern: $wpdb->get_results(...)
56
+ - pattern: $wpdb->get_row(...)
57
+ - pattern: $wpdb->get_var(...)
58
+ - pattern: $wpdb->get_col(...)
59
+ - pattern: get_post_meta(...)
60
+ - pattern: get_option(...)
61
+ - pattern: get_user_meta(...)
62
+ # ── Doctrine ORM (Symfony, PrestaShop, Sylius, Shopware) ──
63
+ - pattern: $REPO->find(...)
64
+ - pattern: $REPO->findOneBy(...)
65
+ - pattern: $REPO->findBy(...)
66
+ - pattern: $REPO->findAll()
67
+ - pattern: $EM->getRepository(...)->find(...)
68
+ - pattern: $EM->getRepository(...)->findOneBy(...)
69
+ - pattern: $this->getDoctrine()->getRepository(...)
70
+ # ── Yii ActiveRecord (Yii1 + Yii2) ──
71
+ - pattern: $MODEL::findOne(...)
72
+ - pattern: $MODEL::findAll(...)
73
+ - pattern: $MODEL::find()->one()
74
+ - pattern: $MODEL::find()->all()
75
+ - pattern: $MODEL::find()->where(...)->one()
76
+ - pattern: $MODEL::find()->where(...)->all()
77
+ - pattern: $MODEL::model()->findByPk(...)
78
+ - pattern: $MODEL::model()->find(...)
79
+ # ── CakePHP ORM ──
80
+ - pattern: $TABLE->get(...)
81
+ - pattern: $TABLE->find(...)->first()
82
+ - pattern: $TABLE->find(...)->all()
83
+ # ── Magento ──
84
+ - pattern: $COLLECTION->getItems()
85
+ - pattern: $MODEL->load(...)->getData(...)
86
+ pattern-sinks:
87
+ - pattern: return $X;
88
+ exact: false
89
+ - pattern: echo $X;
90
+ exact: false
91
+ - pattern: print $X;
92
+ exact: false
93
+ - pattern: $X = $Y;
94
+ exact: false