Skip to content

Commit a8c7ae7

Browse files
Merge pull request #17 from Contrast-Security-OSS/TS-39583
TS-39583: Add API call to mark remediation as failed
2 parents 6ff489b + ce07c8a commit a8c7ae7

File tree

2 files changed

+117
-0
lines changed

2 files changed

+117
-0
lines changed

src/contrast_api.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,22 @@
2121
import json
2222
import sys
2323
from typing import Optional
24+
from enum import Enum
2425
import config
2526
from utils import debug_print
2627

28+
# Define failure categories as an enum to ensure consistency
29+
class FailureCategory(Enum):
30+
INITIAL_BUILD_FAILURE = "INITIAL_BUILD_FAILURE"
31+
EXCEEDED_QA_ATTEMPTS = "EXCEEDED_QA_ATTEMPTS"
32+
QA_AGENT_FAILURE = "QA_AGENT_FAILURE"
33+
GIT_COMMAND_FAILURE = "GIT_COMMAND_FAILURE"
34+
AGENT_FAILURE = "AGENT_FAILURE"
35+
GENERATE_PR_FAILURE = "GENERATE_PR_FAILURE"
36+
HANDLE_PR_MERGE_FAILURE = "HANDLE_PR_MERGE_FAILURE"
37+
HANDLE_PR_CLOSE_FAILURE = "HANDLE_PR_CLOSE_FAILURE"
38+
GENERAL_FAILURE = "GENERAL_FAILURE"
39+
2740
def normalize_host(host: str) -> str:
2841
"""Remove any protocol prefix from host to prevent double prefixing when constructing URLs."""
2942
return host.replace('https://', '').replace('http://', '')
@@ -303,3 +316,66 @@ def notify_remediation_pr_closed(remediation_id: str, contrast_host: str, contra
303316
except json.JSONDecodeError:
304317
print(f"Error decoding JSON response when notifying Remediation service about closed PR for remediation {remediation_id}.", file=sys.stderr)
305318
return False
319+
320+
def notify_remediation_failed(remediation_id: str, failure_category: str, contrast_host: str, contrast_org_id: str, contrast_app_id: str, contrast_auth_key: str, contrast_api_key: str) -> bool:
321+
"""Notifies the Remediation backend service that a remediation has failed.
322+
323+
Args:
324+
remediation_id: The ID of the remediation.
325+
failure_category: The category of failure (e.g., "INITIAL_BUILD_FAILURE").
326+
contrast_host: The Contrast Security host URL.
327+
contrast_org_id: The organization ID.
328+
contrast_app_id: The application ID.
329+
contrast_auth_key: The Contrast authorization key.
330+
contrast_api_key: The Contrast API key.
331+
332+
Returns:
333+
bool: True if the notification was successful, False otherwise.
334+
"""
335+
debug_print(f"--- Notifying Remediation service about failed remediation {remediation_id} with category {failure_category} ---")
336+
api_url = f"https://{normalize_host(contrast_host)}/api/v4/aiml-remediation/organizations/{contrast_org_id}/applications/{contrast_app_id}/remediations/{remediation_id}/failed"
337+
338+
headers = {
339+
"Authorization": contrast_auth_key,
340+
"API-Key": contrast_api_key,
341+
"Content-Type": "application/json",
342+
"Accept": "application/json",
343+
"User-Agent": config.USER_AGENT
344+
}
345+
346+
payload = {
347+
"failureCategory": failure_category
348+
}
349+
350+
try:
351+
debug_print(f"Making PUT request to: {api_url}")
352+
debug_print(f"Payload: {json.dumps(payload)}")
353+
response = requests.put(api_url, headers=headers, json=payload)
354+
response.raise_for_status() # Raises an HTTPError for bad responses (4xx or 5xx)
355+
356+
debug_print(f"Remediation failed notification API response status code: {response.status_code}")
357+
358+
if response.status_code == 204:
359+
debug_print(f"Successfully notified Remediation service API about failed remediation {remediation_id}")
360+
return True
361+
else:
362+
error_message = "Unknown error"
363+
try:
364+
response_json = response.json()
365+
if "messages" in response_json and response_json["messages"]:
366+
error_message = response_json["messages"][0]
367+
except:
368+
error_message = response.text
369+
370+
print(f"Failed to notify Remediation service about failed remediation {remediation_id}. Error: {error_message}", file=sys.stderr)
371+
return False
372+
373+
except requests.exceptions.HTTPError as e:
374+
print(f"HTTP error notifying Remediation service about failed remediation {remediation_id}: {e.response.status_code} - {e.response.text}", file=sys.stderr)
375+
return False
376+
except requests.exceptions.RequestException as e:
377+
print(f"Request error notifying Remediation service about failed remediation {remediation_id}: {e}", file=sys.stderr)
378+
return False
379+
except json.JSONDecodeError:
380+
print(f"Error decoding JSON response when notifying Remediation service about failed remediation {remediation_id}.", file=sys.stderr)
381+
return False

src/main.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,23 @@ def main():
160160
error_analysis = extract_build_errors(prefix_build_output)
161161
print("\n❌ Build is broken ❌ -- No fix attempted.")
162162
print(f"Build output:\n{error_analysis}")
163+
164+
# Notify the Remediation service about the failed build
165+
remediation_notified = contrast_api.notify_remediation_failed(
166+
remediation_id=remediation_id,
167+
failure_category=contrast_api.FailureCategory.INITIAL_BUILD_FAILURE.value,
168+
contrast_host=config.CONTRAST_HOST,
169+
contrast_org_id=config.CONTRAST_ORG_ID,
170+
contrast_app_id=config.CONTRAST_APP_ID,
171+
contrast_auth_key=config.CONTRAST_AUTHORIZATION_KEY,
172+
contrast_api_key=config.CONTRAST_API_KEY
173+
)
174+
175+
if remediation_notified:
176+
print(f"Successfully notified Remediation service about failed build for remediation {remediation_id}.", flush=True)
177+
else:
178+
print(f"Warning: Failed to notify Remediation service about failed build for remediation {remediation_id}.", flush=True)
179+
163180
git_handler.cleanup_branch(new_branch_name)
164181
sys.exit(1) # Exit if the build is broken, no point in proceeding
165182

@@ -221,10 +238,34 @@ def main():
221238
# Skip PR creation if QA was run and the build is failing
222239
# or if the QA agent encountered an error (detected by checking qa_summary_log entries)
223240
if (used_build_command and not build_success) or any(s.startswith("Error during QA agent execution:") for s in qa_summary_log):
241+
failure_category = ""
242+
224243
if any(s.startswith("Error during QA agent execution:") for s in qa_summary_log):
225244
print("\n--- Skipping PR creation as QA Agent encountered an error ---")
245+
failure_category = contrast_api.FailureCategory.QA_AGENT_FAILURE.value
226246
else:
227247
print("\n--- Skipping PR creation as QA Agent failed to fix build issues ---")
248+
# Check if we've exhausted all retry attempts
249+
if len(qa_summary_log) >= max_qa_attempts_setting:
250+
failure_category = contrast_api.FailureCategory.EXCEEDED_QA_ATTEMPTS.value
251+
252+
# Notify the Remediation service about the failed remediation if we have a failure category
253+
if failure_category:
254+
remediation_notified = contrast_api.notify_remediation_failed(
255+
remediation_id=remediation_id,
256+
failure_category=failure_category,
257+
contrast_host=config.CONTRAST_HOST,
258+
contrast_org_id=config.CONTRAST_ORG_ID,
259+
contrast_app_id=config.CONTRAST_APP_ID,
260+
contrast_auth_key=config.CONTRAST_AUTHORIZATION_KEY,
261+
contrast_api_key=config.CONTRAST_API_KEY
262+
)
263+
264+
if remediation_notified:
265+
print(f"Successfully notified Remediation service about {failure_category} for remediation {remediation_id}.", flush=True)
266+
else:
267+
print(f"Warning: Failed to notify Remediation service about {failure_category} for remediation {remediation_id}.", flush=True)
268+
228269
git_handler.cleanup_branch(new_branch_name)
229270
continue # Move to the next vulnerability
230271

0 commit comments

Comments
 (0)