model

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2026 License: MIT Imports: 2 Imported by: 0

Documentation

Overview

Package model defines the core domain types shared by every stage of the tracehound pipeline: capture, flow assembly, fingerprinting, and detection.

This package deliberately has no dependencies outside the standard library. Everything above it (decoders, detectors, storage, API) depends on model, and model depends on nothing, which keeps the detection logic trivially testable without a network interface or a capture library in the loop.

Index

Constants

View Source
const (
	TCPFin uint8 = 1 << 0
	TCPSyn uint8 = 1 << 1
	TCPRst uint8 = 1 << 2
	TCPPsh uint8 = 1 << 3
	TCPAck uint8 = 1 << 4
	TCPUrg uint8 = 1 << 5
)

TCP flag bits, matching the wire layout of the TCP header's flag octet.

Variables

View Source
var (
	TechAppLayerWebProto = Technique{ID: "T1071.001", Name: "Application Layer Protocol: Web Protocols", Tactic: "command-and-control"}
	TechAppLayerDNS      = Technique{ID: "T1071.004", Name: "Application Layer Protocol: DNS", Tactic: "command-and-control"}
	TechExfilOverC2      = Technique{ID: "T1041", Name: "Exfiltration Over C2 Channel", Tactic: "exfiltration"}
	TechExfilDNS         = Technique{ID: "T1048.003", Name: "Exfiltration Over Alternative Protocol", Tactic: "exfiltration"}
	TechNetworkScan      = Technique{ID: "T1046", Name: "Network Service Discovery", Tactic: "discovery"}
	TechRemoteSysDiscov  = Technique{ID: "T1018", Name: "Remote System Discovery", Tactic: "discovery"}
	TechEncryptedChannel = Technique{ID: "T1573", Name: "Encrypted Channel", Tactic: "command-and-control"}
	TechNonStandardPort  = Technique{ID: "T1571", Name: "Non-Standard Port", Tactic: "command-and-control"}
	TechProtocolTunnel   = Technique{ID: "T1572", Name: "Protocol Tunneling", Tactic: "command-and-control"}
)

Common ATT&CK techniques referenced by the built-in detectors, defined once so rule authors and detectors cannot drift on naming.

Functions

func KeyFor

func KeyFor(p *Packet) (FlowKey, Direction)

KeyFor derives the canonical key and direction for an already-decoded packet.

func NewFlowKey

func NewFlowKey(src netip.Addr, srcPort uint16, dst netip.Addr, dstPort uint16, proto Protocol) (FlowKey, Direction)

NewFlowKey builds the canonical key for a packet's endpoints and reports which direction the packet is travelling in that canonical frame.

Types

type Alert

type Alert struct {
	ID          string      `json:"id"`
	Time        time.Time   `json:"time"`
	RuleID      string      `json:"rule_id"`
	Detector    string      `json:"detector"`
	Title       string      `json:"title"`
	Description string      `json:"description,omitempty"`
	Severity    Severity    `json:"severity"`
	Techniques  []Technique `json:"techniques,omitempty"`

	Src     netip.Addr `json:"src,omitzero"`
	Dst     netip.Addr `json:"dst,omitzero"`
	DstPort uint16     `json:"dst_port,omitempty"`
	Proto   string     `json:"proto,omitempty"`

	// Score is the detector's confidence in [0,1]. It is deliberately separate
	// from Severity: severity is how bad this would be if true, score is how
	// sure we are that it is true.
	Score float64 `json:"score"`

	// Evidence holds the supporting measurements. Values are restricted to
	// JSON-native types so the API can pass them through untouched.
	Evidence map[string]any `json:"evidence,omitempty"`
}

Alert is a single detection emitted by a detector.

Evidence carries the detector-specific numbers that justify the verdict -- the periodicity score, the entropy value, the ports touched. An analyst who cannot see why a tool fired will stop trusting the tool, so every detector in tracehound is required to show its work.

type Device

type Device struct {
	Addr      netip.Addr `json:"addr"`
	MAC       string     `json:"mac,omitempty"`
	Hostname  string     `json:"hostname,omitempty"`
	FirstSeen time.Time  `json:"first_seen"`
	LastSeen  time.Time  `json:"last_seen"`

	// JA4s is the set of distinct TLS client fingerprints this host has
	// presented. More than a handful usually means a multi-tenant host — or an
	// implant using a different TLS stack than the browser next to it.
	JA4s []string `json:"ja4s,omitempty"`

	BytesSent uint64 `json:"bytes_sent"`
	BytesRecv uint64 `json:"bytes_recv"`
	Flows     uint64 `json:"flows"`
}

Device is a host observed on the monitored network, keyed by IP address. The passive fingerprints collected here are what turn a packet firehose into an asset inventory: JA4 hashes identify the TLS stack, which in practice identifies the application (and often the malware family).

type Direction

type Direction uint8

Direction indicates which way a packet is travelling relative to the flow's canonical A/B ordering.

const (
	// DirAToB means the packet travelled from FlowKey.A to FlowKey.B.
	DirAToB Direction = iota
	// DirBToA means the packet travelled from FlowKey.B to FlowKey.A.
	DirBToA
)

type Flow

type Flow struct {
	Key FlowKey `json:"-"`

	Client     netip.Addr `json:"client"`
	ClientPort uint16     `json:"client_port"`
	Server     netip.Addr `json:"server"`
	ServerPort uint16     `json:"server_port"`
	Proto      Protocol   `json:"-"`

	FirstSeen time.Time `json:"first_seen"`
	LastSeen  time.Time `json:"last_seen"`

	PacketsToServer uint64 `json:"packets_to_server"`
	PacketsToClient uint64 `json:"packets_to_client"`
	BytesToServer   uint64 `json:"bytes_to_server"`
	BytesToClient   uint64 `json:"bytes_to_client"`

	// TCPFlagsSeen is the union of every TCP flag octet observed on the flow.
	// A flow with SYN but no SYN-ACK, for example, was never established.
	TCPFlagsSeen uint8 `json:"-"`

	// Application-layer attributes, populated opportunistically by decoders.
	SNI  string `json:"sni,omitempty"`
	ALPN string `json:"alpn,omitempty"`
	JA4  string `json:"ja4,omitempty"`
	JA3  string `json:"ja3,omitempty"`
	// JA4S fingerprints the server's response. On its own it is weaker than
	// JA4, but the pair is considerably stronger than either: a client
	// fingerprint says what software connected, and adding the server's says
	// what it connected to. The same pair seen across several victims is a
	// command-and-control framework rather than one unusual host.
	JA4S string `json:"ja4s,omitempty"`
	// contains filtered or unexported fields
}

Flow is an accumulating record of one bidirectional conversation.

Client/Server are assigned from the first packet observed: the sender of the first packet is treated as the client. For TCP this is corrected when a SYN is seen, since the SYN sender is authoritatively the client even if we joined the capture mid-conversation.

func (*Flow) Bytes

func (f *Flow) Bytes() uint64

Bytes is the total byte count in both directions.

func (*Flow) Duration

func (f *Flow) Duration() time.Duration

Duration is the wall-clock span between the first and last observed packet.

func (*Flow) Established

func (f *Flow) Established() bool

Established reports whether a TCP handshake was completed on this flow. Non-TCP flows are always reported as established.

func (*Flow) Observe

func (f *Flow) Observe(p *Packet)

Observe folds a packet into the flow record.

func (*Flow) Packets

func (f *Flow) Packets() uint64

Packets is the total packet count in both directions.

func (*Flow) ProtoString

func (f *Flow) ProtoString() string

ProtoString renders the flow's protocol for JSON and display.

type FlowKey

type FlowKey struct {
	A     netip.Addr
	B     netip.Addr
	APort uint16
	BPort uint16
	Proto Protocol
}

FlowKey identifies a bidirectional conversation.

The two endpoints are stored in a canonical order (A < B by address, then by port) so that packets travelling in either direction hash to the same key. This is what lets a single map lookup find the flow regardless of who sent the packet — the alternative, keying on (src,dst) and probing twice, doubles the hash cost on the hottest path in the program.

type MAC

type MAC [6]byte

MAC is a link-layer address. Stored as a fixed array so Packet stays allocation-free and usable as a map key component.

func (MAC) IsZero

func (m MAC) IsZero() bool

IsZero reports whether the address is unset, which happens for capture sources with no Ethernet layer (e.g. raw IP or Linux cooked captures).

func (MAC) String

func (m MAC) String() string

type Packet

type Packet struct {
	Timestamp time.Time

	SrcMAC MAC
	DstMAC MAC

	Src     netip.Addr
	Dst     netip.Addr
	SrcPort uint16
	DstPort uint16
	Proto   Protocol

	// TCPFlags is the raw flag octet; zero for non-TCP packets.
	TCPFlags uint8

	// CaptureLength is the number of bytes actually captured, WireLength the
	// size of the frame on the wire. They differ when a snaplen truncates.
	CaptureLength int
	WireLength    int

	// Payload is the transport-layer payload (TCP/UDP data), not including
	// headers. Nil when the packet carries no payload or was truncated.
	Payload []byte
}

Packet is a decoded network packet reduced to the fields tracehound reasons about. It is passed by value through the pipeline; Payload aliases the capture buffer and is only valid until the next read from the same source. Anything that needs to outlive the current packet must copy it.

func (*Packet) IsSyn

func (p *Packet) IsSyn() bool

IsSyn reports whether the packet is a bare SYN, i.e. a connection attempt rather than a SYN-ACK response. Port-scan detection keys off this.

func (*Packet) IsSynAck

func (p *Packet) IsSynAck() bool

IsSynAck reports whether the packet accepts a connection, which is the signal that a scanned port was actually open.

type ParseError

type ParseError struct {
	Field string
	Value string
}

ParseError reports an unrecognised enum value in a rule file.

func (*ParseError) Error

func (e *ParseError) Error() string

type Protocol

type Protocol uint8

Protocol is an IANA IP protocol number. We define our own rather than reuse the capture library's type so that detectors never import gopacket.

const (
	ProtoICMP   Protocol = 1
	ProtoTCP    Protocol = 6
	ProtoUDP    Protocol = 17
	ProtoICMPv6 Protocol = 58
)

func (Protocol) String

func (p Protocol) String() string

type Severity

type Severity int

Severity ranks an alert for triage. The string forms match the vocabulary used by Sigma and most SIEMs so rule packs port across cleanly.

const (
	SevInfo Severity = iota
	SevLow
	SevMedium
	SevHigh
	SevCritical
)

func (Severity) MarshalText

func (s Severity) MarshalText() ([]byte, error)

MarshalText renders severity as its lowercase name in JSON and YAML.

func (Severity) String

func (s Severity) String() string

func (*Severity) UnmarshalText

func (s *Severity) UnmarshalText(b []byte) error

UnmarshalText parses a severity name, so rule files can say `severity: high`.

type Technique

type Technique struct {
	ID     string `json:"id" yaml:"id"`         // e.g. "T1071.004"
	Name   string `json:"name" yaml:"name"`     // e.g. "Application Layer Protocol: DNS"
	Tactic string `json:"tactic" yaml:"tactic"` // e.g. "command-and-control"
}

Technique is a MITRE ATT&CK technique reference attached to an alert.

Mapping every detection to ATT&CK is not decoration: it is what lets a SOC answer "which parts of the kill chain can we actually see?" and it is the first thing a detection engineer will look for in a tool like this.

Jump to

Keyboard shortcuts

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