Skip to content

Commit adc837e

Browse files
authored
Merge pull request #36 from Eshrath027/MK-1945
[MK-1947]:Changes to integrate codeassure in ASPM scanner cli
2 parents fae2ea4 + 5211484 commit adc837e

7 files changed

Lines changed: 106 additions & 133 deletions

File tree

.github/workflows/release.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ jobs:
117117
contents: write
118118

119119
env:
120-
PYTHON_VERSION: "3.10"
120+
PYTHON_VERSION: "3.12"
121121

122122
steps:
123123
- name: Checkout code
@@ -152,6 +152,12 @@ jobs:
152152
ls -lahR ../../
153153
cd ../../
154154
155+
- name: Upload Scanner Tarballs to Release
156+
uses: softprops/action-gh-release@v1
157+
with:
158+
files: |
159+
utils/*.tar.gz
160+
155161
- name: Upload ASPM DEB and Scanners for Linux
156162
uses: softprops/action-gh-release@v1
157163
with:

aspm_cli/scan/sast.py

Lines changed: 62 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212

1313
class SASTScanner:
1414
opengrep_image = os.getenv("SCAN_IMAGE", "public.ecr.aws/k9v9d5v2/accuknox/opengrepjob:0.1.0")
15-
claude_image = os.getenv("CLAUDE_IMAGE", "public.ecr.aws/k9v9d5v2/accuknox/ai-sast-claude-cli:latest")
15+
codeassure_image = os.getenv("CODEASSURE_IMAGE", "public.ecr.aws/k9v9d5v2/accuknox/ai-sast-codeassure-cli:0.1.0")
1616
result_file = "results.json"
1717

1818
def __init__(self, command=None, container_mode=True, severity = None,
1919
repo_url=None, commit_ref=None, commit_sha=None,
20-
pipeline_id=None, job_url=None, anthropic_api_key=None, ai_analysis=False):
20+
pipeline_id=None, job_url=None, ai_analysis=True, codeassure_config=None,aiscan_severity=None):
2121
"""
2222
:param command: Raw OpenGrep CLI args (string)
2323
:param container_mode: Run in Docker if True, else use local binary
@@ -26,19 +26,19 @@ def __init__(self, command=None, container_mode=True, severity = None,
2626
:param commit_sha: Commit SHA
2727
:param pipeline_id: CI pipeline ID
2828
:param job_url: CI job URL
29-
:param anthropic_api_key: Anthropic API key for AI analysis
3029
:param ai_analysis: Enable AI analysis of results
3130
"""
3231
self.command = command
3332
self.container_mode = container_mode
3433
self.severity = [s.strip().upper() for s in (severity).split(',')]
34+
self.aiscan_severity = [s.strip().upper() for s in aiscan_severity.split(',')] if ai_analysis and isinstance(aiscan_severity, str) else []
3535
self.repo_url = repo_url
3636
self.commit_ref = commit_ref
3737
self.commit_sha = commit_sha
3838
self.pipeline_id = pipeline_id
3939
self.job_url = job_url
40-
self.anthropic_api_key = anthropic_api_key or os.getenv("ANTHROPIC_API_KEY")
4140
self.ai_analysis = ai_analysis
41+
self.codeassure_config = codeassure_config
4242

4343
def run(self):
4444
try:
@@ -79,6 +79,7 @@ def run(self):
7979
try:
8080
Logger.get_logger().debug("Starting AI analysis of SAST results...")
8181
self._run_ai_analysis()
82+
self._apply_verification_fields()
8283
except Exception as e:
8384
Logger.get_logger().error(f"AI analysis failed: {e}")
8485

@@ -101,169 +102,103 @@ def _run_ai_analysis(self):
101102
This ensures the scan continues successfully even if AI analysis fails.
102103
"""
103104
try:
104-
if not self.anthropic_api_key:
105-
Logger.get_logger().warning("Anthropic API key not provided. Skipping AI analysis.")
106-
return
107-
108105
if self.container_mode:
109-
docker_pull(self.claude_image)
106+
docker_pull(self.codeassure_image)
107+
else:
108+
ToolManager.get_path("codeassure") # raises FileNotFoundError if not installed
110109

111110
# Check if there are any results to analyze
112111
with open(self.result_file, 'r') as f:
113112
current_data = json.load(f)
114113

115114
results = current_data.get("results", [])
116-
Logger.get_logger().info(f"Running Claude AI analysis: {len(results)} findings to analyze.")
115+
Logger.get_logger().info(f"Running AI analysis: {len(results)} findings to analyze.")
117116

118117
if not results or len(results) == 0:
119118
Logger.get_logger().debug("No results to analyze. Skipping AI analysis.")
120119
return
121120

122121

123-
cmd = self._build_claude_command()
124-
ai_result = subprocess.run(cmd, capture_output=True, text=True, check=False)
125-
126-
if ai_result.stderr:
127-
Logger.get_logger().info(f"Claude analysis stderr: {ai_result.stderr}")
122+
cmd = self._build_ai_analysis_command()
123+
124+
ai_result = subprocess.run(cmd, check=False)
128125

129126
if ai_result.returncode != 0:
130-
Logger.get_logger().info(f"AI analysis failed with exit code: {ai_result.returncode}.")
131-
if ai_result.stderr:
132-
Logger.get_logger().warning(f"Error details: {ai_result.stderr[:500]}")
133-
if ai_result.stdout:
134-
Logger.get_logger().info(f"Claude stdout: {ai_result.stdout[:500]}")
135-
Logger.get_logger().warning("Continuing with original results.")
136-
return
137-
138-
if not ai_result.stdout:
139-
Logger.get_logger().warning("AI analysis returned empty output. Continuing with original results.")
127+
Logger.get_logger().warning(f"AI analysis failed with exit code: {ai_result.returncode}. Continuing with original results.")
140128
return
141129

142-
# Extract JSON from Claude's output (remove markdown code blocks if present)
143-
output = ai_result.stdout.strip()
144-
# Try to extract JSON from markdown code blocks
145-
if "```json" in output:
146-
json_start = output.find("```json") + 7
147-
json_end = output.find("```", json_start)
148-
output = output[json_start:json_end].strip()
149-
150-
elif "```" in output:
151-
json_start = output.find("```") + 3
152-
json_end = output.find("```", json_start)
153-
output = output[json_start:json_end].strip()
154-
155-
156-
# Try to find JSON object/array if there's extra text
157-
if not output.startswith('{') and not output.startswith('['):
158-
json_start = min(
159-
output.find('{') if '{' in output else len(output),
160-
output.find('[') if '[' in output else len(output)
161-
)
162-
if json_start < len(output):
163-
output = output[json_start:]
164-
165-
166-
# Parse the AI output
167-
updated_results = json.loads(output)
168-
130+
except Exception as e:
131+
Logger.get_logger().warning(f"Unexpected error during AI analysis: {e}. Continuing with original results.")
132+
Logger.get_logger().debug(f"Exception details: {str(e)}")
169133

170-
# Validate the structure
171-
if not isinstance(updated_results, dict) or "results" not in updated_results:
172-
Logger.get_logger().warning("AI analysis output missing 'results' field. Continuing with original results.")
173-
return
134+
return
135+
136+
def _apply_verification_fields(self):
137+
"""
138+
After codeassure writes verification data, promote is_false_positive
139+
and validation_reason to the top level of each finding.
140+
"""
141+
try:
142+
with open(self.result_file, 'r') as f:
143+
data = json.load(f)
174144

175-
# Preserve metadata fields that were added by process_result_file
176-
metadata_fields = ['repo', 'sha', 'ref', 'run_id', 'repo_url', 'repo_run_url']
177-
for field in metadata_fields:
178-
if field in current_data:
179-
updated_results[field] = current_data[field]
145+
for finding in data.get("results", []):
146+
verification = finding.get("verification", {})
147+
is_vuln = verification.get("is_security_vulnerability")
148+
finding["is_false_positive"] = not bool(is_vuln) if is_vuln is not None else None
149+
finding["validation_reason"] = verification.get("reason")
180150

181-
# Write the updated results
182151
with open(self.result_file, 'w') as f:
183-
json.dump(updated_results, f, indent=2)
184-
185-
186-
Logger.get_logger().debug("AI analysis completed successfully and results updated.")
152+
json.dump(data, f, indent=2)
187153

188-
except json.JSONDecodeError as e:
189-
Logger.get_logger().warning(f"Failed to parse Claude output as JSON: {e}. Continuing with original results.")
190-
Logger.get_logger().debug(f"Raw output: {ai_result.stdout[:500] if 'ai_result' in locals() else 'N/A'}...")
191-
except FileNotFoundError as e:
192-
Logger.get_logger().warning(f"Result file not found during AI analysis: {e}. Continuing without AI analysis.")
154+
Logger.get_logger().debug("Verification fields applied to results.")
193155
except Exception as e:
194-
Logger.get_logger().warning(f"Unexpected error during AI analysis: {e}. Continuing with original results.")
195-
Logger.get_logger().debug(f"Exception details: {str(e)}")
156+
Logger.get_logger().warning(f"Could not apply verification fields: {e}")
196157

197-
return
198-
199158
def validate_updated_results(self, results):
200159
if not isinstance(results, dict) or "results" not in results:
201160
raise ValueError("AI analysis output is not in the expected format.")
202161

203-
# for item in results["results"]:
204-
# if "is_false_positive" not in item or "validation_reason" not in item:
205-
# raise ValueError("Missing required fields in AI analysis results.")
206162

207-
def _build_claude_command(self) -> list[str]:
163+
def _build_ai_analysis_command(self) -> list[str]:
208164
"""
209-
Build the Claude CLI command (local or Docker).
165+
Build the AI Analysis CLI command (local or Docker).
210166
211167
:return: List of command arguments
212168
"""
213169

214-
system_prompt = """You are a security analysis expert with direct file access.
215-
216-
Your task: Analyze SAST findings and determine if they are real vulnerabilities or false positives.
217-
218-
CRITICAL RULES:
219-
1. Use bash tool to read files directly
220-
2. DO NOT generate Python scripts
221-
3. DO NOT output file contents as-is
222-
4. Output ONLY the MODIFIED JSON object with your analysis
223-
224-
For each finding:
225-
1. Read the source code file at the specified path
226-
2. Examine the code at the specified line numbers
227-
3. Analyze: input validation, framework protections, code context, exploitability
228-
4. Add two fields: "is_false_positive" (boolean) and "validation_reason" (string)
229-
230-
Output format: Pure JSON object with all original fields + your two new analysis fields in each finding of results."""
231-
232-
user_prompt = """Task: Analyze security findings in /workspace/results.json
233-
234-
Steps to execute:
235-
1. Read /workspace/results.json using bash
236-
2. For EACH item in the "results" array:
237-
- Read the source file specified in "path" field
238-
- Examine code at the "line" number
239-
- Determine if it's a false positive based on actual code review
240-
- Add "is_false_positive": true/false
241-
- Add "validation_reason": "brief explanation based on code analysis"
242-
243-
3. Output the COMPLETE JSON structure with:
244-
- ALL original fields preserved exactly as they were
245-
- The two new analysis fields added to each result
246-
- Proper JSON formatting
247-
248-
IMPORTANT:
249-
- Output ONLY the final JSON object, nothing else
250-
- No markdown code blocks (no ```)
251-
- No explanations or commentary
252-
- Start output with { and end with }
253-
- The output should be parseable JSON"""
254-
255-
256170
if not self.container_mode:
257-
cmd = ["claude", "--system-prompt", system_prompt, user_prompt]
171+
cmd = [
172+
ToolManager.get_path("codeassure"),
173+
"--codebase", os.getcwd(),
174+
"--findings", self.result_file,
175+
"--output", self.result_file,
176+
]
177+
if self.codeassure_config:
178+
cmd.extend(["--config", self.codeassure_config])
179+
if self.aiscan_severity:
180+
cmd.extend(["--severity", ",".join(self.aiscan_severity)])
181+
258182
else:
259183
cmd = [
260184
"docker", "run", "--rm",
261-
"-e", f"ANTHROPIC_API_KEY={self.anthropic_api_key}",
262-
"-e", "CLAUDE_CODE_MAX_OUTPUT_TOKENS=200000",
263185
"-v", f"{os.getcwd()}:/workspace",
264-
self.claude_image,
265-
"claude", "--system-prompt", system_prompt, user_prompt
266186
]
187+
# if self.codeassure_config:
188+
# config_path = os.path.abspath(self.codeassure_config)
189+
# cmd.extend(["-v", f"{config_path}:/workspace/codeassure.json"])
190+
191+
cmd.extend([
192+
self.codeassure_image,
193+
"--codebase", "/workspace",
194+
"--findings", "/workspace/results.json",
195+
"--output", "/workspace/results.json",
196+
])
197+
if self.codeassure_config:
198+
cmd.extend(["--config", "/workspace/codeassure.json"])
199+
if self.aiscan_severity:
200+
cmd.extend(["--severity", ",".join(self.aiscan_severity)])
201+
267202

268203
return cmd
269204

aspm_cli/scanners/sast_scanner.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,18 @@ def add_arguments(self, parser: argparse.ArgumentParser):
2424
default="INFO,WARNING,LOW,MEDIUM,HIGH,CRITICAL",
2525
help="Comma-separated list of severities to check. If any match, the scan will fail. Defaults to all severities."
2626
)
27+
parser.add_argument(
28+
"--aiscan-severity",
29+
default=None,
30+
help="Comma-separated list of severities to check for AI analysis. If any match, AI analysis will run on those findings."
31+
)
2732
parser.add_argument("--repo-url", default=GitInfo.get_repo_url(), help="Git repository URL")
2833
parser.add_argument("--commit-ref", default=GitInfo.get_commit_ref(), help="Commit reference for scanning")
2934
parser.add_argument("--commit-sha", default=GitInfo.get_commit_sha(), help="Commit SHA for scanning")
3035
parser.add_argument("--pipeline-id", help="Pipeline ID for scanning")
3136
parser.add_argument("--job-url", help="Job URL for scanning")
3237
parser.add_argument("--ai-analysis", action="store_true", help="Enable AI analysis of results")
33-
parser.add_argument("--anthropic-api-key", help="Anthropic API key for AI analysis")
38+
parser.add_argument("--codeassure-config", help="Path to codeassure.json config file for AI analysis")
3439

3540
def validate_config(self, args: argparse.Namespace, validator: ConfigValidator):
3641
validator.validate_sast_scan(
@@ -43,12 +48,13 @@ def run_scan(self, args: argparse.Namespace) -> tuple[int, str]:
4348
command=args.command,
4449
container_mode=args.container_mode,
4550
severity=args.severity,
51+
aiscan_severity=args.aiscan_severity if args.ai_analysis else [],
4652
repo_url=args.repo_url,
4753
commit_ref=args.commit_ref,
4854
commit_sha=args.commit_sha,
4955
pipeline_id=args.pipeline_id,
5056
job_url=args.job_url,
51-
anthropic_api_key=getattr(args, 'anthropic_api_key', None),
52-
ai_analysis=args.ai_analysis
57+
ai_analysis=args.ai_analysis,
58+
codeassure_config=getattr(args, 'codeassure_config', None)
5359
)
5460
return scanner.run()

aspm_cli/tool/download.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ class ToolDownloader:
1919
"secret": "https://github.com/accuknox/aspm-scanner-cli/releases/download/v0.10.1/secret.tar.gz",
2020
"sq-sast": "https://github.com/accuknox/aspm-scanner-cli/releases/download/v0.10.1/sq-sast.tar.gz",
2121
"sast": "https://github.com/accuknox/aspm-scanner-cli/releases/download/v0.10.1/sast.tar.gz",
22-
"dast": "https://github.com/accuknox/aspm-scanner-cli/releases/download/v0.10.1/dast.tar.gz"
22+
"dast": "https://github.com/accuknox/aspm-scanner-cli/releases/download/v0.10.1/dast.tar.gz",
23+
"codeassure": "https://github.com/accuknox/aspm-scanner-cli/releases/download/v0.14.2/codeassure.tar.gz"
2324
},
2425
}
2526

aspm_cli/tool/manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class ToolManager:
3838
"dast-java": Path("dast") / "java" / "bin",
3939
"sast": Path("sast") / "sast",
4040
"sast-rules": Path("sast") / "rules",
41+
"codeassure": Path("codeassure") / "codeassure",
4142
}
4243

4344

aspm_cli/utils/validation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from pydantic import BaseModel, Field, field_validator, model_validator
22
from typing import Literal, Optional
33

4-
ALLOWED_TOOL_TYPES = ["iac", "sast", "secret", "container", "dast", "sq-sast"]
4+
ALLOWED_TOOL_TYPES = ["iac", "sast", "secret", "container", "dast", "sq-sast", "codeassure"]
55

66
class ToolDownloadConfig(BaseModel):
77
tooltype: Optional[str] = Field(default=None)

utils/prepare-aspm-scanners.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,28 @@ tar -czvf "$DAST_TAR" "$DAST_FOLDER"
132132
rm -rf "$TEMP_DIR" "$DAST_FOLDER"
133133
echo "✅ OpenJDK ${JDK_VERSION} + ZAP ${ZAP_VERSION} packaged into '$DAST_TAR'"
134134

135+
### CodeAssure -> codeassure.tar.gz
136+
echo "=== [7/7] Building CodeAssure ==="
137+
CODEASSURE_REPO="https://github.com/accuknox/codeassure-cli.git"
138+
CODEASSURE_FOLDER="codeassure"
139+
CODEASSURE_TAR="codeassure.tar.gz"
140+
TEMP_CODEASSURE="temp_codeassure_build"
141+
142+
rm -rf "$CODEASSURE_FOLDER" "$TEMP_CODEASSURE"
143+
git clone --branch main "$CODEASSURE_REPO" "$TEMP_CODEASSURE"
144+
cd "$TEMP_CODEASSURE"
145+
146+
pip install uv
147+
uv pip install --system -e ".[build]"
148+
pyinstaller codeassure.spec --clean
149+
150+
mkdir -p "../$CODEASSURE_FOLDER"
151+
cp dist/codeassure "../$CODEASSURE_FOLDER/codeassure"
152+
cd ..
153+
154+
cp -r "$CODEASSURE_FOLDER" "$PACKAGE_BIN_DIR"
155+
tar -czvf "$CODEASSURE_TAR" "$CODEASSURE_FOLDER"
156+
rm -rf "$TEMP_CODEASSURE" "$CODEASSURE_FOLDER"
157+
echo "✅ CodeAssure binary packaged as $CODEASSURE_TAR"
158+
135159
echo "🎉 All tools downloaded and prepared successfully."

0 commit comments

Comments
 (0)