diff --git a/install.sh b/install.sh index 7e67bd7..3f1bd05 100755 --- a/install.sh +++ b/install.sh @@ -67,12 +67,26 @@ mkdir -p "$DATA_DIR" mkdir -p "$RUN_DIR" mkdir -p "$(dirname $INSTALL_PREFIX/bin)" +# Stop service if running (to avoid "Text file busy" error) +SERVICE_WAS_RUNNING=false +if systemctl is-active --quiet secrds 2>/dev/null; then + echo -e "${YELLOW}Stopping secrds service to update binaries...${NC}" + systemctl stop secrds + SERVICE_WAS_RUNNING=true + sleep 1 +fi + # Install binaries echo -e "${YELLOW}Installing binaries...${NC}" # Install secrds-agent if [ -f "target/release/secrds-agent" ]; then - cp target/release/secrds-agent "$INSTALL_PREFIX/bin/secrds-agent" + # Try to copy, if it fails due to busy file, wait and retry + if ! cp target/release/secrds-agent "$INSTALL_PREFIX/bin/secrds-agent" 2>/dev/null; then + echo -e "${YELLOW}Waiting for file to be released...${NC}" + sleep 2 + cp target/release/secrds-agent "$INSTALL_PREFIX/bin/secrds-agent" + fi chmod +x "$INSTALL_PREFIX/bin/secrds-agent" echo -e "${GREEN}Installed secrds-agent${NC}" else @@ -145,14 +159,69 @@ chown -R root:root "$CONFIG_DIR" chown -R root:root "$DATA_DIR" chmod 755 "$DATA_DIR" +# Enable service +echo -e "${YELLOW}Enabling secrds service...${NC}" +if systemctl is-enabled secrds &>/dev/null; then + echo -e "${GREEN}Service already enabled${NC}" +else + systemctl enable secrds + echo -e "${GREEN}Service enabled for auto-start${NC}" +fi + +# Reload systemd to pick up any service file changes +systemctl daemon-reload + +# Check if config has Telegram credentials +if grep -q "your_bot_token_here\|your_chat_id_here" "$CONFIG_DIR/config.yaml" 2>/dev/null; then + echo -e "${YELLOW}Warning: Telegram credentials not configured yet${NC}" + echo -e "${YELLOW}The service will start but alerts won't be sent to Telegram${NC}" + echo "" + echo -e "${YELLOW}To start the service now, run:${NC}" + echo " systemctl start secrds" + echo "" + echo -e "${YELLOW}To configure Telegram later:${NC}" + echo " 1. Edit $CONFIG_DIR/config.yaml" + echo " 2. Set telegram.bot_token and telegram.chat_id" + echo " 3. Restart: systemctl restart secrds" +else + # Start or restart the service if config is ready + if [ "$SERVICE_WAS_RUNNING" = true ]; then + echo -e "${YELLOW}Restarting service...${NC}" + if systemctl start secrds; then + sleep 2 + if systemctl is-active --quiet secrds; then + echo -e "${GREEN}Service restarted successfully${NC}" + else + echo -e "${YELLOW}Service restarted but may have issues. Check: systemctl status secrds${NC}" + fi + else + echo -e "${YELLOW}Failed to restart service. Check: systemctl status secrds${NC}" + fi + else + # Start the service if config is ready + if systemctl start secrds; then + sleep 2 + if systemctl is-active --quiet secrds; then + echo -e "${GREEN}Service started successfully${NC}" + else + echo -e "${YELLOW}Service started but may have issues. Check: systemctl status secrds${NC}" + fi + else + echo -e "${YELLOW}Failed to start service. Check: systemctl status secrds${NC}" + fi + fi +fi + +echo "" echo -e "${GREEN}Installation complete!${NC}" echo "" -echo -e "${YELLOW}Next steps:${NC}" -echo "1. Edit $CONFIG_DIR/config.yaml and set telegram.bot_token and telegram.chat_id" -echo "2. Optionally edit $CONFIG_DIR/config.yaml to customize thresholds" -echo "3. Start the service: systemctl start secrds" -echo "4. Enable auto-start: systemctl enable secrds" -echo "5. Check status: systemctl status secrds" +echo -e "${YELLOW}Useful commands:${NC}" +echo " Check status: systemctl status secrds" +echo " View logs: journalctl -u secrds -f" +echo " Stop service: systemctl stop secrds" +echo " Start service: systemctl start secrds" +echo " Restart: systemctl restart secrds" +echo " View alerts: secrds alerts" echo "" echo -e "${GREEN}Installation successful!${NC}" diff --git a/secrds-agent/internal/detector/threat.go b/secrds-agent/internal/detector/threat.go index 05c1a17..e93c196 100644 --- a/secrds-agent/internal/detector/threat.go +++ b/secrds-agent/internal/detector/threat.go @@ -12,13 +12,60 @@ import ( "github.com/secrds/secrds-agent/internal/telegram" ) +// EventType constants matching common.h +const ( + SSH_ATTEMPT = 0 + SSH_FAILURE = 1 + SSH_SUCCESS = 2 + TCP_CONNECT = 3 + TCP_ACCEPT = 4 + TCP_CLOSE = 5 +) + +// ThreatSeverity levels +type ThreatSeverity string + +const ( + SeverityLow ThreatSeverity = "LOW" + SeverityMedium ThreatSeverity = "MEDIUM" + SeverityHigh ThreatSeverity = "HIGH" + SeverityCritical ThreatSeverity = "CRITICAL" +) + +// SSHEventDetail tracks detailed SSH event information +type SSHEventDetail struct { + Timestamp time.Time + EventType uint8 + Port uint16 + PID uint32 +} + +// TCPConnectionDetail tracks TCP connection details +type TCPConnectionDetail struct { + Timestamp time.Time + SrcPort uint16 + DstPort uint16 + EventType uint8 +} + +// IPBehavior tracks behavioral patterns for an IP +type IPBehavior struct { + SSHEvents []SSHEventDetail + TCPConnections []TCPConnectionDetail + FailedSSHCount uint64 + SuccessfulSSHCount uint64 + UniquePorts map[uint16]bool + FirstSeen time.Time + LastSeen time.Time + TotalConnections uint64 +} + type ThreatDetector struct { config *config.Config storage *storage.Storage telegramClient *telegram.Client - mu sync.Mutex - sshAttempts map[string][]time.Time - tcpConnections map[string][]time.Time + mu sync.RWMutex + ipBehaviors map[string]*IPBehavior blockedIPs map[string]bool } @@ -27,8 +74,7 @@ func New(cfg *config.Config, st *storage.Storage, tg *telegram.Client) *ThreatDe config: cfg, storage: st, telegramClient: tg, - sshAttempts: make(map[string][]time.Time), - tcpConnections: make(map[string][]time.Time), + ipBehaviors: make(map[string]*IPBehavior), blockedIPs: make(map[string]bool), } } @@ -46,51 +92,44 @@ func (td *ThreatDetector) ProcessSSHEvent(ip uint32, port uint16, pid uint32, ev defer td.mu.Unlock() now := time.Now() - window := td.config.SSHWindow() - - // Clean old attempts - attempts := td.sshAttempts[ipStr] - validAttempts := []time.Time{} - for _, t := range attempts { - if now.Sub(t) < window { - validAttempts = append(validAttempts, t) - } - } - validAttempts = append(validAttempts, now) - td.sshAttempts[ipStr] = validAttempts - - attemptCount := uint64(len(validAttempts)) - if attemptCount > td.config.SSHThreshold { - alert := &storage.Alert{ - IP: ipStr, - ThreatType: storage.ThreatTypeSSHBruteForce, - Count: attemptCount, - Timestamp: now, - } + // Get or create IP behavior tracking + behavior := td.getOrCreateBehavior(ipStr) + + // Add event + event := SSHEventDetail{ + Timestamp: now, + EventType: eventType, + Port: port, + PID: pid, + } + behavior.SSHEvents = append(behavior.SSHEvents, event) + behavior.LastSeen = now - if err := td.storage.StoreAlert(alert); err != nil { - return fmt.Errorf("failed to store alert: %w", err) - } + // Track failed vs successful attempts + if eventType == SSH_FAILURE { + behavior.FailedSSHCount++ + } else if eventType == SSH_SUCCESS { + behavior.SuccessfulSSHCount++ + } - // Send Telegram alert - tgAlert := &telegram.Alert{ - IP: ipStr, - ThreatType: string(storage.ThreatTypeSSHBruteForce), - Count: attemptCount, - Timestamp: now, - } - if err := td.telegramClient.SendAlert(tgAlert); err != nil { - fmt.Printf("Failed to send Telegram alert: %v\n", err) + // Clean old events (keep last 24 hours) + cutoff := now.Add(-24 * time.Hour) + validEvents := []SSHEventDetail{} + for _, e := range behavior.SSHEvents { + if e.Timestamp.After(cutoff) { + validEvents = append(validEvents, e) } + } + behavior.SSHEvents = validEvents - // Block IP if enabled - if td.config.EnableIPBlocking { - if err := td.blockIP(ipStr); err != nil { - fmt.Printf("Failed to block IP %s: %v\n", ipStr, err) - } else { - td.storage.AddBlockedIP(ipStr) - } + // Advanced threat detection + threats := td.detectSSHThreats(ipStr, behavior, now) + + // Process detected threats + for _, threat := range threats { + if err := td.handleThreat(ipStr, threat); err != nil { + fmt.Printf("Failed to handle threat: %v\n", err) } } @@ -110,56 +149,377 @@ func (td *ThreatDetector) ProcessTCPEvent(srcIP, dstIP uint32, srcPort, dstPort defer td.mu.Unlock() now := time.Now() - window := td.config.TCPWindow() - - // Clean old connections - connections := td.tcpConnections[ipStr] - validConnections := []time.Time{} - for _, t := range connections { - if now.Sub(t) < window { - validConnections = append(validConnections, t) + + // Get or create IP behavior tracking + behavior := td.getOrCreateBehavior(ipStr) + + // Add connection + conn := TCPConnectionDetail{ + Timestamp: now, + SrcPort: srcPort, + DstPort: dstPort, + EventType: eventType, + } + behavior.TCPConnections = append(behavior.TCPConnections, conn) + behavior.LastSeen = now + behavior.TotalConnections++ + + // Track unique destination ports for port scan detection + if eventType == TCP_CONNECT { + if behavior.UniquePorts == nil { + behavior.UniquePorts = make(map[uint16]bool) + } + behavior.UniquePorts[dstPort] = true + } + + // Clean old connections (keep last 24 hours) + cutoff := now.Add(-24 * time.Hour) + validConns := []TCPConnectionDetail{} + for _, c := range behavior.TCPConnections { + if c.Timestamp.After(cutoff) { + validConns = append(validConns, c) + } + } + behavior.TCPConnections = validConns + + // Advanced threat detection + threats := td.detectTCPThreats(ipStr, behavior, now) + + // Process detected threats + for _, threat := range threats { + if err := td.handleThreat(ipStr, threat); err != nil { + fmt.Printf("Failed to handle threat: %v\n", err) + } + } + + return nil +} + +// ThreatInfo contains detailed threat information +type ThreatInfo struct { + ThreatType storage.ThreatType + Severity ThreatSeverity + Count uint64 + Details string + Score float64 +} + +func (td *ThreatDetector) detectSSHThreats(ip string, behavior *IPBehavior, now time.Time) []ThreatInfo { + var threats []ThreatInfo + + // Multi-window analysis + shortWindow := 1 * time.Minute + mediumWindow := 5 * time.Minute + longWindow := 15 * time.Minute + + shortTerm := td.countEventsInWindow(behavior.SSHEvents, now, shortWindow) + mediumTerm := td.countEventsInWindow(behavior.SSHEvents, now, mediumWindow) + longTerm := td.countEventsInWindow(behavior.SSHEvents, now, longWindow) + + // Calculate threat score with exponential weighting + score := td.calculateThreatScore(shortTerm, mediumTerm, longTerm) + + // Failed login analysis + failedInShort := td.countFailedInWindow(behavior.SSHEvents, now, shortWindow) + failedInMedium := td.countFailedInWindow(behavior.SSHEvents, now, mediumWindow) + + // Pattern 1: Rapid brute force attack (high frequency in short window) + if shortTerm >= 10 || (shortTerm >= 5 && failedInShort >= 5) { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeSSHBruteForce, + Severity: SeverityCritical, + Count: shortTerm, + Details: fmt.Sprintf("Rapid brute force: %d attempts in 1 minute", shortTerm), + Score: score, + }) + } else if mediumTerm >= 15 || (mediumTerm >= 10 && failedInMedium >= 10) { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeSSHBruteForce, + Severity: SeverityHigh, + Count: mediumTerm, + Details: fmt.Sprintf("Sustained brute force: %d attempts in 5 minutes", mediumTerm), + Score: score, + }) + } else if longTerm >= 20 { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeSSHBruteForce, + Severity: SeverityMedium, + Count: longTerm, + Details: fmt.Sprintf("Persistent brute force: %d attempts in 15 minutes", longTerm), + Score: score, + }) + } + + // Pattern 2: High failure rate (suspicious activity) + totalAttempts := uint64(len(behavior.SSHEvents)) + if totalAttempts > 0 { + failureRate := float64(behavior.FailedSSHCount) / float64(totalAttempts) + if failureRate > 0.8 && totalAttempts >= 5 { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeSSHBruteForce, + Severity: SeverityHigh, + Count: behavior.FailedSSHCount, + Details: fmt.Sprintf("High failure rate: %.1f%% failures (%d/%d)", failureRate*100, behavior.FailedSSHCount, totalAttempts), + Score: score * failureRate, + }) + } + } + + // Pattern 3: Timing pattern analysis (rapid-fire attempts) + if len(behavior.SSHEvents) >= 3 { + rapidFire := td.detectRapidFirePattern(behavior.SSHEvents, now) + if rapidFire { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeSSHBruteForce, + Severity: SeverityHigh, + Count: uint64(len(behavior.SSHEvents)), + Details: "Rapid-fire attack pattern detected", + Score: score * 1.2, + }) + } + } + + return threats +} + +func (td *ThreatDetector) detectTCPThreats(ip string, behavior *IPBehavior, now time.Time) []ThreatInfo { + var threats []ThreatInfo + + // Multi-window analysis + shortWindow := 30 * time.Second + mediumWindow := 2 * time.Minute + longWindow := 10 * time.Minute + + shortTerm := td.countConnectionsInWindow(behavior.TCPConnections, now, shortWindow) + mediumTerm := td.countConnectionsInWindow(behavior.TCPConnections, now, mediumWindow) + longTerm := td.countConnectionsInWindow(behavior.TCPConnections, now, longWindow) + + // Calculate threat score + score := td.calculateThreatScore(shortTerm, mediumTerm, longTerm) + + // Port scanning detection + uniquePorts := len(behavior.UniquePorts) + portScanThreshold := 5 + + // Pattern 1: Port scanning (many unique ports) + if uniquePorts >= portScanThreshold*3 { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeTCPPortScan, + Severity: SeverityCritical, + Count: uint64(uniquePorts), + Details: fmt.Sprintf("Aggressive port scan: %d unique ports scanned", uniquePorts), + Score: score * float64(uniquePorts) / 10, + }) + } else if uniquePorts >= portScanThreshold*2 { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeTCPPortScan, + Severity: SeverityHigh, + Count: uint64(uniquePorts), + Details: fmt.Sprintf("Port scan detected: %d unique ports", uniquePorts), + Score: score * float64(uniquePorts) / 10, + }) + } else if uniquePorts >= portScanThreshold { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeTCPPortScan, + Severity: SeverityMedium, + Count: uint64(uniquePorts), + Details: fmt.Sprintf("Suspicious port activity: %d unique ports", uniquePorts), + Score: score * float64(uniquePorts) / 10, + }) + } + + // Pattern 2: Connection flood + if shortTerm >= 50 { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeTCPFlood, + Severity: SeverityCritical, + Count: shortTerm, + Details: fmt.Sprintf("Connection flood: %d connections in 30 seconds", shortTerm), + Score: score, + }) + } else if mediumTerm >= 100 { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeTCPFlood, + Severity: SeverityHigh, + Count: mediumTerm, + Details: fmt.Sprintf("Sustained flood: %d connections in 2 minutes", mediumTerm), + Score: score, + }) + } else if longTerm >= 200 { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeTCPFlood, + Severity: SeverityMedium, + Count: longTerm, + Details: fmt.Sprintf("High connection volume: %d connections in 10 minutes", longTerm), + Score: score, + }) + } + + // Pattern 3: Sequential port scanning pattern + if uniquePorts >= portScanThreshold { + sequential := td.detectSequentialPortScan(behavior.TCPConnections) + if sequential { + threats = append(threats, ThreatInfo{ + ThreatType: storage.ThreatTypeTCPPortScan, + Severity: SeverityHigh, + Count: uint64(uniquePorts), + Details: "Sequential port scan pattern detected", + Score: score * 1.3, + }) } } - validConnections = append(validConnections, now) - td.tcpConnections[ipStr] = validConnections - connectionCount := uint64(len(validConnections)) + return threats +} - if connectionCount > td.config.TCPThreshold { - threatType := storage.ThreatTypeTCPPortScan - if connectionCount > td.config.TCPThreshold*2 { - threatType = storage.ThreatTypeTCPFlood +// Helper functions + +func (td *ThreatDetector) getOrCreateBehavior(ip string) *IPBehavior { + if behavior, exists := td.ipBehaviors[ip]; exists { + return behavior + } + behavior := &IPBehavior{ + SSHEvents: []SSHEventDetail{}, + TCPConnections: []TCPConnectionDetail{}, + UniquePorts: make(map[uint16]bool), + FirstSeen: time.Now(), + LastSeen: time.Now(), + FailedSSHCount: 0, + SuccessfulSSHCount: 0, + } + td.ipBehaviors[ip] = behavior + return behavior +} + +func (td *ThreatDetector) countEventsInWindow(events []SSHEventDetail, now time.Time, window time.Duration) uint64 { + count := uint64(0) + cutoff := now.Add(-window) + for _, e := range events { + if e.Timestamp.After(cutoff) { + count++ } + } + return count +} - alert := &storage.Alert{ - IP: ipStr, - ThreatType: threatType, - Count: connectionCount, - Timestamp: now, +func (td *ThreatDetector) countConnectionsInWindow(conns []TCPConnectionDetail, now time.Time, window time.Duration) uint64 { + count := uint64(0) + cutoff := now.Add(-window) + for _, c := range conns { + if c.Timestamp.After(cutoff) { + count++ } + } + return count +} - if err := td.storage.StoreAlert(alert); err != nil { - return fmt.Errorf("failed to store alert: %w", err) +func (td *ThreatDetector) countFailedInWindow(events []SSHEventDetail, now time.Time, window time.Duration) uint64 { + count := uint64(0) + cutoff := now.Add(-window) + for _, e := range events { + if e.Timestamp.After(cutoff) && e.EventType == SSH_FAILURE { + count++ } + } + return count +} + +func (td *ThreatDetector) calculateThreatScore(short, medium, long uint64) float64 { + // Exponential weighting: recent events are more significant + score := float64(short)*3.0 + float64(medium)*1.5 + float64(long)*0.5 + return score +} - // Send Telegram alert - tgAlert := &telegram.Alert{ - IP: ipStr, - ThreatType: string(threatType), - Count: connectionCount, - Timestamp: now, +func (td *ThreatDetector) detectRapidFirePattern(events []SSHEventDetail, now time.Time) bool { + if len(events) < 3 { + return false + } + + // Check last 5 events for rapid-fire pattern (multiple attempts within 5 seconds) + recentEvents := events + if len(events) > 5 { + recentEvents = events[len(events)-5:] + } + + for i := 1; i < len(recentEvents); i++ { + timeDiff := recentEvents[i].Timestamp.Sub(recentEvents[i-1].Timestamp) + if timeDiff < 2*time.Second && recentEvents[i].EventType == SSH_FAILURE { + return true } - if err := td.telegramClient.SendAlert(tgAlert); err != nil { - fmt.Printf("Failed to send Telegram alert: %v\n", err) + } + + return false +} + +func (td *ThreatDetector) detectSequentialPortScan(conns []TCPConnectionDetail) bool { + if len(conns) < 5 { + return false + } + + // Check if ports are being scanned sequentially + recentConns := conns + if len(conns) > 20 { + recentConns = conns[len(conns)-20:] + } + + sequentialCount := 0 + for i := 1; i < len(recentConns); i++ { + portDiff := int(recentConns[i].DstPort) - int(recentConns[i-1].DstPort) + if portDiff > 0 && portDiff <= 10 { + sequentialCount++ } + } - // Block IP if enabled - if td.config.EnableIPBlocking { - if err := td.blockIP(ipStr); err != nil { - fmt.Printf("Failed to block IP %s: %v\n", ipStr, err) - } else { - td.storage.AddBlockedIP(ipStr) - } + // If more than 30% show sequential pattern, it's likely a scan + return float64(sequentialCount)/float64(len(recentConns)-1) > 0.3 +} + +func (td *ThreatDetector) handleThreat(ip string, threat ThreatInfo) error { + // Only alert if severity is MEDIUM or higher, or if score is significant + if threat.Severity == SeverityLow && threat.Score < 10 { + return nil + } + + alert := &storage.Alert{ + IP: ip, + ThreatType: threat.ThreatType, + Count: threat.Count, + Timestamp: time.Now(), + Severity: string(threat.Severity), + Details: threat.Details, + Score: threat.Score, + } + + if err := td.storage.StoreAlert(alert); err != nil { + return fmt.Errorf("failed to store alert: %w", err) + } + + // Send Telegram alert + tgAlert := &telegram.Alert{ + IP: ip, + ThreatType: string(threat.ThreatType), + Count: threat.Count, + Timestamp: time.Now(), + Severity: string(threat.Severity), + Details: threat.Details, + Score: threat.Score, + } + if err := td.telegramClient.SendAlert(tgAlert); err != nil { + fmt.Printf("Failed to send Telegram alert: %v\n", err) + } + + // Auto-block for CRITICAL threats or high-scoring threats + shouldBlock := threat.Severity == SeverityCritical || + (threat.Severity == SeverityHigh && threat.Score > 50) || + (threat.Score > 100) + + if shouldBlock && td.config.EnableIPBlocking { + if err := td.blockIP(ip); err != nil { + fmt.Printf("Failed to block IP %s: %v\n", ip, err) + } else { + td.storage.AddBlockedIP(ip) + fmt.Printf("Auto-blocked IP %s due to %s threat (severity: %s, score: %.1f)\n", + ip, threat.ThreatType, threat.Severity, threat.Score) } } @@ -182,4 +542,3 @@ func u32ToIP(ip uint32) net.IP { byte(ip), } } - diff --git a/secrds-agent/internal/storage/storage.go b/secrds-agent/internal/storage/storage.go index de9fdc4..130622c 100644 --- a/secrds-agent/internal/storage/storage.go +++ b/secrds-agent/internal/storage/storage.go @@ -22,6 +22,9 @@ type Alert struct { ThreatType ThreatType `json:"threat_type"` Count uint64 `json:"count"` Timestamp time.Time `json:"timestamp"` + Severity string `json:"severity,omitempty"` + Details string `json:"details,omitempty"` + Score float64 `json:"score,omitempty"` } type Statistics struct { diff --git a/secrds-agent/internal/telegram/client.go b/secrds-agent/internal/telegram/client.go index 1cbd853..a3d06a1 100644 --- a/secrds-agent/internal/telegram/client.go +++ b/secrds-agent/internal/telegram/client.go @@ -101,17 +101,45 @@ func (c *Client) formatAlert(alert *Alert) string { threatName = "TCP Flood" } - return fmt.Sprintf( - "*Security Alert*\n\n"+ + // Determine severity emoji + severityEmoji := "⚠️" + if alert.Severity != "" { + switch alert.Severity { + case "CRITICAL": + severityEmoji = "🚨" + case "HIGH": + severityEmoji = "🔴" + case "MEDIUM": + severityEmoji = "🟠" + case "LOW": + severityEmoji = "🟡" + } + } + + message := fmt.Sprintf( + "%s *Security Alert*\n\n"+ "*Threat Type:* %s\n"+ - "*Source IP:* %s\n"+ + "*Severity:* %s\n"+ + "*Source IP:* `%s`\n"+ "*Attempt Count:* %d\n"+ "*Timestamp:* %s", + severityEmoji, threatName, + alert.Severity, alert.IP, alert.Count, alert.Timestamp.Format("2006-01-02 15:04:05 UTC"), ) + + if alert.Details != "" { + message += fmt.Sprintf("\n*Details:* %s", alert.Details) + } + + if alert.Score > 0 { + message += fmt.Sprintf("\n*Threat Score:* %.1f", alert.Score) + } + + return message } type Alert struct { @@ -119,5 +147,8 @@ type Alert struct { ThreatType string Count uint64 Timestamp time.Time + Severity string + Details string + Score float64 } diff --git a/secrds-cli/cmd/alerts.go b/secrds-cli/cmd/alerts.go index 9b2b05c..9d4729b 100644 --- a/secrds-cli/cmd/alerts.go +++ b/secrds-cli/cmd/alerts.go @@ -10,6 +10,7 @@ import ( ) var alertsLimit int +var alertsSeverity string var alertsCmd = &cobra.Command{ Use: "alerts", @@ -32,6 +33,9 @@ var alertsCmd = &cobra.Command{ ThreatType string `json:"threat_type"` Count uint64 `json:"count"` Timestamp time.Time `json:"timestamp"` + Severity string `json:"severity,omitempty"` + Details string `json:"details,omitempty"` + Score float64 `json:"score,omitempty"` } `json:"alerts"` } @@ -46,26 +50,78 @@ var alertsCmd = &cobra.Command{ return } + // Filter by severity if specified + if alertsSeverity != "" { + filtered := []struct { + IP string `json:"ip"` + ThreatType string `json:"threat_type"` + Count uint64 `json:"count"` + Timestamp time.Time `json:"timestamp"` + Severity string `json:"severity,omitempty"` + Details string `json:"details,omitempty"` + Score float64 `json:"score,omitempty"` + }{} + for _, alert := range alerts { + if alert.Severity == alertsSeverity { + filtered = append(filtered, alert) + } + } + alerts = filtered + if len(alerts) == 0 { + fmt.Printf("No alerts found with severity: %s\n", alertsSeverity) + return + } + } + // Show newest first start := len(alerts) - alertsLimit if start < 0 { start = 0 } - fmt.Printf("Recent alerts (showing %d of %d):\n\n", alertsLimit, len(alerts)) + fmt.Printf("Recent alerts (showing %d of %d):\n\n", alertsLimit, len(storageData.Alerts)) for i := len(alerts) - 1; i >= start; i-- { alert := alerts[i] - fmt.Printf("Time: %s\n", alert.Timestamp.Format("2006-01-02 15:04:05 UTC")) - fmt.Printf("IP: %s\n", alert.IP) - fmt.Printf("Threat: %s\n", alert.ThreatType) - fmt.Printf("Count: %d\n", alert.Count) - fmt.Println("---") + + // Severity indicator + severityIcon := "⚠️" + severityText := alert.Severity + if severityText == "" { + severityText = "UNKNOWN" + } else { + switch alert.Severity { + case "CRITICAL": + severityIcon = "🚨" + case "HIGH": + severityIcon = "🔴" + case "MEDIUM": + severityIcon = "🟠" + case "LOW": + severityIcon = "🟡" + } + } + + fmt.Printf("%s [%s] %s\n", severityIcon, severityText, alert.ThreatType) + fmt.Printf(" Time: %s\n", alert.Timestamp.Format("2006-01-02 15:04:05 UTC")) + fmt.Printf(" IP: %s\n", alert.IP) + fmt.Printf(" Count: %d\n", alert.Count) + + if alert.Score > 0 { + fmt.Printf(" Score: %.1f\n", alert.Score) + } + + if alert.Details != "" { + fmt.Printf(" Details: %s\n", alert.Details) + } + + fmt.Println() } }, } func init() { alertsCmd.Flags().IntVarP(&alertsLimit, "limit", "l", 10, "Number of alerts to show") + alertsCmd.Flags().StringVarP(&alertsSeverity, "severity", "s", "", "Filter by severity (LOW, MEDIUM, HIGH, CRITICAL)") rootCmd.AddCommand(alertsCmd) } diff --git a/secrds.service b/secrds.service index 26e52ac..e09aa8e 100644 --- a/secrds.service +++ b/secrds.service @@ -13,6 +13,9 @@ Restart=on-failure RestartSec=5s TimeoutStopSec=30s +# Ensure service stays running +RemainAfterExit=no + # Security settings User=root Group=root diff --git a/setup-service.sh b/setup-service.sh new file mode 100755 index 0000000..f9bdba4 --- /dev/null +++ b/setup-service.sh @@ -0,0 +1,142 @@ +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}Setting up secrds daemon service...${NC}" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Please run as root (use sudo)${NC}" + exit 1 +fi + +# Check if binaries exist +if [ ! -f "target/release/secrds-agent" ]; then + echo -e "${RED}Error: secrds-agent binary not found. Please build first: make build${NC}" + exit 1 +fi + +if [ ! -f "target/release/secrds-cli" ]; then + echo -e "${RED}Error: secrds-cli binary not found. Please build first: make build${NC}" + exit 1 +fi + +# Stop service if running (to avoid "Text file busy" error) +SERVICE_WAS_RUNNING=false +if systemctl is-active --quiet secrds 2>/dev/null; then + echo -e "${YELLOW}Stopping secrds service to update binaries...${NC}" + systemctl stop secrds + SERVICE_WAS_RUNNING=true + sleep 1 +fi + +# Install binaries +echo -e "${YELLOW}Installing binaries...${NC}" +# Try to copy, if it fails due to busy file, wait and retry +if ! cp target/release/secrds-agent /usr/local/bin/secrds-agent 2>/dev/null; then + echo -e "${YELLOW}Waiting for file to be released...${NC}" + sleep 2 + cp target/release/secrds-agent /usr/local/bin/secrds-agent +fi +chmod +x /usr/local/bin/secrds-agent +cp target/release/secrds-cli /usr/local/bin/secrds +chmod +x /usr/local/bin/secrds +echo -e "${GREEN}Binaries installed${NC}" + +# Install kernel programs +echo -e "${YELLOW}Installing kernel programs...${NC}" +mkdir -p /usr/local/lib/secrds +if [ -f "secrds-programs/ssh_kprobe.bpf.o" ]; then + cp secrds-programs/ssh_kprobe.bpf.o /usr/local/lib/secrds/ + echo -e "${GREEN}SSH kernel program installed${NC}" +fi +if [ -f "secrds-programs/tcp_trace.bpf.o" ]; then + cp secrds-programs/tcp_trace.bpf.o /usr/local/lib/secrds/ + echo -e "${GREEN}TCP kernel program installed${NC}" +fi + +# Install systemd service +echo -e "${YELLOW}Installing systemd service...${NC}" +if [ -f "secrds.service" ]; then + cp secrds.service /etc/systemd/system/secrds.service + systemctl daemon-reload + echo -e "${GREEN}Service file installed${NC}" +else + echo -e "${RED}Error: secrds.service not found${NC}" + exit 1 +fi + +# Create directories +mkdir -p /etc/secrds +mkdir -p /var/lib/secrds +mkdir -p /var/run +mkdir -p /var/log/secrds + +# Create default config if it doesn't exist +if [ ! -f "/etc/secrds/config.yaml" ]; then + echo -e "${YELLOW}Creating default configuration...${NC}" + cat > /etc/secrds/config.yaml <