From 1c1818b29ca46fe37814d3edcff958765bcde55b Mon Sep 17 00:00:00 2001 From: nahakubuilder Date: Sun, 28 Sep 2025 15:28:39 +0100 Subject: [PATCH] added authentication and API --- app.db | Bin 0 -> 90112 bytes app/dashboard/api.go | 513 +++++++++++++++++++++++++ app/dashboard/auth.go | 482 +++++++++++++++++++++++ app/dashboard/csrf.go | 312 +++++++++++++++ app/dashboard/integration.go | 391 +++++++++++++++++++ app/dashboard/middleware.go | 349 +++++++++++++++++ app/dashboard/threat_analysis.go | 581 ++++++++++++++++++++++++++++ app/dashboard/user_api.go | 553 +++++++++++++++++++++++++++ app/services.go | 50 ++- app/templates/layout.html | 12 + app/templates/threat_reports.html | 395 +++++++++++++++++++ app/templates/threat_rules.html | 373 ++++++++++++++++++ app/templates/users.html | 608 ++++++++++++++++++++++++++++++ app/web.go | 185 +++++++-- 14 files changed, 4762 insertions(+), 42 deletions(-) create mode 100644 app.db create mode 100644 app/dashboard/api.go create mode 100644 app/dashboard/auth.go create mode 100644 app/dashboard/csrf.go create mode 100644 app/dashboard/integration.go create mode 100644 app/dashboard/middleware.go create mode 100644 app/dashboard/threat_analysis.go create mode 100644 app/dashboard/user_api.go create mode 100644 app/templates/threat_reports.html create mode 100644 app/templates/threat_rules.html create mode 100644 app/templates/users.html diff --git a/app.db b/app.db new file mode 100644 index 0000000000000000000000000000000000000000..6f2614c6a31bbeee85095cee5a6913020e3a684d GIT binary patch literal 90112 zcmeI*%X8bt9S3kgq$N=g!;bARJ*b1R>`JUG2~Uzx+i7Hpb`e^w??tfE=34G>0BL(_4D!blPJtJ+#-Pw=MvJBJ{AMx|77rm$F1+ zu~;lVzum=)q|0wDJ3bX_O?QX*qRL(1c%FM*6giH2js3pLe&fe^_97imuxGyO^=U6( z<7R$&c~tnEyTbpPJ3k`)Vf6RIpAHQT|0?s_j5PSWL9PEcgPHW_{a*jg)W!54`#$cg z@c&_1@#qL_Ug^)uS^lBZu<2vReq7!4sOyD)8)S!uQ?D*;l;$fXacOn2v?U(TB(ANB zVaCbeZ?IVI-sS%6>=?h>DVDUH>I1s(Rkw+^9Zy_2R&YEg%odazCrq-1!^9l9)SuPI z`1d+Rd(`usX2Yw}$1TTY6Yc5C$4Zaq#5sc%#A%c5rK`lxZN0)!#=21AOjW=6fHvZ( zOQ)|O%$F!WOvyb}FAQYI#`q5mpO|$TZ(aO+v0Y+i{^oM2v+d&KP$rXcY!-k@>2_&D zT;Et)ncui4-YMM^=Qk^BORH?TmC|bEMvyTGfLJMQRm9ab_Pe>f93}^Vk4Sy@$UJd# zb?L3mQYT9gpA(i2B3tdMLZavvb$6U72+^X8rCalx%N23r?RO@^l8NOzkEnQaZEd+U zzbeM_<#6Vqytv5PS!HP@s*dGS;!~SNq?6uRa$$30gKcm%$Xu?>udIh9)rsc^ZP)HB z5GLPR+bAvFUJbp%Hp;dnCvKE(l~@@IrEmanB&w=n8Vn0#W>uC zX9gf@Zi$$8$Mpo@WZO8qw9~2*+YWkL81cg@lKZsbAEv!9z3?phn69eCuP)4<|3VAM z@dCmVb9f+|&GPS$hp`Hrz3|USB1&OWCq!SYCkD$L?~UD&X%WxcYr6IcJ&x`Wr+x_O zHtR=wZrqC|g7tiXes=aK@3!p!r3coV_nk(k3kE$XH`1T|LH4vB6xvqh@M-kDp@D2^ zoIe=v4Ddl)YmoZB=Xh0rI~WwIp4D_|SNd=}R)@Q&u9=C2f*#&AZ?d~U*7eH^x&6#Q zc42})xYDUWI}d%tnzmOB2N{pD*HK-fKP_jQraiWa)6?RcWfnH#x@14t#kNJlR(Q(FSX(vgP3r;M7aiT}MYX9pF56iJ z?WogJ!diA(Lo)e3ZB|)%B-p@cREVn;ROn0{HRzCfe}8sq{Im|)MPHS>CI+6?(gWFv z3H}$u;b56)8HtH=iBKkHopBIl4Jxr?3z6bReSA*X8>{|)>&R%?UhENd9e@9X60PPg z+i%6=dfafL1C$d&mE3-no9-AkiJ0 zs@y%5VyBmb!71h_`%RxubHXo#^!cB$7kEGb0uX=z1Rwwb2tWV=5P(3>3hbpvxT!mr zCU`!ZQPo?Ob@8UV%WewaYPuF(q&~Z5CkClzn96PyTU3lM&(-`^;&Pqv{qW+5A}RW` zWK1hXQP$?9{G6_xkaUnvjd0iR3$xLh*PE^{mMy}r61wfxwt^%tNOBtYMPj*4b~|Hd zx9&SFcCkIa2>9Lhi>l3TVYwc=d~{AYOEtK@5pLq%mFcJ&Z&cW}hSgx}Y}X@9i|a9K zxn!>%RLG>;fv@*5|FAV@ta1RlFx z+kM4!=qw=^Vy!wYe8dSK34as*B>byq8#y)%0uX=z1Rwwb2tWV=5P$##An?)&TuM#x zN3PSPFQl&X-B(aj7gG~_;#x-EE2*i>k!uUV{r{Bk7f$%M@DKKc2LvDh0SG_<0uX=z z1Rwwb2tWV=FOR@*Dm9jgKkPc#m&#_skM3RI*+qix&jF-^^MCfCe>@-n0SG_<0uX=z z1Rwwb2tWV=5a?k6od5T5aIt9+fB*y_009U<00Izz00bZafwn;K`G4I1M{|Gx1Rwwb z2tWV=5P$##AOHaf^tJ%b|9d;W*g6P600Izz00bZa0SG_<0uX>eAb|6Kv;zo000Izz z00bZa0SG_<0uX>e?+f7kzxQK|Erb9BAOHafKmY;|fB*y_009W#{2y%q0uX=z1Rwwb z2tWV=5P$##Akg~)!T103g3Ymict8LG5P$##AOHafKmY;|fB*y_@B#t{-{Ob4?7_LI zY(`a8V(&POYYMq0OV@t%lYC=q>COFzcPgu`+vT_9+ReLLYYPSa&83C=x3u;3MXz4l zxL4Y67w$S+w|~4mKQl8E7Vvt#X_5MN)APegMN;%>$(UA(qO8p+%A6)YKS`g{)ZqR< zFMP(ae|SIu0uX=z1Rwwb2tWV=5P$##AkbR^gQ;vLyl@cQ|L+_9Gk5-<><=CgfB*y_ z009U<00Izz00bcL5(zxH+&8q6IT$UgQwMiVOE!(XCCPbPr>bnIid9hMf@a8a(NtmYb(v4I`ul4)n_Dea!Z!K zCoVgU-N)kNV!oQ!a^ifeRi}5UdB^c*b+s^~=Ecc7Z&X&6Z-{m00Tpjk>p?RoE^NEa z9Xcy(LDB3-ERz~>o%SM6F7t;hRA&?gBAY~0YL;1(ifS=$D8#l5TQQ3TVkmY|w^`+g zQjqehoY!PSlWb}mx+c{O&92pC%{Gi;zDRWm5 zHsljof-HwDs#45LUkXcb|G!W8m=iu0{{9j*a#R%p5P$##AOHafKmY;|fB*y_@J$f7 zmRjMTcWtL{JhhT}-j$B%{C}4dJ`r}m32@j(2tWV=5P$##AOHafKmY;|fWVg~P)Z++ z@-O<~HGn~p1&)&AHnxs>Mnnjh=3JS4gqS1nGO1dsdCN0{gT9gd4rWZ|(6l literal 0 HcmV?d00001 diff --git a/app/dashboard/api.go b/app/dashboard/api.go new file mode 100644 index 0000000..50c342e --- /dev/null +++ b/app/dashboard/api.go @@ -0,0 +1,513 @@ +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 +} diff --git a/app/dashboard/auth.go b/app/dashboard/auth.go new file mode 100644 index 0000000..d9ac434 --- /dev/null +++ b/app/dashboard/auth.go @@ -0,0 +1,482 @@ +package dashboard + +import ( + "crypto/rand" + "crypto/sha256" + "database/sql" + "encoding/hex" + "fmt" + "log" + "time" + + "golang.org/x/crypto/bcrypt" +) + +// User represents a dashboard user +type User struct { + ID int `json:"id"` + Username string `json:"username"` + Password string `json:"-"` // Never include in JSON + Email string `json:"email"` + Role string `json:"role"` // "admin", "user", "readonly" + Active bool `json:"active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastLogin *time.Time `json:"last_login,omitempty"` +} + +// Session represents a user session +type Session struct { + ID string `json:"id"` + UserID int `json:"user_id"` + Token string `json:"-"` // Never include in JSON + ExpiresAt time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` +} + +// APIKey represents an API key for programmatic access +type APIKey struct { + ID int `json:"id"` + Name string `json:"name"` + Key string `json:"key"` + KeyHash string `json:"-"` // Never include in JSON + UserID int `json:"user_id"` + Permissions string `json:"permissions"` // JSON array of permissions + Active bool `json:"active"` + ExpiresAt *time.Time `json:"expires_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + LastUsed *time.Time `json:"last_used,omitempty"` +} + +// AuthManager handles authentication and authorization +type AuthManager struct { + db *sql.DB +} + +// NewAuthManager creates a new authentication manager +func NewAuthManager(db *sql.DB) (*AuthManager, error) { + am := &AuthManager{db: db} + if err := am.initDatabase(); err != nil { + return nil, fmt.Errorf("failed to initialize auth database: %w", err) + } + return am, nil +} + +// initDatabase creates the authentication tables +func (am *AuthManager) initDatabase() error { + queries := []string{ + `CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + email TEXT, + role TEXT NOT NULL DEFAULT 'user', + active BOOLEAN DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_login DATETIME + )`, + `CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + user_id INTEGER NOT NULL, + token TEXT NOT NULL UNIQUE, + expires_at DATETIME NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + ip_address TEXT, + user_agent TEXT, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + )`, + `CREATE TABLE IF NOT EXISTS api_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + key_value TEXT NOT NULL UNIQUE, + key_hash TEXT NOT NULL, + user_id INTEGER NOT NULL, + permissions TEXT DEFAULT '[]', + active BOOLEAN DEFAULT 1, + expires_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_used DATETIME, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + )`, + `CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token)`, + `CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)`, + `CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)`, + `CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)`, + } + + for _, query := range queries { + if _, err := am.db.Exec(query); err != nil { + return fmt.Errorf("failed to execute query: %s, error: %w", query, err) + } + } + + // Create default admin user if no users exist + return am.createDefaultUser() +} + +// createDefaultUser creates the default admin user if no users exist +func (am *AuthManager) createDefaultUser() error { + var count int + err := am.db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count) + if err != nil { + return err + } + + if count == 0 { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost) + if err != nil { + return err + } + + query := `INSERT INTO users (username, password, email, role, active) VALUES (?, ?, ?, ?, ?)` + _, err = am.db.Exec(query, "admin", string(hashedPassword), "admin@localhost", "admin", true) + if err != nil { + return err + } + + log.Println("Created default admin user (username: admin, password: password)") + } + + return nil +} + +// CreateUser creates a new user +func (am *AuthManager) CreateUser(username, password, email, role string) (*User, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + query := `INSERT INTO users (username, password, email, role, active) VALUES (?, ?, ?, ?, ?)` + result, err := am.db.Exec(query, username, string(hashedPassword), email, role, true) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + return am.GetUserByID(int(id)) +} + +// GetUserByID retrieves a user by ID +func (am *AuthManager) GetUserByID(id int) (*User, error) { + user := &User{} + query := `SELECT id, username, password, email, role, active, created_at, updated_at, last_login + FROM users WHERE id = ?` + + var lastLogin sql.NullTime + err := am.db.QueryRow(query, id).Scan(&user.ID, &user.Username, &user.Password, &user.Email, + &user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt, &lastLogin) + + if err != nil { + return nil, err + } + + if lastLogin.Valid { + user.LastLogin = &lastLogin.Time + } + + return user, nil +} + +// GetUserByUsername retrieves a user by username +func (am *AuthManager) GetUserByUsername(username string) (*User, error) { + user := &User{} + query := `SELECT id, username, password, email, role, active, created_at, updated_at, last_login + FROM users WHERE username = ?` + + var lastLogin sql.NullTime + err := am.db.QueryRow(query, username).Scan(&user.ID, &user.Username, &user.Password, &user.Email, + &user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt, &lastLogin) + + if err != nil { + return nil, err + } + + if lastLogin.Valid { + user.LastLogin = &lastLogin.Time + } + + return user, nil +} + +// AuthenticateUser validates username and password +func (am *AuthManager) AuthenticateUser(username, password string) (*User, error) { + user, err := am.GetUserByUsername(username) + if err != nil { + return nil, err + } + + if !user.Active { + return nil, fmt.Errorf("user account is disabled") + } + + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) + if err != nil { + return nil, fmt.Errorf("invalid credentials") + } + + // Update last login time + _, err = am.db.Exec("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?", user.ID) + if err != nil { + log.Printf("Failed to update last login for user %s: %v", username, err) + } + + return user, nil +} + +// CreateSession creates a new session for a user +func (am *AuthManager) CreateSession(userID int, ipAddress, userAgent string) (*Session, error) { + sessionID := generateRandomString(32) + token := generateRandomString(64) + expiresAt := time.Now().Add(24 * time.Hour) // 24 hour session + + query := `INSERT INTO sessions (id, user_id, token, expires_at, ip_address, user_agent) + VALUES (?, ?, ?, ?, ?, ?)` + + _, err := am.db.Exec(query, sessionID, userID, token, expiresAt, ipAddress, userAgent) + if err != nil { + return nil, err + } + + return &Session{ + ID: sessionID, + UserID: userID, + Token: token, + ExpiresAt: expiresAt, + CreatedAt: time.Now(), + IPAddress: ipAddress, + UserAgent: userAgent, + }, nil +} + +// ValidateSession validates a session token +func (am *AuthManager) ValidateSession(token string) (*User, error) { + var session Session + query := `SELECT s.id, s.user_id, s.expires_at, u.id, u.username, u.email, u.role, u.active + FROM sessions s + JOIN users u ON s.user_id = u.id + WHERE s.token = ? AND s.expires_at > CURRENT_TIMESTAMP AND u.active = 1` + + var user User + err := am.db.QueryRow(query, token).Scan(&session.ID, &session.UserID, &session.ExpiresAt, + &user.ID, &user.Username, &user.Email, &user.Role, &user.Active) + + if err != nil { + return nil, err + } + + return &user, nil +} + +// DeleteSession removes a session +func (am *AuthManager) DeleteSession(token string) error { + _, err := am.db.Exec("DELETE FROM sessions WHERE token = ?", token) + return err +} + +// CleanupExpiredSessions removes expired sessions +func (am *AuthManager) CleanupExpiredSessions() error { + _, err := am.db.Exec("DELETE FROM sessions WHERE expires_at < CURRENT_TIMESTAMP") + return err +} + +// CreateAPIKey creates a new API key +func (am *AuthManager) CreateAPIKey(name string, userID int, permissions []string, expiresAt *time.Time) (*APIKey, error) { + key := "hd_" + generateRandomString(40) // Prefix for identification + keyHash := hashString(key) + + permissionsJSON := "[]" + if len(permissions) > 0 { + // Simple JSON encoding for permissions + permissionsJSON = fmt.Sprintf(`["%s"]`, permissions[0]) + for i := 1; i < len(permissions); i++ { + permissionsJSON = permissionsJSON[:len(permissionsJSON)-1] + fmt.Sprintf(`,"%s"]`, permissions[i]) + } + } + + query := `INSERT INTO api_keys (name, key_value, key_hash, user_id, permissions, active, expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?)` + + result, err := am.db.Exec(query, name, key, keyHash, userID, permissionsJSON, true, expiresAt) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + return &APIKey{ + ID: int(id), + Name: name, + Key: key, + KeyHash: keyHash, + UserID: userID, + Permissions: permissionsJSON, + Active: true, + ExpiresAt: expiresAt, + CreatedAt: time.Now(), + }, nil +} + +// ValidateAPIKey validates an API key and returns the associated user +func (am *AuthManager) ValidateAPIKey(key string) (*User, error) { + keyHash := hashString(key) + + var apiKey APIKey + var user User + query := `SELECT a.id, a.user_id, a.active, a.expires_at, u.id, u.username, u.email, u.role, u.active + FROM api_keys a + JOIN users u ON a.user_id = u.id + WHERE a.key_hash = ? AND a.active = 1 AND u.active = 1` + + var expiresAt sql.NullTime + err := am.db.QueryRow(query, keyHash).Scan(&apiKey.ID, &apiKey.UserID, &apiKey.Active, &expiresAt, + &user.ID, &user.Username, &user.Email, &user.Role, &user.Active) + + if err != nil { + return nil, err + } + + // Check if key is expired + if expiresAt.Valid && expiresAt.Time.Before(time.Now()) { + return nil, fmt.Errorf("API key expired") + } + + // Update last used timestamp + _, err = am.db.Exec("UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?", apiKey.ID) + if err != nil { + log.Printf("Failed to update API key last used: %v", err) + } + + return &user, nil +} + +// GetUsers retrieves all users +func (am *AuthManager) GetUsers() ([]User, error) { + query := `SELECT id, username, email, role, active, created_at, updated_at, last_login + FROM users ORDER BY created_at DESC` + + rows, err := am.db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var users []User + for rows.Next() { + var user User + var lastLogin sql.NullTime + + err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.Role, &user.Active, + &user.CreatedAt, &user.UpdatedAt, &lastLogin) + if err != nil { + return nil, err + } + + if lastLogin.Valid { + user.LastLogin = &lastLogin.Time + } + + users = append(users, user) + } + + return users, nil +} + +// GetAPIKeys retrieves API keys for a user +func (am *AuthManager) GetAPIKeys(userID int) ([]APIKey, error) { + query := `SELECT id, name, key_value, user_id, permissions, active, expires_at, created_at, last_used + FROM api_keys WHERE user_id = ? ORDER BY created_at DESC` + + rows, err := am.db.Query(query, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var keys []APIKey + for rows.Next() { + var key APIKey + var expiresAt, lastUsed sql.NullTime + + err := rows.Scan(&key.ID, &key.Name, &key.Key, &key.UserID, &key.Permissions, + &key.Active, &expiresAt, &key.CreatedAt, &lastUsed) + if err != nil { + return nil, err + } + + if expiresAt.Valid { + key.ExpiresAt = &expiresAt.Time + } + if lastUsed.Valid { + key.LastUsed = &lastUsed.Time + } + + // Mask the key for security (show only first 8 chars) + if len(key.Key) > 8 { + key.Key = key.Key[:8] + "..." + key.Key[len(key.Key)-4:] + } + + keys = append(keys, key) + } + + return keys, nil +} + +// UpdateUser updates user information +func (am *AuthManager) UpdateUser(id int, username, email, role string, active bool) error { + query := `UPDATE users SET username = ?, email = ?, role = ?, active = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?` + _, err := am.db.Exec(query, username, email, role, active, id) + return err +} + +// UpdateUserPassword updates a user's password +func (am *AuthManager) UpdateUserPassword(id int, newPassword string) error { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + + query := `UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?` + _, err = am.db.Exec(query, string(hashedPassword), id) + return err +} + +// DeleteUser deletes a user (and associated sessions/API keys) +func (am *AuthManager) DeleteUser(id int) error { + _, err := am.db.Exec("DELETE FROM users WHERE id = ?", id) + return err +} + +// RevokeAPIKey revokes an API key +func (am *AuthManager) RevokeAPIKey(id int) error { + _, err := am.db.Exec("UPDATE api_keys SET active = 0 WHERE id = ?", id) + return err +} + +// Helper functions + +// generateRandomString generates a random string of specified length +func generateRandomString(length int) string { + bytes := make([]byte, length/2) + if _, err := rand.Read(bytes); err != nil { + panic(err) + } + return hex.EncodeToString(bytes) +} + +// hashString creates a SHA256 hash of a string +func hashString(s string) string { + hash := sha256.Sum256([]byte(s)) + return hex.EncodeToString(hash[:]) +} diff --git a/app/dashboard/csrf.go b/app/dashboard/csrf.go new file mode 100644 index 0000000..359da0e --- /dev/null +++ b/app/dashboard/csrf.go @@ -0,0 +1,312 @@ +package dashboard + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "net/http" + "sync" + "time" +) + +// CSRFManager handles CSRF token generation and validation +type CSRFManager struct { + tokens map[string]time.Time + mutex sync.RWMutex +} + +// NewCSRFManager creates a new CSRF manager +func NewCSRFManager() *CSRFManager { + cm := &CSRFManager{ + tokens: make(map[string]time.Time), + } + + // Start cleanup goroutine + go cm.cleanupExpiredTokens() + + return cm +} + +// GenerateToken generates a new CSRF token +func (cm *CSRFManager) GenerateToken() (string, error) { + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + + token := base64.URLEncoding.EncodeToString(bytes) + + cm.mutex.Lock() + cm.tokens[token] = time.Now().Add(1 * time.Hour) // Token expires in 1 hour + cm.mutex.Unlock() + + return token, nil +} + +// ValidateToken validates a CSRF token +func (cm *CSRFManager) ValidateToken(token string) bool { + if token == "" { + return false + } + + cm.mutex.RLock() + expiresAt, exists := cm.tokens[token] + cm.mutex.RUnlock() + + if !exists { + return false + } + + if time.Now().After(expiresAt) { + // Token expired, remove it + cm.mutex.Lock() + delete(cm.tokens, token) + cm.mutex.Unlock() + return false + } + + return true +} + +// ConsumeToken validates and removes a CSRF token (one-time use) +func (cm *CSRFManager) ConsumeToken(token string) bool { + if token == "" { + return false + } + + cm.mutex.Lock() + defer cm.mutex.Unlock() + + expiresAt, exists := cm.tokens[token] + if !exists { + return false + } + + if time.Now().After(expiresAt) { + delete(cm.tokens, token) + return false + } + + // Remove token after successful validation (one-time use) + delete(cm.tokens, token) + return true +} + +// cleanupExpiredTokens periodically removes expired tokens +func (cm *CSRFManager) cleanupExpiredTokens() { + ticker := time.NewTicker(10 * time.Minute) + defer ticker.Stop() + + for range ticker.C { + now := time.Now() + cm.mutex.Lock() + for token, expiresAt := range cm.tokens { + if now.After(expiresAt) { + delete(cm.tokens, token) + } + } + cm.mutex.Unlock() + } +} + +// CSRFMiddleware provides CSRF protection for HTTP handlers +func (cm *CSRFManager) CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Skip CSRF check for GET, HEAD, OPTIONS requests + if r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" { + next(w, r) + return + } + + // Get CSRF token from header or form + token := r.Header.Get("X-CSRF-Token") + if token == "" { + token = r.FormValue("csrf_token") + } + + if !cm.ConsumeToken(token) { + http.Error(w, "Invalid CSRF token", http.StatusForbidden) + return + } + + next(w, r) + } +} + +// AddCSRFTokenToResponse adds a CSRF token to the response +func (cm *CSRFManager) AddCSRFTokenToResponse(w http.ResponseWriter, data map[string]interface{}) error { + token, err := cm.GenerateToken() + if err != nil { + return fmt.Errorf("failed to generate CSRF token: %w", err) + } + + data["CSRFToken"] = token + w.Header().Set("X-CSRF-Token", token) + + return nil +} + +// InputValidator provides input validation utilities +type InputValidator struct{} + +// NewInputValidator creates a new input validator +func NewInputValidator() *InputValidator { + return &InputValidator{} +} + +// ValidateUsername validates username format +func (iv *InputValidator) ValidateUsername(username string) error { + if len(username) < 3 { + return fmt.Errorf("username must be at least 3 characters long") + } + if len(username) > 50 { + return fmt.Errorf("username must be less than 50 characters long") + } + + // Check for valid characters (alphanumeric, underscore, hyphen) + for _, char := range username { + if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || + (char >= '0' && char <= '9') || char == '_' || char == '-') { + return fmt.Errorf("username can only contain letters, numbers, underscore, and hyphen") + } + } + + return nil +} + +// ValidatePassword validates password strength +func (iv *InputValidator) ValidatePassword(password string) error { + if len(password) < 8 { + return fmt.Errorf("password must be at least 8 characters long") + } + if len(password) > 128 { + return fmt.Errorf("password must be less than 128 characters long") + } + + hasUpper := false + hasLower := false + hasDigit := false + hasSpecial := false + + for _, char := range password { + switch { + case char >= 'A' && char <= 'Z': + hasUpper = true + case char >= 'a' && char <= 'z': + hasLower = true + case char >= '0' && char <= '9': + hasDigit = true + case char >= 32 && char <= 126: // Printable ASCII + if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) { + hasSpecial = true + } + default: + return fmt.Errorf("password contains invalid characters") + } + } + + if !hasUpper { + return fmt.Errorf("password must contain at least one uppercase letter") + } + if !hasLower { + return fmt.Errorf("password must contain at least one lowercase letter") + } + if !hasDigit { + return fmt.Errorf("password must contain at least one digit") + } + if !hasSpecial { + return fmt.Errorf("password must contain at least one special character") + } + + return nil +} + +// ValidateEmail validates email format (basic validation) +func (iv *InputValidator) ValidateEmail(email string) error { + if email == "" { + return nil // Email is optional + } + + if len(email) > 254 { + return fmt.Errorf("email address is too long") + } + + // Basic email validation + atCount := 0 + dotAfterAt := false + + for i, char := range email { + if char == '@' { + atCount++ + if i == 0 || i == len(email)-1 { + return fmt.Errorf("invalid email format") + } + } else if char == '.' && atCount == 1 { + dotAfterAt = true + } + } + + if atCount != 1 || !dotAfterAt { + return fmt.Errorf("invalid email format") + } + + return nil +} + +// ValidateRole validates user role +func (iv *InputValidator) ValidateRole(role string) error { + validRoles := map[string]bool{ + "admin": true, + "user": true, + "readonly": true, + } + + if !validRoles[role] { + return fmt.Errorf("invalid role: must be admin, user, or readonly") + } + + return nil +} + +// ValidateAPIKeyName validates API key name +func (iv *InputValidator) ValidateAPIKeyName(name string) error { + if len(name) < 1 { + return fmt.Errorf("API key name is required") + } + if len(name) > 100 { + return fmt.Errorf("API key name must be less than 100 characters") + } + + // Check for valid characters + for _, char := range name { + if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || + (char >= '0' && char <= '9') || char == '_' || char == '-' || char == ' ') { + return fmt.Errorf("API key name can only contain letters, numbers, underscore, hyphen, and spaces") + } + } + + return nil +} + +// SanitizeString removes potentially dangerous characters from strings +func (iv *InputValidator) SanitizeString(input string) string { + // Remove null bytes and control characters + result := "" + for _, char := range input { + if char >= 32 && char <= 126 { // Printable ASCII only + result += string(char) + } + } + return result +} + +// ValidateInteger validates integer input within range +func (iv *InputValidator) ValidateInteger(value, min, max int) error { + if value < min { + return fmt.Errorf("value must be at least %d", min) + } + if value > max { + return fmt.Errorf("value must be at most %d", max) + } + return nil +} diff --git a/app/dashboard/integration.go b/app/dashboard/integration.go new file mode 100644 index 0000000..1bf73f4 --- /dev/null +++ b/app/dashboard/integration.go @@ -0,0 +1,391 @@ +package dashboard + +import ( + "encoding/json" + "log" + "strings" + "time" +) + +// ThreatManager integrates threat analysis with the main application +type ThreatManager struct { + analyzer *ThreatAnalyzer + api *ThreatAPI + authManager *AuthManager + securityManager *SecurityManager + userAPI *UserAPI +} + +// NewThreatManager creates a new threat manager instance +func NewThreatManager(dbPath string) (*ThreatManager, error) { + analyzer, err := NewThreatAnalyzer(dbPath) + if err != nil { + return nil, err + } + + // Initialize authentication manager with the same database + authManager, err := NewAuthManager(analyzer.db) + if err != nil { + return nil, err + } + + // Initialize security manager + securityManager := NewSecurityManager(authManager) + + // Initialize APIs + api := NewThreatAPI(analyzer) + userAPI := NewUserAPI(authManager, securityManager) + + return &ThreatManager{ + analyzer: analyzer, + api: api, + authManager: authManager, + securityManager: securityManager, + userAPI: userAPI, + }, nil +} + +// ProcessHoneypotRecord processes a honeypot log record for threat analysis +func (tm *ThreatManager) ProcessHoneypotRecord(timestamp time.Time, remoteAddr, remotePort, service string, details map[string]interface{}, rawPayload string) { + // Convert to LogRecord format + record := LogRecord{ + IP: remoteAddr, + Service: service, + Timestamp: timestamp, + Details: details, + } + + // Add additional analysis data + if record.Details == nil { + record.Details = make(map[string]interface{}) + } + + record.Details["remote_port"] = remotePort + record.Details["raw_payload_length"] = len(rawPayload) + + // Analyze payload for suspicious patterns + if suspiciousPatterns := analyzeSuspiciousPatterns(rawPayload, service); len(suspiciousPatterns) > 0 { + record.Details["suspicious_patterns"] = suspiciousPatterns + } + + // Process the record + if err := tm.analyzer.ProcessLogRecord(record); err != nil { + log.Printf("Failed to process log record for threat analysis: %v", err) + } +} + +// analyzeSuspiciousPatterns detects suspicious patterns in payloads +func analyzeSuspiciousPatterns(payload, service string) []string { + var patterns []string + + // Common attack patterns + suspiciousStrings := []string{ + "admin", "root", "administrator", "test", "guest", "user", + "password", "123456", "qwerty", "letmein", "welcome", + "../", "../../", "/etc/passwd", "/etc/shadow", + " + + + +
+
+
+

+ Sign in to Honeypot Dashboard +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+ + + `)) +} + +// handleLogin processes login form submission +func (sm *SecurityManager) handleLogin(w http.ResponseWriter, r *http.Request) { + // Parse form data + err := r.ParseForm() + if err != nil { + http.Redirect(w, r, "/login?error=Invalid+form+data", http.StatusSeeOther) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + redirect := r.FormValue("redirect") + + // Validate input + if err := sm.validator.ValidateUsername(username); err != nil { + http.Redirect(w, r, "/login?error="+err.Error(), http.StatusSeeOther) + return + } + + // Authenticate user + user, err := sm.authManager.AuthenticateUser(username, password) + if err != nil { + http.Redirect(w, r, "/login?error=Invalid+credentials", http.StatusSeeOther) + return + } + + // Create session + session, err := sm.authManager.CreateSession(user.ID, r.RemoteAddr, r.UserAgent()) + if err != nil { + http.Redirect(w, r, "/login?error=Failed+to+create+session", http.StatusSeeOther) + return + } + + // Set session cookie + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: session.Token, + Path: "/", + Expires: session.ExpiresAt, + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteStrictMode, + }) + + // Redirect to intended page or dashboard + if redirect != "" && redirect != "/login" { + http.Redirect(w, r, redirect, http.StatusSeeOther) + } else { + http.Redirect(w, r, "/", http.StatusSeeOther) + } +} diff --git a/app/dashboard/threat_analysis.go b/app/dashboard/threat_analysis.go new file mode 100644 index 0000000..e9a30d3 --- /dev/null +++ b/app/dashboard/threat_analysis.go @@ -0,0 +1,581 @@ +package dashboard + +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + "strings" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +// ThreatRule represents an automated threat detection rule +type ThreatRule struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Service string `json:"service"` // "*" for all services + Condition string `json:"condition"` // "connection_count", "auth_attempts", "scan_pattern" + Threshold int `json:"threshold"` // numeric threshold + TimeWindow int `json:"time_window"` // minutes + Action string `json:"action"` // "block", "alert", "monitor" + Enabled bool `json:"enabled"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ThreatEvent represents a detected threat event +type ThreatEvent struct { + ID int `json:"id"` + IP string `json:"ip"` + Service string `json:"service"` + EventType string `json:"event_type"` // "brute_force", "port_scan", "suspicious_activity" + Severity string `json:"severity"` // "low", "medium", "high", "critical" + Count int `json:"count"` + FirstSeen time.Time `json:"first_seen"` + LastSeen time.Time `json:"last_seen"` + Details map[string]interface{} `json:"details"` + RuleID *int `json:"rule_id,omitempty"` + Blocked bool `json:"blocked"` + CreatedAt time.Time `json:"created_at"` +} + +// IPReport represents comprehensive IP analysis +type IPReport struct { + IP string `json:"ip"` + TotalConnections int `json:"total_connections"` + TotalAuthAttempts int `json:"total_auth_attempts"` + Services []string `json:"services"` + ThreatEvents []ThreatEvent `json:"threat_events"` + ThreatScore int `json:"threat_score"` + IsBlocked bool `json:"is_blocked"` + FirstSeen time.Time `json:"first_seen"` + LastSeen time.Time `json:"last_seen"` + GeoLocation map[string]interface{} `json:"geo_location,omitempty"` +} + +// ThreatAnalyzer handles advanced threat detection and analysis +type ThreatAnalyzer struct { + db *sql.DB +} + +// NewThreatAnalyzer creates a new threat analyzer instance +func NewThreatAnalyzer(dbPath string) (*ThreatAnalyzer, error) { + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + ta := &ThreatAnalyzer{db: db} + if err := ta.initDatabase(); err != nil { + return nil, fmt.Errorf("failed to initialize database: %w", err) + } + + return ta, nil +} + +// initDatabase creates the necessary tables +func (ta *ThreatAnalyzer) initDatabase() error { + queries := []string{ + `CREATE TABLE IF NOT EXISTS threat_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + service TEXT NOT NULL, + condition TEXT NOT NULL, + threshold INTEGER NOT NULL, + time_window INTEGER NOT NULL, + action TEXT NOT NULL, + enabled BOOLEAN DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + `CREATE TABLE IF NOT EXISTS threat_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip TEXT NOT NULL, + service TEXT NOT NULL, + event_type TEXT NOT NULL, + severity TEXT NOT NULL, + count INTEGER DEFAULT 1, + first_seen DATETIME NOT NULL, + last_seen DATETIME NOT NULL, + details TEXT, -- JSON + rule_id INTEGER, + blocked BOOLEAN DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (rule_id) REFERENCES threat_rules(id) + )`, + `CREATE TABLE IF NOT EXISTS ip_analysis ( + ip TEXT PRIMARY KEY, + total_connections INTEGER DEFAULT 0, + total_auth_attempts INTEGER DEFAULT 0, + services TEXT, -- JSON array + threat_score INTEGER DEFAULT 0, + is_blocked BOOLEAN DEFAULT 0, + first_seen DATETIME, + last_seen DATETIME, + geo_location TEXT, -- JSON + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + `CREATE INDEX IF NOT EXISTS idx_threat_events_ip ON threat_events(ip)`, + `CREATE INDEX IF NOT EXISTS idx_threat_events_service ON threat_events(service)`, + `CREATE INDEX IF NOT EXISTS idx_threat_events_last_seen ON threat_events(last_seen)`, + `CREATE INDEX IF NOT EXISTS idx_ip_analysis_threat_score ON ip_analysis(threat_score DESC)`, + } + + for _, query := range queries { + if _, err := ta.db.Exec(query); err != nil { + return fmt.Errorf("failed to execute query: %s, error: %w", query, err) + } + } + + // Insert default rules if none exist + return ta.insertDefaultRules() +} + +// insertDefaultRules creates some default threat detection rules +func (ta *ThreatAnalyzer) insertDefaultRules() error { + defaultRules := []ThreatRule{ + { + Name: "SSH Brute Force", + Description: "Detect SSH brute force attempts", + Service: "ssh", + Condition: "auth_attempts", + Threshold: 10, + TimeWindow: 60, // 1 hour + Action: "block", + Enabled: true, + }, + { + Name: "HTTP Scanning", + Description: "Detect HTTP scanning/crawling behavior", + Service: "http", + Condition: "connection_count", + Threshold: 50, + TimeWindow: 30, // 30 minutes + Action: "alert", + Enabled: true, + }, + { + Name: "Port Scanner", + Description: "Detect port scanning across multiple services", + Service: "*", + Condition: "service_diversity", + Threshold: 5, // 5 different services + TimeWindow: 15, // 15 minutes + Action: "block", + Enabled: true, + }, + { + Name: "FTP Brute Force", + Description: "Detect FTP brute force attempts", + Service: "ftp", + Condition: "auth_attempts", + Threshold: 15, + TimeWindow: 60, + Action: "block", + Enabled: true, + }, + } + + for _, rule := range defaultRules { + exists, err := ta.ruleExists(rule.Name) + if err != nil { + return err + } + if !exists { + if err := ta.CreateRule(rule); err != nil { + log.Printf("Failed to create default rule %s: %v", rule.Name, err) + } + } + } + + return nil +} + +// ruleExists checks if a rule with the given name already exists +func (ta *ThreatAnalyzer) ruleExists(name string) (bool, error) { + var count int + err := ta.db.QueryRow("SELECT COUNT(*) FROM threat_rules WHERE name = ?", name).Scan(&count) + return count > 0, err +} + +// CreateRule creates a new threat detection rule +func (ta *ThreatAnalyzer) CreateRule(rule ThreatRule) error { + query := `INSERT INTO threat_rules (name, description, service, condition, threshold, time_window, action, enabled) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + + _, err := ta.db.Exec(query, rule.Name, rule.Description, rule.Service, rule.Condition, + rule.Threshold, rule.TimeWindow, rule.Action, rule.Enabled) + return err +} + +// GetRules retrieves all threat detection rules +func (ta *ThreatAnalyzer) GetRules() ([]ThreatRule, error) { + query := `SELECT id, name, description, service, condition, threshold, time_window, action, enabled, created_at, updated_at + FROM threat_rules ORDER BY created_at DESC` + + rows, err := ta.db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var rules []ThreatRule + for rows.Next() { + var rule ThreatRule + err := rows.Scan(&rule.ID, &rule.Name, &rule.Description, &rule.Service, &rule.Condition, + &rule.Threshold, &rule.TimeWindow, &rule.Action, &rule.Enabled, &rule.CreatedAt, &rule.UpdatedAt) + if err != nil { + return nil, err + } + rules = append(rules, rule) + } + + return rules, nil +} + +// AnalyzeIP performs comprehensive analysis of an IP address +func (ta *ThreatAnalyzer) AnalyzeIP(ip string) (*IPReport, error) { + report := &IPReport{IP: ip} + + // Get basic IP statistics + query := `SELECT total_connections, total_auth_attempts, services, threat_score, is_blocked, first_seen, last_seen, geo_location + FROM ip_analysis WHERE ip = ?` + + var servicesJSON, geoJSON sql.NullString + err := ta.db.QueryRow(query, ip).Scan(&report.TotalConnections, &report.TotalAuthAttempts, + &servicesJSON, &report.ThreatScore, &report.IsBlocked, &report.FirstSeen, &report.LastSeen, &geoJSON) + + if err != nil && err != sql.ErrNoRows { + return nil, err + } + + // Parse services JSON + if servicesJSON.Valid { + json.Unmarshal([]byte(servicesJSON.String), &report.Services) + } + + // Parse geo location JSON + if geoJSON.Valid { + json.Unmarshal([]byte(geoJSON.String), &report.GeoLocation) + } + + // Get threat events for this IP + report.ThreatEvents, err = ta.GetThreatEventsByIP(ip) + if err != nil { + return nil, err + } + + return report, nil +} + +// GetThreatEventsByIP retrieves all threat events for a specific IP +func (ta *ThreatAnalyzer) GetThreatEventsByIP(ip string) ([]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 ip = ? ORDER BY last_seen DESC` + + rows, err := ta.db.Query(query, ip) + if err != nil { + return nil, err + } + defer rows.Close() + + var events []ThreatEvent + for rows.Next() { + var event ThreatEvent + var detailsJSON sql.NullString + var ruleID sql.NullInt64 + + 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.Valid { + json.Unmarshal([]byte(detailsJSON.String), &event.Details) + } + + if ruleID.Valid { + id := int(ruleID.Int64) + event.RuleID = &id + } + + events = append(events, event) + } + + return events, nil +} + +// GetIPReports retrieves IP reports with filtering options +func (ta *ThreatAnalyzer) GetIPReports(filters map[string]interface{}) ([]IPReport, error) { + query := `SELECT ip, total_connections, total_auth_attempts, services, threat_score, is_blocked, first_seen, last_seen, geo_location + FROM ip_analysis WHERE 1=1` + + var args []interface{} + var conditions []string + + // Apply filters + if service, ok := filters["service"].(string); ok && service != "" { + conditions = append(conditions, "services LIKE ?") + args = append(args, "%\""+service+"\"%") + } + + if minThreatScore, ok := filters["min_threat_score"].(int); ok { + conditions = append(conditions, "threat_score >= ?") + args = append(args, minThreatScore) + } + + if blocked, ok := filters["blocked"].(bool); ok { + conditions = append(conditions, "is_blocked = ?") + args = append(args, blocked) + } + + if len(conditions) > 0 { + query += " AND " + strings.Join(conditions, " AND ") + } + + query += " ORDER BY threat_score DESC, last_seen DESC" + + if limit, ok := filters["limit"].(int); ok && limit > 0 { + query += " LIMIT ?" + args = append(args, limit) + } + + rows, err := ta.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var reports []IPReport + for rows.Next() { + var report IPReport + var servicesJSON, geoJSON sql.NullString + + err := rows.Scan(&report.IP, &report.TotalConnections, &report.TotalAuthAttempts, + &servicesJSON, &report.ThreatScore, &report.IsBlocked, &report.FirstSeen, &report.LastSeen, &geoJSON) + if err != nil { + return nil, err + } + + // Parse JSON fields + if servicesJSON.Valid { + json.Unmarshal([]byte(servicesJSON.String), &report.Services) + } + + if geoJSON.Valid { + json.Unmarshal([]byte(geoJSON.String), &report.GeoLocation) + } + + reports = append(reports, report) + } + + return reports, nil +} + +// ProcessLogRecord analyzes a log record and updates threat intelligence +func (ta *ThreatAnalyzer) ProcessLogRecord(record LogRecord) error { + // Update IP analysis + if err := ta.updateIPAnalysis(record); err != nil { + return err + } + + // Check against threat rules + return ta.checkThreatRules(record) +} + +// LogRecord represents a honeypot log entry (simplified version) +type LogRecord struct { + IP string `json:"ip"` + Service string `json:"service"` + Timestamp time.Time `json:"timestamp"` + Details map[string]interface{} `json:"details"` +} + +// updateIPAnalysis updates the IP analysis table with new log data +func (ta *ThreatAnalyzer) updateIPAnalysis(record LogRecord) error { + // Check if IP exists + var exists bool + err := ta.db.QueryRow("SELECT EXISTS(SELECT 1 FROM ip_analysis WHERE ip = ?)", record.IP).Scan(&exists) + if err != nil { + return err + } + + if exists { + // Update existing record + query := `UPDATE ip_analysis SET + total_connections = total_connections + 1, + last_seen = ?, + updated_at = CURRENT_TIMESTAMP + WHERE ip = ?` + _, err = ta.db.Exec(query, record.Timestamp, record.IP) + } else { + // Insert new record + servicesJSON, _ := json.Marshal([]string{record.Service}) + query := `INSERT INTO ip_analysis (ip, total_connections, services, first_seen, last_seen) + VALUES (?, 1, ?, ?, ?)` + _, err = ta.db.Exec(query, record.IP, string(servicesJSON), record.Timestamp, record.Timestamp) + } + + return err +} + +// checkThreatRules evaluates log records against threat detection rules +func (ta *ThreatAnalyzer) checkThreatRules(record LogRecord) error { + rules, err := ta.GetRules() + if err != nil { + return err + } + + for _, rule := range rules { + if !rule.Enabled { + continue + } + + // Check if rule applies to this service + if rule.Service != "*" && rule.Service != record.Service { + continue + } + + // Evaluate rule condition + triggered, err := ta.evaluateRule(rule, record) + if err != nil { + log.Printf("Error evaluating rule %s: %v", rule.Name, err) + continue + } + + if triggered { + if err := ta.createThreatEvent(rule, record); err != nil { + log.Printf("Error creating threat event: %v", err) + } + } + } + + return nil +} + +// evaluateRule checks if a rule condition is met +func (ta *ThreatAnalyzer) evaluateRule(rule ThreatRule, record LogRecord) (bool, error) { + timeWindow := time.Now().Add(-time.Duration(rule.TimeWindow) * time.Minute) + + switch rule.Condition { + case "connection_count": + var count int + query := `SELECT COUNT(*) FROM ip_analysis WHERE ip = ? AND last_seen >= ?` + err := ta.db.QueryRow(query, record.IP, timeWindow).Scan(&count) + return count >= rule.Threshold, err + + case "auth_attempts": + // This would need to be tracked separately based on log details + // For now, we'll use a simplified approach + if authAttempts, ok := record.Details["auth_attempts"].(float64); ok { + return int(authAttempts) >= rule.Threshold, nil + } + return false, nil + + case "service_diversity": + var serviceCount int + query := `SELECT COUNT(DISTINCT service) FROM threat_events WHERE ip = ? AND last_seen >= ?` + err := ta.db.QueryRow(query, record.IP, timeWindow).Scan(&serviceCount) + return serviceCount >= rule.Threshold, err + + default: + return false, fmt.Errorf("unknown rule condition: %s", rule.Condition) + } +} + +// createThreatEvent creates a new threat event +func (ta *ThreatAnalyzer) createThreatEvent(rule ThreatRule, record LogRecord) error { + detailsJSON, _ := json.Marshal(record.Details) + + // Determine event type and severity based on rule + eventType := "suspicious_activity" + severity := "medium" + + if strings.Contains(strings.ToLower(rule.Name), "brute") { + eventType = "brute_force" + severity = "high" + } else if strings.Contains(strings.ToLower(rule.Name), "scan") { + eventType = "port_scan" + severity = "medium" + } + + query := `INSERT INTO threat_events (ip, service, event_type, severity, first_seen, last_seen, details, rule_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(ip, service, event_type) DO UPDATE SET + count = count + 1, + last_seen = ?, + details = ?` + + _, err := ta.db.Exec(query, record.IP, record.Service, eventType, severity, + record.Timestamp, record.Timestamp, string(detailsJSON), rule.ID, + record.Timestamp, string(detailsJSON)) + + // If this is a blocking rule, add to blocklist + if rule.Action == "block" { + ta.blockIP(record.IP, rule.ID) + } + + return err +} + +// blockIP adds an IP to the blocklist +func (ta *ThreatAnalyzer) blockIP(ip string, ruleID int) error { + // Update IP analysis to mark as blocked + query := `UPDATE ip_analysis SET is_blocked = 1 WHERE ip = ?` + _, err := ta.db.Exec(query, ip) + + // Update threat events to mark as blocked + query2 := `UPDATE threat_events SET blocked = 1 WHERE ip = ? AND rule_id = ?` + _, err2 := ta.db.Exec(query2, ip, ruleID) + + if err != nil { + return err + } + return err2 +} + +// GetBlockedIPs returns all currently blocked IPs +func (ta *ThreatAnalyzer) GetBlockedIPs() ([]string, error) { + query := `SELECT ip FROM ip_analysis WHERE is_blocked = 1 ORDER BY last_seen DESC` + + rows, err := ta.db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var ips []string + for rows.Next() { + var ip string + if err := rows.Scan(&ip); err != nil { + return nil, err + } + ips = append(ips, ip) + } + + return ips, nil +} + +// UnblockIP removes an IP from the blocklist +func (ta *ThreatAnalyzer) UnblockIP(ip string) error { + query := `UPDATE ip_analysis SET is_blocked = 0 WHERE ip = ?` + _, err := ta.db.Exec(query, ip) + + query2 := `UPDATE threat_events SET blocked = 0 WHERE ip = ?` + _, err2 := ta.db.Exec(query2, ip) + + if err != nil { + return err + } + return err2 +} + +// Close closes the database connection +func (ta *ThreatAnalyzer) Close() error { + return ta.db.Close() +} diff --git a/app/dashboard/user_api.go b/app/dashboard/user_api.go new file mode 100644 index 0000000..bdc2b2c --- /dev/null +++ b/app/dashboard/user_api.go @@ -0,0 +1,553 @@ +package dashboard + +import ( + "encoding/json" + "net/http" + "strconv" + "strings" + "time" +) + +// UserAPI handles user management endpoints +type UserAPI struct { + authManager *AuthManager + securityManager *SecurityManager +} + +// NewUserAPI creates a new user API instance +func NewUserAPI(authManager *AuthManager, securityManager *SecurityManager) *UserAPI { + return &UserAPI{ + authManager: authManager, + securityManager: securityManager, + } +} + +// RegisterUserRoutes registers all user management routes +func (ua *UserAPI) RegisterUserRoutes(mux *http.ServeMux, sm *SecurityManager) { + // Authentication routes (no auth required) + mux.HandleFunc("/login", sm.LoginHandler) + mux.HandleFunc("/logout", sm.LogoutHandler) + mux.HandleFunc("/api/auth/login", ua.handleAPILogin) + + // User management routes (require authentication) + mux.HandleFunc("/api/users", sm.APIAuthMiddleware(ua.handleUsers)) + mux.HandleFunc("/api/users/", sm.APIAuthMiddleware(ua.handleUser)) + mux.HandleFunc("/api/users/me", sm.APIAuthMiddleware(ua.handleCurrentUser)) + mux.HandleFunc("/api/users/me/password", sm.APIAuthMiddleware(sm.CSRFMiddleware(ua.handleChangePassword))) + + // API key management routes + mux.HandleFunc("/api/apikeys", sm.APIAuthMiddleware(ua.handleAPIKeys)) + mux.HandleFunc("/api/apikeys/", sm.APIAuthMiddleware(ua.handleAPIKey)) + + // User management page will be handled in web.go +} + +// handleAPILogin handles API-based login +func (ua *UserAPI) handleAPILogin(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var loginReq struct { + Username string `json:"username"` + Password string `json:"password"` + } + + if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil { + ua.sendJSONError(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // Validate input + if err := ua.securityManager.ValidateInput().ValidateUsername(loginReq.Username); err != nil { + ua.sendJSONError(w, err.Error(), http.StatusBadRequest) + return + } + + // Authenticate user + user, err := ua.authManager.AuthenticateUser(loginReq.Username, loginReq.Password) + if err != nil { + ua.sendJSONError(w, "Invalid credentials", http.StatusUnauthorized) + return + } + + // Create session + session, err := ua.authManager.CreateSession(user.ID, r.RemoteAddr, r.UserAgent()) + if err != nil { + ua.sendJSONError(w, "Failed to create session", http.StatusInternalServerError) + return + } + + // Set session cookie + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: session.Token, + Path: "/", + Expires: session.ExpiresAt, + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteStrictMode, + }) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "user": map[string]interface{}{ + "id": user.ID, + "username": user.Username, + "email": user.Email, + "role": user.Role, + }, + }) +} + +// handleUsers handles GET/POST /api/users +func (ua *UserAPI) handleUsers(w http.ResponseWriter, r *http.Request) { + user := ua.securityManager.GetUserFromContext(r.Context()) + if user == nil { + ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) + return + } + + switch r.Method { + case "GET": + ua.handleGetUsers(w, r, user) + case "POST": + ua.handleCreateUser(w, r, user) + default: + ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// handleGetUsers retrieves all users +func (ua *UserAPI) handleGetUsers(w http.ResponseWriter, r *http.Request, currentUser *User) { + // Only admins can list all users + if currentUser.Role != "admin" { + ua.sendJSONError(w, "Insufficient permissions", http.StatusForbidden) + return + } + + users, err := ua.authManager.GetUsers() + if err != nil { + ua.sendJSONError(w, "Failed to retrieve users", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "users": users, + "count": len(users), + }) +} + +// handleCreateUser creates a new user +func (ua *UserAPI) handleCreateUser(w http.ResponseWriter, r *http.Request, currentUser *User) { + // Only admins can create users + if currentUser.Role != "admin" { + ua.sendJSONError(w, "Insufficient permissions", http.StatusForbidden) + return + } + + var createReq struct { + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` + Role string `json:"role"` + } + + if err := json.NewDecoder(r.Body).Decode(&createReq); err != nil { + ua.sendJSONError(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // Validate input + validator := ua.securityManager.ValidateInput() + if err := validator.ValidateUsername(createReq.Username); err != nil { + ua.sendJSONError(w, err.Error(), http.StatusBadRequest) + return + } + if err := validator.ValidatePassword(createReq.Password); err != nil { + ua.sendJSONError(w, err.Error(), http.StatusBadRequest) + return + } + if err := validator.ValidateEmail(createReq.Email); err != nil { + ua.sendJSONError(w, err.Error(), http.StatusBadRequest) + return + } + if err := validator.ValidateRole(createReq.Role); err != nil { + ua.sendJSONError(w, err.Error(), http.StatusBadRequest) + return + } + + // Create user + user, err := ua.authManager.CreateUser(createReq.Username, createReq.Password, createReq.Email, createReq.Role) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + ua.sendJSONError(w, "Username already exists", http.StatusConflict) + } else { + ua.sendJSONError(w, "Failed to create user", http.StatusInternalServerError) + } + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "user": user, + }) +} + +// handleUser handles individual user operations +func (ua *UserAPI) handleUser(w http.ResponseWriter, r *http.Request) { + user := ua.securityManager.GetUserFromContext(r.Context()) + if user == nil { + ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Extract user ID from URL + path := strings.TrimPrefix(r.URL.Path, "/api/users/") + userID, err := strconv.Atoi(path) + if err != nil { + ua.sendJSONError(w, "Invalid user ID", http.StatusBadRequest) + return + } + + switch r.Method { + case "GET": + ua.handleGetUser(w, r, user, userID) + case "PUT": + ua.handleUpdateUser(w, r, user, userID) + case "DELETE": + ua.handleDeleteUser(w, r, user, userID) + default: + ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// handleGetUser retrieves a specific user +func (ua *UserAPI) handleGetUser(w http.ResponseWriter, r *http.Request, currentUser *User, userID int) { + // Users can only view their own profile, admins can view any + if currentUser.Role != "admin" && currentUser.ID != userID { + ua.sendJSONError(w, "Insufficient permissions", http.StatusForbidden) + return + } + + user, err := ua.authManager.GetUserByID(userID) + if err != nil { + ua.sendJSONError(w, "User not found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) +} + +// handleUpdateUser updates a user +func (ua *UserAPI) handleUpdateUser(w http.ResponseWriter, r *http.Request, currentUser *User, userID int) { + // Users can only update their own profile, admins can update any + if currentUser.Role != "admin" && currentUser.ID != userID { + ua.sendJSONError(w, "Insufficient permissions", http.StatusForbidden) + return + } + + var updateReq struct { + Username string `json:"username"` + Email string `json:"email"` + Role string `json:"role"` + Active *bool `json:"active"` + } + + if err := json.NewDecoder(r.Body).Decode(&updateReq); err != nil { + ua.sendJSONError(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // Get existing user + existingUser, err := ua.authManager.GetUserByID(userID) + if err != nil { + ua.sendJSONError(w, "User not found", http.StatusNotFound) + return + } + + // Validate input + validator := ua.securityManager.ValidateInput() + if updateReq.Username != "" { + if err := validator.ValidateUsername(updateReq.Username); err != nil { + ua.sendJSONError(w, err.Error(), http.StatusBadRequest) + return + } + } else { + updateReq.Username = existingUser.Username + } + + if updateReq.Email != "" { + if err := validator.ValidateEmail(updateReq.Email); err != nil { + ua.sendJSONError(w, err.Error(), http.StatusBadRequest) + return + } + } else { + updateReq.Email = existingUser.Email + } + + // Only admins can change role and active status + if currentUser.Role != "admin" { + updateReq.Role = existingUser.Role + active := existingUser.Active + updateReq.Active = &active + } else { + if updateReq.Role != "" { + if err := validator.ValidateRole(updateReq.Role); err != nil { + ua.sendJSONError(w, err.Error(), http.StatusBadRequest) + return + } + } else { + updateReq.Role = existingUser.Role + } + + if updateReq.Active == nil { + active := existingUser.Active + updateReq.Active = &active + } + } + + // Update user + err = ua.authManager.UpdateUser(userID, updateReq.Username, updateReq.Email, updateReq.Role, *updateReq.Active) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + ua.sendJSONError(w, "Username already exists", http.StatusConflict) + } else { + ua.sendJSONError(w, "Failed to update user", http.StatusInternalServerError) + } + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"success": "User updated successfully"}) +} + +// handleDeleteUser deletes a user +func (ua *UserAPI) handleDeleteUser(w http.ResponseWriter, r *http.Request, currentUser *User, userID int) { + // Only admins can delete users + if currentUser.Role != "admin" { + ua.sendJSONError(w, "Insufficient permissions", http.StatusForbidden) + return + } + + // Prevent deleting self + if currentUser.ID == userID { + ua.sendJSONError(w, "Cannot delete your own account", http.StatusBadRequest) + return + } + + err := ua.authManager.DeleteUser(userID) + if err != nil { + ua.sendJSONError(w, "Failed to delete user", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"success": "User deleted successfully"}) +} + +// handleCurrentUser returns current user info +func (ua *UserAPI) handleCurrentUser(w http.ResponseWriter, r *http.Request) { + user := ua.securityManager.GetUserFromContext(r.Context()) + if user == nil { + ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(user) +} + +// handleChangePassword changes user password +func (ua *UserAPI) handleChangePassword(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + user := ua.securityManager.GetUserFromContext(r.Context()) + if user == nil { + ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) + return + } + + var changeReq struct { + CurrentPassword string `json:"current_password"` + NewPassword string `json:"new_password"` + } + + if err := json.NewDecoder(r.Body).Decode(&changeReq); err != nil { + ua.sendJSONError(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // Verify current password + _, err := ua.authManager.AuthenticateUser(user.Username, changeReq.CurrentPassword) + if err != nil { + ua.sendJSONError(w, "Current password is incorrect", http.StatusBadRequest) + return + } + + // Validate new password + if err := ua.securityManager.ValidateInput().ValidatePassword(changeReq.NewPassword); err != nil { + ua.sendJSONError(w, err.Error(), http.StatusBadRequest) + return + } + + // Update password + err = ua.authManager.UpdateUserPassword(user.ID, changeReq.NewPassword) + if err != nil { + ua.sendJSONError(w, "Failed to update password", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"success": "Password updated successfully"}) +} + +// handleAPIKeys handles API key management +func (ua *UserAPI) handleAPIKeys(w http.ResponseWriter, r *http.Request) { + user := ua.securityManager.GetUserFromContext(r.Context()) + if user == nil { + ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) + return + } + + switch r.Method { + case "GET": + ua.handleGetAPIKeys(w, r, user) + case "POST": + ua.handleCreateAPIKey(w, r, user) + default: + ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// handleGetAPIKeys retrieves API keys for current user +func (ua *UserAPI) handleGetAPIKeys(w http.ResponseWriter, r *http.Request, user *User) { + keys, err := ua.authManager.GetAPIKeys(user.ID) + if err != nil { + ua.sendJSONError(w, "Failed to retrieve API keys", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "api_keys": keys, + "count": len(keys), + }) +} + +// handleCreateAPIKey creates a new API key +func (ua *UserAPI) handleCreateAPIKey(w http.ResponseWriter, r *http.Request, user *User) { + var createReq struct { + Name string `json:"name"` + Permissions []string `json:"permissions"` + ExpiresIn *int `json:"expires_in"` // Days from now + } + + if err := json.NewDecoder(r.Body).Decode(&createReq); err != nil { + ua.sendJSONError(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // Validate input + if err := ua.securityManager.ValidateInput().ValidateAPIKeyName(createReq.Name); err != nil { + ua.sendJSONError(w, err.Error(), http.StatusBadRequest) + return + } + + var expiresAt *time.Time + if createReq.ExpiresIn != nil && *createReq.ExpiresIn > 0 { + expiry := time.Now().AddDate(0, 0, *createReq.ExpiresIn) + expiresAt = &expiry + } + + // Create API key + apiKey, err := ua.authManager.CreateAPIKey(createReq.Name, user.ID, createReq.Permissions, expiresAt) + if err != nil { + ua.sendJSONError(w, "Failed to create API key", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "api_key": apiKey, + "warning": "Save this key securely. It will not be shown again.", + }) +} + +// handleAPIKey handles individual API key operations +func (ua *UserAPI) handleAPIKey(w http.ResponseWriter, r *http.Request) { + user := ua.securityManager.GetUserFromContext(r.Context()) + if user == nil { + ua.sendJSONError(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Extract API key ID from URL + path := strings.TrimPrefix(r.URL.Path, "/api/apikeys/") + keyID, err := strconv.Atoi(path) + if err != nil { + ua.sendJSONError(w, "Invalid API key ID", http.StatusBadRequest) + return + } + + switch r.Method { + case "DELETE": + ua.handleRevokeAPIKey(w, r, user, keyID) + default: + ua.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// handleRevokeAPIKey revokes an API key +func (ua *UserAPI) handleRevokeAPIKey(w http.ResponseWriter, r *http.Request, user *User, keyID int) { + // Users can only revoke their own API keys, admins can revoke any + if user.Role != "admin" { + // Check if the API key belongs to the current user + keys, err := ua.authManager.GetAPIKeys(user.ID) + if err != nil { + ua.sendJSONError(w, "Failed to verify API key ownership", http.StatusInternalServerError) + return + } + + found := false + for _, key := range keys { + if key.ID == keyID { + found = true + break + } + } + + if !found { + ua.sendJSONError(w, "API key not found", http.StatusNotFound) + return + } + } + + err := ua.authManager.RevokeAPIKey(keyID) + if err != nil { + ua.sendJSONError(w, "Failed to revoke API key", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"success": "API key revoked successfully"}) +} + + +// sendJSONError sends a JSON error response +func (ua *UserAPI) sendJSONError(w http.ResponseWriter, message string, statusCode int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(map[string]string{"error": message}) +} diff --git a/app/services.go b/app/services.go index a3bee36..6a015ea 100644 --- a/app/services.go +++ b/app/services.go @@ -10,7 +10,6 @@ import ( "crypto/x509/pkix" "encoding/pem" "fmt" - svcs "honeydany/app/services" "io" "log" "math/big" @@ -24,16 +23,19 @@ import ( "time" "golang.org/x/crypto/ssh" + "honeydany/app/dashboard" + svcs "honeydany/app/services" ) // App holds runtime pieces type App struct { - cfg Config - logger *Logger - threatIntel *ThreatIntel - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup + cfg Config + logger *Logger + threatIntel *ThreatIntel + threatManager *dashboard.ThreatManager + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup // keep references to servers for graceful shutdown httpSrvs []*http.Server sshSigner ssh.Signer @@ -203,9 +205,26 @@ func NewApp(cfg Config) (*App, error) { } ti := NewThreatIntel(threatIntelPath, true) + // Initialize threat manager + dbPath := "app.db" + if cfg.LogPath != "" { + dbPath = filepath.Join(filepath.Dir(cfg.LogPath), "app.db") + } + tm, err := dashboard.NewThreatManager(dbPath) + if err != nil { + log.Printf("Failed to initialize threat manager: %v", err) + tm = nil // Continue without threat manager if it fails + } + // Root context for the App used for shutdown signalling ctx, cancel := context.WithCancel(context.Background()) - a := &App{cfg: cfg, logger: l, threatIntel: ti, ctx: ctx, cancel: cancel, conns: make(map[net.Conn]struct{}), restartCh: make(chan struct{}, 1)} + a := &App{cfg: cfg, logger: l, threatIntel: ti, threatManager: tm, ctx: ctx, cancel: cancel, conns: make(map[net.Conn]struct{}), restartCh: make(chan struct{}, 1)} + + // Start periodic threat analysis tasks + if tm != nil { + tm.RunPeriodicTasks() + } + return a, nil } @@ -367,6 +386,11 @@ func (a *App) Shutdown() { _ = a.threatIntel.Save() } + // Close threat manager + if a.threatManager != nil { + _ = a.threatManager.Close() + } + _ = a.logger.Close() } @@ -422,6 +446,16 @@ func (a *App) logEvent(r Record) { if a.threatIntel != nil && r.RemoteAddr != "" && !IsPrivateIP(r.RemoteAddr) { a.threatIntel.RecordActivity(r) } + + // Process with threat manager for advanced analysis + if a.threatManager != nil && r.RemoteAddr != "" && !IsPrivateIP(r.RemoteAddr) { + // Convert map[string]string to map[string]interface{} + details := make(map[string]interface{}) + for k, v := range r.Details { + details[k] = v + } + a.threatManager.ProcessHoneypotRecord(r.Timestamp, r.RemoteAddr, r.RemotePort, r.Service, details, r.RawPayload) + } } // svcLogger adapts services.Record to the app Record and logs it diff --git a/app/templates/layout.html b/app/templates/layout.html index e608a44..5276929 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -10,6 +10,9 @@ {{ else if eq .PageTitle "stats_title" }}Statistics {{ else if eq .PageTitle "blacklist_title" }}Blacklist {{ else if eq .PageTitle "threats_title" }}Top Threats + {{ else if eq .PageTitle "threat_reports_title" }}Threat Reports + {{ else if eq .PageTitle "threat_rules_title" }}Threat Rules + {{ else if eq .PageTitle "users_title" }}User Management {{ else }}Honeypot Dashboard{{ end }} @@ -38,8 +41,11 @@ Honeypot Dashboard Recent Logs Top Threats + Threat Reports + Threat Rules Blacklist Statistics + Users Settings
{{ .Now }}
@@ -59,6 +65,12 @@ {{ template "blacklist_content" . }} {{ else if eq .PageContent "threats_content" }} {{ template "threats_content" . }} + {{ else if eq .PageContent "threat_reports_content" }} + {{ template "threat_reports_content" . }} + {{ else if eq .PageContent "threat_rules_content" }} + {{ template "threat_rules_content" . }} + {{ else if eq .PageContent "users_content" }} + {{ template "users_content" . }} {{ end }}
Honeypot Dashboard
diff --git a/app/templates/threat_reports.html b/app/templates/threat_reports.html new file mode 100644 index 0000000..eafcdc2 --- /dev/null +++ b/app/templates/threat_reports.html @@ -0,0 +1,395 @@ +{{ define "threat_reports_title" }}Threat Reports{{ end }} +{{ define "threat_reports_content" }} +
+
+

Advanced Threat Reports

+ +
+ + +
+
+
Total IPs Tracked
+
-
+
+
+
Blocked IPs
+
-
+
+
+
Threat Events
+
-
+
+
+
Active Rules
+
-
+
+
+ + +
+

Filters

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+

IP Threat Analysis

+
+
+ + + + + + + + + + + + + + + +
IP AddressThreat ScoreConnectionsServicesLast SeenStatusActions
+
+
+ + + +
+ + +{{ end }} diff --git a/app/templates/threat_rules.html b/app/templates/threat_rules.html new file mode 100644 index 0000000..c29746c --- /dev/null +++ b/app/templates/threat_rules.html @@ -0,0 +1,373 @@ +{{ define "threat_rules_title" }}Threat Rules{{ end }} +{{ define "threat_rules_content" }} +
+
+

Threat Detection Rules

+ +
+ + +
+
+

Active Rules

+
+
+ + + + + + + + + + + + + + + + +
NameServiceConditionThresholdTime WindowActionStatusActions
+
+
+ + + +
+ + +{{ end }} diff --git a/app/templates/users.html b/app/templates/users.html new file mode 100644 index 0000000..2152662 --- /dev/null +++ b/app/templates/users.html @@ -0,0 +1,608 @@ +{{ define "users_title" }}User Management{{ end }} +{{ define "users_content" }} +
+
+

User Management

+ +
+ + +
+

Current User

+
+
+ Username: + {{ .CurrentUser.Username }} +
+
+ Role: + {{ .CurrentUser.Role }} +
+
+ Last Login: + + {{ if .CurrentUser.LastLogin }}{{ .CurrentUser.LastLogin.Format "2006-01-02 15:04:05" }}{{ else }}Never{{ end }} + +
+
+
+ + +
+
+

All Users

+
+
+ + + + + + + + + + + + + + + +
UsernameEmailRoleStatusLast LoginCreatedActions
+
+
+ + +
+
+

My API Keys

+ +
+
+ + + + + + + + + + + + + + +
NameKeyStatusLast UsedExpiresActions
+
+
+ + + + + + + + + +
+ + +{{ end }} diff --git a/app/web.go b/app/web.go index eeb188e..c236ac4 100644 --- a/app/web.go +++ b/app/web.go @@ -1,15 +1,17 @@ package app import ( - "embed" - "encoding/json" - "fmt" - "html/template" - "log" - "net/http" - "os" - "strings" - "time" + "embed" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "os" + "strings" + "time" + + "honeydany/app/dashboard" ) //go:embed templates/*.html @@ -34,6 +36,9 @@ func initTemplates() error { "templates/blacklist.html", "templates/stats.html", "templates/settings.html", + "templates/threat_reports.html", + "templates/threat_rules.html", + "templates/users.html", ) if err != nil { return err } templates = t @@ -41,16 +46,42 @@ func initTemplates() error { } func (a *App) startWeb() { - bind := a.cfg.Web.Bind - port := a.cfg.Web.Port - addr := fmt.Sprintf("%s:%d", bind, port) - mux := http.NewServeMux() - if templates == nil { - if err := initTemplates(); err != nil { - log.Printf("template init error: %v", err) - } - } - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + bind := a.cfg.Web.Bind + port := a.cfg.Web.Port + addr := fmt.Sprintf("%s:%d", bind, port) + mux := http.NewServeMux() + if templates == nil { + if err := initTemplates(); err != nil { + log.Printf("template init error: %v", err) + } + } + + // Register authentication and threat analysis routes if threat manager is available + if a.threatManager != nil { + var _ *dashboard.ThreatAPI = a.threatManager.GetAPI() // Ensure dashboard import is used + + // Get security manager + securityManager := a.threatManager.GetSecurityManager() + + // Register user management routes (includes login/logout) + a.threatManager.GetUserAPI().RegisterUserRoutes(mux, securityManager) + + // Register threat analysis API routes (they will handle their own authentication) + a.threatManager.GetAPI().RegisterRoutes(mux) + } + + // Secure dashboard routes with authentication + var authMiddleware func(http.HandlerFunc) http.HandlerFunc + if a.threatManager != nil { + authMiddleware = a.threatManager.GetSecurityManager().AuthMiddleware + } else { + // Fallback if threat manager is not available + authMiddleware = func(next http.HandlerFunc) http.HandlerFunc { + return next // No authentication + } + } + + mux.HandleFunc("/", authMiddleware(func(w http.ResponseWriter, r *http.Request) { stats := map[string]any{} if a.threatIntel != nil { stats = a.threatIntel.GetStats() @@ -61,29 +92,48 @@ func (a *App) startWeb() { "PageTitle": "index_title", "PageContent": "index_content", } + // Add CSRF token if security manager is available + if a.threatManager != nil { + a.threatManager.GetSecurityManager().AddCSRFToken(w, data) + } if templates != nil { _ = templates.ExecuteTemplate(w, "layout.html", data) return } http.Error(w, "templates not loaded", 500) - }) + })) // Settings UI - mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/settings", authMiddleware(func(w http.ResponseWriter, r *http.Request) { data := map[string]any{ "Now": time.Now().Format("2006-01-02 15:04:05 MST"), "Cfg": a.cfg, "PageTitle": "settings_title", "PageContent": "settings_content", } + // Add CSRF token if security manager is available + if a.threatManager != nil { + a.threatManager.GetSecurityManager().AddCSRFToken(w, data) + } if templates != nil { _ = templates.ExecuteTemplate(w, "layout.html", data) return } http.Error(w, "templates not loaded", 500) - }) + })) // API to read/update settings (services + ports) - mux.HandleFunc("/api/settings", func(w http.ResponseWriter, r *http.Request) { + var apiAuthMiddleware func(http.HandlerFunc) http.HandlerFunc + var csrfMiddleware func(http.HandlerFunc) http.HandlerFunc + if a.threatManager != nil { + apiAuthMiddleware = a.threatManager.GetSecurityManager().APIAuthMiddleware + csrfMiddleware = a.threatManager.GetSecurityManager().CSRFMiddleware + } else { + // Fallback if threat manager is not available + apiAuthMiddleware = func(next http.HandlerFunc) http.HandlerFunc { return next } + csrfMiddleware = func(next http.HandlerFunc) http.HandlerFunc { return next } + } + + mux.HandleFunc("/api/settings", apiAuthMiddleware(csrfMiddleware(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.Method { case http.MethodGet: @@ -170,14 +220,14 @@ func (a *App) startWeb() { w.WriteHeader(http.StatusMethodNotAllowed) return } - }) + }))) // Restart endpoint: triggers app restart - mux.HandleFunc("/api/restart", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/restart", apiAuthMiddleware(csrfMiddleware(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed); return } _ = json.NewEncoder(w).Encode(map[string]string{"status":"restarting"}) go func(){ time.Sleep(700*time.Millisecond); a.Restart() }() - }) - mux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) { + }))) + mux.HandleFunc("/logs", authMiddleware(func(w http.ResponseWriter, r *http.Request) { // display last 200 logs var rows []Record if a.logger != nil && a.logger.mode == "sqlite" && a.logger.db != nil { @@ -231,8 +281,8 @@ func (a *App) startWeb() { return } http.Error(w, "templates not loaded", 500) - }) - mux.HandleFunc("/threats", func(w http.ResponseWriter, r *http.Request) { + })) + mux.HandleFunc("/threats", authMiddleware(func(w http.ResponseWriter, r *http.Request) { var threats []*IPThreatInfo if a.threatIntel != nil { threats = a.threatIntel.GetTopThreats(50) @@ -248,8 +298,8 @@ func (a *App) startWeb() { return } http.Error(w, "templates not loaded", 500) - }) - mux.HandleFunc("/blacklist", func(w http.ResponseWriter, r *http.Request) { + })) + mux.HandleFunc("/blacklist", authMiddleware(func(w http.ResponseWriter, r *http.Request) { var bl []string if a.threatIntel != nil { bl = a.threatIntel.GetBlacklistedIPs() @@ -265,8 +315,8 @@ func (a *App) startWeb() { return } http.Error(w, "templates not loaded", 500) - }) - mux.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) { + })) + mux.HandleFunc("/stats", authMiddleware(func(w http.ResponseWriter, r *http.Request) { stats := map[string]any{} var svc map[string]int if a.threatIntel != nil { @@ -287,7 +337,74 @@ func (a *App) startWeb() { return } http.Error(w, "templates not loaded", 500) - }) + })) + + // Threat Reports page + mux.HandleFunc("/threat-reports", authMiddleware(func(w http.ResponseWriter, r *http.Request) { + data := map[string]any{ + "Now": time.Now().Format("2006-01-02 15:04:05 MST"), + "PageTitle": "threat_reports_title", + "PageContent": "threat_reports_content", + } + if templates != nil { + _ = templates.ExecuteTemplate(w, "layout.html", data) + return + } + http.Error(w, "templates not loaded", 500) + })) + + // Threat Rules page + mux.HandleFunc("/threat-rules", authMiddleware(func(w http.ResponseWriter, r *http.Request) { + data := map[string]any{ + "Now": time.Now().Format("2006-01-02 15:04:05 MST"), + "PageTitle": "threat_rules_title", + "PageContent": "threat_rules_content", + } + // Add CSRF token if security manager is available + if a.threatManager != nil { + a.threatManager.GetSecurityManager().AddCSRFToken(w, data) + } + if templates != nil { + _ = templates.ExecuteTemplate(w, "layout.html", data) + return + } + http.Error(w, "templates not loaded", 500) + })) + + // Users page (Admin only) + var roleMiddleware func(string) func(http.HandlerFunc) http.HandlerFunc + if a.threatManager != nil { + roleMiddleware = a.threatManager.GetSecurityManager().RoleMiddleware + } else { + // Fallback if threat manager is not available + roleMiddleware = func(role string) func(http.HandlerFunc) http.HandlerFunc { + return func(next http.HandlerFunc) http.HandlerFunc { return next } + } + } + + mux.HandleFunc("/users", authMiddleware(roleMiddleware("admin")(func(w http.ResponseWriter, r *http.Request) { + // Get current user from context + var currentUser interface{} + if a.threatManager != nil { + currentUser = a.threatManager.GetSecurityManager().GetUserFromContext(r.Context()) + } + + data := map[string]any{ + "Now": time.Now().Format("2006-01-02 15:04:05 MST"), + "CurrentUser": currentUser, + "PageTitle": "users_title", + "PageContent": "users_content", + } + // Add CSRF token if security manager is available + if a.threatManager != nil { + a.threatManager.GetSecurityManager().AddCSRFToken(w, data) + } + if templates != nil { + _ = templates.ExecuteTemplate(w, "layout.html", data) + return + } + http.Error(w, "templates not loaded", 500) + }))) srv := &http.Server{Addr: addr, Handler: mux} a.addHTTPServer(srv)