Skip to content

Commit 2d0a93b

Browse files
committed
fix: address CodeRabbit review - validate agent responses and fail closed
Fix 6 critical/major issues found by CodeRabbit: 1. check_run.py - Validate agent response success: - Check result.success before reading violations - Return failed ProcessingResult on agent failure - Handle dict-backed rules in _convert_rules_to_new_format 2. deployment_protection_rule.py - Fail closed on agent failures: - Strengthen URL validation: require https://api.github.com - Validate agent response before auto-approving deployments - Reject deployment (fail closed) when agent fails instead of auto-approving 3. deployment_review.py - Handle edge cases: - Validate agent response success before acting on data - Handle dict-backed rules in _convert_rules_to_new_format - Return failed ProcessingResult on agent failure These fixes ensure: - Agent failures don't silently pass (previously: empty result = pass) - Invalid deployments are rejected, not auto-approved - Dict-backed rules from loaders don't crash with AttributeError - URL validation requires proper HTTPS GitHub API endpoints
1 parent 8fb03be commit 2d0a93b

3 files changed

Lines changed: 120 additions & 29 deletions

File tree

src/event_processors/check_run.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,25 @@ async def process(self, task: Task) -> ProcessingResult:
8888
rules=formatted_rules,
8989
)
9090

91+
# Validate agent response before acting on data
92+
if not result.success or not result.data or "violations" not in result.data:
93+
logger.error(
94+
"check_run_agent_failure",
95+
extra={
96+
"operation": "check_run",
97+
"agent_success": result.success,
98+
"has_data": bool(result.data),
99+
"has_violations": "violations" in result.data if result.data else False,
100+
},
101+
)
102+
return ProcessingResult(
103+
success=False,
104+
violations=[],
105+
api_calls_made=1,
106+
processing_time_ms=int((time.time() - start_time) * 1000),
107+
error="Agent execution failed or returned invalid response",
108+
)
109+
91110
violations = result.data.get("violations", [])
92111

93112
logger.info("=" * 80)
@@ -103,23 +122,33 @@ async def process(self, task: Task) -> ProcessingResult:
103122
)
104123

105124
def _convert_rules_to_new_format(self, rules: list[Any]) -> list[dict[str, Any]]:
106-
"""Convert Rule objects to the new flat schema format."""
125+
"""Convert Rule objects or dicts to the new flat schema format."""
107126
formatted_rules = []
108127

109128
for rule in rules:
110-
# Convert Rule object to dict format
111-
rule_dict = {
112-
"description": rule.description,
113-
"enabled": rule.enabled,
114-
"severity": rule.severity.value if hasattr(rule.severity, "value") else rule.severity,
115-
"event_types": [et.value if hasattr(et, "value") else et for et in rule.event_types],
116-
"parameters": rule.parameters if hasattr(rule, "parameters") else {},
117-
}
118-
119-
# If no parameters field, try to extract from conditions (backward compatibility)
120-
if not rule_dict["parameters"] and hasattr(rule, "conditions"):
121-
for condition in rule.conditions:
122-
rule_dict["parameters"].update(condition.parameters)
129+
# Handle both Rule objects and dict-backed rules
130+
if isinstance(rule, dict):
131+
rule_dict = {
132+
"description": rule.get("description", ""),
133+
"enabled": rule.get("enabled", True),
134+
"severity": rule.get("severity", "medium"),
135+
"event_types": rule.get("event_types", []),
136+
"parameters": rule.get("parameters", {}),
137+
}
138+
else:
139+
# Convert Rule object to dict format
140+
rule_dict = {
141+
"description": rule.description,
142+
"enabled": rule.enabled,
143+
"severity": rule.severity.value if hasattr(rule.severity, "value") else rule.severity,
144+
"event_types": [et.value if hasattr(et, "value") else et for et in rule.event_types],
145+
"parameters": rule.parameters if hasattr(rule, "parameters") else {},
146+
}
147+
148+
# If no parameters field, try to extract from conditions (backward compatibility)
149+
if not rule_dict["parameters"] and hasattr(rule, "conditions"):
150+
for condition in rule.conditions:
151+
rule_dict["parameters"].update(condition.parameters)
123152

124153
formatted_rules.append(rule_dict)
125154

src/event_processors/deployment_protection_rule.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,16 @@ def get_event_type(self) -> str:
3131

3232
@staticmethod
3333
def _is_valid_callback_url(url: str | None) -> bool:
34-
return bool(url and isinstance(url, str) and url.strip().startswith("http"))
34+
"""Validate callback URL is a proper GitHub API HTTPS endpoint."""
35+
if not url or not isinstance(url, str):
36+
return False
37+
url = url.strip()
38+
# Must be HTTPS and target GitHub API
39+
return url.startswith("https://api.github.com/")
3540

3641
@staticmethod
3742
def _is_valid_environment(env: str | None) -> bool:
43+
"""Validate environment name is present and non-empty."""
3844
return bool(env and isinstance(env, str) and env.strip())
3945

4046
async def process(self, task: Task) -> ProcessingResult:
@@ -198,6 +204,33 @@ async def process(self, task: Task) -> ProcessingResult:
198204
timeout_message=f"Agent execution timed out after {AGENT_TIMEOUT_SECONDS}s",
199205
)
200206

207+
# Validate agent response before acting on data
208+
if not analysis_result.success or not analysis_result.data or "evaluation_result" not in analysis_result.data:
209+
logger.error(
210+
"deployment_agent_failure",
211+
extra={
212+
"operation": "deployment_protection_rule",
213+
"agent_success": analysis_result.success,
214+
"has_data": bool(analysis_result.data),
215+
"has_evaluation_result": "evaluation_result" in analysis_result.data if analysis_result.data else False,
216+
},
217+
)
218+
# Fail closed: don't approve deployments when agent fails
219+
if can_call_callback:
220+
await self._reject_deployment(
221+
deployment_callback_url,
222+
environment,
223+
[{"message": "Agent execution failed - deployment blocked for safety", "severity": "high"}],
224+
installation_id,
225+
)
226+
return ProcessingResult(
227+
success=False,
228+
violations=[],
229+
api_calls_made=1,
230+
processing_time_ms=int((time.time() - start_time) * 1000),
231+
error="Agent execution failed or returned invalid response",
232+
)
233+
201234
violations = []
202235
if analysis_result.data and "evaluation_result" in analysis_result.data:
203236
eval_result = analysis_result.data["evaluation_result"]

src/event_processors/deployment_review.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,25 @@ async def process(self, task: Task) -> ProcessingResult:
118118
rules=formatted_rules,
119119
)
120120

121+
# Validate agent response before acting on data
122+
if not result.success or not result.data or "violations" not in result.data:
123+
logger.error(
124+
"deployment_review_agent_failure",
125+
extra={
126+
"operation": "deployment_review",
127+
"agent_success": result.success,
128+
"has_data": bool(result.data),
129+
"has_violations": "violations" in result.data if result.data else False,
130+
},
131+
)
132+
return ProcessingResult(
133+
success=False,
134+
violations=[],
135+
api_calls_made=1,
136+
processing_time_ms=int((time.time() - start_time) * 1000),
137+
error="Agent execution failed or returned invalid response",
138+
)
139+
121140
violations = result.data.get("violations", [])
122141

123142
logger.info("=" * 80)
@@ -142,23 +161,33 @@ async def prepare_api_data(self, task: Task) -> dict[str, Any]:
142161

143162
@staticmethod
144163
def _convert_rules_to_new_format(rules: list[Any]) -> list[dict[str, Any]]:
145-
"""Convert Rule objects to the new flat schema format."""
164+
"""Convert Rule objects or dicts to the new flat schema format."""
146165
formatted_rules = []
147166

148167
for rule in rules:
149-
# Convert Rule object to dict format
150-
rule_dict = {
151-
"description": rule.description,
152-
"enabled": rule.enabled,
153-
"severity": rule.severity.value if hasattr(rule.severity, "value") else rule.severity,
154-
"event_types": [et.value if hasattr(et, "value") else et for et in rule.event_types],
155-
"parameters": rule.parameters if hasattr(rule, "parameters") else {},
156-
}
157-
158-
# If no parameters field, try to extract from conditions (backward compatibility)
159-
if not rule_dict["parameters"] and hasattr(rule, "conditions"):
160-
for condition in rule.conditions:
161-
rule_dict["parameters"].update(condition.parameters)
168+
# Handle both Rule objects and dict-backed rules
169+
if isinstance(rule, dict):
170+
rule_dict = {
171+
"description": rule.get("description", ""),
172+
"enabled": rule.get("enabled", True),
173+
"severity": rule.get("severity", "medium"),
174+
"event_types": rule.get("event_types", []),
175+
"parameters": rule.get("parameters", {}),
176+
}
177+
else:
178+
# Convert Rule object to dict format
179+
rule_dict = {
180+
"description": rule.description,
181+
"enabled": rule.enabled,
182+
"severity": rule.severity.value if hasattr(rule.severity, "value") else rule.severity,
183+
"event_types": [et.value if hasattr(et, "value") else et for et in rule.event_types],
184+
"parameters": rule.parameters if hasattr(rule, "parameters") else {},
185+
}
186+
187+
# If no parameters field, try to extract from conditions (backward compatibility)
188+
if not rule_dict["parameters"] and hasattr(rule, "conditions"):
189+
for condition in rule.conditions:
190+
rule_dict["parameters"].update(condition.parameters)
162191

163192
formatted_rules.append(rule_dict)
164193

0 commit comments

Comments
 (0)