Skip to content

Commit 149b00d

Browse files
Merge pull request #37 from Contrast-Security-OSS/AIML-48_query_api_for_vuln_details_and_format_for_issue_body
Aiml 48 query api for vuln details and format for issue body
2 parents dc02da4 + a2fda4b commit 149b00d

File tree

10 files changed

+223
-39
lines changed

10 files changed

+223
-39
lines changed

src/agent_handler.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -810,5 +810,3 @@ def _patched_loop_check_closed(self):
810810
return #ignore this error
811811
raise
812812
asyncio.BaseEventLoop._check_closed = _patched_loop_check_closed
813-
814-
# %%

src/build_output_analyzer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,4 @@ def extract_build_errors(build_output):
8484
return "BUILD FAILURE - KEY ERRORS:\n\n" + "\n\n...\n\n".join(result_blocks)
8585

8686
# Fallback: just return the last part of the build output
87-
return "BUILD FAILURE - LAST OUTPUT:\n\n" + "\n".join(tail_lines[-50:])
87+
return "BUILD FAILURE - LAST OUTPUT:\n\n" + "\n".join(tail_lines[-50:])

src/closed_handler.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,5 +143,3 @@ def handle_closed_pr():
143143

144144
if __name__ == "__main__":
145145
handle_closed_pr()
146-
147-
# %%

src/contrast_api.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,3 +434,102 @@ def notify_remediation_failed(remediation_id: str, failure_category: str, contra
434434
except json.JSONDecodeError:
435435
log(f"Error decoding JSON response when notifying Remediation service about failed remediation {remediation_id}.", is_error=True)
436436
return False
437+
438+
def get_vulnerability_details(contrast_host: str, contrast_org_id: str, contrast_app_id: str, contrast_auth_key: str, contrast_api_key: str, github_repo_url: str, max_pull_requests: int = 5, severities: list = None) -> dict:
439+
"""Gets vulnerability remediation details from the Contrast API.
440+
441+
Args:
442+
contrast_host: The Contrast Security host URL
443+
contrast_org_id: The organization ID
444+
contrast_app_id: The application ID
445+
contrast_auth_key: The Contrast authorization key
446+
contrast_api_key: The Contrast API key
447+
github_repo_url: The GitHub repository URL
448+
max_pull_requests: Maximum number of pull requests (default: 5)
449+
severities: List of vulnerability severities to filter by (default: ["CRITICAL", "HIGH"])
450+
451+
Returns:
452+
dict: Contains vulnerability remediation details or None if no vulnerability found
453+
Structure: {
454+
'remediationId': '...',
455+
'vulnerabilityUuid': '...',
456+
'vulnerabilityTitle': '...',
457+
'vulnerabilityRuleName': '...',
458+
'vulnerabilityStatus': '...',
459+
'vulnerabilitySeverity': '...',
460+
'vulnerabilityOverviewStory': '...',
461+
'vulnerabilityEventsSummary': '...',
462+
'vulnerabilityHttpRequestDetails': '...'
463+
}
464+
"""
465+
if severities is None:
466+
severities = ["CRITICAL", "HIGH"]
467+
468+
debug_log("\n--- Fetching vulnerability details from remediation-details API ---")
469+
470+
api_url = f"https://{normalize_host(contrast_host)}/api/v4/aiml-remediation/organizations/{contrast_org_id}/applications/{contrast_app_id}/remediation-details"
471+
debug_log(f"API URL: {api_url}")
472+
473+
headers = {
474+
"Authorization": contrast_auth_key,
475+
"API-Key": contrast_api_key,
476+
"Content-Type": "application/json",
477+
"Accept": "application/json",
478+
"User-Agent": config.USER_AGENT
479+
}
480+
481+
payload = {
482+
"teamserverHost": f"https://{normalize_host(contrast_host)}",
483+
"repoRootDir": str(config.REPO_ROOT),
484+
"repoUrl": github_repo_url,
485+
"maxPullRequests": max_pull_requests,
486+
"severities": severities
487+
}
488+
489+
debug_log(f"Request payload: {json.dumps(payload, indent=2)}")
490+
491+
try:
492+
debug_log(f"Making POST request to: {api_url}")
493+
response = requests.post(api_url, headers=headers, json=payload, timeout=30)
494+
495+
debug_log(f"Remediation-details API Response Status Code: {response.status_code}")
496+
497+
# Handle different status codes
498+
if response.status_code == 204:
499+
log("No vulnerabilities found that need remediation (204 No Content).")
500+
return None
501+
elif response.status_code == 409:
502+
log("At or over the maximum PR limit (409 Conflict).")
503+
return None
504+
elif response.status_code == 200:
505+
response_json = response.json()
506+
debug_log(f"Successfully received vulnerability details from API")
507+
debug_log(f"Response keys: {list(response_json.keys())}")
508+
509+
# Validate that we have required components
510+
required_keys = ['remediationId', 'vulnerabilityUuid', 'vulnerabilityTitle']
511+
missing_keys = [key for key in required_keys if key not in response_json]
512+
513+
if missing_keys:
514+
log(f"Warning: Missing some keys in API response: {missing_keys}")
515+
516+
# Log a summary without exposing sensitive details
517+
debug_log(f"Vulnerability UUID: {response_json.get('vulnerabilityUuid', 'Unknown')}")
518+
debug_log(f"Vulnerability Title: {response_json.get('vulnerabilityTitle', 'Unknown')}")
519+
debug_log(f"Vulnerability Severity: {response_json.get('vulnerabilitySeverity', 'Unknown')}")
520+
debug_log(f"Remediation ID: {response_json.get('remediationId', 'Unknown')}")
521+
522+
return response_json
523+
else:
524+
log(f"Unexpected status code {response.status_code} from remediation-details API: {response.text}", is_error=True)
525+
return None
526+
527+
except requests.exceptions.RequestException as e:
528+
log(f"Error fetching vulnerability details: {e}", is_error=True)
529+
return None
530+
except json.JSONDecodeError:
531+
log("Error decoding JSON response from remediation-details API.", is_error=True)
532+
return None
533+
except Exception as e:
534+
log(f"Unexpected error calling remediation-details API: {e}", is_error=True)
535+
return None

src/external_coding_agent.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,75 @@ def __init__(self, config: Config):
4141
self.config = config
4242
debug_log(f"Initialized ExternalCodingAgent")
4343

44-
def generate_fixes(self, vuln_uuid: str, remediation_id: str, vuln_title: str) -> bool:
44+
def assemble_issue_body(self, vulnerability_details: dict) -> str:
45+
"""
46+
Assembles a GitHub Issue body from vulnerability details.
47+
48+
Args:
49+
vulnerability_details: Dictionary containing vulnerability information
50+
51+
Returns:
52+
str: Formatted GitHub Issue body for the vulnerability
53+
"""
54+
# Extract key details with safe fallbacks
55+
vuln_title = vulnerability_details.get('vulnerabilityTitle', 'Unknown Vulnerability')
56+
vuln_uuid = vulnerability_details.get('vulnerabilityUuid', 'Unknown UUID')
57+
vuln_rule = vulnerability_details.get('vulnerabilityRuleName', 'Unknown Rule')
58+
vuln_severity = vulnerability_details.get('vulnerabilitySeverity', 'Unknown Severity')
59+
vuln_status = vulnerability_details.get('vulnerabilityStatus', 'Unknown Status')
60+
vuln_overview = vulnerability_details.get('vulnerabilityOverviewStory', 'No overview available')
61+
vuln_events = vulnerability_details.get('vulnerabilityEventsSummary', 'No event details available')
62+
vuln_http_details = vulnerability_details.get('vulnerabilityHttpRequestDetails', 'No HTTP request details available')
63+
64+
# Assemble the issue body
65+
issue_body = f"""
66+
# Contrast AI SmartFix Issue Report
67+
68+
This issue should address a vulnerability identified by the Contrast Security platform (ID: [{vuln_uuid}](https://{self.config.CONTRAST_HOST}/Contrast/static/ng/index.html#/{self.config.CONTRAST_ORG_ID}/applications/{self.config.CONTRAST_APP_ID}/vulns/{vuln_uuid})).
69+
70+
# Security Vulnerability: {vuln_title}
71+
72+
## Vulnerability Details
73+
74+
**Rule:** {vuln_rule}
75+
**Severity:** {vuln_severity}
76+
**Status:** {vuln_status}
77+
78+
## Overview
79+
80+
{vuln_overview}
81+
82+
## Technical Details
83+
84+
### Event Summary
85+
```
86+
{vuln_events}
87+
```
88+
89+
### HTTP Request Details
90+
```
91+
{vuln_http_details}
92+
```
93+
94+
## Action Required
95+
96+
Please review this security vulnerability and implement appropriate fixes to address the identified issue.
97+
98+
**Important:** If you cannot find the vulnerability, then take no actions (corrective or otherwise). Simply report that the vulnerability was not found."""
99+
100+
debug_log(f"Assembled issue body with {len(issue_body)} characters")
101+
return issue_body
102+
103+
def generate_fixes(self, vuln_uuid: str, remediation_id: str, vuln_title: str, issue_body: str = None) -> bool:
45104
"""
46105
Generate fixes for vulnerabilities.
47106
107+
Args:
108+
vuln_uuid: The vulnerability UUID
109+
remediation_id: The remediation ID
110+
vuln_title: The vulnerability title
111+
issue_body: The issue body content (optional, uses default if not provided)
112+
48113
Returns:
49114
bool: False if the CODING_AGENT is SMARTFIX, True otherwise
50115
"""
@@ -55,11 +120,15 @@ def generate_fixes(self, vuln_uuid: str, remediation_id: str, vuln_title: str) -
55120
log(f"\n::group::--- Using External Coding Agent ({self.config.CODING_AGENT}) ---")
56121
telemetry_handler.update_telemetry("additionalAttributes.codingAgent", "EXTERNAL-COPILOT")
57122

58-
# Hard-coded vulnerability label for now, will be passed as argument later
123+
# Generate labels and issue details
59124
vulnerability_label = f"contrast-vuln-id:VULN-{vuln_uuid}"
60125
remediation_label = f"smartfix-id:{remediation_id}"
61126
issue_title = vuln_title
62-
issue_body = "This is a fake issue body for testing purposes."
127+
128+
# Use the provided issue_body or fall back to default
129+
if issue_body is None:
130+
log(f"Failed to generate issue body for vulnerability id {vuln_uuid}", is_error=True)
131+
error_exit(remediation_id, FailureCategory.AGENT_FAILURE.value)
63132

64133
# Use git_handler to find if there's an existing issue with this label
65134
issue_number = git_handler.find_issue_with_label(vulnerability_label)
@@ -162,5 +231,3 @@ def _poll_for_pr(self, issue_number: int, remediation_id: str, vulnerability_lab
162231
return None
163232

164233
# Additional methods will be implemented later
165-
166-
# %%

src/git_handler.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -723,5 +723,3 @@ def add_labels_to_pr(pr_number: int, labels: List[str]) -> bool:
723723
except Exception as e:
724724
log(f"Failed to add labels to PR #{pr_number}: {e}", is_error=True)
725725
return False
726-
727-
# %%

src/main.py

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -276,28 +276,52 @@ def main():
276276
log(f"\n--- Reached max PR limit ({max_open_prs_setting}). Current open PRs: {current_open_pr_count}. Stopping processing. ---")
277277
break
278278

279-
# --- Fetch Next Vulnerability and Prompts from New API ---
280-
log("\n::group::--- Fetching next vulnerability and prompts from Contrast API ---")
279+
# --- Fetch Next Vulnerability Data from API ---
280+
if config.CODING_AGENT == "SMARTFIX":
281+
# For SMARTFIX, get vulnerability with prompts
282+
log("\n::group::--- Fetching next vulnerability and prompts from Contrast API ---")
283+
vulnerability_data = contrast_api.get_vulnerability_with_prompts(
284+
config.CONTRAST_HOST, config.CONTRAST_ORG_ID, config.CONTRAST_APP_ID,
285+
config.CONTRAST_AUTHORIZATION_KEY, config.CONTRAST_API_KEY,
286+
max_open_prs_setting, github_repo_url, config.VULNERABILITY_SEVERITIES
287+
)
288+
log("\n::endgroup::")
281289

282-
vulnerability_data = contrast_api.get_vulnerability_with_prompts(
283-
config.CONTRAST_HOST, config.CONTRAST_ORG_ID, config.CONTRAST_APP_ID,
284-
config.CONTRAST_AUTHORIZATION_KEY, config.CONTRAST_API_KEY,
285-
max_open_prs_setting, github_repo_url, config.VULNERABILITY_SEVERITIES
286-
)
287-
log("\n::endgroup::")
290+
if not vulnerability_data:
291+
log("No more vulnerabilities found to process or API error occurred. Stopping processing.")
292+
break
288293

289-
if not vulnerability_data:
290-
log("No more vulnerabilities found to process or API error occurred. Stopping processing.")
291-
break
294+
# Extract vulnerability details and prompts from the response
295+
vuln_uuid = vulnerability_data['vulnerabilityUuid']
296+
vuln_title = vulnerability_data['vulnerabilityTitle']
297+
remediation_id = vulnerability_data['remediationId']
298+
fix_system_prompt = vulnerability_data['fixSystemPrompt']
299+
fix_user_prompt = vulnerability_data['fixUserPrompt']
300+
qa_system_prompt = vulnerability_data['qaSystemPrompt']
301+
qa_user_prompt = vulnerability_data['qaUserPrompt']
302+
else:
303+
# For external coding agents (like GITHUB_COPILOT), get vulnerability details
304+
log("\n::group::--- Fetching next vulnerability details from Contrast API ---")
305+
vulnerability_data = contrast_api.get_vulnerability_details(
306+
config.CONTRAST_HOST, config.CONTRAST_ORG_ID, config.CONTRAST_APP_ID,
307+
config.CONTRAST_AUTHORIZATION_KEY, config.CONTRAST_API_KEY,
308+
github_repo_url, max_open_prs_setting, config.VULNERABILITY_SEVERITIES
309+
)
310+
log("\n::endgroup::")
311+
312+
if not vulnerability_data:
313+
log("No more vulnerabilities found to process or API error occurred. Stopping processing.")
314+
break
292315

293-
# Extract vulnerability details and prompts from the response
294-
vuln_uuid = vulnerability_data['vulnerabilityUuid']
295-
vuln_title = vulnerability_data['vulnerabilityTitle']
296-
remediation_id = vulnerability_data['remediationId']
297-
fix_system_prompt = vulnerability_data['fixSystemPrompt']
298-
fix_user_prompt = vulnerability_data['fixUserPrompt']
299-
qa_system_prompt = vulnerability_data['qaSystemPrompt']
300-
qa_user_prompt = vulnerability_data['qaUserPrompt']
316+
# Extract vulnerability details from the response (no prompts for external agents)
317+
vuln_uuid = vulnerability_data['vulnerabilityUuid']
318+
vuln_title = vulnerability_data['vulnerabilityTitle']
319+
remediation_id = vulnerability_data['remediationId']
320+
# No prompts available for external coding agents
321+
fix_system_prompt = None
322+
fix_user_prompt = None
323+
qa_system_prompt = None
324+
qa_user_prompt = None
301325

302326
# Populate vulnInfo in telemetry
303327
telemetry_handler.update_telemetry("vulnInfo.vulnId", vuln_uuid)
@@ -345,7 +369,10 @@ def main():
345369
# --- Check if we need to use the external coding agent ---
346370
if config.CODING_AGENT != "SMARTFIX":
347371
external_agent = ExternalCodingAgent(config)
348-
if external_agent.generate_fixes(vuln_uuid, remediation_id, vuln_title):
372+
# Assemble the issue body from vulnerability details
373+
issue_body = external_agent.assemble_issue_body(vulnerability_data)
374+
# Pass the assembled issue body to generate_fixes()
375+
if external_agent.generate_fixes(vuln_uuid, remediation_id, vuln_title, issue_body):
349376
log(f"\n\n--- External Coding Agent successfully generated fixes ---")
350377
processed_one = True
351378
contrast_api.send_telemetry_data()

src/qa_handler.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,3 @@ def run_qa_loop(
256256
log(f"\n\u274c Build failed after {qa_attempts} QA attempts.")
257257

258258
return build_success, changed_files, build_command, qa_summary_log
259-
# %%

src/utils.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,5 +257,3 @@ def error_exit(remediation_id: str, failure_code: Optional[str] = None):
257257

258258
# Exit with error code
259259
sys.exit(1)
260-
261-
# %%

test/test_external_coding_agent.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def test_generate_fixes_with_smartfix(self, mock_debug_log):
8383
agent = ExternalCodingAgent(self.config)
8484

8585
# Call generate_fixes
86-
result = agent.generate_fixes("1234-FAKE-ABCD", "1REM-FAKE-ABCD", "Fake Vulnerability Title")
86+
result = agent.generate_fixes("1234-FAKE-ABCD", "1REM-FAKE-ABCD", "Fake Vulnerability Title", "Fake issue body.")
8787

8888
# Assert that result is False
8989
self.assertFalse(result)
@@ -125,7 +125,7 @@ def test_generate_fixes_with_external_agent_pr_created(self, mock_log, mock_debu
125125
agent = ExternalCodingAgent(self.config)
126126

127127
# Call generate_fixes
128-
result = agent.generate_fixes("1234-FAKE-ABCD", "1REM-FAKE-ABCD", "Fake Vulnerability Title")
128+
result = agent.generate_fixes("1234-FAKE-ABCD", "1REM-FAKE-ABCD", "Fake Vulnerability Title", "Fake issue body.")
129129

130130
# Assert that result is True
131131
self.assertTrue(result)
@@ -171,7 +171,7 @@ def test_generate_fixes_with_external_agent_pr_timeout(self, mock_log, mock_debu
171171

172172
try:
173173
# Call generate_fixes
174-
result = agent.generate_fixes("1234-FAKE-ABCD", "1REM-FAKE-ABCD", "Fake Vulnerability Title")
174+
result = agent.generate_fixes("1234-FAKE-ABCD", "1REM-FAKE-ABCD", "Fake Vulnerability Title", "Fake issue body.")
175175

176176
# Assert that result is False when PR is not created
177177
self.assertFalse(result)
@@ -229,7 +229,7 @@ def test_generate_fixes_with_existing_issue(self, mock_log, mock_debug_log, mock
229229

230230
try:
231231
# Call generate_fixes
232-
result = agent.generate_fixes("1234-FAKE-ABCD", "1REM-FAKE-ABCD", "Fake Vulnerability Title")
232+
result = agent.generate_fixes("1234-FAKE-ABCD", "1REM-FAKE-ABCD", "Fake Vulnerability Title", "Fake issue body.")
233233

234234
# Assert that result is True
235235
self.assertTrue(result)

0 commit comments

Comments
 (0)