codepathfinder 1.2.0__py3-none-manylinux_2_17_aarch64.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.
- codepathfinder/__init__.py +48 -0
- codepathfinder/bin/pathfinder +0 -0
- codepathfinder/cli/__init__.py +204 -0
- codepathfinder/config.py +92 -0
- codepathfinder/dataflow.py +193 -0
- codepathfinder/decorators.py +158 -0
- codepathfinder/ir.py +107 -0
- codepathfinder/logic.py +101 -0
- codepathfinder/matchers.py +243 -0
- codepathfinder/presets.py +135 -0
- codepathfinder/propagation.py +250 -0
- codepathfinder-1.2.0.dist-info/METADATA +111 -0
- codepathfinder-1.2.0.dist-info/RECORD +33 -0
- codepathfinder-1.2.0.dist-info/WHEEL +5 -0
- codepathfinder-1.2.0.dist-info/entry_points.txt +2 -0
- codepathfinder-1.2.0.dist-info/licenses/LICENSE +661 -0
- codepathfinder-1.2.0.dist-info/top_level.txt +2 -0
- rules/__init__.py +36 -0
- rules/container_combinators.py +209 -0
- rules/container_decorators.py +223 -0
- rules/container_ir.py +104 -0
- rules/container_matchers.py +230 -0
- rules/container_programmatic.py +115 -0
- rules/python/__init__.py +0 -0
- rules/python/deserialization/__init__.py +0 -0
- rules/python/deserialization/pickle_loads.py +479 -0
- rules/python/django/__init__.py +0 -0
- rules/python/django/sql_injection.py +355 -0
- rules/python/flask/__init__.py +0 -0
- rules/python/flask/debug_mode.py +374 -0
- rules/python/injection/__init__.py +0 -0
- rules/python_decorators.py +177 -0
- rules/python_ir.py +80 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Matcher functions for Dockerfile and docker-compose rules.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Any, List, Dict
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Matcher:
|
|
11
|
+
"""Base class for all matchers."""
|
|
12
|
+
|
|
13
|
+
type: str
|
|
14
|
+
params: Dict[str, Any] = field(default_factory=dict)
|
|
15
|
+
|
|
16
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
17
|
+
"""Convert matcher to dictionary for JSON IR."""
|
|
18
|
+
return {"type": self.type, **self.params}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# --- Dockerfile Matchers ---
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def instruction(
|
|
25
|
+
type: str,
|
|
26
|
+
# FROM instruction
|
|
27
|
+
base_image: Optional[str] = None,
|
|
28
|
+
image_tag: Optional[str] = None,
|
|
29
|
+
image_tag_regex: Optional[str] = None,
|
|
30
|
+
missing_digest: Optional[bool] = None,
|
|
31
|
+
# USER instruction
|
|
32
|
+
user_name: Optional[str] = None,
|
|
33
|
+
user_name_regex: Optional[str] = None,
|
|
34
|
+
# EXPOSE instruction
|
|
35
|
+
port: Optional[int] = None,
|
|
36
|
+
port_less_than: Optional[int] = None,
|
|
37
|
+
port_greater_than: Optional[int] = None,
|
|
38
|
+
protocol: Optional[str] = None,
|
|
39
|
+
# ARG instruction
|
|
40
|
+
arg_name: Optional[str] = None,
|
|
41
|
+
arg_name_regex: Optional[str] = None,
|
|
42
|
+
# COPY/ADD instruction
|
|
43
|
+
copy_from: Optional[str] = None,
|
|
44
|
+
chown: Optional[str] = None,
|
|
45
|
+
missing_flag: Optional[str] = None,
|
|
46
|
+
# HEALTHCHECK instruction
|
|
47
|
+
healthcheck_interval_less_than: Optional[str] = None,
|
|
48
|
+
healthcheck_timeout_greater_than: Optional[str] = None,
|
|
49
|
+
healthcheck_retries_greater_than: Optional[int] = None,
|
|
50
|
+
# LABEL instruction
|
|
51
|
+
label_key: Optional[str] = None,
|
|
52
|
+
label_value_regex: Optional[str] = None,
|
|
53
|
+
# CMD/ENTRYPOINT
|
|
54
|
+
command_form: Optional[str] = None,
|
|
55
|
+
# WORKDIR
|
|
56
|
+
workdir_not_absolute: Optional[bool] = None,
|
|
57
|
+
# STOPSIGNAL
|
|
58
|
+
signal_not_in: Optional[List[str]] = None,
|
|
59
|
+
# Generic matchers
|
|
60
|
+
contains: Optional[str] = None,
|
|
61
|
+
not_contains: Optional[str] = None,
|
|
62
|
+
regex: Optional[str] = None,
|
|
63
|
+
not_regex: Optional[str] = None,
|
|
64
|
+
# Custom validation
|
|
65
|
+
validate: Optional[callable] = None,
|
|
66
|
+
) -> Matcher:
|
|
67
|
+
"""
|
|
68
|
+
Match a Dockerfile instruction by type and properties.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
instruction(type="FROM", image_tag="latest")
|
|
72
|
+
instruction(type="USER", user_name="root")
|
|
73
|
+
instruction(type="ARG", arg_name_regex=r"(?i).*password.*")
|
|
74
|
+
"""
|
|
75
|
+
params = {"instruction": type}
|
|
76
|
+
|
|
77
|
+
# Add non-None parameters
|
|
78
|
+
if base_image is not None:
|
|
79
|
+
params["base_image"] = base_image
|
|
80
|
+
if image_tag is not None:
|
|
81
|
+
params["image_tag"] = image_tag
|
|
82
|
+
if image_tag_regex is not None:
|
|
83
|
+
params["image_tag_regex"] = image_tag_regex
|
|
84
|
+
if missing_digest is not None:
|
|
85
|
+
params["missing_digest"] = missing_digest
|
|
86
|
+
if user_name is not None:
|
|
87
|
+
params["user_name"] = user_name
|
|
88
|
+
if user_name_regex is not None:
|
|
89
|
+
params["user_name_regex"] = user_name_regex
|
|
90
|
+
if port is not None:
|
|
91
|
+
params["port"] = port
|
|
92
|
+
if port_less_than is not None:
|
|
93
|
+
params["port_less_than"] = port_less_than
|
|
94
|
+
if port_greater_than is not None:
|
|
95
|
+
params["port_greater_than"] = port_greater_than
|
|
96
|
+
if protocol is not None:
|
|
97
|
+
params["protocol"] = protocol
|
|
98
|
+
if arg_name is not None:
|
|
99
|
+
params["arg_name"] = arg_name
|
|
100
|
+
if arg_name_regex is not None:
|
|
101
|
+
params["arg_name_regex"] = arg_name_regex
|
|
102
|
+
if copy_from is not None:
|
|
103
|
+
params["copy_from"] = copy_from
|
|
104
|
+
if chown is not None:
|
|
105
|
+
params["chown"] = chown
|
|
106
|
+
if missing_flag is not None:
|
|
107
|
+
params["missing_flag"] = missing_flag
|
|
108
|
+
if healthcheck_interval_less_than is not None:
|
|
109
|
+
params["healthcheck_interval_less_than"] = healthcheck_interval_less_than
|
|
110
|
+
if healthcheck_timeout_greater_than is not None:
|
|
111
|
+
params["healthcheck_timeout_greater_than"] = healthcheck_timeout_greater_than
|
|
112
|
+
if healthcheck_retries_greater_than is not None:
|
|
113
|
+
params["healthcheck_retries_greater_than"] = healthcheck_retries_greater_than
|
|
114
|
+
if label_key is not None:
|
|
115
|
+
params["label_key"] = label_key
|
|
116
|
+
if label_value_regex is not None:
|
|
117
|
+
params["label_value_regex"] = label_value_regex
|
|
118
|
+
if command_form is not None:
|
|
119
|
+
params["command_form"] = command_form
|
|
120
|
+
if workdir_not_absolute is not None:
|
|
121
|
+
params["workdir_not_absolute"] = workdir_not_absolute
|
|
122
|
+
if signal_not_in is not None:
|
|
123
|
+
params["signal_not_in"] = signal_not_in
|
|
124
|
+
if contains is not None:
|
|
125
|
+
params["contains"] = contains
|
|
126
|
+
if not_contains is not None:
|
|
127
|
+
params["not_contains"] = not_contains
|
|
128
|
+
if regex is not None:
|
|
129
|
+
params["regex"] = regex
|
|
130
|
+
if not_regex is not None:
|
|
131
|
+
params["not_regex"] = not_regex
|
|
132
|
+
if validate is not None:
|
|
133
|
+
# Custom validation stored separately
|
|
134
|
+
params["has_custom_validate"] = True
|
|
135
|
+
|
|
136
|
+
return Matcher(type="instruction", params=params)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def missing(
|
|
140
|
+
instruction: str,
|
|
141
|
+
label_key: Optional[str] = None,
|
|
142
|
+
) -> Matcher:
|
|
143
|
+
"""
|
|
144
|
+
Match when a Dockerfile instruction is missing.
|
|
145
|
+
|
|
146
|
+
Examples:
|
|
147
|
+
missing(instruction="USER")
|
|
148
|
+
missing(instruction="HEALTHCHECK")
|
|
149
|
+
missing(instruction="LABEL", label_key="maintainer")
|
|
150
|
+
"""
|
|
151
|
+
params = {"instruction": instruction}
|
|
152
|
+
if label_key is not None:
|
|
153
|
+
params["label_key"] = label_key
|
|
154
|
+
|
|
155
|
+
return Matcher(type="missing_instruction", params=params)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# --- docker-compose Matchers ---
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def service_has(
|
|
162
|
+
key: str,
|
|
163
|
+
equals: Optional[Any] = None,
|
|
164
|
+
not_equals: Optional[Any] = None,
|
|
165
|
+
contains: Optional[str] = None,
|
|
166
|
+
not_contains: Optional[str] = None,
|
|
167
|
+
contains_any: Optional[List[str]] = None,
|
|
168
|
+
regex: Optional[str] = None,
|
|
169
|
+
env_name_regex: Optional[str] = None,
|
|
170
|
+
env_value_regex: Optional[str] = None,
|
|
171
|
+
volume_type: Optional[str] = None,
|
|
172
|
+
source_regex: Optional[str] = None,
|
|
173
|
+
target_regex: Optional[str] = None,
|
|
174
|
+
published_port_less_than: Optional[int] = None,
|
|
175
|
+
) -> Matcher:
|
|
176
|
+
"""
|
|
177
|
+
Match docker-compose services with specific properties.
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
service_has(key="privileged", equals=True)
|
|
181
|
+
service_has(key="volumes", contains="/var/run/docker.sock")
|
|
182
|
+
service_has(key="network_mode", equals="host")
|
|
183
|
+
"""
|
|
184
|
+
params = {"key": key}
|
|
185
|
+
|
|
186
|
+
if equals is not None:
|
|
187
|
+
params["equals"] = equals
|
|
188
|
+
if not_equals is not None:
|
|
189
|
+
params["not_equals"] = not_equals
|
|
190
|
+
if contains is not None:
|
|
191
|
+
params["contains"] = contains
|
|
192
|
+
if not_contains is not None:
|
|
193
|
+
params["not_contains"] = not_contains
|
|
194
|
+
if contains_any is not None:
|
|
195
|
+
params["contains_any"] = contains_any
|
|
196
|
+
if regex is not None:
|
|
197
|
+
params["regex"] = regex
|
|
198
|
+
if env_name_regex is not None:
|
|
199
|
+
params["env_name_regex"] = env_name_regex
|
|
200
|
+
if env_value_regex is not None:
|
|
201
|
+
params["env_value_regex"] = env_value_regex
|
|
202
|
+
if volume_type is not None:
|
|
203
|
+
params["volume_type"] = volume_type
|
|
204
|
+
if source_regex is not None:
|
|
205
|
+
params["source_regex"] = source_regex
|
|
206
|
+
if target_regex is not None:
|
|
207
|
+
params["target_regex"] = target_regex
|
|
208
|
+
if published_port_less_than is not None:
|
|
209
|
+
params["published_port_less_than"] = published_port_less_than
|
|
210
|
+
|
|
211
|
+
return Matcher(type="service_has", params=params)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def service_missing(
|
|
215
|
+
key: str,
|
|
216
|
+
value_contains: Optional[str] = None,
|
|
217
|
+
) -> Matcher:
|
|
218
|
+
"""
|
|
219
|
+
Match docker-compose services missing a property.
|
|
220
|
+
|
|
221
|
+
Examples:
|
|
222
|
+
service_missing(key="read_only")
|
|
223
|
+
service_missing(key="security_opt")
|
|
224
|
+
service_missing(key="security_opt", value_contains="no-new-privileges")
|
|
225
|
+
"""
|
|
226
|
+
params = {"key": key}
|
|
227
|
+
if value_contains is not None:
|
|
228
|
+
params["value_contains"] = value_contains
|
|
229
|
+
|
|
230
|
+
return Matcher(type="service_missing", params=params)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Programmatic access to Dockerfile and docker-compose objects.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Callable, Dict, Any
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ProgrammaticMatcher:
|
|
11
|
+
"""Wraps a custom validation function."""
|
|
12
|
+
|
|
13
|
+
check_function: Callable
|
|
14
|
+
description: str = ""
|
|
15
|
+
|
|
16
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
17
|
+
return {
|
|
18
|
+
"type": "programmatic",
|
|
19
|
+
"has_callable": True,
|
|
20
|
+
"description": self.description,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def custom_check(check: Callable, description: str = "") -> ProgrammaticMatcher:
|
|
25
|
+
"""
|
|
26
|
+
Create a custom validation function.
|
|
27
|
+
|
|
28
|
+
The check function receives the parsed dockerfile or compose object
|
|
29
|
+
and should return True if the rule matches (vulnerability found).
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
@dockerfile_rule(id="DOCKER-CUSTOM-001")
|
|
33
|
+
def last_user_is_root():
|
|
34
|
+
def check(dockerfile):
|
|
35
|
+
final_user = dockerfile.get_final_user()
|
|
36
|
+
return final_user is None or final_user.user_name == "root"
|
|
37
|
+
return custom_check(check, "Check if last USER is root")
|
|
38
|
+
"""
|
|
39
|
+
return ProgrammaticMatcher(check_function=check, description=description)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DockerfileAccess:
|
|
43
|
+
"""
|
|
44
|
+
Provides programmatic access to Dockerfile structure.
|
|
45
|
+
Used in custom validation functions.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, dockerfile_graph):
|
|
49
|
+
self._graph = dockerfile_graph
|
|
50
|
+
|
|
51
|
+
def get_instructions(self, instruction_type: str):
|
|
52
|
+
"""Get all instructions of a type."""
|
|
53
|
+
return self._graph.GetInstructions(instruction_type)
|
|
54
|
+
|
|
55
|
+
def has_instruction(self, instruction_type: str) -> bool:
|
|
56
|
+
"""Check if instruction type exists."""
|
|
57
|
+
return self._graph.HasInstruction(instruction_type)
|
|
58
|
+
|
|
59
|
+
def get_final_user(self):
|
|
60
|
+
"""Get the last USER instruction."""
|
|
61
|
+
return self._graph.GetFinalUser()
|
|
62
|
+
|
|
63
|
+
def is_running_as_root(self) -> bool:
|
|
64
|
+
"""Check if container runs as root."""
|
|
65
|
+
return self._graph.IsRunningAsRoot()
|
|
66
|
+
|
|
67
|
+
def get_stages(self):
|
|
68
|
+
"""Get all build stages."""
|
|
69
|
+
return self._graph.GetStages()
|
|
70
|
+
|
|
71
|
+
def is_multi_stage(self) -> bool:
|
|
72
|
+
"""Check if Dockerfile uses multi-stage build."""
|
|
73
|
+
return self._graph.IsMultiStage()
|
|
74
|
+
|
|
75
|
+
def get_stage_by_alias(self, alias: str):
|
|
76
|
+
"""Get a stage by its AS alias."""
|
|
77
|
+
return self._graph.GetStageByAlias(alias)
|
|
78
|
+
|
|
79
|
+
def get_final_stage(self):
|
|
80
|
+
"""Get the final build stage."""
|
|
81
|
+
return self._graph.GetFinalStage()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ComposeAccess:
|
|
85
|
+
"""
|
|
86
|
+
Provides programmatic access to docker-compose structure.
|
|
87
|
+
Used in custom validation functions.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, compose_graph):
|
|
91
|
+
self._graph = compose_graph
|
|
92
|
+
|
|
93
|
+
def get_services(self):
|
|
94
|
+
"""Get all service names."""
|
|
95
|
+
return self._graph.GetServices()
|
|
96
|
+
|
|
97
|
+
def service_has(self, service_name: str, key: str, value) -> bool:
|
|
98
|
+
"""Check if service has property with value."""
|
|
99
|
+
return self._graph.ServiceHas(service_name, key, value)
|
|
100
|
+
|
|
101
|
+
def service_get(self, service_name: str, key: str):
|
|
102
|
+
"""Get service property value."""
|
|
103
|
+
return self._graph.ServiceGet(service_name, key)
|
|
104
|
+
|
|
105
|
+
def get_privileged_services(self):
|
|
106
|
+
"""Get services with privileged: true."""
|
|
107
|
+
return self._graph.GetPrivilegedServices()
|
|
108
|
+
|
|
109
|
+
def services_with_docker_socket(self):
|
|
110
|
+
"""Get services mounting Docker socket."""
|
|
111
|
+
return self._graph.ServicesWithDockerSocket()
|
|
112
|
+
|
|
113
|
+
def services_with_host_network(self):
|
|
114
|
+
"""Get services using host network mode."""
|
|
115
|
+
return self._graph.ServicesWithHostNetwork()
|
rules/python/__init__.py
ADDED
|
File without changes
|
|
File without changes
|