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.
rules/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """Container security rules DSL for Dockerfile and docker-compose."""
2
+
3
+ from .container_decorators import dockerfile_rule, compose_rule
4
+ from .container_matchers import instruction, missing, service_has, service_missing
5
+ from .container_ir import compile_all_rules, compile_to_json
6
+ from .container_combinators import (
7
+ all_of,
8
+ any_of,
9
+ none_of,
10
+ instruction_after,
11
+ instruction_before,
12
+ stage,
13
+ final_stage_has,
14
+ )
15
+ from .container_programmatic import custom_check, DockerfileAccess, ComposeAccess
16
+
17
+ __all__ = [
18
+ "dockerfile_rule",
19
+ "compose_rule",
20
+ "instruction",
21
+ "missing",
22
+ "service_has",
23
+ "service_missing",
24
+ "compile_all_rules",
25
+ "compile_to_json",
26
+ "all_of",
27
+ "any_of",
28
+ "none_of",
29
+ "instruction_after",
30
+ "instruction_before",
31
+ "stage",
32
+ "final_stage_has",
33
+ "custom_check",
34
+ "DockerfileAccess",
35
+ "ComposeAccess",
36
+ ]
@@ -0,0 +1,209 @@
1
+ """
2
+ Logic combinators for container rules.
3
+ """
4
+
5
+ from typing import List, Dict, Any, Union, Callable
6
+ from dataclasses import dataclass, field
7
+ from .container_matchers import Matcher
8
+
9
+
10
+ @dataclass
11
+ class CombinatorMatcher:
12
+ """Represents a logic combinator (AND, OR, NOT)."""
13
+
14
+ combinator_type: str # "all_of", "any_of", "none_of"
15
+ conditions: List[Union[Matcher, "CombinatorMatcher", Dict, Callable]]
16
+
17
+ def to_dict(self) -> Dict[str, Any]:
18
+ """Convert to JSON IR."""
19
+ serialized_conditions = []
20
+ for cond in self.conditions:
21
+ if hasattr(cond, "to_dict"):
22
+ serialized_conditions.append(cond.to_dict())
23
+ elif isinstance(cond, dict):
24
+ serialized_conditions.append(cond)
25
+ elif callable(cond):
26
+ serialized_conditions.append(
27
+ {"type": "custom_function", "has_callable": True}
28
+ )
29
+ else:
30
+ serialized_conditions.append(cond)
31
+
32
+ return {"type": self.combinator_type, "conditions": serialized_conditions}
33
+
34
+
35
+ def all_of(*conditions: Union[Matcher, Dict, Callable]) -> CombinatorMatcher:
36
+ """
37
+ Combine matchers with AND logic.
38
+ All conditions must match for the rule to trigger.
39
+
40
+ Example:
41
+ all_of(
42
+ instruction(type="FROM", image_tag="latest"),
43
+ missing(instruction="USER"),
44
+ instruction(type="RUN", contains="sudo")
45
+ )
46
+ """
47
+ return CombinatorMatcher(combinator_type="all_of", conditions=list(conditions))
48
+
49
+
50
+ def any_of(*conditions: Union[Matcher, Dict, Callable]) -> CombinatorMatcher:
51
+ """
52
+ Combine matchers with OR logic.
53
+ Any condition can match for the rule to trigger.
54
+
55
+ Example:
56
+ any_of(
57
+ instruction(type="USER", user_name="root"),
58
+ missing(instruction="USER"),
59
+ instruction(type="FROM", base_image="scratch")
60
+ )
61
+ """
62
+ return CombinatorMatcher(combinator_type="any_of", conditions=list(conditions))
63
+
64
+
65
+ def none_of(*conditions: Union[Matcher, Dict, Callable]) -> CombinatorMatcher:
66
+ """
67
+ Combine matchers with NOT logic.
68
+ None of the conditions should match for the rule to pass.
69
+ (Inverse: if any matches, rule triggers as violation)
70
+
71
+ Example:
72
+ none_of(
73
+ instruction(type="HEALTHCHECK"),
74
+ instruction(type="USER", user_name_not="root")
75
+ )
76
+ """
77
+ return CombinatorMatcher(combinator_type="none_of", conditions=list(conditions))
78
+
79
+
80
+ @dataclass
81
+ class SequenceMatcher:
82
+ """Represents instruction sequence validation."""
83
+
84
+ sequence_type: str # "after" or "before"
85
+ instruction: Union[str, Matcher, Dict]
86
+ reference: Union[str, Matcher, Dict]
87
+ not_followed_by: bool = False
88
+
89
+ def to_dict(self) -> Dict[str, Any]:
90
+ """Convert to JSON IR."""
91
+
92
+ def serialize_ref(ref):
93
+ if isinstance(ref, str):
94
+ return {"instruction": ref}
95
+ elif hasattr(ref, "to_dict"):
96
+ return ref.to_dict()
97
+ elif isinstance(ref, dict):
98
+ return ref
99
+ return ref
100
+
101
+ return {
102
+ "type": f"instruction_{self.sequence_type}",
103
+ "instruction": serialize_ref(self.instruction),
104
+ "reference": serialize_ref(self.reference),
105
+ "not_followed_by": self.not_followed_by,
106
+ }
107
+
108
+
109
+ def instruction_after(
110
+ instruction: Union[str, Matcher],
111
+ after: Union[str, Matcher],
112
+ not_followed_by: bool = False,
113
+ ) -> SequenceMatcher:
114
+ """
115
+ Check that an instruction appears after another.
116
+
117
+ Example:
118
+ # Ensure CMD comes after USER
119
+ instruction_after(instruction="CMD", after="USER")
120
+
121
+ # Ensure apt-get install follows apt-get update
122
+ instruction_after(
123
+ instruction=instruction(type="RUN", contains="apt-get install"),
124
+ after=instruction(type="RUN", contains="apt-get update")
125
+ )
126
+ """
127
+ return SequenceMatcher(
128
+ sequence_type="after",
129
+ instruction=instruction,
130
+ reference=after,
131
+ not_followed_by=not_followed_by,
132
+ )
133
+
134
+
135
+ def instruction_before(
136
+ instruction: Union[str, Matcher],
137
+ before: Union[str, Matcher],
138
+ not_followed_by: bool = False,
139
+ ) -> SequenceMatcher:
140
+ """
141
+ Check that an instruction appears before another.
142
+
143
+ Example:
144
+ instruction_before(instruction="USER", before="CMD")
145
+ """
146
+ return SequenceMatcher(
147
+ sequence_type="before",
148
+ instruction=instruction,
149
+ reference=before,
150
+ not_followed_by=not_followed_by,
151
+ )
152
+
153
+
154
+ @dataclass
155
+ class StageMatcher:
156
+ """Matcher for multi-stage build stage queries."""
157
+
158
+ stage_type: str
159
+ params: Dict[str, Any] = field(default_factory=dict)
160
+
161
+ def to_dict(self) -> Dict[str, Any]:
162
+ return {"type": f"stage_{self.stage_type}", **self.params}
163
+
164
+
165
+ def stage(
166
+ alias: str = None,
167
+ base_image: str = None,
168
+ is_final: bool = None,
169
+ ) -> StageMatcher:
170
+ """
171
+ Query a specific build stage.
172
+
173
+ Example:
174
+ stage(alias="builder")
175
+ stage(is_final=True)
176
+ stage(base_image="alpine")
177
+ """
178
+ params = {}
179
+ if alias is not None:
180
+ params["alias"] = alias
181
+ if base_image is not None:
182
+ params["base_image"] = base_image
183
+ if is_final is not None:
184
+ params["is_final"] = is_final
185
+
186
+ return StageMatcher(stage_type="query", params=params)
187
+
188
+
189
+ def final_stage_has(
190
+ instruction: Union[str, Matcher] = None,
191
+ missing_instruction: str = None,
192
+ ) -> StageMatcher:
193
+ """
194
+ Check properties of the final build stage.
195
+
196
+ Example:
197
+ final_stage_has(missing_instruction="USER")
198
+ final_stage_has(instruction=instruction(type="USER", user_name="root"))
199
+ """
200
+ params = {}
201
+ if instruction is not None:
202
+ if isinstance(instruction, str):
203
+ params["instruction"] = instruction
204
+ elif hasattr(instruction, "to_dict"):
205
+ params["instruction"] = instruction.to_dict()
206
+ if missing_instruction is not None:
207
+ params["missing_instruction"] = missing_instruction
208
+
209
+ return StageMatcher(stage_type="final_has", params=params)
@@ -0,0 +1,223 @@
1
+ """
2
+ Decorators for Dockerfile and docker-compose rules.
3
+ """
4
+
5
+ import atexit
6
+ import json
7
+ import sys
8
+ from typing import Callable, Dict, Any, List
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass
13
+ class RuleMetadata:
14
+ """Metadata for a container security rule."""
15
+
16
+ id: str
17
+ name: str = ""
18
+ severity: str = "MEDIUM"
19
+ category: str = "security"
20
+ cwe: str = ""
21
+ cve: str = ""
22
+ tags: str = ""
23
+ message: str = ""
24
+ file_pattern: str = ""
25
+
26
+
27
+ @dataclass
28
+ class DockerfileRuleDefinition:
29
+ """Complete definition of a Dockerfile rule."""
30
+
31
+ metadata: RuleMetadata
32
+ matcher: Dict[str, Any]
33
+ rule_function: Callable
34
+
35
+
36
+ @dataclass
37
+ class ComposeRuleDefinition:
38
+ """Complete definition of a docker-compose rule."""
39
+
40
+ metadata: RuleMetadata
41
+ matcher: Dict[str, Any]
42
+ rule_function: Callable
43
+
44
+
45
+ # Global registries
46
+ _dockerfile_rules: List[DockerfileRuleDefinition] = []
47
+ _compose_rules: List[ComposeRuleDefinition] = []
48
+ _auto_execute_enabled = False
49
+
50
+
51
+ def _enable_auto_execute() -> None:
52
+ """
53
+ Enable automatic rule compilation and output when script ends.
54
+
55
+ This provides consistent behavior with code analysis rules -
56
+ no __main__ block needed.
57
+ """
58
+ global _auto_execute_enabled
59
+ if _auto_execute_enabled:
60
+ return
61
+
62
+ _auto_execute_enabled = True
63
+
64
+ def _output_rules():
65
+ """Output all container rules as JSON when script ends."""
66
+ if not _dockerfile_rules and not _compose_rules:
67
+ return
68
+
69
+ # Compile rules to JSON IR format
70
+ from . import container_ir
71
+
72
+ compiled = container_ir.compile_all_rules()
73
+
74
+ # Output to stdout for Go loader to capture
75
+ print(json.dumps(compiled))
76
+
77
+ # Register cleanup handler
78
+ atexit.register(_output_rules)
79
+
80
+
81
+ def _register_rule() -> None:
82
+ """
83
+ Check if auto-execution should be enabled when a rule is registered.
84
+
85
+ Enables auto-execution if the module is being executed directly (not imported).
86
+ """
87
+ # Check if module is being executed directly
88
+ frame = sys._getframe(2) # Get caller's frame (the module defining the rule)
89
+ if frame.f_globals.get("__name__") == "__main__":
90
+ _enable_auto_execute()
91
+
92
+
93
+ def dockerfile_rule(
94
+ id: str,
95
+ name: str = "",
96
+ severity: str = "MEDIUM",
97
+ category: str = "security",
98
+ cwe: str = "",
99
+ cve: str = "",
100
+ tags: str = "",
101
+ message: str = "",
102
+ ) -> Callable:
103
+ """
104
+ Decorator for Dockerfile security rules.
105
+
106
+ Example:
107
+ @dockerfile_rule(id="DOCKER-001", severity="HIGH", cwe="CWE-250",
108
+ tags="security,docker,privilege-escalation")
109
+ def container_runs_as_root():
110
+ return missing(instruction="USER")
111
+ """
112
+
113
+ def decorator(func: Callable) -> Callable:
114
+ # Get matcher from function
115
+ matcher_result = func()
116
+
117
+ # Convert to dict if it's a Matcher object
118
+ if hasattr(matcher_result, "to_dict"):
119
+ matcher_dict = matcher_result.to_dict()
120
+ elif isinstance(matcher_result, dict):
121
+ matcher_dict = matcher_result
122
+ else:
123
+ raise ValueError(f"Rule {id} must return a matcher or dict")
124
+
125
+ # Create rule definition
126
+ metadata = RuleMetadata(
127
+ id=id,
128
+ name=name or func.__name__.replace("_", " ").title(),
129
+ severity=severity,
130
+ category=category,
131
+ cwe=cwe,
132
+ cve=cve,
133
+ tags=tags,
134
+ message=message or f"Security issue detected by {id}",
135
+ file_pattern="Dockerfile*",
136
+ )
137
+
138
+ rule_def = DockerfileRuleDefinition(
139
+ metadata=metadata,
140
+ matcher=matcher_dict,
141
+ rule_function=func,
142
+ )
143
+
144
+ _dockerfile_rules.append(rule_def)
145
+ _register_rule() # Enable auto-execution if running as script
146
+
147
+ # Return original function (can be called for testing)
148
+ return func
149
+
150
+ return decorator
151
+
152
+
153
+ def compose_rule(
154
+ id: str,
155
+ name: str = "",
156
+ severity: str = "MEDIUM",
157
+ category: str = "security",
158
+ cwe: str = "",
159
+ cve: str = "",
160
+ tags: str = "",
161
+ message: str = "",
162
+ ) -> Callable:
163
+ """
164
+ Decorator for docker-compose security rules.
165
+
166
+ Example:
167
+ @compose_rule(id="COMPOSE-001", severity="HIGH", cwe="CWE-250",
168
+ tags="security,docker-compose,privilege-escalation")
169
+ def privileged_service():
170
+ return service_has(key="privileged", equals=True)
171
+ """
172
+
173
+ def decorator(func: Callable) -> Callable:
174
+ matcher_result = func()
175
+
176
+ if hasattr(matcher_result, "to_dict"):
177
+ matcher_dict = matcher_result.to_dict()
178
+ elif isinstance(matcher_result, dict):
179
+ matcher_dict = matcher_result
180
+ else:
181
+ raise ValueError(f"Rule {id} must return a matcher or dict")
182
+
183
+ metadata = RuleMetadata(
184
+ id=id,
185
+ name=name or func.__name__.replace("_", " ").title(),
186
+ severity=severity,
187
+ category=category,
188
+ cwe=cwe,
189
+ cve=cve,
190
+ tags=tags,
191
+ message=message or f"Security issue detected by {id}",
192
+ file_pattern="**/docker-compose*.yml",
193
+ )
194
+
195
+ rule_def = ComposeRuleDefinition(
196
+ metadata=metadata,
197
+ matcher=matcher_dict,
198
+ rule_function=func,
199
+ )
200
+
201
+ _compose_rules.append(rule_def)
202
+ _register_rule() # Enable auto-execution if running as script
203
+
204
+ return func
205
+
206
+ return decorator
207
+
208
+
209
+ def get_dockerfile_rules() -> List[DockerfileRuleDefinition]:
210
+ """Get all registered Dockerfile rules."""
211
+ return _dockerfile_rules.copy()
212
+
213
+
214
+ def get_compose_rules() -> List[ComposeRuleDefinition]:
215
+ """Get all registered docker-compose rules."""
216
+ return _compose_rules.copy()
217
+
218
+
219
+ def clear_rules():
220
+ """Clear all registered rules (for testing)."""
221
+ global _dockerfile_rules, _compose_rules
222
+ _dockerfile_rules = []
223
+ _compose_rules = []
rules/container_ir.py ADDED
@@ -0,0 +1,104 @@
1
+ """
2
+ JSON IR (Intermediate Representation) compiler for container rules.
3
+ """
4
+
5
+ import json
6
+ from typing import List, Dict, Any
7
+
8
+ from .container_decorators import (
9
+ get_dockerfile_rules,
10
+ get_compose_rules,
11
+ )
12
+
13
+
14
+ def compile_dockerfile_rules() -> List[Dict[str, Any]]:
15
+ """
16
+ Compile all Dockerfile rules to JSON IR.
17
+
18
+ Returns list of rule definitions ready for Go executor.
19
+ """
20
+ rules = get_dockerfile_rules()
21
+ compiled = []
22
+
23
+ for rule in rules:
24
+ ir = {
25
+ "id": rule.metadata.id,
26
+ "name": rule.metadata.name,
27
+ "severity": rule.metadata.severity,
28
+ "category": rule.metadata.category,
29
+ "cwe": rule.metadata.cwe,
30
+ "message": rule.metadata.message,
31
+ "file_pattern": rule.metadata.file_pattern,
32
+ "rule_type": "dockerfile",
33
+ "matcher": rule.matcher,
34
+ }
35
+ compiled.append(ir)
36
+
37
+ return compiled
38
+
39
+
40
+ def compile_compose_rules() -> List[Dict[str, Any]]:
41
+ """
42
+ Compile all docker-compose rules to JSON IR.
43
+
44
+ Returns list of rule definitions ready for Go executor.
45
+ """
46
+ rules = get_compose_rules()
47
+ compiled = []
48
+
49
+ for rule in rules:
50
+ ir = {
51
+ "id": rule.metadata.id,
52
+ "name": rule.metadata.name,
53
+ "severity": rule.metadata.severity,
54
+ "category": rule.metadata.category,
55
+ "cwe": rule.metadata.cwe,
56
+ "message": rule.metadata.message,
57
+ "file_pattern": rule.metadata.file_pattern,
58
+ "rule_type": "compose",
59
+ "matcher": rule.matcher,
60
+ }
61
+ compiled.append(ir)
62
+
63
+ return compiled
64
+
65
+
66
+ def compile_all_rules() -> Dict[str, List[Dict[str, Any]]]:
67
+ """
68
+ Compile all container rules to JSON IR.
69
+
70
+ Returns dict with 'dockerfile' and 'compose' rule lists.
71
+ """
72
+ return {
73
+ "dockerfile": compile_dockerfile_rules(),
74
+ "compose": compile_compose_rules(),
75
+ }
76
+
77
+
78
+ def compile_to_json(pretty: bool = True) -> str:
79
+ """
80
+ Compile all rules to JSON string.
81
+
82
+ Args:
83
+ pretty: If True, format with indentation.
84
+
85
+ Returns:
86
+ JSON string of all compiled rules.
87
+ """
88
+ compiled = compile_all_rules()
89
+ if pretty:
90
+ return json.dumps(compiled, indent=2)
91
+ return json.dumps(compiled)
92
+
93
+
94
+ def write_ir_file(filepath: str, pretty: bool = True):
95
+ """
96
+ Write compiled rules to JSON file.
97
+
98
+ Args:
99
+ filepath: Output file path.
100
+ pretty: If True, format with indentation.
101
+ """
102
+ json_str = compile_to_json(pretty=pretty)
103
+ with open(filepath, "w") as f:
104
+ f.write(json_str)