514 lines
14 KiB
Go
514 lines
14 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ThreatAPI handles HTTP endpoints for threat analysis
|
|
type ThreatAPI struct {
|
|
analyzer *ThreatAnalyzer
|
|
}
|
|
|
|
// NewThreatAPI creates a new threat API instance
|
|
func NewThreatAPI(analyzer *ThreatAnalyzer) *ThreatAPI {
|
|
return &ThreatAPI{analyzer: analyzer}
|
|
}
|
|
|
|
// RegisterRoutes registers all threat analysis API routes
|
|
func (api *ThreatAPI) RegisterRoutes(mux *http.ServeMux) {
|
|
// IP Reports and Analysis
|
|
mux.HandleFunc("/api/threat/reports", api.handleIPReports)
|
|
mux.HandleFunc("/api/threat/ip/", api.handleIPAnalysis)
|
|
|
|
// Threat Rules Management
|
|
mux.HandleFunc("/api/threat/rules", api.handleThreatRules)
|
|
mux.HandleFunc("/api/threat/rules/", api.handleThreatRule)
|
|
|
|
// Blocklist Management
|
|
mux.HandleFunc("/api/threat/blocklist", api.handleBlocklist)
|
|
mux.HandleFunc("/api/threat/block", api.handleBlockIP)
|
|
mux.HandleFunc("/api/threat/unblock", api.handleUnblockIP)
|
|
|
|
// Threat Events
|
|
mux.HandleFunc("/api/threat/events", api.handleThreatEvents)
|
|
|
|
// Statistics
|
|
mux.HandleFunc("/api/threat/stats", api.handleThreatStats)
|
|
}
|
|
|
|
// handleIPReports handles GET /api/threat/reports with filtering
|
|
func (api *ThreatAPI) handleIPReports(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Parse query parameters for filtering
|
|
filters := make(map[string]interface{})
|
|
|
|
if service := r.URL.Query().Get("service"); service != "" {
|
|
filters["service"] = service
|
|
}
|
|
|
|
if minScore := r.URL.Query().Get("min_threat_score"); minScore != "" {
|
|
if score, err := strconv.Atoi(minScore); err == nil {
|
|
filters["min_threat_score"] = score
|
|
}
|
|
}
|
|
|
|
if blocked := r.URL.Query().Get("blocked"); blocked != "" {
|
|
filters["blocked"] = blocked == "true"
|
|
}
|
|
|
|
if limit := r.URL.Query().Get("limit"); limit != "" {
|
|
if l, err := strconv.Atoi(limit); err == nil && l > 0 {
|
|
filters["limit"] = l
|
|
}
|
|
} else {
|
|
filters["limit"] = 100 // Default limit
|
|
}
|
|
|
|
reports, err := api.analyzer.GetIPReports(filters)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to get IP reports: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"reports": reports,
|
|
"count": len(reports),
|
|
"filters": filters,
|
|
})
|
|
}
|
|
|
|
// handleIPAnalysis handles GET /api/threat/ip/{ip}
|
|
func (api *ThreatAPI) handleIPAnalysis(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Extract IP from URL path
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/threat/ip/")
|
|
if path == "" {
|
|
http.Error(w, "IP address required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
report, err := api.analyzer.AnalyzeIP(path)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to analyze IP: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(report)
|
|
}
|
|
|
|
// handleThreatRules handles GET/POST /api/threat/rules
|
|
func (api *ThreatAPI) handleThreatRules(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
rules, err := api.analyzer.GetRules()
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to get rules: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"rules": rules,
|
|
"count": len(rules),
|
|
})
|
|
|
|
case http.MethodPost:
|
|
var rule ThreatRule
|
|
if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
|
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := api.analyzer.CreateRule(rule); err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to create rule: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "created"})
|
|
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// handleThreatRule handles individual rule operations (PUT/DELETE)
|
|
func (api *ThreatAPI) handleThreatRule(w http.ResponseWriter, r *http.Request) {
|
|
// Extract rule ID from URL path
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/threat/rules/")
|
|
ruleID, err := strconv.Atoi(path)
|
|
if err != nil {
|
|
http.Error(w, "Invalid rule ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case "PUT":
|
|
var rule ThreatRule
|
|
if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
|
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
rule.ID = ruleID
|
|
if err := api.updateRule(rule); err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to update rule: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "updated"})
|
|
|
|
case "DELETE":
|
|
if err := api.deleteRule(ruleID); err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to delete rule: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "deleted"})
|
|
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// updateRule updates an existing threat rule
|
|
func (api *ThreatAPI) updateRule(rule ThreatRule) error {
|
|
query := `UPDATE threat_rules SET
|
|
name = ?, description = ?, service = ?, condition = ?,
|
|
threshold = ?, time_window = ?, action = ?, enabled = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?`
|
|
|
|
_, err := api.analyzer.db.Exec(query, rule.Name, rule.Description, rule.Service,
|
|
rule.Condition, rule.Threshold, rule.TimeWindow, rule.Action, rule.Enabled, rule.ID)
|
|
return err
|
|
}
|
|
|
|
// deleteRule deletes a threat rule
|
|
func (api *ThreatAPI) deleteRule(ruleID int) error {
|
|
query := `DELETE FROM threat_rules WHERE id = ?`
|
|
_, err := api.analyzer.db.Exec(query, ruleID)
|
|
return err
|
|
}
|
|
|
|
// handleBlocklist handles GET /api/threat/blocklist
|
|
func (api *ThreatAPI) handleBlocklist(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
blockedIPs, err := api.analyzer.GetBlockedIPs()
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to get blocklist: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"blocked_ips": blockedIPs,
|
|
"count": len(blockedIPs),
|
|
})
|
|
}
|
|
|
|
// handleBlockIP handles POST /api/threat/block
|
|
func (api *ThreatAPI) handleBlockIP(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var request struct {
|
|
IP string `json:"ip"`
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if request.IP == "" {
|
|
http.Error(w, "IP address required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := api.analyzer.blockIP(request.IP, 0); err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to block IP: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"status": "blocked",
|
|
"ip": request.IP,
|
|
})
|
|
}
|
|
|
|
// handleUnblockIP handles POST /api/threat/unblock
|
|
func (api *ThreatAPI) handleUnblockIP(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var request struct {
|
|
IP string `json:"ip"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if request.IP == "" {
|
|
http.Error(w, "IP address required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := api.analyzer.UnblockIP(request.IP); err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to unblock IP: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"status": "unblocked",
|
|
"ip": request.IP,
|
|
})
|
|
}
|
|
|
|
// handleThreatEvents handles GET /api/threat/events
|
|
func (api *ThreatAPI) handleThreatEvents(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Parse query parameters
|
|
ip := r.URL.Query().Get("ip")
|
|
service := r.URL.Query().Get("service")
|
|
eventType := r.URL.Query().Get("event_type")
|
|
severity := r.URL.Query().Get("severity")
|
|
|
|
limit := 100
|
|
if l := r.URL.Query().Get("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
|
|
events, err := api.getThreatEvents(ip, service, eventType, severity, limit)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to get threat events: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"events": events,
|
|
"count": len(events),
|
|
})
|
|
}
|
|
|
|
// getThreatEvents retrieves threat events with filtering
|
|
func (api *ThreatAPI) getThreatEvents(ip, service, eventType, severity string, limit int) ([]ThreatEvent, error) {
|
|
query := `SELECT id, ip, service, event_type, severity, count, first_seen, last_seen, details, rule_id, blocked, created_at
|
|
FROM threat_events WHERE 1=1`
|
|
|
|
var args []interface{}
|
|
var conditions []string
|
|
|
|
if ip != "" {
|
|
conditions = append(conditions, "ip = ?")
|
|
args = append(args, ip)
|
|
}
|
|
|
|
if service != "" {
|
|
conditions = append(conditions, "service = ?")
|
|
args = append(args, service)
|
|
}
|
|
|
|
if eventType != "" {
|
|
conditions = append(conditions, "event_type = ?")
|
|
args = append(args, eventType)
|
|
}
|
|
|
|
if severity != "" {
|
|
conditions = append(conditions, "severity = ?")
|
|
args = append(args, severity)
|
|
}
|
|
|
|
if len(conditions) > 0 {
|
|
query += " AND " + strings.Join(conditions, " AND ")
|
|
}
|
|
|
|
query += " ORDER BY last_seen DESC LIMIT ?"
|
|
args = append(args, limit)
|
|
|
|
rows, err := api.analyzer.db.Query(query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var events []ThreatEvent
|
|
for rows.Next() {
|
|
var event ThreatEvent
|
|
var detailsJSON string
|
|
var ruleID *int
|
|
|
|
err := rows.Scan(&event.ID, &event.IP, &event.Service, &event.EventType, &event.Severity,
|
|
&event.Count, &event.FirstSeen, &event.LastSeen, &detailsJSON, &ruleID, &event.Blocked, &event.CreatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if detailsJSON != "" {
|
|
json.Unmarshal([]byte(detailsJSON), &event.Details)
|
|
}
|
|
|
|
event.RuleID = ruleID
|
|
events = append(events, event)
|
|
}
|
|
|
|
return events, nil
|
|
}
|
|
|
|
// handleThreatStats handles GET /api/threat/stats
|
|
func (api *ThreatAPI) handleThreatStats(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
stats, err := api.getThreatStats()
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Failed to get threat stats: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(stats)
|
|
}
|
|
|
|
// getThreatStats calculates various threat statistics
|
|
func (api *ThreatAPI) getThreatStats() (map[string]interface{}, error) {
|
|
stats := make(map[string]interface{})
|
|
|
|
// Total unique IPs
|
|
var totalIPs int
|
|
err := api.analyzer.db.QueryRow("SELECT COUNT(*) FROM ip_analysis").Scan(&totalIPs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stats["total_ips"] = totalIPs
|
|
|
|
// Blocked IPs
|
|
var blockedIPs int
|
|
err = api.analyzer.db.QueryRow("SELECT COUNT(*) FROM ip_analysis WHERE is_blocked = 1").Scan(&blockedIPs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stats["blocked_ips"] = blockedIPs
|
|
|
|
// Total threat events
|
|
var totalEvents int
|
|
err = api.analyzer.db.QueryRow("SELECT COUNT(*) FROM threat_events").Scan(&totalEvents)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stats["total_threat_events"] = totalEvents
|
|
|
|
// Events by severity
|
|
severityQuery := `SELECT severity, COUNT(*) FROM threat_events GROUP BY severity`
|
|
rows, err := api.analyzer.db.Query(severityQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
severityStats := make(map[string]int)
|
|
for rows.Next() {
|
|
var severity string
|
|
var count int
|
|
if err := rows.Scan(&severity, &count); err != nil {
|
|
return nil, err
|
|
}
|
|
severityStats[severity] = count
|
|
}
|
|
stats["events_by_severity"] = severityStats
|
|
|
|
// Events by type
|
|
typeQuery := `SELECT event_type, COUNT(*) FROM threat_events GROUP BY event_type`
|
|
rows, err = api.analyzer.db.Query(typeQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
typeStats := make(map[string]int)
|
|
for rows.Next() {
|
|
var eventType string
|
|
var count int
|
|
if err := rows.Scan(&eventType, &count); err != nil {
|
|
return nil, err
|
|
}
|
|
typeStats[eventType] = count
|
|
}
|
|
stats["events_by_type"] = typeStats
|
|
|
|
// Top threat IPs (last 24 hours)
|
|
topIPsQuery := `SELECT ip, threat_score FROM ip_analysis
|
|
WHERE last_seen >= ? AND threat_score > 0
|
|
ORDER BY threat_score DESC LIMIT 10`
|
|
|
|
yesterday := time.Now().Add(-24 * time.Hour)
|
|
rows, err = api.analyzer.db.Query(topIPsQuery, yesterday)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var topIPs []map[string]interface{}
|
|
for rows.Next() {
|
|
var ip string
|
|
var score int
|
|
if err := rows.Scan(&ip, &score); err != nil {
|
|
return nil, err
|
|
}
|
|
topIPs = append(topIPs, map[string]interface{}{
|
|
"ip": ip,
|
|
"score": score,
|
|
})
|
|
}
|
|
stats["top_threat_ips"] = topIPs
|
|
|
|
// Active rules count
|
|
var activeRules int
|
|
err = api.analyzer.db.QueryRow("SELECT COUNT(*) FROM threat_rules WHERE enabled = 1").Scan(&activeRules)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stats["active_rules"] = activeRules
|
|
|
|
return stats, nil
|
|
}
|