1212
1313class 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
0 commit comments