Red Team Activity Taxonomy¶
This document is the source of truth for the OCSF activity taxonomy emitted
by the Kali MCP server's red-team structured logging path. The TypeScript
classifier (mcp/mcp-red/src/classifier.ts) mirrors the table below—when
the table changes, the classifier and its tests change with it.
OCSF-aligned vocabulary follows the same conventions as the Python detection
and attack models in src/aptl/core/detection.py and
src/aptl/core/attacks.py, in particular the severity_id enum (0–6) and
the OCSF event class IDs.
The boundary that wires this taxonomy onto live MCP command execution is captured in ADR-027—Red Team Structured Logging Boundary.
Required base fields¶
Every OCSF red-team record produced by logRedTeamCommand
(mcp/mcp-red/src/logger.ts) carries these fields, regardless of which
activity row matched:
| Field | Type | Source |
|---|---|---|
time |
number (epoch ms) | Date.now() at emit time—OCSF timestamp_t is milliseconds since the Unix epoch |
severity_id |
0–6 | classifier default; bumped to MEDIUM on success: false |
category_uid |
number | OCSF event category (1 System, 3 IAM, 4 Network, 5 Discovery, 6 Application)—required by OCSF Base Event |
category_name |
string | OCSF category name |
class_uid |
number | classifier match |
class_name |
string | classifier match (OCSF class name) |
activity_id |
number | classifier match |
type_uid |
number | class_uid * 100 + activity_id |
metadata.product.name |
aptl-mcp-red |
constant |
metadata.product.vendor_name |
APTL |
constant |
process.cmd_line |
string (redacted) | input command run through the shared redact() helper from aptl-mcp-common plus a tool-agnostic short--p pre-mask for non-numeric values |
aptl.activity_type |
string | classifier match |
aptl.tool |
string | leading executable token (basename); set even for the generic fallback so SIEM consumers can discriminate curl, python, bash, etc. |
aptl.tool_name |
string | MCP tool name (for example kali_run_command) |
aptl.agent_name |
string | aptl-kali-red |
aptl.exit_code |
number (optional) | command exit code, when known |
aptl.signal |
string (optional) | terminating signal name when the command was killed |
Optional fields populated by the extractor or by the caller's context:
attacks[]: populated when the classification carries a MITRE technique ({ technique: { uid: 'T1046' }, tactic: { name: 'Discovery' } }).dst_endpoint:{ ip?, hostname?, port?, ports?, port_range?, cidr? }from the extractor.port_rangecarries the original spec string when a range expansion would exceed the cap (1024 ports).actor.user.name:target_userfrom the extractor (for examplessh user@host,hydra -l user). Only populated for tool families where the flag really means a target user (SSH-style, credential brute-force, host discovery); curl--user user:passis the local Basic-auth credential pair and is intentionally NOT promoted into this field.http_request.url: extracted URL (run throughredact()before storage so query-string secrets are masked).connection_info.protocol_name: extracted URL scheme.file.path: wordlist or output file path; scoped per tool family (web_discovery / credential_brute_force for-w, scanners for-o).status_id: OCSF normalized outcome:1(Success),2(Failure),0(Unknown).status: OCSF normalized outcome label (Success/Failure).status_code: source-specific outcome: numeric exit code (as a string) when known, or signal name for signal-terminated commands.duration:duration_msfrom the caller.aptl.session_id: propagated from the MCP tool args.
Activity catalogue¶
| activity_type | OCSF category_uid | OCSF class_uid / class_name | activity_id | type_uid | MITRE technique | MITRE tactic | Default severity_id | Tools / patterns |
|---|---|---|---|---|---|---|---|---|
port_scan |
4 Network Activity | 4001 Network Activity | 1 (Open) | 400101 | T1046 | Discovery | LOW (2) | nmap, masscan, rustscan, unicornscan |
network_connection |
4 Network Activity | 4001 Network Activity | 6 (Traffic) | 400106 | T1095 | Command and Control | MEDIUM (3) | nc, ncat, socat |
ssh_login_attempt |
3 IAM | 3002 Authentication | 1 (Logon) | 300201 | T1021.004 | Lateral Movement | LOW (2) | ssh, plink |
credential_brute_force |
3 IAM | 3002 Authentication | 1 (Logon) | 300201 | T1110 | Credential Access | HIGH (4) | hydra, medusa, patator, crowbar |
password_cracking |
1 System | 1007 Process Activity | 1 (Launch) | 100701 | T1110.002 | Credential Access | MEDIUM (3) | john, hashcat |
web_attack |
6 Application | 6001 Web Resources Activity | 99 (Other) | 600199 | T1190 | Initial Access | MEDIUM (3) | sqlmap, nikto, wpscan, xsstrike |
web_discovery |
6 Application | 6001 Web Resources Activity | 99 (Other) | 600199 | T1595.003 | Reconnaissance | LOW (2) | gobuster, dirb, dirbuster, wfuzz, ffuf, feroxbuster |
host_discovery |
5 Discovery | 5001 Device Inventory Info | 1 (Inventory Info) | 500101 | T1018 | Discovery | LOW (2) | enum4linux / enum4linux-ng, smbclient, smbmap, crackmapexec / cme / nxc, nbtscan, rpcclient, ldapsearch, kerbrute, bloodhound-python / bloodhound.py / sharphound, arping, fping, fierce, whatweb, wafw00f, dnsenum, dnsrecon, dig, host, nslookup, tcpdump, tshark |
remote_execution |
3 IAM | 3002 Authentication | 1 (Logon) | 300201 | T1021 | Lateral Movement | HIGH (4) | evil-winrm, impacket family (psexec.py, smbexec.py, wmiexec.py, dcomexec.py, atexec.py, secretsdump.py, getuserspns.py, getnpusers.py, ntlmrelayx.py, and the impacket-* aliases) |
network_poisoning |
4 Network Activity | 4001 Network Activity | 6 (Traffic) | 400106 | T1557 | Credential Access | HIGH (4) | responder, inveigh, mitm6 |
credential_dumping |
1 System | 1007 Process Activity | 1 (Launch) | 100701 | T1003 | Credential Access | HIGH (4) | mimikatz, pypykatz, lsassy, gosecretsdump |
exploit_framework |
1 System | 1007 Process Activity | 1 (Launch) | 100701 | T1059 | Execution | HIGH (4) | msfconsole, msfvenom, setoolkit, searchsploit, cewl |
process_execution (fallback) |
1 System | 1007 Process Activity | 1 (Launch) | 100701 | — | — | INFO (1) | anything else; the leading executable is preserved on aptl.tool for SIEM discrimination |
OCSF activity-id notes. web_attack and web_discovery use activity_id: 99 (Other) because OCSF Web Resources Activity defines IDs 1–7 as Create/Read/Update/Delete/Send/Import/Export; none of which semantically matches "attack" or "wordlist scan." Using Other plus the MITRE technique attached via attacks[] keeps SIEM consumers from seeing misleading activity labels (for example "Import" on a sqlmap injection). host_discovery uses class 5001 Device Inventory Info under category 5 Discovery; the prior class id 1009 was an invalid mix that schema-aware consumers could not normalize.
The classifier resolves the leading executable token by:
- Splitting on top-level shell separators (
&&,||,;,|) with single- and double-quote awareness. This meansnmap … && nc … 4444classifies asport_scan, notnetwork_connection, andecho "nmap is …"does not classify asport_scan. - Stripping a leading
sudoand anyKEY=valueenv assignments. - Taking the basename of the resulting executable so
/usr/bin/nmapmatchesnmap.
If no entry matches the executable, the generic process_execution
fallback fires—the contract per ADR-027 is that every command produces
an OCSF record, never null or a thrown exception.
Severity defaults¶
The default_severity_id column reflects the best-guess sensitivity of
the activity itself, not the success/failure of the individual run. The
logger applies one promotion rule on top:
If
success === falseand the default is belowMEDIUM, bump the emittedseverity_idtoMEDIUM.
Failed runs of inherently low-signal activities (port_scan, web_discovery)
are slightly more interesting because they often indicate a target with
hardening or a misconfigured agent; high-severity activities
(credential_brute_force, exploit_framework) keep their default—they
are already at or above MEDIUM.
Metadata extraction contract¶
mcp/mcp-red/src/extractor.ts extracts OCSF object fields from the command
string using quote-aware tokenisation. Behaviour is intentionally
conservative; if the command shape doesn't surface a field, the extractor
omits it rather than guessing.
Supported shapes:
- IPv4: bare addresses with octet validation. Out-of-range octets
(for example
999.0.0.1) are rejected. - IPv4 CIDR:
10.0.0.0/24. Out-of-range prefix lengths (/99) drop the CIDR but keep the bare IP. - IPv6:
::1,2001:db8::1, full-form addresses. Conservative pattern; ambiguous shapes are skipped rather than mis-attributed. - Ports:
-p 22,--port 22(single connection port for SSH-style tools),-p 22,80,443,-p 1-1024(port-list spec for scanners). Out-of-range ports (>65535 or 0) are rejected. host:portpositional pair:nc 10.0.0.1 4444extracts both.- SSH-style
user@host[:port]: populatestarget_user,dst_endpoint.ip/hostname,dst_endpoint.port. -l <user>/--user <user>: populatestarget_user.-w <path>/--wordlist <path>/-o <path>/--output <path>: populatesfile.path.- URLs:
https?://host[:port]/pathextracts the URL, hostname, port, and protocol. Used bycurl,sqlmap,nikto,gobuster,wfuzz,ffuf,feroxbuster, etc. - Credential-tool protocol token:
hydra … <host> sshrecordssshasprotocol. Recognised values:ssh,ftp,http,https,mysql,mssql,postgres,rdp,smb,telnet,vnc,imap,pop3,snmp.
Credential handling¶
Per ADR-027, secret values are never lifted into structured fields.
hydra -p hunter2 …: the literal password value stays only inprocess.cmd_line, where the sharedredact()helper masks it.hydra -P /path/to/list.txt …: the wordlist path goes tofile.path; the file's contents are never read or recorded.Authorization: Bearer X,--password X, cookie headers, URL userinfo, PEM blocks—all redacted at theprocess.cmd_lineboundary by the shared helper.
The extractor cannot eliminate the risk that a secret token appears in an
unanticipated place. The redact() policy is the defense against that;
the extractor only surfaces non-secret structured fields.
Sink and transport¶
Two channels run in parallel from the same postToolHook:
-
OCSF SIEM stream (
logger.ts+stderrJsonlSink): a single JSONL line per command, prefixed with the literal sentinel[OCSF]. External collectors can tail the MCP server's stderr and grep for the sentinel without parsing every line. This stream is enriched with classifier + extractor output for SIEM correlation. -
Research-grade raw capture (
capture.ts): every tool call (not just command tools) is appended as a JSONL record to<APTL_STATE_DIR>/runs/<run_id>/mcp-side/tool-calls.jsonl(per-run scope; falls back to the_unboundsentinel when no scenario is active). The capture is intentionally minimal: timestamp, tool name, redacted args, redacted result/error, exit_code, signal, duration, session_id. Researchers can re-parse this stream with their own logic in pandas/notebooks; the parser's classification choices are not load-bearing for analysis.
The legacy APTL_RED_CAPTURE_PATH env-var override was removed
under ADR-033 (OBS-003 non-contamination); its only purpose was
bind-mounting into the wazuh-manager container for SIEM ingestion,
which the non-contamination principle forbids.
Per ADR-027 as amended by ADR-033, the OCSF records are NOT
shipped to Wazuh / OpenSearch. The legacy kali_redteam syslog
ingestion path is removed: the Kali container no longer ships a
Wazuh agent, no longer forwards rsyslog to the SIEM, and
config/wazuh_cluster/kali_redteam_rules.xml /
kali_decoders.xml are deleted. OCSF records land in the per-run
JSONL at <state>/runs/<run_id>/mcp-side/ocsf.jsonl alongside the
tool-call JSONL and the PTY tee, plus a stderr [OCSF] line for
local development visibility.
Cross-references¶
- ADR-027—boundary, guardrails, and non-goals for OCSF schema work (amended by ADR-033).
- ADR-033—Red-Side Behavioural Capture and Non-Contamination Boundary (supersedes the SIEM-transport portion of ADR-027).
src/aptl/core/detection.py:SeverityIdenum (Python source of truth for the 0–6 scale).src/aptl/core/attacks.py: MITRE technique reference shape.mcp/aptl-mcp-common/src/redaction.ts: shared TypeScript redaction used byprocess.cmd_lineserialization.