added authentication and API
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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[:])
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
"<script>", "javascript:", "eval(", "exec(",
|
||||
"union select", "drop table", "insert into",
|
||||
"wget", "curl", "nc -", "netcat", "/bin/sh", "/bin/bash",
|
||||
"cmd.exe", "powershell", "certutil",
|
||||
}
|
||||
|
||||
payloadLower := strings.ToLower(payload)
|
||||
|
||||
for _, pattern := range suspiciousStrings {
|
||||
if strings.Contains(payloadLower, pattern) {
|
||||
patterns = append(patterns, pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// Service-specific patterns
|
||||
switch service {
|
||||
case "ssh":
|
||||
sshPatterns := []string{
|
||||
"SSH-2.0-libssh", "SSH-2.0-PuTTY", "SSH-2.0-OpenSSH",
|
||||
}
|
||||
for _, pattern := range sshPatterns {
|
||||
if strings.Contains(payload, pattern) {
|
||||
patterns = append(patterns, "ssh_client:"+pattern)
|
||||
}
|
||||
}
|
||||
|
||||
case "http", "https":
|
||||
httpPatterns := []string{
|
||||
"User-Agent:", "Mozilla/", "curl/", "wget/", "python-requests/",
|
||||
"sqlmap", "nikto", "nmap", "masscan", "zmap",
|
||||
}
|
||||
for _, pattern := range httpPatterns {
|
||||
if strings.Contains(payloadLower, pattern) {
|
||||
patterns = append(patterns, "http_pattern:"+pattern)
|
||||
}
|
||||
}
|
||||
|
||||
case "ftp":
|
||||
ftpPatterns := []string{
|
||||
"USER anonymous", "USER ftp", "PASS anonymous",
|
||||
}
|
||||
for _, pattern := range ftpPatterns {
|
||||
if strings.Contains(payloadLower, pattern) {
|
||||
patterns = append(patterns, "ftp_pattern:"+pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return patterns
|
||||
}
|
||||
|
||||
// GetAnalyzer returns the threat analyzer instance
|
||||
func (tm *ThreatManager) GetAnalyzer() *ThreatAnalyzer {
|
||||
return tm.analyzer
|
||||
}
|
||||
|
||||
// GetAPI returns the threat API instance
|
||||
func (tm *ThreatManager) GetAPI() *ThreatAPI {
|
||||
return tm.api
|
||||
}
|
||||
|
||||
// GetSecurityManager returns the security manager instance
|
||||
func (tm *ThreatManager) GetSecurityManager() *SecurityManager {
|
||||
return tm.securityManager
|
||||
}
|
||||
|
||||
// GetUserAPI returns the user API instance
|
||||
func (tm *ThreatManager) GetUserAPI() *UserAPI {
|
||||
return tm.userAPI
|
||||
}
|
||||
|
||||
// GetBlockedIPs returns currently blocked IPs for firewall integration
|
||||
func (tm *ThreatManager) GetBlockedIPs() ([]string, error) {
|
||||
return tm.analyzer.GetBlockedIPs()
|
||||
}
|
||||
|
||||
// IsIPBlocked checks if an IP is currently blocked
|
||||
func (tm *ThreatManager) IsIPBlocked(ip string) (bool, error) {
|
||||
report, err := tm.analyzer.AnalyzeIP(ip)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return report.IsBlocked, nil
|
||||
}
|
||||
|
||||
// RunPeriodicTasks starts background tasks for threat analysis
|
||||
func (tm *ThreatManager) RunPeriodicTasks() {
|
||||
// Run threat score calculation every 5 minutes
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if err := tm.updateThreatScores(); err != nil {
|
||||
log.Printf("Failed to update threat scores: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Run rule evaluation every minute
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if err := tm.evaluateAllRules(); err != nil {
|
||||
log.Printf("Failed to evaluate rules: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// updateThreatScores recalculates threat scores for all IPs
|
||||
func (tm *ThreatManager) updateThreatScores() error {
|
||||
// Get all IPs from the last 24 hours
|
||||
query := `SELECT DISTINCT ip FROM ip_analysis WHERE last_seen >= ?`
|
||||
yesterday := time.Now().Add(-24 * time.Hour)
|
||||
|
||||
rows, err := tm.analyzer.db.Query(query, yesterday)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var ips []string
|
||||
for rows.Next() {
|
||||
var ip string
|
||||
if err := rows.Scan(&ip); err != nil {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
|
||||
// Calculate threat score for each IP
|
||||
for _, ip := range ips {
|
||||
score := tm.calculateThreatScore(ip)
|
||||
|
||||
// Update the threat score in database
|
||||
updateQuery := `UPDATE ip_analysis SET threat_score = ?, updated_at = CURRENT_TIMESTAMP WHERE ip = ?`
|
||||
_, err := tm.analyzer.db.Exec(updateQuery, score, ip)
|
||||
if err != nil {
|
||||
log.Printf("Failed to update threat score for IP %s: %v", ip, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateThreatScore calculates a threat score (0-100) for an IP
|
||||
func (tm *ThreatManager) calculateThreatScore(ip string) int {
|
||||
score := 0
|
||||
|
||||
// Get IP statistics
|
||||
var connections, authAttempts int
|
||||
var servicesJSON string
|
||||
query := `SELECT total_connections, total_auth_attempts, services FROM ip_analysis WHERE ip = ?`
|
||||
err := tm.analyzer.db.QueryRow(query, ip).Scan(&connections, &authAttempts, &servicesJSON)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Base score from connections (max 20 points)
|
||||
if connections > 100 {
|
||||
score += 20
|
||||
} else if connections > 50 {
|
||||
score += 15
|
||||
} else if connections > 20 {
|
||||
score += 10
|
||||
} else if connections > 10 {
|
||||
score += 5
|
||||
}
|
||||
|
||||
// Score from auth attempts (max 25 points)
|
||||
if authAttempts > 50 {
|
||||
score += 25
|
||||
} else if authAttempts > 20 {
|
||||
score += 20
|
||||
} else if authAttempts > 10 {
|
||||
score += 15
|
||||
} else if authAttempts > 5 {
|
||||
score += 10
|
||||
}
|
||||
|
||||
// Score from service diversity (max 15 points)
|
||||
var services []string
|
||||
if servicesJSON != "" {
|
||||
json.Unmarshal([]byte(servicesJSON), &services)
|
||||
}
|
||||
serviceCount := len(services)
|
||||
if serviceCount > 5 {
|
||||
score += 15
|
||||
} else if serviceCount > 3 {
|
||||
score += 10
|
||||
} else if serviceCount > 1 {
|
||||
score += 5
|
||||
}
|
||||
|
||||
// Score from threat events (max 40 points)
|
||||
var eventCount int
|
||||
eventQuery := `SELECT COUNT(*) FROM threat_events WHERE ip = ? AND last_seen >= ?`
|
||||
yesterday := time.Now().Add(-24 * time.Hour)
|
||||
err = tm.analyzer.db.QueryRow(eventQuery, ip, yesterday).Scan(&eventCount)
|
||||
if err == nil {
|
||||
if eventCount > 10 {
|
||||
score += 40
|
||||
} else if eventCount > 5 {
|
||||
score += 30
|
||||
} else if eventCount > 2 {
|
||||
score += 20
|
||||
} else if eventCount > 0 {
|
||||
score += 10
|
||||
}
|
||||
}
|
||||
|
||||
// Cap at 100
|
||||
if score > 100 {
|
||||
score = 100
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// evaluateAllRules runs all active rules against recent activity
|
||||
func (tm *ThreatManager) evaluateAllRules() error {
|
||||
// Get recent log entries (last 5 minutes)
|
||||
// This would need to be integrated with the actual logging system
|
||||
// For now, we'll just update existing threat events
|
||||
|
||||
rules, err := tm.analyzer.GetRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if !rule.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
// Evaluate rule against recent activity
|
||||
if err := tm.evaluateRuleForRecentActivity(rule); err != nil {
|
||||
log.Printf("Failed to evaluate rule %s: %v", rule.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// evaluateRuleForRecentActivity evaluates a rule against recent activity
|
||||
func (tm *ThreatManager) evaluateRuleForRecentActivity(rule ThreatRule) error {
|
||||
timeWindow := time.Now().Add(-time.Duration(rule.TimeWindow) * time.Minute)
|
||||
|
||||
// Get IPs with recent activity
|
||||
query := `SELECT DISTINCT ip FROM ip_analysis WHERE last_seen >= ?`
|
||||
rows, err := tm.analyzer.db.Query(query, timeWindow)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var ips []string
|
||||
for rows.Next() {
|
||||
var ip string
|
||||
if err := rows.Scan(&ip); err != nil {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
|
||||
// Evaluate rule for each IP
|
||||
for _, ip := range ips {
|
||||
// Create a mock log record for evaluation
|
||||
record := LogRecord{
|
||||
IP: ip,
|
||||
Service: rule.Service,
|
||||
Timestamp: time.Now(),
|
||||
Details: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
triggered, err := tm.analyzer.evaluateRule(rule, record)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if triggered {
|
||||
// Check if we already have a recent event for this IP/rule combination
|
||||
var existingCount int
|
||||
checkQuery := `SELECT COUNT(*) FROM threat_events
|
||||
WHERE ip = ? AND rule_id = ? AND last_seen >= ?`
|
||||
err := tm.analyzer.db.QueryRow(checkQuery, ip, rule.ID, timeWindow).Scan(&existingCount)
|
||||
|
||||
if err == nil && existingCount == 0 {
|
||||
// Create new threat event
|
||||
if err := tm.analyzer.createThreatEvent(rule, record); err != nil {
|
||||
log.Printf("Failed to create threat event: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the threat manager and its resources
|
||||
func (tm *ThreatManager) Close() error {
|
||||
return tm.analyzer.Close()
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ContextKey type for context keys
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
UserContextKey ContextKey = "user"
|
||||
)
|
||||
|
||||
// SecurityManager combines authentication and CSRF protection
|
||||
type SecurityManager struct {
|
||||
authManager *AuthManager
|
||||
csrfManager *CSRFManager
|
||||
validator *InputValidator
|
||||
}
|
||||
|
||||
// NewSecurityManager creates a new security manager
|
||||
func NewSecurityManager(authManager *AuthManager) *SecurityManager {
|
||||
return &SecurityManager{
|
||||
authManager: authManager,
|
||||
csrfManager: NewCSRFManager(),
|
||||
validator: NewInputValidator(),
|
||||
}
|
||||
}
|
||||
|
||||
// AuthMiddleware provides authentication for web pages
|
||||
func (sm *SecurityManager) AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip authentication for login page and static assets
|
||||
if r.URL.Path == "/login" || r.URL.Path == "/api/auth/login" ||
|
||||
strings.HasPrefix(r.URL.Path, "/static/") {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for session token in cookie
|
||||
cookie, err := r.Cookie("session_token")
|
||||
if err != nil {
|
||||
sm.redirectToLogin(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := sm.authManager.ValidateSession(cookie.Value)
|
||||
if err != nil {
|
||||
sm.redirectToLogin(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Add user to request context
|
||||
ctx := context.WithValue(r.Context(), UserContextKey, user)
|
||||
next(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
// APIAuthMiddleware provides authentication for API endpoints
|
||||
func (sm *SecurityManager) APIAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var user *User
|
||||
var err error
|
||||
|
||||
// Check for API key in header
|
||||
apiKey := r.Header.Get("X-API-Key")
|
||||
if apiKey != "" {
|
||||
user, err = sm.authManager.ValidateAPIKey(apiKey)
|
||||
if err != nil {
|
||||
sm.sendJSONError(w, "Invalid API key", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Check for session token in cookie (for web-based API calls)
|
||||
cookie, err := r.Cookie("session_token")
|
||||
if err != nil {
|
||||
sm.sendJSONError(w, "Authentication required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
user, err = sm.authManager.ValidateSession(cookie.Value)
|
||||
if err != nil {
|
||||
sm.sendJSONError(w, "Invalid session", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add user to request context
|
||||
ctx := context.WithValue(r.Context(), UserContextKey, user)
|
||||
next(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
// RoleMiddleware checks if user has required role
|
||||
func (sm *SecurityManager) RoleMiddleware(requiredRole string) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := sm.GetUserFromContext(r.Context())
|
||||
if user == nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !sm.hasPermission(user.Role, requiredRole) {
|
||||
http.Error(w, "Insufficient permissions", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CSRFMiddleware provides CSRF protection
|
||||
func (sm *SecurityManager) CSRFMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return sm.csrfManager.CSRFMiddleware(next)
|
||||
}
|
||||
|
||||
// GetUserFromContext extracts user from request context
|
||||
func (sm *SecurityManager) GetUserFromContext(ctx context.Context) *User {
|
||||
user, ok := ctx.Value(UserContextKey).(*User)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// hasPermission checks if a role has permission for an action
|
||||
func (sm *SecurityManager) hasPermission(userRole, requiredRole string) bool {
|
||||
roleHierarchy := map[string]int{
|
||||
"readonly": 1,
|
||||
"user": 2,
|
||||
"admin": 3,
|
||||
}
|
||||
|
||||
userLevel, exists := roleHierarchy[userRole]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
requiredLevel, exists := roleHierarchy[requiredRole]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
return userLevel >= requiredLevel
|
||||
}
|
||||
|
||||
// redirectToLogin redirects to login page
|
||||
func (sm *SecurityManager) redirectToLogin(w http.ResponseWriter, r *http.Request) {
|
||||
// If it's an AJAX request, return JSON error
|
||||
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" ||
|
||||
strings.Contains(r.Header.Get("Accept"), "application/json") {
|
||||
sm.sendJSONError(w, "Authentication required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise redirect to login page
|
||||
http.Redirect(w, r, "/login?redirect="+r.URL.Path, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// sendJSONError sends a JSON error response
|
||||
func (sm *SecurityManager) 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})
|
||||
}
|
||||
|
||||
// AddCSRFToken adds CSRF token to response data
|
||||
func (sm *SecurityManager) AddCSRFToken(w http.ResponseWriter, data map[string]interface{}) error {
|
||||
return sm.csrfManager.AddCSRFTokenToResponse(w, data)
|
||||
}
|
||||
|
||||
// ValidateInput validates input using the input validator
|
||||
func (sm *SecurityManager) ValidateInput() *InputValidator {
|
||||
return sm.validator
|
||||
}
|
||||
|
||||
// LoginHandler handles user login
|
||||
func (sm *SecurityManager) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
// Show login form
|
||||
sm.showLoginForm(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
sm.handleLogin(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
// LogoutHandler handles user logout
|
||||
func (sm *SecurityManager) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Get session token from cookie
|
||||
cookie, err := r.Cookie("session_token")
|
||||
if err == nil {
|
||||
// Delete session from database
|
||||
sm.authManager.DeleteSession(cookie.Value)
|
||||
}
|
||||
|
||||
// Clear session cookie
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session_token",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: r.TLS != nil,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
|
||||
// Redirect to login page
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// showLoginForm displays the login form
|
||||
func (sm *SecurityManager) showLoginForm(w http.ResponseWriter, r *http.Request) {
|
||||
data := map[string]interface{}{
|
||||
"Title": "Login",
|
||||
"Redirect": r.URL.Query().Get("redirect"),
|
||||
"Error": r.URL.Query().Get("error"),
|
||||
}
|
||||
|
||||
// Add CSRF token
|
||||
sm.AddCSRFToken(w, data)
|
||||
|
||||
// For now, return a simple HTML form
|
||||
// This will be replaced with proper template rendering
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html class="dark h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login - Honeypot Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa',
|
||||
500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="h-full bg-gray-900">
|
||||
<div class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-white">
|
||||
Sign in to Honeypot Dashboard
|
||||
</h2>
|
||||
</div>
|
||||
<form class="mt-8 space-y-6" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="` + data["CSRFToken"].(string) + `">
|
||||
<input type="hidden" name="redirect" value="` + data["Redirect"].(string) + `">
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label for="username" class="sr-only">Username</label>
|
||||
<input id="username" name="username" type="text" required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-700 bg-gray-800 text-gray-100 placeholder-gray-400 rounded-t-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Username">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="sr-only">Password</label>
|
||||
<input id="password" name="password" type="password" required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-700 bg-gray-800 text-gray-100 placeholder-gray-400 rounded-b-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
+42
-8
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
@@ -38,8 +41,11 @@
|
||||
<a href="/" class="text-primary-400 hover:text-primary-300 font-semibold">Honeypot Dashboard</a>
|
||||
<a href="/logs" class="text-gray-300 hover:text-white">Recent Logs</a>
|
||||
<a href="/threats" class="text-gray-300 hover:text-white">Top Threats</a>
|
||||
<a href="/threat-reports" class="text-gray-300 hover:text-white">Threat Reports</a>
|
||||
<a href="/threat-rules" class="text-gray-300 hover:text-white">Threat Rules</a>
|
||||
<a href="/blacklist" class="text-gray-300 hover:text-white">Blacklist</a>
|
||||
<a href="/stats" class="text-gray-300 hover:text-white">Statistics</a>
|
||||
<a href="/users" class="text-gray-300 hover:text-white">Users</a>
|
||||
<a href="/settings" class="text-gray-300 hover:text-white">Settings</a>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400">{{ .Now }}</div>
|
||||
@@ -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 }}
|
||||
</main>
|
||||
<footer class="border-t border-gray-800 py-6 text-center text-gray-500 text-sm">Honeypot Dashboard</footer>
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
{{ define "threat_reports_title" }}Threat Reports{{ end }}
|
||||
{{ define "threat_reports_content" }}
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-semibold text-white">Advanced Threat Reports</h1>
|
||||
<button id="refresh-btn" class="px-4 py-2 bg-primary-600 hover:bg-primary-500 rounded text-white">
|
||||
Refresh Data
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Threat Statistics Overview -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-400">Total IPs Tracked</div>
|
||||
<div id="stat-total-ips" class="text-3xl font-bold text-primary-400">-</div>
|
||||
</div>
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-400">Blocked IPs</div>
|
||||
<div id="stat-blocked-ips" class="text-3xl font-bold text-red-400">-</div>
|
||||
</div>
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-400">Threat Events</div>
|
||||
<div id="stat-threat-events" class="text-3xl font-bold text-yellow-400">-</div>
|
||||
</div>
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4">
|
||||
<div class="text-sm text-gray-400">Active Rules</div>
|
||||
<div id="stat-active-rules" class="text-3xl font-bold text-green-400">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4">
|
||||
<h2 class="text-lg font-semibold text-white mb-4">Filters</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Service</label>
|
||||
<select id="filter-service" class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100">
|
||||
<option value="">All Services</option>
|
||||
<option value="ssh">SSH</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
<option value="ftp">FTP</option>
|
||||
<option value="smtp">SMTP</option>
|
||||
<option value="telnet">Telnet</option>
|
||||
<option value="mysql">MySQL</option>
|
||||
<option value="postgresql">PostgreSQL</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Min Threat Score</label>
|
||||
<input id="filter-threat-score" type="number" min="0" max="100"
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100"
|
||||
placeholder="0">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Status</label>
|
||||
<select id="filter-blocked" class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100">
|
||||
<option value="">All</option>
|
||||
<option value="true">Blocked</option>
|
||||
<option value="false">Not Blocked</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button id="apply-filters" class="w-full px-4 py-2 bg-primary-600 hover:bg-primary-500 rounded text-white">
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Reports Table -->
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-lg">
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-white">IP Threat Analysis</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<thead class="bg-gray-800">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">IP Address</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Threat Score</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Connections</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Services</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Last Seen</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ip-reports-table" class="bg-gray-900 divide-y divide-gray-800">
|
||||
<!-- Dynamic content will be inserted here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Threat Events Modal -->
|
||||
<div id="threat-events-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-gray-800 rounded-lg max-w-4xl w-full max-h-screen overflow-y-auto">
|
||||
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
|
||||
<h3 id="modal-title" class="text-lg font-semibold text-white">Threat Events</h3>
|
||||
<button id="close-modal" class="text-gray-400 hover:text-white">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="modal-content" class="p-4">
|
||||
<!-- Dynamic content will be inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentReports = [];
|
||||
|
||||
// Load initial data
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadThreatStats();
|
||||
loadIPReports();
|
||||
});
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('refresh-btn').addEventListener('click', function() {
|
||||
loadThreatStats();
|
||||
loadIPReports();
|
||||
});
|
||||
|
||||
document.getElementById('apply-filters').addEventListener('click', loadIPReports);
|
||||
|
||||
document.getElementById('close-modal').addEventListener('click', function() {
|
||||
document.getElementById('threat-events-modal').classList.add('hidden');
|
||||
});
|
||||
|
||||
// Load threat statistics
|
||||
async function loadThreatStats() {
|
||||
try {
|
||||
const response = await fetch('/api/threat/stats');
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('stat-total-ips').textContent = data.total_ips || 0;
|
||||
document.getElementById('stat-blocked-ips').textContent = data.blocked_ips || 0;
|
||||
document.getElementById('stat-threat-events').textContent = data.total_threat_events || 0;
|
||||
document.getElementById('stat-active-rules').textContent = data.active_rules || 0;
|
||||
} catch (error) {
|
||||
console.error('Failed to load threat stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load IP reports with filters
|
||||
async function loadIPReports() {
|
||||
const filters = new URLSearchParams();
|
||||
|
||||
const service = document.getElementById('filter-service').value;
|
||||
if (service) filters.append('service', service);
|
||||
|
||||
const threatScore = document.getElementById('filter-threat-score').value;
|
||||
if (threatScore) filters.append('min_threat_score', threatScore);
|
||||
|
||||
const blocked = document.getElementById('filter-blocked').value;
|
||||
if (blocked) filters.append('blocked', blocked);
|
||||
|
||||
filters.append('limit', '50');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/threat/reports?${filters.toString()}`);
|
||||
const data = await response.json();
|
||||
currentReports = data.reports || [];
|
||||
renderIPReports(currentReports);
|
||||
} catch (error) {
|
||||
console.error('Failed to load IP reports:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Render IP reports table
|
||||
function renderIPReports(reports) {
|
||||
const tbody = document.getElementById('ip-reports-table');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (reports.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="px-4 py-4 text-center text-gray-400">No data available</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
reports.forEach(report => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-800';
|
||||
|
||||
const threatScoreColor = getThreatScoreColor(report.threat_score);
|
||||
const statusBadge = report.is_blocked
|
||||
? '<span class="px-2 py-1 text-xs bg-red-600 text-white rounded">Blocked</span>'
|
||||
: '<span class="px-2 py-1 text-xs bg-green-600 text-white rounded">Active</span>';
|
||||
|
||||
const services = (report.services || []).join(', ') || 'None';
|
||||
const lastSeen = report.last_seen ? new Date(report.last_seen).toLocaleString() : 'Never';
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="px-4 py-2 text-sm text-gray-300">
|
||||
<button class="text-primary-400 hover:text-primary-300" onclick="showIPDetails('${report.ip}')">
|
||||
${report.ip}
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-sm">
|
||||
<span class="font-semibold ${threatScoreColor}">${report.threat_score}</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">${report.total_connections}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">${services}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">${lastSeen}</td>
|
||||
<td class="px-4 py-2 text-sm">${statusBadge}</td>
|
||||
<td class="px-4 py-2 text-sm">
|
||||
<div class="flex space-x-2">
|
||||
${!report.is_blocked
|
||||
? `<button onclick="blockIP('${report.ip}')" class="px-2 py-1 text-xs bg-red-600 hover:bg-red-500 text-white rounded">Block</button>`
|
||||
: `<button onclick="unblockIP('${report.ip}')" class="px-2 py-1 text-xs bg-green-600 hover:bg-green-500 text-white rounded">Unblock</button>`
|
||||
}
|
||||
<button onclick="showThreatEvents('${report.ip}')" class="px-2 py-1 text-xs bg-blue-600 hover:bg-blue-500 text-white rounded">Events</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Get color class for threat score
|
||||
function getThreatScoreColor(score) {
|
||||
if (score >= 80) return 'text-red-400';
|
||||
if (score >= 60) return 'text-orange-400';
|
||||
if (score >= 40) return 'text-yellow-400';
|
||||
if (score >= 20) return 'text-blue-400';
|
||||
return 'text-gray-400';
|
||||
}
|
||||
|
||||
// Show IP details
|
||||
async function showIPDetails(ip) {
|
||||
try {
|
||||
const response = await fetch(`/api/threat/ip/${ip}`);
|
||||
const report = await response.json();
|
||||
|
||||
document.getElementById('modal-title').textContent = `IP Analysis: ${ip}`;
|
||||
|
||||
const content = `
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 class="font-semibold text-white mb-2">Statistics</h4>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div>Total Connections: <span class="text-primary-400">${report.total_connections}</span></div>
|
||||
<div>Auth Attempts: <span class="text-primary-400">${report.total_auth_attempts}</span></div>
|
||||
<div>Threat Score: <span class="font-semibold ${getThreatScoreColor(report.threat_score)}">${report.threat_score}</span></div>
|
||||
<div>Status: ${report.is_blocked ? '<span class="text-red-400">Blocked</span>' : '<span class="text-green-400">Active</span>'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-white mb-2">Timeline</h4>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div>First Seen: <span class="text-gray-300">${report.first_seen ? new Date(report.first_seen).toLocaleString() : 'Unknown'}</span></div>
|
||||
<div>Last Seen: <span class="text-gray-300">${report.last_seen ? new Date(report.last_seen).toLocaleString() : 'Unknown'}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-semibold text-white mb-2">Services Accessed</h4>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
${(report.services || []).map(service =>
|
||||
`<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">${service}</span>`
|
||||
).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-semibold text-white mb-2">Recent Threat Events</h4>
|
||||
<div class="max-h-64 overflow-y-auto">
|
||||
${renderThreatEventsTable(report.threat_events || [])}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('modal-content').innerHTML = content;
|
||||
document.getElementById('threat-events-modal').classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Failed to load IP details:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Show threat events for IP
|
||||
async function showThreatEvents(ip) {
|
||||
try {
|
||||
const response = await fetch(`/api/threat/events?ip=${ip}&limit=100`);
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('modal-title').textContent = `Threat Events: ${ip}`;
|
||||
document.getElementById('modal-content').innerHTML = renderThreatEventsTable(data.events || []);
|
||||
document.getElementById('threat-events-modal').classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Failed to load threat events:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Render threat events table
|
||||
function renderThreatEventsTable(events) {
|
||||
if (events.length === 0) {
|
||||
return '<div class="text-center text-gray-400 py-4">No threat events found</div>';
|
||||
}
|
||||
|
||||
return `
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<thead class="bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-300 uppercase">Type</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-300 uppercase">Severity</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-300 uppercase">Service</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-300 uppercase">Count</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-300 uppercase">Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800">
|
||||
${events.map(event => `
|
||||
<tr>
|
||||
<td class="px-3 py-2 text-sm text-gray-300">${event.event_type}</td>
|
||||
<td class="px-3 py-2 text-sm">
|
||||
<span class="px-2 py-1 text-xs rounded ${getSeverityColor(event.severity)}">${event.severity}</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-300">${event.service}</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-300">${event.count}</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-300">${new Date(event.last_seen).toLocaleString()}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
// Get severity color class
|
||||
function getSeverityColor(severity) {
|
||||
switch (severity) {
|
||||
case 'critical': return 'bg-red-600 text-white';
|
||||
case 'high': return 'bg-orange-600 text-white';
|
||||
case 'medium': return 'bg-yellow-600 text-white';
|
||||
case 'low': return 'bg-blue-600 text-white';
|
||||
default: return 'bg-gray-600 text-white';
|
||||
}
|
||||
}
|
||||
|
||||
// Block IP
|
||||
async function blockIP(ip) {
|
||||
if (!confirm(`Are you sure you want to block IP ${ip}?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/threat/block', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ip: ip, reason: 'Manual block from dashboard' })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadIPReports(); // Refresh the table
|
||||
loadThreatStats(); // Refresh stats
|
||||
} else {
|
||||
alert('Failed to block IP');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to block IP:', error);
|
||||
alert('Failed to block IP');
|
||||
}
|
||||
}
|
||||
|
||||
// Unblock IP
|
||||
async function unblockIP(ip) {
|
||||
if (!confirm(`Are you sure you want to unblock IP ${ip}?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/threat/unblock', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ip: ip })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadIPReports(); // Refresh the table
|
||||
loadThreatStats(); // Refresh stats
|
||||
} else {
|
||||
alert('Failed to unblock IP');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to unblock IP:', error);
|
||||
alert('Failed to unblock IP');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
@@ -0,0 +1,373 @@
|
||||
{{ define "threat_rules_title" }}Threat Rules{{ end }}
|
||||
{{ define "threat_rules_content" }}
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-semibold text-white">Threat Detection Rules</h1>
|
||||
<button id="add-rule-btn" class="px-4 py-2 bg-primary-600 hover:bg-primary-500 rounded text-white">
|
||||
Add New Rule
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Rules Table -->
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-lg">
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-white">Active Rules</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<thead class="bg-gray-800">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Name</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Service</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Condition</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Threshold</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Time Window</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Action</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rules-table" class="bg-gray-900 divide-y divide-gray-800">
|
||||
<!-- Dynamic content will be inserted here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rule Form Modal -->
|
||||
<div id="rule-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-gray-800 rounded-lg max-w-2xl w-full">
|
||||
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
|
||||
<h3 id="modal-title" class="text-lg font-semibold text-white">Add New Rule</h3>
|
||||
<button id="close-modal" class="text-gray-400 hover:text-white">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form id="rule-form" class="p-4 space-y-4">
|
||||
<input type="hidden" id="rule-id" name="id">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Rule Name</label>
|
||||
<input type="text" id="rule-name" name="name" required
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100"
|
||||
placeholder="e.g., SSH Brute Force Detection">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Service</label>
|
||||
<select id="rule-service" name="service" required
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100">
|
||||
<option value="*">All Services</option>
|
||||
<option value="ssh">SSH</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
<option value="ftp">FTP</option>
|
||||
<option value="smtp">SMTP</option>
|
||||
<option value="imap">IMAP</option>
|
||||
<option value="telnet">Telnet</option>
|
||||
<option value="mysql">MySQL</option>
|
||||
<option value="postgresql">PostgreSQL</option>
|
||||
<option value="mongodb">MongoDB</option>
|
||||
<option value="rdp">RDP</option>
|
||||
<option value="smb">SMB</option>
|
||||
<option value="sip">SIP</option>
|
||||
<option value="vnc">VNC</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Description</label>
|
||||
<textarea id="rule-description" name="description" rows="2"
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100"
|
||||
placeholder="Describe what this rule detects..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Condition</label>
|
||||
<select id="rule-condition" name="condition" required
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100">
|
||||
<option value="connection_count">Connection Count</option>
|
||||
<option value="auth_attempts">Authentication Attempts</option>
|
||||
<option value="service_diversity">Service Diversity</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Threshold</label>
|
||||
<input type="number" id="rule-threshold" name="threshold" required min="1"
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100"
|
||||
placeholder="e.g., 10">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Time Window (minutes)</label>
|
||||
<input type="number" id="rule-time-window" name="time_window" required min="1"
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100"
|
||||
placeholder="e.g., 60">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Action</label>
|
||||
<select id="rule-action" name="action" required
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100">
|
||||
<option value="alert">Alert Only</option>
|
||||
<option value="block">Block IP</option>
|
||||
<option value="monitor">Monitor</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="flex items-center text-sm text-gray-300">
|
||||
<input type="checkbox" id="rule-enabled" name="enabled" checked
|
||||
class="mr-2 h-4 w-4 text-primary-600 bg-gray-900 border-gray-700 rounded">
|
||||
Rule Enabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button type="button" id="cancel-btn" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-primary-600 hover:bg-primary-500 rounded text-white">
|
||||
Save Rule
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentRules = [];
|
||||
let editingRuleId = null;
|
||||
|
||||
// Load initial data
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadRules();
|
||||
});
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('add-rule-btn').addEventListener('click', function() {
|
||||
showRuleModal();
|
||||
});
|
||||
|
||||
document.getElementById('close-modal').addEventListener('click', hideRuleModal);
|
||||
document.getElementById('cancel-btn').addEventListener('click', hideRuleModal);
|
||||
|
||||
document.getElementById('rule-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
saveRule();
|
||||
});
|
||||
|
||||
// Load threat rules
|
||||
async function loadRules() {
|
||||
try {
|
||||
const response = await fetch('/api/threat/rules');
|
||||
const data = await response.json();
|
||||
currentRules = data.rules || [];
|
||||
renderRulesTable(currentRules);
|
||||
} catch (error) {
|
||||
console.error('Failed to load rules:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Render rules table
|
||||
function renderRulesTable(rules) {
|
||||
const tbody = document.getElementById('rules-table');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (rules.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="px-4 py-4 text-center text-gray-400">No rules configured</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
rules.forEach(rule => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-800';
|
||||
|
||||
const statusBadge = rule.enabled
|
||||
? '<span class="px-2 py-1 text-xs bg-green-600 text-white rounded">Enabled</span>'
|
||||
: '<span class="px-2 py-1 text-xs bg-gray-600 text-white rounded">Disabled</span>';
|
||||
|
||||
const actionBadge = getActionBadge(rule.action);
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="px-4 py-2 text-sm text-gray-300">
|
||||
<div class="font-medium">${rule.name}</div>
|
||||
<div class="text-xs text-gray-500">${rule.description || ''}</div>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">
|
||||
<span class="px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">${rule.service}</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">${rule.condition}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">${rule.threshold}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">${rule.time_window}m</td>
|
||||
<td class="px-4 py-2 text-sm">${actionBadge}</td>
|
||||
<td class="px-4 py-2 text-sm">${statusBadge}</td>
|
||||
<td class="px-4 py-2 text-sm">
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="editRule(${rule.id})" class="px-2 py-1 text-xs bg-blue-600 hover:bg-blue-500 text-white rounded">Edit</button>
|
||||
<button onclick="toggleRule(${rule.id}, ${!rule.enabled})" class="px-2 py-1 text-xs ${rule.enabled ? 'bg-gray-600 hover:bg-gray-500' : 'bg-green-600 hover:bg-green-500'} text-white rounded">
|
||||
${rule.enabled ? 'Disable' : 'Enable'}
|
||||
</button>
|
||||
<button onclick="deleteRule(${rule.id})" class="px-2 py-1 text-xs bg-red-600 hover:bg-red-500 text-white rounded">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Get action badge HTML
|
||||
function getActionBadge(action) {
|
||||
switch (action) {
|
||||
case 'block':
|
||||
return '<span class="px-2 py-1 text-xs bg-red-600 text-white rounded">Block</span>';
|
||||
case 'alert':
|
||||
return '<span class="px-2 py-1 text-xs bg-yellow-600 text-white rounded">Alert</span>';
|
||||
case 'monitor':
|
||||
return '<span class="px-2 py-1 text-xs bg-blue-600 text-white rounded">Monitor</span>';
|
||||
default:
|
||||
return '<span class="px-2 py-1 text-xs bg-gray-600 text-white rounded">Unknown</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Show rule modal
|
||||
function showRuleModal(rule = null) {
|
||||
editingRuleId = rule ? rule.id : null;
|
||||
|
||||
if (rule) {
|
||||
document.getElementById('modal-title').textContent = 'Edit Rule';
|
||||
document.getElementById('rule-id').value = rule.id;
|
||||
document.getElementById('rule-name').value = rule.name;
|
||||
document.getElementById('rule-description').value = rule.description || '';
|
||||
document.getElementById('rule-service').value = rule.service;
|
||||
document.getElementById('rule-condition').value = rule.condition;
|
||||
document.getElementById('rule-threshold').value = rule.threshold;
|
||||
document.getElementById('rule-time-window').value = rule.time_window;
|
||||
document.getElementById('rule-action').value = rule.action;
|
||||
document.getElementById('rule-enabled').checked = rule.enabled;
|
||||
} else {
|
||||
document.getElementById('modal-title').textContent = 'Add New Rule';
|
||||
document.getElementById('rule-form').reset();
|
||||
document.getElementById('rule-enabled').checked = true;
|
||||
}
|
||||
|
||||
document.getElementById('rule-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Hide rule modal
|
||||
function hideRuleModal() {
|
||||
document.getElementById('rule-modal').classList.add('hidden');
|
||||
editingRuleId = null;
|
||||
}
|
||||
|
||||
// Save rule
|
||||
async function saveRule() {
|
||||
const formData = new FormData(document.getElementById('rule-form'));
|
||||
const ruleData = {
|
||||
name: formData.get('name'),
|
||||
description: formData.get('description'),
|
||||
service: formData.get('service'),
|
||||
condition: formData.get('condition'),
|
||||
threshold: parseInt(formData.get('threshold')),
|
||||
time_window: parseInt(formData.get('time_window')),
|
||||
action: formData.get('action'),
|
||||
enabled: formData.has('enabled')
|
||||
};
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (editingRuleId) {
|
||||
// Update existing rule
|
||||
response = await fetch(`/api/threat/rules/${editingRuleId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ruleData)
|
||||
});
|
||||
} else {
|
||||
// Create new rule
|
||||
response = await fetch('/api/threat/rules', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(ruleData)
|
||||
});
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
hideRuleModal();
|
||||
loadRules();
|
||||
} else {
|
||||
const error = await response.text();
|
||||
alert(`Failed to save rule: ${error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save rule:', error);
|
||||
alert('Failed to save rule');
|
||||
}
|
||||
}
|
||||
|
||||
// Edit rule
|
||||
function editRule(ruleId) {
|
||||
const rule = currentRules.find(r => r.id === ruleId);
|
||||
if (rule) {
|
||||
showRuleModal(rule);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle rule enabled/disabled
|
||||
async function toggleRule(ruleId, enabled) {
|
||||
const rule = currentRules.find(r => r.id === ruleId);
|
||||
if (!rule) return;
|
||||
|
||||
const updatedRule = { ...rule, enabled: enabled };
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/threat/rules/${ruleId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updatedRule)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadRules();
|
||||
} else {
|
||||
alert('Failed to update rule');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle rule:', error);
|
||||
alert('Failed to update rule');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete rule
|
||||
async function deleteRule(ruleId) {
|
||||
const rule = currentRules.find(r => r.id === ruleId);
|
||||
if (!rule) return;
|
||||
|
||||
if (!confirm(`Are you sure you want to delete the rule "${rule.name}"?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/threat/rules/${ruleId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadRules();
|
||||
} else {
|
||||
alert('Failed to delete rule');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete rule:', error);
|
||||
alert('Failed to delete rule');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
@@ -0,0 +1,608 @@
|
||||
{{ define "users_title" }}User Management{{ end }}
|
||||
{{ define "users_content" }}
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-semibold text-white">User Management</h1>
|
||||
<button id="add-user-btn" class="px-4 py-2 bg-primary-600 hover:bg-primary-500 rounded text-white">
|
||||
Add New User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Current User Info -->
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4">
|
||||
<h2 class="text-lg font-semibold text-white mb-2">Current User</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-gray-400">Username:</span>
|
||||
<span class="text-white ml-2" id="current-username">{{ .CurrentUser.Username }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-400">Role:</span>
|
||||
<span class="text-white ml-2 capitalize" id="current-role">{{ .CurrentUser.Role }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-400">Last Login:</span>
|
||||
<span class="text-white ml-2" id="current-last-login">
|
||||
{{ if .CurrentUser.LastLogin }}{{ .CurrentUser.LastLogin.Format "2006-01-02 15:04:05" }}{{ else }}Never{{ end }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-lg">
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-white">All Users</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<thead class="bg-gray-800">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Username</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Email</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Role</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Last Login</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Created</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-table" class="bg-gray-900 divide-y divide-gray-800">
|
||||
<!-- Dynamic content will be inserted here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Keys Section -->
|
||||
<div class="bg-gray-800 border border-gray-700 rounded-lg">
|
||||
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
|
||||
<h2 class="text-lg font-semibold text-white">My API Keys</h2>
|
||||
<button id="add-apikey-btn" class="px-3 py-1 bg-blue-600 hover:bg-blue-500 rounded text-white text-sm">
|
||||
Generate API Key
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<thead class="bg-gray-800">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Name</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Key</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Last Used</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Expires</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="apikeys-table" class="bg-gray-900 divide-y divide-gray-800">
|
||||
<!-- Dynamic content will be inserted here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Form Modal -->
|
||||
<div id="user-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-gray-800 rounded-lg max-w-md w-full">
|
||||
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
|
||||
<h3 id="user-modal-title" class="text-lg font-semibold text-white">Add New User</h3>
|
||||
<button id="close-user-modal" class="text-gray-400 hover:text-white">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form id="user-form" class="p-4 space-y-4">
|
||||
<input type="hidden" id="user-id" name="id">
|
||||
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Username</label>
|
||||
<input type="text" id="user-username" name="username" required
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100"
|
||||
placeholder="Enter username">
|
||||
<div id="username-error" class="text-red-400 text-sm mt-1 hidden"></div>
|
||||
</div>
|
||||
|
||||
<div id="password-field">
|
||||
<label class="block text-sm text-gray-300 mb-1">Password</label>
|
||||
<input type="password" id="user-password" name="password" required
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100"
|
||||
placeholder="Enter password">
|
||||
<div id="password-error" class="text-red-400 text-sm mt-1 hidden"></div>
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
Password must be at least 8 characters with uppercase, lowercase, digit, and special character.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Email (Optional)</label>
|
||||
<input type="email" id="user-email" name="email"
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100"
|
||||
placeholder="Enter email address">
|
||||
<div id="email-error" class="text-red-400 text-sm mt-1 hidden"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Role</label>
|
||||
<select id="user-role" name="role" required
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100">
|
||||
<option value="readonly">Read Only</option>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="user-active" name="active" checked
|
||||
class="mr-2 h-4 w-4 text-primary-600 bg-gray-900 border-gray-700 rounded">
|
||||
<label for="user-active" class="text-sm text-gray-300">Active</label>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button type="button" id="cancel-user-btn" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-primary-600 hover:bg-primary-500 rounded text-white">
|
||||
Save User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key Form Modal -->
|
||||
<div id="apikey-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-gray-800 rounded-lg max-w-md w-full">
|
||||
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold text-white">Generate API Key</h3>
|
||||
<button id="close-apikey-modal" class="text-gray-400 hover:text-white">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form id="apikey-form" class="p-4 space-y-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Name</label>
|
||||
<input type="text" id="apikey-name" name="name" required
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100"
|
||||
placeholder="e.g., Production API, Mobile App">
|
||||
<div id="apikey-name-error" class="text-red-400 text-sm mt-1 hidden"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Expires In (Days)</label>
|
||||
<select id="apikey-expires" name="expires_in"
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-gray-100">
|
||||
<option value="">Never</option>
|
||||
<option value="30">30 days</option>
|
||||
<option value="90">90 days</option>
|
||||
<option value="365">1 year</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button type="button" id="cancel-apikey-btn" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-primary-600 hover:bg-primary-500 rounded text-white">
|
||||
Generate Key
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key Display Modal -->
|
||||
<div id="apikey-display-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-gray-800 rounded-lg max-w-lg w-full">
|
||||
<div class="p-4 border-b border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-white">API Key Generated</h3>
|
||||
</div>
|
||||
<div class="p-4 space-y-4">
|
||||
<div class="bg-yellow-900 border border-yellow-700 rounded p-3">
|
||||
<div class="text-yellow-200 text-sm font-medium">⚠️ Important</div>
|
||||
<div class="text-yellow-100 text-sm mt-1">
|
||||
Save this API key securely. It will not be shown again.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">API Key</label>
|
||||
<div class="flex">
|
||||
<input type="text" id="generated-apikey" readonly
|
||||
class="flex-1 bg-gray-900 border border-gray-700 rounded-l px-3 py-2 text-gray-100 font-mono text-sm">
|
||||
<button id="copy-apikey" class="px-3 py-2 bg-primary-600 hover:bg-primary-500 rounded-r text-white text-sm">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button id="close-apikey-display" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded text-white">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentUsers = [];
|
||||
let currentAPIKeys = [];
|
||||
let editingUserId = null;
|
||||
const currentUserId = parseInt('{{ .CurrentUser.ID }}');
|
||||
|
||||
// Load initial data
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadUsers();
|
||||
loadAPIKeys();
|
||||
});
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('add-user-btn').addEventListener('click', function() {
|
||||
showUserModal();
|
||||
});
|
||||
|
||||
document.getElementById('add-apikey-btn').addEventListener('click', function() {
|
||||
showAPIKeyModal();
|
||||
});
|
||||
|
||||
document.getElementById('close-user-modal').addEventListener('click', hideUserModal);
|
||||
document.getElementById('cancel-user-btn').addEventListener('click', hideUserModal);
|
||||
document.getElementById('close-apikey-modal').addEventListener('click', hideAPIKeyModal);
|
||||
document.getElementById('cancel-apikey-btn').addEventListener('click', hideAPIKeyModal);
|
||||
document.getElementById('close-apikey-display').addEventListener('click', hideAPIKeyDisplayModal);
|
||||
|
||||
document.getElementById('user-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
saveUser();
|
||||
});
|
||||
|
||||
document.getElementById('apikey-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
generateAPIKey();
|
||||
});
|
||||
|
||||
document.getElementById('copy-apikey').addEventListener('click', function() {
|
||||
const input = document.getElementById('generated-apikey');
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
this.textContent = 'Copied!';
|
||||
setTimeout(() => this.textContent = 'Copy', 2000);
|
||||
});
|
||||
|
||||
// Load users
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const response = await fetch('/api/users');
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
currentUsers = data.users || [];
|
||||
renderUsersTable(currentUsers);
|
||||
} else {
|
||||
console.error('Failed to load users:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load users:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load API keys
|
||||
async function loadAPIKeys() {
|
||||
try {
|
||||
const response = await fetch('/api/apikeys');
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
currentAPIKeys = data.api_keys || [];
|
||||
renderAPIKeysTable(currentAPIKeys);
|
||||
} else {
|
||||
console.error('Failed to load API keys:', data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load API keys:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Render users table
|
||||
function renderUsersTable(users) {
|
||||
const tbody = document.getElementById('users-table');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (users.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="px-4 py-4 text-center text-gray-400">No users found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
users.forEach(user => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-800';
|
||||
|
||||
const statusBadge = user.active
|
||||
? '<span class="px-2 py-1 text-xs bg-green-600 text-white rounded">Active</span>'
|
||||
: '<span class="px-2 py-1 text-xs bg-red-600 text-white rounded">Inactive</span>';
|
||||
|
||||
const roleBadge = getRoleBadge(user.role);
|
||||
const lastLogin = user.last_login ? new Date(user.last_login).toLocaleString() : 'Never';
|
||||
const created = new Date(user.created_at).toLocaleDateString();
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="px-4 py-2 text-sm text-gray-300 font-medium">${user.username}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">${user.email || '-'}</td>
|
||||
<td class="px-4 py-2 text-sm">${roleBadge}</td>
|
||||
<td class="px-4 py-2 text-sm">${statusBadge}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">${lastLogin}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">${created}</td>
|
||||
<td class="px-4 py-2 text-sm">
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="editUser(${user.id})" class="px-2 py-1 text-xs bg-blue-600 hover:bg-blue-500 text-white rounded">Edit</button>
|
||||
${user.id !== currentUserId ? `<button onclick="deleteUser(${user.id})" class="px-2 py-1 text-xs bg-red-600 hover:bg-red-500 text-white rounded">Delete</button>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Render API keys table
|
||||
function renderAPIKeysTable(keys) {
|
||||
const tbody = document.getElementById('apikeys-table');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (keys.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="px-4 py-4 text-center text-gray-400">No API keys found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
keys.forEach(key => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-800';
|
||||
|
||||
const statusBadge = key.active
|
||||
? '<span class="px-2 py-1 text-xs bg-green-600 text-white rounded">Active</span>'
|
||||
: '<span class="px-2 py-1 text-xs bg-gray-600 text-white rounded">Revoked</span>';
|
||||
|
||||
const lastUsed = key.last_used ? new Date(key.last_used).toLocaleString() : 'Never';
|
||||
const expires = key.expires_at ? new Date(key.expires_at).toLocaleDateString() : 'Never';
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="px-4 py-2 text-sm text-gray-300 font-medium">${key.name}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300 font-mono">${key.key}</td>
|
||||
<td class="px-4 py-2 text-sm">${statusBadge}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">${lastUsed}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-300">${expires}</td>
|
||||
<td class="px-4 py-2 text-sm">
|
||||
${key.active ? `<button onclick="revokeAPIKey(${key.id})" class="px-2 py-1 text-xs bg-red-600 hover:bg-red-500 text-white rounded">Revoke</button>` : ''}
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Get role badge HTML
|
||||
function getRoleBadge(role) {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
return '<span class="px-2 py-1 text-xs bg-red-600 text-white rounded">Admin</span>';
|
||||
case 'user':
|
||||
return '<span class="px-2 py-1 text-xs bg-blue-600 text-white rounded">User</span>';
|
||||
case 'readonly':
|
||||
return '<span class="px-2 py-1 text-xs bg-gray-600 text-white rounded">Read Only</span>';
|
||||
default:
|
||||
return '<span class="px-2 py-1 text-xs bg-gray-600 text-white rounded">Unknown</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Show user modal
|
||||
function showUserModal(user = null) {
|
||||
editingUserId = user ? user.id : null;
|
||||
|
||||
if (user) {
|
||||
document.getElementById('user-modal-title').textContent = 'Edit User';
|
||||
document.getElementById('user-id').value = user.id;
|
||||
document.getElementById('user-username').value = user.username;
|
||||
document.getElementById('user-email').value = user.email || '';
|
||||
document.getElementById('user-role').value = user.role;
|
||||
document.getElementById('user-active').checked = user.active;
|
||||
document.getElementById('password-field').style.display = 'none';
|
||||
} else {
|
||||
document.getElementById('user-modal-title').textContent = 'Add New User';
|
||||
document.getElementById('user-form').reset();
|
||||
document.getElementById('password-field').style.display = 'block';
|
||||
document.getElementById('user-role').value = 'user';
|
||||
}
|
||||
|
||||
// Clear errors
|
||||
clearFormErrors();
|
||||
|
||||
document.getElementById('user-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Hide user modal
|
||||
function hideUserModal() {
|
||||
document.getElementById('user-modal').classList.add('hidden');
|
||||
editingUserId = null;
|
||||
}
|
||||
|
||||
// Show API key modal
|
||||
function showAPIKeyModal() {
|
||||
document.getElementById('apikey-form').reset();
|
||||
clearAPIKeyFormErrors();
|
||||
document.getElementById('apikey-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Hide API key modal
|
||||
function hideAPIKeyModal() {
|
||||
document.getElementById('apikey-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Hide API key display modal
|
||||
function hideAPIKeyDisplayModal() {
|
||||
document.getElementById('apikey-display-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Save user
|
||||
async function saveUser() {
|
||||
clearFormErrors();
|
||||
|
||||
const formData = new FormData(document.getElementById('user-form'));
|
||||
const userData = {
|
||||
username: formData.get('username'),
|
||||
email: formData.get('email'),
|
||||
role: formData.get('role'),
|
||||
active: formData.has('active')
|
||||
};
|
||||
|
||||
if (!editingUserId) {
|
||||
userData.password = formData.get('password');
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (editingUserId) {
|
||||
response = await fetch(`/api/users/${editingUserId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
} else {
|
||||
response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
hideUserModal();
|
||||
loadUsers();
|
||||
} else {
|
||||
showFormError(data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
showFormError('Failed to save user');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate API key
|
||||
async function generateAPIKey() {
|
||||
clearAPIKeyFormErrors();
|
||||
|
||||
const formData = new FormData(document.getElementById('apikey-form'));
|
||||
const keyData = {
|
||||
name: formData.get('name'),
|
||||
permissions: ['read', 'write'] // Default permissions
|
||||
};
|
||||
|
||||
const expiresIn = formData.get('expires_in');
|
||||
if (expiresIn) {
|
||||
keyData.expires_in = parseInt(expiresIn);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/apikeys', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(keyData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
hideAPIKeyModal();
|
||||
document.getElementById('generated-apikey').value = data.api_key.key;
|
||||
document.getElementById('apikey-display-modal').classList.remove('hidden');
|
||||
loadAPIKeys();
|
||||
} else {
|
||||
showAPIKeyFormError(data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
showAPIKeyFormError('Failed to generate API key');
|
||||
}
|
||||
}
|
||||
|
||||
// Edit user
|
||||
function editUser(userId) {
|
||||
const user = currentUsers.find(u => u.id === userId);
|
||||
if (user) {
|
||||
showUserModal(user);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete user
|
||||
async function deleteUser(userId) {
|
||||
if (!confirm('Are you sure you want to delete this user?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadUsers();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
alert('Failed to delete user: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Failed to delete user');
|
||||
}
|
||||
}
|
||||
|
||||
// Revoke API key
|
||||
async function revokeAPIKey(keyId) {
|
||||
if (!confirm('Are you sure you want to revoke this API key?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/apikeys/${keyId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadAPIKeys();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
alert('Failed to revoke API key: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Failed to revoke API key');
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling functions
|
||||
function clearFormErrors() {
|
||||
document.getElementById('username-error').classList.add('hidden');
|
||||
document.getElementById('password-error').classList.add('hidden');
|
||||
document.getElementById('email-error').classList.add('hidden');
|
||||
}
|
||||
|
||||
function clearAPIKeyFormErrors() {
|
||||
document.getElementById('apikey-name-error').classList.add('hidden');
|
||||
}
|
||||
|
||||
function showFormError(message) {
|
||||
// Show error in appropriate field or general error
|
||||
alert('Error: ' + message);
|
||||
}
|
||||
|
||||
function showAPIKeyFormError(message) {
|
||||
alert('Error: ' + message);
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
+151
-34
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user