baseline

package
v0.11.4 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Index

Constants

View Source
const SchemaVersion = 1

SchemaVersion is incremented on any breaking change to the baseline JSON format.

Variables

This section is empty.

Functions

This section is empty.

Types

type Baseline

type Baseline struct {
	KnownUsers map[string]UserProfile `json:"known_users"`

	// KnownActors is the set of actors seen during the baseline window.
	// Used by detector-new-actor.
	KnownActors []string `json:"known_actors,omitempty"`

	// KnownCreatedEntities is the set of principal identities (display_name /
	// principal_id / member) already OBSERVED being created via an entity-creation
	// event during the baseline window. detector-new-actor's created-entity gate
	// (createdEntityEvaluate) keys on THIS set — NOT KnownActors — so a genuinely
	// new created principal whose display_name collides with an existing known ACTOR
	// name still fires the first time it is created, while a repeat scan of the same
	// creation is gated. Kept distinct from KnownActors precisely so "this name is a
	// known actor" and "this created-principal entity was already seen" cannot be
	// conflated (the fix for the created-principal name-collision false negative).
	KnownCreatedEntities []string `json:"known_created_entities,omitempty"`

	// FrequencyTables maps "source:event_type" → baseline event count.
	// Used by detector-volume-anomaly.
	FrequencyTables map[string]int `json:"frequency_tables,omitempty"`

	// ActorHours maps actor → list of UTC hours (0-23) seen during baseline.
	// Used by detector-unusual-timing.
	ActorHours map[string][]int `json:"actor_hours,omitempty"`

	// ActorRoles maps actor → list of known role/permission keys.
	// Used by detector-priv-escalation.
	ActorRoles map[string][]string `json:"actor_roles,omitempty"`

	// Relationships maps a relationship key (the scenario's "actor:target" shape,
	// or any caller convention) → the historical actor↔target relationship record.
	// EVAL FIDELITY (FIX 4): legion's academy fed its agent the scenario's
	// relationships table so the model could answer "has this actor touched this
	// target before, and how often". Reconstructing it here lets check-baseline
	// surface the same relationship evidence the academy showed — without it the
	// portable eval projected only aggregate frequencies and the parity number
	// measured a model blind to actor↔target history.
	Relationships map[string]Relationship `json:"relationships,omitempty"`
}

Baseline holds historical patterns for known users and entities.

func Build added in v0.11.1

func Build(events []event.Event) *Baseline

Build derives a *Baseline from a corpus of historical events — the store's PRIOR accumulated event stream. It is the keystone that makes `mallcop scan` idempotent across runs: the pipeline gates the baseline-dependent detectors (new-actor, volume-anomaly, unusual-timing, priv-escalation, unusual-login) on the baseline Build produces from the events already seen, so an actor/pattern is investigated ONCE — the first scan it appears — and NOT re-investigated (and re-charged for inference) on every subsequent steady-state scan.

It populates ONLY the fields the shipped baseline-dependent detectors actually read (audited against every core/detect/*.go baseline access):

  • KnownActors — every distinct non-empty actor, PLUS the principals NAMED in entity-creation events (so new-actor's ACTOR-novelty gate does not re-fire when a created principal later authors its own events). Read by new-actor (IsKnownActor).
  • KnownCreatedEntities — the identities already OBSERVED being created via an entity-creation event. Read by new-actor's created-entity gate (createdEntityEvaluate → IsKnownCreatedEntity), kept SEPARATE from KnownActors so a brand-new created principal whose display_name collides with a known actor still fires the first time it is created.
  • FrequencyTables — "source:event_type:actor" → prior count. Read by volume-anomaly (FreqCountActor) and surfaced by check-baseline.
  • ActorHours — actor → sorted distinct UTC hours seen. Read by unusual-timing (HasActorHours / ActorHours / KnownHour).
  • ActorRoles — actor → sorted distinct role keys the priv-escalation detector would actually FIRE (and gate) on: the event type is an elevation type, the event isElevated (with the removal guard), and the role key came from an EXPLICIT role/permission field. Recorded only when the detector would genuinely investigate it — never off a non-firing event. Read by priv-escalation (IsKnownRole). See the SECURITY note below.
  • KnownUsers — actor → UserProfile{KnownIPs, KnownGeos, LastSeen} built from the actor's LOGIN events that carried at least one ip or geo (so HasLoginProfile stays meaningful). Read by unusual-login (HasUser / HasLoginProfile / KnownIP / KnownGeo).

It deliberately does NOT populate the 2-segment "source:event_type" frequency keys (exfil-pattern / rate-anomaly's FreqCount) nor Relationships (unusual-resource-access / the observe force-escalate predicates): those detectors judge the EVENT (content / magnitude / lateral-movement), not actor novelty, so leaving them unset keeps their behavior byte-identical to the current empty-baseline production path — no new activation, no regression.

SECURITY (does not hide a real attack): the baseline gates NEW-ACTOR-ness and PER-ROLE novelty only. A genuinely new actor, a genuinely new role for a known actor, a login from a new IP, a genuine volume spike — each still fires the first time it is seen. Detectors that judge the event itself (priv-escalation on admin_action / boundary-removal, exfil, secrets, injection, …) are NOT gated by the fields Build sets, so a known actor committing a NEW incident still fires. ActorRoles records ONLY explicit role/permission fields — never priv-escalation's event-type fallback (admin_action, boundary-delete keywords) — so the highest- severity field-less escalations fail SAFE (re-fire and re-escalate) rather than risk being suppressed by a baselined event type.

Output is deterministic: every slice is sorted, so the same event set always yields byte-identical JSON (the persisted KindBaseline snapshot is reproducible).

func Load

func Load(path string) (*Baseline, error)

Load reads and parses a baseline JSON file from disk.

func (*Baseline) Clone added in v0.8.0

func (b *Baseline) Clone() *Baseline

Clone returns a DEEP copy of the baseline: every map and slice is reallocated so a caller that mutates the returned value (or any nested slice/map inside it) cannot corrupt the original or any other holder of the original. Scalar, string, and time.Time fields are value-copied. A nil receiver clones to a fresh empty &Baseline{} so callers always get a usable, isolated value.

This is the per-detector input-isolation primitive (K7 HOLE 1a): detect.Detect threads ONE *Baseline through every registered detector, so a detector that mutated it would silently narrow what every later detector sees. Handing each detector its own clone makes that structurally impossible.

func (*Baseline) FreqCount

func (b *Baseline) FreqCount(source, eventType string) int

FreqCount returns the baseline event count for "source:event_type".

func (*Baseline) FreqCountActor added in v0.7.0

func (b *Baseline) FreqCountActor(source, eventType, actor string) int

FreqCountActor returns the baseline event count for the 3-segment key "source:event_type:actor" — the actor-aware shape the corpus frequency_tables use. volume-anomaly keys on this so the per-actor baseline (not a 2-segment aggregate) drives the spike ratio, matching the corpus key shape exactly.

func (*Baseline) HasActorHours

func (b *Baseline) HasActorHours() bool

HasActorHours returns true when there is any timing baseline data.

func (*Baseline) HasLoginProfile added in v0.7.0

func (b *Baseline) HasLoginProfile(actor string) bool

HasLoginProfile returns true when the actor has a baseline UserProfile carrying at least one known IP or known geo — i.e. there is login-location history to compare a new login against. A known actor with an empty profile (no IPs, no geos) returns false: unusual-login has no basis to flag the login as unusual and must not over-fire (eval-fidelity gate).

func (*Baseline) HasUser

func (b *Baseline) HasUser(actor string) bool

HasUser returns true when the actor appears in the baseline.

func (*Baseline) IsKnownActor

func (b *Baseline) IsKnownActor(actor string) bool

IsKnownActor returns true when the actor appears in KnownActors.

func (*Baseline) IsKnownCreatedEntity added in v0.11.1

func (b *Baseline) IsKnownCreatedEntity(name string) bool

IsKnownCreatedEntity returns true when the identity was already observed being CREATED (via an entity-creation event) during the baseline window. This is the gate detector-new-actor's created-entity check uses so a repeat scan of the same principal creation is suppressed while a brand-new created principal — even one whose name collides with a known actor — still fires the first time.

func (*Baseline) IsKnownRole

func (b *Baseline) IsKnownRole(actor, role string) bool

IsKnownRole returns true when actor+role is in the actor roles baseline.

func (*Baseline) KnownGeo

func (b *Baseline) KnownGeo(actor, geo string) bool

KnownGeo returns true when the geo is in the actor's profile.

func (*Baseline) KnownHour

func (b *Baseline) KnownHour(actor string, hour int) bool

KnownHour returns true when the given UTC hour is in the actor's known hours.

func (*Baseline) KnownIP

func (b *Baseline) KnownIP(actor, ip string) bool

KnownIP returns true when the IP is in the actor's profile.

func (*Baseline) RelationshipsFor added in v0.7.0

func (b *Baseline) RelationshipsFor(entity string) map[string]Relationship

RelationshipsFor returns the relationship records whose key references the given entity — the scenario keys an "actor:target" pair, so a key whose actor segment (before the first ':') equals the entity, OR any key that contains the entity as a segment, is returned. The result maps the matching relationship KEY → record (empty map, never nil, when there is no relationship data). Case-insensitive. This is the query check-baseline uses to surface actor↔target history (FIX 4).

type Engine

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

Engine maintains learned frequency tables and known-entity sets. Use NewEngine() to create, Update() to populate, and Save()/LoadEngine() for persistence.

func LoadEngine

func LoadEngine(path string) (*Engine, error)

LoadEngine reads a baseline file from disk. Returns an error if the file is corrupt, unreadable, or has a schema version mismatch. A schema mismatch or corrupt file MUST fail loudly — never return an empty engine silently.

func NewEngine

func NewEngine() *Engine

NewEngine returns a zero-value baseline engine ready for Update.

func (*Engine) EventCount

func (e *Engine) EventCount(entity string) int

EventCount returns the number of events seen for the entity within the window. Returns 0 for unknown entities.

func (*Engine) IsKnown

func (e *Engine) IsKnown(entity string) bool

IsKnown returns true if the entity was observed within the baseline window.

func (*Engine) Save

func (e *Engine) Save(path string) error

Save writes the baseline to disk as deterministic JSON. Same engine state always produces identical bytes (sorted keys, stable encoding).

func (*Engine) Update

func (e *Engine) Update(events []event.Event, window time.Duration, now time.Time)

Update rebuilds the engine state from the provided events using the given sliding window. now is the reference timestamp for window boundary calculation (use time.Now() in production). Calling Update again replaces all prior state.

type Relationship added in v0.7.0

type Relationship struct {
	Count     int    `json:"count"`
	FirstSeen string `json:"first_seen,omitempty"`
	LastSeen  string `json:"last_seen,omitempty"`
}

Relationship is a historical actor↔target relationship record: how many times an actor has acted on a target and the first/last time it did. Mirrors the scenario baseline's relationships entry so check-baseline can surface the academy's relationship evidence (FIX 4). FirstSeen/LastSeen are kept as the raw scenario strings (RFC3339 in the corpus) — the tool surfaces them verbatim as evidence.

type UserProfile

type UserProfile struct {
	KnownIPs  []string  `json:"known_ips"`
	KnownGeos []string  `json:"known_geos"` // e.g. "US", "GB"
	LastSeen  time.Time `json:"last_seen"`
}

UserProfile captures the expected behaviour for a single actor.

Jump to

Keyboard shortcuts

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