22
33from __future__ import annotations
44
5+ import json
56from dataclasses import dataclass
67from datetime import datetime , timezone
78from pathlib import Path
1819from ..src .report_writer import build_report_payload , render_markdown_report , write_report_files
1920from ..src .rule_engine import run_rule_engine
2021from ..src .review_types import (
22+ DiffLineType ,
2123 FindingDisposition ,
2224 FindingSource ,
25+ FilterDecisionRecord ,
26+ FilterDecisionType ,
2327 ReviewCategory ,
2428 ReviewConclusion ,
2529 ReviewFinding ,
@@ -91,6 +95,7 @@ def run_review_task(config: ReviewAgentConfig) -> tuple[ReviewTask, ReviewReport
9195 sandbox_run = execute_skill_script (invocation , runtime = config .runtime )
9296 task .add_sandbox_run (sandbox_run )
9397 all_findings .extend (_sandbox_run_findings (task , sandbox_run ))
98+ all_findings .extend (_sandbox_output_findings (task , sandbox_run ))
9499 else :
95100 task .add_sandbox_run (
96101 build_blocked_run (
@@ -99,6 +104,7 @@ def run_review_task(config: ReviewAgentConfig) -> tuple[ReviewTask, ReviewReport
99104 reason = decision .reason ,
100105 )
101106 )
107+ all_findings .extend (_filter_decision_findings (task , decision ))
102108
103109 processed_findings = dedupe_and_classify_findings (all_findings )
104110 for finding in processed_findings :
@@ -206,3 +212,146 @@ def _sandbox_run_findings(task: ReviewTask, sandbox_run) -> list[ReviewFinding]:
206212 source = FindingSource .SANDBOX ,
207213 )
208214 ]
215+
216+
217+ def _sandbox_output_findings (task : ReviewTask , sandbox_run ) -> list [ReviewFinding ]:
218+ """Promote successful skill-script diagnostics into structured findings."""
219+
220+ if sandbox_run .status != sandbox_run .status .SUCCEEDED or not sandbox_run .stdout :
221+ return []
222+
223+ try :
224+ payload = json .loads (sandbox_run .stdout )
225+ except json .JSONDecodeError :
226+ return []
227+
228+ if sandbox_run .name != "run_linters" :
229+ return []
230+
231+ findings : list [ReviewFinding ] = []
232+ for warning in payload .get ("warnings" , []):
233+ mapping = _linter_warning_to_finding (task , warning )
234+ if mapping is not None :
235+ findings .append (mapping )
236+ return findings
237+
238+
239+ def _linter_warning_to_finding (task : ReviewTask , warning : str ) -> ReviewFinding | None :
240+ """Map deterministic linter warnings onto the canonical finding schema."""
241+
242+ warning_map = {
243+ "Security-sensitive call detected: eval" : {
244+ "needle" : "eval(" ,
245+ "severity" : ReviewSeverity .HIGH ,
246+ "title" : "Use of eval introduces code execution risk" ,
247+ "recommendation" : (
248+ "Replace eval with explicit parsing, a whitelist-based dispatcher, "
249+ "or a safe literal parser."
250+ ),
251+ "confidence" : 0.99 ,
252+ },
253+ "Shell execution enabled in subprocess call" : {
254+ "needle" : "shell=True" ,
255+ "severity" : ReviewSeverity .HIGH ,
256+ "title" : "subprocess call enables shell execution" ,
257+ "recommendation" : (
258+ "Pass an argument list and avoid shell=True unless a reviewed shell command "
259+ "is unavoidable."
260+ ),
261+ "confidence" : 0.96 ,
262+ },
263+ "TLS verification disabled" : {
264+ "needle" : "verify=False" ,
265+ "severity" : ReviewSeverity .MEDIUM ,
266+ "title" : "TLS certificate verification is disabled" ,
267+ "recommendation" : (
268+ "Keep certificate verification enabled or document a controlled "
269+ "test-only exception."
270+ ),
271+ "confidence" : 0.89 ,
272+ },
273+ }
274+ mapped = warning_map .get (warning )
275+ if mapped is None :
276+ return None
277+
278+ file_path , line_number , evidence = _find_added_line_evidence (
279+ task ,
280+ needle = mapped ["needle" ],
281+ )
282+ return ReviewFinding (
283+ severity = mapped ["severity" ],
284+ category = ReviewCategory .SECURITY ,
285+ file = file_path ,
286+ line = line_number ,
287+ title = mapped ["title" ],
288+ evidence = evidence ,
289+ recommendation = mapped ["recommendation" ],
290+ confidence = mapped ["confidence" ],
291+ source = FindingSource .SKILL_SCRIPT ,
292+ )
293+
294+
295+ def _filter_decision_findings (
296+ task : ReviewTask ,
297+ decision : FilterDecisionRecord ,
298+ ) -> list [ReviewFinding ]:
299+ """Expose non-allow filter decisions in the final report."""
300+
301+ if decision .decision == FilterDecisionType .ALLOW :
302+ return []
303+
304+ severity = ReviewSeverity .MEDIUM
305+ title = "Sandbox invocation requires human review"
306+ recommendation = (
307+ "Use local runtime only for explicit development fallback, or implement a real "
308+ "isolated executor before allowing non-local runtimes."
309+ )
310+ if decision .decision == FilterDecisionType .DENY :
311+ severity = ReviewSeverity .HIGH
312+ title = "Sandbox invocation was denied by filter policy"
313+ recommendation = "Adjust the diff or invocation so it complies with the sandbox policy."
314+
315+ file_path = (
316+ task .parsed_diff .changed_paths [0 ]
317+ if task .parsed_diff and task .parsed_diff .changed_paths
318+ else task .review_input .source
319+ )
320+ return [
321+ ReviewFinding (
322+ severity = severity ,
323+ category = ReviewCategory .SANDBOX ,
324+ file = file_path ,
325+ line = None ,
326+ title = title ,
327+ evidence = f"{ decision .target } : { decision .reason } " ,
328+ recommendation = recommendation ,
329+ confidence = 0.9 if decision .decision == FilterDecisionType .DENY else 0.75 ,
330+ source = FindingSource .FILTER ,
331+ disposition = FindingDisposition .NEEDS_HUMAN_REVIEW ,
332+ )
333+ ]
334+
335+
336+ def _find_added_line_evidence (
337+ task : ReviewTask ,
338+ * ,
339+ needle : str ,
340+ ) -> tuple [str , int | None , str ]:
341+ """Locate the added line that triggered a sandbox warning."""
342+
343+ if task .parsed_diff is None :
344+ return task .review_input .source , None , needle
345+
346+ for changed_file in task .parsed_diff .files :
347+ for hunk in changed_file .hunks :
348+ for line in hunk .lines :
349+ if line .line_type != DiffLineType .ADD :
350+ continue
351+ if needle not in line .text :
352+ continue
353+ return changed_file .display_path , line .new_line_no , line .raw_line
354+
355+ if task .parsed_diff .changed_paths :
356+ return task .parsed_diff .changed_paths [0 ], None , needle
357+ return task .review_input .source , None , needle
0 commit comments