package report import ( "encoding/json" "fmt" "os" "os/exec" "strings" "github.com/user/package-review/internal/security" ) // WriteJSON writes the audit results as JSON func WriteJSON(audit *security.Audit, outputPath string) error { data := map[string]interface{}{ "timestamp": audit.Inventory.Timestamp, "system": audit.Inventory.System, "summary": map[string]interface{}{ "total_packages": audit.Inventory.CountPackages(), "by_source": audit.Inventory.CountBySource(), "total_findings": len(audit.Findings), "vulnerable_count": audit.CountVulnerable(), "high_severity": len(audit.FilterByMinSeverity(7.0)), "critical_findings": len(audit.FilterByMinSeverity(9.0)), }, "findings": audit.Findings, "mitigations": audit.Mitigations, } jsonBytes, err := json.MarshalIndent(data, "", " ") if err != nil { return fmt.Errorf("marshal JSON: %w", err) } if err := os.WriteFile(outputPath, jsonBytes, 0o644); err != nil { return fmt.Errorf("write JSON file: %w", err) } return nil } // GenerateAstroSite generates an Astro static site from the audit results func GenerateAstroSite(audit *security.Audit, outputHTMLPath string) error { // Create intermediate JSON for Astro to consume astroDataFile := "audit_data.json" // Write audit data to JSON file that Astro will use data := map[string]interface{}{ "timestamp": audit.Inventory.Timestamp, "system": audit.Inventory.System, "findings": audit.Findings, "mitigations": audit.Mitigations, "summary": map[string]interface{}{ "total_packages": audit.Inventory.CountPackages(), "vulnerable_count": audit.CountVulnerable(), "high_severity": len(audit.FilterByMinSeverity(7.0)), "critical_findings": len(audit.FilterByMinSeverity(9.0)), }, } jsonBytes, err := json.MarshalIndent(data, "", " ") if err != nil { return fmt.Errorf("marshal Astro data: %w", err) } if err := os.WriteFile(astroDataFile, jsonBytes, 0o644); err != nil { return fmt.Errorf("write Astro data file: %w", err) } // Check if Astro is available _, err = exec.LookPath("astro") if err != nil { // Fallback: generate a simple HTML report without Astro fmt.Println(" ⚠️ Astro not found, generating basic HTML report...") return generateBasicHTML(audit, outputHTMLPath) } // Build Astro site cmd := exec.Command("astro", "build", "--outDir", "dist") if err := cmd.Run(); err != nil { fmt.Println(" ⚠️ Astro build failed, generating basic HTML report...") return generateBasicHTML(audit, outputHTMLPath) } return nil } // generateBasicHTML creates a simple HTML report as fallback func generateBasicHTML(audit *security.Audit, outputPath string) error { html := ` Security Audit Report

🔒 Security Audit Report

Generated: ` + audit.Inventory.Timestamp.String() + `

Summary

` + fmt.Sprintf("%d", audit.Inventory.CountPackages()) + `
Total Packages
` + fmt.Sprintf("%d", audit.CountVulnerable()) + `
With CVEs
` + fmt.Sprintf("%d", len(audit.FilterByMinSeverity(9.0))) + `
CRITICAL
` + fmt.Sprintf("%d", len(audit.FilterByMinSeverity(7.0))-len(audit.FilterByMinSeverity(9.0))) + `
HIGH

Findings

` for _, finding := range audit.Findings { cssClass := "cvss-low" switch { case finding.CVSS >= 9.0: cssClass = "cvss-critical" case finding.CVSS >= 7.0: cssClass = "cvss-high" case finding.CVSS >= 4.0: cssClass = "cvss-medium" } html += `
` + finding.PackageName + ` (` + finding.PackageVersion + `)
` + finding.CVE + `
` + fmt.Sprintf("%.1f", finding.CVSS) + `

` + finding.Summary + `

` // Find mitigation for this finding for _, mit := range audit.Mitigations { if mit.Finding.CVE == finding.CVE && mit.Finding.PackageName == finding.PackageName { html += `
Recommendation: ` + strings.ToUpper(mit.Recommendation) + `
Rationale: ` + mit.Rationale + `
Next Steps: ` + mit.NextSteps + `
` break } } html += `
` } html += `
` if err := os.WriteFile(outputPath, []byte(html), 0o644); err != nil { return fmt.Errorf("write HTML file: %w", err) } return nil }