tinysoc / README.md
Mroqui's picture
Link Field Notes and tag field-notes badge
2378bed verified
|
Raw
History Blame Contribute Delete
9.86 kB
---
title: TinySOC
emoji: 🛰️
colorFrom: red
colorTo: gray
sdk: gradio
sdk_version: 4.44.1
app_file: app_tinysoc.py
pinned: false
license: mit
tags:
- build-small
- backyard-ai
- off-the-grid
- well-tuned
- off-brand
- llama-champion
- field-notes
- tiny-titan
- best-demo
short_description: A fully-local AI SOC analyst on a small model
---
# 🛰️ TinySOC: a local SOC analyst for people with no SOC
**TinySOC** triages security logs for a **solo operator or a small MSP with no SOC
team**. It reads a log stream (Linux syslog, a Wazuh alert feed, or raw Windows
events), flags what actually looks wrong, highlights *why*, and explains each
finding in plain English: severity, summary, risk, and the next concrete action.
Submission to the **Build Small Hackathon** (Hugging Face × Gradio), *Backyard AI*
track. The "someone I know" is the solo security operator drowning in alert noise.
> **Runs 100% locally on CPU via `llama.cpp`: no cloud API, no data leaves the box.**
## Submission (Build Small Hackathon)
- **Demo video:** https://huggingface.co/spaces/build-small-hackathon/tinysoc/resolve/main/assets/demo.mp4
- **Social post:** https://x.com/Mroqui/status/2066654531258118382
- **Team (Hugging Face):** [@Mroqui](https://huggingface.co/Mroqui)
- **Field notes (write-up):** [FIELD_NOTES.md](./FIELD_NOTES.md)
- **Considered for:** `build-small`, `backyard-ai`, `off-the-grid`, `well-tuned`, `off-brand`, `llama-champion`, `field-notes`, `tiny-titan`, `best-demo`
<video controls src="https://huggingface.co/spaces/build-small-hackathon/tinysoc/resolve/main/assets/demo.mp4" width="100%"></video>
## Why a *small, local* model is the point (not an accessory)
Most "explain this alert" demos could call a cloud API and be done. TinySOC can't,
**by design**, and that is exactly what makes the small local model load-bearing:
- The **highlighter** scores every token of a log line by its **perplexity**
(how surprising it is given normal behavior). That needs **token-level logprobs
from the model itself**, something cloud chat APIs do not expose. So the model
has to run **locally**, and a *small* one keeps it real-time on CPU.
- Perplexity only **paints** novel tokens (a random C2 domain, a base64 blob). It
does **not** decide maliciousness, a code model finds a reverse shell mundane.
The deterministic baseline decides; perplexity highlights; the model explains.
## How it works: three honest stages
1. **Baseline (deterministic detector).** Learns a host profile from normal logs
(users, processes, IPs, active hours) and scores each line with a noisy-OR per
axis, emitting plain-English reasons. This **carries detection**, no LLM guesswork.
2. **Perplexity (highlighter).** A small **base** code model scores token surprise
in a single forward pass, underlining the spans that are genuinely novel.
3. **Explanation.** A small (≤4B) model turns a flagged line + its reasons into a
compact JSON triage: `severity`, `summary`, `why`, `next_action`,
`likely_false_positive`.
Timestamps and hostnames are masked before perplexity scoring, so it measures
*message content*, not the clock.
## Inputs it understands
- **Linux without Wazuh**: paste `/var/log/auth.log` or `journalctl -o short`.
- **Windows with Wazuh**: Wazuh JSON alerts, including `data.win` events.
- **Windows without Wazuh**: `Get-WinEvent | ConvertTo-Json` output.
- **Live Wazuh** (dev): tails a local Wazuh manager's `alerts.json`.
The UI is a **dark SOC console**: a live feed, one-shot injection, log upload,
five built-in scenarios, a per-axis risk radar, and **provenance badges**
(SAMPLE / INJECTED·DEMO / YOUR LOG / LIVE WAZUH) so a real alert is never confused
with demo data.
## Tracks and badges
Tagged in the YAML above to be considered for:
- **`build-small`** (base) and **`backyard-ai`** (track): a local-first tool for the
solo operator working in their own backyard, no cloud, no data leaving the box.
- **`off-the-grid`**: no cloud APIs, the whole pipeline runs on CPU on the box in
front of you.
- **`well-tuned`**: ships a fine-tuned explainer published on the Hub
(`Mroqui/TinySOC-Qwen2.5-3B`).
- **`off-brand`**: a hand-built dark SOC-console theme with custom panels, radar,
and heatmaps, not a default Gradio look.
- **`llama-champion`**: the model runs through the `llama.cpp` runtime
(`llama-cpp-python`).
- **`field-notes`**: a full write-up of the design bets and the things that bit me,
see [FIELD_NOTES.md](./FIELD_NOTES.md).
- **`tiny-titan`**: a small (≤4B) model does load-bearing work, token-level
perplexity highlighting plus plain-English triage, not a giant model in disguise.
- **`best-demo`**: app, narrated demo video, and social post, a live dashboard that
learns a baseline, flags an injected attack, and explains it in seconds.
## Run locally
```bash
pip install -r requirements.txt
python app_tinysoc.py
```
On first launch the models download automatically: a small **base** model for the
perplexity highlighter and a small **instruct** model for the explanation.
## Configuration (environment variables)
| Variable | Default | Role |
|----------|---------|------|
| `WEC_BACKEND` | `llamacpp` | Inference backend: `llamacpp` (self-contained) or `ollama` (dev) |
| `WEC_PPL_REPO` / `WEC_PPL_FILE` | `mradermacher/Qwen2.5-Coder-1.5B-GGUF` / `Qwen2.5-Coder-1.5B.Q4_K_M.gguf` | Highlighter **base** GGUF (perplexity) |
| `WEC_PPL_MODEL_PATH` | - | Explicit local path for the highlighter GGUF |
| `WEC_MODEL_REPO` / `WEC_MODEL_FILE` | `Mroqui/TinySOC-Qwen2.5-3B` / `TinySOC-Qwen2.5-3B-Q5_K_M.gguf` | Explainer GGUF |
| `WEC_MODEL_PATH` | - | Explicit local path for the explainer GGUF |
| `WEC_N_CTX` / `WEC_PPL_N_CTX` | `4096` / `1024` | Context size (explainer / highlighter) |
| `WEC_N_GPU_LAYERS` | `0` | GPU layers (set `-1` on a GPU grant) |
| `WEC_FLAG_THRESHOLD` | `0.6` | Baseline score above which a line is enriched |
| `WEC_OLLAMA_URL` | `http://localhost:11434` | Ollama server (dev backend only) |
| `WEC_WAZUH_CONTAINER` | `wazuh-wazuh.manager-1` | Wazuh manager Docker container to tail (Docker live source) |
| `WEC_ALERTS_PATH` | `/var/ossec/logs/alerts/alerts.json` | Path to `alerts.json` inside that container |
| `WEC_ALERTS_FILE` | - | Tail this local `alerts.json` directly, no Docker (file live source) |
| `WEC_ALLOW_UI_SOURCE` | `false` | Allow choosing the live source from the UI. Keep off on a shared/public deployment (see security note) |
| `WEC_TZ` | `America/Toronto` | Local time zone for displayed timestamps |
**Dev:** point at a remote Ollama server (`WEC_BACKEND=ollama` +
`WEC_OLLAMA_URL=http://<host>:11434`) to skip CPU model loads, see `.env.example`.
**Public Space:** keep the `llamacpp` default; a Space cannot reach a private IP.
### Live feed sources
The live feed reads new Wazuh alerts as they happen. Two interchangeable
transports read the **same Wazuh `alerts.json` schema**, so pick whichever fits
your deployment:
- **Docker (default):** tails the Wazuh manager container. If your container has
a different name, set `WEC_WAZUH_CONTAINER` (and `WEC_ALERTS_PATH` if the alert
file lives elsewhere).
- **Plain file (no Docker):** set `WEC_ALERTS_FILE` to a local `alerts.json`. This
fits a native (non Docker) Wazuh install, or a setup that mounts or ships its
`alerts.json` onto the host running TinySOC.
The app auto-detects the source: if neither a reachable container nor a readable
file is found (for example on a public Space), the live tab is hidden and the
other inputs stay fully usable. The on-demand "Generate live event" injection
works in Docker mode only; in file mode the upstream pipeline is not ours to
write to, so that button is inert.
**Format scope.** The live tab is Wazuh specific: it expects Wazuh alert JSON
and reads severity from `rule.level`. Other sources (Zeek, Suricata `eve.json`,
Security Onion, an Elasticsearch or OpenSearch index) use different schemas and
are **not** wired into the live tab. Feed those through **paste or upload**
instead: the baseline plus perplexity engines run on raw log text and are format
agnostic, they just score from the learned baseline rather than a vendor
severity field. Native live support for those sources would mean adding a parser
per format, which is out of scope here.
**Security note.** Choosing the live source from the UI is gated by
`WEC_ALLOW_UI_SOURCE` and is **off by default**. On a shared or public deployment
this prevents a visitor from pointing the file tail at arbitrary local files and
reading them through the feed. With the gate off, the app only reads the source
the operator configured via env vars; the UI fields are shown read-only. Enable
the gate only when you self-host and trust who can reach the UI.
> **Why we read logits directly, not `echo` logprobs.** `llama-cpp-python`'s
> echoed prompt logprobs are misaligned in the 0.3.x line, so the highlighter loads
> the model with `logits_all=True` and reads `Llama.eval()` / `.scores` to compute
> each token's true conditional log-probability in one pass.
### Fine-tuned explainer
TinySOC defaults to a distilled fine-tune of Qwen2.5-3B specialized for security
log triage, auto-downloaded from the Hub (`Mroqui/TinySOC-Qwen2.5-3B`). To override
it, point `WEC_MODEL_PATH` (a local GGUF) or `WEC_MODEL_REPO`/`WEC_MODEL_FILE` at
another model.
## Limits we own
- The deterministic baseline carries detection; the LLM only explains and is a
suggestion engine, not a verdict. Pure novelty detection produces false
positives on real Windows fleets (benign-but-rare events), so live detection is
shown on a controlled feed, with **badged injection** to demonstrate a catch.
- Bundled examples are **anonymized** (TEST-NET IPs, fake hostnames). Never paste
real infrastructure data or PII into a public Space.