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,186 @@
1
+ rules:
2
+ - id: user-input-pollution-reachability
3
+ message: "User input pollution reachability analysis"
4
+ severity: WARNING
5
+ languages: [php]
6
+ mode: taint
7
+ pattern-sources: &user_input_sources
8
+ # ── PHP superglobals ──
9
+ - pattern: $_GET[...]
10
+ - pattern: $_POST[...]
11
+ - pattern: $_REQUEST[...]
12
+ - pattern: $_COOKIE[...]
13
+ - pattern: $_SESSION[...]
14
+ - pattern: $_SERVER[...]
15
+ - pattern: $_ENV[...]
16
+ - pattern: $_FILES[...]
17
+ - pattern: $_GET
18
+ - pattern: $_POST
19
+ - pattern: $_REQUEST
20
+ - pattern: $_COOKIE
21
+ - pattern: $_SESSION
22
+ # ── Standard PHP input ──
23
+ - pattern: file_get_contents("php://input")
24
+ # ── CodeIgniter input ──
25
+ - pattern: $this->input->post(...)
26
+ - pattern: $this->input->get(...)
27
+ - pattern: $this->input->get_post(...)
28
+ - pattern: $this->input->cookie(...)
29
+ - pattern: $this->input->server(...)
30
+ - pattern: $this->input->input_stream(...)
31
+ # ── Laravel request ──
32
+ - pattern: $request->input(...)
33
+ - pattern: $request->get(...)
34
+ - pattern: $request->post(...)
35
+ - pattern: $request->query(...)
36
+ - pattern: $request->all()
37
+ - pattern: $request->only(...)
38
+ - pattern: $request->except(...)
39
+ - pattern: $request->cookie(...)
40
+ - pattern: $request->file(...)
41
+ - pattern: $request->validated(...)
42
+ - pattern: $request->safe(...)
43
+ - pattern: $request->integer(...)
44
+ - pattern: $request->boolean(...)
45
+ - pattern: $request->string(...)
46
+ - pattern: $request->float(...)
47
+ - pattern: $request->date(...)
48
+ - pattern: $request->enum(...)
49
+ - pattern: request()->input(...)
50
+ - pattern: request()->get(...)
51
+ - pattern: request($KEY)
52
+ # ── WordPress input ──
53
+ - pattern: get_query_var(...)
54
+ - pattern: $wp_query->get(...)
55
+ # ── Yii2 request ──
56
+ - pattern: Yii::$app->request->post(...)
57
+ - pattern: Yii::$app->request->get(...)
58
+ - pattern: Yii::$app->request->getBodyParam(...)
59
+ - pattern: Yii::$app->request->getBodyParams()
60
+ - pattern: Yii::$app->request->getQueryParam(...)
61
+ - pattern: Yii::$app->request->getQueryParams()
62
+ - pattern: $this->request->post(...)
63
+ - pattern: $this->request->get(...)
64
+ - pattern: $this->request->getBodyParam(...)
65
+ - pattern: $this->request->getQueryParam(...)
66
+ # ── Yii1 request ──
67
+ - pattern: Yii::app()->request->getParam(...)
68
+ - pattern: Yii::app()->request->getPost(...)
69
+ - pattern: Yii::app()->request->getQuery(...)
70
+ # ── Symfony HttpFoundation ──
71
+ - pattern: $request->request->get(...)
72
+ - pattern: $request->request->all()
73
+ - pattern: $request->query->get(...)
74
+ - pattern: $request->query->all()
75
+ - pattern: $request->cookies->get(...)
76
+ - pattern: $request->attributes->get(...)
77
+ - pattern: $request->headers->get(...)
78
+ - pattern: $request->files->get(...)
79
+ - pattern: $request->getContent()
80
+ # ── PSR-7 (Slim, Symfony bridge, Mezzio) ──
81
+ - pattern: $request->getParsedBody()
82
+ - pattern: $request->getQueryParams()
83
+ - pattern: $request->getAttribute(...)
84
+ - pattern: $request->getUploadedFiles()
85
+ # ── CakePHP request ──
86
+ - pattern: $this->request->getData(...)
87
+ - pattern: $this->request->getQuery(...)
88
+ - pattern: $this->request->getParam(...)
89
+ - pattern: $this->request->getCookie(...)
90
+ # ── Magento ──
91
+ - pattern: $this->getRequest()->getParam(...)
92
+ - pattern: $this->getRequest()->getParams()
93
+ - pattern: $this->getRequest()->getPost(...)
94
+ - pattern: $this->getRequest()->getPostValue(...)
95
+ - pattern: $this->getRequest()->getQuery(...)
96
+ - pattern: $request->getParam(...)
97
+ - pattern: $request->getPost(...)
98
+ # ── OpenCart (array access on request object) ──
99
+ - pattern: $this->request->post[...]
100
+ - pattern: $this->request->get[...]
101
+ - pattern: $this->request->request[...]
102
+ - pattern: $this->request->cookie[...]
103
+ - pattern: $this->request->files[...]
104
+ - pattern: $this->request->server[...]
105
+ pattern-propagators: &user_input_propagators
106
+ # Yii2 / Eloquent massive assignment: tainted source array flows into
107
+ # the model instance, tainting every later property read on it.
108
+ - pattern: $OBJ->load($FROM, ...)
109
+ from: $FROM
110
+ to: $OBJ
111
+ - pattern: $OBJ->setAttributes($FROM, ...)
112
+ from: $FROM
113
+ to: $OBJ
114
+ - pattern: $OBJ->fill($FROM, ...)
115
+ from: $FROM
116
+ to: $OBJ
117
+ # Laravel Eloquent in-place mass update: $model->update($request->all())
118
+ - pattern: $OBJ->update($FROM, ...)
119
+ from: $FROM
120
+ to: $OBJ
121
+ pattern-sinks:
122
+ - pattern: return $X;
123
+ exact: false
124
+ - pattern: echo $X;
125
+ exact: false
126
+ - pattern: print $X;
127
+ exact: false
128
+ - pattern: $X = $Y;
129
+ exact: false
130
+
131
+ - id: user-input-call-argument-reachability
132
+ message: "User input reaches a call argument"
133
+ severity: WARNING
134
+ languages: [php]
135
+ mode: taint
136
+ metadata:
137
+ seed_from_source_trace: true
138
+ pattern-sources:
139
+ - pattern-either: *user_input_sources
140
+ - &typed_request_property
141
+ patterns:
142
+ - pattern-either:
143
+ - patterns:
144
+ - pattern: $REQ->$FIELD
145
+ - metavariable-type:
146
+ metavariable: $REQ
147
+ type: Illuminate\Http\Request
148
+ - patterns:
149
+ - pattern: $REQ->$FIELD
150
+ - metavariable-type:
151
+ metavariable: $REQ
152
+ type: \Illuminate\Http\Request
153
+ - metavariable-regex:
154
+ metavariable: $FIELD
155
+ regex: '^[A-Za-z_][A-Za-z0-9_]*$'
156
+ - pattern-not-inside: $REQ->$FIELD(...)
157
+ - pattern-not-inside: |
158
+ function $FUNC(...) { ... $REQ = $VALUE; ... }
159
+ - pattern-not-inside: |
160
+ function $FUNC(...) { ... $ALIAS =& $REQ; ... }
161
+ - pattern-not-inside: |
162
+ function $FUNC(...) { ... $CALL(..., $REQ, ...); ... }
163
+ exact: true
164
+ pattern-propagators: *user_input_propagators
165
+ pattern-sinks:
166
+ - patterns:
167
+ - pattern: $FUNCTION(..., $ARG, ...)
168
+ - focus-metavariable: $ARG
169
+ exact: false
170
+ - patterns:
171
+ - pattern: $OBJECT->$METHOD(..., $ARG, ...)
172
+ - focus-metavariable: $ARG
173
+ exact: false
174
+ - patterns:
175
+ - pattern: $CLASS::$METHOD(..., $ARG, ...)
176
+ - focus-metavariable: $ARG
177
+ exact: false
178
+ - patterns:
179
+ - pattern: $OBJECT?->$METHOD(..., $ARG, ...)
180
+ - focus-metavariable: $ARG
181
+ exact: false
182
+ - patterns:
183
+ - pattern: new $CLASS(..., $ARG, ...)
184
+ - focus-metavariable: $ARG
185
+ exact: false
186
+ - *typed_request_property
cobraphp/config.py ADDED
@@ -0,0 +1,50 @@
1
+ """Analysis configuration: one flat, immutable `AnalysisConfig` (docs/design.md, Configuration)."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from pathlib import Path
6
+
7
+ DEFAULT_MAX_CONCURRENT_FILES = 4
8
+ DEFAULT_CONTEXT_DEPTH = 2
9
+ # Provisional in the Kotlin library (`AnalysisSettings.kt`).
10
+ DEFAULT_CANDIDATE_SET_BOUND = 3
11
+ DEFAULT_EXTRA_PATHS_PER_SINK = 1
12
+
13
+
14
+ class TargetFileSystem(Enum):
15
+ """How an include finds a file whose name differs in case."""
16
+
17
+ CASE_SENSITIVE = "CASE_SENSITIVE"
18
+ CASE_INSENSITIVE = "CASE_INSENSITIVE"
19
+
20
+
21
+ class StorageImplementation(Enum):
22
+ """The construction-time storage choice; report semantics do not change."""
23
+
24
+ INTEGRATED = "INTEGRATED"
25
+ CAPABILITIES = "CAPABILITIES"
26
+
27
+
28
+ @dataclass(frozen=True, slots=True, kw_only=True)
29
+ class AnalysisConfig:
30
+ """Configuration of `analyze_project` and `build_project`.
31
+
32
+ Fields marked `build` are read when the project is built; fields marked `analyze` are read by each analysis.
33
+ Replace fields with `dataclasses.replace(config, context_depth=3)`.
34
+ """
35
+
36
+ # build
37
+ document_sets: tuple[Path, ...] = ()
38
+ libraries: tuple[str, ...] = ()
39
+ should_skip_unparsable_files: bool = True
40
+ php_executable: Path | None = None
41
+ storage_implementation: StorageImplementation = StorageImplementation.INTEGRATED
42
+ max_concurrent_files: int = DEFAULT_MAX_CONCURRENT_FILES
43
+ target_file_system: TargetFileSystem = TargetFileSystem.CASE_SENSITIVE
44
+ context_depth: int = DEFAULT_CONTEXT_DEPTH
45
+ candidate_set_bound: int = DEFAULT_CANDIDATE_SET_BOUND
46
+ should_call_zend_error: bool = False
47
+ should_fail_fast: bool = False
48
+ # analyze; `None` means the same as `context_depth`
49
+ off_path_depth_limit: int | None = None
50
+ extra_paths_per_sink: int = field(default=DEFAULT_EXTRA_PATHS_PER_SINK)
cobraphp/entries.py ADDED
@@ -0,0 +1,39 @@
1
+ """The entry functions; each blocks until its task ends. Names follow the Kotlin entries in snake case."""
2
+
3
+ from collections.abc import Sequence
4
+ from pathlib import Path
5
+
6
+ from cobraphp._placeholder import not_implemented
7
+ from cobraphp.config import AnalysisConfig
8
+ from cobraphp.project import BuiltProject
9
+ from cobraphp.results import DiffReport, Output, ValidationReport
10
+
11
+
12
+ def analyze_project(project_dir: Path, config: AnalysisConfig | None = None) -> list[Output]:
13
+ """Parse, interpret and analyze a project, and release everything before returning.
14
+
15
+ `config=None` means `AnalysisConfig()`. Returns every `Finding` and `Unanalyzed`, ordered by file, line
16
+ (`None` first), output kind, then vulnerability type for findings or function and reason for unanalyzed parts.
17
+
18
+ Raises `ProjectNotFoundError`, `ParsingFailedError`, `DependencyUnavailableError`, `DocumentSetInvalidError`,
19
+ `ValueError` for a bad argument or setting, `RuntimeError` for an invariant breach.
20
+ """
21
+ not_implemented()
22
+
23
+
24
+ def build_project(project_dir: Path, config: AnalysisConfig | None = None) -> BuiltProject:
25
+ """Build and freeze a project and return a handle the caller closes.
26
+
27
+ Raises what `analyze_project` raises before its analysis stage.
28
+ """
29
+ not_implemented()
30
+
31
+
32
+ def validate_document_sets(document_sets: Sequence[Path]) -> ValidationReport:
33
+ """Verify Document Sets and report their problems and the Source kinds and Vulnerability types they use."""
34
+ not_implemented()
35
+
36
+
37
+ def diff_results(before: Sequence[Output], after: Sequence[Output]) -> DiffReport:
38
+ """Compare the findings of two output lists, matched by `Finding.id`."""
39
+ not_implemented()
cobraphp/errors.py ADDED
@@ -0,0 +1,25 @@
1
+ """Errors of the public interface, one per Kotlin `*Exception` (see docs/design.md, Exception / Error Types)."""
2
+
3
+
4
+ class CobraPhpError(Exception):
5
+ """Root of the run failures a caller can fix. Never raised directly; only its subclasses are."""
6
+
7
+
8
+ class ProjectNotFoundError(CobraPhpError):
9
+ """The project directory does not exist."""
10
+
11
+
12
+ class ParsingFailedError(CobraPhpError):
13
+ """A file failed to parse while `AnalysisConfig.should_skip_unparsable_files` is false."""
14
+
15
+
16
+ class DependencyUnavailableError(CobraPhpError):
17
+ """A required external program, the PHP interpreter that runs the parser, is missing."""
18
+
19
+
20
+ class GraphExportError(CobraPhpError):
21
+ """The export target of `BuiltProject.export_graphs` is invalid or not writable."""
22
+
23
+
24
+ class DocumentSetInvalidError(CobraPhpError):
25
+ """A Document Set fails verification. It carries the `Diagnostic` list; the attribute name is undecided."""
cobraphp/graphs.py ADDED
@@ -0,0 +1,63 @@
1
+ """Graph access, provisional: plain-data view of the graph export file (docs/design.md, Graph Access).
2
+
3
+ `BuiltProject.export_graphs` writes the export; `read_graph_export` reads it. Both are proposed, not decided.
4
+ """
5
+
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import TypeAlias
10
+
11
+ from cobraphp._placeholder import not_implemented
12
+
13
+ JsonValue: TypeAlias = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
14
+
15
+
16
+ @dataclass(frozen=True, slots=True, kw_only=True)
17
+ class GraphNode:
18
+ """One node record: `graph` is the graph name (AST, ADG, PDG or CCG); `state` and `path` occur on ADG nodes."""
19
+
20
+ id: str
21
+ graph: str
22
+ type: str
23
+ state: str | None
24
+ path: str | None
25
+ props: Mapping[str, JsonValue]
26
+
27
+
28
+ @dataclass(frozen=True, slots=True, kw_only=True)
29
+ class GraphEdge:
30
+ """One edge record; `labels` are the label ids of the contexts the edge is recorded under."""
31
+
32
+ source: str
33
+ target: str
34
+ type: str
35
+ labels: tuple[str, ...]
36
+ props: Mapping[str, JsonValue]
37
+
38
+
39
+ @dataclass(frozen=True, slots=True, kw_only=True)
40
+ class LabelOrder:
41
+ """One label-order record: the parent and name of a label id."""
42
+
43
+ label: str
44
+ parent: str
45
+ name: str
46
+
47
+
48
+ @dataclass(frozen=True, slots=True, kw_only=True)
49
+ class GraphExport:
50
+ """The content of one graph export file: header, nodes, edges and label-order records, in file order."""
51
+
52
+ header: Mapping[str, JsonValue]
53
+ nodes: tuple[GraphNode, ...]
54
+ edges: tuple[GraphEdge, ...]
55
+ label_order: tuple[LabelOrder, ...]
56
+
57
+
58
+ def read_graph_export(export_file: Path) -> GraphExport:
59
+ """Read the file `BuiltProject.export_graphs` wrote.
60
+
61
+ Raises `ValueError` when the file is not a graph export.
62
+ """
63
+ not_implemented()
cobraphp/project.py ADDED
@@ -0,0 +1,49 @@
1
+ """The handle of a built project."""
2
+
3
+ from pathlib import Path
4
+ from types import TracebackType
5
+ from typing import Self
6
+
7
+ from cobraphp._placeholder import not_implemented
8
+ from cobraphp.config import AnalysisConfig
9
+ from cobraphp.results import Output
10
+
11
+
12
+ class BuiltProject:
13
+ """A parsed and frozen project that stays open until `close`; use it as a context manager.
14
+
15
+ Concurrent `analyze` calls on one handle run one at a time.
16
+ """
17
+
18
+ def __init__(self, *, project_dir: Path, config: AnalysisConfig) -> None:
19
+ self.project_dir = project_dir
20
+ self.config = config
21
+
22
+ def analyze(self, config: AnalysisConfig | None = None) -> list[Output]:
23
+ """Analyze the project; `None` means `self.config`.
24
+
25
+ Raises `ValueError` naming the field when a `build` field of `config` differs from `self.config`.
26
+ """
27
+ not_implemented()
28
+
29
+ def export_graphs(self, output_file: Path) -> None:
30
+ """Write the graph export (provisional, see docs/design.md, Graph Access).
31
+
32
+ Raises `GraphExportError` when the target is invalid or not writable.
33
+ """
34
+ not_implemented()
35
+
36
+ def close(self) -> None:
37
+ """Release the project; a second call has no effect."""
38
+ not_implemented()
39
+
40
+ def __enter__(self) -> Self:
41
+ return self
42
+
43
+ def __exit__(
44
+ self,
45
+ exc_type: type[BaseException] | None,
46
+ exc_value: BaseException | None,
47
+ traceback: TracebackType | None,
48
+ ) -> None:
49
+ self.close()
cobraphp/py.typed ADDED
File without changes
cobraphp/results.py ADDED
@@ -0,0 +1,116 @@
1
+ """Result types: plain, immutable values (docs/design.md, Results)."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import TypeAlias
7
+
8
+
9
+ @dataclass(frozen=True, slots=True, kw_only=True)
10
+ class Location:
11
+ """A project-relative file and an optional line; `line` is `None` when unknown."""
12
+
13
+ file: Path
14
+ line: int | None
15
+
16
+
17
+ @dataclass(frozen=True, slots=True, kw_only=True)
18
+ class TaintSource:
19
+ """A Taint source of a `Finding`."""
20
+
21
+ location: Location
22
+ subject: str
23
+ source_kinds: tuple[str, ...]
24
+
25
+
26
+ @dataclass(frozen=True, slots=True, kw_only=True)
27
+ class CallSite:
28
+ """A Call site and the name of its callee."""
29
+
30
+ location: Location
31
+ callee: str
32
+
33
+
34
+ @dataclass(frozen=True, slots=True, kw_only=True)
35
+ class VulnerableCallPath:
36
+ """A Vulnerable call path from an entry point to the Sink call site."""
37
+
38
+ id: str
39
+ entry_point: Path
40
+ call_sites: tuple[CallSite, ...]
41
+
42
+
43
+ @dataclass(frozen=True, slots=True, kw_only=True)
44
+ class Finding:
45
+ """A taint flow from a Taint source to a Sink without the required Sanitizer.
46
+
47
+ One `Finding` appears per sink call and Vulnerability type.
48
+ """
49
+
50
+ id: str
51
+ vulnerability_type: str
52
+ sink: CallSite
53
+ port: str
54
+ sources: tuple[TaintSource, ...]
55
+ sanitizers: tuple[CallSite, ...]
56
+ paths: tuple[VulnerableCallPath, ...]
57
+ slice_node_ids: tuple[str, ...]
58
+
59
+ @property
60
+ def file(self) -> Path:
61
+ """The file of the sink."""
62
+ return self.sink.location.file
63
+
64
+ @property
65
+ def line(self) -> int | None:
66
+ """The line of the sink, or `None`."""
67
+ return self.sink.location.line
68
+
69
+
70
+ class UnanalyzedReason(Enum):
71
+ """Why a part of the project is Unanalyzed."""
72
+
73
+ PARSE_FAILURE = "parse failure"
74
+ INTERNAL_ERROR = "internal error"
75
+ TIME_LIMIT = "time limit expired"
76
+
77
+
78
+ @dataclass(frozen=True, slots=True, kw_only=True)
79
+ class Unanalyzed:
80
+ """A part of the project the analysis did not cover."""
81
+
82
+ file: Path
83
+ line: int | None
84
+ function: str | None
85
+ reason: UnanalyzedReason
86
+
87
+ def __str__(self) -> str:
88
+ """Render `file:line function(): reason`, omitting an absent line and function."""
89
+ head = f"{self.file}" if self.line is None else f"{self.file}:{self.line}"
90
+ name = "" if self.function is None else f" {self.function}()"
91
+ return f"{head}{name}: {self.reason.value}"
92
+
93
+
94
+ Output: TypeAlias = Finding | Unanalyzed
95
+
96
+
97
+ @dataclass(frozen=True, slots=True, kw_only=True)
98
+ class Diagnostic:
99
+ """One problem of a Document Set. Its fields are undecided in the Kotlin design."""
100
+
101
+
102
+ @dataclass(frozen=True, slots=True, kw_only=True)
103
+ class ValidationReport:
104
+ """The result of `validate_document_sets`."""
105
+
106
+ problems: tuple[Diagnostic, ...]
107
+ source_kinds_used: tuple[str, ...]
108
+ vulnerability_types_used: tuple[str, ...]
109
+
110
+
111
+ @dataclass(frozen=True, slots=True, kw_only=True)
112
+ class DiffReport:
113
+ """Findings added and removed between two output lists, matched by `Finding.id`.
114
+
115
+ Its fields are undecided in the Kotlin design.
116
+ """
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: cobraphp
3
+ Version: 0.1.0.dev1
4
+ Summary: Placeholder for the Python interface of CobraPHP, a static taint analyzer for PHP projects, and a pure-Python static analysis over php-parser-py and Semgrep.
5
+ License-Expression: GPL-2.0-only
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: cpg2py>=1.2.0
10
+ Requires-Dist: php-parser-py>=2.0.3
11
+ Requires-Dist: semgrep==1.85.0
12
+ Requires-Dist: setuptools<82
13
+ Dynamic: license-file
14
+
15
+ # bindings-python-cobraphp
16
+
17
+ Python interface of [CobraPHP](https://github.com/jhu-seclab-cobra/CobraPHP), the Cobra static taint analyzer for PHP web applications. The package `cobraphp` returns plain Python data: dataclasses, enums, `pathlib.Path` and built-in containers.
18
+
19
+ ## Status
20
+
21
+ **Placeholder.** This package is a placeholder for the CobraPHP Python interface. The jar interface is not implemented, and `cobraphp.analysis` is an interim pure-Python implementation that the CobraPHP implementation will replace in a later version. Interfaces and results may change between versions.
22
+
23
+ Two parts. The jar interface is declared only: its types, functions and errors are typed, and every entry raises `NotImplementedError` ("implementation waits for the CobraPHP jar"). The implementation will run the CobraPHP jar in the Python process through JPype; JPype is planned and not yet a dependency. The subpackage `cobraphp.analysis` is implemented: a pure-Python static analysis over [php-parser-py](https://pypi.org/project/php-parser-py/) and [Semgrep](https://semgrep.dev) that builds a call graph and Semgrep-seeded data flow from a parsed PHP project. The version is a pre-release, `0.1.0.dev1`. The name follows the pattern `bindings-<language>-<analyzer>`.
24
+
25
+ ## Install
26
+
27
+ From a checkout, with [uv](https://docs.astral.sh/uv/):
28
+
29
+ ```
30
+ uv sync
31
+ ```
32
+
33
+ `uv sync` installs the package, its dependencies at the versions of `uv.lock` and the check tools. `cobraphp.analysis` runs the `semgrep` program, which the Semgrep dependency installs into the environment; it must be on `PATH`, for example by running commands through `uv run`. The subpackage's Semgrep run sends no version-check request.
34
+
35
+ The planned release wheel will bundle the CobraPHP jar. Which Java runtime it uses is undecided ([open gaps](docs/design.md#open-gaps)). Python 3.11 or later.
36
+
37
+ ## Examples
38
+
39
+ Analyze a project and read the result:
40
+
41
+ ```python
42
+ from pathlib import Path
43
+ from cobraphp import AnalysisConfig, Finding, Unanalyzed, analyze_project
44
+
45
+ outputs = analyze_project(Path("/srv/project"), AnalysisConfig(context_depth=3))
46
+ for output in outputs:
47
+ print(output) # a Finding prints its dataclass; an Unanalyzed prints `file:line function(): reason`
48
+ complete = not any(isinstance(o, Unanalyzed) for o in outputs)
49
+ findings = [o for o in outputs if isinstance(o, Finding)]
50
+ ```
51
+
52
+ Build once, analyze several times, and export the graphs:
53
+
54
+ ```python
55
+ from dataclasses import replace
56
+ from cobraphp import build_project
57
+
58
+ with build_project(Path("/srv/project")) as built:
59
+ more = built.analyze(replace(built.config, extra_paths_per_sink=3))
60
+ built.export_graphs(Path("graphs.jsonl"))
61
+ ```
62
+
63
+ Build the call graph and the data flow of a project with `cobraphp.analysis`:
64
+
65
+ ```python
66
+ from pathlib import Path
67
+ from php_parser_py import PHPVersion, parse_project
68
+ from cobraphp.analysis import USER_INPUT, backward_slice, build_call_graph, build_data_flow, make_app_file_filter
69
+
70
+ root = Path("/srv/project")
71
+ ast = parse_project(str(root), target_php=PHPVersion(8, 5), file_filter=make_app_file_filter(root))
72
+ calls = [e for e in build_call_graph(ast).edges if e.type == "CCG_SITE_CALL"]
73
+ flow = build_data_flow(ast, timeout_seconds=600)
74
+ for node_id in flow.tainted[USER_INPUT]:
75
+ print(node_id, backward_slice(flow, node_id, max_depth=3))
76
+ ```
77
+
78
+ Handle a failure:
79
+
80
+ ```python
81
+ from cobraphp import ProjectNotFoundError
82
+
83
+ try:
84
+ analyze_project(Path("/missing"))
85
+ except ProjectNotFoundError as error:
86
+ print(error)
87
+ ```
88
+
89
+ ## Design
90
+
91
+ [docs/design.md](docs/design.md) states the types, functions, errors, lifecycle and open items. Companions: [mapping to Kotlin](docs/design-mapping.md), [backend boundary](docs/design-backend.md), [graph access](docs/design-graphs.md), [extension points](docs/design-extension.md), [query languages](docs/design-queries.md) and [analysis interface](docs/design-analysis-interface.md), the last four proposed, and [static analysis subpackage](docs/design-analysis-subpackage.md). The implementation plan is [docs/todo.md](docs/todo.md). Terms are those of the [glossary](docs/glossary.md).
92
+
93
+ ## Checks
94
+
95
+ ```
96
+ python -m compileall src
97
+ python -m pytest
98
+ python -m mypy
99
+ ```
100
+
101
+ Run them inside the environment, for example as `uv run python -m pytest`. The tests marked `integration` run the `semgrep` program; `python -m pytest -m "not integration"` skips them.
102
+
103
+ ## License
104
+
105
+ GPL-2.0-only, the licence of CobraPHP; see [LICENSE](LICENSE).
@@ -0,0 +1,35 @@
1
+ cobraphp/__init__.py,sha256=rDu9Dt2Wk7ounQRML_kEgd1q5alC_Dquc6QTCZDfdu4,1496
2
+ cobraphp/_backend.py,sha256=2C5kx4EUJjlwP2YGd6sT50y7uOionEKIZS9DEOEAFBE,1851
3
+ cobraphp/_placeholder.py,sha256=zdWzLNcbWG_B4iiU2vYm_S61zaV3pBuksort176S0R8,371
4
+ cobraphp/config.py,sha256=kcZZq8wjkZuc4tpB401JD4TMtBtwpr9YsLQjhEC-_LI,1821
5
+ cobraphp/entries.py,sha256=z4FAnYjwBL1Kgi9dv31KKmFf2nzP5157EWxtRrQ1jZk,1678
6
+ cobraphp/errors.py,sha256=oNVyF0Ld5PwLBkpxmCj--CSw9xfg11m2y2tJkWN8wSY,912
7
+ cobraphp/graphs.py,sha256=5f4ZbC4mouxlkeGTEhivDbhyK2ZwdO87KPWCnCnaXLI,1793
8
+ cobraphp/project.py,sha256=LL4RhwIB-eInyIqD30Us948bDFizDZzK31YhgZOOug0,1497
9
+ cobraphp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ cobraphp/results.py,sha256=fc5CUVPZ__SDdKyxQ1UyQJt3FSM6yfzOfaBCGL1Wtss,3055
11
+ cobraphp/analysis/__init__.py,sha256=Wzkrw6mkSHT3RjiyENA-r1lFNR0a3awu3ej8XTwJb1Q,2373
12
+ cobraphp/analysis/_ast_types.py,sha256=4_7ga_CcAbRxLTKuXCn-Bhn7Elprnq13r561hxJTtSw,661
13
+ cobraphp/analysis/_call_builder.py,sha256=TAQAxBSLwgrVzBFaU9QNLcDtHwbLNRSYg-kZlb3-ptw,6189
14
+ cobraphp/analysis/_call_graph.py,sha256=nlo_8VCvc5JzVTrH7MMicbT49Rgwd-EuAewLrJoALx8,8081
15
+ cobraphp/analysis/_call_graph_nodes.py,sha256=MuXtGdNe4iNals-0yPeLhykdxEr1JEjk1h_YypLLIaA,6205
16
+ cobraphp/analysis/_callee_index.py,sha256=ieDh75i4Wqbi2eJ5aghUHhGqBINB2CXDS07qtUbepAk,5230
17
+ cobraphp/analysis/_defuse_builder.py,sha256=2MZvzFqqGMYS4HRIEoC_wuXT3_tFDZnJ584tox0jJ5s,14662
18
+ cobraphp/analysis/_errors.py,sha256=cn71QQpesqbkitJdkg9bMIcvXlPixRxol7CjOMZM0i0,531
19
+ cobraphp/analysis/_file_filter.py,sha256=V5QY8iskS0wx7nyaqya_LWGRqkAorUTDWUdEEyUtpqU,6014
20
+ cobraphp/analysis/_project_root.py,sha256=cuS_sOGZBqDp5H6HWn_xLaxAyVlsfxwADgc0MheC-fY,1050
21
+ cobraphp/analysis/_records.py,sha256=GcafhC4iaJS2U-idCe5l3VM-AawBAMSmikiKAHaOQMA,5937
22
+ cobraphp/analysis/_semgrep_runner.py,sha256=t8E9SVEgIw-yCiG5rOdAo-N2gHSd60vrE1WwwntTWfk,12452
23
+ cobraphp/analysis/_source_kinds.py,sha256=0LH0kkSQ13c7Ys2kf4QhwuCpyKKC3rn6c9ZXadkr47Q,144
24
+ cobraphp/analysis/_taint_graph.py,sha256=HXd_g-yuzItyWQ9pj_p1M91UHKpoLOBwlBPqmwbnUuQ,7661
25
+ cobraphp/analysis/_taint_propagator.py,sha256=byfvZj1xdnngpf57rBS6esd3dTLqy6CZfusqOvZVx7I,10400
26
+ cobraphp/analysis/_taint_rules.py,sha256=b77hFwh6zURdqfx6jWZwTbS24hE03t7wtlYEckgaO5I,4822
27
+ cobraphp/analysis/frameworks.toml,sha256=MUGFYwMC6P7h-cj2_WUMGytYHWA9ZCTuGg_7LTkmy1k,1885
28
+ cobraphp/analysis/php_ast_types.toml,sha256=fvh3SfjGm1IyJvRkBAhnOLnxCdT5ejowxDBRuOIxOcg,1032
29
+ cobraphp/analysis/rules/database_taint.yaml,sha256=xCx64v2RjponPfEYZOQUqSqP_ltV8YB-t5NldjOstcI,3495
30
+ cobraphp/analysis/rules/user_input_taint.yaml,sha256=47nknffSg_wIKGPg8Ya-r4POa4nWswopju2IEkcFvtc,6998
31
+ cobraphp-0.1.0.dev1.dist-info/licenses/LICENSE,sha256=-cN1ob5KQfe3AwHdg8kcuJ5BVnR4hZt37vN1pS14JQU,18091
32
+ cobraphp-0.1.0.dev1.dist-info/METADATA,sha256=8Cj5ahaqteGDm30ccpJbsX385XE6_0F8LFpH5Uo7Ndo,4910
33
+ cobraphp-0.1.0.dev1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
34
+ cobraphp-0.1.0.dev1.dist-info/top_level.txt,sha256=55CayiaOZMnPngJQf8IqO50E-l0BQONI8DTpnn-gtJk,9
35
+ cobraphp-0.1.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+