-
-
Notifications
You must be signed in to change notification settings - Fork 0
AI Features
All AI surfaces share a common infrastructure:
-
OpenAIClient(source/app/iris_engine/ai/openai_client.py) — thin wrapper around the OpenAI-compatible HTTP API; backends can be LM Studio or any OpenAI-compatible endpoint -
case_ai_artifacttable — content-addressed cache keyed oninput_hash(MD5 of prompt + payload); adding one IOC re-runs only the affected specialist, not the whole summary -
Async job queue —
POSTto any AI endpoint returns202 + task_id; a dedicatedai_workercontainer runs jobs off theai_queueso long LLM calls never block a gunicorn worker
/manage/settings → AI tab
Two-slot design — a Primary backend and an Alt backend. The active-slot radio switches all AI calls with no restart.
Per-feature overrides: a collapsible table at the bottom of the AI tab lets you pin individual surfaces to a specific slot regardless of the global radio.
| Backend | How to configure |
|---|---|
| LM Studio (local) | Primary slot URL: http://host.docker.internal:1234/v1
|
| OpenAI / Azure OpenAI | Any OpenAI-compatible URL + API key |
| Anthropic API | Any OpenAI-compatible gateway in front of the Claude API |
For AI features with a "reason then compose" shape, the compose stage routes to a smaller/faster sibling model:
| Configured model | Synthesizer routes to |
|---|---|
| Claude Opus / Sonnet | claude-haiku-4-5 |
| LM Studio / other | unchanged (same model) |
This is configured via SYNTHESIZER_FAST_MODEL_MAP in case_summary.py.
When adding a new "reason then compose" feature, add the pairing there.
Endpoint: POST /api/v2/cases/<id>/ai/summary
Prompt files: case_summary.md (synthesizer) + case_summary_{notes,timeline,iocs,assets}.md
Cache kind: case_summary:<domain> (specialists) + case_summary (final)
Multi-pass map-reduce:
- Four domain specialists (notes / timeline / IOCs / assets) run in a
ThreadPoolExecutor - A synthesizer composes a 7-section executive output from the specialists' output
Cold path: ~12 s on LM Studio gpt-oss-20b, ~26 ms on full cache hit.
Pool worker threads do NOT inherit the calling thread's Flask app context — each worker
pushes its own with app.app_context():. ORM objects created in worker context detach
on exit; return artifact IDs and re-fetch from the main thread.
?sync=true keeps the old inline path for scripted use.
Endpoint: POST /api/v2/cases/<id>/ai/ask
Prompt files: case_chat_<variant>.md (falls back to case_chat.md)
Body: {question, history, variant?, csrf_token}
Available on six case-detail tabs: Notes, Timeline, Assets, IOC, Tasks, Evidence.
Each tab passes chat_variant to the include which loads the matching specialized prompt.
Multi-turn; client owns the history array.
?sync=true bypasses the job queue for scripted use.
Endpoint: POST /api/v2/cases/<id>/ai/timeline/events/<eid>/analysis
Prompt file: event_analysis.md
Cache kind: event_analysis:<event_id>
3 paragraphs: what the event detects / what likely happened / triage hint (80-160 words). Right slide-in opened by clicking a timeline card body.
Endpoint: POST /api/v2/cases/<id>/ai/timeline-analysis
Prompt file: case_timeline_analysis.md (prompt id TimelineNarrativeSystemPrompt-v4)
Cache kind: timeline_analysis
Three sections (prose only, 250-450 words):
- What the timeline tells us
- What's still uncertain
- Where to dig next
Flag-aware: is_flagged: false events contribute with HIGH confidence (reviewed fact);
is_flagged: true events contribute with MEDIUM confidence (provisional).
The panel header shows model · prompt_id · age from the cached artifact.
Endpoint: POST /api/v2/cases/<id>/ai/attack-suggestion
Stateless (not cached)
Returns up to 4 validated ATT&CK technique IDs plus a single UKC v1.3 phase.
Per-type regex shape validation, confidence ≥ 0.5 enforced server-side.
A Set Event Category button on the event modal auto-selects the matching dropdown option.
UKC phases are also wired into the Event Category dropdown (7 phases added via post_init).
Endpoint: POST /api/v2/cases/<id>/ai/ioc-extraction
Stateless (not cached)
Returns up to 10 IOC candidates with:
-
type_idpre-resolved against the liveIocTypetable - Per-type regex shape sanity (drops mis-classified IOCs)
- Noise flags (
⚠ Public DNS resolver,⚠ CDN domain, etc.) - Dedup against existing case IOCs (renders
in caseinstead of+ add)
Accept all is async/await-serialized to avoid celery prefork concurrency crashes.
Available in both the modal note editor and the inline (full-page) note editor.
+ add auto-fires POST /api/v2/cases/<id>/iocs/<id>/source-notes to create the IOC ↔ Note provenance link.
Endpoint: POST /api/v2/cases/<id>/ai/evidence-type-suggestion
Stateless (not cached)
Auto-fires alongside the hash/size step when the analyst clicks Process in the
Register Evidence modal. Reads filename + size + first 4 KB as hex (file never leaves
the browser via FileReader). Returns one validated EvidenceTypes catalog entry with
confidence + reason. Auto-applies to the dropdown; analyst override clears the chip.
Evidence type catalog is snapshotted at request time — admin-added types are picked up automatically.
Endpoint: POST /api/v2/alerts/<alert_id>/ai/case-template-suggestion
Stateless (not cached)
Auto-fires when the Escalate modal opens. Pulls alert title + description + source +
severity + classification + tags + first 20 IOCs + first 20 assets from the Alert ORM.
Returns one validated CaseTemplate with confidence + reason. Validated 13/13 MATCH
against the full template catalog at ≥ 0.92 confidence.
Regression harness: scripts/sim_alert_template_pick.py --all (exit 2 on any non-MATCH).
Endpoint: POST /api/v2/cases/<id>/ai/tag-suggestion (object-type variants)
Stateless (not cached)
Validates output against the bundled MISP catalog (169 taxonomies + 122 galaxies = 66,109 records). ✨ Suggest tags pill appears on IOC, asset, task, and event modals.
Endpoint: POST /api/v2/correlation/cluster-narrative
Prompt file: cluster_narrative.md (prompt id ClusterNarrativeSystemPrompt-v2)
Cache: case_ai_artifact anchored to min(cluster.case_ids), kind cluster_narrative:<cluster_id>
Generates a short narrative for a cross-case IOC cluster. Cached server-side;
force: true bypasses cache. Also cached client-side in CORR._narrativeCache[cluster_id]
for toggle-within-session without API calls.
Entity-name prohibition (v2 prompt, load-bearing for STIX safety): the prompt explicitly forbids echoing specific organization names, client names, or case identifiers in the output. Victims are described by sector role only. This makes cached narratives safe to embed in STIX bundles shared with third parties.
When bumping the prompt, update both the # ClusterNarrativeSystemPrompt-<N> header in
cluster_narrative.md AND the PROMPT_ID constant in cluster_narrative.py. Existing
cache entries miss automatically (input_hash includes full prompt text).
POST to /api/v2/cases/<id>/ai/summary or .../ai/ask returns:
{"task_id": "abc123", "state": "queued"}Poll:
GET /api/v2/ai/jobs/<task_id>
→ {state: "running"|"done"|"error"|"cancelled", result: {...}}
DELETE /api/v2/ai/jobs/<task_id> # cancel a queued job
GET /api/v2/ai/jobs?case_id=<id>&state=<state> # list jobs
Adding a new async surface: add one entry to the FEATURES registry in
source/app/iris_engine/ai/ai_jobs.py ({runner, kind: 'artifact'|'dict', priority}).
No dispatcher edits needed.
OpenAIClient.extract_content() strips internal chain-of-thought before returning text
to orchestrators. Callers receive clean JSON/text and need no per-feature handling.
| Format | Models | Stripping |
|---|---|---|
| Gemma-4 channel format | Gemma-4, Gemma-4-e4b | Extracts everything after the last <|channel>output marker; if the output channel is empty, falls back to _last_json_object(thought) which walks the thought channel backwards for the last valid JSON block |
<think> tags |
DeepSeek R1, Qwen-thinking | Strips <think>…</think> with re.DOTALL; also handles truncated (unclosed) tags |
content = None is normalised to "" before stripping.
Reasoning models spend 500–1 000 tokens on the thinking step before emitting output.
Budget default_max_tokens = thinking_budget + output_budget:
- The ATT&CK suggester uses
default_max_tokens = 2 000(was 800 — reasoning models hitfinish_reason=lengthmid-JSON fence at 800) - Short JSON responses still need at least 200–400 tokens above expected output size
- When adding a new orchestrator, test with a small
max_tokensfirst to surface truncation
Diagnostic: "AI backend returned non-JSON content (parse error: Expecting value: line 1 column 1 (char 0))" → model completed its thinking channel but emitted nothing in the output channel; either max_tokens is too low or _last_json_object found no valid JSON in the thought.
Three layers all need matching headroom when adding any synchronous AI path:
| Layer | Setting | Current value |
|---|---|---|
OpenAIClient.timeout |
urllib socket timeout | 600 s |
nginx proxy_read_timeout
|
AI endpoint location block | 10 min |
Gunicorn --timeout
|
worker ceiling | 600 s (baked into entrypoint, needs rebuild) |
Fast suggesters (ATT&CK, IOC, evidence type, case template) use 60-90 s.
- Create
source/app/iris_engine/ai/<name>.py - Call
OpenAIClient.build_default_client(feature='<prompt_file_stem>')— this makes the surface participate in per-feature backend routing automatically - Load the prompt from
source/app/resources/ai_prompts/<name>.md - Cache output in
case_ai_artifactwith a descriptivekinddiscriminator - If the orchestrator has a "reason then compose" shape, add the model pairing to
SYNTHESIZER_FAST_MODEL_MAPincase_summary.py - If it should be async, add it to
FEATURESinai_jobs.py - Add the
featurekey to the per-feature override table inmanage_srv_settings.html