Documentation
¶
Index ¶
- Constants
- type Baseline
- func (b *Baseline) Clone() *Baseline
- func (b *Baseline) FreqCount(source, eventType string) int
- func (b *Baseline) FreqCountActor(source, eventType, actor string) int
- func (b *Baseline) HasActorHours() bool
- func (b *Baseline) HasLoginProfile(actor string) bool
- func (b *Baseline) HasUser(actor string) bool
- func (b *Baseline) IsKnownActor(actor string) bool
- func (b *Baseline) IsKnownCreatedEntity(name string) bool
- func (b *Baseline) IsKnownRole(actor, role string) bool
- func (b *Baseline) KnownGeo(actor, geo string) bool
- func (b *Baseline) KnownHour(actor string, hour int) bool
- func (b *Baseline) KnownIP(actor, ip string) bool
- func (b *Baseline) RelationshipsFor(entity string) map[string]Relationship
- type Engine
- type Relationship
- type UserProfile
Constants ¶
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
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 (*Baseline) Clone ¶ added in v0.8.0
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) FreqCountActor ¶ added in v0.7.0
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 ¶
HasActorHours returns true when there is any timing baseline data.
func (*Baseline) HasLoginProfile ¶ added in v0.7.0
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) IsKnownActor ¶
IsKnownActor returns true when the actor appears in KnownActors.
func (*Baseline) IsKnownCreatedEntity ¶ added in v0.11.1
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 ¶
IsKnownRole returns true when actor+role is in the actor roles baseline.
func (*Baseline) KnownHour ¶
KnownHour returns true when the given UTC hour is in the actor's known hours.
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 ¶
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 ¶
EventCount returns the number of events seen for the entity within the window. Returns 0 for unknown entities.
func (*Engine) IsKnown ¶
IsKnown returns true if the entity was observed within the baseline window.
func (*Engine) Save ¶
Save writes the baseline to disk as deterministic JSON. Same engine state always produces identical bytes (sorted keys, stable encoding).
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.