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,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,,
|