netmon

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package netmon is the offline core of Phase 6: network flow observability and egress analysis. It consumes recorded connection/DNS events (from a fixture today, from Phase-5 eBPF telemetry tomorrow — see Source), attributes each flow to a process+container+workload, aggregates per-workload flow logs, and runs deterministic detection heuristics (C2 beaconing, data exfiltration, lateral movement, cloud-metadata access, DNS tunnelling/DGA).

It deliberately knows nothing about the CLI or HTTP frontends. The engine module in internal/modules/netpolicy adapts the anomalies and generated policies produced here onto the unified Finding model. The only shared dependency is engine.Severity, so an anomaly's risk speaks the same language as every other finding in the tool.

Determinism is a hard requirement: nothing here reads the wall clock or a random source. Any notion of "now" is injected through Options, and every slice the package returns is sorted by a stable key. Same capture in, byte identical anomalies and policies out — that is what makes the golden tests meaningful and what lets an agent trust a generated allowlist.

Index

Constants

This section is empty.

Variables

View Source
var ErrLiveCaptureUnavailable = errors.New("netmon: live capture not wired (eBPF backend parked — see NOTES.md)")

ErrLiveCaptureUnavailable is returned by the live Source until real capture is wired. It is a sentinel so callers can distinguish "no capture backend" from a genuine runtime failure and fall back to a recorded Source.

Functions

func IsExternal

func IsExternal(ip string) bool

IsExternal reports whether an IP is a parseable, routable address outside the private/reserved ranges. An empty or unparseable string is not a valid external IP, so it returns false (the FQDN-only case is handled separately).

func IsIMDS

func IsIMDS(ip string) bool

IsIMDS reports whether an IP is a known cloud metadata endpoint.

func IsInternal

func IsInternal(ip string) bool

IsInternal reports whether an IP falls in a private/reserved range. An unparseable or empty IP is treated as external so we never under-report egress to an unknown host.

Types

type Anomaly

type Anomaly struct {
	Kind     AnomalyKind
	Severity engine.Severity
	Workload string
	Dest     string  // human destination summary, "" for workload-scoped anomalies
	Title    string  // one human line
	Detail   string  // why it fired
	Score    float64 // 0..1 confidence, deterministic
	Evidence map[string]string
}

Anomaly is one detection result, scoped to a workload. Severity speaks the engine's language so the module adapter is a straight projection. Evidence is a small, machine-consumable map an agent can reason over — the "explain" half of the AI-age mandate lives here.

type AnomalyKind

type AnomalyKind string

AnomalyKind names a detected behaviour. Kinds are stable strings so the netpolicy module can map each to a DS-RAT-NET rule id and the right references.

const (
	KindIMDS        AnomalyKind = "imds_access"
	KindBeacon      AnomalyKind = "c2_beacon"
	KindExfil       AnomalyKind = "exfil_volume"
	KindLowAndSlow  AnomalyKind = "exfil_low_and_slow"
	KindLateral     AnomalyKind = "lateral_movement"
	KindDNSTunnel   AnomalyKind = "dns_tunnel"
	KindDGA         AnomalyKind = "dns_dga"
	KindBlockedEg   AnomalyKind = "blocked_egress"
	KindAgentEgress AnomalyKind = "agent_egress_unknown" // AI-age feature (off by default)
	KindAnomalousEg AnomalyKind = "anomalous_egress"     // AI-age feature (off by default)
	KindHostNetwork AnomalyKind = "host_network"
	KindFreeForAll  AnomalyKind = "unrestricted_east_west"
)

type Capture

type Capture struct {
	// PolicyMode describes what egress enforcement was in place when the capture
	// was taken ("none", "audit", "enforce"). Absent means unknown/none, which is
	// itself worth flagging.
	PolicyMode string     `json:"policy_mode,omitempty"`
	Workloads  []Workload `json:"workloads"`
	Flows      []Flow     `json:"flows"`
	DNS        []DNSEvent `json:"dns,omitempty"`
}

Capture is a recorded window of network telemetry: the workloads seen and the flow and DNS events attributed to them. It is the unit a Source yields and the shape of every fixture under testdata/.

func DecodeCapture

func DecodeCapture(r io.Reader) (*Capture, error)

DecodeCapture parses a JSON capture from r with the same size bound as LoadCapture. It rejects trailing garbage so a truncated-then-padded file is caught rather than silently half-read.

func LoadCapture

func LoadCapture(path string) (*Capture, error)

LoadCapture reads a JSON capture from a file path, enforcing the size bound, and returns it normalized and ready to analyse.

func (*Capture) Encode

func (c *Capture) Encode(w io.Writer) error

Encode writes the capture as indented JSON — used to persist a fixture from a live/telemetry recording so it can be replayed deterministically in CI.

func (*Capture) Normalize

func (c *Capture) Normalize()

Normalize sorts every slice in the capture into a canonical order so that two captures with the same events (in any input order) analyse identically. It is idempotent and safe to call more than once.

type DNSEvent

type DNSEvent struct {
	WorkloadID string   `json:"workload_id"`
	TSUnix     int64    `json:"ts_unix"`
	QName      string   `json:"qname"`
	QType      string   `json:"qtype"`           // A, AAAA, TXT, NULL, ...
	RCode      string   `json:"rcode,omitempty"` // NOERROR, NXDOMAIN, ...
	Answers    []string `json:"answers,omitempty"`
}

DNSEvent is a single resolver query/response, attributed to a workload. It is the raw material for tunnelling and DGA detection and for turning IP flows into FQDN egress allowlists.

type DestStat

type DestStat struct {
	Key        string // FQDN:port when known, else IP:port
	Host       string // FQDN if known else IP
	FQDN       string
	IP         string
	Port       uint16
	Proto      Protocol
	Internal   bool
	IMDS       bool
	Count      int
	BytesTx    int64
	BytesRx    int64
	FirstTS    int64
	LastTS     int64
	Denied     int     // flows with a deny verdict
	Timestamps []int64 // per-connection timestamps, ascending — beaconing input
}

DestStat rolls up every egress flow to a single logical destination. It is the unit beaconing/exfil reason over and the unit a policy allow-rule maps to.

type Direction

type Direction string

Direction is the flow direction relative to the attributed workload.

const (
	// Egress is workload-initiated traffic leaving the workload — the primary
	// concern for exfiltration, C2, and least-privilege egress policy.
	Egress Direction = "egress"
	// Ingress is traffic arriving at the workload.
	Ingress Direction = "ingress"
)

type EgressClass

type EgressClass string

EgressClass labels a destination's inferred intent.

const (
	// ClassIntended is a destination the workload contacts consistently — the
	// baseline that a least-privilege allowlist should permit.
	ClassIntended EgressClass = "intended"
	// ClassAnomalous is a rare, one-off, or otherwise unexpected destination that
	// warrants review before it enters an allowlist.
	ClassAnomalous EgressClass = "anomalous"
)

type EgressIntent

type EgressIntent struct {
	Dest       string
	Class      EgressClass
	Reasons    []string
	Count      int
	BytesTx    int64
	FQDN       string
	Confidence float64
}

EgressIntent is the classification of one destination, with a rationale that is both human-readable and machine-parseable (the reasons slice).

func ClassifyEgress

func ClassifyEgress(fl *FlowLog, o Options) []EgressIntent

ClassifyEgress buckets a workload's external destinations into intended vs anomalous using deterministic, explainable rules (no model, no randomness). It is exported so both the intent detector and policy generation can reuse the same reasoning — an agent applying a policy sees exactly why each entry is on (or off) the allowlist. Results are sorted by destination for stability.

type Endpoint

type Endpoint struct {
	IP        string `json:"ip,omitempty"`
	Port      uint16 `json:"port,omitempty"`
	FQDN      string `json:"fqdn,omitempty"`
	Workload  string `json:"workload,omitempty"`
	Namespace string `json:"namespace,omitempty"`
}

Endpoint is one side of a flow. For an external destination IP and Port are set and FQDN is filled in when DNS correlation resolved the name; for an in-cluster peer Workload/Namespace identify it independently of its ephemeral IP (the whole point of identity-based policy).

type Flow

type Flow struct {
	// WorkloadID links the flow to a Workload in the same Capture.
	WorkloadID string    `json:"workload_id"`
	TSUnix     int64     `json:"ts_unix"`
	Proto      Protocol  `json:"proto"`
	Direction  Direction `json:"direction"`
	Src        Endpoint  `json:"src"`
	Dst        Endpoint  `json:"dst"`
	BytesTx    int64     `json:"bytes_tx"`
	BytesRx    int64     `json:"bytes_rx"`
	Verdict    Verdict   `json:"verdict,omitempty"`
	// Process attribution (the eBPF forensic correlation we get from Phase 5).
	Process string `json:"process,omitempty"`
	PID     int    `json:"pid,omitempty"`
}

Flow is a single observed connection record, already attributed to the workload that opened it and the process inside that workload.

type FlowLog

type FlowLog struct {
	Workload Workload
	Egress   []Flow
	Ingress  []Flow
	DNS      []DNSEvent
	// Dests aggregates egress by destination (FQDN-preferred key) in stable order.
	Dests []*DestStat
}

FlowLog is one workload's attributed traffic, split into destinations for egress analysis. It is the searchable per-flow view the market-parity checklist asks for (src/dst identity, port, verdict), grouped for reuse.

func BuildFlowLogs

func BuildFlowLogs(c *Capture) []*FlowLog

BuildFlowLogs groups a normalized capture into per-workload flow logs, sorted by workload id. A flow whose WorkloadID has no matching Workload descriptor is still attributed under a synthesized identity so telemetry gaps never drop coverage.

func (*FlowLog) ExternalDests

func (fl *FlowLog) ExternalDests() []*DestStat

ExternalDests returns the routable-internet destinations in stable order — the set egress policy and exfil detection care about.

type LiveSource

type LiveSource struct {
	// Iface optionally names the interface to attach to; unused until wired.
	Iface string
}

LiveSource is the placeholder for the Linux eBPF flow/DNS probe. On Linux the real implementation will attach kprobes/tracepoints (or consume Phase-5 telemetry) and stream Flow/DNSEvent records. Loading an eBPF program requires a loader dependency and CAP_BPF, which are out of scope for the deterministic offline core, so this returns the sentinel today. The interface is identical to the non-Linux stub so the rest of the package is platform-agnostic.

func NewLiveSource

func NewLiveSource(iface string) *LiveSource

NewLiveSource returns a live capture source for the given interface.

func (*LiveSource) Capture

func (s *LiveSource) Capture(_ context.Context) (*Capture, error)

Capture reports that live capture is not yet available on this build. Real eBPF attach + ringbuffer consumption is parked as a master action; callers should use a RecordedSource for offline analysis.

type Options

type Options struct {
	// Now is the reference time. Zero means "derive from the capture window",
	// which keeps analysis deterministic without any wall-clock read.
	Now time.Time

	// Beaconing.
	BeaconMinSamples int     // minimum connections to a dest before periodicity is meaningful
	BeaconMaxCV      float64 // max coefficient of variation of intervals to call it regular
	BeaconMaxBytes   int64   // per-connection byte ceiling; beacons are small

	// Exfiltration.
	ExfilMinBytes     int64   // total egress bytes to one external dest to flag volume exfil
	ExfilTxRxRatio    float64 // tx:rx ratio above which traffic is upload-heavy
	LowSlowMinBytes   int64   // cumulative bytes for low-and-slow
	LowSlowMinSpanSec int64   // minimum duration for low-and-slow

	// Lateral movement.
	LateralMinPeers int // distinct internal peers before fan-out is suspicious

	// DNS.
	DNSTunnelMinQueries int     // queries under one parent before tunnelling is considered
	DNSEntropyThreshold float64 // Shannon entropy (bits/char) above which a label looks generated
	DGAMinNXDomain      int     // NXDOMAIN responses before a DGA is considered

	// AI-age features (off by default).
	EnableAgentEgress bool // flag AI-agent workloads reaching unknown model/inference hosts
	EnableIntent      bool // cluster destinations into intended vs anomalous
	IntentMinCount    int  // connection count at/above which a dest is "intended" (baseline)
}

Options tunes detection. The zero value is a safe, deterministic default with the AI-age features OFF — callers opt in explicitly. Thresholds are exposed so they can be tightened per environment without editing the heuristics.

type Protocol

type Protocol string

Protocol is the L4 protocol of a flow.

const (
	ProtoTCP  Protocol = "tcp"
	ProtoUDP  Protocol = "udp"
	ProtoICMP Protocol = "icmp"
)

type RecordedSource

type RecordedSource struct {
	// contains filtered or unexported fields
}

RecordedSource replays a Capture that was loaded from a fixture or an upstream recorder. It is the deterministic Source used in tests and in the offline `dsecrat net` path.

func NewRecordedSource

func NewRecordedSource(c *Capture) *RecordedSource

NewRecordedSource wraps an already-loaded capture as a Source.

func (*RecordedSource) Capture

func (s *RecordedSource) Capture(_ context.Context) (*Capture, error)

Capture returns the recorded window. The returned pointer is the stored capture; callers that mutate it should clone first.

type Report

type Report struct {
	Logs      []*FlowLog
	Anomalies []Anomaly
}

Report is the full deterministic result of analysing a capture: the derived per-workload flow logs plus every anomaly, sorted stably.

func Analyze

func Analyze(c *Capture, opts Options) *Report

Analyze runs every detector over a capture and returns a deterministic report. It builds the per-workload flow logs once and hands them to each heuristic.

func (*Report) Highest

func (r *Report) Highest() engine.Severity

Highest returns the most severe anomaly severity in the report, or SeverityUnknown when there are none — used for --fail-on gating.

type Source

type Source interface {
	// Capture collects the currently available events. It must respect ctx and
	// must not block indefinitely.
	Capture(ctx context.Context) (*Capture, error)
}

Source yields a window of observed network telemetry. Implementations must return a Capture whose events are attributed to workloads; callers Normalize before analysis.

type Verdict

type Verdict string

Verdict records whether the dataplane allowed or dropped a flow. A capture taken in policy "audit" mode carries verdicts even when nothing is enforced, which is what lets us alert on unexpected drops (or unexpected allows).

const (
	VerdictAllow   Verdict = "allow"
	VerdictDeny    Verdict = "deny"
	VerdictUnknown Verdict = ""
)

type Workload

type Workload struct {
	ID          string            `json:"id"`
	Name        string            `json:"name,omitempty"`
	Namespace   string            `json:"namespace,omitempty"`
	Labels      map[string]string `json:"labels,omitempty"`
	Kind        string            `json:"kind,omitempty"`
	HostNetwork bool              `json:"host_network,omitempty"`
}

Workload is the identity a flow is attributed to. Kind carries an optional classification ("agent" marks an AI-agent workload, which the agent-egress governance feature keys off).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL