Skip to content

[Threat Intel 1/11] Gate enrichment completion on extract_iocs, drop detection_actionability - #287197

Merged
stephmilovic merged 3 commits into
elastic:mainfrom
stephmilovic:threat-intel-1-workflow-gate
Aug 26, 2026
Merged

[Threat Intel 1/11] Gate enrichment completion on extract_iocs, drop detection_actionability#287197
stephmilovic merged 3 commits into
elastic:mainfrom
stephmilovic:threat-intel-1-workflow-gate

Conversation

@stephmilovic

@stephmilovic stephmilovic commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Two corrections to threat-intel work that already merged, ahead of the supply pipeline that consumes it.

extract_iocs runs with on-failure: continue, but the completion gate only probed classify_severity.error and enrich_taxonomy.error. So a transient IOC-route failure still wrote extraction_method: workflow_v2, which is what load_pending_reports filters on, and the report was never looked at again. It stays permanently enriched with no IOCs, and since the promote task only scans reports that have extracted.iocs, it contributes nothing to the indicator index ever. A rolling restart, a request timeout, or a body over the 5MB route bound gets you there.

extract_iocs belongs in the probe set because it is the step whose output the rest of the pipeline actually consumes. Moving the gate from payload-presence to step health was right, it just left out the step that matters most downstream.

Also removed detection_actionability from taxonomy enrichment. It scored how useful a report is for writing a detection rule, which is Detection Watch's call (Dark Watch D20), and nothing downstream consumed it. The completeness gate was its only reader, and it was using field presence as a stand-in for step health, which does not work here because every taxonomy field has a legitimate falsy value: relevance 0.0, diamond_suitable false, empty category and region arrays.

The generator fixtures come along because they carried the same field, plus the reports index rename from the earlier data-stream design.

No workflow version bump: these workflows are not installed anywhere yet.

Needs @elastic/workflows-eng since it touches enrich_threat_report.yaml. It is the only PR in the series that does.

Where this sits

# PR Depends on Status
1 #287197 workflow gate correction + fixtures ← this PR nothing ✅ merged
2 #287198 contracts, constants, shared libs nothing ✅ merged
3 #287199 content parsing #287198 closed
4 #287200 SSRF-guarded HTTP client #287198 ✅ merged
5 #287201 index templates, seeding, inference features #287198, #287200 ✅ merged
6 #287202 indicator alias #287198, #287201 ✅ merged
7 #287203 IOC extraction #287198, #287200 ✅ merged
8 #287204 LLM services, routes, source catalog API #287198, #287200, #287202, #287203 ✅ merged
9 #287205 RSS adapter #287198, #287200, #287203, #287204 ✅ merged
10 #287206 remaining adapters, dispatcher, fetch_source step #287198, #287200, #287203, #287205 ✅ merged
11 #287207 promote/scrub tasks and plugin wiring #287198#287206 ✅ merged
12 #287479 generator fixture article URLs nothing ✅ merged

#287479 came first in the merge train and is already on main. The numbered series (#287197#287207) stacks on top in dependency order. #287199 (content parsing) was closed; IOC extraction ships its own section-header classifier.

…etection_actionability

Two corrections to already-merged threat-intel work, ahead of the supply pipeline
that consumes it.

`extract_iocs` runs with `on-failure: continue`, but the completion gate only
probed `classify_severity.error` and `enrich_taxonomy.error`. A transient
IOC-route failure therefore still wrote `extraction_method: workflow_v2`, which is
what `load_pending_reports` filters on, so the report was never looked at again.
It stays permanently enriched with no IOCs, and since the promote task only scans
reports that have `extracted.iocs`, it contributes nothing to the indicator index
ever. A rolling restart, a request timeout, or a body over the 5MB route bound is
enough to get there. `extract_iocs` belongs in the probe set because it is the
step whose output the rest of the pipeline actually consumes.

Also removed `detection_actionability` from the taxonomy output. It scored how
useful a report is for writing a detection rule, which is Detection Watch's call,
and nothing downstream consumed it. The completeness gate was the only reader, and
it was using field presence as a stand-in for step health, which is unsound here
because every taxonomy field has a legitimate falsy value.

The generator fixtures come along because they carried the same field, plus the
reports index rename from the earlier data-stream design.

No workflow version bump: these workflows are not installed anywhere yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@stephmilovic stephmilovic added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting Team:Threat Hunting Security Solution Threat Hunting Team Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. Team:One Workflow Team label for One Workflow (Workflow automation) v9.6.0 labels Aug 25, 2026
@stephmilovic
stephmilovic marked this pull request as ready for review August 25, 2026 22:33
@stephmilovic
stephmilovic requested review from a team as code owners August 25, 2026 22:33
@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

Pinging @elastic/security-threat-hunting (Team:Threat Hunting)

@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

Pinging @elastic/security-solution (Team: SecuritySolution)

@stephmilovic

Copy link
Copy Markdown
Contributor Author

@elasticmachine merge upstream

@jonwalstedt jonwalstedt added the reviewer:libra PR review with Libra. This disables Claude and Scout reviewers label Aug 26, 2026

@kibanamachine kibanamachine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Libra found 1 issue.

Generated by Libra

Comment on lines 196 to 203
with:
extraction_method: workflow_v2

- name: mark_llm_enrich_incomplete
type: data.set
if: "NOT steps.classify_severity.output.level : * OR NOT steps.enrich_taxonomy.output.detection_actionability : *"
if: "${{ steps.extract_iocs.error != null or steps.classify_severity.error != null or steps.enrich_taxonomy.error != null }}"
with:
extraction_method: pending

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regression-critical failure gate is changed without an automated workflow test that exercises it. The only test changes in this PR remove fixture assertions, so there is no coverage proving that an extract_iocs step failure leaves extraction_method as pending (or that all-success marks workflow_v2); a future expression or workflow-engine change could silently reintroduce the permanently skipped-report failure this PR is fixing.

Add a workflow execution test covering at least the all-success case and each continued step-error case, asserting the persisted lineage.extraction_method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added coverage in enrich_threat_report.test.ts.

One honest caveat on what it is: this package has no workflow-execution harness, so it is not an execution test. It follows the alert_analysis_workflow.test.ts precedent and parses the shipped yaml. So it cannot prove how the engine evaluates the expression at run time.

What it does pin is the structure the fix depends on, which is where the bug actually lived:

  • all three continued steps are probed in the complete gate (steps.<step>.error == null)
  • all three are probed in the incomplete gate (!= null)
  • the two gates stay complementary, so updating one and not the other fails
  • all three steps still carry on-failure: continue, without which a failure aborts before either gate is reached and the whole change is unreachable
  • detection_actionability is gone

I checked it bites rather than assuming: reverting the gate to the previous two-step form fails 2 of the 13 tests (requires extract_iocs to have succeeded before marking complete and keeps the two gates complementary).

Runtime evaluation of the gate is genuinely untested, and I would rather say so than imply otherwise. It needs a workflow-execution harness, which does not exist in kbn-workflows today and is more than this two-file PR should carry. Tracking it for the follow-up read PR, which already owns install and space-isolation testing.

@jonwalstedt jonwalstedt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good except for the missing test coverage for the extract_iocs step failure that Libra already commented on, add the missing coverage and it is good to go.,

The gate change in this PR had no coverage, so a future expression or
workflow-engine change could silently reintroduce the permanently skipped report.
Pins that all three continued steps are probed, that the complete and incomplete
gates stay complementary, and that all three still use on-failure continue, which
is what makes the gate reachable. Reverting the gate fails this test.

Structural rather than execution: this package has no workflow-execution harness,
so it follows the alert_analysis_workflow.test.ts precedent of parsing the shipped
yaml. It does not prove how the engine evaluates the expression at run time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@stephmilovic

Copy link
Copy Markdown
Contributor Author

Thanks @jonwalstedt. Coverage added in enrich_threat_report.test.ts, details in the thread above.

Short version: it is a yaml-structure test, not an execution test, because this package has no workflow-execution harness. It pins that all three continued steps are probed by both gates and that they still use on-failure: continue, and I verified it fails if the gate is reverted. Runtime evaluation of the expression is still untested and I called that out explicitly rather than let the coverage look stronger than it is.

@kibanamachine

Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Metrics [docs]

✅ unchanged

Test Failures

  • [job] [logs] FTR Configs #116 / Reporting Functional Tests with Security enabled Access to Management > Reporting non privilege user does not allow user that does not have reporting privileges
  • [job] [logs] FTR Configs #116 / Reporting Functional Tests with Security enabled Access to Management > Reporting non privilege user does not allow user to access schedules that does not have reporting privileges

History

@stephmilovic
stephmilovic merged commit c843e83 into elastic:main Aug 26, 2026
19 of 20 checks passed
smith pushed a commit to Dosant/kibana that referenced this pull request Aug 27, 2026
…detection_actionability (elastic#287197)

First of 11 PRs replacing elastic#285795, which was +20k lines in one diff.

`extract_iocs` runs with `on-failure: continue`, but the enrichment completion gate
only probed `classify_severity.error` and `enrich_taxonomy.error`. A transient
IOC-route failure therefore still wrote `extraction_method: workflow_v2`, which is
what `load_pending_reports` filters on, so the report was never revisited. It stays
permanently enriched with no IOCs, and since the promote task only scans reports
that have `extracted.iocs`, it contributes nothing to the indicator index ever.

Also removed `detection_actionability` from taxonomy enrichment. It scored how
useful a report is for writing a detection rule, which is Detection Watch's call,
and nothing downstream consumed it.

Adds yaml coverage for the gate: all three continued steps are probed by both
gates, the gates stay complementary, and all three still use `on-failure:
continue`. Structural rather than execution, since kbn-workflows has no
execution harness.
dej611 pushed a commit to dej611/kibana that referenced this pull request Aug 31, 2026
…detection_actionability (elastic#287197)

First of 11 PRs replacing elastic#285795, which was +20k lines in one diff.

`extract_iocs` runs with `on-failure: continue`, but the enrichment completion gate
only probed `classify_severity.error` and `enrich_taxonomy.error`. A transient
IOC-route failure therefore still wrote `extraction_method: workflow_v2`, which is
what `load_pending_reports` filters on, so the report was never revisited. It stays
permanently enriched with no IOCs, and since the promote task only scans reports
that have `extracted.iocs`, it contributes nothing to the indicator index ever.

Also removed `detection_actionability` from taxonomy enrichment. It scored how
useful a report is for writing a detection rule, which is Detection Watch's call,
and nothing downstream consumed it.

Adds yaml coverage for the gate: all three continued steps are probed by both
gates, the gates stay complementary, and all three still use `on-failure:
continue`. Structural rather than execution, since kbn-workflows has no
execution harness.
stephmilovic added a commit that referenced this pull request Sep 4, 2026
…og API (#287204)

## Summary

Adds LLM enrichment services and Threat Intelligence HTTP routes,
including the fixed-catalog source list and enable/disable API. Manual
report creation and provenance URLs go through the shared HTTP/HTTPS
normalizer.

Also wires the routes, inference features, and one-time bootstrap into
`plugin.ts`, gated by `threatIntelSupplyEnabled`. This wiring is
interim: it exists so the enrichment eval suite (see Evaluation below)
has routes to call ahead of the full pipeline wiring. `#287207` replaces
it with a proper `wiring.ts` module; that PR's description has a rebase
note covering the `plugin.ts` conflict resolution.

## Where this sits

| # | PR | Depends on | Status |
|---|---|---|---|
| 1 | #287197 workflow gate correction + fixtures | nothing | ✅ merged |
| 2 | #287198 contracts, constants, shared libs | nothing | ✅ merged |
| 3 | #287199 content parsing | #287198 | closed |
| 4 | #287200 SSRF-guarded HTTP client | #287198 | ✅ merged |
| 5 | #287201 index templates, seeding, inference features | #287198,
#287200 | ✅ merged |
| 6 | #287202 indicator alias | #287198, #287201 | ✅ merged |
| 7 | #287203 IOC extraction | #287198, #287200 | ✅ merged |
| 8 | #287204 LLM services, routes, source catalog API **← this PR** |
#287198, #287200, #287202, #287203 | 👀 ready |
| 9 | #287205 RSS adapter | #287198, #287200, #287203, #287204 | draft |
| 10 | #287206 remaining adapters, dispatcher, fetch_source step |
#287198, #287200, #287203, #287205 | draft |
| 11 | #287207 promote/scrub tasks and plugin wiring | #287198#287206 |
draft |
| 12 | #287479 generator fixture article URLs | nothing | ✅ merged |
| 13 | #289343 Scout API tests for the deterministic routes | #287204 |
draft |
| 14 | #289345 enrichment eval suite | #287204 | draft |


#287479 came first in the merge train and is already on `main`. The
numbered series (#287197#287207) stacks on top in dependency order.
#287199 (content parsing) was closed; IOC extraction ships its own
section-header classifier.

## Scope review follow-up

- Shared `normalizeProvenanceUrl` helper (HTTP/HTTPS only, strips
credentials, bounded length).
- Source create/delete routes removed. Updates may change only
`enabled`.
- Lists and mutations reject catalog IDs outside the approved set.
- Ensures the per-space indicator alias on `list_sources` (idempotent).

## Evaluation

A `@kbn/evals` suite exercises all four enrichment stages against the
BlackHat demo pack article text, posting each input directly to the
internal route and scoring the structured response. It runs as a tracked
baseline (scores recorded, no build-failing thresholds yet). Latest full
run, EIS Claude Sonnet 4.6 as model and judge, 1 repetition, 20
examples:

| Stage | Evaluator | Score |
|---|---|---|
| assess_relevance | IsIntelligenceMatch | 1.00 |
| assess_relevance | RelevanceShapeValid | 1.00 |
| classify_severity | SeverityExactMatch | 0.83 |
| classify_severity | SeverityWithinOneLevel | 1.00 |
| enrich_taxonomy | CategoryRecall | 0.80 |
| enrich_taxonomy | RegionRecall | 1.00 |
| extract_diamond | DiamondNoIocLeak | 1.00 |
| extract_diamond | DiamondSignalCount | 1.00 |
| extract_diamond | criteria (LLM judge, majority vote) | 1.00 |

The suite ships in #289345, a separate eval-only PR that stacks on this
one (it needs the routes and the flag-gated wiring added here). The four
deterministic routes are covered separately by Scout API tests in
#289343. Any prompt or calibration fix the evals surface lands by
amending this PR, not the eval PR.

## To test

```
node scripts/jest --config x-pack/solutions/security/plugins/security_solution/server/threat_intel/jest.config.js routes/list_sources.test.ts services/provenance_url.test.ts
```

_PR developed with Cursor + Auto_

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Jon Wålstedt <jon.walstedt@elastic.co>
Co-authored-by: Cursor <cursoragent@cursor.com>
jonwalstedt added a commit that referenced this pull request Sep 11, 2026
…d the flag (#287207)

## Summary

Wires Threat Intelligence behind `threatIntelSupplyEnabled`: bootstrap,
managed workflow installation, promote/scrub tasks, and route
registration. Promote mirrors extracted IOCs into
`.threat-intel-indicators` for Indicator Match rules.

## Where this sits

| # | PR | Depends on | Status |
|---|---|---|---|
| 1 | #287197 workflow gate correction + fixtures | nothing | ✅ merged |
| 2 | #287198 contracts, constants, shared libs | nothing | ✅ merged |
| 3 | #287199 content parsing | #287198 | closed |
| 4 | #287200 SSRF-guarded HTTP client | #287198 | ✅ merged |
| 5 | #287201 index templates, seeding, inference features | #287198,
#287200 | ✅ merged |
| 6 | #287202 indicator alias | #287198, #287201 | ✅ merged |
| 7 | #287203 IOC extraction | #287198, #287200 | ✅ merged |
| 8 | #287204 LLM services, routes, source catalog API | #287198,
#287200, #287202, #287203 | ✅ merged |
| 9 | #287205 RSS adapter | #287198, #287200, #287203, #287204 | ✅
merged |
| 10 | #287206 remaining adapters, dispatcher, fetch_source step |
#287198, #287200, #287203, #287205 | ✅ merged |
| 11 | #287207 promote/scrub tasks and plugin wiring **← this PR** |
#287198#287206 | ready for review |
| 12 | #287479 generator fixture article URLs | nothing | ✅ merged |
| 13 | #289343 Scout API tests for the deterministic routes | #287204 |
ready for review |
| 14 | #289345 enrichment eval suite | #287204 | ready for review |
| 15 | #290144 read APIs, readiness, space-keyed attribution | #287207 |
draft |


#287479 came first in the merge train and is already on `main`. The
numbered series (#287197#287205) is merged; #287206 and #287207 now sit
directly on `main`. #287199 (content parsing) was closed; IOC extraction
ships its own section-header classifier.

## Scope review follow-up

- Installs the managed workflows with the right space topology:
`attribute_alerts_to_reports` installs per space (it queries
`.alerts-security.alerts-*` and writes per-space hit totals, so a global
install would clobber every space's totals onto one shared doc), while
`ingest_threat_feeds` and `enrich_threat_report` install once globally.
Alert analysis and threat intel share a single `ready()` call so neither
install set is dropped, and spaces created after boot are reconciled on
the promote task. All three ship `enabled: false`, and enrich routes its
HTTP calls through a fixed space (`default`) instead of the install-time
`workflow.spaceId` (which is `'*'` globally).
- Ensures the default-space indicator alias on start.
- Chunks promotion bulk writes and treats HTTP 408/500 as retryable bulk
failures.
- Sanitizes provenance and extracted IOC reference URLs during
promotion.
- Documents direct-index cross-space isolation as the blocker to
enabling the feature.

## Bootstrap fix folded in from #289343

`seed_default_sources.ts` sorted the legacy-source disable scan on
`_id`, which Elasticsearch rejects with `illegal_argument_exception:
Fielddata access on the _id field is disallowed` unless
`indices.id_field_data.enabled` is set, so it failed bootstrap on a
stock cluster. Found this while getting #289343's Scout suite green and
moved the fix here since this PR is the one wiring up bootstrap and
already needs `security-threat-hunting` review, so it isn't adding a
reviewer #289343 wouldn't otherwise need.

## Product boundary

`threatIntelSupplyEnabled` defaults to false. Do not enable until
direct-index cross-space isolation is hardened or the administrator
trust model is explicitly accepted.

## To test

```
node scripts/jest --config x-pack/solutions/security/plugins/security_solution/server/threat_intel/jest.config.js wiring.test.ts tasks/promote_threat_indicators.test.ts
node scripts/jest x-pack/solutions/security/plugins/security_solution/server/workflows/
node scripts/jest src/platform/packages/shared/kbn-workflows/managed/definitions/threat_intel/
```

_PR developed with Cursor + Auto + Sonnet 5 + Opus 4.8_

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Jon Wålstedt <jon.walstedt@elastic.co>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting release_note:skip Skip the PR/issue when compiling release notes reviewer:libra PR review with Libra. This disables Claude and Scout reviewers Team:One Workflow Team label for One Workflow (Workflow automation) Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. Team:Threat Hunting Security Solution Threat Hunting Team v9.6.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants