Skip to content

Commit a05ea16

Browse files
Merge pull request #53 from Contrast-Security-OSS/AIML-116_refactor_library_directory_structure
AIML-116 refactor library directory structure
2 parents a3372fb + 244ec40 commit a05ea16

23 files changed

+710
-50
lines changed

src/agent_handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@
5252
try:
5353
from google.adk.agents import Agent
5454
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
55-
from src.extensions.smartfix_litellm import SmartFixLiteLlm
56-
from src.extensions.smartfix_llm_agent import SmartFixLlmAgent
55+
from src.smartfix.extensions.smartfix_litellm import SmartFixLiteLlm
56+
from src.smartfix.extensions.smartfix_llm_agent import SmartFixLlmAgent
5757
from google.adk.runners import Runner
5858
from google.adk.sessions import InMemorySessionService
5959
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters, StdioConnectionParams

src/github/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""GitHub Provider Implementation
2+
3+
This package contains GitHub-specific implementations for the SmartFix system,
4+
including SCM provider implementations, API clients, and GitHub Action integrations.
5+
6+
Key Components:
7+
- GitHubScmProvider: GitHub implementation of the ScmProvider interface
8+
- GitHubApiClient: GitHub API integration and operations
9+
- ExternalCodingAgent: GitHub Copilot integration (moved from src/)
10+
"""
11+
12+
# Import classes for easy access
13+
try:
14+
from .external_coding_agent import ExternalCodingAgent # noqa: F401
15+
16+
__all__ = [
17+
"ExternalCodingAgent",
18+
]
19+
except ImportError:
20+
# During development, dependencies may not be available
21+
__all__ = []
22+
23+
# TODO: Add other GitHub components as they are implemented:
24+
# - GitHubScmProvider
25+
# - GitHubApiClient
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
# -
2+
# #%L
3+
# Contrast AI SmartFix
4+
# %%
5+
# Copyright (C) 2025 Contrast Security, Inc.
6+
# %%
7+
8+
# License: Commercial
9+
# NOTICE: This Software and the patented inventions embodied within may only be
10+
# used as part of Contrast Security's commercial offerings. Even though it is
11+
# made available through public repositories, use of this Software is subject to
12+
# the applicable End User Licensing Agreement found at
13+
# https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
14+
# between Contrast Security and the End User. The Software may not be reverse
15+
# engineered, modified, repackaged, sold, redistributed or otherwise used in a
16+
# way not consistent with the End User License Agreement.
17+
# #L%
18+
#
19+
20+
import time
21+
from typing import Optional
22+
from src.utils import log, debug_log, error_exit, tail_string
23+
from src.contrast_api import FailureCategory, notify_remediation_pr_opened
24+
from src.config import Config
25+
from src import git_handler
26+
from src import telemetry_handler
27+
28+
29+
class ExternalCodingAgent:
30+
"""
31+
A class that interfaces with an external coding agent through an API or command line.
32+
This agent is used as an alternative to the built-in SmartFix coding agent.
33+
"""
34+
35+
def __init__(self, config: Config):
36+
"""
37+
Initialize the ExternalCodingAgent with configuration settings.
38+
39+
Args:
40+
config: The application configuration object
41+
"""
42+
self.config = config
43+
debug_log("Initialized ExternalCodingAgent")
44+
45+
def assemble_issue_body(self, vulnerability_details: dict) -> str:
46+
"""
47+
Assembles a GitHub Issue body from vulnerability details.
48+
49+
Args:
50+
vulnerability_details: Dictionary containing vulnerability information
51+
52+
Returns:
53+
str: Formatted GitHub Issue body for the vulnerability
54+
"""
55+
# Extract key details with safe fallbacks
56+
vuln_title = vulnerability_details.get('vulnerabilityTitle', 'Unknown Vulnerability')
57+
vuln_uuid = vulnerability_details.get('vulnerabilityUuid', 'Unknown UUID')
58+
vuln_rule = vulnerability_details.get('vulnerabilityRuleName', 'Unknown Rule')
59+
vuln_severity = vulnerability_details.get('vulnerabilitySeverity', 'Unknown Severity')
60+
vuln_status = vulnerability_details.get('vulnerabilityStatus', 'Unknown Status')
61+
62+
# Get raw values first to check if they're empty
63+
raw_overview = vulnerability_details.get('vulnerabilityOverviewStory', '')
64+
raw_events = vulnerability_details.get('vulnerabilityEventsSummary', '')
65+
raw_http_details = vulnerability_details.get('vulnerabilityHttpRequestDetails', '')
66+
67+
# Tail large fields to reasonable limits to prevent GitHub's 64k character limit (only if they have content)
68+
vuln_overview = tail_string(raw_overview, 8000) if raw_overview.strip() else None
69+
vuln_events = tail_string(raw_events, 20000) if raw_events.strip() else None
70+
vuln_http_details = tail_string(raw_http_details, 4000) if raw_http_details.strip() else None
71+
72+
# Start building the issue body
73+
contrast_url = (f"https://{self.config.CONTRAST_HOST}/Contrast/static/ng/index.html#/"
74+
f"{self.config.CONTRAST_ORG_ID}/applications/{self.config.CONTRAST_APP_ID}/vulns/{vuln_uuid}")
75+
76+
issue_body = f"""
77+
# Contrast AI SmartFix Issue Report
78+
79+
This issue should address a vulnerability identified by the Contrast Security platform (ID: [{vuln_uuid}]({contrast_url})).
80+
81+
# Security Vulnerability: {vuln_title}
82+
83+
## Vulnerability Details
84+
85+
**Rule:** {vuln_rule}
86+
**Severity:** {vuln_severity}
87+
**Status:** {vuln_status} """
88+
89+
# Add overview section only if content is available
90+
if vuln_overview:
91+
issue_body += f"""
92+
93+
## Overview
94+
95+
{vuln_overview}"""
96+
97+
# Add technical details section only if we have event summary or HTTP details
98+
if vuln_events or vuln_http_details:
99+
issue_body += """
100+
101+
## Technical Details"""
102+
103+
# Add event summary subsection only if content is available
104+
if vuln_events:
105+
issue_body += f"""
106+
107+
### Event Summary
108+
```
109+
{vuln_events}
110+
```"""
111+
112+
# Add HTTP request details subsection only if content is available
113+
if vuln_http_details:
114+
issue_body += f"""
115+
116+
### HTTP Request Details
117+
```
118+
{vuln_http_details}
119+
```"""
120+
121+
# Add the action required section
122+
issue_body += """
123+
124+
## Action Required
125+
126+
Please review this security vulnerability and implement appropriate fixes to address the identified issue.
127+
128+
**Important:** If you cannot find the vulnerability, then take no actions (corrective or otherwise). Simply report that the vulnerability was not found."""
129+
130+
debug_log(f"Assembled issue body with {len(issue_body)} characters")
131+
return issue_body
132+
133+
def generate_fixes(self, vuln_uuid: str, remediation_id: str, vuln_title: str, issue_body: str = None) -> bool:
134+
"""
135+
Generate fixes for vulnerabilities.
136+
137+
Args:
138+
vuln_uuid: The vulnerability UUID
139+
remediation_id: The remediation ID
140+
vuln_title: The vulnerability title
141+
issue_body: The issue body content (optional, uses default if not provided)
142+
143+
Returns:
144+
bool: False if the CODING_AGENT is SMARTFIX, True otherwise
145+
"""
146+
if hasattr(self.config, 'CODING_AGENT') and self.config.CODING_AGENT == "SMARTFIX":
147+
debug_log("SMARTFIX agent detected, ExternalCodingAgent.generate_fixes returning False")
148+
return False
149+
150+
log(f"\n::group::--- Using External Coding Agent ({self.config.CODING_AGENT}) ---")
151+
telemetry_handler.update_telemetry("additionalAttributes.codingAgent", "EXTERNAL-COPILOT")
152+
153+
# Generate labels and issue details
154+
vulnerability_label = f"contrast-vuln-id:VULN-{vuln_uuid}"
155+
remediation_label = f"smartfix-id:{remediation_id}"
156+
issue_title = vuln_title
157+
158+
# Use the provided issue_body or fall back to default
159+
if issue_body is None:
160+
log(f"Failed to generate issue body for vulnerability id {vuln_uuid}", is_error=True)
161+
error_exit(remediation_id, FailureCategory.AGENT_FAILURE.value)
162+
163+
# Use git_handler to find if there's an existing issue with this label
164+
issue_number = git_handler.find_issue_with_label(vulnerability_label)
165+
166+
if issue_number is None:
167+
# Check if this is because Issues are disabled
168+
if not git_handler.check_issues_enabled():
169+
log("GitHub Issues are disabled for this repository. External coding agent requires Issues to be enabled.", is_error=True)
170+
error_exit(remediation_id, FailureCategory.GIT_COMMAND_FAILURE.value)
171+
172+
debug_log(f"No GitHub issue found with label {vulnerability_label}")
173+
issue_number = git_handler.create_issue(issue_title, issue_body, vulnerability_label, remediation_label)
174+
if not issue_number:
175+
log(f"Failed to create issue with labels {vulnerability_label}, {remediation_label}", is_error=True)
176+
error_exit(remediation_id, FailureCategory.AGENT_FAILURE.value)
177+
else:
178+
debug_log(f"Found existing GitHub issue #{issue_number} with label {vulnerability_label}")
179+
if not git_handler.reset_issue(issue_number, remediation_label):
180+
log(f"Failed to reset issue #{issue_number} with labels {vulnerability_label}, {remediation_label}", is_error=True)
181+
error_exit(remediation_id, FailureCategory.AGENT_FAILURE.value)
182+
183+
telemetry_handler.update_telemetry("additionalAttributes.externalIssueNumber", issue_number)
184+
185+
# Poll for PR creation by the external agent
186+
log(f"Waiting for external agent to create a PR for issue #{issue_number}")
187+
188+
# Poll for a PR to be created by the external agent (100 attempts, 5 seconds apart = ~8.3 minutes max)
189+
pr_info = self._poll_for_pr(issue_number, remediation_id, vulnerability_label, remediation_label, max_attempts=100, sleep_seconds=5)
190+
191+
log("\n::endgroup::")
192+
if pr_info:
193+
pr_number = pr_info.get("number")
194+
pr_url = pr_info.get("url")
195+
log(f"External agent created PR #{pr_number} at {pr_url}")
196+
telemetry_handler.update_telemetry("resultInfo.prCreated", True)
197+
telemetry_handler.update_telemetry("additionalAttributes.prStatus", "OPEN")
198+
telemetry_handler.update_telemetry("additionalAttributes.prNumber", pr_number)
199+
telemetry_handler.update_telemetry("additionalAttributes.prUrl", pr_url)
200+
return True
201+
else:
202+
log("External agent failed to create a PR within the timeout period", is_error=True)
203+
telemetry_handler.update_telemetry("resultInfo.prCreated", False)
204+
telemetry_handler.update_telemetry("resultInfo.failureReason", "PR creation timeout")
205+
telemetry_handler.update_telemetry("resultInfo.failureCategory", FailureCategory.AGENT_FAILURE.name)
206+
return False
207+
208+
def _poll_for_pr(self, issue_number: int, remediation_id: str, vulnerability_label: str,
209+
remediation_label: str, max_attempts: int = 100, sleep_seconds: int = 5) -> Optional[dict]:
210+
"""
211+
Poll for a PR to be created by the external agent.
212+
213+
Args:
214+
issue_number: The issue number to check for a PR
215+
remediation_id: The remediation ID for telemetry and API notification
216+
max_attempts: Maximum number of polling attempts (default: 100)
217+
sleep_seconds: Time to sleep between attempts (default: 5 seconds)
218+
219+
Returns:
220+
Optional[dict]: PR information if found, None if not found after max attempts
221+
"""
222+
debug_log(f"Polling for PR creation for issue #{issue_number}, max {max_attempts} attempts with {sleep_seconds}s interval")
223+
224+
for attempt in range(1, max_attempts + 1):
225+
debug_log(f"Polling attempt {attempt}/{max_attempts} for PR related to issue #{issue_number}")
226+
227+
pr_info = git_handler.find_open_pr_for_issue(issue_number)
228+
229+
if pr_info:
230+
pr_number = pr_info.get("number")
231+
pr_url = pr_info.get("url")
232+
233+
# Add vulnerability and remediation labels to the PR
234+
labels_to_add = [vulnerability_label, remediation_label]
235+
if git_handler.add_labels_to_pr(pr_number, labels_to_add):
236+
debug_log(f"Successfully added labels to PR #{pr_number}: {labels_to_add}")
237+
else:
238+
log(f"Failed to add labels to PR #{pr_number}", is_error=True)
239+
return None
240+
241+
debug_log(f"Found PR #{pr_number} for issue #{issue_number} after {attempt} attempts")
242+
243+
# Notify the Remediation backend about the PR
244+
success = notify_remediation_pr_opened(
245+
remediation_id=remediation_id,
246+
pr_number=pr_number,
247+
pr_url=pr_url,
248+
contrast_host=self.config.CONTRAST_HOST,
249+
contrast_org_id=self.config.CONTRAST_ORG_ID,
250+
contrast_app_id=self.config.CONTRAST_APP_ID,
251+
contrast_auth_key=self.config.CONTRAST_AUTHORIZATION_KEY,
252+
contrast_api_key=self.config.CONTRAST_API_KEY
253+
)
254+
255+
if success:
256+
log(f"Successfully notified remediation backend about PR #{pr_number}")
257+
else:
258+
log(f"Failed to notify remediation backend about PR #{pr_number}", is_error=True)
259+
260+
return pr_info
261+
262+
# Sleep before the next attempt, but don't sleep after the last attempt
263+
if attempt < max_attempts:
264+
time.sleep(sleep_seconds)
265+
266+
log(f"No PR found for issue #{issue_number} after {max_attempts} polling attempts", is_error=True)
267+
return None
268+
269+
# Additional methods will be implemented later

src/github/github_api_client.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""GitHub API Client Implementation
2+
3+
This module provides comprehensive GitHub API integration including authentication,
4+
API operations, rate limiting, and error handling for SmartFix operations.
5+
"""
6+
7+
8+
class GitHubApiClient:
9+
"""
10+
GitHub API client for SmartFix operations.
11+
12+
This class handles all GitHub API interactions including authentication,
13+
rate limiting, error handling, and provides high-level operations for
14+
SmartFix functionality.
15+
16+
Key Responsibilities:
17+
- GitHub API authentication (tokens, GitHub Actions)
18+
- Repository operations (create, clone, delete)
19+
- Pull request operations (create, update, merge, close)
20+
- Issue operations (create, update, assign, label)
21+
- Branch operations (create, delete, compare)
22+
- API rate limiting and optimization
23+
- Comprehensive error handling and retry logic
24+
25+
Features:
26+
- Support for both GitHub.com and GitHub Enterprise
27+
- Automatic rate limit handling
28+
- Request/response logging and debugging
29+
- Connection pooling and caching
30+
"""
31+
32+
def __init__(self, token: str = None, base_url: str = "https://api.github.com"):
33+
"""
34+
Initialize GitHub API client.
35+
36+
Args:
37+
token: GitHub authentication token
38+
base_url: GitHub API base URL (for Enterprise support)
39+
"""
40+
# TODO: Implementation will be added in Task 4.2.3
41+
self.token = token
42+
self.base_url = base_url
43+
44+
# TODO: Implement API methods:
45+
# - authenticate()
46+
# - create_repository()
47+
# - create_pull_request()
48+
# - create_issue()
49+
# - add_labels()
50+
# - assign_reviewers()
51+
# - etc.

src/github/github_scm_provider.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""GitHub SCM Provider Implementation
2+
3+
This module provides the GitHub-specific implementation of the ScmProvider interface,
4+
including GitHub API operations, repository management, and GitHub Action integration.
5+
"""
6+
7+
# TODO: Import will be added when ScmProvider interface is implemented
8+
# from src.smartfix.domains.scm.provider import ScmProvider
9+
10+
11+
class GitHubScmProvider:
12+
"""
13+
GitHub implementation of the ScmProvider interface.
14+
15+
This class provides GitHub-specific implementations for all SCM operations
16+
including repository management, branch operations, pull request handling,
17+
and GitHub API integration.
18+
19+
Key Responsibilities:
20+
- GitHub repository operations (clone, branch, commit)
21+
- Pull request creation and management
22+
- GitHub API authentication and rate limiting
23+
- GitHub-specific error handling and recovery
24+
- Integration with GitHub Actions environment
25+
26+
TODO: Implement ScmProvider interface when available
27+
"""
28+
29+
def __init__(self):
30+
"""Initialize GitHub SCM provider with configuration and API client."""
31+
# TODO: Implementation will be added in Task 4.2.1
32+
pass
33+
34+
# TODO: Implement all ScmProvider interface methods:
35+
# - create_repository()
36+
# - create_branch()
37+
# - commit_changes()
38+
# - create_pull_request()
39+
# - etc.

0 commit comments

Comments
 (0)