package security import ( "encoding/json" "fmt" "os/exec" "strings" ) // analyzeWithCLI sends findings to Claude via the claude command-line tool func analyzeWithCLI(a *Audit, findings []Finding) error { fmt.Println(" Using Claude Code CLI for analysis...") for _, finding := range findings { // Build prompt for Claude prompt := buildClaudePrompt(finding) // Call claude in non-interactive print mode (-p). There is no "ask" // subcommand; passing one makes the CLI treat "ask" as the prompt. cmd := exec.Command("claude", "-p", prompt) output, err := cmd.Output() if err != nil { fmt.Printf(" ⚠️ Error analyzing %s %s: %v\n", finding.PackageName, finding.CVE, err) continue } a.Mitigations = append(a.Mitigations, parseMitigation(finding, string(output))) } return nil } // claudeResponse is the strict JSON schema we ask Claude to emit. type claudeResponse struct { Recommendation string `json:"recommendation"` Rationale string `json:"rationale"` NextSteps string `json:"next_steps"` } // parseMitigation builds a Mitigation from Claude's raw stdout. It prefers the // structured JSON we request, falling back to heuristic extraction if the // model returned free-form text. func parseMitigation(finding Finding, output string) Mitigation { if cr, ok := parseClaudeJSON(output); ok { rec := strings.ToLower(strings.TrimSpace(cr.Recommendation)) switch rec { case "update", "replace", "protect", "monitor": default: rec = "unknown" } return Mitigation{ Finding: finding, Recommendation: rec, Rationale: cr.Rationale, NextSteps: cr.NextSteps, } } // Fallback: heuristic parsing of free-form text. return Mitigation{ Finding: finding, Rationale: output, Recommendation: inferRecommendation(output), NextSteps: inferNextSteps(output), } } // parseClaudeJSON extracts a claudeResponse from output that may be wrapped in // Markdown code fences or surrounded by prose. func parseClaudeJSON(output string) (claudeResponse, bool) { s := strings.TrimSpace(output) // Strip a leading ```json / ``` fence and its closing fence if present. if strings.HasPrefix(s, "```") { if i := strings.IndexByte(s, '\n'); i != -1 { s = s[i+1:] } s = strings.TrimSuffix(strings.TrimSpace(s), "```") s = strings.TrimSpace(s) } // Narrow to the outermost JSON object. start := strings.IndexByte(s, '{') end := strings.LastIndexByte(s, '}') if start == -1 || end == -1 || end < start { return claudeResponse{}, false } var cr claudeResponse if err := json.Unmarshal([]byte(s[start:end+1]), &cr); err != nil { return claudeResponse{}, false } return cr, true } // analyzeWithSDK sends findings to Claude via Anthropic SDK // This requires ANTHROPIC_API_KEY environment variable func analyzeWithSDK(a *Audit, findings []Finding) error { fmt.Println(" Using Anthropic SDK for analysis...") // This is a placeholder - full SDK integration would go here // For now, fallback to CLI mode return analyzeWithCLI(a, findings) } // buildClaudePrompt constructs a prompt for Claude analysis func buildClaudePrompt(f Finding) string { return fmt.Sprintf(`You are a security expert analyzing a single software vulnerability. Package: %s (version %s) CVE: %s CVSS Score: %.1f Severity: %s Summary: %s Choose ONE recommended action: - update: update to a patched version - replace: replace with an alternative package - protect: add extra protections without updating - monitor: monitor and plan an update Respond with ONLY a JSON object (no prose, no Markdown fences) in exactly this shape: {"recommendation": "", "rationale": "", "next_steps": ""}`, f.PackageName, f.PackageVersion, f.CVE, f.CVSS, f.Severity, f.Summary) } // inferRecommendation extracts the recommendation from Claude's response func inferRecommendation(response string) string { response = strings.ToUpper(response) if strings.Contains(response, "UPDATE") { return "update" } if strings.Contains(response, "REPLACE") { return "replace" } if strings.Contains(response, "PROTECT") { return "protect" } if strings.Contains(response, "MONITOR") { return "monitor" } return "unknown" } // inferNextSteps extracts action items from Claude's response func inferNextSteps(response string) string { // Extract the "steps" section if present lines := strings.Split(response, "\n") var steps []string foundSteps := false for _, line := range lines { trimmed := strings.TrimSpace(line) if strings.Contains(strings.ToLower(line), "step") { foundSteps = true } if foundSteps && strings.HasPrefix(trimmed, "-") || strings.HasPrefix(trimmed, "•") || strings.HasPrefix(trimmed, "1") { steps = append(steps, trimmed) } } if len(steps) > 0 { return strings.Join(steps, "\n") } return response } // MarshalJSON converts Mitigation to JSON, handling the Finding struct func (m Mitigation) MarshalJSON() ([]byte, error) { type Alias Mitigation return json.Marshal(&struct { *Alias }{ Alias: (*Alias)(&m), }) }