feat(go): advance validation sidecar toward production
This commit is contained in:
parent
6a06eba255
commit
f85c2e831a
92 changed files with 8881 additions and 91 deletions
95
go-agent/internal/detectors/first_seen.go
Normal file
95
go-agent/internal/detectors/first_seen.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package detectors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/config"
|
||||
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model"
|
||||
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/netutil"
|
||||
)
|
||||
|
||||
// FirstSeen ports the Python first_seen detector (SIDs 100074 and 100075).
|
||||
// Baseline state is injected for now; durable local storage belongs to a later
|
||||
// sidecar persistence tranche.
|
||||
func FirstSeen(state model.State, cfg config.Config) []model.Alert {
|
||||
if !cfg.Enabled("first_seen") ||
|
||||
state.FirstSeenPublicDestinations == nil ||
|
||||
state.FirstSeenListenerPorts == nil {
|
||||
return []model.Alert{}
|
||||
}
|
||||
alerts := make([]model.Alert, 0)
|
||||
for _, socket := range state.Sockets {
|
||||
comm := socket.Comm
|
||||
if comm == "" {
|
||||
comm = "?"
|
||||
}
|
||||
if socket.State == "ESTAB" {
|
||||
host, port := netutil.SplitHostPort(socket.Peer)
|
||||
target := host + ":" + port
|
||||
if netutil.IsPublicIP(host) && port != "" &&
|
||||
!contains(state.FirstSeenPublicDestinations[comm], target) {
|
||||
// Record before considering whether to alert. The first armed pass
|
||||
// learns silently, and duplicate sockets in one sweep must not emit
|
||||
// duplicate rarity alerts.
|
||||
state.FirstSeenPublicDestinations[comm] = append(
|
||||
state.FirstSeenPublicDestinations[comm], target)
|
||||
sort.Strings(state.FirstSeenPublicDestinations[comm])
|
||||
if state.FirstSeenInitialized {
|
||||
alerts = append(alerts, model.Alert{
|
||||
SID: 100074, Severity: "MEDIUM",
|
||||
Signature: "first_public_destination", Classtype: "network-rarity",
|
||||
Key: "firstdest:" + comm + ":" + target,
|
||||
Detail: fmt.Sprintf("comm=%s first public destination %s", comm, target),
|
||||
PIDs: alertPIDs(socket.PID),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (socket.State == "LISTEN" || socket.State == "UNCONN") &&
|
||||
(socket.Kind == "tcp" || socket.Kind == "udp" || socket.Kind == "") {
|
||||
_, port := netutil.SplitHostPort(socket.Local)
|
||||
if port != "" && port != "0" && isDigits(port) &&
|
||||
!contains(state.FirstSeenListenerPorts[comm], port) {
|
||||
state.FirstSeenListenerPorts[comm] = append(
|
||||
state.FirstSeenListenerPorts[comm], port)
|
||||
sort.Slice(state.FirstSeenListenerPorts[comm], func(i, j int) bool {
|
||||
left, _ := strconv.Atoi(state.FirstSeenListenerPorts[comm][i])
|
||||
right, _ := strconv.Atoi(state.FirstSeenListenerPorts[comm][j])
|
||||
return left < right
|
||||
})
|
||||
if state.FirstSeenInitialized {
|
||||
alerts = append(alerts, model.Alert{
|
||||
SID: 100075, Severity: "MEDIUM",
|
||||
Signature: "first_listener_port", Classtype: "network-rarity",
|
||||
Key: "firstlisten:" + comm + ":" + port,
|
||||
Detail: fmt.Sprintf("comm=%s first listening port %s", comm, port),
|
||||
PIDs: alertPIDs(socket.PID),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
|
||||
func contains(values []string, wanted string) bool {
|
||||
for _, value := range values {
|
||||
if value == wanted {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isDigits(value string) bool {
|
||||
for _, char := range value {
|
||||
if char < '0' || char > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue