docs: add BEHAVE-SHELL reference page — all 37 primitives, attribution state machine, calibration data

2026-05-10 04:27:14 -04:00
parent e7d3353bfe
commit 58915d8115
3 changed files with 643 additions and 60 deletions

627
BEHAVE-SHELL.md Normal file

@@ -0,0 +1,627 @@
# BEHAVE-SHELL
BEHAVE-SHELL is DECNET's behavioural biometrics engine for interactive shell
sessions. It transforms raw PTY recordings into 37 attribution primitives
that fingerprint *how* an operator works — their motor patterns, cognitive
style, OPSEC habits, and emotional state — independently of what IP address
or tooling they use.
The primitives feed the [Identity-Resolution](Identity-Resolution) attribution
state machine, which accumulates evidence across sessions to answer: *is this
the same hands?*
---
## Design principles
- **Pure extraction library.** `extract_session()` takes an iterable of
asciinema events and yields `Observation` envelopes. No I/O, no DB access,
no bus calls. The worker owns all side effects.
- **PII by design.** Command text is never stored in plain form — only the
SHA-256 of the first token is retained. Output is reduced to a byte count
and an error verdict. Prompt lines are ANSI-stripped and capped at 256
characters.
- **Idempotent persistence.** `UniqueConstraint(evidence_ref, primitive)`
on the observations table means replaying a shard never duplicates rows.
- **Confidence capping.** Emotional-valence features carry a hard confidence
cap of 0.50 — they contribute, but never dominate an attribution decision.
---
## Data flow
```
PTY session
sessrec.c — writes JSONL shard per session
│ {"sid": id, "t": ts, "ch": "i"|"o", "d": data}
│ Non-UTF-8 bytes handled via surrogateescape
attacker.session.ended bus event
_handler.handle_session_ended()
│ Reads shard from disk → parse_shard_line() → AsciinemaEvent tuples
build_session_context() (_ctx.py, ~573 lines)
│ Seven derivation steps (see below)
extract_session() (extract.py)
│ Fan-out across 37 registered feature functions (FEATURES registry)
│ Each yields 0..N Observation envelopes
Upsert ObservationRow → publish attacker.observation.*
attribution_worker (attribution_worker.py)
│ Consumes attacker.observation.> bus events
│ Runs aggregate() per (identity_uuid, primitive)
AttributionStateRow state ∈ {unknown, stable, drifting, conflicted, multi_actor}
```
---
## Session context derivation
`build_session_context()` performs a single-pass walk over the raw event
stream and produces a `SessionContext` that all 37 feature functions read.
The seven derivation steps, in order:
| Step | What it computes |
|---|---|
| **Paste-burst detection** | Groups consecutive paste-class events (≥4 chars within 200 ms) into `paste_bursts` |
| **Typing-burst segmentation** | Splits the keystroke stream at think-pauses > 2.0 s into `typing_bursts[][]` (dropped if < 3 IATs) |
| **Correction signals** | Counts backspaces (`0x7f`, `0x08`) and kill-line sequences (`0x15`, `0x17`); records IATs between each backspace and the preceding keystroke |
| **Per-command intra-typing IATs** | For each command, extracts keystroke inter-arrival times from that command's span only |
| **Command segmentation** | Splits on `\r`/`\n`; per command records `first_token_hash` (SHA-256), tab count, readline shortcut count, and pipe count |
| **Inter-command IAT gaps** | Time between consecutive commands |
| **Error detection** | Scans output between commands for canonical error patterns (`"command not found"`, `"Permission denied"`, `"No such file"`) to set `command.errored` |
| **PS1 prompt detection** | Regex for `$`, `#`, `%`, `>` suffix after ANSI stripping; caps at 256 chars |
| **Keyboard layout fingerprinting** | Builds unigram and bigram histograms from typed letters |
| **Lexical counters** | Obscenity hits, positive/negative sentiment tokens, max caps run, max consecutive `!` run |
### Key data structures
```
SessionContext
sid: str
t_start, t_end, duration_s: float
input_events, output_events: tuple[AsciinemaEvent]
iats: tuple[float] # inter-keystroke intervals
paste_bursts: tuple[PasteBurst]
typing_bursts: tuple[tuple[float]]
backspace_count, kill_line_count: int
intra_command_iats: tuple[tuple[float]]
commands: tuple[Command]
inter_cmd_iats: tuple[float]
prompt_lines: tuple[PromptLine]
typed_unigram_counts, typed_bigram_counts: Mapping[str, int]
typed_letter_count: int
obscenity_hits, positive_lex_hits, negative_lex_hits: int
caps_run_max, bang_run_max: int
Command
start_ts, end_ts: float
first_token_hash: str # SHA-256 of first token only
tab_count, shortcut_count, pipe_count: int
errored: bool
output_bytes: int
followed_by_prompt: bool
PromptLine
ts: float
suffix_char: str # $ # % >
raw_line: str # ANSI-stripped, ≤256 chars
is_root: bool
```
---
## The 37 primitives
### Motor (9) — muscle memory and physical interaction style
These primitives capture *how* an operator's fingers interact with the
keyboard — patterns that persist across sessions, accounts, and even
operating systems.
#### 1. `input_modality`
Values: `typed` | `pasted` | `mixed`
Ratio of paste events to total input events. ≥40 % pasted and ≤5 %
typed → `pasted`; ≤5 % pasted → `typed`; otherwise `mixed`.
A script kiddie running pre-written one-liners pastes habitually. A
seasoned operator types most commands from memory.
#### 2. `paste_burst_rate`
Values: `none` | `occasional` | `habitual`
Coarser bucketing of the paste ratio. ≥50 % → `habitual`,
≥10 % → `occasional`.
#### 3. `keystroke_cadence`
Values: `steady` | `bursty` | `hunt_and_peck` | `machine`
Median coefficient of variation (CV) of within-burst inter-keystroke
intervals (IKIs).
| CV | Mean IKI | Label |
|---|---|---|
| < 0.30 | < 30 ms | `machine` |
| < 0.45 | any | `steady` |
| < 0.70 | any | `bursty` |
| ≥ 0.70 | any | `hunt_and_peck` |
`machine` catches automated input that passes as human visually but has
inhumanly uniform inter-key timing.
#### 4. `motor_stability`
Values: `steady` | `variable` | `tremor`
Fraction of IKIs below the tremor floor (30 ms). ≥20 % → `tremor`
(physiological or tool-simulated). Otherwise the median CV classifies
`steady` vs `variable`.
#### 5. `error_correction`
Values: `immediate` | `deferred` | `absent` | `route_around`
Timing of backspace relative to the preceding keystroke. Median ≤500 ms
`immediate` (noticed fast, muscle-memory correction). Median > 500 ms
`deferred` (reads output then corrects). Zero backspaces but kill-line
present → `route_around` (ctrl-u / ctrl-w). No corrections at all →
`absent`.
#### 6. `command_chunking`
Values: `fluent` | `fragmented` | `single_command`
Median CV of per-command intra-typing IKIs. < 0.40 → `fluent` (commands
typed as rehearsed phrases). Otherwise `fragmented`. Only one command
in session → `single_command`.
#### 7. `shell_mastery.tab_completion`
Values: `none` | `occasional` | `habitual`
Fraction of commands containing at least one `0x09` (tab) keystroke.
0 → `none`, < 50 % → `occasional`, ≥ 50 % → `habitual`.
Operators who tab-complete heavily know the filesystem; those who never do
either memorise paths or are running a prepared script.
#### 8. `shell_mastery.shortcut_usage`
Values: `none` | `moderate` | `heavy`
Readline control-byte count (ctrl-a, ctrl-e, ctrl-r, etc.) per command.
< 0.05 → `none`, < 0.15 → `moderate`, ≥ 0.15 → `heavy`.
#### 9. `shell_mastery.pipe_chaining_depth`
Values: `shallow` | `moderate` | `deep`
Median pipe count per command. ≤1 → `shallow`, 2 → `moderate`, ≥3 → `deep`.
---
### Cognitive (11) — decision-making and planning style
These primitives capture *how* an operator thinks — their command repertoire,
response to failure, and how much they read output before acting.
#### 10. `inter_command_latency_class`
Values: `instant` | `typing_speed` | `deliberate` | `llm_lightweight` | `llm_heavyweight` | `long`
Median inter-command pause bucketed against calibrated thresholds:
| Threshold | Label | What it suggests |
|---|---|---|
| ≤ 0.30 s | `instant` | Scripted or replay |
| ≤ 1.50 s | `typing_speed` | Commands prepared, typing only |
| ≤ 2.00 s | `deliberate` | Reads output before next command |
| ≤ 8.00 s | `llm_lightweight` | May be consulting a fast LLM / notes |
| ≤ 30.00 s | `llm_heavyweight` | Consulting a slow LLM or manual reference |
| > 30.00 s | `long` | Long pauses — possibly interrupted or cautious |
`llm_lightweight` and `llm_heavyweight` were calibrated against Claude
Free (fast) and Claude (slow) assisted operator sessions — a novel class
of adversary DECNET is designed to detect.
#### 11. `command_branch_diversity`
Values: `linear_playbook` | `adaptive_branching` | `unknown`
Unique first-token / total command ratio. < 5 commands → `unknown`.
≥ 70 % unique → `linear_playbook` (each command is different — following
a prepared list). < 70 % → `adaptive_branching` (repeating tools,
iterating on a problem).
#### 12. `feedback_loop_engagement`
Values: `closed_loop` | `fire_and_forget` | `unknown`
Pearson correlation between per-command output bytes and the following
inter-command pause. r > 0.30 → `closed_loop` (pauses longer when there
is more output to read). Otherwise `fire_and_forget`. Requires ≥5
command/output/pause triples.
#### 13. `inter_command_consistency`
Values: `metronomic` | `variable` | `bimodal`
CV of inter-command IKIs. < 0.40 → `metronomic` (scripts, beacons).
> 1.50 → `bimodal` (two distinct paces — often short commands interleaved
with long waits for a compile or download). Otherwise `variable`.
#### 14. `cognitive_load`
Values: `low` | `medium` | `high`
Composite score: mean of (intra-typing CV / 1.0, error rate, pause CV / 1.5).
< 0.33 → `low`, < 0.67 → `medium`, otherwise `high`.
High cognitive load across multiple sessions on the same identity is a
signal of an operator working outside their comfort zone — new target OS,
unfamiliar tooling, or time pressure.
#### 15. `exploration_style`
Values: `methodical` | `targeted` | `chaotic`
`repetition_rate` = 1 unique/total commands.
`backtrack_rate` = fraction of commands that jump back to a previously used
tool category. Backtrack ≥30 % → `chaotic`. Repetition ≥50 % → `targeted`
(narrow focus, known objective). Otherwise `methodical`.
#### 16. `planning_depth`
Values: `deep` | `reactive` | `shallow`
`deep_pause_frac` = fraction of inter-command IKIs > 2.0 s.
`reactive_frac` = fraction ≤ 0.30 s. ≥40 % deep pauses → `deep`.
≥50 % reactive → `reactive`. Otherwise `shallow`.
#### 17. `tool_vocabulary`
Values: `narrow` | `moderate` | `broad`
Distinct first-token count (absolute). ≤3 → `narrow`, ≥10 → `broad`.
#### 18. `error_resilience.retry_tactic`
Values: `retry_same` | `pivot` | `fallback`
Post-error behaviour: does the operator retry the same command, switch to
a different approach, or fall back to reconnaissance? Skipped if no errors
occurred in the session.
#### 19. `error_resilience.frustration_typing`
Values: `low` | `moderate` | `high`
Delta between median intra-IKI after an error vs. after a success.
< 10 % delta → `low`, < 30 % → `moderate`, ≥30 % → `high`.
Fast typing after errors suggests frustration; slow typing suggests
deliberation.
#### 20. `error_resilience.fallback_to_man`
Values: `present` | `absent`
After an error, does the next command start with `man`, `help`, or `info`?
Skipped if no errors. `present` indicates an operator consulting
documentation — less automated, less rehearsed.
---
### Temporal (4) — session rhythm and pacing
#### 21. `session_duration`
Values: `short` | `medium` | `long` | `marathon`
| Duration | Label |
|---|---|
| < 60 s | `short` — single recon or scan |
| < 600 s | `medium` — targeted interaction |
| < 3600 s | `long` — sustained operation |
| ≥ 3600 s | `marathon` — extended presence / slow-burn APT |
#### 22. `escalation_pattern`
Values: `bursty` | `sustained`
Dynamic window analysis (window width = max(10 s, duration / target)).
CV and zero-window fraction classify whether activity clusters into bursts
separated by idle periods, or maintains a consistent level throughout.
#### 23. `landing_ritual`
Values: `cleanup` | `exploration` | `passive`
First ~5 commands classified by intent tokens. `cleanup` if the operator
immediately starts removing evidence; `exploration` if they run
reconnaissance commands (`id`, `whoami`, `uname`, `ls`); `passive` if
they do nothing that reveals intent.
#### 24. `exit_behavior`
Values: `cleanup` | `standard` | `anomalous`
Last ~5 commands. `cleanup` if history/log deletion or `exit`/`logout`
appears. `anomalous` if the session ends abruptly with no recognisable
closing pattern.
---
### Environmental (5) — operator's local setup
These are stable across an operator's career and change only when they
switch machines or retool.
#### 25. `shell_type`
Values: `bash` | `sh` | `zsh` | `fish` | `unknown`
Detected from PS1 prompt regex patterns after ANSI stripping.
#### 26. `terminal_multiplexer`
Values: `tmux` | `screen` | `none`
Detected from PS1 markers and characteristic escape sequences.
#### 27. `locale`
Values: `en-US` | `en` | `other` | `unknown`
Language-specific keywords in prompt lines and error messages.
#### 28. `keyboard_layout`
Values: `qwerty` | `dvorak` | `colemak` | `other`
Bigram frequency analysis of the typed character stream. Operators who
touch-type on Dvorak produce a statistically distinct bigram distribution
that persists even when typing non-English commands.
#### 29. `numpad_usage`
Values: `occasional` | `frequent` | `none`
Keystroke pattern detection for numpad-originated digits.
---
### Operational (4) — mission and OPSEC posture
#### 30. `objective`
Values: `recon` | `exfil` | `persistence` | `lateral` | `destructive`
Token-based intent classification of command first-tokens. Majority vote
across classified tokens; precedence order applied for ties. Skipped if
fewer than 3 classified tokens.
Example token mappings:
- `recon`: `id`, `whoami`, `uname`, `cat`, `find`, `ls`, `ps`, `netstat`
- `exfil`: `scp`, `curl`, `wget`, `base64`, `nc`, `rsync`
- `persistence`: `crontab`, `echo`, `tee`, `systemctl`, `rc.local`
- `lateral`: `ssh`, `xfreerdp`, `psexec`, `wmiexec`
- `destructive`: `rm`, `shred`, `dd`, `mkfs`, `kill`
#### 31. `opsec_discipline`
Values: `careful` | `learning` | `careless`
Presence of history-disabling tokens (`unset HISTFILE`, `HISTSIZE=0`,
`history -c`) and cleanup activity in the session tail. Both → `careful`.
History-only → `learning` (knows to cover tracks but forgets cleanup).
Neither → `careless`.
#### 32. `cleanup_behavior`
Values: `thorough` | `partial` | `none`
Distinct cleanup tokens in the last 5 commands. ≥3 → `thorough`,
12 → `partial`, 0 → `none`.
#### 33. `multi_actor_indicators`
Values: `solo` | `handoff_detected`
Splits commands at the session's temporal midpoint and compares the median
intra-IKI of each half. If the delta exceeds 50 % and both halves have
≥4 commands, `handoff_detected` is emitted — the session was likely shared
between two operators (e.g. initial access handed to a post-exploitation
specialist).
---
### Emotional valence (4) — stress and cognitive state
These features have a hard confidence cap of **0.50** — they contribute to
attribution but cannot dominate it. They require ≥80 typed letters to emit.
#### 34. `valence`
Values: `positive` | `neutral` | `negative`
Lexical positive/negative token counts. `positive` if positive count >
(negative + obscenity) and ≥2 positive tokens.
#### 35. `arousal`
Values: `low_calm` | `medium_engaged` | `high_agitated`
`high_agitated` if ≥5 consecutive caps, ≥3 consecutive `!`, or fastest
IKI < 60 ms on ≥30 keystrokes. `low_calm` if slowest IKI > 300 ms.
Otherwise `medium_engaged`.
#### 36. `stress_response`
Values: `none` | `eustress_positive` | `distress_negative`
Post-error vs baseline typing speed ratio. ≥1.20 → `eustress` (types
faster under pressure — experienced). ≤ 1/1.20 → `distress` (types
slower — less experienced or genuinely stressed).
#### 37. `frustration_venting`
Values: `low` | `moderate` | `high`
Post-error frustration token count plus obscenity count.
---
## Attribution state machine
Primitives feed a per-`(identity_uuid, primitive)` state machine in
`decnet/correlation/attribution/aggregate.py`.
### States
| State | Meaning | Condition |
|---|---|---|
| `unknown` | Insufficient data | < 3 observations |
| `stable` | Consistent value | Recent N agree AND no drift from older N |
| `drifting` | Recently changed | Recent N agree BUT differ from older N |
| `conflicted` | Contradictory values | Recent N are split (high CV) |
| `multi_actor` | Multiple operators | `conflicted` + cross-session alternation |
Window size N = 5 (categorical primitives). EWMA is used for numeric
primitives (Phase 3).
### Multi-actor detection
The attribution worker runs a `_multi_actor_tick` every 60 seconds. For
every `(identity, primitive)` pair in `conflicted` state, it checks whether
the alternation pattern across sessions is consistent with a credential
being shared between two distinct operators. When ≥2 primitives
independently flag `multi_actor` for the same identity, the bus emits:
```
attribution.profile.multi_actor_suspected
{identity_uuid, primitives: [...], evidence_summary, confidence, ts}
```
`confidence` is capped at 0.60 — cross-primitive agreement is the real
signal, but a hard cap prevents over-alarming on noisy primitives.
---
## Database tables
### `ObservationRow`
One row per `(evidence_ref, primitive)`. `evidence_ref` is the session
shard identifier — the `UniqueConstraint` makes re-processing idempotent.
| Column | Type | Description |
|---|---|---|
| `id` | UUID PK | |
| `identity_uuid` | FK → `attacker_identities` | |
| `attacker_uuid` | FK → `attackers` | Direct link for pre-clusterer path |
| `evidence_ref` | TEXT | Shard ID |
| `primitive` | TEXT | e.g. `keystroke_cadence` |
| `value` | TEXT | Categorical label or serialised numeric |
| `confidence` | FLOAT | 0.01.0 |
| `observed_at` | DATETIME | Session end time |
### `AttributionStateRow`
One row per `(identity_uuid, primitive)`. Updated by the attribution
worker each time a new observation arrives.
| Column | Type | Description |
|---|---|---|
| `identity_uuid` | FK → `attacker_identities` | |
| `primitive` | TEXT | |
| `state` | TEXT | `unknown`/`stable`/`drifting`/`conflicted`/`multi_actor` |
| `current_value` | TEXT | Most recent or EWMA value |
| `confidence` | FLOAT | |
| `observation_count` | INT | Total observations aggregated |
| `last_observation_ts` | DATETIME | |
---
## Key thresholds
All calibration constants live in `decnet/profiler/behave_shell/_thresholds.py`
(416 lines). The values below are the defaults; they can be overridden per
deployment without touching feature code.
| Constant | Value | Used by |
|---|---|---|
| `PASTE_MIN_CHARS_PER_EVENT` | 4 | Paste detection |
| `PASTE_BURST_MAX_IAT_S` | 0.20 | Paste burst grouping |
| `MODALITY_PASTED_MIN` | 0.40 | `input_modality` |
| `CV_STEADY_MAX` | 0.45 | `keystroke_cadence` |
| `TREMOR_FAST_FLOOR_S` | 0.030 | `motor_stability` |
| `IKI_THINK_MAX_S` | 2.0 | Typing-burst split |
| `INTER_CMD_INSTANT_MAX` | 0.30 s | `inter_command_latency_class` |
| `INTER_CMD_LLM_LIGHTWEIGHT_MAX` | 8.0 s | LLM-assisted detection |
| `INTER_CMD_LLM_HEAVYWEIGHT_MAX` | 30.0 s | LLM-assisted detection |
| `BRANCH_DIVERSITY_LINEAR_MIN` | 0.70 | `command_branch_diversity` |
| `FEEDBACK_CORRELATION_MIN` | 0.30 | `feedback_loop_engagement` |
| `PAUSE_CV_METRONOMIC_MAX` | 0.40 | `inter_command_consistency` |
| `PAUSE_CV_BIMODAL_MIN` | 1.50 | `inter_command_consistency` |
| `SESSION_DURATION_SHORT_MAX` | 60 s | `session_duration` |
| `SESSION_DURATION_MEDIUM_MAX` | 600 s | `session_duration` |
| `SESSION_DURATION_LONG_MAX` | 3600 s | `session_duration` |
| `MIN_OBSERVATIONS_FOR_STATE` | 3 | Attribution state machine |
| `CATEGORICAL_WINDOW_N` | 5 | Attribution window |
| `MULTI_ACTOR_TICK_SECS` | 60 | Multi-actor tick |
| `EMOTIONAL_VALENCE_CONFIDENCE_CAP` | 0.50 | All `emotional_valence` features |
---
## Calibration
The system was calibrated against five behavioural classes across 15 sessions
(424 total observations):
| Class | Sessions | Observations | Description |
|---|---|---|---|
| `HUMAN` | 1 | 34 | Human operator, no assistance |
| `YOU-sim` | 2 | 59 | Human-simulated scripted attacker |
| `LW-sim` | 5 | 136 | Lightweight LLM-assisted operator |
| `CLAUDE-FF` | 3 | 84 | Claude (fast/free tier) assisted |
| `CLAUDE-CL` | 4 | 111 | Claude (standard tier) assisted |
All classes emit ≥27 distinct primitives (pass threshold).
The `inter_command_latency_class` thresholds `llm_lightweight` (≤8 s) and
`llm_heavyweight` (≤30 s) were derived from timing measurements of these
sessions — DECNET can distinguish a human-with-fast-LLM from an unassisted
human in a single session with moderate confidence, and with high confidence
across 3+ sessions.
---
## Testing
```bash
# Offline smoke test — 5 shards, mock bus, must emit ≥27 distinct per class
scripts/behave_shell/smoke.sh
# Live round-trip — replay calibration shards through a running DECNET
scripts/behave_shell/replay_calibration.py
```
---
## File reference
```
decnet/profiler/behave_shell/
__init__.py Public API: extract_session()
extract.py Entry point — fans out to FEATURES registry (51 lines)
_ctx.py SessionContext builder (573 lines)
_parse.py Asciinema JSONL parsing (272 lines)
_handler.py Bus subscriber — disk I/O, persistence, publish (235 lines)
_intent.py Token → intent classification (115 lines)
_thresholds.py All calibration constants (416 lines)
_features/
__init__.py FEATURES registry — list of 37 functions (104 lines)
motor.py Primitives 19 (422 lines)
cognitive.py Primitives 1020 (593 lines)
temporal.py Primitives 2124 (237 lines)
environmental.py Primitives 2529 (352 lines)
operational.py Primitives 3033 (218 lines)
emotional_valence.py Primitives 3437 (223 lines)
decnet/correlation/
attribution_worker.py Bus loop: consume observations, run tick
attribution/
aggregate.py State machine: unknown→stable→drifting→conflicted→multi_actor
_thresholds.py Attribution-layer thresholds
decnet/web/db/models/
observations.py ObservationRow schema
attribution_state.py AttributionStateRow schema
```
---
## Related pages
- [Fingerprinting](Fingerprinting) — all fingerprint layers, including the
BEHAVE-SHELL summary
- [Identity-Resolution](Identity-Resolution) — how observations are clustered
into attacker identities and how state machine transitions propagate
- [Service-Personas](Service-Personas) — enabling session recording and
BEHAVE-SHELL per service

@@ -178,69 +178,24 @@ Command content reveals intent directly: reconnaissance (`id`, `whoami`,
persistence (`crontab -e`, `echo >> ~/.bashrc`), exfiltration
(`curl`, `wget`, `base64`, `scp`).
#### Keystroke dynamics (BEHAVE-SHELL spec)
#### Keystroke dynamics (BEHAVE-SHELL)
The BEHAVE-SHELL spec (`decnet/profiler/behave_shell/`) extracts
fine-grained typing and session behaviour from the PTY stream. These
become **attribution primitives** — per-`(identity_uuid, primitive)`
state-machine entries that accumulate evidence across sessions.
The BEHAVE-SHELL engine (`decnet/profiler/behave_shell/`) extracts **37
attribution primitives** across six domains from the PTY stream: motor
(typing cadence, error correction, shell mastery), cognitive (planning depth,
feedback loop engagement, tool vocabulary), temporal (session duration,
escalation pattern, landing and exit rituals), environmental (shell type,
keyboard layout, terminal multiplexer), operational (objective, OPSEC
discipline, multi-actor detection), and emotional valence (stress response,
frustration venting).
**Motor patterns** (muscle memory, latency):
Each primitive feeds a per-`(identity_uuid, primitive)` state machine
(`unknown → stable → drifting → conflicted → multi_actor`). When two or
more primitives independently reach `multi_actor`, an
`attribution.profile.multi_actor_suspected` bus event fires.
| Primitive | Description |
|---|---|
| `interarrival_mean_sec` | Mean time between keystrokes/commands |
| `interarrival_p75_sec`, `interarrival_p99_sec` | Tail latency — distinguishes human from bot |
| `flow_rate_cmd_per_sec` | Command execution rate |
| `burst_event_count` | Clustering in time (burst size) |
| `typing_speed_wpm` | Estimated words per minute |
| `error_correction_ratio` | Backspace and correction frequency |
**Cognitive patterns** (decision-making):
| Primitive | Description |
|---|---|
| `command_error_rate` | Failure-command ratio |
| `retry_on_failure_ratio` | Persistence on error |
| `command_redo_rate` | Repeating the same failed command |
| `pipeline_breadth`, `pipeline_depth` | Command composition style |
| `distinct_tools_used` | Toolkit diversity per session |
| `tool_switch_frequency` | How often the operator changes tool |
| `verbose_flag_usage` | `-v`/`-vv` flag frequency (confidence proxy) |
**Temporal patterns** (working hours, rhythm):
| Primitive | Description |
|---|---|
| `activity_hour_of_day_entropy` | Consistency of working hours |
| `activity_day_of_week_entropy` | Weekly routine |
| `session_duration_p50_sec`, `p95_sec` | Session length distribution |
| `gaps_between_sessions_p50_sec` | Rest period / tool pacing |
**Environmental patterns** (operator setup):
| Primitive | Description |
|---|---|
| `shell_type` | bash / sh / zsh / fish / etc. |
| `environment_vars_entropy` | Degree of environment customisation |
| `working_directory_volatility` | Directory-jumping frequency |
| `tty_capabilities` | Terminal rows, cols, and `$TERM` value |
**Operational patterns** (technique selection):
| Primitive | Description |
|---|---|
| `privilege_escalation_attempts` | `sudo` / `su` frequency |
| `lateral_movement_attempts` | SSH/RDP connection attempts |
| `data_exfiltration_indicators` | `scp`, `curl`, `wget`, `base64`, `zcat` |
| `credential_access_attempts` | Greping for passwords, SSH key files |
| `persistence_technique_count` | Crontab edits, `.bashrc` modifications |
Each primitive has a state machine: `unknown → stable → drifting →
conflicted → multi_actor`. When two or more primitives independently flag
`multi_actor` (e.g. two distinct shell types alternating per session),
an `attribution.profile.multi_actor_suspected` bus event fires — a strong
indicator of a shared credential or a compromised operator account.
See **[BEHAVE-SHELL](BEHAVE-SHELL)** for the complete primitive reference,
computation logic, calibration data, and thresholds.
---

@@ -51,6 +51,7 @@
- [Testing-and-CI](Testing-and-CI)
- [Campaign-Clustering](Campaign-Clustering)
- [Identity-Resolution](Identity-Resolution)
- [BEHAVE-SHELL](BEHAVE-SHELL)
- [Performance-Story](Performance-Story)
- [Tracing-and-Profiling](Tracing-and-Profiling)