Files
artex/detections/triage/README.md
T
dela 0335d572de
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
First Commit
2026-10-09 08:38:16 +08:00

114 lines
7.6 KiB
Markdown

# ARTEX host triage
English · [한국어](README.ko.md)
> 한국어: [`artex_host_triage.py`](artex_host_triage.py) 는 ARTEX 가 돌았다고 의심되는 **호스트 한 대에서 직접**
> 돌리는 읽기 전용 분류(triage) 스크립트입니다. SIEM(Sigma)·네트워크 센서(Suricata)·위협 인텔리전스
> 플랫폼(지표 CSV·MISP)을 쓰는 방어자 말고, SIEM 없이 의심 호스트의 셸 앞에 선 대응자를 위한 것입니다.
> 리슨 포트·기록 프록시 아티팩트·로그 마커·PostgreSQL 스키마를 저장소 소스에 근거해 점검하고, 각 발견에
> 같은 한계(포트는 바꿀 수 있음, CA 파일명은 단독 mitmproxy 와 공유됨 등)를 함께 적습니다. 자신이 소유하거나
> 서면 허가를 받은 호스트에만 사용하십시오. 한국어 전체 문서는 **[README.ko.md](README.ko.md)** 를 보십시오.
A read-only triage helper you run **on a single suspected host** to answer "did ARTEX run here?" from
local state. The rest of this directory serves defenders who run a SIEM ([Sigma](../sigma/)), a network
sensor ([Suricata](../suricata/)), or a threat-intelligence platform (the [indicators](../indicators/)).
This script serves the other responder: the one at a host's shell, with no SIEM, who needs a quick,
defensible answer from what is on the box.
It operationalizes the same fingerprints the rest of the directory ships, **plus the three host/DB
indicators the [indicator list](../indicators/artex_indicators.csv) deliberately carries without a Sigma
rule** because they are not log- or network-observable and can only be checked on the host itself
(`server-listen-port`, `recording-proxy-endpoint`, `postgres-exploration-schema`).
## What it checks
Every check is grounded in a string or path verified in this repository's source, and every finding
carries the same honest caveat as the matching Sigma rule or indicator row.
- **Listening ports** — `:8787` (admin UI) and `127.0.0.1:8788` (recording proxy), the defaults of the
`--addr` / `--proxy` flags in [`cmd/artex/main.go`](../../cmd/artex/main.go). Parsed from `ss`/`netstat`/`lsof`
on the live host, or from a file you pass with `--ports-from`.
- **Recording-proxy artifacts** — the MITM CA the recorder writes on first start,
`<data-dir>/traffic/_ca/mitmproxy-ca-cert.pem`, and the sibling `_index/index.sqlite` and `_blobs/`
([`traffic/traffic.go`](../../traffic/traffic.go); the data directory default is `data/` next to the binary).
The CA is the trust anchor of an adversary-in-the-middle traffic recorder (ATT&CK T1557).
- **Log markers** — the enrichment prober UA `artex-enrich/1.0` ([`enrich/enrich.go`](../../enrich/enrich.go)),
the self-update egress UA `artex-selfupdate` ([`selfupdate/github.go`](../../selfupdate/github.go)), and
the platform-guard audit marker ([`guard/guard.go`](../../guard/guard.go); kept verbatim, including the
non-ASCII framing, so the grep matches) in the log file(s) you point it at. Rotated logs compressed as
`.gz`/`.bz2`/`.xz` are decompressed and scanned too, so the host's log history is covered; a format with
no standard-library codec (`.zst`/`.lz4`) is reported as **skipped** rather than silently treated as
clean — decompress it first or `grep` it by hand.
- **PostgreSQL exploration schema** — the dual-graph tables (`exploration_nodes`/`_edges`/`_anchors` with
`assets`/`companies`/`activity` and the `agent_prompts` seed) in the ARTEX store
([`db/schema.sql`](../../db/schema.sql)). Run against a DSN with `psql` if available; otherwise the
script prints the exact read-only query for you to run by hand.
- **Process env injection** — a running process whose environment carries a proxy var (`HTTP_PROXY` /
`HTTPS_PROXY` / `ALL_PROXY`) **together with** a toolchain CA-trust var (`SSL_CERT_FILE` /
`CURL_CA_BUNDLE` / `REQUESTS_CA_BUNDLE` / `GIT_SSL_CAINFO` / `NODE_EXTRA_CA_CERTS`) pointing at a
`mitmproxy-ca-cert.pem`. ARTEX injects exactly these into every worker tool it spawns
([`agent/worker.go`](../../agent/worker.go) `proxyEnv`, asserted by
[`agent/proxyenv_test.go`](../../agent/proxyenv_test.go)). The variable **names are hard-coded** in the
source (only the values are configurable), so this tell survives an operator renaming the binary or
changing the ports — a stronger signal than the bare listen port. Read from `/proc` on the live Linux
host, or from a captured dump with `--proc-from`. A proxy and a mitmproxy CA together are reported high;
a mitmproxy CA alone, or the ARTEX default proxy endpoint (`127.0.0.1:8788`) alone, is medium; a
corporate proxy with no mitmproxy CA is deliberately not flagged.
A hit is a **triage lead, not an attribution**, and the absence of every finding is **not** a clean bill
of health: an operator can rename the binary, move the data directory, or change the ports.
## Platform support
The script is pure Python 3 (standard library only), so it runs wherever Python 3 does — verified on
Linux (the CI self-test) and macOS. Two checks are OS-specific, and both degrade cleanly rather than
failing:
- **Live port scan** — tries `ss`, then `netstat`, then `lsof`, and uses the first that produces output.
On Linux that is `ss`/`netstat`; on macOS/BSD, where `ss` is absent and `netstat` does not take the
Linux `-ltnp` flags (it exits with empty output), it falls through to `lsof -nP -iTCP -sTCP:LISTEN`,
parsed the same way. Pass `--ports-from` to read a saved listing instead of scanning live.
- **Live process-env scan** — reads `/proc`, so it runs only on Linux. On a host without `/proc`
(macOS/BSD) it is reported as **skipped**, not clean; capture a dump on the Linux host and pass it with
`--proc-from` (see Usage).
The remaining checks — recording-proxy artifacts, log markers, and the PostgreSQL schema — read the
filesystem, log files, and (with a DSN) `psql`, so they are OS-independent.
## Usage
```sh
# check a host end to end
detections/triage/artex_host_triage.py \
--data-dir /opt/artex/data \
--log /var/log/syslog --log-dir /var/log/artex \
--pg-dsn "$ARTEX_PG_DSN"
# machine-readable findings, and exit non-zero if anything fired
detections/triage/artex_host_triage.py --data-dir /opt/artex/data --json --exit-code
# offline / forensic image: read a captured process-environment dump
# make the dump on the host with:
# for p in /proc/[0-9]*; do echo "# $p"; tr '\0' '\n' < "$p/environ"; echo; done > proc_env_dump.txt
detections/triage/artex_host_triage.py --proc-from proc_env_dump.txt
# reproducible fixture test (no host state touched)
detections/triage/artex_host_triage.py --self-test
```
The script is pure Python 3 standard library: no install, no network, and it writes nothing anywhere
except the `--self-test`'s own temporary directory. It reads host state (open ports, a data directory,
log files, and — only if you pass a DSN — the database) and prints what it found. Exit code is `0` by
default (triage, not a gate); pass `--exit-code` to make it `1` when any indicator fired.
## How this stays honest
The `--self-test` builds a synthetic host — a data directory with a planted CA, index, and blob store; a
log containing each marker; a port listing; and a captured process-environment dump — and asserts every
check fires on it, then asserts a clean host, a benign log, and a corporate-proxy process produce **zero**
findings (no false positives). It is wired into the
[`detections` CI workflow](../../.github/workflows/detections.yml) and re-run by
[`detections/tests/run-all.sh`](../tests/run-all.sh), so a change that breaks a check, or that drifts an
indicator away from the source string it greps for, fails the merge gate. A detection you cannot run is
only a claim.