328 lines
11 KiB
Go
328 lines
11 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// BlocklistExporter handles exporting threat intelligence for external use
|
|
type BlocklistExporter struct {
|
|
analyzer *ThreatAnalyzer
|
|
}
|
|
|
|
// NewBlocklistExporter creates a new blocklist exporter
|
|
func NewBlocklistExporter(analyzer *ThreatAnalyzer) *BlocklistExporter {
|
|
return &BlocklistExporter{
|
|
analyzer: analyzer,
|
|
}
|
|
}
|
|
|
|
// RegisterExportRoutes registers blocklist export endpoints
|
|
func (be *BlocklistExporter) RegisterExportRoutes(mux *http.ServeMux, sm *SecurityManager) {
|
|
// Public blocklist endpoints (no auth required for consumption)
|
|
mux.HandleFunc("/api/export/blocklist/txt", be.handleTxtBlocklist)
|
|
mux.HandleFunc("/api/export/blocklist/json", be.handleJSONBlocklist)
|
|
mux.HandleFunc("/api/export/blocklist/csv", be.handleCSVBlocklist)
|
|
mux.HandleFunc("/api/export/blocklist/suricata", be.handleSuricataBlocklist)
|
|
mux.HandleFunc("/api/export/blocklist/pf", be.handlePFBlocklist)
|
|
mux.HandleFunc("/api/export/blocklist/iptables", be.handleIPTablesBlocklist)
|
|
|
|
// Statistics endpoint
|
|
mux.HandleFunc("/api/export/stats", be.handleStats)
|
|
}
|
|
|
|
// BlocklistEntry represents an entry in the blocklist
|
|
type BlocklistEntry struct {
|
|
IP string `json:"ip"`
|
|
ThreatScore int `json:"threat_score"`
|
|
FirstSeen time.Time `json:"first_seen"`
|
|
LastSeen time.Time `json:"last_seen"`
|
|
TotalEvents int `json:"total_events"`
|
|
Services []string `json:"services"`
|
|
EventTypes []string `json:"event_types"`
|
|
IsBlocked bool `json:"is_blocked"`
|
|
Confidence string `json:"confidence"` // high, medium, low
|
|
}
|
|
|
|
// getBlocklistEntries retrieves blocklist entries with filtering
|
|
func (be *BlocklistExporter) getBlocklistEntries(minScore int, maxAge time.Duration, includeUnblocked bool) ([]BlocklistEntry, error) {
|
|
// Get IP reports with filters
|
|
filters := map[string]interface{}{
|
|
"min_threat_score": minScore,
|
|
}
|
|
|
|
if !includeUnblocked {
|
|
filters["is_blocked"] = true
|
|
}
|
|
|
|
reports, err := be.analyzer.GetIPReports(filters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var entries []BlocklistEntry
|
|
cutoff := time.Now().Add(-maxAge)
|
|
|
|
for _, report := range reports {
|
|
// Skip old entries if maxAge is specified
|
|
if maxAge > 0 && report.LastSeen.Before(cutoff) {
|
|
continue
|
|
}
|
|
|
|
// Get threat events for this IP
|
|
events, _ := be.analyzer.GetThreatEventsByIP(report.IP)
|
|
|
|
var services []string
|
|
var eventTypes []string
|
|
eventTypeMap := make(map[string]bool)
|
|
|
|
services = report.Services
|
|
|
|
for _, event := range events {
|
|
if !eventTypeMap[event.EventType] {
|
|
eventTypes = append(eventTypes, event.EventType)
|
|
eventTypeMap[event.EventType] = true
|
|
}
|
|
}
|
|
|
|
// Determine confidence level
|
|
confidence := "low"
|
|
if report.ThreatScore >= 80 {
|
|
confidence = "high"
|
|
} else if report.ThreatScore >= 50 {
|
|
confidence = "medium"
|
|
}
|
|
|
|
entries = append(entries, BlocklistEntry{
|
|
IP: report.IP,
|
|
ThreatScore: report.ThreatScore,
|
|
FirstSeen: report.FirstSeen,
|
|
LastSeen: report.LastSeen,
|
|
TotalEvents: len(events),
|
|
Services: services,
|
|
EventTypes: eventTypes,
|
|
IsBlocked: report.IsBlocked,
|
|
Confidence: confidence,
|
|
})
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
// handleTxtBlocklist exports blocklist in plain text format (one IP per line)
|
|
func (be *BlocklistExporter) handleTxtBlocklist(w http.ResponseWriter, r *http.Request) {
|
|
minScore := be.getIntParam(r, "min_score", 50)
|
|
maxAge := be.getDurationParam(r, "max_age", 30*24*time.Hour) // 30 days default
|
|
includeUnblocked := be.getBoolParam(r, "include_unblocked", false)
|
|
|
|
entries, err := be.getBlocklistEntries(minScore, maxAge, includeUnblocked)
|
|
if err != nil {
|
|
http.Error(w, "Failed to get blocklist", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=honeypot_blocklist.txt")
|
|
|
|
// Add header comment
|
|
fmt.Fprintf(w, "# Honeypot Threat Intelligence Blocklist\n")
|
|
fmt.Fprintf(w, "# Generated: %s\n", time.Now().UTC().Format(time.RFC3339))
|
|
fmt.Fprintf(w, "# Min Score: %d\n", minScore)
|
|
fmt.Fprintf(w, "# Total IPs: %d\n", len(entries))
|
|
fmt.Fprintf(w, "#\n")
|
|
|
|
for _, entry := range entries {
|
|
fmt.Fprintf(w, "%s\n", entry.IP)
|
|
}
|
|
}
|
|
|
|
// handleJSONBlocklist exports blocklist in JSON format
|
|
func (be *BlocklistExporter) handleJSONBlocklist(w http.ResponseWriter, r *http.Request) {
|
|
minScore := be.getIntParam(r, "min_score", 50)
|
|
maxAge := be.getDurationParam(r, "max_age", 30*24*time.Hour)
|
|
includeUnblocked := be.getBoolParam(r, "include_unblocked", false)
|
|
|
|
entries, err := be.getBlocklistEntries(minScore, maxAge, includeUnblocked)
|
|
if err != nil {
|
|
http.Error(w, "Failed to get blocklist", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
response := map[string]interface{}{
|
|
"generated_at": time.Now().UTC(),
|
|
"min_score": minScore,
|
|
"total_ips": len(entries),
|
|
"entries": entries,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=honeypot_blocklist.json")
|
|
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
// handleCSVBlocklist exports blocklist in CSV format
|
|
func (be *BlocklistExporter) handleCSVBlocklist(w http.ResponseWriter, r *http.Request) {
|
|
minScore := be.getIntParam(r, "min_score", 50)
|
|
maxAge := be.getDurationParam(r, "max_age", 30*24*time.Hour)
|
|
includeUnblocked := be.getBoolParam(r, "include_unblocked", false)
|
|
|
|
entries, err := be.getBlocklistEntries(minScore, maxAge, includeUnblocked)
|
|
if err != nil {
|
|
http.Error(w, "Failed to get blocklist", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/csv")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=honeypot_blocklist.csv")
|
|
|
|
// CSV header
|
|
fmt.Fprintf(w, "ip,threat_score,first_seen,last_seen,total_events,services,event_types,confidence\n")
|
|
|
|
for _, entry := range entries {
|
|
fmt.Fprintf(w, "%s,%d,%s,%s,%d,\"%s\",\"%s\",%s\n",
|
|
entry.IP,
|
|
entry.ThreatScore,
|
|
entry.FirstSeen.Format(time.RFC3339),
|
|
entry.LastSeen.Format(time.RFC3339),
|
|
entry.TotalEvents,
|
|
strings.Join(entry.Services, ";"),
|
|
strings.Join(entry.EventTypes, ";"),
|
|
entry.Confidence,
|
|
)
|
|
}
|
|
}
|
|
|
|
// handleSuricataBlocklist exports blocklist in Suricata format
|
|
func (be *BlocklistExporter) handleSuricataBlocklist(w http.ResponseWriter, r *http.Request) {
|
|
minScore := be.getIntParam(r, "min_score", 70) // Higher threshold for Suricata
|
|
maxAge := be.getDurationParam(r, "max_age", 7*24*time.Hour) // 7 days for active blocking
|
|
|
|
entries, err := be.getBlocklistEntries(minScore, maxAge, false)
|
|
if err != nil {
|
|
http.Error(w, "Failed to get blocklist", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=honeypot_suricata.rules")
|
|
|
|
// Generate Suricata rules
|
|
fmt.Fprintf(w, "# Honeypot Threat Intelligence - Suricata Rules\n")
|
|
fmt.Fprintf(w, "# Generated: %s\n", time.Now().UTC().Format(time.RFC3339))
|
|
fmt.Fprintf(w, "#\n")
|
|
|
|
for i, entry := range entries {
|
|
fmt.Fprintf(w, "drop ip %s any -> any any (msg:\"Honeypot Threat - %s (Score: %d)\"; sid:%d; rev:1;)\n",
|
|
entry.IP, strings.Join(entry.EventTypes, ","), entry.ThreatScore, 1000000+i)
|
|
}
|
|
}
|
|
|
|
// handlePFBlocklist exports blocklist in PF (BSD firewall) format
|
|
func (be *BlocklistExporter) handlePFBlocklist(w http.ResponseWriter, r *http.Request) {
|
|
minScore := be.getIntParam(r, "min_score", 60)
|
|
maxAge := be.getDurationParam(r, "max_age", 7*24*time.Hour)
|
|
|
|
entries, err := be.getBlocklistEntries(minScore, maxAge, false)
|
|
if err != nil {
|
|
http.Error(w, "Failed to get blocklist", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=honeypot_pf_blocklist.conf")
|
|
|
|
fmt.Fprintf(w, "# Honeypot Threat Intelligence - PF Blocklist\n")
|
|
fmt.Fprintf(w, "# Generated: %s\n", time.Now().UTC().Format(time.RFC3339))
|
|
fmt.Fprintf(w, "# Usage: pfctl -t honeypot_threats -T add -f honeypot_pf_blocklist.conf\n")
|
|
fmt.Fprintf(w, "#\n")
|
|
|
|
for _, entry := range entries {
|
|
fmt.Fprintf(w, "%s\n", entry.IP)
|
|
}
|
|
}
|
|
|
|
// handleIPTablesBlocklist exports blocklist in iptables format
|
|
func (be *BlocklistExporter) handleIPTablesBlocklist(w http.ResponseWriter, r *http.Request) {
|
|
minScore := be.getIntParam(r, "min_score", 60)
|
|
maxAge := be.getDurationParam(r, "max_age", 7*24*time.Hour)
|
|
|
|
entries, err := be.getBlocklistEntries(minScore, maxAge, false)
|
|
if err != nil {
|
|
http.Error(w, "Failed to get blocklist", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=honeypot_iptables.sh")
|
|
|
|
fmt.Fprintf(w, "#!/bin/bash\n")
|
|
fmt.Fprintf(w, "# Honeypot Threat Intelligence - iptables Rules\n")
|
|
fmt.Fprintf(w, "# Generated: %s\n", time.Now().UTC().Format(time.RFC3339))
|
|
fmt.Fprintf(w, "#\n\n")
|
|
|
|
for _, entry := range entries {
|
|
fmt.Fprintf(w, "iptables -A INPUT -s %s -j DROP # Score: %d, Events: %d\n",
|
|
entry.IP, entry.ThreatScore, entry.TotalEvents)
|
|
}
|
|
}
|
|
|
|
// handleStats provides statistics about the threat intelligence
|
|
func (be *BlocklistExporter) handleStats(w http.ResponseWriter, r *http.Request) {
|
|
// Get various statistics
|
|
allEntries, _ := be.getBlocklistEntries(0, 0, true)
|
|
blockedEntries, _ := be.getBlocklistEntries(0, 0, false)
|
|
highThreatEntries, _ := be.getBlocklistEntries(80, 0, true)
|
|
recentEntries, _ := be.getBlocklistEntries(0, 24*time.Hour, true)
|
|
|
|
stats := map[string]interface{}{
|
|
"generated_at": time.Now().UTC(),
|
|
"total_ips": len(allEntries),
|
|
"blocked_ips": len(blockedEntries),
|
|
"high_threat_ips": len(highThreatEntries),
|
|
"recent_ips_24h": len(recentEntries),
|
|
"export_formats": []string{"txt", "json", "csv", "suricata", "pf", "iptables"},
|
|
"api_endpoints": map[string]string{
|
|
"txt": "/api/export/blocklist/txt",
|
|
"json": "/api/export/blocklist/json",
|
|
"csv": "/api/export/blocklist/csv",
|
|
"suricata": "/api/export/blocklist/suricata",
|
|
"pf": "/api/export/blocklist/pf",
|
|
"iptables": "/api/export/blocklist/iptables",
|
|
},
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(stats)
|
|
}
|
|
|
|
// Helper functions for parameter parsing
|
|
func (be *BlocklistExporter) getIntParam(r *http.Request, param string, defaultValue int) int {
|
|
if val := r.URL.Query().Get(param); val != "" {
|
|
if parsed, err := strconv.Atoi(val); err == nil {
|
|
return parsed
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func (be *BlocklistExporter) getDurationParam(r *http.Request, param string, defaultValue time.Duration) time.Duration {
|
|
if val := r.URL.Query().Get(param); val != "" {
|
|
if parsed, err := time.ParseDuration(val); err == nil {
|
|
return parsed
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func (be *BlocklistExporter) getBoolParam(r *http.Request, param string, defaultValue bool) bool {
|
|
if val := r.URL.Query().Get(param); val != "" {
|
|
return val == "true" || val == "1"
|
|
}
|
|
return defaultValue
|
|
}
|