added authentication and API

This commit is contained in:
2025-09-28 15:28:39 +01:00
parent 8235d7eedd
commit 1c1818b29c
14 changed files with 4762 additions and 42 deletions
BIN
View File
Binary file not shown.
+513
View File
@@ -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
}
+482
View File
@@ -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[:])
}
+312
View File
@@ -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
}
+391
View File
@@ -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()
}
+349
View File
@@ -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)
}
}
+581
View File
@@ -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()
}
+553
View File
@@ -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
View File
@@ -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
+12
View File
@@ -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>
+395
View File
@@ -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 }}
+373
View File
@@ -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 }}
+608
View File
@@ -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
View File
@@ -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)