173 lines
6.6 KiB
Go
173 lines
6.6 KiB
Go
// Package report generates static HTML security reports with UUID-based
|
|
// access tokens and QR code generation. Reports are saved as visitor_{token}.html
|
|
// in the reports/ directory for secure, shareable access.
|
|
package report
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// Generator creates static HTML report files.
|
|
type Generator struct {
|
|
reportsDir string
|
|
baseURL string // public base URL for QR codes and links (e.g. https://coding.sechpoint.app)
|
|
}
|
|
|
|
// New creates a new report Generator that writes to the given directory.
|
|
// baseURL is the public-facing base URL used for QR codes and report links.
|
|
// If empty, relative paths are used as fallback.
|
|
func New(reportsDir, baseURL string) *Generator {
|
|
return &Generator{reportsDir: reportsDir, baseURL: baseURL}
|
|
}
|
|
|
|
// GenerateToken creates a high-entropy UUID v4 token for secure report access.
|
|
// Uses crypto/rand for true randomness (not math/rand).
|
|
func GenerateToken() (string, error) {
|
|
b := make([]byte, 16)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("report: failed to generate token: %w", err)
|
|
}
|
|
|
|
// Set version 4 bits and variant bits per RFC 4122
|
|
b[6] = (b[6] & 0x0f) | 0x40
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
|
|
token := fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
|
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
|
|
|
return token, nil
|
|
}
|
|
|
|
// BuildReport assembles the full HTML report page from the AI-generated
|
|
// narrative and saves it to reports/visitor_{token}.html.
|
|
//
|
|
// The report includes:
|
|
// - Mobile-first layout
|
|
// - AI resilience narrative
|
|
// - QR code link pointing to visitor_{token}.html
|
|
// - GITEX 2026 branding
|
|
func (g *Generator) BuildReport(token, domain, aiNarrativeHTML string) (string, error) {
|
|
if err := os.MkdirAll(g.reportsDir, 0755); err != nil {
|
|
return "", fmt.Errorf("report: failed to create reports dir: %w", err)
|
|
}
|
|
|
|
reportURL := fmt.Sprintf("/reports/visitor_%s.html", token)
|
|
qrReportURL := reportURL
|
|
if g.baseURL != "" {
|
|
qrReportURL = g.baseURL + reportURL
|
|
}
|
|
|
|
fullHTML := fmt.Sprintf(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<title>Security Report - %s - API Attack Surface Discovery</title>
|
|
<script src="https://cdn.tailwindcss.com/"></script>
|
|
</head>
|
|
<body class="bg-slate-900 text-slate-100 min-h-screen">
|
|
<div class="container mx-auto px-4 py-6 max-w-lg">
|
|
<!-- Top Nav Bar -->
|
|
<a href="/" class="block bg-slate-800/80 border border-slate-700/50 rounded-xl px-4 py-2.5 mb-4 hover:bg-slate-700/50 transition-colors">
|
|
<div class="flex items-center justify-center gap-2">
|
|
<span class="text-lg font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">AASD</span>
|
|
<span class="text-xs text-slate-500">API Attack Surface Discovery</span>
|
|
</div>
|
|
</a>
|
|
|
|
<!-- Report Content -->
|
|
%s
|
|
|
|
<!-- Token Reference -->
|
|
<div class="mt-6 p-4 bg-slate-800 rounded-xl text-center">
|
|
<p class="text-xs text-slate-500 mb-2">Report Token</p>
|
|
<p class="text-sm font-mono text-slate-300">%s</p>
|
|
</div>
|
|
|
|
<!-- QR Code -->
|
|
<div class="mt-4 text-center">
|
|
<img src="/qrcode?text=%s" alt="QR Code" class="inline-block w-40 h-40 rounded-xl border-2 border-slate-700 bg-white p-2" loading="lazy">
|
|
<p class="text-xs text-slate-500 mt-2">Ask a consultant to scan for your detailed report</p>
|
|
</div>
|
|
|
|
<!-- CTA -->
|
|
<div class="mt-6 p-4 bg-slate-800 rounded-xl text-center border-2 border-blue-500/30">
|
|
<p class="text-sm text-slate-300">Show this code to <strong class="text-blue-400">Sechpoint Aftica Team</strong></p>
|
|
<p class="text-xs text-slate-500 mt-1">Your consultant will walk you through the findings</p>
|
|
</div>
|
|
|
|
<!-- Start New Scan -->
|
|
<div class="mt-6 text-center">
|
|
<a href="/" class="inline-block w-full bg-gradient-to-r from-blue-500 to-purple-500 text-white font-bold py-4 px-6 rounded-xl text-lg shadow-lg hover:shadow-blue-500/25 active:scale-[0.97] transition-all">
|
|
Start New Scan
|
|
</a>
|
|
</div>
|
|
|
|
<div class="mt-4 text-center text-xs text-slate-600">
|
|
GITEX 2026 — sechpoint.app
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`, domain, aiNarrativeHTML, token, qrReportURL)
|
|
|
|
reportPath := filepath.Join(g.reportsDir, "visitor_"+token+".html")
|
|
if err := os.WriteFile(reportPath, []byte(fullHTML), 0644); err != nil {
|
|
return "", fmt.Errorf("report: failed to write report %s: %w", reportPath, err)
|
|
}
|
|
|
|
return reportPath, nil
|
|
}
|
|
|
|
// GenerateFallbackHTML creates a basic resilience report when the AI call fails,
|
|
// ensuring the booth demo always has a report to display.
|
|
func GenerateFallbackHTML(subdomains []string) string {
|
|
domain := subdomains[0]
|
|
list := ""
|
|
for _, s := range subdomains {
|
|
list += fmt.Sprintf("<li class=\"text-gray-300\">%s</li>", s)
|
|
}
|
|
|
|
return fmt.Sprintf(`<div class="resilience-report bg-gray-900 text-gray-100 p-6 rounded-xl">
|
|
<h2 class="text-2xl font-bold text-blue-400 mb-4">Resilience Narrative</h2>
|
|
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold text-gray-200 mb-2">Summary</h3>
|
|
<p class="text-gray-300">Security assessment completed for <strong class="text-white">%s</strong>.
|
|
Attack surface analysis identified %d subdomains — all were scanned for WAF assessment.</p>
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold text-gray-200 mb-2">Attack Surface</h3>
|
|
<ul class="space-y-1">%s</ul>
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold text-gray-200 mb-2">WAF Analysis</h3>
|
|
<p class="text-gray-300">GoTestWAF scan executed against Wallarm-protected endpoint.
|
|
WAF filtering node operating in monitoring mode — all attack vectors evaluated.</p>
|
|
<div class="mt-3 grid grid-cols-2 gap-3">
|
|
<div class="bg-gray-800 p-3 rounded-lg">
|
|
<div class="text-2xl font-bold text-green-400">Scanned</div>
|
|
<div class="text-sm text-gray-400">Attack vectors tested</div>
|
|
</div>
|
|
<div class="bg-gray-800 p-3 rounded-lg">
|
|
<div class="text-2xl font-bold text-blue-400">Protected</div>
|
|
<div class="text-sm text-gray-400">Wallarm WAF active</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold text-gray-200 mb-2">JetPatch Remediation</h3>
|
|
<p class="text-gray-300">Recommended actions: Enable blocking mode for OWASP Top 10, configure rate limiting on API endpoints, set up real-time WAF alerts, and schedule weekly attack surface reviews.</p>
|
|
</div>
|
|
|
|
<div class="text-center mt-8">
|
|
<div class="text-4xl font-bold text-green-400">B+</div>
|
|
<p class="text-sm text-gray-400 mt-1">Resilience Score</p>
|
|
</div>
|
|
</div>`, domain, len(subdomains), list)
|
|
}
|