Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
185 commits
Select commit Hold shift + click to select a range
ea4dac4
feat: route LM through configurable api_base with yaml authority
HackedRico Apr 27, 2026
7bbdf5a
feat(gui): MCP server checklist + enabled_servers in submit payloads
HackedRico Apr 27, 2026
fec033e
test: integration tests for RANGE MCP server discovery and merge
HackedRico Apr 27, 2026
fb7a0c8
fix: re-enable factory autolog and stop calling dspy.configure per-run
HackedRico Apr 27, 2026
11f20c2
feat(api): /defaults endpoint, history routes, drop dead setup_routes
HackedRico Apr 27, 2026
7f0b21a
feat(gui): pull Global Model Config defaults from /plugin/mcp/defaults
HackedRico Apr 27, 2026
7675e60
refactor: move MCP plugin code into framework subfolders
HackedRico Apr 27, 2026
255173c
feat: add Workflow and Capability dataclass contracts
HackedRico Apr 27, 2026
451e4da
feat: register Author and Plan and Execute as Workflow instances
HackedRico Apr 27, 2026
f3021f1
feat: register RAG as a Capability instance
HackedRico Apr 27, 2026
6314963
feat: discover workflows and capabilities at boot
HackedRico Apr 27, 2026
308420e
feat(svc): switch orchestrator to workflow registry
HackedRico Apr 27, 2026
5cc20d5
feat(api): expose workflow and capability registries via HTTP
HackedRico Apr 27, 2026
a7a3014
feat(gui): render workflows from discovery and scope per-workflow UI
HackedRico Apr 27, 2026
9153dc9
docs: add REFACTOR_PLAN.md describing the workflow and capability red…
HackedRico Apr 27, 2026
03c93f3
fix(workflows): demand substantive output instead of methodology recap
HackedRico Apr 27, 2026
a58c166
chore(gitignore): exclude env files and local planning docs
HackedRico Apr 30, 2026
a012c6d
refactor(env): load .env once in the plugin parent process
HackedRico Apr 30, 2026
9571b7e
refactor(config): unify resolver and namespace credential env vars
HackedRico Apr 30, 2026
ee8317c
refactor(svc): route LM resolution through the unified resolver
HackedRico Apr 30, 2026
e93facd
refactor(workflows): drop duplicate yaml loaders and silent UI overrides
HackedRico Apr 30, 2026
921371e
feat(rag): allow embeddings to use a separate provider
HackedRico Apr 30, 2026
45113fd
feat(gui): make splash a plugin readiness landing page
HackedRico Apr 30, 2026
1a95d8b
refactor(config): promote config to a package with parent and subproc…
HackedRico Apr 30, 2026
6c31b37
refactor(svc): drop numbered section banners and trim noise comments
HackedRico Apr 30, 2026
ec157f4
refactor(config): collapse the config package back into a flat module
HackedRico Apr 30, 2026
88f896e
refactor(dspy_env): own the LM bootstrap inline; share env-var names …
HackedRico Apr 30, 2026
c3e819f
fix(gui): render the workflow's process_result as a Result panel
HackedRico Apr 30, 2026
9ca6ecd
feat(svc): in-memory run-state cache for live polling
HackedRico Apr 30, 2026
5dfefab
refactor(api): read live run state from the cache, not MLflow
HackedRico Apr 30, 2026
09669db
feat(gui): render the Result panel as nested lists with bold headings
HackedRico Apr 30, 2026
f3e1380
style(gui): match Result panel to the dark theme and fix bold contrast
HackedRico Apr 30, 2026
e537d37
update gitignore
HackedRico Apr 30, 2026
411caef
chores: no longer used docs
HackedRico Apr 30, 2026
0d7430c
docs: support for N plugin mcp servers
HackedRico Apr 30, 2026
6958ecf
feat(gui): chat-style workflow page for the author signature
HackedRico May 6, 2026
59cebb1
fix(core-mcp): harden caldera URL endpoint construction
HackedRico May 6, 2026
b35b76e
docs: ui redesign and multi-turn chat plan documents
HackedRico May 6, 2026
efb51f2
feat(workflows): add supports_chat_history opt-in flag
HackedRico May 6, 2026
fda9340
feat(plan_execute): thread chat_history through signature and run
HackedRico May 6, 2026
a7d7c76
feat(mcp_svc): per-session chat history for opt-in workflows
HackedRico May 6, 2026
322c619
feat(mcp_api): accept and echo session_id on /execute
HackedRico May 6, 2026
a2724f1
feat(gui): track session_id across turns and adapt composer copy
HackedRico May 6, 2026
2d85d7c
feat(chat-history): user-controlled session-wide history toggle
HackedRico May 6, 2026
c12670f
style(chat-sidebar): left-align small-cap field labels
HackedRico May 6, 2026
67ada30
ux(chat): move back button from right sidebar to top-left of header
HackedRico May 6, 2026
9fb2dee
style(chat-sidebar): align sidebar header strip with chat header
HackedRico May 7, 2026
5ecd0a8
style(chat-header): drop border on back button so it aligns with title
HackedRico May 7, 2026
f96ad27
style(chat-header): pin back-button and title to equal height
HackedRico May 7, 2026
97b4b43
style(chat-header): align back button and title by text baseline
HackedRico May 7, 2026
7fa6ba6
style(chat-header): trim header padding and equalize control heights
HackedRico May 7, 2026
d3dd235
style(chat-header): center align row for even padding above and below
HackedRico May 7, 2026
0180386
style(chat-header): match font sizes on back button and title
HackedRico May 7, 2026
0994b3a
fix(chat-header): outrank bulma's h2 margin so author sits beside back
HackedRico May 7, 2026
069b560
feat(gui): route plan_execute through the chat workflow component
HackedRico May 7, 2026
5f67ac0
docs: update for plan_execute now using the chat workflow component
HackedRico May 7, 2026
0f956b5
fix(chat-header): keep history toggle and new chat side by side
HackedRico May 7, 2026
fb11ed0
fix(mcp_svc): strip dspy field markers from result and reasoning
HackedRico May 7, 2026
d68fb19
chores: remove unused docs
HackedRico May 7, 2026
f81d322
ignore graphify context
HackedRico May 7, 2026
c282960
chore(gitignore): split graphify scope rules into .graphifyignore
HackedRico May 8, 2026
b417433
fix(chat-rag): inject $api so the sidebar file picker actually fetches
HackedRico May 8, 2026
b2d71e8
fix(workflows): translate DSPy parse failures and raise max_tokens de…
HackedRico May 8, 2026
b40f6d7
fix(chat): surface the backend run error in the failure panel
HackedRico May 8, 2026
837ef21
fix(chat): persist transcript and sessionId per workflow across remounts
HackedRico May 8, 2026
21f6661
fix(mcp_svc): persist chat-history sessions to disk across restarts
HackedRico May 8, 2026
7f37d6b
fix(mcp_api): drop per-poll status log to DEBUG
HackedRico May 8, 2026
2ac6263
feat: consolidate CTI-backed MCP execution
May 12, 2026
24f01f4
test: use placeholder model API key
May 12, 2026
0e7673c
Merge remote-tracking branch 'caldera-mcp/main' into feat/mcp-cti-pla…
May 12, 2026
302e77f
docs: add MCP workflow screenshots
May 12, 2026
ee05be0
docs: refresh MCP README
May 12, 2026
03f2cc2
docs: correct MCP README structure
May 12, 2026
c6251fe
fix(mcp): api_key_env fallback + ae-e2e workflow registration
May 13, 2026
2310e8c
ui(mcp): remove AE-E2E card, simplify Plan and Execute, surface dynam…
May 13, 2026
4d94502
ui(mcp): retire the Author workflow
May 13, 2026
e509cf1
Revert "ui(mcp): retire the Author workflow"
May 13, 2026
ac139d4
ui(mcp): drop MCP Servers panel from workspace level 1
May 13, 2026
00b4deb
ui(mcp): keep Plan and Execute description visible everywhere except …
May 13, 2026
7911844
fix(mcp/ae-e2e): pass access dict to create_object_from_schema
May 14, 2026
86e107c
SSL-verify + timeout config end-to-end, dynamic MLflow port, .gitigno…
May 18, 2026
44ac837
Merge branch 'feat/ssl-verify-mlflow-dynamic' into 'main'
May 18, 2026
44f9fb1
Clean up vendored D3FEND CAD upstream tooling
May 27, 2026
8234014
Merge branch 'chore/cleanup-vendored-d3fend-tooling' into 'main'
May 27, 2026
b278607
Merge branch 'main' of https://github.com/HackedRico/mcp
HackedRico Aug 28, 2026
c8fecd6
Merge remote-tracking branch 'upstream/main'
HackedRico Aug 28, 2026
77243bb
Merge branch 'main' of https://github.com/HackedRico/mcp
HackedRico Aug 28, 2026
fdf1d39
Merge remote-tracking branch 'upstream/main'
HackedRico Aug 28, 2026
b133292
Merge remote-tracking branch 'upstream/main'
HackedRico Aug 28, 2026
cf8cae8
fix(config): make get_config and set_config agree on the envelope
HackedRico Aug 28, 2026
e02d99d
fix(imports): materialize numpy before spaCy loads
HackedRico Aug 28, 2026
e8e3bb8
test: drop tests for modules that no longer exist
HackedRico Aug 28, 2026
b47e291
fix(stage1): report per-file failures honestly
HackedRico Aug 28, 2026
57fef37
feat(cti): restore offline IR extraction
HackedRico Aug 28, 2026
1756b0a
refactor(cti)!: drop the CALDERA fact bridge
HackedRico Aug 28, 2026
d43b3e9
refactor(cti)!: drop D3FEND enrichment
HackedRico Aug 28, 2026
e402970
refactor(mcp)!: drop the detection validation tool
HackedRico Aug 28, 2026
24f12b1
refactor(cti)!: drop entity extraction and shrink the STIX builder
HackedRico Aug 28, 2026
9dd92df
refactor(cti): extract the shared text helpers
HackedRico Aug 28, 2026
d42013e
feat(nlp): load the spaCy model once per process
HackedRico Aug 28, 2026
5a20892
feat(mcp): add cti_pipeline_build_adversary
HackedRico Aug 28, 2026
71e776b
refactor(workflows)!: delete ae-e2e
HackedRico Aug 28, 2026
de997dc
refactor(cti): drop the nltk dependency
HackedRico Aug 28, 2026
ae14069
fix(cti): drop the linguistic source from technique attribution
HackedRico Aug 28, 2026
e479bff
test(cti): does fusing several reports raise recall?
HackedRico Aug 28, 2026
2b9007d
build(deps): declare what is imported, drop what is not
HackedRico Aug 28, 2026
bdb9cde
docs: describe the real install and first run
HackedRico Aug 28, 2026
3356bfb
fix(mcp): match only parent to child, and cap per technique
HackedRico Aug 28, 2026
fdbfce0
fix(stage1): restore the taxonomy that grounding and the platform fil…
HackedRico Aug 28, 2026
6a35b58
fix: address the branch review
HackedRico Aug 28, 2026
07a0f2f
build(deps): declare the test-only requirements
HackedRico Aug 28, 2026
8cf7633
fix(mcp)!: order abilities by ATT&CK kill chain, not CALDERA tactic
HackedRico Aug 28, 2026
6e4193a
fix(cti): record which extractor produced the IR, and stop discarding…
HackedRico Aug 28, 2026
dd06a81
refactor(cti): drop plumbing the deletions left behind
HackedRico Aug 28, 2026
f136bff
test: make the regression guards able to fail
HackedRico Aug 28, 2026
6dc17d4
test: stop asserting the schema fills a key with no producer
HackedRico Aug 28, 2026
86ebf88
fix(gui): stop reporting success for work that did not happen
HackedRico Aug 28, 2026
9f532f7
test: stop the integration test mutating the operator's config
HackedRico Aug 28, 2026
edd8800
fix(mcp): let optional tool args accept the default they declare
HackedRico Aug 28, 2026
c1b6ffa
perf(cti): cache the MITRE taxonomy and normalized patterns
HackedRico Aug 30, 2026
7629bc2
refactor(mcp): derive the kill chain order from the bundle
HackedRico Aug 30, 2026
7ea9ca1
fix(mcp): send the operation keys core actually reads
HackedRico Aug 30, 2026
05864f8
fix(cti): stop naming the adversary after the payload
HackedRico Aug 30, 2026
479638a
fix(api): serve uploaded STIX from the directory the readers use
HackedRico Aug 30, 2026
8ab22d3
fix(api): reject malformed CTI requests with 400 instead of 500
HackedRico Aug 30, 2026
6ca2168
fix(cti): fall back to offline IR when the endpoint is unreachable
HackedRico Aug 30, 2026
0e82e35
refactor(config): layer the cti profile over llm
HackedRico Aug 30, 2026
f63cec9
feat(gui): accept drag and drop for CTI uploads
HackedRico Aug 30, 2026
d516983
style: repair the server description and correct the model size
HackedRico Aug 30, 2026
04f1f50
fix(config): declare provider on llm so layered profiles inherit it
HackedRico Aug 30, 2026
4f10622
style(gui): group the endpoint fields in Global Model Config
HackedRico Aug 30, 2026
acf4b27
fix(rag): fall back to the configured model for embeddings
HackedRico Aug 30, 2026
e421709
refactor(gui): drop the duplicated endpoint fields from the CTI panel
HackedRico Aug 30, 2026
385056f
refactor!: remove the mock response toggle
HackedRico Aug 30, 2026
f9c9000
style(gui): give the CTI tables the full page width
HackedRico Aug 30, 2026
74d42e7
feat(gui): select CTI rows by clicking anywhere in them
HackedRico Aug 30, 2026
050b0e6
feat(cti): preview an uploaded report in the app
HackedRico Aug 30, 2026
0ed36fe
style(gui): keep the extraction panel open and fill its column
HackedRico Aug 30, 2026
91a00ce
style(gui): align the two CTI tables and add a raw View column
HackedRico Aug 30, 2026
7b70631
fix(gui): put the preview close control in the corner
HackedRico Aug 30, 2026
e319c91
style(gui): make the extraction panel legible and say what offline does
HackedRico Aug 30, 2026
15d9fb6
style(gui): let the dropzone take the ingest box's leftover height
HackedRico Aug 30, 2026
6242bb9
fix(mcp): stop the fallback metadata advertising deleted capabilities
HackedRico Aug 30, 2026
1d5480f
fix(api)!: require authorization on every plugin route
HackedRico Aug 30, 2026
dca8884
fix(cti): write the repair debug log under data, not the cwd
HackedRico Aug 30, 2026
57fe64e
fix(api)!: contain both delete endpoints to their own directories
HackedRico Aug 30, 2026
4aa3668
docs: fix the clone url and declare poppler
HackedRico Aug 30, 2026
b790e01
fix(cti): report missing extraction dependencies instead of failing b…
HackedRico Aug 30, 2026
cfbf85f
feat(cti): surface pipeline failures in the browser
HackedRico Aug 30, 2026
5336574
fix(cti): stop crediting a model that did not run
HackedRico Aug 30, 2026
89936d3
fix(config): show which model extraction will actually use
HackedRico Aug 30, 2026
6b5fd44
fix(config)!: merge into a config section instead of replacing it
HackedRico Aug 30, 2026
2344a80
docs(config): remove a duplicate cti heading from the example
HackedRico Aug 30, 2026
aa15428
refactor(config)!: one endpoint, and workloads may only tune generation
HackedRico Aug 30, 2026
64b1b10
fix(gui): stop the model panel writing local.yml on page load
HackedRico Aug 31, 2026
7f87aec
fix(config): make the writer enforce the rule the reader applies
HackedRico Aug 31, 2026
ecc509f
fix(cti): scope extraction to the reports the operator selected
HackedRico Aug 31, 2026
c412da6
fix(config): honour a pre-allowlist connection instead of retargeting
HackedRico Aug 31, 2026
0fab610
docs: correct the config docs to match the allowlist
HackedRico Aug 31, 2026
2f3d32b
fix(config): one source of truth for the connection
HackedRico Aug 31, 2026
da559ac
refactor(config)!: drop the pre-allowlist compatibility path
HackedRico Aug 31, 2026
63fbb29
fix(author): pass the predicted field, not the whole Prediction
HackedRico Aug 31, 2026
8d739fc
fix(cti): a markdown upload was accepted and never parsed
HackedRico Aug 31, 2026
18e846c
fix(gui): diff the config sync against the server, not the mount snap…
HackedRico Aug 31, 2026
74aac98
docs(config): tighten the default.yml comments
HackedRico Aug 31, 2026
656ba7d
fix(mlflow): record the cause of a failure, not the group wrapping it
HackedRico Aug 31, 2026
a6584b4
test(config): stop the live tests writing the operator's config
HackedRico Aug 31, 2026
31f31ca
test: no test may leave conf/local.yml changed
HackedRico Aug 31, 2026
9f0deed
fix(config): one temperature and one max_tokens, shown by both panels
HackedRico Aug 31, 2026
3174bd1
fix(cti)!: two reports could clean to one file and one was lost
HackedRico Aug 31, 2026
2c0479b
fix(cti): a report is processed when it has an IR, not when it moves
HackedRico Aug 31, 2026
080ad78
fix(config): close the writer gaps the workload allowlist left open
HackedRico Aug 31, 2026
1d34faf
fix(nlp): keep two-word threat actor names, and drop three dead guards
HackedRico Aug 31, 2026
b76a7c8
chore: replace datetime.utcnow, deprecated since python 3.12
HackedRico Aug 31, 2026
070c251
refactor(cti)!: stop extracting what the bundle throws away
HackedRico Aug 31, 2026
b30f068
fix(cti): a report containing a file hash aborted stage 2
HackedRico Aug 31, 2026
4e08fef
fix(config): a configured temperature of 0 was exported as 0.5
HackedRico Aug 31, 2026
3952913
refactor: delete code nothing reaches
HackedRico Aug 31, 2026
e0533cb
test: stop the live suites writing a connection to a running server
HackedRico Aug 31, 2026
4a7e7d0
docs: say what the pipeline extracts and what cti may set
HackedRico Aug 31, 2026
296854c
test: make the STIX compliance check able to fail
HackedRico Aug 31, 2026
62a96b3
fix(hook): stop importing the plugin from spawning a server
HackedRico Aug 31, 2026
12a27da
fix(llm): say which model failed and where the setting lives
HackedRico Aug 31, 2026
4a5c044
test: stop the live suites touching the operator's data
HackedRico Aug 31, 2026
6345a44
fix(hook): stop MLflow outliving the server that started it
HackedRico Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 91 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The plugin is designed to keep the LLM grounded in MCP tools and server-side con
## Features

- **Author workflow**: Create CALDERA abilities and adversaries from an operator prompt while using available MCP server tools.
- **Plan and Execute workflow**: Select or upload CTI/STIX, build an adversary from the observed techniques, run the CALDERA operation against available agents, and summarize detection coverage.
- **Plan and Execute workflow**: Select or upload CTI/STIX, build an adversary from the observed techniques, run the CALDERA operation against available agents, and report which techniques it covered.
- **CTI ingest pipeline**: Upload raw CTI in HTML, PDF, plaintext, or Markdown and produce STIX 2.1 bundles for retrieval and planning.
- **STIX selection for planning**: Pick generated STIX bundles from the Plan and Execute workspace. Selecting STIX automatically enables CTI retrieval for that run.
- **Model and CTI/RAG profiles**: Configure the chat model, API base, API key, temperature, token budget, and tool-call budget. CTI/RAG can use the chat model by default or a saved profile when it needs a different endpoint.
Expand Down Expand Up @@ -68,9 +68,45 @@ The CTI ingest workflow stages raw CTI files, runs extraction, and displays gene

<br>

### CTI Pipeline Fidelity
### What the pipeline produces

The CTI pipeline extracts structure from STIX and related observables, including hosts, identities, user accounts, domains, software, ATT&CK platform hints, and CVE references. These become CALDERA facts so an operation runs against values the report actually named, not placeholders.
A threat report tells you **what** to run. Your environment tells you **where**.

The pipeline extracts ATT&CK techniques, the named threat actor, and file
hash observables, and nothing else. It does not extract hosts, accounts, domains, services or
software, and does not turn them into CALDERA facts. Those describe the
report's victim, not your estate; CALDERA discovers facts about your hosts
at runtime, which is why the abilities that use them carry `has_agent_copy`
and `no_backwards_movement` requirements.

The one insertion point into core CALDERA is the adversary profile:

```
upload report(s) -> clean -> IR -> ATT&CK techniques -> STIX bundle
|
"build an adversary from this and run it" v
build_adversary attack-pattern -> ability.technique_id -> Adversary
+ unmatched_techniques + platform_excluded
run_operation Adversary + your agents + runtime-discovered facts
```

### Known limits

- **Extraction quality.** Scored against the committed measuring stick with
`tests/test_pipeline_score.py`: precision 0.83, F1 0.89. That fixture names
its technique ids explicitly, so its recall is an upper bound, not a field
estimate. Recall on prose-only reports is materially lower, and fusing
several reports on the same actor is the cheapest way to raise it.
- **No detection scoring.** The `detections` plugin does not ship in this
repository, so nothing here scores an operation against SIEM rules.
- **Platform matching is coarse.** `build_adversary` knows which platforms
your agents run, not whether a Windows agent is domain-joined. It reports
the gap in `platform_excluded` rather than closing it.
- **Fusion does not check identity.** Fusing bundles for two different
actors merges them into one profile without complaint.
- **Operations are not agent-scoped.** `run_operation` runs against the whole
`red` group; the `agent_paws` argument is not honoured by core's operation
schema.

## Installation

Expand All @@ -79,7 +115,7 @@ Install MCP like a standard CALDERA plugin.
1. Clone or copy this repository into the CALDERA plugin directory:

```bash
git clone https://gitlab.mitre.org/caldera/caldera-mcp.git plugins/mcp
git clone https://github.com/mitre/mcp.git plugins/mcp
```

2. Add `mcp` to your CALDERA plugin list in `conf/local.yml`. Keep local configuration in `conf/local.yml`; do not commit that file.
Expand All @@ -92,25 +128,39 @@ plugins:
- mcp
```

3. Optional integrations can be enabled by adding their plugins as well:
3. Install plugin requirements from the CALDERA virtual environment:

```yaml
plugins:
- magma
- sandcat
- stockpile
- detections
- mcp
```bash
source venv/bin/activate
pip install -r plugins/mcp/requirements.txt
```

4. Install plugin requirements from the CALDERA virtual environment:
4. Install the spaCy model. pip cannot do this from `requirements.txt`:

```bash
source venv/bin/activate
pip install -r plugins/mcp/requirements.txt
python -m spacy download en_core_web_lg
```

Install poppler as well, which supplies `pdftotext`. Without it the
pipeline still ingests TXT, MD and HTML, and PDF uploads report the
missing dependency rather than failing silently:

```bash
brew install poppler # macOS
apt install poppler-utils # Debian/Ubuntu
```

5. Copy `.env.example` to `.env` and set the LLM endpoint and credential. Both are required; nothing ships in `conf/default.yml`.
To run the test suite, also install the dev requirements:

```bash
pip install -r plugins/mcp/requirements-dev.txt
```

5. Copy `.env.example` to `.env` and set the LLM endpoint and credential.
Both are optional: with neither set, IR extraction falls back to a
deterministic offline extractor. That extractor trades roughly 15 to 20
percent of recall for sub-second processing, so configure a model when
extraction quality matters.

```bash
cp plugins/mcp/.env.example plugins/mcp/.env
Expand Down Expand Up @@ -164,12 +214,14 @@ Use Upload CTI to stage raw reports, run CTI extraction, and produce STIX 2.1 bu
Use Plan and Execute for CTI-driven operations. The workflow can:

- Read selected STIX bundles and CTI/RAG context.
- Extract hosts, operating systems, domains, users, and software.
- Turn those into a CALDERA fact source so operations run on values the report named.
- Build and run the CALDERA operation.
- Summarize operator-review gaps and detection coverage.
- Fuse several bundles describing the same actor into one.
- Build an adversary from the observed techniques, previewing it first.
- Run the CALDERA operation against agents that have checked in.
- Report which techniques had no matching ability, and which had one that
no live agent can run.

The workflow should report missing evidence rather than inventing hosts, users, domains, or services.
The CTI names techniques, not your estate. The workflow reports a gap
rather than answering a question about your environment from a report.

## Configuration

Expand All @@ -184,28 +236,32 @@ MCP supports saved endpoint profiles in the UI. Profiles can carry:
- Maximum tool calls
- Maximum tokens

CTI/RAG model settings can use the chat endpoint by default or a separate saved profile when CTI extraction benefits from a different model.
CTI and RAG share the chat endpoint. Extraction differs from chat only in its generation settings, which the CTI Extraction Model panel edits.

### Local Settings

Prefer environment variables for local values, and keep `conf/default.yml` limited to safe defaults.

To pin values on disk instead, set them in `conf/local.yml`. That file is overlaid onto `conf/default.yml` key by key, so it only needs the keys it changes.

The `llm` block sits at the top level of the file:
To pin values on disk instead, create `conf/local.yml`. That file is a **sparse overlay**, not a full config: it is deep-merged onto `conf/default.yml` key by key, so it needs only the keys it changes. A file containing one key is valid and complete.

```yaml
llm:
model: openai/gpt-oss-120b
api_base: https://api.example.com/v1
api_base_env: MCP_LLM_API_BASE
api_key_env: MCP_LLM_API_KEY
temperature: 0.5
max_tool_calls: 5
max_tokens: 24000
offline: false

cti:
timeout: 120
offline: false
```

A value set here outranks the environment variable named beside it, so leave `api_base` empty to keep resolving it from `MCP_LLM_API_BASE`. The `cti` block takes the same shape and may name a different variable if CTI extraction should reach a separate endpoint.
Three rules that are easy to trip over:

- **`cti` layers over `llm`, but only for its own generation settings.** A workload profile may set `top_p`, `timeout`, `stream`, `offline` and `embed_model`. Everything else belongs to `llm`: the connection (`provider`, `model`, `api_base`, credentials, `ssl_verify`), and `temperature` and `max_tokens`, which two panels display and so are stored once. Anything else under `cti` is dropped and logged, and `set_config` rejects it outright.
- **Precedence differs by field.** `api_key` resolves environment-first and is never read from this file. `api_base` resolves yaml-first, so a value here beats `MCP_LLM_API_BASE`; leave it empty to keep using the variable.
- **A key with no value is `null`, and null overrides.** A bare `llm:` heading wipes the shipped block rather than falling through to it. Omit the block instead.

The GUI's **Global Model Config** panel writes the connection to the `llm` profile in `conf/local.yml`, so it configures every workload including CTI extraction. Only fields you change are written, so a value still coming from `MCP_LLM_API_BASE` is left alone rather than pinned to disk. The API key is the exception: it stays in `localStorage` and rides each request, and `set_config` strips credentials before writing.

## API Surface

Expand All @@ -220,6 +276,10 @@ The plugin exposes MCP workflow APIs through CALDERA's aiohttp server. The UI us
- Run CTI pipeline steps.
- Read run history and transcripts.

The CTI pipeline MCP server exposes five tools: `cti_pipeline_ingest_cti`,
`cti_pipeline_fuse`, `cti_pipeline_build_adversary`,
`cti_pipeline_run_operation` and `cti_pipeline_wait_for_agents`.

When adding new UI functionality, prefer extending the existing MCP API routes instead of creating separate side channels.

## Architecture
Expand Down
47 changes: 26 additions & 21 deletions app/capabilities/rag.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __init__(
if stix_bundle_path:
self.load_stix_bundle(stix_bundle_path)

def load_stix_bundle(self, stix_bundle_path: str, embed_model: str = 'openai/text-embedding-3-small'):
def load_stix_bundle(self, stix_bundle_path: str, embed_model: str | None = None):
"""Load STIX bundle from file path and build embeddings."""
try:
with open(stix_bundle_path, 'r') as f:
Expand All @@ -45,7 +45,7 @@ def load_stix_bundle(self, stix_bundle_path: str, embed_model: str = 'openai/tex
except json.JSONDecodeError:
raise ValueError(f"Invalid JSON in STIX bundle: {stix_bundle_path}")

def initialize_from_bundles(self, stix_bundles: List[dict], embed_model: str = 'openai/text-embedding-3-small'):
def initialize_from_bundles(self, stix_bundles: List[dict], embed_model: str | None = None):
"""Initialize the RAG service with multiple STIX bundles and create retriever."""
all_corpus = []
all_adv_step = {}
Expand All @@ -56,7 +56,19 @@ def initialize_from_bundles(self, stix_bundles: List[dict], embed_model: str = '

self.corpus = all_corpus
self.adv_step = all_adv_step


if not self.corpus:
# Embeddings() raises numpy AxisError on an empty corpus, and the
# caller swallows it and silently runs without CTI grounding.
# Bundles with no attack-pattern and no threat-actor are the
# normal way to get here. search_cti_title handles search=None.
self.search = None
self.log.warning(
"[RAG] No attack-pattern or threat-actor objects in the "
"selected STIX; CTI retrieval is unavailable for this run"
)
return

self.log.info("Initializing embeddings and retriever for STIX corpus")
embedder_kwargs = {"api_key": self.api_key}
if self.api_base:
Expand All @@ -77,10 +89,7 @@ def extract_text_chunks(self, stix_bundle: dict) -> tuple[List[str], Dict[str, s
adv_step = {}

for obj in stix_bundle.get("objects", []):
if obj.get("type") in [
"attack-pattern", "malware", "tool", "threat-actor",
"intrusion-set", "identity", "indicator", "report"
]:
if obj.get("type") in ["attack-pattern", "threat-actor"]:
name = obj.get("name", "")
description = obj.get("description", "")

Expand Down Expand Up @@ -160,18 +169,7 @@ def get_context_for_task(self, task: str) -> Dict[str, any]:
}


# Legacy functions for backward compatibility
def search_cti_title(query: str) -> list[str]:
"""Legacy function - use RAGService instead."""
if 'global_rag_service' in globals():
return global_rag_service.search_cti_title(query)
return ["RAG service not initialized"]

def search_cti_data_by_title(name: str) -> str:
"""Legacy function - use RAGService instead."""
if 'global_rag_service' in globals():
return global_rag_service.search_cti_data_by_title(name)
return "RAG service not initialized"


# ---------------------------------------------------------------------------
Expand All @@ -184,7 +182,6 @@ def search_cti_data_by_title(name: str) -> str:
# capability registry, this declaration is dormant; the legacy code path in
# mcp_svc still wires RAG into runs directly.

import os
import asyncio
from pathlib import Path

Expand Down Expand Up @@ -253,7 +250,7 @@ async def _enrich(prompt: str, settings: dict) -> dict:
settings keys (all optional):
rag_files list of filenames under plugins/mcp/data/
topk int, defaults to 5
embed_model str, defaults to openai/text-embedding-3-small
embed_model str, falls back to the chat model when unset
api_key str, embedding key fallback (mcp_svc fills this with
the resolved chat-LLM key when the caller does not
override it via embed_api_key)
Expand All @@ -271,7 +268,15 @@ async def _enrich(prompt: str, settings: dict) -> dict:
embed_api_key = settings.get("embed_api_key") or settings.get("api_key") or ""
embed_api_base = settings.get("embed_api_base") or settings.get("api_base") or None
ssl_verify = settings.get("ssl_verify")
embed_model = settings.get("embed_model") or "openai/text-embedding-3-small"
# An undeclared embedding model falls back to the chat model. A deployment
# pointing at its own gateway has no reason to reach for an OpenAI model
# name it never chose, and that name would be sent to that gateway.
embed_model = settings.get("embed_model") or settings.get("model")
if not embed_model:
raise ValueError(
"No embedding model: set embed_model, or a model on the global "
"LLM profile for it to inherit."
)
topk = int(settings.get("topk") or 5)

base_dir = Path(__file__).resolve().parent.parent.parent / "data"
Expand Down
42 changes: 21 additions & 21 deletions app/cti_ingest_svc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,21 @@
Logging is preserved verbatim for debug capture via:
python3 cti_ingest_svc.py --base-dir data --step all 2>&1 | tee -a debug_mitre.log

| Step | Description |
| ------------- | -------------------------------------- |
| Raw → Clean | Normalize source material |
| Clean → IR | LLM extraction of entities & behaviors |
| NLP Layers | Improve IR quality |
| MITRE | Map behaviors to ATT&CK |
| Relationships | Build entity graph |
| Validation | Ensure graph correctness |
| Outputs | Persist IR + summary |
| Scenario | (Optional) Narrative generation |
| Step | Description |
| ------------ | ---------------------------------------------- |
| raw-to-clean | Normalize source material |
| debug-ir | Stop after IR extraction |
| stage2 | IR -> STIX bundle only |
| all | Everything, in order |

'''

import argparse
import asyncio
import json, re
import shutil
from enum import Enum
from pathlib import Path

from plugins.mcp.app.cti_pipeline_stage1 import (
ensure_dirs,
step_raw_to_clean,
step_parse_to_ir
)
Expand Down Expand Up @@ -70,20 +63,14 @@ def run_stage(self, base_dir: Path, step: str):
step_raw_to_clean(base_dir)

case "debug-ir":
step_parse_to_ir(base_dir, stop_after="ir")

case "debug-mitre":
step_parse_to_ir(base_dir, stop_after="mitre")

case "debug-relationships":
step_parse_to_ir(base_dir, stop_after="relationships")
step_parse_to_ir(base_dir, stop_after="ir", only=self.selected)

case "stage2":
self.run_stage2(base_dir)

case "all":
step_raw_to_clean(base_dir)
step_parse_to_ir(base_dir)
step_parse_to_ir(base_dir, only=self.selected)

# Only run Stage 2 if IR exists
ir_dir = base_dir / "outputs_ir" / "complete"
Expand Down Expand Up @@ -121,12 +108,25 @@ def status(self):
}

def finalize_run(self, base_dir: Path, selected: list[str]):
"""Retire the reports this run actually processed.

This moved everything in uploads/ and never read its own argument, so
a report the operator did not select was retired having never been
cleaned or extracted. list_cti_raw stamps a literal "processed" on
that directory, so it then rendered green.

An empty selection still means everything, matching the convention
step_parse_to_ir uses for the same reason.
"""
uploads = base_dir / "raw" / "uploads"
processed = base_dir / "raw" / "processed"

processed.mkdir(parents=True, exist_ok=True)

names = set(selected or [])
for f in uploads.iterdir():
if names and f.name not in names:
continue
shutil.move(str(f), processed / f.name)

@staticmethod
Expand Down
Loading
Loading