-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[Threat Intel 10/11] Remaining adapters, dispatcher, and fetch_source step #287206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
stephmilovic
merged 29 commits into
elastic:main
from
stephmilovic:threat-intel-8-adapters-rest
Sep 9, 2026
Merged
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
3c14ba6
feat(threat-intel): RSS, STIX, and TAXII source adapters
stephmilovic d5891e7
fix(threat-intel): redact feed URLs in the remaining adapter log lines
stephmilovic 6b62139
fix(threat-intel): redact the last raw feed URL in the taxii adapter
stephmilovic 5e10320
docs(threat-intel): correct why the scoped client cannot reach the pl…
stephmilovic 0137dfb
feat(threat-intel): resolve Atom/RSS namespace and type semantics in …
stephmilovic 57b1b4c
refactor(threat-intel): trim comment verbosity in parse_rss and rss_a…
stephmilovic 7eea22f
docs(threat-intel): note why the indicator index sits outside `.kibana-`
stephmilovic 158436e
refactor(threat-intel): convert RSS markup to bounded text, drop HTML…
jonwalstedt 8325c7d
Narrow ingest adapters to structured RSS
stephmilovic 5dd6ba0
Narrow the fetch adapter source contract
stephmilovic c2af0e7
Exclude hidden RSS fragment content
stephmilovic c309e6c
fix(threat-intel): resolve RSS adapter URLs from the code catalog
stephmilovic eabdc63
feat(threat-intel): remaining adapters, the dispatcher, and the fetch…
stephmilovic f3f69c0
fix(threat-intel): redact feed URLs and validate every KEV field the …
stephmilovic 2e65093
refactor(threat-intel): keep REST adapters catalog-only, drop content…
jonwalstedt 53c2ab4
Remove unreachable threat intel adapters
stephmilovic f27260f
Remove dead text-list reference metadata
stephmilovic 996c632
Remove dead KEV reference metadata
stephmilovic ad9d322
Satisfy text-list chunking lint
stephmilovic 7208883
Remove dead text-list reference classification
stephmilovic 3f61825
Address scope-review findings from deep PR review
stephmilovic 3edf6ea
fix(threat-intel): resolve KEV and text-list adapter URLs from the co…
stephmilovic 5813f23
Deslop: drop decorative section banners and a no-op comment
stephmilovic 86cf8c1
Fix prettier formatting in the indicator-list adapter tests
stephmilovic 0995b58
fix(threat-intel): report a KEV shape failure as a shape failure
stephmilovic 0b556ab
Merge branch 'main' into threat-intel-8-adapters-rest
elasticmachine 97c0101
Address review: KEV optional-field type guard, source name, tierBasis…
stephmilovic 5627e13
Changes from node scripts/check
kibanamachine b0cf683
Merge remote-tracking branch 'upstream/main' into threat-intel-8-adap…
stephmilovic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
19 changes: 19 additions & 0 deletions
19
x-pack/solutions/security/plugins/security_solution/public/threat_intel/jest.config.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| module.exports = { | ||
| preset: '@kbn/test', | ||
| rootDir: '../../../../../../..', | ||
| roots: ['<rootDir>/x-pack/solutions/security/plugins/security_solution/public/threat_intel'], | ||
| coverageDirectory: | ||
| '<rootDir>/target/kibana-coverage/jest/x-pack/solutions/security/plugins/security_solution/public/threat_intel', | ||
| coverageReporters: ['text', 'html'], | ||
| collectCoverageFrom: [ | ||
| '<rootDir>/x-pack/solutions/security/plugins/security_solution/public/threat_intel/**/*.{ts,tsx}', | ||
| ], | ||
| moduleNameMapper: require('../../server/__mocks__/module_name_map'), | ||
| }; |
24 changes: 24 additions & 0 deletions
24
...urity_solution/public/threat_intel/workflows/step_types/fetch_source/fetch_source_step.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| import React from 'react'; | ||
| import type { PublicStepDefinition } from '@kbn/workflows-extensions/public'; | ||
| import { fetchSourceStepCommonDefinition } from '../../../../../common/threat_intel/workflows/step_types/fetch_source/fetch_source_common'; | ||
|
|
||
| /** YAML editor schema for threat_intel.fetch_source (handler is server-side). */ | ||
| export const fetchSourceStepDefinition: PublicStepDefinition = { | ||
| ...fetchSourceStepCommonDefinition, | ||
| icon: React.lazy(() => | ||
| import('@elastic/eui/es/components/icon/assets/download') | ||
| .then(({ icon }) => ({ default: icon })) | ||
| .catch(() => | ||
| import('@elastic/eui/es/components/icon/assets/globe').then(({ icon }) => ({ | ||
| default: icon, | ||
| })) | ||
| ) | ||
| ), | ||
| }; | ||
8 changes: 8 additions & 0 deletions
8
.../plugins/security_solution/public/threat_intel/workflows/step_types/fetch_source/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| export { fetchSourceStepDefinition } from './fetch_source_step'; |
8 changes: 8 additions & 0 deletions
8
...ions/security/plugins/security_solution/public/threat_intel/workflows/step_types/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| export { registerThreatIntelWorkflowSteps } from './register_workflow_steps'; |
40 changes: 40 additions & 0 deletions
40
...ecurity_solution/public/threat_intel/workflows/step_types/register_workflow_steps.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| import type { PublicStepDefinition } from '@kbn/workflows-extensions/public'; | ||
| import { workflowsExtensionsMock } from '@kbn/workflows-extensions/public/mocks'; | ||
| import { registerThreatIntelWorkflowSteps } from './register_workflow_steps'; | ||
| import { fetchSourceStepDefinition } from './fetch_source'; | ||
| import { FETCH_SOURCE_STEP_TYPE } from '../../../../common/threat_intel/workflows/step_types/fetch_source/fetch_source_common'; | ||
|
|
||
| type StepLoader = () => Promise<PublicStepDefinition | undefined>; | ||
|
|
||
| describe('registerThreatIntelWorkflowSteps (public)', () => { | ||
| it('registers exactly one step definition for threat_intel.fetch_source', () => { | ||
| const workflowsExtensions = workflowsExtensionsMock.createSetup(); | ||
|
|
||
| registerThreatIntelWorkflowSteps(workflowsExtensions); | ||
|
|
||
| expect(workflowsExtensions.registerStepDefinition).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('async loader resolves to the fetch_source step definition', async () => { | ||
| const workflowsExtensions = workflowsExtensionsMock.createSetup(); | ||
|
|
||
| registerThreatIntelWorkflowSteps(workflowsExtensions); | ||
|
|
||
| const [loader] = workflowsExtensions.registerStepDefinition.mock.calls.map( | ||
| ([arg]) => arg as StepLoader | ||
| ); | ||
|
|
||
| const definition = await loader(); | ||
| expect(definition).toBe(fetchSourceStepDefinition); | ||
| // Sanity-check the id matches the YAML-side step type so the editor | ||
| // can find the schema for `type: threat_intel.fetch_source`. | ||
| expect(definition?.id).toBe(FETCH_SOURCE_STEP_TYPE); | ||
| }); | ||
| }); |
36 changes: 36 additions & 0 deletions
36
...ins/security_solution/public/threat_intel/workflows/step_types/register_workflow_steps.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| import type { WorkflowsExtensionsPublicPluginSetup } from '@kbn/workflows-extensions/public'; | ||
|
|
||
| /** | ||
| * Register threat-intel-owned workflow step types with the public | ||
| * `workflowsExtensions` setup contract so the YAML editor's strict-schema | ||
| * validator (see | ||
| * `workflows_management/public/features/validate_workflow_yaml/model/use_workflow_json_schema.ts`) | ||
| * picks them up. | ||
| * | ||
| * Mirrors the server-side `registerThreatIntelWorkflowSteps` in | ||
| * `server/threat_intel/workflows/step_types/index.ts`. Steps are | ||
| * loaded lazily via async loaders to keep the heavy | ||
| * `fetch_source_common` Zod schemas off the critical-path bundle until the | ||
| * editor actually requests them. | ||
| * | ||
| * The caller is expected to invoke this only when the optional | ||
| * `workflowsExtensions` plugin is present and the | ||
| * `threatIntelSupplyEnabled` experimental feature is on, matching | ||
| * the gating policy applied server-side. Without that gate, dark-flagged | ||
| * deployments would advertise a step type whose handler is never | ||
| * registered. | ||
| */ | ||
| export const registerThreatIntelWorkflowSteps = ( | ||
| workflowsExtensions: WorkflowsExtensionsPublicPluginSetup | ||
| ): void => { | ||
| workflowsExtensions.registerStepDefinition(async () => | ||
| import('./fetch_source').then((m) => m.fetchSourceStepDefinition) | ||
| ); | ||
| }; |
73 changes: 73 additions & 0 deletions
73
...tions/security/plugins/security_solution/server/threat_intel/adapters/fingerprint.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| import { createHash } from 'crypto'; | ||
| import { buildFingerprint } from './fingerprint'; | ||
|
|
||
| describe('buildFingerprint', () => { | ||
| it('length-prefixes each part so the seed is unambiguous', () => { | ||
| // The seed is `<len>:<part>` per part, concatenated. There is no workflow-side | ||
| // computation to match: the definitions only pass `content_fingerprint` | ||
| // through, and the engine has no sha256 Liquid filter, so this encoding is | ||
| // ours alone and only has to be self-consistent. | ||
| const expected = createHash('sha256') | ||
| .update('28:https://example.com/feed.xml7:item-425:Title') | ||
| .digest('hex'); | ||
| expect(buildFingerprint(['https://example.com/feed.xml', 'item-42', 'Title'])).toBe(expected); | ||
| }); | ||
|
|
||
| // A plain join on `:` collided across part boundaries, so two different feed | ||
| // items produced one fingerprint and the second was deduplicated away as | ||
| // already-ingested. Titles, URLs, and ids routinely contain colons. | ||
| it('does not collide when a colon moves across a part boundary', () => { | ||
| expect(buildFingerprint(['a:b', 'c'])).not.toBe(buildFingerprint(['a', 'b:c'])); | ||
| }); | ||
|
|
||
| it('does not collide when a part boundary shifts', () => { | ||
| expect(buildFingerprint(['https://evil.test', 'a'])).not.toBe( | ||
| buildFingerprint(['https://evil.test:a', '']) | ||
| ); | ||
| }); | ||
|
|
||
| it('returns a 64-char hex digest', () => { | ||
| expect(buildFingerprint(['a', 'b'])).toMatch(/^[0-9a-f]{64}$/); | ||
| }); | ||
|
|
||
| it('NFKC-normalizes parts so unicode equivalents collapse', () => { | ||
| // U+FB01 (fi ligature) vs ASCII "fi". | ||
| expect(buildFingerprint(['url', 'id', '\ufb01nal'])).toBe( | ||
| buildFingerprint(['url', 'id', 'final']) | ||
| ); | ||
| }); | ||
|
|
||
| it('trims leading/trailing whitespace per part', () => { | ||
| expect(buildFingerprint([' url ', ' id '])).toBe(buildFingerprint(['url', 'id'])); | ||
| }); | ||
|
|
||
| it('treats undefined/null parts as empty strings (still positional)', () => { | ||
| // Two leading missing parts are still part of the seed shape — a | ||
| // re-fetch with the same shape produces the same digest. | ||
| const a = buildFingerprint([undefined, undefined, 'id']); | ||
| const b = buildFingerprint([undefined, undefined, 'id']); | ||
| expect(a).toBe(b); | ||
| // …but a missing part is *positionally distinct* from the part | ||
| // moving up by one — `:::id` and `id` produce different digests. | ||
| expect(a).not.toBe(buildFingerprint(['id'])); | ||
| }); | ||
|
|
||
| it('produces a stable digest for the same logical input', () => { | ||
| const fp1 = buildFingerprint(['https://example.com', 'id-1', 'modified-2026-05-01']); | ||
| const fp2 = buildFingerprint(['https://example.com', 'id-1', 'modified-2026-05-01']); | ||
| expect(fp1).toBe(fp2); | ||
| }); | ||
|
|
||
| it('produces different digests when the version stamp changes', () => { | ||
| const a = buildFingerprint(['https://example.com', 'id-1', '2026-05-01']); | ||
| const b = buildFingerprint(['https://example.com', 'id-1', '2026-05-02']); | ||
| expect(a).not.toBe(b); | ||
| }); | ||
| }); |
30 changes: 30 additions & 0 deletions
30
.../solutions/security/plugins/security_solution/server/threat_intel/adapters/fingerprint.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| import { createHash } from 'crypto'; | ||
|
|
||
| /** | ||
| * SHA-256 over NFKC-normalized parts, each length-prefixed. | ||
| * | ||
| * A plain `join(':')` is ambiguous: `['a:b', 'c']` and `['a', 'b:c']` produce the | ||
| * same seed, so two different items collide on one fingerprint and the second is | ||
| * deduplicated away as though it had already been ingested. The parts here are | ||
| * feed-controlled titles, URLs, and ids, which routinely contain colons, so this | ||
| * is reachable rather than theoretical. | ||
| * | ||
| * Length-prefixing makes the seed unambiguous. Nothing outside this module | ||
| * recomputes these values (the workflows only pass `content_fingerprint` through), | ||
| * so the change is safe; already-stored reports will be re-ingested once under | ||
| * their new fingerprint. | ||
| */ | ||
| export const buildFingerprint = (parts: ReadonlyArray<string | undefined | null>): string => { | ||
| const seed = parts | ||
| .map((part) => (part ?? '').trim().normalize('NFKC')) | ||
| .map((part) => `${part.length}:${part}`) | ||
| .join(''); | ||
| return createHash('sha256').update(seed).digest('hex'); | ||
| }; |
9 changes: 9 additions & 0 deletions
9
x-pack/solutions/security/plugins/security_solution/server/threat_intel/adapters/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| export { runAdapter, UnknownAdapterError } from './run_adapter'; | ||
| export type { AdapterRunContext, FetchAdapter, NormalizedReport, SourceHit } from './types'; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.