Files
honeydany/app/dashboard/api.go
T

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
}