Skip to content

Commit ada1adf

Browse files
Merge pull request #43 from Contrast-Security-OSS/AIML-59_add_python_linter
AIML-59 Add python linter
2 parents baea52c + 62c1718 commit ada1adf

21 files changed

+1423
-1079
lines changed

.flake8

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
[flake8]
2+
# Configuration for flake8 Python linter
3+
4+
# Maximum line length
5+
max-line-length = 180
6+
7+
# Exclude directories and files from linting
8+
exclude =
9+
.git,
10+
__pycache__,
11+
.venv,
12+
venv,
13+
env,
14+
.env,
15+
build,
16+
dist,
17+
*.egg-info
18+
19+
# Ignore specific error codes
20+
ignore =
21+
# E203: whitespace before ':' (conflicts with black formatter)
22+
E203,
23+
# W503: line break before binary operator (conflicts with black formatter)
24+
W503,
25+
# E501: line too long (we set max-line-length instead)
26+
# E501
27+
28+
# Select specific error codes to check (optional - if not specified, checks most things)
29+
select =
30+
E, # pycodestyle errors
31+
W, # pycodestyle warnings
32+
F, # pyflakes
33+
C, # mccabe complexity
34+
35+
# Maximum complexity allowed
36+
max-complexity = 15
37+
38+
# Show the source code for each error
39+
show-source = True
40+
41+
# Count the number of occurrences of each error/warning code
42+
statistics = True
43+
44+
# Enable showing the pep8 source for each error
45+
show-pep8 = True

src/agent_handler.py

Lines changed: 371 additions & 253 deletions
Large diffs are not rendered by default.

src/build_output_analyzer.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#-
1+
# -
22
# #%L
33
# Contrast AI SmartFix
44
# %%
@@ -20,44 +20,44 @@
2020
def extract_build_errors(build_output):
2121
"""
2222
Extract the most relevant error information from build output.
23-
23+
2424
This function captures error blocks with context before and after errors,
2525
and intelligently extends blocks when errors are found in sequence.
26-
26+
2727
Args:
2828
build_output: The complete output from the build command
29-
29+
3030
Returns:
3131
str: A condensed report of the most relevant error regions
3232
"""
3333
# If output is small enough, just return it all
3434
if len(build_output) < 2000:
3535
return build_output
36-
36+
3737
lines = build_output.splitlines()
38-
38+
3939
# Look at the last part of the output (where errors typically appear)
4040
tail_lines = lines[-500:] if len(lines) > 500 else lines
41-
41+
4242
# Common error indicators across build systems
4343
error_indicators = ["error", "exception", "failed", "failure", "fatal"]
44-
44+
4545
# Process the lines to find error regions with their context
4646
context_size = 5 # Number of lines to include before an error
4747
error_regions = [] # Will hold start and end indices of error regions
48-
48+
4949
# First pass: identify all error lines
5050
error_line_indices = []
5151
for i, line in enumerate(tail_lines):
5252
line_lower = line.lower()
5353
if any(indicator in line_lower for indicator in error_indicators):
5454
error_line_indices.append(i)
55-
55+
5656
# Second pass: merge nearby errors into regions
5757
if error_line_indices:
5858
current_region_start = max(0, error_line_indices[0] - context_size)
5959
current_region_end = error_line_indices[0] + context_size
60-
60+
6161
for idx in error_line_indices[1:]:
6262
# If this error is within or close to current region, extend the region
6363
if idx - context_size <= current_region_end + 2: # Allow small gaps
@@ -68,20 +68,20 @@ def extract_build_errors(build_output):
6868
error_regions.append((current_region_start, min(current_region_end, len(tail_lines) - 1)))
6969
current_region_start = max(0, idx - context_size)
7070
current_region_end = idx + context_size
71-
71+
7272
# Don't forget the last region
7373
error_regions.append((current_region_start, min(current_region_end, len(tail_lines) - 1)))
74-
74+
7575
# Extract the text from each error region
7676
error_blocks = []
7777
for start, end in error_regions:
7878
region_lines = tail_lines[start:end + 1]
7979
error_blocks.append("\n".join(region_lines))
80-
80+
8181
# If we found error blocks, return them (up to 3 most recent)
8282
if error_blocks:
8383
result_blocks = error_blocks[-3:] if len(error_blocks) > 3 else error_blocks
8484
return "BUILD FAILURE - KEY ERRORS:\n\n" + "\n\n...\n\n".join(result_blocks)
85-
85+
8686
# Fallback: just return the last part of the build output
8787
return "BUILD FAILURE - LAST OUTPUT:\n\n" + "\n".join(tail_lines[-50:])

src/closed_handler.py

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#-
1+
# -
22
# #%L
33
# Contrast AI SmartFix
44
# %%
@@ -28,25 +28,24 @@
2828
from src.git_handler import extract_issue_number_from_branch
2929
import src.telemetry_handler as telemetry_handler
3030

31-
def handle_closed_pr():
32-
"""Handles the logic when a pull request is closed without merging."""
33-
telemetry_handler.initialize_telemetry()
34-
35-
log("--- Handling Closed (Unmerged) Contrast AI SmartFix Pull Request ---")
3631

37-
# Get PR event details from environment variables set by GitHub Actions
32+
def _load_github_event() -> dict:
33+
"""Load and parse the GitHub event data."""
3834
event_path = os.getenv("GITHUB_EVENT_PATH")
3935
if not event_path:
4036
log("Error: GITHUB_EVENT_PATH not set. Cannot process PR event.", is_error=True)
4137
sys.exit(1)
4238

4339
try:
4440
with open(event_path, 'r') as f:
45-
event_data = json.load(f)
41+
return json.load(f)
4642
except Exception as e:
4743
log(f"Error reading or parsing GITHUB_EVENT_PATH file: {e}", is_error=True)
4844
sys.exit(1)
4945

46+
47+
def _validate_pr_event(event_data: dict) -> dict:
48+
"""Validate the PR event and return PR data."""
5049
if event_data.get("action") != "closed":
5150
log("PR action is not 'closed'. Skipping.")
5251
sys.exit(0)
@@ -57,20 +56,22 @@ def handle_closed_pr():
5756
sys.exit(0)
5857

5958
debug_log("Pull request was closed without merging.")
59+
return pull_request
6060

61-
# Get the branch name from the PR
61+
62+
def _extract_remediation_info(pull_request: dict) -> tuple:
63+
"""Extract remediation ID and other info from PR data."""
6264
branch_name = pull_request.get("head", {}).get("ref")
6365
if not branch_name:
6466
log("Error: Could not determine branch name from PR.", is_error=True)
6567
sys.exit(1)
66-
67-
debug_log(f"Branch name: {branch_name}")
6868

69+
debug_log(f"Branch name: {branch_name}")
6970
labels = pull_request.get("labels", [])
7071

7172
# Extract remediation ID from branch name or PR labels
7273
remediation_id = None
73-
74+
7475
# Check if this is a branch created by external agent (e.g., GitHub Copilot)
7576
if branch_name.startswith("copilot/fix"):
7677
debug_log("Branch appears to be created by external agent. Extracting remediation ID from PR labels.")
@@ -87,21 +88,21 @@ def handle_closed_pr():
8788
# Use original method for branches created by SmartFix
8889
remediation_id = extract_remediation_id_from_branch(branch_name)
8990
telemetry_handler.update_telemetry("additionalAttributes.codingAgent", "INTERNAL-SMARTFIX")
90-
91+
9192
if not remediation_id:
9293
if branch_name.startswith("copilot/fix"):
9394
log(f"Error: Could not extract remediation ID from PR labels for external agent branch: {branch_name}", is_error=True)
9495
else:
9596
log(f"Error: Could not extract remediation ID from branch name: {branch_name}", is_error=True)
96-
# If we can't find the remediation ID, we can't proceed
9797
sys.exit(1)
98-
99-
debug_log(f"Extracted Remediation ID: {remediation_id}")
100-
telemetry_handler.update_telemetry("additionalAttributes.remediationId", remediation_id)
101-
102-
# Try to extract vulnerability UUID from PR labels
98+
99+
return remediation_id, labels
100+
101+
102+
def _extract_vulnerability_info(labels: list) -> str:
103+
"""Extract vulnerability UUID from PR labels."""
103104
vuln_uuid = "unknown"
104-
105+
105106
for label in labels:
106107
label_name = label.get("name", "")
107108
if label_name.startswith("contrast-vuln-id:VULN-"):
@@ -111,16 +112,16 @@ def handle_closed_pr():
111112
if vuln_uuid and vuln_uuid != "unknown":
112113
debug_log(f"Extracted Vulnerability UUID from PR label: {vuln_uuid}")
113114
break
114-
telemetry_handler.update_telemetry("vulnInfo.vulnId", vuln_uuid)
115-
telemetry_handler.update_telemetry("vulnInfo.vulnRule", "unknown")
116-
115+
117116
if vuln_uuid == "unknown":
118117
debug_log("Could not extract vulnerability UUID from PR labels. Telemetry may be incomplete.")
119118

120-
121-
# Notify the Remediation backend service about the closed PR
119+
return vuln_uuid
120+
121+
122+
def _notify_remediation_service(remediation_id: str):
123+
"""Notify the Remediation backend service about the closed PR."""
122124
log(f"Notifying Remediation service about closed PR for remediation {remediation_id}...")
123-
# Get config instance using the canonical OO approach
124125
config = get_config()
125126
remediation_notified = contrast_api.notify_remediation_pr_closed(
126127
remediation_id=remediation_id,
@@ -130,16 +131,42 @@ def handle_closed_pr():
130131
contrast_auth_key=config.CONTRAST_AUTHORIZATION_KEY,
131132
contrast_api_key=config.CONTRAST_API_KEY
132133
)
133-
134+
134135
if remediation_notified:
135136
log(f"Successfully notified Remediation service about closed PR for remediation {remediation_id}.")
136137
else:
137138
log(f"Failed to notify Remediation service about closed PR for remediation {remediation_id}.", is_error=True)
138139

140+
141+
def handle_closed_pr():
142+
"""Handles the logic when a pull request is closed without merging."""
143+
telemetry_handler.initialize_telemetry()
144+
145+
log("--- Handling Closed (Unmerged) Contrast AI SmartFix Pull Request ---")
146+
147+
# Load and validate GitHub event data
148+
event_data = _load_github_event()
149+
pull_request = _validate_pr_event(event_data)
150+
151+
# Extract remediation and vulnerability information
152+
remediation_id, labels = _extract_remediation_info(pull_request)
153+
vuln_uuid = _extract_vulnerability_info(labels)
154+
155+
# Update telemetry with extracted information
156+
debug_log(f"Extracted Remediation ID: {remediation_id}")
157+
telemetry_handler.update_telemetry("additionalAttributes.remediationId", remediation_id)
158+
telemetry_handler.update_telemetry("vulnInfo.vulnId", vuln_uuid)
159+
telemetry_handler.update_telemetry("vulnInfo.vulnRule", "unknown")
160+
161+
# Notify the Remediation backend service
162+
_notify_remediation_service(remediation_id)
163+
164+
# Complete telemetry and finish
139165
telemetry_handler.update_telemetry("additionalAttributes.prStatus", "CLOSED")
140166
contrast_api.send_telemetry_data()
141-
167+
142168
log("--- Closed Contrast AI SmartFix Pull Request Handling Complete ---")
143169

170+
144171
if __name__ == "__main__":
145172
handle_closed_pr()

0 commit comments

Comments
 (0)