[Threat Intel 1/11] Gate enrichment completion on extract_iocs, drop detection_actionability - #287197
Conversation
…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>
|
Pinging @elastic/security-threat-hunting (Team:Threat Hunting) |
|
Pinging @elastic/security-solution (Team: SecuritySolution) |
|
@elasticmachine merge upstream |
kibanamachine
left a comment
There was a problem hiding this comment.
Libra found 1 issue.
Generated by Libra
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_actionabilityis 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
left a comment
There was a problem hiding this comment.
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>
|
Thanks @jonwalstedt. Coverage added in 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 |
💛 Build succeeded, but was flaky
Failed CI StepsMetrics [docs]
Test Failures
History
|
…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.
…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.
…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>
…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>
Two corrections to threat-intel work that already merged, ahead of the supply pipeline that consumes it.
extract_iocsruns withon-failure: continue, but the completion gate only probedclassify_severity.errorandenrich_taxonomy.error. So a transient IOC-route failure still wroteextraction_method: workflow_v2, which is whatload_pending_reportsfilters 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 haveextracted.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_iocsbelongs 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_actionabilityfrom 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: relevance0.0,diamond_suitablefalse, 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
#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.