128 lines
3.3 KiB
Go
128 lines
3.3 KiB
Go
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
// Package agent owns the parallel Go sweep loop. It emits JSON-compatible
|
|
// records but does not write Python daemon state or replace the production
|
|
// service during Phase 1.
|
|
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/config"
|
|
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/detectors"
|
|
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model"
|
|
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/schema"
|
|
)
|
|
|
|
const Version = "0.7.0"
|
|
|
|
// CaptureFunc returns one injectable SystemState-equivalent sweep.
|
|
type CaptureFunc func() (model.State, error)
|
|
|
|
// EmitFunc receives one enodia.event.v1-compatible record.
|
|
type EmitFunc func(map[string]any) error
|
|
|
|
// Agent is the Phase 1 sidecar sweep loop.
|
|
type Agent struct {
|
|
Config config.Config
|
|
Capture CaptureFunc
|
|
Host func() (string, error)
|
|
Now func() time.Time
|
|
}
|
|
|
|
// New builds an agent with production clock and hostname providers.
|
|
func New(cfg config.Config, capture CaptureFunc) *Agent {
|
|
return &Agent{
|
|
Config: cfg,
|
|
Capture: capture,
|
|
Host: os.Hostname,
|
|
Now: time.Now,
|
|
}
|
|
}
|
|
|
|
// Sweep captures state once and returns alert events followed by one status
|
|
// event. Retained snapshot fields remain empty until persistence is ported.
|
|
func (a *Agent) Sweep() ([]map[string]any, error) {
|
|
state, err := a.Capture()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
host, err := a.Host()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
timestamp := a.Now().Local().Format(time.RFC3339Nano)
|
|
|
|
alerts := make([]model.Alert, 0)
|
|
if a.Config.Enabled("ld_preload") {
|
|
alerts = append(alerts, detectors.LDPreload(state)...)
|
|
}
|
|
if a.Config.Enabled("deleted_exe") {
|
|
alerts = append(alerts, detectors.DeletedExe(state)...)
|
|
}
|
|
if a.Config.Enabled("input_snooper") {
|
|
alerts = append(alerts, detectors.InputSnooper(state, a.Config)...)
|
|
}
|
|
if a.Config.Enabled("credential_access") {
|
|
alerts = append(alerts, detectors.CredentialAccess(state, a.Config)...)
|
|
}
|
|
events := make([]map[string]any, 0, len(alerts)+1)
|
|
for _, alert := range alerts {
|
|
record, err := schema.Build("alert", alert, host, timestamp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
events = append(events, record)
|
|
}
|
|
status := schema.Status{
|
|
Schema: schema.StatusV1,
|
|
Version: Version,
|
|
Running: true,
|
|
TotalAlerts: 0,
|
|
Counts: map[string]int{},
|
|
LastAlert: nil,
|
|
EBPF: "unknown",
|
|
EBPFExec: "unknown",
|
|
EBPFSyscall: "unknown",
|
|
Host: host,
|
|
}
|
|
record, err := schema.Build("status", status, host, timestamp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(events, record), nil
|
|
}
|
|
|
|
// Run emits one sweep immediately and then waits for each configured interval.
|
|
// once is used by parity checks and operator-visible smoke tests.
|
|
func (a *Agent) Run(ctx context.Context, once bool, emit EmitFunc) error {
|
|
if a.Capture == nil || emit == nil {
|
|
return fmt.Errorf("capture and emit functions are required")
|
|
}
|
|
for {
|
|
events, err := a.Sweep()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, event := range events {
|
|
if err := emit(event); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if once {
|
|
return nil
|
|
}
|
|
timer := time.NewTimer(a.Config.SampleInterval)
|
|
select {
|
|
case <-ctx.Done():
|
|
if !timer.Stop() {
|
|
<-timer.C
|
|
}
|
|
return nil
|
|
case <-timer.C:
|
|
}
|
|
}
|
|
}
|