feat(go): persist Python-compatible incidents

This commit is contained in:
Luna 2026-07-22 01:59:49 -07:00
parent f85c2e831a
commit 538d1d954a
No known key found for this signature in database
13 changed files with 555 additions and 46 deletions

View file

@ -61,6 +61,9 @@ type Config struct {
HeartbeatMaxAge int
MaxSnapshots int
MaxSnapshotAgeDays int
IncidentTracking bool
IncidentWindow int
IncidentLineageDepth int
Detectors map[string]bool
InputSnooperAllowComms map[string]bool
CredentialAccessAllowComms map[string]bool
@ -97,6 +100,9 @@ func Default() Config {
HeartbeatMaxAge: 120,
MaxSnapshots: 300,
MaxSnapshotAgeDays: 60,
IncidentTracking: true,
IncidentWindow: 1800,
IncidentLineageDepth: 8,
Detectors: detectors,
InputSnooperAllowComms: stringSet(defaultInputSnooperAllowComms),
CredentialAccessAllowComms: stringSet(defaultCredentialAccessAllowComms),
@ -204,6 +210,24 @@ func Load(path string) (Config, error) {
return Config{}, fmt.Errorf("max_snapshot_age_days must be non-negative: %q", value)
}
cfg.MaxSnapshotAgeDays = days
case "incident_tracking":
enabled, err := boolean(value)
if err != nil {
return Config{}, fmt.Errorf("incident_tracking: %w", err)
}
cfg.IncidentTracking = enabled
case "incident_window":
window, err := nonNegativeInt(value)
if err != nil {
return Config{}, fmt.Errorf("incident_window: %w", err)
}
cfg.IncidentWindow = window
case "incident_lineage_depth":
depth, err := nonNegativeInt(value)
if err != nil {
return Config{}, fmt.Errorf("incident_lineage_depth: %w", err)
}
cfg.IncidentLineageDepth = depth
case "detectors":
names, err := stringArray(value)
if err != nil {
@ -351,6 +375,14 @@ func nonNegativeSeconds(value string) (time.Duration, error) {
return time.Duration(seconds * float64(time.Second)), nil
}
func nonNegativeInt(value string) (int, error) {
parsed, err := strconv.Atoi(value)
if err != nil || parsed < 0 {
return 0, fmt.Errorf("must be non-negative: %q", value)
}
return parsed, nil
}
func boolean(value string) (bool, error) {
switch value {
case "true":

View file

@ -15,7 +15,9 @@ func TestDefaultsMatchPython(t *testing.T) {
cfg.BaselineGrace != 10*time.Second ||
cfg.SUIDRefresh != time.Hour || cfg.SUIDScanInterval != time.Minute ||
cfg.HeartbeatMaxAge != 120 || cfg.MaxSnapshots != 300 ||
cfg.MaxSnapshotAgeDays != 60 || !cfg.FirstSeenEnabled {
cfg.MaxSnapshotAgeDays != 60 || !cfg.IncidentTracking ||
cfg.IncidentWindow != 1800 || cfg.IncidentLineageDepth != 8 ||
!cfg.FirstSeenEnabled {
t.Fatalf("unexpected defaults: %+v", cfg)
}
if !cfg.Enabled("deleted_exe") {
@ -34,6 +36,9 @@ suid_scan_interval = 15
heartbeat_max_age = 45
max_snapshots = 25
max_snapshot_age_days = 7
incident_tracking = false
incident_window = 90
incident_lineage_depth = 3
detectors = [
"deleted_exe",
"egress",
@ -67,7 +72,8 @@ unknown_future_key = true
if cfg.SampleInterval != 1500*time.Millisecond || cfg.Cooldown != 30*time.Second ||
cfg.BaselineGrace != 2500*time.Millisecond ||
cfg.SUIDRefresh != 2*time.Minute || cfg.SUIDScanInterval != 15*time.Second ||
cfg.HeartbeatMaxAge != 45 || cfg.MaxSnapshots != 25 || cfg.MaxSnapshotAgeDays != 7 {
cfg.HeartbeatMaxAge != 45 || cfg.MaxSnapshots != 25 || cfg.MaxSnapshotAgeDays != 7 ||
cfg.IncidentTracking || cfg.IncidentWindow != 90 || cfg.IncidentLineageDepth != 3 {
t.Fatalf("unexpected parsed config: %+v", cfg)
}
if !cfg.Enabled("deleted_exe") || !cfg.Enabled("egress") || cfg.Enabled("reverse_shell") {