behave-shell 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,37 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """BEHAVE observation envelope and primitive registry — DECNET-aligned.
3
+
4
+ Public API:
5
+
6
+ from spec import Observation, Window, OBSERVATION_SCHEMA_VERSION
7
+ from spec import PRIMITIVE_REGISTRY, ValueKind, ValueTypeSpec
8
+ from spec import event_topic_for, to_event_payload, from_event_payload
9
+
10
+ See ``spec.envelope`` for the central PII-discipline statement that binds every
11
+ sensor emitting BEHAVE observations.
12
+ """
13
+
14
+ from .envelope import OBSERVATION_SCHEMA_VERSION, Observation, ObservationValue, Window
15
+ from .event_adapter import (
16
+ TOPIC_PREFIX,
17
+ event_topic_for,
18
+ from_event_payload,
19
+ to_event_payload,
20
+ )
21
+ from .primitives import PRIMITIVE_REGISTRY, ValueKind, ValueTypeSpec, get, is_known
22
+
23
+ __all__ = [
24
+ "OBSERVATION_SCHEMA_VERSION",
25
+ "Observation",
26
+ "ObservationValue",
27
+ "Window",
28
+ "PRIMITIVE_REGISTRY",
29
+ "ValueKind",
30
+ "ValueTypeSpec",
31
+ "is_known",
32
+ "get",
33
+ "TOPIC_PREFIX",
34
+ "event_topic_for",
35
+ "to_event_payload",
36
+ "from_event_payload",
37
+ ]
@@ -0,0 +1,57 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """BEHAVE-SHELL Observation envelope (registry-aware subclass).
3
+
4
+ The base envelope (`Observation`, `Window`, `OBSERVATION_SCHEMA_VERSION`,
5
+ `ObservationValue`) lives in `behave-core`; it enforces only structural
6
+ invariants (window ordering, confidence bounds, schema version, no extras).
7
+
8
+ This module subclasses the core `Observation` to add registry-aware validation
9
+ against `BEHAVE-SHELL`'s `PRIMITIVE_REGISTRY`. The subclass is exported under
10
+ the same name `Observation` so existing imports (``from spec.envelope import
11
+ Observation``) continue to resolve to the registry-validated form without
12
+ consumer changes.
13
+
14
+ PII discipline (lifted from DECNET ``attackers.py:268-285,308-311``) — see the
15
+ core envelope module docstring for the binding statement.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from pydantic import model_validator
21
+
22
+ from behave_core.spec.envelope import (
23
+ OBSERVATION_SCHEMA_VERSION,
24
+ ObservationValue,
25
+ Window,
26
+ )
27
+ from behave_core.spec.envelope import Observation as _BaseObservation
28
+
29
+ from .primitives import PRIMITIVE_REGISTRY
30
+
31
+
32
+ class Observation(_BaseObservation):
33
+ """Shell-domain Observation: base envelope + BEHAVE-SHELL registry check."""
34
+
35
+ @model_validator(mode="after")
36
+ def _validate_against_shell_registry(self) -> "Observation":
37
+ spec = PRIMITIVE_REGISTRY.get(self.primitive)
38
+ if spec is None:
39
+ raise ValueError(
40
+ f"unknown primitive {self.primitive!r}; "
41
+ f"add it to spec/primitives.py:PRIMITIVE_REGISTRY first"
42
+ )
43
+ try:
44
+ spec.validate_value(self.value)
45
+ except ValueError as exc:
46
+ raise ValueError(
47
+ f"value invalid for primitive {self.primitive!r}: {exc}"
48
+ ) from None
49
+ return self
50
+
51
+
52
+ __all__ = [
53
+ "OBSERVATION_SCHEMA_VERSION",
54
+ "Observation",
55
+ "ObservationValue",
56
+ "Window",
57
+ ]
@@ -0,0 +1,58 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """DECNET bus interop. Aligns BEHAVE Observation with DECNET Event payload shape.
3
+
4
+ DECNET's Event (decnet/bus/base.py:26) carries ``(topic, payload, type, v, ts, id)``.
5
+ A BEHAVE Observation maps onto that envelope as follows:
6
+
7
+ topic = "attacker.observation." + observation.primitive
8
+ payload = observation.model_dump(exclude={"id", "ts", "v"})
9
+ type = observation.primitive
10
+ v = observation.v
11
+ ts = observation.ts
12
+ id = observation.id
13
+
14
+ The publisher must set ``topic`` from the primitive when calling ``bus.publish()``;
15
+ DECNET's bus does not trust topic from the wire (anti-spoofing, base.py:60-76).
16
+
17
+ This module does NOT import DECNET. The adapter speaks dicts; consumers wire it
18
+ to their own bus.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Any
24
+
25
+ from .envelope import Observation
26
+
27
+ TOPIC_PREFIX: str = "attacker.observation"
28
+
29
+
30
+ def event_topic_for(primitive: str) -> str:
31
+ """Return the canonical DECNET bus topic for a BEHAVE primitive."""
32
+ return f"{TOPIC_PREFIX}.{primitive}"
33
+
34
+
35
+ def to_event_payload(obs: Observation) -> dict[str, Any]:
36
+ """Project an Observation into a dict suitable for ``Event.payload``.
37
+
38
+ Excludes ``id``, ``ts``, and ``v`` because those are carried at the Event
39
+ envelope level by DECNET, not in the payload body.
40
+ """
41
+ return obs.model_dump(exclude={"id", "ts", "v"}, mode="json")
42
+
43
+
44
+ def from_event_payload(primitive: str, payload: dict[str, Any]) -> Observation:
45
+ """Reconstruct an Observation from ``(topic-derived primitive, Event.payload)``.
46
+
47
+ The ``primitive`` argument is the trailing segment of the bus topic, NOT a
48
+ field read from the payload — relying on the wire-side ``primitive`` field
49
+ would let a misbehaving publisher spoof observations on topics they don't
50
+ actually publish to. This mirrors DECNET's ``Event.from_dict`` discipline
51
+ (decnet/bus/base.py:60-76).
52
+ """
53
+ if "primitive" in payload and payload["primitive"] != primitive:
54
+ raise ValueError(
55
+ f"payload.primitive ({payload['primitive']!r}) does not match "
56
+ f"topic-derived primitive ({primitive!r}); refusing to reconstruct"
57
+ )
58
+ return Observation.model_validate({**payload, "primitive": primitive})
@@ -0,0 +1,730 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ """BEHAVE primitive registry.
3
+
4
+ Source-of-truth for what `Observation.primitive` may be and what `Observation.value`
5
+ must look like.
6
+
7
+ Adding a new primitive is a deliberate registry edit. Sensors are expected to fail
8
+ loudly if they construct an `Observation` with an unknown primitive — that is by
9
+ design.
10
+
11
+ PII discipline: the value-type specs here describe the SHAPE of the value, not
12
+ its content. Sensors are still bound by the rules in `spec/envelope.py`'s module
13
+ docstring — never put raw keystrokes, command bodies, credentials, or payload
14
+ bytes into a value, regardless of what shape this registry permits.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from enum import Enum
20
+ from typing import Any, Optional
21
+
22
+ from pydantic import BaseModel, Field
23
+
24
+
25
+ class ValueKind(str, Enum):
26
+ """Discriminator for the shape an `Observation.value` must take."""
27
+
28
+ CATEGORICAL = "categorical" # str, must appear in `allowed`
29
+ NUMERIC = "numeric" # int | float, optional min/max bounds
30
+ HASH = "hash" # str — hex / base64 / fingerprint string
31
+ ARRAY = "array" # list, element shape given by `array_of`
32
+ FREE_STRING = "free_string" # arbitrary string (e.g. BCP-47 locale, p0f label)
33
+ BOOL = "bool" # plain boolean
34
+
35
+
36
+ class ValueTypeSpec(BaseModel):
37
+ """Per-primitive value-type spec.
38
+
39
+ Only the fields relevant to ``kind`` should be populated; the rest stay None.
40
+ Validation in ``Observation`` consults this spec to accept or reject a value
41
+ for a given primitive.
42
+ """
43
+
44
+ kind: ValueKind
45
+ allowed: Optional[list[str]] = Field(
46
+ default=None, description="CATEGORICAL only — enum of valid string values"
47
+ )
48
+ min_val: Optional[float] = Field(default=None, description="NUMERIC lower bound (inclusive)")
49
+ max_val: Optional[float] = Field(default=None, description="NUMERIC upper bound (inclusive)")
50
+ array_of: Optional[ValueKind] = Field(
51
+ default=None, description="ARRAY only — kind of each element"
52
+ )
53
+ notes: Optional[str] = Field(default=None, description="Free-form note for registry readers")
54
+
55
+ def validate_value(self, value: Any) -> None:
56
+ """Raise ``ValueError`` if *value* does not conform to this spec."""
57
+ if self.kind is ValueKind.CATEGORICAL:
58
+ if not isinstance(value, str):
59
+ raise ValueError(f"expected categorical string, got {type(value).__name__}")
60
+ if self.allowed is not None and value not in self.allowed:
61
+ raise ValueError(
62
+ f"value {value!r} not in allowed set {self.allowed!r}"
63
+ )
64
+ elif self.kind is ValueKind.NUMERIC:
65
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
66
+ raise ValueError(f"expected numeric, got {type(value).__name__}")
67
+ if self.min_val is not None and value < self.min_val:
68
+ raise ValueError(f"value {value} below min_val {self.min_val}")
69
+ if self.max_val is not None and value > self.max_val:
70
+ raise ValueError(f"value {value} above max_val {self.max_val}")
71
+ elif self.kind is ValueKind.HASH:
72
+ if not isinstance(value, str) or not value:
73
+ raise ValueError("expected non-empty hash string")
74
+ elif self.kind is ValueKind.FREE_STRING:
75
+ if not isinstance(value, str):
76
+ raise ValueError(f"expected string, got {type(value).__name__}")
77
+ elif self.kind is ValueKind.BOOL:
78
+ if not isinstance(value, bool):
79
+ raise ValueError(f"expected bool, got {type(value).__name__}")
80
+ elif self.kind is ValueKind.ARRAY:
81
+ if not isinstance(value, list):
82
+ raise ValueError(f"expected array, got {type(value).__name__}")
83
+ if self.array_of is None:
84
+ return
85
+ element_spec = ValueTypeSpec(kind=self.array_of)
86
+ for i, element in enumerate(value):
87
+ try:
88
+ element_spec.validate_value(element)
89
+ except ValueError as exc:
90
+ raise ValueError(f"array element [{i}]: {exc}") from None
91
+
92
+
93
+ # ─── Convenience constructors (keep the registry table readable) ────────────
94
+
95
+ def _cat(*allowed: str, notes: Optional[str] = None) -> ValueTypeSpec:
96
+ return ValueTypeSpec(kind=ValueKind.CATEGORICAL, allowed=list(allowed), notes=notes)
97
+
98
+ def _num(min_val: Optional[float] = None, max_val: Optional[float] = None, notes: Optional[str] = None) -> ValueTypeSpec:
99
+ return ValueTypeSpec(kind=ValueKind.NUMERIC, min_val=min_val, max_val=max_val, notes=notes)
100
+
101
+ def _hash(notes: Optional[str] = None) -> ValueTypeSpec:
102
+ return ValueTypeSpec(kind=ValueKind.HASH, notes=notes)
103
+
104
+ def _str(notes: Optional[str] = None) -> ValueTypeSpec:
105
+ return ValueTypeSpec(kind=ValueKind.FREE_STRING, notes=notes)
106
+
107
+ def _bool(notes: Optional[str] = None) -> ValueTypeSpec:
108
+ return ValueTypeSpec(kind=ValueKind.BOOL, notes=notes)
109
+
110
+ def _array(of: ValueKind, notes: Optional[str] = None) -> ValueTypeSpec:
111
+ return ValueTypeSpec(kind=ValueKind.ARRAY, array_of=of, notes=notes)
112
+
113
+
114
+ # ─── The registry ───────────────────────────────────────────────────────────
115
+
116
+ PRIMITIVE_REGISTRY: dict[str, ValueTypeSpec] = {
117
+ # ── motor.* ────────────────────────────────────────────────────────────
118
+ # Motor primitives capture the physical mechanics of keyboard interaction —
119
+ # rhythm, precision, and habitual movements that are hard to fake and stable
120
+ # across sessions even when operators change tools or objectives.
121
+ "motor.keystroke_cadence": _cat(
122
+ "steady", "bursty", "hunt_and_peck", "machine",
123
+ notes="Rhythm of raw key input across the session. steady=metronomic rate "
124
+ "matching a confident typist. bursty=fast bursts separated by thinking "
125
+ "pauses. hunt_and_peck=search-first-then-type characteristic of unfamiliar "
126
+ "keyboard layout or low typing skill. machine=mechanically regular cadence "
127
+ "suggesting scripted or pasted input rather than live typing.",
128
+ ),
129
+ "motor.motor_stability": _cat(
130
+ "steady", "variable", "tremor",
131
+ notes="Consistency of individual key hold and flight times (dwell/flight). "
132
+ "steady=low variance, typical of a confident touch-typist. variable=high "
133
+ "variance, common under cognitive load or on an unfamiliar keyboard. "
134
+ "tremor=rhythmic instability distinct from cognitive-load variance — may "
135
+ "indicate physical condition or a non-human input device.",
136
+ ),
137
+ "motor.error_correction": _cat(
138
+ "immediate", "deferred", "absent", "route_around",
139
+ notes="How the operator corrects typing mistakes. immediate=backspace within ~1s "
140
+ "of the error (automatic self-monitoring, muscle memory). deferred=correction "
141
+ "after pausing to read output. absent=no correction — operator proceeds "
142
+ "despite errors, typical of scripts or operators who know the shell will "
143
+ "fail loudly. route_around=operator avoids retyping by using history recall "
144
+ "or rewriting the command differently.",
145
+ ),
146
+ "motor.command_chunking": _cat(
147
+ "fluent", "fragmented", "single_command",
148
+ notes="Whether commands are typed in a single continuous flow or as fragments. "
149
+ "fluent=typed in one pass from memory with no mid-command pauses. "
150
+ "fragmented=typed in chunks with mid-command pauses — operator is composing "
151
+ "while typing, common when adapting a remembered skeleton to the current "
152
+ "context. single_command=operator runs exactly one complete command at a "
153
+ "time and never constructs pipelines inline.",
154
+ ),
155
+ "motor.paste_burst_rate": _cat(
156
+ "none", "occasional", "habitual",
157
+ notes="Frequency of large clipboard-paste events relative to typed input. "
158
+ "Distinguishes an operator driving a terminal interactively from a script "
159
+ "feeding one. habitual=operator primarily works by pasting pre-prepared "
160
+ "command blocks; none=entirely typed.",
161
+ ),
162
+ "motor.input_modality": _cat(
163
+ "typed", "pasted", "mixed",
164
+ notes="Dominant input modality across the session — first-class promotion of "
165
+ "the paste-vs-type axis. typed=operator types commands character by "
166
+ "character. pasted=operator pastes pre-prepared blocks. mixed=substantial "
167
+ "use of both.",
168
+ ),
169
+ # motor.shell_mastery.*
170
+ "motor.shell_mastery.tab_completion": _cat(
171
+ "none", "occasional", "habitual",
172
+ notes="Tab key completion usage across the session. habitual=operator relies on "
173
+ "it constantly (inferred from the latency pattern: short pause then rapid "
174
+ "continuation after a partial path or command). none=operator types full "
175
+ "paths and commands without completion. Strong indicator of shell familiarity.",
176
+ ),
177
+ "motor.shell_mastery.shortcut_usage": _cat(
178
+ "none", "moderate", "heavy",
179
+ notes="Use of shell keyboard shortcuts (Ctrl+R for history search, Ctrl+A/E for "
180
+ "line navigation, Ctrl+L for clear, Alt+. for last argument, etc.). Heavy "
181
+ "usage indicates deep shell muscle memory, reliably stable across sessions.",
182
+ ),
183
+ "motor.shell_mastery.pipe_chaining_depth": _cat(
184
+ "shallow", "moderate", "deep",
185
+ notes="Maximum depth of pipeline chains observed (cmd | cmd | cmd...). shallow=0-1 "
186
+ "pipes, moderate=2-3, deep=4+. Reflects preference for composing Unix tools "
187
+ "rather than running one-off commands. Correlates with cognitive.tool_vocabulary.",
188
+ ),
189
+
190
+ # ── cognitive.* ────────────────────────────────────────────────────────
191
+ # Cognitive primitives capture how the operator thinks and makes decisions —
192
+ # their planning style, how they respond to uncertainty, and signs that they
193
+ # are human vs. automated.
194
+ "cognitive.cognitive_load": _cat(
195
+ "low", "medium", "high",
196
+ notes="Inferred mental workload derived from timing patterns, error rate, and "
197
+ "inter-command variance. high=long pauses before and after commands, "
198
+ "frequent error-retry cycles, fragmented command chunking. Collapses "
199
+ "multiple temporal and motor signals into a holistic load estimate. "
200
+ "Useful as a composite feature for downstream attribution rather than "
201
+ "a standalone signal.",
202
+ ),
203
+ "cognitive.exploration_style": _cat(
204
+ "methodical", "chaotic", "targeted",
205
+ notes="How the operator navigates an unfamiliar environment. methodical=systematic "
206
+ "enumeration (ls→cat→id→uname in a logical sequence). chaotic=non-sequential "
207
+ "jumps between unrelated commands with no visible thread. targeted=operator "
208
+ "knows exactly what they want and goes straight for it without exploring.",
209
+ ),
210
+ "cognitive.planning_depth": _cat(
211
+ "deep", "shallow", "reactive",
212
+ notes="Whether the operator works from a pre-formed plan. deep=commands follow a "
213
+ "visible logical sequence (recon→pivot→exfil) with little backtracking. "
214
+ "shallow=opportunistic — follows each output where it leads. reactive=operator "
215
+ "responds only to errors or surprises rather than driving toward an objective.",
216
+ ),
217
+ "cognitive.tool_vocabulary": _cat(
218
+ "narrow", "moderate", "broad",
219
+ notes="Breadth of distinct tools and commands used across the session. narrow=operator "
220
+ "relies on a small fixed toolset (e.g. only curl, grep, ls). broad=operator "
221
+ "reaches for the best tool for each subtask, suggesting deep familiarity with "
222
+ "the Unix ecosystem or the target environment.",
223
+ ),
224
+ "cognitive.inter_command_latency_class": _cat(
225
+ "instant", "typing_speed", "deliberate",
226
+ "llm_lightweight", "llm_heavyweight", "long",
227
+ notes="llm_lightweight = 2-8s (orchestrated agents w/ small models or terse "
228
+ "prompts); llm_heavyweight = 8-30s (reasoning-class agents in tool "
229
+ "loops with text generation between calls); long = >30s (likely "
230
+ "human-supervised LLM workflow). The two LLM bands are the v0.2 "
231
+ "split of the original llm_roundtrip 2-8s band, which conflated "
232
+ "lightweight and reasoning-class operators.",
233
+ ),
234
+ "cognitive.inter_command_consistency": _cat(
235
+ "metronomic", "variable", "bimodal",
236
+ notes="Dispersion (CV) of inter-command pauses; metronomic = LLM-pure, "
237
+ "variable = human, bimodal = LLM-assisted human (LLM-paced bursts + "
238
+ "human-thinking gaps). v0.1 uses CV thresholds; true bimodal "
239
+ "detection (Hartigan dip / two-peak detection) is v0.2.",
240
+ ),
241
+ "cognitive.command_branch_diversity": _cat(
242
+ "linear_playbook", "adaptive_branching", "unknown",
243
+ notes="Content-based (not timing-based) discriminator between scripted "
244
+ "playbook execution and adaptive branching. Computed from the "
245
+ "set of first-token binaries in the session: low repetition "
246
+ "(unique/total ratio near 1) = linear_playbook (each step a "
247
+ "different canonical recon command). High repetition (multiple "
248
+ "invocations of the same tool with different args) = adaptive_"
249
+ "branching (operator iterating on a tool to follow up on a "
250
+ "finding). Empirically (CLAUDE-FF vs CLAUDE-CL on 2026-05-02): "
251
+ "fire-and-forget runs 10 distinct tools, closed-loop runs 5-6 "
252
+ "tools with curl repeated as the operator chases a thread.",
253
+ ),
254
+ "cognitive.feedback_loop_engagement": _cat(
255
+ "closed_loop", "fire_and_forget", "unknown",
256
+ notes="Whether the operator's pace correlates with the volume of output "
257
+ "they observed before issuing the next command. closed_loop = "
258
+ "positive Pearson r between preceding output bytes and subsequent "
259
+ "pause (pause grows with output to read/ingest). fire_and_forget = "
260
+ "no correlation (operator paces independently of output, e.g. "
261
+ "scripted recon, prerecorded playbook). unknown = insufficient "
262
+ "samples to compute. CUTS ACROSS the LLM/human axis: humans reading "
263
+ "real output are closed_loop, scripted humans and fire-and-forget "
264
+ "LLM agents are fire_and_forget, closed-loop LLM agents (true plan-"
265
+ "execute-observe) are closed_loop. Replaces the v0.1 "
266
+ "output_pause_correlation primitive — same underlying measurement, "
267
+ "more honest framing.",
268
+ ),
269
+ # cognitive.error_resilience.*
270
+ "cognitive.error_resilience.retry_tactic": _cat(
271
+ "rerun", "modify", "switch", "abort",
272
+ notes="What the operator does when a command fails. rerun=identical retry with "
273
+ "no changes (hoping transient error clears). modify=adjusts the command "
274
+ "before retrying (flags, paths, arguments). switch=abandons the tool and "
275
+ "tries a different one for the same goal. abort=gives up on that objective "
276
+ "and moves on.",
277
+ ),
278
+ "cognitive.error_resilience.frustration_typing": _cat(
279
+ "low", "moderate", "high",
280
+ notes="Elevated typing speed or error rate immediately after a command failure, "
281
+ "indicating an emotional response to the setback. high=sharp speed spike "
282
+ "and error burst post-failure. A behavioral tell that separates emotionally "
283
+ "reactive humans from scripted operators or composed professionals.",
284
+ ),
285
+ "cognitive.error_resilience.fallback_to_man": _cat(
286
+ "absent", "present",
287
+ notes="Whether the operator invokes man, --help, or -h when stuck. present is a "
288
+ "tell for unfamiliarity with the specific tool in use — an operator who "
289
+ "knows their tools cold rarely needs to. Absent in scripted runs.",
290
+ ),
291
+
292
+ # ── temporal.* ─────────────────────────────────────────────────────────
293
+ # Temporal primitives characterize WHEN and HOW LONG an operator works.
294
+ # Stable across sessions; hard to fake consistently over a campaign.
295
+ "temporal.session_timing": _cat(
296
+ "diurnal", "nocturnal", "irregular",
297
+ notes="Hour-of-day distribution of the operator's activity. diurnal=activity "
298
+ "peaks align with local business hours (09:00-18:00). nocturnal=peaks in "
299
+ "local night hours (22:00-06:00). irregular=no discernible daily pattern. "
300
+ "The local timezone must be established separately (see cultural.*) to "
301
+ "interpret diurnal/nocturnal meaningfully.",
302
+ ),
303
+ "temporal.session_duration": _cat(
304
+ "short", "medium", "long", "marathon",
305
+ notes="Typical duration of a single continuous session. short=<15min, "
306
+ "medium=15-90min, long=90min-4hr, marathon=>4hr. Stable individual "
307
+ "characteristic — some operators always work in short sprints, others "
308
+ "in long unbroken stretches.",
309
+ ),
310
+ "temporal.escalation_pattern": _cat(
311
+ "sustained", "erratic", "bursty",
312
+ notes="How activity intensity changes across a session. sustained=constant "
313
+ "command rate throughout. erratic=unpredictable spikes and lulls. "
314
+ "bursty=concentrated activity followed by extended quiet — common when "
315
+ "an operator waits for a long-running process before continuing.",
316
+ ),
317
+ "temporal.persistence": _cat(
318
+ "hit_and_run", "return_visitor", "resident",
319
+ notes="Cross-session return behavior. hit_and_run=one or very few sessions then "
320
+ "disappears. return_visitor=returns periodically (e.g. weekly maintenance). "
321
+ "resident=near-continuous presence, behaves as if the compromised host is "
322
+ "a persistent workstation.",
323
+ ),
324
+ # temporal.lifecycle_markers.*
325
+ "temporal.lifecycle_markers.landing_ritual": _cat(
326
+ "present", "absent",
327
+ notes="Whether the operator runs a recognizable sequence of commands at session "
328
+ "start (e.g. whoami → id → uname -a → hostname → ip addr). present=a "
329
+ "fingerprinted landing ritual is detected, suggesting established habit or "
330
+ "a pre-written checklist. absent=operator jumps straight to objective work.",
331
+ ),
332
+ "temporal.lifecycle_markers.exit_behavior": _cat(
333
+ "graceful", "abrupt", "cleanup",
334
+ notes="How the session ends. graceful=explicit logout or exit command. "
335
+ "abrupt=connection drops without cleanup (killed, network failure, or "
336
+ "scripted timeout). cleanup=operator deletes logs, tools, or temp files "
337
+ "before exiting — the strongest opsec signal in this category.",
338
+ ),
339
+ "temporal.lifecycle_markers.idle_periodicity": _cat(
340
+ "random", "periodic",
341
+ notes="Whether intra-session pauses (idle gaps >30s) occur at statistically "
342
+ "regular intervals or at random. periodic=heartbeat-like idle pattern — "
343
+ "may indicate an LLM polling loop, an automated keepalive, or a human "
344
+ "following a timed workflow. random=human thinking pauses with no "
345
+ "detectable rhythm.",
346
+ ),
347
+
348
+ # ── operational.* ──────────────────────────────────────────────────────
349
+ # Operational primitives describe WHAT the operator is trying to do and HOW
350
+ # carefully they're hiding it. These are coarser inferences from command patterns
351
+ # rather than direct measurements.
352
+ "operational.opsec_discipline": _cat(
353
+ "careful", "careless", "learning",
354
+ notes="How carefully the operator minimizes their forensic footprint. "
355
+ "careful=history disabled (HISTFILE=/dev/null), tools removed after use, "
356
+ "proxy/VPN confirmed, log entries tampered. careless=no precautions — "
357
+ "history on, tools left in /tmp, no timestamp cover. learning=inconsistent "
358
+ "and improving across sessions, characteristic of an operator developing "
359
+ "their craft mid-campaign.",
360
+ ),
361
+ "operational.cleanup_behavior": _cat(
362
+ "thorough", "partial", "none",
363
+ notes="What the operator does with artifacts (uploaded tools, compiled binaries, "
364
+ "temp files) at session end. thorough=removes everything explicitly, "
365
+ "including bash history. partial=removes some artifacts but misses others "
366
+ "(common). none=leaves all artifacts — operator either trusts the implant "
367
+ "to cover or does not expect forensic review.",
368
+ ),
369
+ "operational.objective": _cat(
370
+ "recon", "exfil", "persistence", "lateral", "destructive",
371
+ notes="Inferred mission objective from command-pattern analysis. recon=enumeration "
372
+ "and data collection without exfiltration. exfil=active data transfer out "
373
+ "of scope. persistence=installing mechanisms to survive reboot or session "
374
+ "end (cron, systemd, ssh key). lateral=pivoting to adjacent hosts. "
375
+ "destructive=wipe, encrypt, or sabotage commands.",
376
+ ),
377
+ "operational.multi_actor_indicators": _cat(
378
+ "solo", "handoff_detected", "team_coordinated",
379
+ notes="Whether the session shows signs of more than one person operating. "
380
+ "handoff_detected=a detectable style break mid-session (motor cadence, "
381
+ "vocabulary, or latency class changes sharply at a point in time). "
382
+ "team_coordinated=multiple style signatures interleaved or simultaneous "
383
+ "activity from the same account across sessions.",
384
+ ),
385
+
386
+ # ── environmental.* ────────────────────────────────────────────────────
387
+ # Environmental primitives describe the physical and software context the
388
+ # operator works from. Stable per-campaign; often reveals national origin
389
+ # or infrastructure choices.
390
+ "environmental.keyboard_layout": _cat(
391
+ "qwerty", "azerty", "qwertz", "other",
392
+ notes="Inferred keyboard layout from characteristic key-sequence errors. An "
393
+ "AZERTY-trained typist on a QWERTY keyboard makes specific substitutions "
394
+ "(q↔a, z↔w, m→,) that are statistically distinguishable from random "
395
+ "errors. Reliable when error volume is sufficient (typically >50 errors "
396
+ "in the session).",
397
+ ),
398
+ "environmental.locale": _str(
399
+ notes="BCP-47 tag (e.g. 'en-US', 'pt-BR'); free string by deliberate choice — "
400
+ "locale is not a closed enum. Inferred from keyboard layout, cultural "
401
+ "timing patterns, and command-line character encoding artifacts.",
402
+ ),
403
+ "environmental.numpad_usage": _cat(
404
+ "detected", "not_detected",
405
+ notes="Whether the operator uses a numeric keypad for digit entry, inferred from "
406
+ "keycode patterns. detected signals a desktop keyboard rather than a laptop, "
407
+ "which narrows the physical environment.",
408
+ ),
409
+ "environmental.terminal_multiplexer": _cat(
410
+ "none", "tmux", "screen",
411
+ notes="Presence of tmux or screen, inferred from keybinding escape sequences "
412
+ "(Ctrl+B or Ctrl+A prefixes) and window-switching patterns. Multiplexer use "
413
+ "suggests a persistent, organized working style.",
414
+ ),
415
+ "environmental.shell_type": _cat(
416
+ "bash", "zsh", "fish", "cmd.exe", "powershell",
417
+ notes="Shell environment, inferred from syntax patterns (array syntax, string "
418
+ "quoting style, builtin names). powershell and cmd.exe immediately flag a "
419
+ "Windows-native operator, which constraints the likely toolchain.",
420
+ ),
421
+
422
+ # ── cultural.* ─────────────────────────────────────────────────────────
423
+ # Cultural primitives exploit the fact that human work patterns are shaped by
424
+ # local time, religion, and social convention. These signals are hard to sustain
425
+ # as deception across a long campaign.
426
+ "cultural.meal_break_gaps": _cat(
427
+ "none_detected", "morning", "midday", "evening", "late_night",
428
+ notes="Whether activity gaps align with regional meal times. morning=09:00-10:00 "
429
+ "local, midday=12:00-14:00, evening=19:00-21:00, late_night=00:00-02:00. "
430
+ "Absent if the operator works through typical meal windows. Requires "
431
+ "environmental.locale or a known timezone to interpret.",
432
+ ),
433
+ "cultural.periodic_micro_pauses": _cat(
434
+ "none_detected", "regular_intervals_detected",
435
+ notes="Short, rhythmic pauses of 5-15 minutes recurring at consistent intervals "
436
+ "within a session. May correspond to prayer times (Salah — 5 daily, "
437
+ "spaced ~2-3hr in active hours), smoke breaks, or other cultural micro-"
438
+ "rituals. regular_intervals_detected means the null hypothesis of random "
439
+ "pauses is rejected at p<0.05.",
440
+ ),
441
+ "cultural.dst_behavior": _cat(
442
+ "shifts_with_dst", "anchored_to_utc", "unknown",
443
+ notes="Whether the operator's active-hours window shifts by 1 hour at daylight "
444
+ "saving transitions. shifts_with_dst=schedule follows local civil time "
445
+ "(the operator lives there). anchored_to_utc=schedule is clock-fixed, "
446
+ "suggesting automated infrastructure or an operator who deliberately anchors "
447
+ "to UTC to defeat this analysis.",
448
+ ),
449
+ "cultural.weekend_cadence": _cat(
450
+ "fri_sat", "sat_sun", "no_weekend", "irregular",
451
+ notes="Which two-day block the operator treats as a weekend (low-activity days). "
452
+ "fri_sat=Middle Eastern / Israeli weekend pattern. sat_sun=Western / "
453
+ "East Asian pattern. no_weekend=operator works 7 days at uniform intensity. "
454
+ "A reliable national-origin signal when observed across multiple weeks.",
455
+ ),
456
+ "cultural.holiday_gaps": _cat(
457
+ "none_detected", "specific_dates_detected",
458
+ notes="Whether unexplained multi-day inactivity gaps align with known public "
459
+ "holiday calendars. specific_dates_detected triggers when a gap of >=2 days "
460
+ "falls within ±1 day of a public holiday in at least one candidate locale. "
461
+ "Requires a multi-session corpus spanning calendar events.",
462
+ ),
463
+
464
+ # ── emotional_valence.* ────────────────────────────────────────────────
465
+ # Emotional valence primitives infer affective state from TYPING DYNAMICS —
466
+ # pace, error rate, and aggression in key input. They do NOT read message
467
+ # content; BEHAVE-SHELL is content-blind.
468
+ "emotional_valence.valence": _cat(
469
+ "positive", "neutral", "negative",
470
+ notes="Overall affective tone inferred from typing dynamics across the session. "
471
+ "Positive=fluent, low-error, engaged pace. Negative=error-heavy, erratic, "
472
+ "showing markers of frustration or stress. This is a coarse aggregate; "
473
+ "see arousal and stress_response for finer-grained breakdown.",
474
+ ),
475
+ "emotional_valence.arousal": _cat(
476
+ "low_calm", "medium_engaged", "high_agitated",
477
+ notes="How energized or activated the operator appears. low_calm=slow, deliberate "
478
+ "pace with long inter-command gaps. high_agitated=fast, error-prone bursts "
479
+ "with short pauses. This dimension is orthogonal to valence: a calm "
480
+ "professional and a calm automated script are both low_calm.",
481
+ ),
482
+ "emotional_valence.stress_response": _cat(
483
+ "none", "eustress_positive", "distress_negative",
484
+ notes="Whether detected high arousal reflects positive challenge or negative overload. "
485
+ "eustress_positive=speed-up with low error rate (operator in the zone, engaged "
486
+ "problem-solving). distress_negative=speed-up accompanied by rising error rate "
487
+ "and frustration-venting markers (overloaded, panicking). none=arousal is "
488
+ "insufficient to classify.",
489
+ ),
490
+ "emotional_valence.frustration_venting": _cat(
491
+ "none", "detected",
492
+ notes="Detectable outburst signal: a sudden spike in typing speed or rapid-fire "
493
+ "backspace/delete keys immediately following a string of command failures. "
494
+ "Distinct from sustained high arousal — this is a transient, failure-triggered "
495
+ "event. Absent in scripted runs; strong human indicator.",
496
+ ),
497
+
498
+ # ── toolchain.tls.* ────────────────────────────────────────────────────
499
+ # TLS fingerprints identify the client and server stacks by their handshake
500
+ # parameters. Each tool, library, and OS tends to produce a recognizable
501
+ # fingerprint even when the payload is encrypted.
502
+ "toolchain.tls.ja3_client": _hash(
503
+ notes="MD5 hash of TLS ClientHello parameters: SSLVersion, Ciphers, Extensions, "
504
+ "EllipticCurves, EllipticCurvePointFormats (Salesforce, 2017). Fingerprints "
505
+ "the client TLS stack — curl, OpenSSL, Metasploit, Cobalt Strike, and most "
506
+ "offensive tools each produce a distinct hash. Searchable against public "
507
+ "databases (e.g. ja3er.com).",
508
+ ),
509
+ "toolchain.tls.ja3s_server": _hash(
510
+ notes="MD5 hash of TLS ServerHello parameters: SSLVersion, Cipher, Extensions. "
511
+ "Fingerprints the server TLS stack. Useful for identifying C2 servers by "
512
+ "their TLS response even when IP addresses rotate — the server library "
513
+ "version (e.g. OpenSSL vs. WolfSSL) is often stable.",
514
+ ),
515
+ "toolchain.tls.ja4_client": _hash(
516
+ notes="JA4 fingerprint (FoxIO, 2023): replaces JA3 with a sortable, "
517
+ "human-readable format (e.g. t13d1516h2_8daaf6152771_e5627efa2ab1) that "
518
+ "is more robust to TLS extension order randomization. Encodes TLS version, "
519
+ "cipher count, extension count, ALPN, cipher hash, and extension hash in "
520
+ "three underscore-separated fields.",
521
+ ),
522
+ "toolchain.tls.ja4s_server": _hash(
523
+ notes="JA4 server-side fingerprint: encodes the chosen cipher, extension list, "
524
+ "and ALPN from the ServerHello. More stable than JA3S when the server "
525
+ "randomizes cipher ordering — JA4S hashes the sorted cipher list. "
526
+ ),
527
+ "toolchain.tls.jarm_server": _hash(
528
+ notes="62-char JARM hash (Salesforce, 2020). Actively probes the server by "
529
+ "sending 10 specially crafted TLS ClientHellos and hashing the ServerHello "
530
+ "responses. Fingerprints the server TLS stack at a deeper level than JA3S — "
531
+ "detects Cobalt Strike, Metasploit, and major C2 frameworks reliably even "
532
+ "when they use custom certificates.",
533
+ ),
534
+ "toolchain.tls.tls_cert_simhash": _hash(
535
+ notes="SHA-256 hex of the leaf certificate's DER-encoded bytes. Tracks the "
536
+ "specific certificate in use, not just the stack. Useful for correlating "
537
+ "C2 infrastructure that reuses self-signed certs across campaigns.",
538
+ ),
539
+
540
+ # ── toolchain.transport.* ──────────────────────────────────────────────
541
+ "toolchain.transport.tcp_stack": _str(
542
+ notes="p0f label for the TCP/IP stack (e.g. 'Linux 5.x', 'Windows 10'). Inferred "
543
+ "from TCP header field quirks (TTL, window size, options order, DF bit). "
544
+ "Reveals the OS of the connecting host even before any application-layer "
545
+ "protocol is seen.",
546
+ ),
547
+ "toolchain.transport.h2_akamai_fingerprint": _str(
548
+ notes="HTTP/2 SETTINGS frame + priority frame + pseudo-header order hash. "
549
+ "Different HTTP/2 client libraries produce distinct SETTINGS and priority "
550
+ "combinations (curl vs. Python requests vs. Go net/http). "
551
+ "status: planned",
552
+ ),
553
+ "toolchain.transport.quic_client": _str(
554
+ notes="QUIC initial packet fingerprint derived from transport parameters and "
555
+ "connection ID length patterns. Fingerprints the QUIC library in use. "
556
+ "status: planned",
557
+ ),
558
+
559
+ # ── toolchain.ssh.* ────────────────────────────────────────────────────
560
+ "toolchain.ssh.hassh_client": _hash(
561
+ notes="MD5 hash of SSH client KEX parameters: kex_algorithms, encryption_algorithms, "
562
+ "mac_algorithms, compression_algorithms (Salesforce, 2018). Each SSH client "
563
+ "library (OpenSSH, PuTTY, libssh, Paramiko, Impacket) produces a distinct "
564
+ "HASSH. Stable across versions within a major release.",
565
+ ),
566
+ "toolchain.ssh.hassh_server": _hash(
567
+ notes="MD5 hash of SSH server KEX parameters (same field set as HASSH client). "
568
+ "Fingerprints the SSH daemon — useful for identifying honeypots, implants, "
569
+ "or non-standard SSH servers. status: partial",
570
+ ),
571
+ "toolchain.ssh.ssh_client_banner": _str(
572
+ notes="RFC 4253 protocol version string sent by the SSH client (e.g. "
573
+ "'SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6'). Often unmodified even in "
574
+ "offensive tooling, providing an easy first-pass fingerprint.",
575
+ ),
576
+ "toolchain.ssh.kex_algorithm_order": _array(
577
+ ValueKind.FREE_STRING,
578
+ notes="Ordered list of key-exchange algorithms offered in the SSH ClientHello "
579
+ "(e.g. ['curve25519-sha256', 'ecdh-sha2-nistp256', 'diffie-hellman-group14-sha256']). "
580
+ "Different clients (OpenSSH, PuTTY, Paramiko, Impacket's smbexec) advertise "
581
+ "distinct KEX orderings, providing a secondary fingerprint beyond HASSH. "
582
+ ),
583
+
584
+ # ── toolchain.http.* ───────────────────────────────────────────────────
585
+ "toolchain.http.user_agent_tool_class": _cat(
586
+ "nmap_nse", "sqlmap", "nuclei", "masscan", "curl", "metasploit",
587
+ "ffuf", "gobuster", "feroxbuster", "nikto", "wpscan", "evilwinrm",
588
+ "impacket", "unknown",
589
+ notes="Tool classification from User-Agent string and HTTP behavior fingerprint. "
590
+ "Known offensive tools typically use default User-Agent strings or omit the "
591
+ "header entirely, making them trivially classifiable. unknown=no match in "
592
+ "the known-tool list.",
593
+ ),
594
+ "toolchain.http.header_order_fingerprint": _str(
595
+ notes="Hash of the HTTP request header name order. Different HTTP client libraries "
596
+ "emit headers in distinct sequences (Host first vs. last, Accept-Encoding "
597
+ "presence, etc.). Fingerprints the underlying HTTP library independently of "
598
+ "the User-Agent. status: planned",
599
+ ),
600
+ "toolchain.http.body_oddities": _array(
601
+ ValueKind.FREE_STRING,
602
+ notes="List of anomalous body characteristics (e.g. 'multipart_boundary_static', "
603
+ "'json_key_order_fixed', 'soap_envelope_namespace_style'). Captures "
604
+ "tool-specific body serialization tics. status: planned",
605
+ ),
606
+
607
+ # ── toolchain.c2.* ─────────────────────────────────────────────────────
608
+ # C2 (Command and Control) primitives characterize the beaconing and callback
609
+ # behavior of implants. Even encrypted C2 traffic leaves timing and structural
610
+ # fingerprints.
611
+ "toolchain.c2.beacon_family": _cat(
612
+ "cobalt_strike", "sliver", "havoc", "mythic",
613
+ "merlin", "brc4", "nighthawk", "unknown",
614
+ notes="C2 framework identified from beacon timing, traffic shape, and protocol "
615
+ "fingerprints. cobalt_strike, sliver, havoc, mythic=well-characterized "
616
+ "open-source or widely-used commercial frameworks. merlin, brc4, "
617
+ "nighthawk=status: planned (less common; less training data).",
618
+ ),
619
+ "toolchain.c2.beacon_interval_ms": _num(
620
+ min_val=0,
621
+ notes="Median inter-arrival time (IAT) between beacon callbacks, in milliseconds. "
622
+ "Cobalt Strike default is 60000ms (60s). Operators often lower this for "
623
+ "interactivity. Very short intervals (<1000ms) suggest an interactive shell "
624
+ "rather than a true beacon.",
625
+ ),
626
+ "toolchain.c2.beacon_jitter_cv": _num(
627
+ min_val=0,
628
+ notes="Coefficient of variation (std/mean) of beacon IATs. Higher CV means more "
629
+ "randomized jitter — a deliberate evasion technique to defeat fixed-interval "
630
+ "detection. Cobalt Strike's default jitter is 0% (CV≈0); operators who "
631
+ "understand detection set it to 20-50%.",
632
+ ),
633
+ "toolchain.c2.sleep_skew": _cat(
634
+ "none", "gaussian", "uniform", "walk",
635
+ notes="Type of jitter applied to beacon sleep intervals. none=fixed interval "
636
+ "(detectable by timing analysis). gaussian=normally-distributed jitter "
637
+ "(common in Cobalt Strike with jitter set). uniform=flat random range. "
638
+ "walk=random-walk drift (each sleep shifts from the previous). "
639
+ "status: partial",
640
+ ),
641
+ "toolchain.c2.c2_callback_endpoint": _str(
642
+ notes="URL or host:port of the C2 callback endpoint observed in traffic. "
643
+ "Plain string — do not store post-decryption content here.",
644
+ ),
645
+ "toolchain.c2.attack_software_id": _str(
646
+ notes="MITRE ATT&CK Software ID (e.g. 'S0154' for Cobalt Strike). Provides a "
647
+ "stable cross-reference to the MITRE knowledge base for attribution reporting.",
648
+ ),
649
+
650
+ # ── toolchain.protocol_abuse.* ─────────────────────────────────────────
651
+ # Protocol abuse primitives capture non-standard or offensive use of standard
652
+ # protocols — DNS tunneling, SMB negotiation quirks, Kerberos downgrade attempts,
653
+ # and LLMNR/NBNS poisoning tools.
654
+ "toolchain.protocol_abuse.dns_exfil_tool": _cat(
655
+ "iodine", "dnscat2", "custom_high_entropy", "none",
656
+ notes="DNS tunneling tool identified from query patterns. iodine=base32-encoded "
657
+ "data in subdomains with TYPE NULL queries. dnscat2=TYPE TXT queries with "
658
+ "specific length/entropy patterns. custom_high_entropy=high-entropy "
659
+ "subdomains consistent with tunneling but not matching a known tool signature. "
660
+ "status: planned",
661
+ ),
662
+ "toolchain.protocol_abuse.smb_dialect": _cat(
663
+ "SMB1", "SMB2.0.2", "SMB2.1", "SMB3.0", "SMB3.0.2", "SMB3.1.1",
664
+ notes="SMB protocol dialect negotiated by the client. SMB1 use in 2024+ is a "
665
+ "strong indicator of legacy tooling or deliberate downgrade (EternalBlue-era "
666
+ "exploits require SMB1). SMB3.1.1 with pre-auth integrity check is the "
667
+ "modern hardened default. status: planned",
668
+ ),
669
+ "toolchain.protocol_abuse.kerberos_etype_offer": _hash(
670
+ notes="Hash of the set of encryption types offered in the Kerberos AS-REQ etype "
671
+ "list. Clients that offer RC4-HMAC (etype 23) alongside modern etypes are "
672
+ "candidates for AS-REP roasting or Kerberoasting tooling (Rubeus, Impacket "
673
+ "GetUserSPNs). The hash captures the exact etype combination without "
674
+ "storing the cleartext list.",
675
+ ),
676
+ "toolchain.protocol_abuse.ldap_bind_pattern": _cat(
677
+ "simple", "sasl_gssapi", "ntlm", "ntlmssp_v1", "responder_like",
678
+ notes="LDAP bind mechanism used by the client. simple=cleartext credentials "
679
+ "(dangerous, immediately suspicious in modern environments). "
680
+ "sasl_gssapi=Kerberos-backed GSSAPI (normal). ntlm=NTLM challenge-response. "
681
+ "ntlmssp_v1=downgraded NTLMv1 (Responder target). responder_like=sequence "
682
+ "of binds matching Responder or similar MITM tools. status: partial",
683
+ ),
684
+ "toolchain.protocol_abuse.responder_signature": _str(
685
+ notes="Boolean + variant string indicating whether Responder (or a compatible tool) "
686
+ "was detected. Convention: 'false' if absent; 'true:llmnr', 'true:nbtns', "
687
+ "'true:mdns' for the poisoning protocol detected. Responder poisons LLMNR, "
688
+ "NBNS, and mDNS broadcasts to capture Net-NTLMv2 hashes. status: planned",
689
+ ),
690
+ "toolchain.protocol_abuse.mitm6_signature": _bool(
691
+ notes="Whether mitm6 (Fox-IT tool) activity is detected. mitm6 abuses IPv6 router "
692
+ "advertisement messages on predominantly IPv4 networks to force Windows hosts "
693
+ "to use an attacker-controlled DNS server, enabling credential relay attacks. "
694
+ "status: planned",
695
+ ),
696
+
697
+ # ── toolchain.payload.* ────────────────────────────────────────────────
698
+ "toolchain.payload.payload_simhash": _hash(
699
+ notes="64-bit SimHash of the observed payload binary or shellcode. SimHash "
700
+ "preserves near-duplicate relationships: two payloads that are 90% similar "
701
+ "will have low Hamming distance (<4 bits difference on a 64-bit hash), "
702
+ "enabling family clustering even when the operator applies minor obfuscation. "
703
+ "Stored as a 16-char hex string.",
704
+ ),
705
+ "toolchain.payload.payload_entropy_class": _cat(
706
+ "low", "medium", "high", "packed",
707
+ notes="Shannon entropy class of the payload bytes. packed=entropy >7.2 bits/byte, "
708
+ "characteristic of UPX or custom packing, encrypted shellcode, or base64-"
709
+ "compressed payloads. high=6.5-7.2, typical of unencrypted compiled code. "
710
+ "low=<5.5, typical of scripts or plaintext. status: planned",
711
+ ),
712
+ "toolchain.payload.loader_family": _cat(
713
+ "donut", "sgn", "pe2sh", "nimcrypt", "unknown",
714
+ notes="Shellcode/loader family identified from structural signatures. donut=Donut "
715
+ "framework (TheWover) — converts .NET assemblies and PE files to position-"
716
+ "independent shellcode with a recognizable header. sgn=Shikata-Ga-Nai encoder "
717
+ "(Metasploit) — polymorphic XOR encoder with a distinct feedback register "
718
+ "pattern. pe2sh=PE-to-shellcode conversion. nimcrypt=Nim-based loader with "
719
+ "AES-encrypted payload. status: planned",
720
+ ),
721
+ }
722
+
723
+
724
+ def is_known(primitive: str) -> bool:
725
+ return primitive in PRIMITIVE_REGISTRY
726
+
727
+
728
+ def get(primitive: str) -> ValueTypeSpec:
729
+ """Return the value-type spec for *primitive*; raise KeyError if unknown."""
730
+ return PRIMITIVE_REGISTRY[primitive]
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: behave-shell
3
+ Version: 0.1.0
4
+ Summary: BEHAVE-SHELL — shell-session behavioral observation registry, layered on behave-core
5
+ Author: ANTI
6
+ License: GPL-3.0-or-later
7
+ Project-URL: Source, https://git.resacachile.cl/anti/BEHAVE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: pydantic>=2.6
10
+ Requires-Dist: behave-core>=0.1.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8; extra == "dev"
13
+ Requires-Dist: pytest-cov; extra == "dev"
14
+ Requires-Dist: ruff; extra == "dev"
@@ -0,0 +1,9 @@
1
+ behave_shell/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ behave_shell/spec/__init__.py,sha256=dVUWWszZZUN-RxL6FPOcTEjNJqerBzdl0eIyek-ecLM,1043
3
+ behave_shell/spec/envelope.py,sha256=3d5uIHSH_dYYAQRiZy7e7JwKyF9j7OTbXpwYtOH0MvU,1919
4
+ behave_shell/spec/event_adapter.py,sha256=oZT8K1x2NHCwtGJleT2AEptZIf0MltKEOdSaEVoNQxU,2287
5
+ behave_shell/spec/primitives.py,sha256=lrB_VOJOB9_VExbwBfwTuaoq8YhWboV-l78PVT1BnS8,44578
6
+ behave_shell-0.1.0.dist-info/METADATA,sha256=YaYpvfLoCx-JwZ1t_3ld6-6mMckNEFPv-aHVJP-BXqM,477
7
+ behave_shell-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ behave_shell-0.1.0.dist-info/top_level.txt,sha256=HdZe9TxoTMMUcKGfbXPZlK-OsG0FFhx-XaYJWXsxMCc,13
9
+ behave_shell-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ behave_shell