Skip to content

AI Features

zach115th edited this page Jul 20, 2026 · 7 revisions

AI Features

Overview

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_artifact table — content-addressed cache keyed on input_hash (MD5 of prompt + payload); adding one IOC re-runs only the affected specialist, not the whole summary
  • Async job queuePOST to any AI endpoint returns 202 + task_id; a dedicated ai_worker container runs jobs off the ai_queue so long LLM calls never block a gunicorn worker

Backend configuration

Admin UI

/manage/settingsAI 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.

Supported backends

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

Synthesizer model routing

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.

Surfaces

Executive case summary

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:

  1. Four domain specialists (notes / timeline / IOCs / assets) run in a ThreadPoolExecutor
  2. 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.

Case-scoped chat assistant

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.

Per-event AI analysis drawer

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.

Running master-timeline analysis panel

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):

  1. What the timeline tells us
  2. What's still uncertain
  3. 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.

MITRE ATT&CK + Unified Kill Chain suggestion

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).

IOC extraction from notes

Endpoint: POST /api/v2/cases/<id>/ai/ioc-extraction
Stateless (not cached)

Returns up to 10 IOC candidates with:

  • type_id pre-resolved against the live IocType table
  • Per-type regex shape sanity (drops mis-classified IOCs)
  • Noise flags (⚠ Public DNS resolver, ⚠ CDN domain, etc.)
  • Dedup against existing case IOCs (renders in case instead 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.

AI-suggested evidence type

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.

AI-suggested case template on alert escalation

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).

AI tag suggester

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.

AI cluster narrative

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).

Async job queue

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.

Reasoning model support

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.

max_tokens for reasoning models

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 hit finish_reason=length mid-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_tokens first 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.

Timeout chain

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.

Adding a new AI orchestrator

  1. Create source/app/iris_engine/ai/<name>.py
  2. Call OpenAIClient.build_default_client(feature='<prompt_file_stem>') — this makes the surface participate in per-feature backend routing automatically
  3. Load the prompt from source/app/resources/ai_prompts/<name>.md
  4. Cache output in case_ai_artifact with a descriptive kind discriminator
  5. If the orchestrator has a "reason then compose" shape, add the model pairing to SYNTHESIZER_FAST_MODEL_MAP in case_summary.py
  6. If it should be async, add it to FEATURES in ai_jobs.py
  7. Add the feature key to the per-feature override table in manage_srv_settings.html

Clone this wiki locally