{"_id":"@aionsec/athanor","_rev":"2-bcd658aeb44bd4a030b79ed7c00e778a","name":"@aionsec/athanor","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@aionsec/athanor","version":"0.1.0","keywords":["security","telemetry","zeek","sysmon","detection","threat-hunting","distillation"],"license":"MIT","_id":"@aionsec/athanor@0.1.0","maintainers":[{"name":"faanross69","email":"info@aionsec.ai"}],"homepage":"https://github.com/aionsec/athanor#readme","bugs":{"url":"https://github.com/aionsec/athanor/issues"},"bin":{"athanor":"dist/cli.js"},"dist":{"shasum":"f171e1dba54ab3476f489fc09b7861772370b9ef","tarball":"https://registry.npmjs.org/@aionsec/athanor/-/athanor-0.1.0.tgz","fileCount":179,"integrity":"sha512-owAGuYttBeozszxHmdi/zTvOGGlbexYDneJ4+2wEQE4MtmGyuXikpLCEZTKAJbp4CuktqZ9WXY+q/blJ4NCizQ==","signatures":[{"sig":"MEUCIFnIFgAYYdwUqIoUIdxF4n1/d1mHOXR2Qm36kyrASCkdAiEAgaUyuGic/CJph4X96ktG7HVD3a/ql1d/vstuqWdZB0M=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":511069},"type":"module","engines":{"node":">=22"},"gitHead":"93bcf58c4ff25b925276459ab0331ff80d1858bb","scripts":{"test":"node --import tsx --test 'test/**/*.test.ts'","build":"rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/cli.js","smoke":"bash scripts/smoke-dist.sh","typecheck":"tsc --noEmit","smoke:install":"bash scripts/smoke-install.sh"},"_npmUser":{"name":"faanross69","email":"info@aionsec.ai"},"repository":{"url":"git+https://github.com/aionsec/athanor.git","type":"git"},"_npmVersion":"10.9.4","description":"Raw security telemetry in, scored candidates out — a deterministic distillation pipeline for Zeek and Sysmon logs.","directories":{},"_nodeVersion":"22.22.0","dependencies":{"zod":"^3.25.76","yaml":"^2.8.3"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","typescript":"^5.6.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/athanor_0.1.0_1788461553925_0.975235171528521","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"_id":"@aionsec/athanor@0.1.1","bin":{"athanor":"dist/cli.js"},"bugs":{"url":"https://github.com/aionsec/athanor/issues"},"dist":{"shasum":"662aae125590149b70ab3852478bec3bf2069bac","tarball":"https://registry.npmjs.org/@aionsec/athanor/-/athanor-0.1.1.tgz","fileCount":179,"integrity":"sha512-+A9BmEN8LrUoNSQdU2xakfO4wRT3UU6T1tzXbYxRWD9HsTupQi/5U+JsTyIk9I8JiSw8bI6UvZh0YjhuokaxVg==","signatures":[{"sig":"MEUCIQCP7uIk9sGH1AJmowQs92JHGcP6Y1m2VKOzMW3FZ+wsbAIgS0C5+rld/lSjtgxtyqFxKEVfkWL7bNXOa1yULqLQXMs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDcBB+RUJgbpU9IbwG0bwaJojC0CvAra5hi1ylYWuz3qwIgU9z9vu56NheeKoqFqpEG6WdJQuuBNE7vnuvbNxvvdj0="}],"unpackedSize":511103},"name":"@aionsec/athanor","type":"module","engines":{"node":">=22"},"gitHead":"1f3244c827d24194331f3bd0886023de3daf2ae2","license":"MIT","scripts":{"test":"node --import tsx --test \"test/**/*.test.ts\"","build":"node scripts/build.mjs","smoke":"bash scripts/smoke-dist.sh","typecheck":"tsc --noEmit","smoke:install":"bash scripts/smoke-install.sh"},"version":"0.1.1","_npmUser":{"name":"faanross69","email":"info@aionsec.ai"},"homepage":"https://github.com/aionsec/athanor#readme","keywords":["security","telemetry","zeek","sysmon","detection","threat-hunting","distillation"],"repository":{"url":"git+https://github.com/aionsec/athanor.git","type":"git"},"_npmVersion":"10.8.2","description":"Raw security telemetry in, scored candidates out — a deterministic distillation pipeline for Zeek and Sysmon logs.","directories":{},"maintainers":[{"name":"faanross69","email":"info@aionsec.ai"}],"_nodeVersion":"20.18.1","dependencies":{"zod":"^3.25.76","yaml":"^2.8.3"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","typescript":"^5.6.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/athanor_0.1.1_1788633278943_0.9633681665208234"}}},"time":{"created":"2026-09-03T18:52:33.774Z","modified":"2026-09-05T18:34:39.281Z","0.1.0":"2026-09-03T18:52:34.110Z","0.1.1":"2026-09-05T18:34:39.120Z"},"bugs":{"url":"https://github.com/aionsec/athanor/issues"},"license":"MIT","homepage":"https://github.com/aionsec/athanor#readme","keywords":["security","telemetry","zeek","sysmon","detection","threat-hunting","distillation"],"repository":{"url":"git+https://github.com/aionsec/athanor.git","type":"git"},"description":"Raw security telemetry in, scored candidates out — a deterministic distillation pipeline for Zeek and Sysmon logs.","maintainers":[{"name":"faanross69","email":"info@aionsec.ai"}],"readme":"# athanor\n\n**Raw telemetry in, scored candidates out.** A folder of Zeek and Sysmon logs goes in;\na JSON file of scored, enriched, evidence-linked candidates comes out. One command, no\nservices, no database, no network calls.\n\n```bash\nnpx @aionsec/athanor ./telemetry/\n```\n\nAn *athanor* is the slow furnace an alchemist kept at a steady heat for days while a\nmixture reduced to whatever was actually in it. That is the job here. Security telemetry\narrives in volumes no analyst and no language model can read: hundreds of thousands of\nlines in which a handful of things are worth a human minute. Distillation is the step\nthat turns that volume into a short, scored, ranked list — and in most tooling it is the\nstep you are not allowed to look at. It happens inside a product, behind a score you\ncannot reproduce, on rules you cannot read.\n\nathanor is that step, extracted and handed over. Every threshold is in a file you can\nopen, every score is arithmetic you can follow, and the same folder distills to the same\nbytes on any machine. It ships five candidate types, a complete pipeline, and a sample\nintrusion dataset to run it against.\n\nIt is MIT-licensed and stands alone. It also accompanies AionSec's Agentic Security\nEngineering course (<https://aionsec.ai>), where the distillation stage it implements is\nthe seam the rest of the material is built on.\n\n---\n\n## Quickstart\n\nNode 22 or newer.\n\n```bash\n# Distill a folder of logs into ./candidates.json\nnpx @aionsec/athanor ./telemetry/\n\n# Choose the output path, and keep what the emit floors dropped\nnpx @aionsec/athanor ./telemetry/ -o candidates.json --discards caput.json\n```\n\nOr from a clone, which also gets you the sample dataset:\n\n```bash\ngit clone https://github.com/aionsec/athanor && cd athanor\nnpm install && npm run build\nnode dist/cli.js ./fixtures/raw -o candidates.json\n```\n\nThat last command prints:\n\n```\nathanor: 3859 events from 4 files in ./fixtures/raw\n  conn.log           zeek/conn                 1614\n  ssl.log            zeek/ssl                   480\n  sysmon-eid1.jsonl  sysmon/process_create      151\n  sysmon-eid3.jsonl  sysmon/network_connect    1614\ncandidates: 10\n  beacon                            5\n  data_transfer                     1\n  tls_anomaly                       1\n  unusual_parent_child_anomaly      2\n  powershell_invocation_anomaly     1   (3 below the 0.6 floor)\ncaput mortuum (dropped by the emit floors): 3\nwrote /…/candidates.json\n```\n\n3,859 events to 10 candidates. The summary goes to stderr, the JSON to the output path,\nso `-o /dev/stdout | jq` works.\n\n```\nUsage: athanor <telemetry-dir> [options]\n\nDistills a folder of raw telemetry into scored candidates. Recognized dialects:\nZeek conn.log / ssl.log (JSON lines), Sysmon EID 1 / EID 3 JSONL, PowerShell 4104\nJSONL, or a single already-normalized events.json.\n\nOptions:\n  -o, --output <path>    where to write the candidates (default: candidates.json)\n      --config <path>    athanor.yaml; without it the built-in defaults apply.\n                         Merged per key: what it names wins, what it does not name\n                         keeps the default, and a key set to null is removed\n      --discards <path>  also write the caput mortuum — the candidates the emit\n                         floors dropped\n      --version          print the version and exit\n  -h, --help             show this help\n\nExit codes: 0 = distilled, 1 = bad usage, unreadable input or unwritable output.\n```\n\n## What comes out\n\nEach candidate is a JSON object carrying its own score, the features that produced the\nscore, the ids of every event it was built from, and an enrichment block. The three\nnetwork types also carry an attribution block (which process on which host, and how\nconfident that link is); the two process types are built from Sysmon EID 1 and already\nname their process, so there is nothing to attribute. Nothing is a bare number without\nits inputs.\n\n| Type | Reads | Scores |\n| --- | --- | --- |\n| `beacon` | Zeek `conn` | interval regularity, byte and duration consistency, hourly coverage |\n| `data_transfer` | Zeek `conn` | producer-consumer ratio and outbound volume |\n| `tls_anomaly` | Zeek `ssl` | certificate, JA3/JA4 fingerprint and SNI anomalies (strongest signal wins) |\n| `unusual_parent_child_anomaly` | Sysmon EID 1 | parent-child process pairs against a tiered taxonomy |\n| `powershell_invocation_anomaly` | Sysmon EID 1 (+ EID 7) | rename, custom host, parent and command-line dimensions |\n\nEvery candidate then passes through the same back half: attribution against Sysmon EID 3\nfor the three network types, local frequency analysis over nine entity tables, and\nenrichment (rarity, first-seen, geo, business-hours proportion, threat-intel and\nprotocol-mismatch flags). See [docs/design.md](docs/design.md) for the stage-by-stage\naccount.\n\n## Supported inputs\n\nPoint athanor at a **directory**. It reads the files in that directory — it does not\ndescend into subdirectories.\n\n| Input | Recognized as | Notes |\n| --- | --- | --- |\n| Zeek `conn.log` | `zeek/conn` | JSON lines, epoch-second `ts` |\n| Zeek `ssl.log` | `zeek/ssl` | JSON lines, pre-joined with x509 certificate columns |\n| Sysmon EID 1 JSONL | `sysmon/process_create` | native Sysmon field names, one flat object per line |\n| Sysmon EID 3 JSONL | `sysmon/network_connect` | same; a mixed EID 1 + EID 3 file is dispatched per line |\n| PowerShell 4104 JSONL | `powershell/script_block` | parsed and normalized; **no scorer reads it in v1** |\n| `events.json` | normalized events | an array of already-normalized events, ids trusted as written |\n\nFiles are classified by name first (`conn.log`, `sysmon-eid3.jsonl`, `ssl.log.gz`) and by\nthe shape of their first record second, so an unhelpfully named export still lands in the\nright lane. A file athanor cannot classify is an **error**, not a skip: silently ignoring\na log file is silently losing evidence.\n\nRotated `.gz` logs are read directly — the magic bytes decide, so a mislabeled file works\ntoo. Any other container (zstd, xz, bzip2, zip, lz4, 7-zip, tar) is named and left for you\nto unpack. UTF-8 byte-order marks are stripped; a UTF-16 export is refused with a message\nsaying so.\n\nAn `athanor.yaml`, and whatever file you named with `--config`, are skipped and reported\nif they happen to sit inside the telemetry folder. Configuration is not evidence.\n\n## What v1 does not do\n\n**JSON logs only.** Zeek's *default* ASCII (tab-separated) logs are not supported. Re-run\nZeek with `redef LogAscii::use_json=T;` in `local.zeek`, or use the `json-streaming-logs`\npackage. That case is refused by name rather than left to fail as \"malformed JSON\".\nathanor also reads Zeek's default epoch-second timestamps, not the `JSON::TS_ISO8601` form.\n\n**The PowerShell 4104 lane is parsed but not scored.** Script-block records are read,\nvalidated, normalized and merged into the event stream — and then no scorer consumes\nthem, because none of the five candidate types reads script blocks. The lane exists as the\nworked example for extending ingest, and the plumbing behind it is real: the frequency\ntables and the rarity enrichment already know how to key on a script-block hash. Writing\nthe scorer that uses it is the natural first contribution.\n[docs/extending.md](docs/extending.md) walks the lane end to end.\n\n**Sysmon EID 7 (`image_load`) has no raw parser.** The PowerShell scorer reads image-load\nevents to spot a custom PowerShell host, and the normalized schema defines them — but\ningest has no EID 7 dialect, so the only way to supply them today is the `events.json`\nlane. On a folder of raw Sysmon logs that dimension never fires.\n\n**The known-bad TLS fingerprint sets are empty.** The TLS scorer's JA3, JA3S, JA3+JA3S\npair and JA4X sets ship empty, so its fingerprint dimension always scores 0 under the\ndefault configuration; `tls_anomaly` candidates come from the certificate and SNI\ndimensions alone. Those sets are a scorer-level constant, not something `athanor.yaml`\nreaches.\n\n**The bundled reference tables are placeholders.** `data/geoip/minimal.json`,\n`data/lots/minimal.json` and `data/threat-intel/minimal.json` hold a handful of\ndocumentation addresses between them. On real telemetry, `geo_country`, `geo_asn`,\n`lots_match` and `threat_intel_match` will report nothing until you replace those files\nwith tables of your own. The taxonomies that drive the process scorers\n(`data/unusual-parent-child-anomaly/`, `data/powershell-invocation/`) are real and\ncomplete; the three lookup tables are not.\n\n**athanor is an in-memory kit.** Every file is read whole and every event stays resident.\nThat is right for a course dataset or a day of one estate's logs, and wrong for a\nmulti-gigabyte export: slice big estates into folders you can hold in memory rather than\npointing athanor at all of it at once.\n\n## Ids, and what changes them\n\n**Event ids are reconstructed, not read.** Raw logs carry no event ids. athanor sorts\nevery parsed record from every dialect together — by timestamp, then by a fixed\nper-dialect rank — and numbers the result `evt-00001`, `evt-00002`, and so on.\n\nOne consequence surprises people: **adding a file to a folder renumbers the events.** A record inserted mid-stream shifts every later id by\none, and since candidate ids are content hashes computed over records that include those\nevent ids, the candidate ids change with them. The same folder always distills to the same\nbytes; a folder with one more log file in it does not.\n\nTwo smaller cases the run reports rather than hides:\n\n- When two records tie on both sort keys, the rule cannot separate them, and the merge\n  falls back to file and line order. That is stable for a given folder and not stable\n  across a re-ordered export — so the CLI warns and names the pairs.\n- Timestamps are read as UTC regardless of the machine's timezone. A `TimeCreated` with no\n  zone is UTC, and a locale-format stamp is refused rather than guessed at, so the same\n  folder distills identically in every timezone.\n\nPresentation ids (`BCN-001`, `DT-001`, `TLS-001`, `UPCA-001`, `PSI-001`) are assigned last,\nby rank within each type. They are labels for reading a single run's output, not\nidentities: the candidate that is `BCN-002` today is `BCN-003` tomorrow if a stronger\nbeacon appears. The content-derived id is preserved as `pipeline_candidate_id`, and that\none is stable for as long as the candidate's contents are.\n\n## Emit floors, and the caput mortuum\n\nEach candidate type has a minimum score. A candidate below its floor is not emitted. The\ndefaults are `beacon` 0.40, `tls_anomaly` 0.40, `powershell_invocation_anomaly` 0.60,\n`unusual_parent_child_anomaly` 0.60, and no floor at all for `data_transfer`.\n\nA threshold that deletes evidence without saying so is the thing this tool exists to\nargue against, so the count is always printed and `--discards <path>` writes the discarded\npile itself. It is called the **caput mortuum** — the \"dead head\", what alchemists named\nthe residue left in the vessel once distillation had taken everything useful out of it.\nReading it is how you find out that your floor was set wrong.\n\n## Config\n\n`--config <path>` names a YAML file. Without it, the built-in defaults apply. athanor never\npicks up a stray `athanor.yaml` from the working directory, because a config that is found\nrather than named changes scores without anyone asking.\n\nThe file is merged into the defaults **per key**, and the whole rule is three lines:\n\n1. a key you name takes your value;\n2. a key you do not name keeps its default;\n3. a key you set to `null` is removed.\n\nSo this config, meant to see more beacons, changes the beacon floor and nothing else — the\n`powershell_invocation_anomaly` (0.6), `unusual_parent_child_anomaly` (0.6) and\n`tls_anomaly` (0.4) floors all stand:\n\n```yaml\nemit_floors:\n  beacon: 0.2\n```\n\nand this one clears the TLS floor while keeping the rest, so every `tls_anomaly` candidate\nis emitted however low it scores:\n\n```yaml\nemit_floors:\n  tls_anomaly: null\n```\n\n`emit_floors: null` clears them all. The same three rules govern `presentation_ids`,\nincluding its `prefixes` map, key by key. `distill_candidates` is the one exception: it is\na list rather than a mapping, so declaring it replaces the list — and declaring it *empty*\nis an error rather than a run that quietly distills nothing.\n\nEvery run that used a `--config` prints one line naming what the file overrode and what it\nremoved:\n\n```\nconfig: /estate/athanor.yaml — 1 key overridden (emit_floors.beacon), 1 removed (emit_floors.tls_anomaly); every key the config does not name keeps its built-in default\n```\n\n## Sample data\n\n`fixtures/raw/` is a synthetic intrusion scenario: four raw dialect files, 3,859 events\nacross 26 hosts, one compromised workstation, several perfectly ordinary things that score\nhigh, and ten candidates at the other end. Nothing in it was captured from a real network.\n[fixtures/README.md](fixtures/README.md) describes what is in the dataset and what each\ncandidate is looking at.\n\nIt is also the test contract. Three conformance pins enter the pipeline at three different\npoints — raw folder, normalized events, pre-enriched events — and each asserts that the\nresult is byte-for-byte identical to the committed golden file.\n\n## Documentation\n\n- **[docs/schema.md](docs/schema.md)** — the normalized event schema: every event type,\n  every field, every format, and the rules that admit or refuse a record.\n- **[docs/extending.md](docs/extending.md)** — write a converter for your own log format.\n  One existing parser walked through line by line, then what to add and how to prove it\n  works.\n- **[docs/design.md](docs/design.md)** — how the pipeline is built and why: the stages,\n  the floors, determinism and canonical serialization, and the testing philosophy.\n\n## Development\n\n```bash\nnpm test              # unit suites, the canon contract and the three conformance pins\nnpm run typecheck\nnpm run build         # → dist/\nnpm run smoke         # re-runs the compiled CLI over fixtures/raw against the golden\nnpm run smoke:install # packs the tarball, installs it clean, runs the INSTALLED bin\n```\n\nThe two smokes are packaging tripwires and they catch different things. `npm run smoke`\nproves the *compiled* CLI still reproduces `fixtures/candidates_enriched.json` byte for\nbyte, which is what catches a build layout that breaks `data/` resolution — a failure that\nproduces different scores rather than an error. `npm run smoke:install` proves the\n*packaged* CLI does, installed into a clean prefix from the tarball, so a `files` list that\nforgot `data/`, a runtime dependency left in `devDependencies`, or a bin that does not\nresolve all surface here rather than in someone's terminal. Run both after touching\n`tsconfig.build.json` or the `files` / `bin` / `dependencies` fields.\n\n## Contributing\n\nIssues and pull requests are welcome. The most useful contributions, roughly in order:\n\n1. **A converter for a log format athanor does not read.** EDR exports, Windows Event Log\n   JSON, Suricata EVE, cloud audit logs. [docs/extending.md](docs/extending.md) is written\n   for exactly this.\n2. **A scorer for the 4104 lane** — the events are already there, unused.\n3. **Real reference tables** to replace the three placeholder lookups.\n4. **Bug reports with the telemetry that caused them**, synthetic or redacted.\n\nOne rule governs everything else: `fixtures/candidates_enriched.json` is a contract, not\nan output. If a change moves it, the change is what needs explaining.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","readmeFilename":"README.md"}