mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[TIP] Add new Threat intelligence plugin (#136479)
* [TIP] Add Threat Intelligence plugin - create Threat Intelligence plugin and integrate with Security Solution plugin - setup jest unit tests, i18n, Cypress tests and Storybook - fetch Indicator of Compromise, and display in data-grid - add flyout components to show IOCs details (table and JSON) - add new threatIntelInt entry to kbn-doc-links package https://github.com/elastic/security-team/issues/4329 https://github.com/elastic/security-team/issues/4138 https://github.com/elastic/security-team/issues/4241 https://github.com/elastic/security-team/issues/4242 https://github.com/elastic/security-team/issues/4244 https://github.com/elastic/security-team/issues/4245 Co-authored-by: lgmys <lgmys@pm.me> Co-authored-by: Maxim Kholod <maxim.kholod@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b127231787
commit
19aa51e5a8
108 changed files with 6155 additions and 38 deletions
|
@ -32,6 +32,7 @@ disabled:
|
|||
- x-pack/test/security_solution_cypress/response_ops_cli_config.ts
|
||||
- x-pack/test/security_solution_cypress/upgrade_config.ts
|
||||
- x-pack/test/security_solution_cypress/visual_config.ts
|
||||
- x-pack/test/threat_intelligence_cypress/visual_config.ts
|
||||
- x-pack/test/functional_enterprise_search/with_host_configured.config.ts
|
||||
- x-pack/plugins/apm/ftr_e2e/ftr_config_open.ts
|
||||
- x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts
|
||||
|
|
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
|
@ -589,6 +589,12 @@ x-pack/plugins/session_view @elastic/awp-platform
|
|||
x-pack/plugins/security_solution/public/common/components/sessions_viewer @elastic/awp-platform
|
||||
x-pack/plugins/security_solution/public/kubernetes @elastic/awp-platform
|
||||
|
||||
## Security Solution sub teams - Protections Experience
|
||||
x-pack/plugins/threat_intelligence @elastic/protections-experience
|
||||
x-pack/plugins/security_solution/public/threat_intelligence @elastic/protections-experience
|
||||
x-pack/test/threat_intelligence_cypress @elastic/protections-experience
|
||||
|
||||
|
||||
# Security Intelligence And Analytics
|
||||
/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics
|
||||
|
||||
|
|
|
@ -639,6 +639,10 @@ Documentation: https://www.elastic.co/guide/en/kibana/master/task-manager-produc
|
|||
|Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/threat_intelligence/README.md[threatIntelligence]
|
||||
|Elastic Threat Intelligence makes it easy to analyze and investigate potential security threats by aggregating data from multiple sources in one place. You’ll be able to view data from all activated threat intelligence feeds and take action.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/timelines/README.md[timelines]
|
||||
|Timelines is a plugin that provides a grid component with accompanying server side apis to help users identify events of interest and perform root cause analysis within Kibana.
|
||||
|
||||
|
|
|
@ -336,13 +336,13 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
ruleApiOverview: `${SECURITY_SOLUTION_DOCS}rule-api-overview.html`,
|
||||
},
|
||||
securitySolution: {
|
||||
trustedApps: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/trusted-apps-ov.html`,
|
||||
eventFilters: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/event-filters.html`,
|
||||
blocklist: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/blocklist.html`,
|
||||
trustedApps: `${SECURITY_SOLUTION_DOCS}trusted-apps-ov.html`,
|
||||
eventFilters: `${SECURITY_SOLUTION_DOCS}event-filters.html`,
|
||||
blocklist: `${SECURITY_SOLUTION_DOCS}blocklist.html`,
|
||||
threatIntelInt: `${SECURITY_SOLUTION_DOCS}es-threat-intel-integrations.html`,
|
||||
policyResponseTroubleshooting: {
|
||||
full_disk_access: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/deploy-elastic-endpoint.html#enable-fda-endpoint`,
|
||||
macos_system_ext: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/deploy-elastic-endpoint.html#system-extension-endpoint`,
|
||||
full_disk_access: `${SECURITY_SOLUTION_DOCS}deploy-elastic-endpoint.html#enable-fda-endpoint`,
|
||||
macos_system_ext: `${SECURITY_SOLUTION_DOCS}deploy-elastic-endpoint.html#system-extension-endpoint`,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
|
@ -653,7 +653,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
rustOverview: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/rust-api/${DOC_LINK_VERSION}/overview.html`,
|
||||
},
|
||||
endpoints: {
|
||||
troubleshooting: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/ts-management.html#ts-endpoints`,
|
||||
troubleshooting: `${SECURITY_SOLUTION_DOCS}ts-management.html#ts-endpoints`,
|
||||
},
|
||||
legal: {
|
||||
privacyStatement: `${ELASTIC_WEBSITE_URL}legal/privacy-statement`,
|
||||
|
|
|
@ -249,6 +249,7 @@ export interface DocLinks {
|
|||
full_disk_access: string;
|
||||
macos_system_ext: string;
|
||||
};
|
||||
readonly threatIntelInt: string;
|
||||
};
|
||||
readonly query: {
|
||||
readonly eql: string;
|
||||
|
|
|
@ -58,7 +58,7 @@ pageLoadAssetSize:
|
|||
telemetry: 51957
|
||||
telemetryManagementSection: 38586
|
||||
transform: 41007
|
||||
triggersActionsUi: 119000 #This is temporary. Check https://github.com/elastic/kibana/pull/130710#issuecomment-1119843458 & https://github.com/elastic/kibana/issues/130728
|
||||
triggersActionsUi: 119000
|
||||
upgradeAssistant: 81241
|
||||
urlForwarding: 32579
|
||||
usageCollection: 39762
|
||||
|
@ -132,3 +132,4 @@ pageLoadAssetSize:
|
|||
expressionXY: 36000
|
||||
kibanaUsageCollection: 16463
|
||||
kubernetesSecurity: 77234
|
||||
threatIntelligence: 29195
|
||||
|
|
|
@ -39,6 +39,7 @@ export const storybookAliases = {
|
|||
presentation: 'src/plugins/presentation_util/storybook',
|
||||
security_solution: 'x-pack/plugins/security_solution/.storybook',
|
||||
shared_ux: 'packages/kbn-shared-ux-storybook/src/config',
|
||||
threat_intelligence: 'x-pack/plugins/threat_intelligence/.storybook',
|
||||
ui_actions_enhanced: 'src/plugins/ui_actions_enhanced/.storybook',
|
||||
unified_search: 'src/plugins/unified_search/.storybook',
|
||||
};
|
||||
|
|
|
@ -66,21 +66,22 @@ export const PROJECTS = [
|
|||
createProject('x-pack/plugins/fleet/cypress/tsconfig.json', {
|
||||
name: 'fleet/cypress',
|
||||
}),
|
||||
|
||||
createProject('x-pack/plugins/synthetics/e2e/tsconfig.json', {
|
||||
name: 'uptime/synthetics-e2e-tests',
|
||||
disableTypeCheck: true,
|
||||
}),
|
||||
|
||||
createProject('x-pack/plugins/ux/e2e/tsconfig.json', {
|
||||
name: 'ux/synthetics-e2e-tests',
|
||||
disableTypeCheck: true,
|
||||
}),
|
||||
|
||||
createProject('x-pack/plugins/observability/e2e/tsconfig.json', {
|
||||
name: 'observability/synthetics-e2e-tests',
|
||||
disableTypeCheck: true,
|
||||
}),
|
||||
createProject('x-pack/plugins/threat_intelligence/cypress/tsconfig.json', {
|
||||
name: 'threat_intelligence/cypress',
|
||||
disableTypeCheck: true,
|
||||
}),
|
||||
|
||||
// Glob patterns to be all search at once
|
||||
...findProjects([
|
||||
|
|
|
@ -407,6 +407,8 @@
|
|||
"@kbn/task-manager-plugin/*": ["x-pack/plugins/task_manager/*"],
|
||||
"@kbn/telemetry-collection-xpack-plugin": ["x-pack/plugins/telemetry_collection_xpack"],
|
||||
"@kbn/telemetry-collection-xpack-plugin/*": ["x-pack/plugins/telemetry_collection_xpack/*"],
|
||||
"@kbn/threat-intelligence-plugin": ["x-pack/plugins/threat_intelligence"],
|
||||
"@kbn/threat-intelligence-plugin/*": ["x-pack/plugins/threat_intelligence/*"],
|
||||
"@kbn/timelines-plugin": ["x-pack/plugins/timelines"],
|
||||
"@kbn/timelines-plugin/*": ["x-pack/plugins/timelines/*"],
|
||||
"@kbn/transform-plugin": ["x-pack/plugins/transform"],
|
||||
|
|
|
@ -68,7 +68,8 @@
|
|||
"xpack.urlDrilldown": "plugins/drilldowns/url_drilldown",
|
||||
"xpack.watcher": "plugins/watcher",
|
||||
"xpack.observability": "plugins/observability",
|
||||
"xpack.banners": "plugins/banners"
|
||||
"xpack.banners": "plugins/banners",
|
||||
"xpack.threatIntelligence": "plugins/threat_intelligence"
|
||||
},
|
||||
"exclude": ["examples"],
|
||||
"translations": [
|
||||
|
|
|
@ -81,11 +81,21 @@ export enum SecurityPageName {
|
|||
case = 'cases', // must match `CasesDeepLinkId.cases`
|
||||
caseConfigure = 'cases_configure', // must match `CasesDeepLinkId.casesConfigure`
|
||||
caseCreate = 'cases_create', // must match `CasesDeepLinkId.casesCreate`
|
||||
/*
|
||||
* Warning: Computed values are not permitted in an enum with string valued members
|
||||
* All cloud security posture page names must match `CloudSecurityPosturePageId` in x-pack/plugins/cloud_security_posture/public/common/navigation/types.ts
|
||||
*/
|
||||
cloudSecurityPostureBenchmarks = 'cloud_security_posture-benchmarks',
|
||||
cloudSecurityPostureDashboard = 'cloud_security_posture-dashboard',
|
||||
cloudSecurityPostureFindings = 'cloud_security_posture-findings',
|
||||
cloudSecurityPostureRules = 'cloud_security_posture-rules',
|
||||
dashboardsLanding = 'dashboards',
|
||||
detections = 'detections',
|
||||
detectionAndResponse = 'detection_response',
|
||||
endpoints = 'endpoints',
|
||||
eventFilters = 'event_filters',
|
||||
exceptions = 'exceptions',
|
||||
exploreLanding = 'explore',
|
||||
hostIsolationExceptions = 'host_isolation_exceptions',
|
||||
hosts = 'hosts',
|
||||
hostsAnomalies = 'hosts-anomalies',
|
||||
|
@ -93,6 +103,7 @@ export enum SecurityPageName {
|
|||
hostsRisk = 'hosts-risk',
|
||||
hostsEvents = 'hosts-events',
|
||||
investigate = 'investigate',
|
||||
kubernetes = 'kubernetes',
|
||||
landing = 'get_started',
|
||||
network = 'network',
|
||||
networkAnomalies = 'network-anomalies',
|
||||
|
@ -100,11 +111,14 @@ export enum SecurityPageName {
|
|||
networkExternalAlerts = 'network-external_alerts',
|
||||
networkHttp = 'network-http',
|
||||
networkTls = 'network-tls',
|
||||
noPage = '',
|
||||
overview = 'overview',
|
||||
policies = 'policy',
|
||||
responseActions = 'response_actions',
|
||||
rules = 'rules',
|
||||
rulesCreate = 'rules-create',
|
||||
sessions = 'sessions',
|
||||
threatIntelligence = 'threat-intelligence',
|
||||
timelines = 'timelines',
|
||||
timelinesTemplates = 'timelines-templates',
|
||||
trustedApps = 'trusted_apps',
|
||||
|
@ -113,21 +127,8 @@ export enum SecurityPageName {
|
|||
usersAuthentications = 'users-authentications',
|
||||
usersAnomalies = 'users-anomalies',
|
||||
usersRisk = 'users-risk',
|
||||
sessions = 'sessions',
|
||||
usersEvents = 'users-events',
|
||||
usersExternalAlerts = 'users-external_alerts',
|
||||
kubernetes = 'kubernetes',
|
||||
exploreLanding = 'explore',
|
||||
dashboardsLanding = 'dashboards',
|
||||
noPage = '',
|
||||
/*
|
||||
* Warning: Computed values are not permitted in an enum with string valued members
|
||||
* All cloud security posture page names must match `CloudSecurityPosturePageId` in x-pack/plugins/cloud_security_posture/public/common/navigation/types.ts
|
||||
*/
|
||||
cloudSecurityPostureDashboard = 'cloud_security_posture-dashboard',
|
||||
cloudSecurityPostureFindings = 'cloud_security_posture-findings',
|
||||
cloudSecurityPostureBenchmarks = 'cloud_security_posture-benchmarks',
|
||||
cloudSecurityPostureRules = 'cloud_security_posture-rules',
|
||||
}
|
||||
|
||||
export const EXPLORE_PATH = '/explore' as const;
|
||||
|
@ -156,6 +157,7 @@ export const HOST_ISOLATION_EXCEPTIONS_PATH =
|
|||
`${MANAGEMENT_PATH}/host_isolation_exceptions` as const;
|
||||
export const BLOCKLIST_PATH = `${MANAGEMENT_PATH}/blocklist` as const;
|
||||
export const RESPONSE_ACTIONS_PATH = `${MANAGEMENT_PATH}/response_actions` as const;
|
||||
export const THREAT_INTELLIGENCE_PATH = '/threat_intelligence' as const;
|
||||
|
||||
export const APP_OVERVIEW_PATH = `${APP_PATH}${OVERVIEW_PATH}` as const;
|
||||
export const APP_LANDING_PATH = `${APP_PATH}${LANDING_PATH}` as const;
|
||||
|
@ -180,6 +182,7 @@ export const APP_HOST_ISOLATION_EXCEPTIONS_PATH =
|
|||
`${APP_PATH}${HOST_ISOLATION_EXCEPTIONS_PATH}` as const;
|
||||
export const APP_BLOCKLIST_PATH = `${APP_PATH}${BLOCKLIST_PATH}` as const;
|
||||
export const APP_RESPONSE_ACTIONS_PATH = `${APP_PATH}${RESPONSE_ACTIONS_PATH}` as const;
|
||||
export const APP_THREAT_INTELLIGENCE_PATH = `${APP_PATH}${THREAT_INTELLIGENCE_PATH}` as const;
|
||||
|
||||
// cloud logs to exclude from default index pattern
|
||||
export const EXCLUDE_ELASTIC_CLOUD_INDICES = ['-*elastic-cloud-logs-*'];
|
||||
|
|
|
@ -12,23 +12,24 @@
|
|||
"actions",
|
||||
"alerting",
|
||||
"cases",
|
||||
"cloudSecurityPosture",
|
||||
"data",
|
||||
"embeddable",
|
||||
"eventLog",
|
||||
"features",
|
||||
"inspector",
|
||||
"kubernetesSecurity",
|
||||
"lens",
|
||||
"licensing",
|
||||
"maps",
|
||||
"ruleRegistry",
|
||||
"sessionView",
|
||||
"taskManager",
|
||||
"threatIntelligence",
|
||||
"timelines",
|
||||
"triggersActionsUi",
|
||||
"uiActions",
|
||||
"unifiedSearch",
|
||||
"kubernetesSecurity",
|
||||
"cloudSecurityPosture"
|
||||
"unifiedSearch"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"encryptedSavedObjects",
|
||||
|
|
|
@ -45,6 +45,7 @@ import {
|
|||
DASHBOARDS,
|
||||
CREATE_NEW_RULE,
|
||||
RESPONSE_ACTIONS,
|
||||
THREAT_INTELLIGENCE,
|
||||
} from '../translations';
|
||||
import {
|
||||
OVERVIEW_PATH,
|
||||
|
@ -69,6 +70,7 @@ import {
|
|||
KUBERNETES_PATH,
|
||||
RULES_CREATE_PATH,
|
||||
RESPONSE_ACTIONS_PATH,
|
||||
THREAT_INTELLIGENCE_PATH,
|
||||
} from '../../../common/constants';
|
||||
import type { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
import { hasCapabilities, subscribeAppLinks } from '../../common/links';
|
||||
|
@ -384,6 +386,17 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.threatIntelligence,
|
||||
title: THREAT_INTELLIGENCE,
|
||||
path: THREAT_INTELLIGENCE_PATH,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
keywords: [
|
||||
i18n.translate('xpack.securitySolution.search.threatIntelligence', {
|
||||
defaultMessage: 'Threat Intelligence',
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: SecurityPageName.kubernetes,
|
||||
title: KUBERNETES,
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
APP_KUBERNETES_PATH,
|
||||
APP_LANDING_PATH,
|
||||
APP_RESPONSE_ACTIONS_PATH,
|
||||
APP_THREAT_INTELLIGENCE_PATH,
|
||||
} from '../../../common/constants';
|
||||
|
||||
export const navTabs: SecurityNav = {
|
||||
|
@ -173,6 +174,13 @@ export const navTabs: SecurityNav = {
|
|||
disabled: false,
|
||||
urlKey: 'administration',
|
||||
},
|
||||
[SecurityPageName.threatIntelligence]: {
|
||||
id: SecurityPageName.threatIntelligence,
|
||||
name: i18n.THREAT_INTELLIGENCE,
|
||||
href: APP_THREAT_INTELLIGENCE_PATH,
|
||||
disabled: false,
|
||||
urlKey: 'threat_intelligence',
|
||||
},
|
||||
};
|
||||
|
||||
export const securityNavGroup: SecurityNavGroup = {
|
||||
|
|
|
@ -138,3 +138,10 @@ export const NO_PERMISSIONS_MSG = (subPluginKey: string) =>
|
|||
export const NO_PERMISSIONS_TITLE = i18n.translate('xpack.securitySolution.noPermissionsTitle', {
|
||||
defaultMessage: 'Privileges required',
|
||||
});
|
||||
|
||||
export const THREAT_INTELLIGENCE = i18n.translate(
|
||||
'xpack.securitySolution.navigation.threatIntelligence',
|
||||
{
|
||||
defaultMessage: 'Threat Intelligence',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -64,6 +64,7 @@ export const securityNavKeys = [
|
|||
SecurityPageName.trustedApps,
|
||||
SecurityPageName.users,
|
||||
SecurityPageName.kubernetes,
|
||||
SecurityPageName.threatIntelligence,
|
||||
] as const;
|
||||
export type SecurityNavKey = typeof securityNavKeys[number];
|
||||
|
||||
|
|
|
@ -116,6 +116,16 @@ Object {
|
|||
"name": "Users",
|
||||
"onClick": [Function],
|
||||
},
|
||||
Object {
|
||||
"data-href": "securitySolutionUI/threat-intelligence",
|
||||
"data-test-subj": "navigation-threat-intelligence",
|
||||
"disabled": false,
|
||||
"href": "securitySolutionUI/threat-intelligence",
|
||||
"id": "threat-intelligence",
|
||||
"isSelected": false,
|
||||
"name": "Threat Intelligence",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"name": "Explore",
|
||||
},
|
||||
|
|
|
@ -109,6 +109,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) {
|
|||
...(navTabs[SecurityPageName.users] != null
|
||||
? [navTabs[SecurityPageName.users]]
|
||||
: []),
|
||||
navTabs[SecurityPageName.threatIntelligence],
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -42,4 +42,5 @@ export type UrlStateType =
|
|||
| 'rules'
|
||||
| 'timeline'
|
||||
| 'explore'
|
||||
| 'dashboards';
|
||||
| 'dashboards'
|
||||
| 'threat_intelligence';
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
|
@ -20,6 +20,7 @@ import { links as networkLinks } from '../network/links';
|
|||
import { links as usersLinks } from '../users/links';
|
||||
import { links as kubernetesLinks } from '../kubernetes/links';
|
||||
import { dashboardLinks as cloudSecurityPostureLinks } from '../cloud_security_posture/links';
|
||||
import { links as threatIntelligenceLinks } from '../threat_intelligence/links';
|
||||
|
||||
export const dashboardsLandingLinks: LinkItem = {
|
||||
id: SecurityPageName.dashboardsLanding,
|
||||
|
@ -50,7 +51,7 @@ export const threatHuntingLandingLinks: LinkItem = {
|
|||
defaultMessage: 'Explore',
|
||||
}),
|
||||
],
|
||||
links: [hostsLinks, networkLinks, usersLinks],
|
||||
links: [hostsLinks, networkLinks, usersLinks, threatIntelligenceLinks],
|
||||
skipUrlState: true,
|
||||
hideTimeline: true,
|
||||
};
|
||||
|
|
|
@ -13,19 +13,17 @@
|
|||
import { Cases } from './cases';
|
||||
import { Detections } from './detections';
|
||||
import { Exceptions } from './exceptions';
|
||||
|
||||
import { Hosts } from './hosts';
|
||||
import { Users } from './users';
|
||||
import { Network } from './network';
|
||||
import { Kubernetes } from './kubernetes';
|
||||
import { Overview } from './overview';
|
||||
import { Rules } from './rules';
|
||||
|
||||
import { Timelines } from './timelines';
|
||||
import { Management } from './management';
|
||||
import { LandingPages } from './landing_pages';
|
||||
|
||||
import { CloudSecurityPosture } from './cloud_security_posture';
|
||||
import { ThreatIntelligence } from './threat_intelligence';
|
||||
|
||||
/**
|
||||
* The classes used to instantiate the sub plugins. These are grouped into a single object for the sake of bundling them in a single dynamic import.
|
||||
|
@ -38,12 +36,12 @@ const subPluginClasses = {
|
|||
Users,
|
||||
Network,
|
||||
Kubernetes,
|
||||
|
||||
Overview,
|
||||
Rules,
|
||||
Timelines,
|
||||
Management,
|
||||
LandingPages,
|
||||
CloudSecurityPosture,
|
||||
ThreatIntelligence,
|
||||
};
|
||||
export { subPluginClasses };
|
||||
|
|
|
@ -331,6 +331,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
management: new subPluginClasses.Management(),
|
||||
landingPages: new subPluginClasses.LandingPages(),
|
||||
cloudSecurityPosture: new subPluginClasses.CloudSecurityPosture(),
|
||||
threatIntelligence: new subPluginClasses.ThreatIntelligence(),
|
||||
};
|
||||
}
|
||||
return this._subPlugins;
|
||||
|
@ -359,6 +360,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
management: subPlugins.management.start(core, plugins),
|
||||
landingPages: subPlugins.landingPages.start(),
|
||||
cloudSecurityPosture: subPlugins.cloudSecurityPosture.start(),
|
||||
threatIntelligence: subPlugins.threatIntelligence.start(),
|
||||
};
|
||||
}
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import type { SecuritySubPlugin } from '../app/types';
|
||||
import { routes } from './routes';
|
||||
|
||||
export class ThreatIntelligence {
|
||||
public setup() {}
|
||||
|
||||
public start(): SecuritySubPlugin {
|
||||
return {
|
||||
routes,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
import { SecurityPageName, SERVER_APP_ID, THREAT_INTELLIGENCE_PATH } from '../../common/constants';
|
||||
import { THREAT_INTELLIGENCE } from '../app/translations';
|
||||
import type { LinkItem } from '../common/links';
|
||||
import threatIntelligencePageImg from '../common/images/threat_intelligence.png';
|
||||
|
||||
export const links: LinkItem = {
|
||||
id: SecurityPageName.threatIntelligence,
|
||||
title: THREAT_INTELLIGENCE,
|
||||
path: THREAT_INTELLIGENCE_PATH,
|
||||
landingImage: threatIntelligencePageImg,
|
||||
description: i18n.translate('xpack.securitySolution.appLinks.threatIntelligence.description', {
|
||||
defaultMessage:
|
||||
'Elastic threat intelligence helps you see if you are open to or have been subject to current or historical known threats.',
|
||||
}),
|
||||
capabilities: [`${SERVER_APP_ID}.show`],
|
||||
globalNavEnabled: false,
|
||||
globalSearchKeywords: [
|
||||
i18n.translate('xpack.securitySolution.appLinks.threatIntelligence', {
|
||||
defaultMessage: 'Threat Intelligence',
|
||||
}),
|
||||
],
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
|
||||
import { SpyRoute } from '../../common/utils/route/spy_routes';
|
||||
import { SecurityPageName } from '../../../common/constants';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
const ThreatIntelligence = () => {
|
||||
const services = useKibana().services;
|
||||
const { threatIntelligence } = services;
|
||||
const ThreatIntelligencePlugin = threatIntelligence.getComponent();
|
||||
|
||||
return (
|
||||
<SecuritySolutionPageWrapper noPadding>
|
||||
<ThreatIntelligencePlugin />
|
||||
<SpyRoute pageName={SecurityPageName.threatIntelligence} />
|
||||
</SecuritySolutionPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThreatIntelligencePage = React.memo(ThreatIntelligence);
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
|
||||
import { ThreatIntelligencePage } from './pages/threat_intelligence';
|
||||
import { SecurityPageName, THREAT_INTELLIGENCE_PATH } from '../../common/constants';
|
||||
import type { SecuritySubPluginRoutes } from '../app/types';
|
||||
|
||||
const ThreatIntelligenceRoutes = () => (
|
||||
<TrackApplicationView viewId={SecurityPageName.threatIntelligence}>
|
||||
<ThreatIntelligencePage />
|
||||
</TrackApplicationView>
|
||||
);
|
||||
|
||||
export const routes: SecuritySubPluginRoutes = [
|
||||
{
|
||||
path: THREAT_INTELLIGENCE_PATH,
|
||||
render: ThreatIntelligenceRoutes,
|
||||
},
|
||||
];
|
|
@ -39,6 +39,7 @@ import type {
|
|||
SavedObjectsTaggingApi,
|
||||
SavedObjectTaggingOssPluginStart,
|
||||
} from '@kbn/saved-objects-tagging-oss-plugin/public';
|
||||
import type { ThreatIntelligencePluginStart } from '@kbn/threat-intelligence-plugin/public';
|
||||
import type { ResolverPluginSetup } from './resolver/types';
|
||||
import type { Inspect } from '../common/search_strategy';
|
||||
import type { Detections } from './detections';
|
||||
|
@ -54,6 +55,7 @@ import type { Timelines } from './timelines';
|
|||
import type { Management } from './management';
|
||||
import type { LandingPages } from './landing_pages';
|
||||
import type { CloudSecurityPosture } from './cloud_security_posture';
|
||||
import type { ThreatIntelligence } from './threat_intelligence';
|
||||
|
||||
export interface SetupPlugins {
|
||||
home?: HomePublicPluginSetup;
|
||||
|
@ -87,6 +89,7 @@ export interface StartPlugins {
|
|||
osquery?: OsqueryPluginStart;
|
||||
security: SecurityPluginSetup;
|
||||
cloudSecurityPosture: CspClientPluginStart;
|
||||
threatIntelligence: ThreatIntelligencePluginStart;
|
||||
}
|
||||
|
||||
export interface StartPluginsDependencies extends StartPlugins {
|
||||
|
@ -127,6 +130,7 @@ export interface SubPlugins {
|
|||
management: Management;
|
||||
landingPages: LandingPages;
|
||||
cloudSecurityPosture: CloudSecurityPosture;
|
||||
threatIntelligence: ThreatIntelligence;
|
||||
}
|
||||
|
||||
// TODO: find a better way to defined these types
|
||||
|
@ -144,4 +148,5 @@ export interface StartedSubPlugins {
|
|||
management: ReturnType<Management['start']>;
|
||||
landingPages: ReturnType<LandingPages['start']>;
|
||||
cloudSecurityPosture: ReturnType<CloudSecurityPosture['start']>;
|
||||
threatIntelligence: ReturnType<ThreatIntelligence['start']>;
|
||||
}
|
||||
|
|
|
@ -32,19 +32,20 @@
|
|||
{ "path": "../actions/tsconfig.json" },
|
||||
{ "path": "../alerting/tsconfig.json" },
|
||||
{ "path": "../cases/tsconfig.json" },
|
||||
{ "path": "../cloud_security_posture/tsconfig.json" },
|
||||
{ "path": "../encrypted_saved_objects/tsconfig.json" },
|
||||
{ "path": "../features/tsconfig.json" },
|
||||
{ "path": "../fleet/tsconfig.json" },
|
||||
{ "path": "../kubernetes_security/tsconfig.json" },
|
||||
{ "path": "../licensing/tsconfig.json" },
|
||||
{ "path": "../lists/tsconfig.json" },
|
||||
{ "path": "../maps/tsconfig.json" },
|
||||
{ "path": "../ml/tsconfig.json" },
|
||||
{ "path": "../osquery/tsconfig.json" },
|
||||
{ "path": "../spaces/tsconfig.json" },
|
||||
{ "path": "../security/tsconfig.json" },
|
||||
{ "path": "../timelines/tsconfig.json" },
|
||||
{ "path": "../session_view/tsconfig.json" },
|
||||
{ "path": "../kubernetes_security/tsconfig.json" },
|
||||
{ "path": "../cloud_security_posture/tsconfig.json" }
|
||||
{ "path": "../security/tsconfig.json" },
|
||||
{ "path": "../spaces/tsconfig.json" },
|
||||
{ "path": "../threat_intelligence/tsconfig.json" },
|
||||
{ "path": "../timelines/tsconfig.json" }
|
||||
]
|
||||
}
|
||||
|
|
8
x-pack/plugins/threat_intelligence/.storybook/main.js
Normal file
8
x-pack/plugins/threat_intelligence/.storybook/main.js
Normal file
|
@ -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.
|
||||
*/
|
||||
|
||||
module.exports = require('@kbn/storybook').defaultConfig;
|
135
x-pack/plugins/threat_intelligence/CONTRIBUTING.md
Normal file
135
x-pack/plugins/threat_intelligence/CONTRIBUTING.md
Normal file
|
@ -0,0 +1,135 @@
|
|||
# Contributing
|
||||
|
||||
Before contributing to this plugin, make sure you read the [contributing guide for Kibana](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md), as well as the [STYLEGUIDE](https://github.com/elastic/kibana/blob/main/STYLEGUIDE.mdx) and [TYPESCRIPT](https://github.com/elastic/kibana/blob/main/TYPESCRIPT.md) md files..
|
||||
|
||||
> Kibana recommends working on a fork of the [elastic/kibana repository](https://github.com/elastic/kibana) (see [here](https://docs.github.com/en/get-started/quickstart/fork-a-repo) to learn about forks).
|
||||
|
||||
> This plugin uses TypeScript, see Kibana's recommendation here.
|
||||
|
||||
## Submitting a Pull Request (PR)
|
||||
|
||||
Before you submit your PR, consider the following guidelines:
|
||||
|
||||
1. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add.
|
||||
|
||||
2. Make your changes in a new git branch.
|
||||
|
||||
```
|
||||
git checkout -b my-branch main
|
||||
```
|
||||
|
||||
3. Commit your changes using a descriptive commit message that follows our commit message conventions:
|
||||
|
||||
```
|
||||
git commit -a
|
||||
```
|
||||
|
||||
4. Push your branch to GitHub:
|
||||
|
||||
```
|
||||
git push origin my-fix-branch
|
||||
```
|
||||
|
||||
5. In GitHub, create a PR.
|
||||
|
||||
Note: If changes are suggested, then make the required updates, [rebase](https://hackernoon.com/git-merge-vs-rebase-whats-the-diff-76413c117333) your branch, and force push (this will update your PR):
|
||||
|
||||
```
|
||||
git rebase main -i
|
||||
git push -f
|
||||
```
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
> **Note:** These guidelines are **recommended - not mandatory**.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
Each commit message consists of a **header**, **body**, and **footer**.
|
||||
|
||||
```
|
||||
<subject>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
[TIP] Threat Intelligence initial commit
|
||||
|
||||
- create Threat Intelligence plugin and integrate with Security Solution plugin
|
||||
- update Kibana CODEOWNERS
|
||||
- setup jest unit tests, i18n and Storybook
|
||||
|
||||
elastic/security-team#4329
|
||||
elastic/security-team#4241
|
||||
```
|
||||
|
||||
#### Subject
|
||||
|
||||
The subject should contain a succinct description of the change. Use the imperative, present tense: "change" not "changed" nor "changes".
|
||||
|
||||
#### Body
|
||||
Just as in the subject, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.
|
||||
|
||||
#### Footer
|
||||
|
||||
The footer should contain any information about **Breaking Changes** and is also the place to reference issues.
|
||||
|
||||
#### Revert
|
||||
|
||||
If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
|
||||
|
||||
## Code Review
|
||||
|
||||
You should review every line of code you have been asked to review and consider each of the following points during code review.
|
||||
|
||||
#### Functionality
|
||||
|
||||
Do the changes do what the developer intended? It is hard to understand how some changes will impact a user when you are just reading code. You should run the code yourself and validate the behavior.
|
||||
|
||||
#### Complexity
|
||||
|
||||
Are the changes more complex than necessary? Are individual lines, functions, classes, etc. too complex? "Too complex" usually means "cannot be understood quickly by code readers". It can also mean "developers are likely to introduce bugs when they try to call or modify the code."
|
||||
|
||||
A particular type of complexity is over-engineering, where developers have made the code more generic than it needs to be, or added functionality that is not presently needed. The developer should solve the problem they know needs to be solved now, not the problem they speculate might need to be solved in the future.
|
||||
|
||||
#### Tests
|
||||
|
||||
Ask for tests as appropriate for the changes. Truly untestable features are rare. Make sure the tests are correct, sensible, and useful. Will the tests actually fail when the code is broken?
|
||||
|
||||
Remember that tests are also code that has to be maintained. Do not accept complexity in tests just because they do not have a user-facing impact.
|
||||
|
||||
#### Comments
|
||||
|
||||
Did the developer write clear and understandable comments? Are all the comments necessary? Comments should explain why some code exists, not what it is doing. If the code is not clear enough to explain itself, then it should be made simpler. Comments are for information that the code itself cannot contain, like the reasoning behind a decision.
|
||||
|
||||
Are there TODO comments? TODOs just pile up in code and become stale over time. The developer should create a Jira issue and link to the issue from their comment.
|
||||
|
||||
Comments are different from documentation, which instead expresses the purpose of some code, how it should be used, and how it behaves when used.
|
||||
|
||||
### Documentation
|
||||
|
||||
If changes are made to how developers build, test, interact with, or release code, check to see that associated documentation was updated, including READMEs..
|
||||
|
||||
## How to test
|
||||
|
||||
**Storybook:**
|
||||
|
||||
`node scripts/storybook threat_intelligence`
|
||||
|
||||
**Unit tests:**
|
||||
|
||||
`npm run test:jest --config ./x-pack/plugins/threat_intelligence`
|
||||
|
||||
**E2E tests:**
|
||||
|
||||
```
|
||||
node scripts/build_kibana_platform_plugins
|
||||
cd x-pack/plugins/threat_intelligence
|
||||
yarn cypress:open-as-ci
|
||||
```
|
||||
|
15
x-pack/plugins/threat_intelligence/FAQ.md
Normal file
15
x-pack/plugins/threat_intelligence/FAQ.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# FAQ
|
||||
|
||||
### Where can I find the UI for the Threat Intelligence plugin?
|
||||
|
||||
Kibana recommends working on a fork of the [elastic/kibana repository](https://github.com/elastic/kibana) (see [here](https://docs.github.com/en/get-started/quickstart/fork-a-repo) to learn about forks).
|
||||
|
||||
### How is the Threat Intelligence code loaded in Kibana?
|
||||
|
||||
The Threat Intelligence plugin is loaded within the [security_solution](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution) plugin.
|
||||
|
||||
### I'm not seeing any data in the Indicators' table
|
||||
|
||||
See this [documentation here](https://github.com/elastic/security-team/blob/main/docs/protections-team/threat-intelligence-services/protections-experience/development-setup.mdx) to get Threat Intelligence feed in Kibana.
|
||||
|
||||
Once you have the feed running, go to `Management > Advanced Settings > Threat indices` and add `filebeat-*` to the list (comma separated).
|
44
x-pack/plugins/threat_intelligence/README.md
Executable file
44
x-pack/plugins/threat_intelligence/README.md
Executable file
|
@ -0,0 +1,44 @@
|
|||
# Threat Intelligence
|
||||
|
||||
Elastic Threat Intelligence makes it easy to analyze and investigate potential security threats by aggregating data from multiple sources in one place. You’ll be able to view data from all activated threat intelligence feeds and take action.
|
||||
|
||||
### Where to find the UI for this plugin?
|
||||
|
||||
The Threat Intelligence UI is displayed in Kibana Security, under the Explore section.
|
||||
|
||||
## Quick Start
|
||||
|
||||
See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment.
|
||||
|
||||
Verify your node version [here](https://github.com/elastic/kibana/blob/main/.node-version).
|
||||
|
||||
**Run ES:**
|
||||
|
||||
`yarn es snapshot --license trial`
|
||||
|
||||
**Run Kibana:**
|
||||
|
||||
```
|
||||
yarn kbn reset && yarn kbn bootstrap
|
||||
yarn start --no-base-path
|
||||
```
|
||||
|
||||
### Useful hints
|
||||
|
||||
Export local instance data to es_archives (will be loaded in cypress tests).
|
||||
|
||||
```
|
||||
TEST_ES_PORT=9200 node scripts/es_archiver save x-pack/test/threat_intelligence_cypress/es_archives/threat_intelligence "logs-ti*"
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
See [FAQ.md](https://github.com/elastic/kibana/blob/main/x-pack/plugins/threat_intelligence/FAQ.md) for questions you may have.
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](https://github.com/elastic/kibana/blob/main/x-pack/plugins/threat_intelligence/CONTRIBUTING.md) for information on contributing.
|
||||
|
||||
## Issues
|
||||
|
||||
Please report any issues in [this GitHub project](https://github.com/orgs/elastic/projects/758/).
|
12
x-pack/plugins/threat_intelligence/common/constants.ts
Normal file
12
x-pack/plugins/threat_intelligence/common/constants.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 const API_ROUTE_SOURCES = '/internal/threat_intelligence/source';
|
||||
|
||||
export const EMPTY_VALUE = '-';
|
||||
|
||||
export const DEFAULT_THREAT_INDEX_KEY = 'securitySolution:defaultThreatIndex' as const;
|
11
x-pack/plugins/threat_intelligence/common/types/feed.ts
Normal file
11
x-pack/plugins/threat_intelligence/common/types/feed.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 interface Feed {
|
||||
name: string;
|
||||
lastSeen: Date;
|
||||
}
|
52
x-pack/plugins/threat_intelligence/common/types/indicator.ts
Normal file
52
x-pack/plugins/threat_intelligence/common/types/indicator.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 enum RawIndicatorFieldId {
|
||||
Type = 'threat.indicator.type',
|
||||
FirstSeen = 'threat.indicator.first_seen',
|
||||
LastSeen = 'threat.indicator.last_seen',
|
||||
MarkingTLP = 'threat.indicator.marking.tlp',
|
||||
Feed = 'threat.feed.name',
|
||||
Ip = 'threat.indicator.ip',
|
||||
EmailAddress = 'threat.indicator.email.address',
|
||||
UrlFull = 'threat.indicator.url.full',
|
||||
UrlOriginal = 'threat.indicator.url.original',
|
||||
UrlDomain = 'threat.indicator.url.domain',
|
||||
FileMd5 = 'threat.indicator.file.hash.md5',
|
||||
FileSha256 = 'threat.indicator.file.hash.sha256',
|
||||
}
|
||||
|
||||
export interface Indicator {
|
||||
fields: Partial<Record<RawIndicatorFieldId, unknown[]>>;
|
||||
}
|
||||
|
||||
export const generateMockIndicator = (): Indicator => ({
|
||||
fields: {
|
||||
'threat.indicator.type': ['ipv4-addr'],
|
||||
'threat.indicator.ip': ['12.68.554.87'],
|
||||
'threat.indicator.first_seen': ['2022-01-01T01:01:01.000Z'],
|
||||
'threat.feed.name': ['Abuse_CH'],
|
||||
},
|
||||
});
|
||||
|
||||
export const generateMockUrlIndicator = (): Indicator => {
|
||||
const indicator = generateMockIndicator();
|
||||
|
||||
indicator.fields['threat.indicator.type'] = ['url'];
|
||||
indicator.fields['threat.indicator.url.full'] = ['https://google.com'];
|
||||
|
||||
return indicator;
|
||||
};
|
||||
|
||||
export const generateMockFileIndicator = (): Indicator => {
|
||||
const indicator = generateMockIndicator();
|
||||
|
||||
indicator.fields['threat.indicator.type'] = ['file'];
|
||||
indicator.fields['threat.indicator.file.hash.sha256'] = ['sample_sha256_hash'];
|
||||
|
||||
return indicator;
|
||||
};
|
20
x-pack/plugins/threat_intelligence/cypress/cypress.json
Normal file
20
x-pack/plugins/threat_intelligence/cypress/cypress.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"baseUrl": "http://localhost:5601",
|
||||
"defaultCommandTimeout": 120000,
|
||||
"execTimeout": 120000,
|
||||
"pageLoadTimeout": 120000,
|
||||
"retries": {
|
||||
"runMode": 2
|
||||
},
|
||||
"screenshotsFolder": "../../../target/kibana-threat-intelligence/cypress/screenshots",
|
||||
"trashAssetsBeforeRuns": false,
|
||||
"video": false,
|
||||
"videosFolder": "../../../target/kibana-threat-intelligence/cypress/videos",
|
||||
"viewportHeight": 946,
|
||||
"viewportWidth": 1680,
|
||||
"env": {
|
||||
"protocol": "http",
|
||||
"hostname": "localhost",
|
||||
"configport": "5601"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 {
|
||||
DEFAULT_LAYOUT_TITLE,
|
||||
FLYOUT_JSON,
|
||||
FLYOUT_TABLE,
|
||||
FLYOUT_TABS,
|
||||
FLYOUT_TITLE,
|
||||
INDICATORS_TABLE,
|
||||
TOGGLE_FLYOUT_BUTTON,
|
||||
} from '../../screens/indicators';
|
||||
import { login } from '../../tasks/login';
|
||||
|
||||
before(() => {
|
||||
login();
|
||||
});
|
||||
|
||||
describe('Indicators page', () => {
|
||||
before(() => {
|
||||
cy.visit('/app/security/threat_intelligence');
|
||||
});
|
||||
|
||||
it('should navigate to the indicators page, click on a flyout button and inspect flyout', () => {
|
||||
cy.get(DEFAULT_LAYOUT_TITLE).should('have.text', 'Indicators');
|
||||
|
||||
cy.get(INDICATORS_TABLE).should('exist');
|
||||
|
||||
cy.get(TOGGLE_FLYOUT_BUTTON).should('exist').first().click();
|
||||
|
||||
cy.get(FLYOUT_TITLE).should('contain', 'Indicator:');
|
||||
|
||||
cy.get(FLYOUT_TABLE).should('exist').and('contain.text', 'threat.indicator.type');
|
||||
|
||||
cy.get(FLYOUT_TABS).should('exist').children().should('have.length', 2).last().click();
|
||||
|
||||
cy.get(FLYOUT_JSON).should('exist').and('contain.text', 'threat.indicator.type');
|
||||
});
|
||||
});
|
29
x-pack/plugins/threat_intelligence/cypress/plugins/index.js
Normal file
29
x-pack/plugins/threat_intelligence/cypress/plugins/index.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { TABLE_TEST_ID as FLYOUT_TABLE_TEST_ID } from '../../public/modules/indicators/components/indicators_flyout_table/indicators_flyout_table';
|
||||
import { CODE_BLOCK_TEST_ID } from '../../public/modules/indicators/components/indicators_flyout_json/indicators_flyout_json';
|
||||
import { TABS_TEST_ID } from '../../public/modules/indicators/components/indicators_flyout/indicators_flyout';
|
||||
import { BUTTON_TEST_ID } from '../../public/modules/indicators/components/open_indicator_flyout_button/open_indicator_flyout_button';
|
||||
import { TABLE_TEST_ID as INDICATORS_TABLE_TEST_ID } from '../../public/modules/indicators/components/indicators_table/indicators_table';
|
||||
import { TITLE_TEST_ID as LAYOUT_TITLE_TEST_ID } from '../../public/components/layout';
|
||||
import { TITLE_TEST_ID as FLYOUT_TITLE_TEST_ID } from '../../public/modules/indicators/components/indicators_flyout/indicators_flyout';
|
||||
|
||||
export const DEFAULT_LAYOUT_TITLE = `[data-test-subj="${LAYOUT_TITLE_TEST_ID}"]`;
|
||||
|
||||
export const INDICATORS_TABLE = `[data-test-subj="${INDICATORS_TABLE_TEST_ID}"]`;
|
||||
|
||||
export const TOGGLE_FLYOUT_BUTTON = `[data-test-subj="${BUTTON_TEST_ID}"]`;
|
||||
|
||||
export const FLYOUT_TITLE = `[data-test-subj="${FLYOUT_TITLE_TEST_ID}"]`;
|
||||
|
||||
export const FLYOUT_TABS = `[data-test-subj="${TABS_TEST_ID}"]`;
|
||||
|
||||
export const FLYOUT_TABLE = `[data-test-subj="${FLYOUT_TABLE_TEST_ID}"]`;
|
||||
|
||||
export const FLYOUT_JSON = `[data-test-subj="${CODE_BLOCK_TEST_ID}"]`;
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
27
x-pack/plugins/threat_intelligence/cypress/support/index.js
Normal file
27
x-pack/plugins/threat_intelligence/cypress/support/index.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
341
x-pack/plugins/threat_intelligence/cypress/tasks/login.ts
Normal file
341
x-pack/plugins/threat_intelligence/cypress/tasks/login.ts
Normal file
|
@ -0,0 +1,341 @@
|
|||
/*
|
||||
* 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 Url from 'url';
|
||||
import type { UrlObject } from 'url';
|
||||
|
||||
import * as yaml from 'js-yaml';
|
||||
|
||||
import type { ROLES } from './privileges';
|
||||
import { hostDetailsUrl, LOGOUT_URL } from './navigation';
|
||||
|
||||
/**
|
||||
* Credentials in the `kibana.dev.yml` config file will be used to authenticate
|
||||
* with Kibana when credentials are not provided via environment variables
|
||||
*/
|
||||
const KIBANA_DEV_YML_PATH = '../../../config/kibana.dev.yml';
|
||||
|
||||
/**
|
||||
* The configuration path in `kibana.dev.yml` to the username to be used when
|
||||
* authenticating with Kibana.
|
||||
*/
|
||||
const ELASTICSEARCH_USERNAME_CONFIG_PATH = 'config.elasticsearch.username';
|
||||
|
||||
/**
|
||||
* The configuration path in `kibana.dev.yml` to the password to be used when
|
||||
* authenticating with Kibana.
|
||||
*/
|
||||
const ELASTICSEARCH_PASSWORD_CONFIG_PATH = 'config.elasticsearch.password';
|
||||
|
||||
/**
|
||||
* The `CYPRESS_ELASTICSEARCH_USERNAME` environment variable specifies the
|
||||
* username to be used when authenticating with Kibana
|
||||
*/
|
||||
const ELASTICSEARCH_USERNAME = 'ELASTICSEARCH_USERNAME';
|
||||
|
||||
/**
|
||||
* The `CYPRESS_ELASTICSEARCH_PASSWORD` environment variable specifies the
|
||||
* username to be used when authenticating with Kibana
|
||||
*/
|
||||
const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD';
|
||||
|
||||
/**
|
||||
* The Kibana server endpoint used for authentication
|
||||
*/
|
||||
const LOGIN_API_ENDPOINT = '/internal/security/login';
|
||||
|
||||
/**
|
||||
* cy.visit will default to the baseUrl which uses the default kibana test user
|
||||
* This function will override that functionality in cy.visit by building the baseUrl
|
||||
* directly from the environment variables set up in x-pack/test/security_solution_cypress/runner.ts
|
||||
*
|
||||
* @param role string role/user to log in with
|
||||
* @param route string route to visit
|
||||
*/
|
||||
export const getUrlWithRoute = (role: ROLES, route: string) => {
|
||||
const url = Cypress.config().baseUrl;
|
||||
const kibana = new URL(String(url));
|
||||
const theUrl = `${Url.format({
|
||||
auth: `${role}:changeme`,
|
||||
username: role,
|
||||
password: 'changeme',
|
||||
protocol: kibana.protocol.replace(':', ''),
|
||||
hostname: kibana.hostname,
|
||||
port: kibana.port,
|
||||
} as UrlObject)}${route.startsWith('/') ? '' : '/'}${route}`;
|
||||
cy.log(`origin: ${theUrl}`);
|
||||
return theUrl;
|
||||
};
|
||||
|
||||
interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a URL with basic auth using the passed in user.
|
||||
*
|
||||
* @param user the user information to build the basic auth with
|
||||
* @param route string route to visit
|
||||
*/
|
||||
export const constructUrlWithUser = (user: User, route: string) => {
|
||||
const url = Cypress.config().baseUrl;
|
||||
const kibana = new URL(String(url));
|
||||
const hostname = kibana.hostname;
|
||||
const username = user.username;
|
||||
const password = user.password;
|
||||
const protocol = kibana.protocol.replace(':', '');
|
||||
const port = kibana.port;
|
||||
|
||||
const path = `${route.startsWith('/') ? '' : '/'}${route}`;
|
||||
const strUrl = `${protocol}://${username}:${password}@${hostname}:${port}${path}`;
|
||||
const builtUrl = new URL(strUrl);
|
||||
|
||||
cy.log(`origin: ${builtUrl.href}`);
|
||||
return builtUrl.href;
|
||||
};
|
||||
|
||||
export const getCurlScriptEnvVars = () => ({
|
||||
ELASTICSEARCH_URL: Cypress.env('ELASTICSEARCH_URL'),
|
||||
ELASTICSEARCH_USERNAME: Cypress.env('ELASTICSEARCH_USERNAME'),
|
||||
ELASTICSEARCH_PASSWORD: Cypress.env('ELASTICSEARCH_PASSWORD'),
|
||||
KIBANA_URL: Cypress.config().baseUrl,
|
||||
});
|
||||
|
||||
export const postRoleAndUser = (role: ROLES) => {
|
||||
const env = getCurlScriptEnvVars();
|
||||
const detectionsRoleScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_role.sh`;
|
||||
const detectionsRoleJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_role.json`;
|
||||
const detectionsUserScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_user.sh`;
|
||||
const detectionsUserJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_user.json`;
|
||||
|
||||
// post the role
|
||||
cy.exec(`bash ${detectionsRoleScriptPath} ${detectionsRoleJsonPath}`, {
|
||||
env,
|
||||
});
|
||||
|
||||
// post the user associated with the role to elasticsearch
|
||||
cy.exec(`bash ${detectionsUserScriptPath} ${detectionsUserJsonPath}`, {
|
||||
env,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteRoleAndUser = (role: ROLES) => {
|
||||
const env = getCurlScriptEnvVars();
|
||||
const detectionsUserDeleteScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/delete_detections_user.sh`;
|
||||
|
||||
// delete the role
|
||||
cy.exec(`bash ${detectionsUserDeleteScriptPath}`, {
|
||||
env,
|
||||
});
|
||||
};
|
||||
|
||||
export const loginWithUser = (user: User) => {
|
||||
cy.request({
|
||||
body: {
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: {
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
},
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'POST',
|
||||
url: constructUrlWithUser(user, LOGIN_API_ENDPOINT),
|
||||
});
|
||||
};
|
||||
|
||||
export const loginWithRole = async (role: ROLES) => {
|
||||
postRoleAndUser(role);
|
||||
const theUrl = Url.format({
|
||||
auth: `${role}:changeme`,
|
||||
username: role,
|
||||
password: 'changeme',
|
||||
protocol: Cypress.env('protocol'),
|
||||
hostname: Cypress.env('hostname'),
|
||||
port: Cypress.env('configport'),
|
||||
} as UrlObject);
|
||||
cy.log(`origin: ${theUrl}`);
|
||||
cy.request({
|
||||
body: {
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: {
|
||||
username: role,
|
||||
password: 'changeme',
|
||||
},
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'POST',
|
||||
url: getUrlWithRoute(role, LOGIN_API_ENDPOINT),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticates with Kibana using, if specified, credentials specified by
|
||||
* environment variables. The credentials in `kibana.dev.yml` will be used
|
||||
* for authentication when the environment variables are unset.
|
||||
*
|
||||
* To speed the execution of tests, prefer this non-interactive authentication,
|
||||
* which is faster than authentication via Kibana's interactive login page.
|
||||
*/
|
||||
export const login = (role?: ROLES) => {
|
||||
if (role != null) {
|
||||
loginWithRole(role);
|
||||
} else if (credentialsProvidedByEnvironment()) {
|
||||
loginViaEnvironmentCredentials();
|
||||
} else {
|
||||
loginViaConfig();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns `true` if the credentials used to login to Kibana are provided
|
||||
* via environment variables
|
||||
*/
|
||||
const credentialsProvidedByEnvironment = (): boolean =>
|
||||
Cypress.env(ELASTICSEARCH_USERNAME) != null && Cypress.env(ELASTICSEARCH_PASSWORD) != null;
|
||||
|
||||
/**
|
||||
* Authenticates with Kibana by reading credentials from the
|
||||
* `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD`
|
||||
* environment variables, and POSTing the username and password directly to
|
||||
* Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed).
|
||||
*/
|
||||
const loginViaEnvironmentCredentials = () => {
|
||||
cy.log(
|
||||
`Authenticating via environment credentials from the \`CYPRESS_${ELASTICSEARCH_USERNAME}\` and \`CYPRESS_${ELASTICSEARCH_PASSWORD}\` environment variables`
|
||||
);
|
||||
|
||||
// programmatically authenticate without interacting with the Kibana login page
|
||||
cy.request({
|
||||
body: {
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: {
|
||||
username: Cypress.env(ELASTICSEARCH_USERNAME),
|
||||
password: Cypress.env(ELASTICSEARCH_PASSWORD),
|
||||
},
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-env' },
|
||||
method: 'POST',
|
||||
url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticates with Kibana by reading credentials from the
|
||||
* `kibana.dev.yml` file and POSTing the username and password directly to
|
||||
* Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed).
|
||||
*/
|
||||
const loginViaConfig = () => {
|
||||
cy.log(
|
||||
`Authenticating via config credentials \`${ELASTICSEARCH_USERNAME_CONFIG_PATH}\` and \`${ELASTICSEARCH_PASSWORD_CONFIG_PATH}\` from \`${KIBANA_DEV_YML_PATH}\``
|
||||
);
|
||||
|
||||
// read the login details from `kibana.dev.yaml`
|
||||
cy.readFile(KIBANA_DEV_YML_PATH).then((kibanaDevYml) => {
|
||||
const config = yaml.safeLoad(kibanaDevYml);
|
||||
|
||||
// programmatically authenticate without interacting with the Kibana login page
|
||||
cy.request({
|
||||
body: {
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: {
|
||||
username: config.elasticsearch.username,
|
||||
password: config.elasticsearch.password,
|
||||
},
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'POST',
|
||||
url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the configured auth details that were used to spawn cypress
|
||||
*
|
||||
* @returns the default Elasticsearch username and password for this environment
|
||||
*/
|
||||
export const getEnvAuth = (): User => {
|
||||
if (credentialsProvidedByEnvironment()) {
|
||||
return {
|
||||
username: Cypress.env(ELASTICSEARCH_USERNAME),
|
||||
password: Cypress.env(ELASTICSEARCH_PASSWORD),
|
||||
};
|
||||
} else {
|
||||
let user: User = { username: '', password: '' };
|
||||
cy.readFile(KIBANA_DEV_YML_PATH).then((devYml) => {
|
||||
const config = yaml.safeLoad(devYml);
|
||||
user = { username: config.elasticsearch.username, password: config.elasticsearch.password };
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticates with Kibana, visits the specified `url`, and waits for the
|
||||
* Kibana global nav to be displayed before continuing
|
||||
*/
|
||||
export const loginAndWaitForPage = (
|
||||
url: string,
|
||||
role?: ROLES,
|
||||
onBeforeLoadCallback?: (win: Cypress.AUTWindow) => void
|
||||
) => {
|
||||
login(role);
|
||||
cy.visit(
|
||||
`${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))`,
|
||||
{
|
||||
onBeforeLoad(win) {
|
||||
if (onBeforeLoadCallback) {
|
||||
onBeforeLoadCallback(win);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
cy.get('[data-test-subj="headerGlobalNav"]');
|
||||
};
|
||||
export const waitForPage = (url: string) => {
|
||||
cy.visit(
|
||||
`${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))`
|
||||
);
|
||||
cy.get('[data-test-subj="headerGlobalNav"]');
|
||||
};
|
||||
|
||||
export const loginAndWaitForPageWithoutDateRange = (url: string, role?: ROLES) => {
|
||||
login(role);
|
||||
cy.visit(role ? getUrlWithRoute(role, url) : url);
|
||||
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
|
||||
};
|
||||
|
||||
export const loginWithUserAndWaitForPage = (url: string, user: User) => {
|
||||
loginWithUser(user);
|
||||
cy.visit(constructUrlWithUser(user, url));
|
||||
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
|
||||
};
|
||||
|
||||
export const loginAndWaitForHostDetailsPage = (hostName = 'suricata-iowa') => {
|
||||
loginAndWaitForPage(hostDetailsUrl(hostName));
|
||||
cy.get('[data-test-subj="loading-spinner"]', { timeout: 12000 }).should('not.exist');
|
||||
};
|
||||
|
||||
export const waitForPageWithoutDateRange = (url: string, role?: ROLES) => {
|
||||
cy.visit(role ? getUrlWithRoute(role, url) : url);
|
||||
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
|
||||
};
|
||||
|
||||
export const logout = () => {
|
||||
cy.visit(LOGOUT_URL);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 const INTEGRATIONS = 'app/integrations#/';
|
||||
export const FLEET = 'app/fleet/';
|
||||
export const LOGIN_API_ENDPOINT = '/internal/security/login';
|
||||
export const LOGOUT_API_ENDPOINT = '/api/security/logout';
|
||||
export const LOGIN_URL = '/login';
|
||||
export const LOGOUT_URL = '/logout';
|
||||
|
||||
export const hostDetailsUrl = (hostName: string) =>
|
||||
`/app/security/hosts/${hostName}/authentications`;
|
||||
|
||||
export const navigateTo = (page: string) => {
|
||||
cy.visit(page);
|
||||
};
|
232
x-pack/plugins/threat_intelligence/cypress/tasks/privileges.ts
Normal file
232
x-pack/plugins/threat_intelligence/cypress/tasks/privileges.ts
Normal file
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
* 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 { constructUrlWithUser, getEnvAuth } from './login';
|
||||
|
||||
interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
description?: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
username: string;
|
||||
full_name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface FeaturesPrivileges {
|
||||
[featureId: string]: string[];
|
||||
}
|
||||
|
||||
interface ElasticsearchIndices {
|
||||
names: string[];
|
||||
privileges: string[];
|
||||
}
|
||||
|
||||
interface ElasticSearchPrivilege {
|
||||
cluster?: string[];
|
||||
indices?: ElasticsearchIndices[];
|
||||
}
|
||||
|
||||
interface KibanaPrivilege {
|
||||
spaces: string[];
|
||||
base?: string[];
|
||||
feature?: FeaturesPrivileges;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
name: string;
|
||||
privileges: {
|
||||
elasticsearch?: ElasticSearchPrivilege;
|
||||
kibana?: KibanaPrivilege[];
|
||||
};
|
||||
}
|
||||
|
||||
// Create roles with allowed combinations of Fleet and Integrations
|
||||
export const FleetAllIntegrAllRole: Role = {
|
||||
name: 'fleet_all_int_all_role',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
fleetv2: ['all'],
|
||||
fleet: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const FleetAllIntegrAllUser: User = {
|
||||
username: 'fleet_all_int_all_user',
|
||||
password: 'password',
|
||||
roles: [FleetAllIntegrAllRole.name],
|
||||
};
|
||||
|
||||
export const FleetAllIntegrReadRole: Role = {
|
||||
name: 'fleet_all_int_read_user',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
fleetv2: ['all'],
|
||||
fleet: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
export const FleetAllIntegrReadUser: User = {
|
||||
username: 'fleet_all_int_read_user',
|
||||
password: 'password',
|
||||
roles: [FleetAllIntegrReadRole.name],
|
||||
};
|
||||
export const FleetAllIntegrNoneRole: Role = {
|
||||
name: 'fleet_all_int_none_role',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
fleetv2: ['all'],
|
||||
fleet: ['none'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
export const FleetAllIntegrNoneUser: User = {
|
||||
username: 'fleet_all_int_none_user',
|
||||
password: 'password',
|
||||
roles: [FleetAllIntegrNoneRole.name],
|
||||
};
|
||||
export const FleetNoneIntegrAllRole: Role = {
|
||||
name: 'fleet_none_int_all_role',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
fleetv2: ['none'],
|
||||
fleet: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
export const FleetNoneIntegrAllUser: User = {
|
||||
username: 'fleet_none_int_all_user',
|
||||
password: 'password',
|
||||
roles: [FleetNoneIntegrAllRole.name],
|
||||
};
|
||||
|
||||
const getUserInfo = (user: User): UserInfo => ({
|
||||
username: user.username,
|
||||
full_name: user.username.replace('_', ' '),
|
||||
email: `${user.username}@elastic.co`,
|
||||
});
|
||||
|
||||
export enum ROLES {
|
||||
elastic = 'elastic',
|
||||
}
|
||||
|
||||
export const createUsersAndRoles = (users: User[], roles: Role[]) => {
|
||||
const envUser = getEnvAuth();
|
||||
for (const role of roles) {
|
||||
cy.log(`Creating role: ${JSON.stringify(role)}`);
|
||||
cy.request({
|
||||
body: role.privileges,
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'PUT',
|
||||
url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`),
|
||||
})
|
||||
.its('status')
|
||||
.should('eql', 204);
|
||||
}
|
||||
|
||||
for (const user of users) {
|
||||
const userInfo = getUserInfo(user);
|
||||
cy.log(`Creating user: ${JSON.stringify(user)}`);
|
||||
cy.request({
|
||||
body: {
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
roles: user.roles,
|
||||
full_name: userInfo.full_name,
|
||||
email: userInfo.email,
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'POST',
|
||||
url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`),
|
||||
})
|
||||
.its('status')
|
||||
.should('eql', 200);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUsersAndRoles = (users: User[], roles: Role[]) => {
|
||||
const envUser = getEnvAuth();
|
||||
for (const user of users) {
|
||||
cy.log(`Deleting user: ${JSON.stringify(user)}`);
|
||||
cy.request({
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'DELETE',
|
||||
url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`),
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
.its('status')
|
||||
.should('oneOf', [204, 404]);
|
||||
}
|
||||
|
||||
for (const role of roles) {
|
||||
cy.log(`Deleting role: ${JSON.stringify(role)}`);
|
||||
cy.request({
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'DELETE',
|
||||
url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`),
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
.its('status')
|
||||
.should('oneOf', [204, 404]);
|
||||
}
|
||||
};
|
23
x-pack/plugins/threat_intelligence/cypress/tsconfig.json
Normal file
23
x-pack/plugins/threat_intelligence/cypress/tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"include": [
|
||||
"**/*",
|
||||
"fixtures/**/*.json"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"cypress",
|
||||
"cypress-file-upload",
|
||||
"cypress-pipe",
|
||||
"node",
|
||||
],
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../tsconfig.json" }
|
||||
]
|
||||
}
|
12
x-pack/plugins/threat_intelligence/jest.config.js
Normal file
12
x-pack/plugins/threat_intelligence/jest.config.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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/plugins/threat_intelligence'],
|
||||
};
|
12
x-pack/plugins/threat_intelligence/kibana.json
Normal file
12
x-pack/plugins/threat_intelligence/kibana.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"id": "threatIntelligence",
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"ui": true,
|
||||
"owner": {
|
||||
"name": "Protections Experience Team",
|
||||
"githubTeam": "protections-experience"
|
||||
},
|
||||
"description": "Elastic threat intelligence helps you see if you are open to or have been subject to current or historical known threats",
|
||||
"requiredPlugins": ["data", "kibanaReact"]
|
||||
}
|
23
x-pack/plugins/threat_intelligence/package.json
Normal file
23
x-pack/plugins/threat_intelligence/package.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"author": "Elastic",
|
||||
"name": "threat_intelligence",
|
||||
"scripts": {
|
||||
"cypress": "../../../node_modules/.bin/cypress",
|
||||
"cypress:open": "yarn cypress open --config-file ./cypress/cypress.json",
|
||||
"cypress:open:ccs": "yarn cypress:open --config integrationFolder=./cypress/ccs_integration",
|
||||
"cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/threat_intelligence_cypress/visual_config.ts",
|
||||
"cypress:open:upgrade": "yarn cypress:open --config integrationFolder=./cypress/upgrade_integration",
|
||||
"cypress:run": "yarn cypress:run:reporter --browser chrome --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status",
|
||||
"cypress:run:spec": "yarn cypress:run:reporter --browser chrome --spec ${SPEC_LIST:-'./cypress/integration/**/*.spec.ts'}; status=$?; yarn junit:merge && exit $status",
|
||||
"cypress:run:cases": "yarn cypress:run:reporter --browser chrome --spec './cypress/integration/cases/*.spec.ts'; status=$?; yarn junit:merge && exit $status",
|
||||
"cypress:run:firefox": "yarn cypress:run:reporter --browser firefox --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status",
|
||||
"cypress:run:reporter": "yarn cypress run --config-file ./cypress/cypress.json --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json",
|
||||
"cypress:run:respops": "yarn cypress:run:reporter --browser chrome --spec ./cypress/integration/detection_alerts/*.spec.ts,./cypress/integration/detection_rules/*.spec.ts,./cypress/integration/exceptions/*.spec.ts; status=$?; yarn junit:merge && exit $status",
|
||||
"cypress:run:ccs": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/ccs_integration; status=$?; yarn junit:merge && exit $status",
|
||||
"cypress:run-as-ci": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/threat_intelligence_cypress/cli_config_parallel.ts",
|
||||
"cypress:run-as-ci:firefox": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/threat_intelligence_cypress/config.firefox.ts",
|
||||
"cypress:run:upgrade": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/upgrade_integration",
|
||||
"cypress:run:upgrade:old": "yarn cypress:run:reporter --browser chrome --config integrationFolder=./cypress/upgrade_integration --spec ./cypress/upgrade_integration/threat_hunting/**/*.spec.ts,./cypress/upgrade_integration/detections/**/custom_query_rule.spec.ts; status=$?; yarn junit:merge && exit $status",
|
||||
"junit:merge": "../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-threat-intelligence/cypress/results/mochawesome*.json > ../../../target/kibana-threat-intelligence/cypress/results/output.json && ../../../node_modules/.bin/marge ../../../target/kibana-threat-intelligence/cypress/results/output.json --reportDir ../../../target/kibana-threat-intelligence/cypress/results && mkdir -p ../../../target/junit && cp ../../../target/kibana-threat-intelligence/cypress/results/*.xml ../../../target/junit/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { Observable } from 'rxjs';
|
||||
import * as hook from '../../hooks/use_kibana';
|
||||
|
||||
export const mockSearchService = (subject: Observable<unknown>) => {
|
||||
const search = jest.fn().mockReturnValue(subject);
|
||||
const showError = jest.fn();
|
||||
const getUiSetting = jest.fn();
|
||||
|
||||
(hook as jest.Mocked<typeof hook>).useKibana.mockReturnValue({
|
||||
services: { data: { search: { search, showError } }, uiSettings: { get: getUiSetting } },
|
||||
} as any);
|
||||
|
||||
return {
|
||||
search,
|
||||
showError,
|
||||
getUiSetting,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { VFC } from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const TestProvidersComponent: VFC<Props> = ({ children }) => (
|
||||
<I18nProvider>{children}</I18nProvider>
|
||||
);
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 moment from 'moment-timezone';
|
||||
import { fullDateFormatter } from './dates';
|
||||
import { EMPTY_VALUE } from '../../../common/constants';
|
||||
|
||||
const mockValidStringDate = '1 Jan 2022 00:00:00 GMT';
|
||||
const mockInvalidStringDate = 'invalid date';
|
||||
|
||||
moment.suppressDeprecationWarnings = true;
|
||||
moment.tz.setDefault('UTC');
|
||||
|
||||
describe('dates', () => {
|
||||
describe('fullDateFormatter', () => {
|
||||
it('should return date string in FULL_DATE format for valid string date', () => {
|
||||
expect(fullDateFormatter(mockValidStringDate)).toEqual('January 1st 2022 @ 00:00:00');
|
||||
});
|
||||
|
||||
it('should return date string in FULL_DATE format for valid moment date', () => {
|
||||
const date = moment(mockValidStringDate);
|
||||
expect(fullDateFormatter(date)).toEqual('January 1st 2022 @ 00:00:00');
|
||||
});
|
||||
|
||||
it('should return EMPTY_VALUE for invalid string date', () => {
|
||||
expect(fullDateFormatter(mockInvalidStringDate)).toEqual(EMPTY_VALUE);
|
||||
});
|
||||
|
||||
it('should return EMPTY_VALUE for invalid moment date', () => {
|
||||
const date = moment(mockInvalidStringDate);
|
||||
expect(fullDateFormatter(date)).toEqual(EMPTY_VALUE);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
moment.suppressDeprecationWarnings = true;
|
||||
import { EMPTY_VALUE } from '../../../common/constants';
|
||||
|
||||
const FULL_DATE = 'MMMM Do YYYY @ HH:mm:ss';
|
||||
|
||||
export const fullDateFormatter = (date: string | moment.Moment): string => {
|
||||
const momentDate: moment.Moment = typeof date === 'string' ? moment(date) : date;
|
||||
return momentDate.isValid() ? momentDate.format(FULL_DATE) : EMPTY_VALUE;
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { EmptyPage } from '.';
|
||||
|
||||
export default {
|
||||
component: BasicEmptyPage,
|
||||
title: 'EmptyPage',
|
||||
};
|
||||
|
||||
export function BasicEmptyPage() {
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
http: {
|
||||
basePath: {
|
||||
get: () => '',
|
||||
},
|
||||
},
|
||||
docLinks: {
|
||||
links: {
|
||||
securitySolution: {
|
||||
threatIntelInt: 'https://google.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as Partial<CoreStart>);
|
||||
return (
|
||||
<KibanaReactContext.Provider>
|
||||
<EmptyPage />
|
||||
</KibanaReactContext.Provider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import { TestProvidersComponent } from '../../common/mocks/test_providers';
|
||||
import { EmptyPage, INTEGRATION_LINK_ID, DOCS_LINK_TEST_ID } from './empty_page';
|
||||
import { useIntegrationsPageLink } from '../../hooks/use_integrations_page_link';
|
||||
import { useTIDocumentationLink } from '../../hooks/use_documentation_link';
|
||||
|
||||
jest.mock('../../hooks/use_integrations_page_link');
|
||||
jest.mock('../../hooks/use_documentation_link');
|
||||
|
||||
const INTEGRATION_HREF = 'INTEGRATION_HREF';
|
||||
const DOCUMENTATION_HREF = 'DOCUMENTATION_HREF';
|
||||
|
||||
describe('<EmptyPage />', () => {
|
||||
it('should render', () => {
|
||||
(
|
||||
useIntegrationsPageLink as jest.MockedFunction<typeof useIntegrationsPageLink>
|
||||
).mockReturnValue(INTEGRATION_HREF);
|
||||
(useTIDocumentationLink as jest.MockedFunction<typeof useTIDocumentationLink>).mockReturnValue(
|
||||
DOCUMENTATION_HREF
|
||||
);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProvidersComponent>
|
||||
<EmptyPage />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
const integrationsPageLink = getByTestId(`${INTEGRATION_LINK_ID}`);
|
||||
|
||||
expect(screen.getByText('Get started with Elastic Threat Intelligence')).toBeInTheDocument();
|
||||
|
||||
expect(integrationsPageLink).toBeInTheDocument();
|
||||
expect(integrationsPageLink).toHaveAttribute('href', INTEGRATION_HREF);
|
||||
|
||||
const documentationLink = getByTestId(`${DOCS_LINK_TEST_ID}`);
|
||||
|
||||
expect(documentationLink).toBeInTheDocument();
|
||||
expect(documentationLink).toHaveAttribute('href', DOCUMENTATION_HREF);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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, { VFC } from 'react';
|
||||
|
||||
import { EuiEmptyPrompt, EuiImage, EuiButton, EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useTIDocumentationLink } from '../../hooks/use_documentation_link';
|
||||
import { useIntegrationsPageLink } from '../../hooks/use_integrations_page_link';
|
||||
import illustration from './integrations_light.svg';
|
||||
|
||||
export const DOCS_LINK_TEST_ID = 'tiEmptyPageDocsLink';
|
||||
export const EMPTY_PROMPT_TEST_ID = 'tiEmptyPage';
|
||||
export const INTEGRATION_LINK_ID = 'tiEmptyPageIntegrationsPageLink';
|
||||
|
||||
export const EmptyPage: VFC = () => {
|
||||
const integrationsPageLink = useIntegrationsPageLink();
|
||||
const documentationLink = useTIDocumentationLink();
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
icon={
|
||||
<EuiImage
|
||||
size="fullWidth"
|
||||
alt={i18n.translate('xpack.threatIntelligence.common.emptyPage.imgAlt', {
|
||||
defaultMessage: 'Enable Threat Intelligence Integrations',
|
||||
})}
|
||||
src={illustration}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.title"
|
||||
defaultMessage="Get started with Elastic Threat Intelligence"
|
||||
/>
|
||||
</h3>
|
||||
}
|
||||
titleSize="s"
|
||||
layout="horizontal"
|
||||
color="transparent"
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.body1"
|
||||
defaultMessage="Elastic Threat Intelligence makes it easy to analyze and investigate potential security
|
||||
threats by aggregating data from multiple sources in one place."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.body2"
|
||||
defaultMessage="You’ll be able to view data from all activated threat intelligence feeds and take action
|
||||
from this page."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.body3"
|
||||
defaultMessage="To get started with Elastic Threat Intelligence, enable one or more Threat Intelligence
|
||||
Integrations from the Integrations page or ingest data using filebeat. For more
|
||||
information, view the {docsLink}."
|
||||
values={{
|
||||
docsLink: (
|
||||
<EuiLink
|
||||
href={documentationLink}
|
||||
target="_blank"
|
||||
data-test-subj={DOCS_LINK_TEST_ID}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.docsLinkText"
|
||||
defaultMessage="Security app documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj={INTEGRATION_LINK_ID}
|
||||
href={integrationsPageLink}
|
||||
color="primary"
|
||||
iconType="plusInCircle"
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.buttonText"
|
||||
defaultMessage="Add Integrations"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
data-test-subj={EMPTY_PROMPT_TEST_ID}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 * from './empty_page';
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 89 KiB |
|
@ -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 * from './layout';
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { Story } from '@storybook/react';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { DefaultPageLayout } from './layout';
|
||||
|
||||
export default {
|
||||
component: DefaultPageLayout,
|
||||
title: 'DefaultPageLayout',
|
||||
};
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
const title = 'Title with border below';
|
||||
const children = <EuiText>Content with border above</EuiText>;
|
||||
|
||||
return <DefaultPageLayout pageTitle={title} children={children} />;
|
||||
};
|
||||
|
||||
export const NoBorder: Story<void> = () => {
|
||||
const title = 'Title without border';
|
||||
const border = false;
|
||||
const children = <EuiText>Content without border</EuiText>;
|
||||
|
||||
return <DefaultPageLayout pageTitle={title} border={border} children={children} />;
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { DefaultPageLayout, TITLE_TEST_ID } from './layout';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
describe('<Layout />', () => {
|
||||
describe('when pageTitle is not specified', () => {
|
||||
beforeEach(() => {
|
||||
render(<DefaultPageLayout />);
|
||||
});
|
||||
|
||||
it('should not render secondary heading', () => {
|
||||
expect(screen.queryByTestId(`${TITLE_TEST_ID}`)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when pageTitle is passed, it should be rendered as secondary heading', () => {
|
||||
beforeEach(() => {
|
||||
render(<DefaultPageLayout pageTitle="Stranger Threats" />);
|
||||
});
|
||||
|
||||
it('should render secondary heading', () => {
|
||||
expect(screen.queryByText('Stranger Threats')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { EuiPageHeader, EuiPageHeaderSection, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
export interface LayoutProps {
|
||||
pageTitle?: string;
|
||||
border?: boolean;
|
||||
}
|
||||
|
||||
export const TITLE_TEST_ID = 'tiDefaultPageLayoutTitle';
|
||||
|
||||
export const DefaultPageLayout: FC<LayoutProps> = ({ children, pageTitle, border = true }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiPageHeader alignItems="center" bottomBorder={border}>
|
||||
<EuiPageHeaderSection>
|
||||
{pageTitle && (
|
||||
<EuiText>
|
||||
<h2 data-test-subj={TITLE_TEST_ID}>{pageTitle}</h2>
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiSpacer size="l" />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { DocLinks } from '@kbn/doc-links';
|
||||
import { useKibana } from './use_kibana';
|
||||
|
||||
const useKibanaDocumentationLinks = (): DocLinks => useKibana().services.docLinks.links;
|
||||
|
||||
export const useTIDocumentationLink = (): string =>
|
||||
useKibanaDocumentationLinks().securitySolution.threatIntelInt;
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { useKibana } from './use_kibana';
|
||||
|
||||
const useKibanaBasePath = (): string => useKibana().services.http.basePath.get();
|
||||
|
||||
export const useIntegrationsPageLink = () =>
|
||||
`${useKibanaBasePath()}/app/integrations/browse?q=threat%20intelligence`;
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { KibanaContextProvider, useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import { Services } from '../types';
|
||||
|
||||
const useTypedKibana = () => useKibana<Services>();
|
||||
|
||||
export { KibanaContextProvider, useTypedKibana as useKibana };
|
14
x-pack/plugins/threat_intelligence/public/index.ts
Executable file
14
x-pack/plugins/threat_intelligence/public/index.ts
Executable file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { ThreatIntelligencePlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new ThreatIntelligencePlugin();
|
||||
}
|
||||
|
||||
export type { ThreatIntelligencePluginSetup, ThreatIntelligencePluginStart } from './types';
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { Story } from '@storybook/react';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsFlyout } from './indicators_flyout';
|
||||
|
||||
export default {
|
||||
component: IndicatorsFlyout,
|
||||
title: 'IndicatorsFlyout',
|
||||
};
|
||||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
// eslint-disable-next-line no-console
|
||||
return <IndicatorsFlyout indicator={mockIndicator} closeFlyout={() => console.log('closing')} />;
|
||||
};
|
||||
|
||||
export const EmtpyIndicator: Story<void> = () => {
|
||||
return (
|
||||
<IndicatorsFlyout
|
||||
indicator={{ fields: {} } as Indicator}
|
||||
// eslint-disable-next-line no-console
|
||||
closeFlyout={() => console.log('closing')}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { IndicatorsFlyout, SUBTITLE_TEST_ID, TITLE_TEST_ID } from './indicators_flyout';
|
||||
import { generateMockIndicator, RawIndicatorFieldId } from '../../../../../common/types/indicator';
|
||||
import { unwrapValue } from '../../lib/unwrap_value';
|
||||
import { displayValue } from '../../lib/display_value';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
import { fullDateFormatter } from '../../../../common/utils/dates';
|
||||
|
||||
const mockIndicator = generateMockIndicator();
|
||||
|
||||
describe('<IndicatorsFlyout />', () => {
|
||||
it('should render ioc id in title and first_seen in subtitle', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsFlyout indicator={mockIndicator} closeFlyout={() => {}} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(getByTestId(TITLE_TEST_ID).innerHTML).toContain(
|
||||
`Indicator: ${displayValue(mockIndicator)}`
|
||||
);
|
||||
expect(getByTestId(SUBTITLE_TEST_ID).innerHTML).toContain(
|
||||
`First seen: ${fullDateFormatter(
|
||||
unwrapValue(mockIndicator, RawIndicatorFieldId.FirstSeen) as string
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it(`should render ${EMPTY_VALUE} in on invalid indicator first_seen value`, () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsFlyout indicator={{ fields: {} }} closeFlyout={() => {}} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(getByTestId(TITLE_TEST_ID).innerHTML).toContain(`Indicator: ${EMPTY_VALUE}`);
|
||||
expect(getByTestId(SUBTITLE_TEST_ID).innerHTML).toContain(`First seen: ${EMPTY_VALUE}`);
|
||||
});
|
||||
|
||||
it(`should render ${EMPTY_VALUE} in title and subtitle on invalid indicator`, () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsFlyout
|
||||
indicator={{ fields: { 'threat.indicator.first_seen': ['abc'] } }}
|
||||
closeFlyout={() => {}}
|
||||
/>
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(getByTestId(TITLE_TEST_ID).innerHTML).toContain(`Indicator: ${EMPTY_VALUE}`);
|
||||
expect(getByTestId(SUBTITLE_TEST_ID).innerHTML).toContain(`First seen: ${EMPTY_VALUE}`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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, { useMemo, useState, VFC } from 'react';
|
||||
import {
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiSpacer,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { fullDateFormatter } from '../../../../common/utils/dates';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { Indicator, RawIndicatorFieldId } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsFlyoutJson } from '../indicators_flyout_json/indicators_flyout_json';
|
||||
import { IndicatorsFlyoutTable } from '../indicators_flyout_table/indicators_flyout_table';
|
||||
import { unwrapValue } from '../../lib/unwrap_value';
|
||||
import { displayValue } from '../../lib/display_value';
|
||||
|
||||
export const TITLE_TEST_ID = 'tiIndicatorFlyoutTitle';
|
||||
export const SUBTITLE_TEST_ID = 'tiIndicatorFlyoutSubtitle';
|
||||
export const TABS_TEST_ID = 'tiIndicatorFlyoutTabs';
|
||||
|
||||
const enum TAB_IDS {
|
||||
table,
|
||||
json,
|
||||
}
|
||||
|
||||
export const IndicatorsFlyout: VFC<{ indicator: Indicator; closeFlyout: () => void }> = ({
|
||||
indicator,
|
||||
closeFlyout,
|
||||
}) => {
|
||||
const [selectedTabId, setSelectedTabId] = useState(TAB_IDS.table);
|
||||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: TAB_IDS.table,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.flyout.tableTabLabel"
|
||||
defaultMessage="Table"
|
||||
/>
|
||||
),
|
||||
content: <IndicatorsFlyoutTable indicator={indicator} />,
|
||||
},
|
||||
{
|
||||
id: TAB_IDS.json,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.flyout.jsonTabLabel"
|
||||
defaultMessage="JSON"
|
||||
/>
|
||||
),
|
||||
content: <IndicatorsFlyoutJson indicator={indicator} />,
|
||||
},
|
||||
],
|
||||
[indicator]
|
||||
);
|
||||
const onSelectedTabChanged = (id: number) => setSelectedTabId(id);
|
||||
|
||||
const renderTabs = tabs.map((tab, index) => (
|
||||
<EuiTab
|
||||
onClick={() => onSelectedTabChanged(tab.id)}
|
||||
isSelected={tab.id === selectedTabId}
|
||||
key={index}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
));
|
||||
|
||||
const selectedTabContent = useMemo(
|
||||
() => tabs.find((obj) => obj.id === selectedTabId)?.content,
|
||||
[selectedTabId, tabs]
|
||||
);
|
||||
|
||||
const firstSeen = unwrapValue(indicator, RawIndicatorFieldId.FirstSeen);
|
||||
const value = displayValue(indicator) || EMPTY_VALUE;
|
||||
const formattedFirstSeen: string = firstSeen ? fullDateFormatter(firstSeen) : EMPTY_VALUE;
|
||||
const flyoutTitleId = useGeneratedHtmlId({
|
||||
prefix: 'simpleFlyoutTitle',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={closeFlyout} aria-labelledby={flyoutTitleId}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle>
|
||||
<h2 data-test-subj={TITLE_TEST_ID} id={flyoutTitleId}>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.flyout.panelTitle"
|
||||
defaultMessage="Indicator: {title}"
|
||||
values={{ title: value }}
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size={'xs'}>
|
||||
<p data-test-subj={SUBTITLE_TEST_ID}>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.flyout.panelSubTitle"
|
||||
defaultMessage="First seen: {subTitle}"
|
||||
values={{ subTitle: formattedFirstSeen }}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTabs data-test-subj={TABS_TEST_ID} style={{ marginBottom: '-25px' }}>
|
||||
{renderTabs}
|
||||
</EuiTabs>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>{selectedTabContent}</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { Story } from '@storybook/react';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsFlyoutJson } from './indicators_flyout_json';
|
||||
|
||||
export default {
|
||||
component: IndicatorsFlyoutJson,
|
||||
title: 'IndicatorsFlyoutJson',
|
||||
};
|
||||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
return <IndicatorsFlyoutJson indicator={mockIndicator} />;
|
||||
};
|
||||
|
||||
export const EmptyIndicator: Story<void> = () => {
|
||||
return <IndicatorsFlyoutJson indicator={{} as unknown as Indicator} />;
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import {
|
||||
CODE_BLOCK_TEST_ID,
|
||||
EMPTY_PROMPT_TEST_ID,
|
||||
IndicatorsFlyoutJson,
|
||||
} from './indicators_flyout_json';
|
||||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
|
||||
describe('<IndicatorsFlyoutJson />', () => {
|
||||
it('should render code block component on valid indicator', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsFlyoutJson indicator={mockIndicator} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(getByTestId(CODE_BLOCK_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error message on invalid indicator', () => {
|
||||
const { getByTestId, getByText } = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsFlyoutJson indicator={{} as unknown as Indicator} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(getByTestId(EMPTY_PROMPT_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByText('Unable to display indicator information')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText('There was an error displaying the indicator fields and values.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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, { VFC } from 'react';
|
||||
import { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
|
||||
export const EMPTY_PROMPT_TEST_ID = 'tiFlyoutJsonEmptyPrompt';
|
||||
export const CODE_BLOCK_TEST_ID = 'tiFlyoutJsonCodeBlock';
|
||||
|
||||
export const IndicatorsFlyoutJson: VFC<{ indicator: Indicator }> = ({ indicator }) => {
|
||||
return Object.keys(indicator).length === 0 ? (
|
||||
<EuiEmptyPrompt
|
||||
iconType="alert"
|
||||
color="danger"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.flyoutJson.errorMessageTitle"
|
||||
defaultMessage="Unable to display indicator information"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.flyoutJson.errorMessageBody"
|
||||
defaultMessage="There was an error displaying the indicator fields and values."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj={EMPTY_PROMPT_TEST_ID}
|
||||
/>
|
||||
) : (
|
||||
<EuiCodeBlock language="json" lineNumbers data-test-subj={CODE_BLOCK_TEST_ID}>
|
||||
{JSON.stringify(indicator, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { Story } from '@storybook/react';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsFlyoutTable } from './indicators_flyout_table';
|
||||
|
||||
export default {
|
||||
component: IndicatorsFlyoutTable,
|
||||
title: 'IndicatorsFlyoutTable',
|
||||
};
|
||||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
return <IndicatorsFlyoutTable indicator={mockIndicator} />;
|
||||
};
|
||||
|
||||
export const EmptyIndicator: Story<void> = () => {
|
||||
return <IndicatorsFlyoutTable indicator={{} as unknown as Indicator} />;
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
import {
|
||||
generateMockIndicator,
|
||||
Indicator,
|
||||
RawIndicatorFieldId,
|
||||
} from '../../../../../common/types/indicator';
|
||||
import {
|
||||
EMPTY_PROMPT_TEST_ID,
|
||||
IndicatorsFlyoutTable,
|
||||
TABLE_TEST_ID,
|
||||
} from './indicators_flyout_table';
|
||||
import { unwrapValue } from '../../lib/unwrap_value';
|
||||
import { displayValue } from '../../lib/display_value';
|
||||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
|
||||
describe('<IndicatorsFlyoutTable />', () => {
|
||||
it('should render fields and values in table', () => {
|
||||
const { getByTestId, getByText } = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsFlyoutTable indicator={mockIndicator} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(getByTestId(TABLE_TEST_ID)).toBeInTheDocument();
|
||||
|
||||
expect(getByText(RawIndicatorFieldId.Feed)).toBeInTheDocument();
|
||||
|
||||
expect(getByText(displayValue(mockIndicator) as string)).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
getByText(unwrapValue(mockIndicator, RawIndicatorFieldId.Feed) as string)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error message on invalid indicator', () => {
|
||||
const { getByTestId, getByText } = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsFlyoutTable indicator={{} as unknown as Indicator} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(getByTestId(EMPTY_PROMPT_TEST_ID)).toBeInTheDocument();
|
||||
expect(getByText('Unable to display indicator information')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText('There was an error displaying the indicator fields and values.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { EuiEmptyPrompt, EuiInMemoryTable } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { VFC } from 'react';
|
||||
import { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { Indicator, RawIndicatorFieldId } from '../../../../../common/types/indicator';
|
||||
import { unwrapValue } from '../../lib/unwrap_value';
|
||||
|
||||
export const EMPTY_PROMPT_TEST_ID = 'tiFlyoutTableEmptyPrompt';
|
||||
export const TABLE_TEST_ID = 'tiFlyoutTableMemoryTable';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'field',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.flyoutTable.fieldColumnLabel"
|
||||
defaultMessage="Field"
|
||||
/>
|
||||
),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.flyoutTable.valueColumnLabel"
|
||||
defaultMessage="Value"
|
||||
/>
|
||||
),
|
||||
truncateText: true,
|
||||
},
|
||||
];
|
||||
const search = {
|
||||
box: {
|
||||
incremental: true,
|
||||
schema: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const IndicatorsFlyoutTable: VFC<{ indicator: Indicator }> = ({ indicator }) => {
|
||||
const items: Array<{ field: string; value: string }> = [];
|
||||
|
||||
for (const key in indicator.fields) {
|
||||
if (!indicator.fields.hasOwnProperty(key)) continue;
|
||||
items.push({
|
||||
field: key,
|
||||
value: unwrapValue(indicator, key as RawIndicatorFieldId) || EMPTY_VALUE,
|
||||
});
|
||||
}
|
||||
|
||||
return items.length === 0 ? (
|
||||
<EuiEmptyPrompt
|
||||
iconType="alert"
|
||||
color="danger"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.flyoutTable.errorMessageTitle"
|
||||
defaultMessage="Unable to display indicator information"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.flyoutTable.errorMessageBody"
|
||||
defaultMessage="There was an error displaying the indicator fields and values."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj={EMPTY_PROMPT_TEST_ID}
|
||||
/>
|
||||
) : (
|
||||
<EuiInMemoryTable
|
||||
items={items}
|
||||
columns={columns}
|
||||
search={search}
|
||||
sorting={true}
|
||||
data-test-subj={TABLE_TEST_ID}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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, { VFC } from 'react';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { OpenIndicatorFlyoutButton } from '../open_indicator_flyout_button/open_indicator_flyout_button';
|
||||
|
||||
export const ActionsRowCell: VFC<{ indicator: Indicator }> = ({ indicator }) => (
|
||||
<OpenIndicatorFlyoutButton indicator={indicator} />
|
||||
);
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { EMPTY_VALUE } from '../../../../../common/constants';
|
||||
import { Indicator, RawIndicatorFieldId } from '../../../../../common/types/indicator';
|
||||
import { displayValue } from '../../lib/display_value';
|
||||
import { unwrapValue } from '../../lib/unwrap_value';
|
||||
|
||||
export enum ComputedIndicatorFieldId {
|
||||
DisplayValue = 'display_value',
|
||||
}
|
||||
|
||||
export const cellRendererFactory = (indicators: Indicator[], from: number) => {
|
||||
return ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => {
|
||||
const indicator = indicators[rowIndex - from];
|
||||
|
||||
if (!indicator) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (columnId === ComputedIndicatorFieldId.DisplayValue) {
|
||||
return displayValue(indicator) || EMPTY_VALUE;
|
||||
}
|
||||
|
||||
return unwrapValue(indicator, columnId as RawIndicatorFieldId) || EMPTY_VALUE;
|
||||
};
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsTable } from './indicators_table';
|
||||
|
||||
export default {
|
||||
component: IndicatorsTable,
|
||||
title: 'IndicatorsTable',
|
||||
};
|
||||
|
||||
const indicatorsFixture: Indicator[] = Array(10).fill(generateMockIndicator());
|
||||
|
||||
const stub = () => void 0;
|
||||
|
||||
export function WithIndicators() {
|
||||
return (
|
||||
<IndicatorsTable
|
||||
loadData={stub}
|
||||
firstLoad={false}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
pageIndex: 0,
|
||||
pageSizeOptions: [10, 25, 50],
|
||||
}}
|
||||
indicators={indicatorsFixture}
|
||||
onChangePage={stub}
|
||||
onChangeItemsPerPage={stub}
|
||||
indicatorCount={indicatorsFixture.length * 2}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { act, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { IndicatorsTable, IndicatorsTableProps } from './indicators_table';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
|
||||
const stub = () => {};
|
||||
|
||||
const tableProps: IndicatorsTableProps = {
|
||||
loadData: stub,
|
||||
onChangePage: stub,
|
||||
onChangeItemsPerPage: stub,
|
||||
indicators: [],
|
||||
pagination: { pageSize: 10, pageIndex: 0, pageSizeOptions: [10] },
|
||||
indicatorCount: 0,
|
||||
firstLoad: false,
|
||||
};
|
||||
|
||||
const indicatorsFixture = [
|
||||
{
|
||||
fields: {
|
||||
'threat.indicator.type': ['url'],
|
||||
},
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
'threat.indicator.type': ['file'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('<IndicatorsTable />', () => {
|
||||
it('should render loading spinner on first load', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsTable {...tableProps} firstLoad={true} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render datagrid when first load is done', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsTable
|
||||
{...tableProps}
|
||||
firstLoad={false}
|
||||
indicatorCount={indicatorsFixture.length}
|
||||
indicators={indicatorsFixture}
|
||||
/>
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('grid')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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, { VFC, useState, useMemo } from 'react';
|
||||
import {
|
||||
EuiDataGrid,
|
||||
EuiDataGridCellValueElementProps,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Indicator, RawIndicatorFieldId } from '../../../../../common/types/indicator';
|
||||
import { UseIndicatorsValue } from '../../hooks/use_indicators';
|
||||
import { cellRendererFactory, ComputedIndicatorFieldId } from './cell_renderer';
|
||||
import { ActionsRowCell } from './actions_row_cell';
|
||||
|
||||
interface Column {
|
||||
id: RawIndicatorFieldId | ComputedIndicatorFieldId;
|
||||
displayAsText: string;
|
||||
}
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
id: ComputedIndicatorFieldId.DisplayValue,
|
||||
displayAsText: i18n.translate('xpack.threatIntelligence.indicator.table.indicatorColumTitle', {
|
||||
defaultMessage: 'Indicator',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RawIndicatorFieldId.Type,
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.threatIntelligence.indicator.table.indicatorTypeColumTitle',
|
||||
{
|
||||
defaultMessage: 'Indicator type',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
id: RawIndicatorFieldId.Feed,
|
||||
displayAsText: i18n.translate('xpack.threatIntelligence.indicator.table.FeedColumTitle', {
|
||||
defaultMessage: 'Feed',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RawIndicatorFieldId.FirstSeen,
|
||||
displayAsText: i18n.translate('xpack.threatIntelligence.indicator.table.firstSeenColumTitle', {
|
||||
defaultMessage: 'First seen',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RawIndicatorFieldId.LastSeen,
|
||||
displayAsText: i18n.translate('xpack.threatIntelligence.indicator.table.lastSeenColumTitle', {
|
||||
defaultMessage: 'Last seen',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: RawIndicatorFieldId.MarkingTLP,
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.threatIntelligence.indicator.table.tlpMarketingColumTitle',
|
||||
{
|
||||
defaultMessage: 'TLP Marking',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export type IndicatorsTableProps = UseIndicatorsValue;
|
||||
|
||||
export const TABLE_TEST_ID = 'tiIndicatorsTable';
|
||||
|
||||
export const IndicatorsTable: VFC<IndicatorsTableProps> = ({
|
||||
indicators,
|
||||
indicatorCount,
|
||||
onChangePage,
|
||||
onChangeItemsPerPage,
|
||||
pagination,
|
||||
firstLoad,
|
||||
}) => {
|
||||
const [visibleColumns, setVisibleColumns] = useState<Array<Column['id']>>(
|
||||
columns.map((column) => column.id)
|
||||
);
|
||||
const renderCellValue = useMemo(
|
||||
() => cellRendererFactory(indicators, pagination.pageIndex * pagination.pageSize),
|
||||
[indicators, pagination.pageIndex, pagination.pageSize]
|
||||
);
|
||||
|
||||
const start = pagination.pageIndex * pagination.pageSize;
|
||||
const end = start + pagination.pageSize;
|
||||
|
||||
if (firstLoad) {
|
||||
return <EuiLoadingSpinner size="m" />;
|
||||
}
|
||||
|
||||
const leadingControlColumns = [
|
||||
{
|
||||
id: 'Actions',
|
||||
width: 72,
|
||||
headerCellRender: () => (
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.indicator.table.actionColumnLabel"
|
||||
defaultMessage="Actions"
|
||||
/>
|
||||
),
|
||||
rowCellRender: (cveProps: EuiDataGridCellValueElementProps) => {
|
||||
const indicator: Indicator = indicators[cveProps.rowIndex];
|
||||
return <ActionsRowCell indicator={indicator} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiDataGrid
|
||||
aria-labelledby={'indicators-table'}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
columns={columns}
|
||||
columnVisibility={{
|
||||
visibleColumns,
|
||||
setVisibleColumns: setVisibleColumns as (cols: string[]) => void,
|
||||
}}
|
||||
rowCount={indicatorCount}
|
||||
renderCellValue={renderCellValue}
|
||||
toolbarVisibility={{
|
||||
showDisplaySelector: false,
|
||||
showFullScreenSelector: false,
|
||||
additionalControls: {
|
||||
left: {
|
||||
prepend: (
|
||||
<EuiText style={{ display: 'inline' }} size="xs">
|
||||
Showing {start + 1}-{end > indicatorCount ? indicatorCount : end} of{' '}
|
||||
{indicatorCount} indicators
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
},
|
||||
}}
|
||||
pagination={{
|
||||
...pagination,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
gridStyle={{
|
||||
border: 'horizontal',
|
||||
header: 'underline',
|
||||
cellPadding: 'm',
|
||||
fontSize: 's',
|
||||
}}
|
||||
data-test-subj={TABLE_TEST_ID}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { Story } from '@storybook/react';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { OpenIndicatorFlyoutButton } from './open_indicator_flyout_button';
|
||||
|
||||
export default {
|
||||
component: OpenIndicatorFlyoutButton,
|
||||
title: 'ViewDetailsButton',
|
||||
};
|
||||
|
||||
const mockIndicator: Indicator = generateMockIndicator();
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
return <OpenIndicatorFlyoutButton indicator={mockIndicator} />;
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { BUTTON_TEST_ID, OpenIndicatorFlyoutButton } from './open_indicator_flyout_button';
|
||||
import { generateMockIndicator } from '../../../../../common/types/indicator';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
|
||||
const mockIndicator = generateMockIndicator();
|
||||
|
||||
describe('<IndicatorsFlyout />', () => {
|
||||
it('should render expand button if flyout is closed', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvidersComponent>
|
||||
<OpenIndicatorFlyoutButton indicator={mockIndicator} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(getByTestId(BUTTON_TEST_ID).innerHTML).toContain('expand');
|
||||
});
|
||||
|
||||
it(`should render minimize button if flyout is open`, () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProvidersComponent>
|
||||
<OpenIndicatorFlyoutButton indicator={{ fields: {} }} />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
const button = getByTestId(BUTTON_TEST_ID);
|
||||
button.click();
|
||||
expect(button.innerHTML).toContain('minimize');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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, { useState, VFC } from 'react';
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Indicator } from '../../../../../common/types/indicator';
|
||||
import { IndicatorsFlyout } from '../indicators_flyout/indicators_flyout';
|
||||
|
||||
export const BUTTON_TEST_ID = 'tiToggleIndicatorFlyoutButton';
|
||||
|
||||
export const OpenIndicatorFlyoutButton: VFC<{ indicator: Indicator }> = ({ indicator }) => {
|
||||
const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
|
||||
|
||||
const buttonLabel: string = i18n.translate(
|
||||
'xpack.threatIntelligence.indicator.table.viewDetailsButton',
|
||||
{
|
||||
defaultMessage: 'View details',
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiToolTip content={buttonLabel} delay="long">
|
||||
<EuiButtonIcon
|
||||
data-test-subj={BUTTON_TEST_ID}
|
||||
color={isFlyoutOpen ? 'primary' : 'text'}
|
||||
iconType={isFlyoutOpen ? 'minimize' : 'expand'}
|
||||
isSelected={isFlyoutOpen}
|
||||
iconSize="s"
|
||||
aria-label={buttonLabel}
|
||||
onClick={() => setIsFlyoutOpen(!isFlyoutOpen)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
{isFlyoutOpen && (
|
||||
<IndicatorsFlyout indicator={indicator} closeFlyout={() => setIsFlyoutOpen(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* 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 { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useIndicators, RawIndicatorsResponse } from './use_indicators';
|
||||
import { BehaviorSubject, throwError } from 'rxjs';
|
||||
import { IKibanaSearchResponse } from '@kbn/data-plugin/common';
|
||||
import { mockSearchService } from '../../../common/mocks/mock_kibana_search_service';
|
||||
import { DEFAULT_THREAT_INDEX_KEY } from '../../../../common/constants';
|
||||
|
||||
jest.mock('../../../hooks/use_kibana');
|
||||
|
||||
const indicatorsResponse = { rawResponse: { hits: { hits: [], total: 0 } } };
|
||||
|
||||
describe('useIndicators()', () => {
|
||||
let mockSearch: ReturnType<typeof mockSearchService>;
|
||||
|
||||
describe('when mounted', () => {
|
||||
beforeEach(() => {
|
||||
mockSearch = mockSearchService(new BehaviorSubject(indicatorsResponse));
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
renderHook(() => useIndicators());
|
||||
});
|
||||
|
||||
it('should query the database for threat indicators', async () => {
|
||||
expect(mockSearch.search).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should retrieve index patterns from settings', () => {
|
||||
expect(mockSearch.getUiSetting).toHaveBeenCalledWith(DEFAULT_THREAT_INDEX_KEY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when query fails', () => {
|
||||
beforeEach(async () => {
|
||||
mockSearch = mockSearchService(throwError(() => new Error('some random error')));
|
||||
|
||||
renderHook(() => useIndicators());
|
||||
});
|
||||
|
||||
it('should show an error', async () => {
|
||||
expect(mockSearch.showError).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockSearch.search).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
query: expect.any(Object),
|
||||
from: expect.any(Number),
|
||||
size: expect.any(Number),
|
||||
fields: expect.any(Array),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
abortSignal: expect.any(AbortSignal),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when query is successful', () => {
|
||||
beforeEach(async () => {
|
||||
mockSearch = mockSearchService(
|
||||
new BehaviorSubject<IKibanaSearchResponse<RawIndicatorsResponse>>({
|
||||
rawResponse: { hits: { hits: [{ fields: {} }], total: 1 } },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call mapping function on every hit', async () => {
|
||||
const { result } = renderHook(() => useIndicators());
|
||||
|
||||
expect(result.current.indicatorCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
beforeEach(async () => {
|
||||
mockSearch = mockSearchService(
|
||||
new BehaviorSubject<IKibanaSearchResponse<RawIndicatorsResponse>>({
|
||||
rawResponse: { hits: { hits: [{ fields: {} }], total: 1 } },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('when page changes', () => {
|
||||
it('should run the query again with pagination parameters', async () => {
|
||||
const { result } = renderHook(() => useIndicators());
|
||||
|
||||
await act(async () => {
|
||||
result.current.onChangePage(42);
|
||||
});
|
||||
|
||||
expect(mockSearch.search).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(mockSearch.search).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
size: 25,
|
||||
from: 0,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
|
||||
expect(mockSearch.search).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
size: 25,
|
||||
from: 42 * 25,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
|
||||
expect(result.current.pagination.pageIndex).toEqual(42);
|
||||
});
|
||||
|
||||
describe('when page size changes', () => {
|
||||
it('should fetch the first page and update internal page size', async () => {
|
||||
const { result } = renderHook(() => useIndicators());
|
||||
|
||||
await act(async () => {
|
||||
result.current.onChangeItemsPerPage(50);
|
||||
});
|
||||
|
||||
expect(mockSearch.search).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(mockSearch.search).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
size: 25,
|
||||
from: 0,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
|
||||
expect(mockSearch.search).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
size: 50,
|
||||
from: 0,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
|
||||
expect(result.current.pagination.pageIndex).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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 {
|
||||
IEsSearchRequest,
|
||||
IKibanaSearchResponse,
|
||||
isCompleteResponse,
|
||||
isErrorResponse,
|
||||
} from '@kbn/data-plugin/common';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Indicator } from '../../../../common/types/indicator';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { DEFAULT_THREAT_INDEX_KEY } from '../../../../common/constants';
|
||||
|
||||
const PAGE_SIZES = [10, 25, 50];
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = PAGE_SIZES[1];
|
||||
|
||||
export interface UseIndicatorsValue {
|
||||
loadData: (from: number, size: number) => void;
|
||||
indicators: Indicator[];
|
||||
indicatorCount: number;
|
||||
pagination: Pagination;
|
||||
onChangeItemsPerPage: (value: number) => void;
|
||||
onChangePage: (value: number) => void;
|
||||
firstLoad: boolean;
|
||||
}
|
||||
|
||||
export interface RawIndicatorsResponse {
|
||||
hits: {
|
||||
hits: any[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface Pagination {
|
||||
pageSize: number;
|
||||
pageIndex: number;
|
||||
pageSizeOptions: number[];
|
||||
}
|
||||
|
||||
export const useIndicators = (): UseIndicatorsValue => {
|
||||
const {
|
||||
services: {
|
||||
data: { search: searchService },
|
||||
uiSettings,
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const defaultThreatIndices = uiSettings.get<string[]>(DEFAULT_THREAT_INDEX_KEY);
|
||||
|
||||
const searchSubscription$ = useRef(new Subscription());
|
||||
const abortController = useRef(new AbortController());
|
||||
|
||||
const [indicators, setIndicators] = useState<Indicator[]>([]);
|
||||
const [indicatorCount, setIndicatorCount] = useState<number>(0);
|
||||
const [firstLoad, setFirstLoad] = useState(true);
|
||||
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
pageSizeOptions: PAGE_SIZES,
|
||||
});
|
||||
|
||||
const refresh = useCallback(
|
||||
async (from: number, size: number) => {
|
||||
abortController.current = new AbortController();
|
||||
|
||||
searchSubscription$.current = searchService
|
||||
.search<IEsSearchRequest, IKibanaSearchResponse<RawIndicatorsResponse>>(
|
||||
{
|
||||
params: {
|
||||
index: defaultThreatIndices,
|
||||
body: {
|
||||
size,
|
||||
from,
|
||||
fields: [{ field: '*', include_unmapped: true }],
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'event.category': {
|
||||
value: 'threat',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'event.type': {
|
||||
value: 'indicator',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
abortSignal: abortController.current.signal,
|
||||
}
|
||||
)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
setIndicators(response.rawResponse.hits.hits);
|
||||
setIndicatorCount(response.rawResponse.hits.total || 0);
|
||||
|
||||
if (isCompleteResponse(response)) {
|
||||
searchSubscription$.current.unsubscribe();
|
||||
} else if (isErrorResponse(response)) {
|
||||
searchSubscription$.current.unsubscribe();
|
||||
}
|
||||
|
||||
setFirstLoad(false);
|
||||
},
|
||||
error: (msg) => {
|
||||
searchService.showError(msg);
|
||||
searchSubscription$.current.unsubscribe();
|
||||
|
||||
setFirstLoad(false);
|
||||
},
|
||||
});
|
||||
},
|
||||
[defaultThreatIndices, searchService]
|
||||
);
|
||||
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
async (pageSize) => {
|
||||
setPagination((currentPagination) => ({
|
||||
...currentPagination,
|
||||
pageSize,
|
||||
pageIndex: 0,
|
||||
}));
|
||||
|
||||
refresh(0, pageSize);
|
||||
},
|
||||
[refresh, setPagination]
|
||||
);
|
||||
|
||||
const onChangePage = useCallback(
|
||||
async (pageIndex) => {
|
||||
setPagination((currentPagination) => ({ ...currentPagination, pageIndex }));
|
||||
refresh(pageIndex * pagination.pageSize, pagination.pageSize);
|
||||
},
|
||||
[pagination.pageSize, refresh]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
refresh(0, DEFAULT_PAGE_SIZE);
|
||||
|
||||
return () => abortController.current.abort();
|
||||
}, [refresh]);
|
||||
|
||||
return {
|
||||
loadData: refresh,
|
||||
indicators,
|
||||
indicatorCount,
|
||||
pagination,
|
||||
onChangePage,
|
||||
onChangeItemsPerPage,
|
||||
firstLoad,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { mockSearchService } from '../../../common/mocks/mock_kibana_search_service';
|
||||
|
||||
import { useIndicatorsTotalCount } from './use_indicators_total_count';
|
||||
import { DEFAULT_THREAT_INDEX_KEY } from '../../../../common/constants';
|
||||
|
||||
jest.mock('../../../hooks/use_kibana');
|
||||
|
||||
const indicatorsResponse = { rawResponse: { hits: { hits: [], total: 0 } } };
|
||||
|
||||
describe('useIndicatorsTotalCount()', () => {
|
||||
let mockSearch: ReturnType<typeof mockSearchService>;
|
||||
|
||||
describe('when mounted', () => {
|
||||
beforeEach(() => {
|
||||
mockSearch = mockSearchService(new BehaviorSubject(indicatorsResponse));
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
renderHook(() => useIndicatorsTotalCount());
|
||||
});
|
||||
|
||||
it('should query the database for threat indicators', async () => {
|
||||
expect(mockSearch.search).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should retrieve index patterns from settings', () => {
|
||||
expect(mockSearch.getUiSetting).toHaveBeenCalledWith(DEFAULT_THREAT_INDEX_KEY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when rerendered', () => {
|
||||
beforeEach(async () => {
|
||||
mockSearch = mockSearchService(new BehaviorSubject(indicatorsResponse));
|
||||
});
|
||||
|
||||
it('should not call the database when rerendered', async () => {
|
||||
const { rerender } = renderHook(() => useIndicatorsTotalCount());
|
||||
|
||||
rerender();
|
||||
|
||||
expect(mockSearch.search).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when query succeeds', () => {
|
||||
beforeEach(async () => {
|
||||
mockSearch = mockSearchService(new BehaviorSubject(indicatorsResponse));
|
||||
});
|
||||
|
||||
it('should return the total count', async () => {
|
||||
const { result } = renderHook(() => useIndicatorsTotalCount());
|
||||
|
||||
expect(result.current.count).toEqual(indicatorsResponse.rawResponse.hits.total);
|
||||
expect(result.current.isLoading).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import {
|
||||
IEsSearchRequest,
|
||||
IKibanaSearchResponse,
|
||||
isCompleteResponse,
|
||||
} from '@kbn/data-plugin/common';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { RawIndicatorsResponse } from './use_indicators';
|
||||
import { DEFAULT_THREAT_INDEX_KEY } from '../../../../common/constants';
|
||||
|
||||
export const useIndicatorsTotalCount = () => {
|
||||
const {
|
||||
services: {
|
||||
data: { search: searchService },
|
||||
uiSettings,
|
||||
},
|
||||
} = useKibana();
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const defaultThreatIndex = uiSettings.get<string[]>(DEFAULT_THREAT_INDEX_KEY);
|
||||
const query = {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'event.category': {
|
||||
value: 'threat',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'event.type': {
|
||||
value: 'indicator',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const req = {
|
||||
params: {
|
||||
index: defaultThreatIndex,
|
||||
body: {
|
||||
size: 0,
|
||||
query,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setIsLoading(true);
|
||||
searchService
|
||||
.search<IEsSearchRequest, IKibanaSearchResponse<RawIndicatorsResponse>>(req)
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
if (isCompleteResponse(res)) {
|
||||
const returnedCount = res.rawResponse.hits.total || 0;
|
||||
|
||||
setCount(returnedCount);
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [searchService, uiSettings]);
|
||||
|
||||
return { count, isLoading };
|
||||
};
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import { TestProvidersComponent } from '../../common/mocks/test_providers';
|
||||
import { IndicatorsPage } from './indicators_page';
|
||||
import { useIndicators } from './hooks/use_indicators';
|
||||
import { useIndicatorsTotalCount } from './hooks/use_indicators_total_count';
|
||||
import { TABLE_TEST_ID as INDICATORS_TABLE_TEST_ID } from './components/indicators_table/indicators_table';
|
||||
import { EMPTY_PROMPT_TEST_ID } from '../../components/empty_page';
|
||||
import { useIntegrationsPageLink } from '../../hooks/use_integrations_page_link';
|
||||
import { useTIDocumentationLink } from '../../hooks/use_documentation_link';
|
||||
|
||||
jest.mock('./hooks/use_indicators');
|
||||
jest.mock('./hooks/use_indicators_total_count');
|
||||
jest.mock('../../hooks/use_integrations_page_link');
|
||||
jest.mock('../../hooks/use_documentation_link');
|
||||
|
||||
const stub = () => {};
|
||||
|
||||
describe('<IndicatorsPage />', () => {
|
||||
beforeAll(() => {
|
||||
(useIndicators as jest.MockedFunction<typeof useIndicators>).mockReturnValue({
|
||||
indicators: [],
|
||||
indicatorCount: 0,
|
||||
firstLoad: false,
|
||||
pagination: { pageIndex: 0, pageSize: 10, pageSizeOptions: [10] },
|
||||
onChangeItemsPerPage: stub,
|
||||
onChangePage: stub,
|
||||
loadData: stub,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the contents without crashing', async () => {
|
||||
(
|
||||
useIndicatorsTotalCount as jest.MockedFunction<typeof useIndicatorsTotalCount>
|
||||
).mockReturnValue({
|
||||
count: 10,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsPage />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(getByTestId(INDICATORS_TABLE_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render empty page when no indicators are found', async () => {
|
||||
(
|
||||
useIndicatorsTotalCount as jest.MockedFunction<typeof useIndicatorsTotalCount>
|
||||
).mockReturnValue({
|
||||
count: 0,
|
||||
isLoading: false,
|
||||
});
|
||||
(
|
||||
useIntegrationsPageLink as jest.MockedFunction<typeof useIntegrationsPageLink>
|
||||
).mockReturnValue('');
|
||||
(useTIDocumentationLink as jest.MockedFunction<typeof useTIDocumentationLink>).mockReturnValue(
|
||||
''
|
||||
);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsPage />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(getByTestId(EMPTY_PROMPT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render indicators table when count is being loaded', async () => {
|
||||
(
|
||||
useIndicatorsTotalCount as jest.MockedFunction<typeof useIndicatorsTotalCount>
|
||||
).mockReturnValue({
|
||||
count: 0,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsPage />
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(getByTestId(INDICATORS_TABLE_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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, { VFC } from 'react';
|
||||
import { IndicatorsTable } from './components/indicators_table/indicators_table';
|
||||
import { useIndicators } from './hooks/use_indicators';
|
||||
import { EmptyPage } from '../../components/empty_page';
|
||||
import { useIndicatorsTotalCount } from './hooks/use_indicators_total_count';
|
||||
import { DefaultPageLayout } from '../../components/layout';
|
||||
|
||||
export const IndicatorsPage: VFC = () => {
|
||||
const indicators = useIndicators();
|
||||
const { count: indicatorsTotalCount, isLoading: isIndicatorsTotalCountLoading } =
|
||||
useIndicatorsTotalCount();
|
||||
const showEmptyPage = !isIndicatorsTotalCountLoading && indicatorsTotalCount === 0;
|
||||
|
||||
return showEmptyPage ? (
|
||||
<EmptyPage />
|
||||
) : (
|
||||
<DefaultPageLayout pageTitle={'Indicators'}>
|
||||
<IndicatorsTable {...indicators} />
|
||||
</DefaultPageLayout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 {
|
||||
generateMockFileIndicator,
|
||||
generateMockIndicator,
|
||||
generateMockUrlIndicator,
|
||||
Indicator,
|
||||
} from '../../../../common/types/indicator';
|
||||
import { displayValue } from './display_value';
|
||||
|
||||
type ExpectedIndicatorValue = string | null;
|
||||
|
||||
const cases: Array<[Indicator, ExpectedIndicatorValue]> = [
|
||||
[generateMockIndicator(), '12.68.554.87'],
|
||||
[generateMockUrlIndicator(), 'https://google.com'],
|
||||
[generateMockFileIndicator(), 'sample_sha256_hash'],
|
||||
|
||||
// Indicator with no fields should yield null as a display value
|
||||
[{ fields: {} }, null],
|
||||
|
||||
// Same for an empty object
|
||||
[{} as any, null],
|
||||
|
||||
// And falsy value
|
||||
[null, null],
|
||||
];
|
||||
|
||||
describe('displayValue()', () => {
|
||||
describe.each<[Indicator, ExpectedIndicatorValue]>(cases)(
|
||||
'%s',
|
||||
(indicator, expectedDisplayValue) => {
|
||||
it(`should render the indicator as ${expectedDisplayValue}`, () => {
|
||||
expect(displayValue(indicator)).toEqual(expectedDisplayValue);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { Indicator, RawIndicatorFieldId } from '../../../../common/types/indicator';
|
||||
import { unwrapValue } from './unwrap_value';
|
||||
|
||||
export type IndicatorValueExtractor = (indicator: Indicator) => string | null;
|
||||
export type IndicatorTypePredicate = (indicatorType: string | null) => boolean;
|
||||
|
||||
type MapperRule = [predicate: IndicatorTypePredicate, extract: IndicatorValueExtractor];
|
||||
|
||||
/**
|
||||
* Predicates to help identify identicator by type
|
||||
*/
|
||||
const isIpIndicator: IndicatorTypePredicate = (indicatorType) =>
|
||||
!!indicatorType && ['ipv4-addr', 'ipv6-addr'].includes(indicatorType);
|
||||
|
||||
const isFileIndicator: IndicatorTypePredicate = (indicatorType) => indicatorType === 'file';
|
||||
const isUrlIndicator: IndicatorTypePredicate = (indicatorType) => indicatorType === 'url';
|
||||
|
||||
/**
|
||||
* Display value extraction logic
|
||||
*/
|
||||
const extractStub = () => null;
|
||||
|
||||
const extractIp = (indicator: Indicator) => unwrapValue(indicator, RawIndicatorFieldId.Ip);
|
||||
|
||||
const extractUrl = (indicator: Indicator) => unwrapValue(indicator, RawIndicatorFieldId.UrlFull);
|
||||
|
||||
const extractFile = (indicator: Indicator) =>
|
||||
unwrapValue(indicator, RawIndicatorFieldId.FileSha256);
|
||||
|
||||
/**
|
||||
* Pairs rule condition with display value extraction logic
|
||||
*/
|
||||
const rulesArray: MapperRule[] = [
|
||||
[isIpIndicator, extractIp],
|
||||
[isUrlIndicator, extractUrl],
|
||||
[isFileIndicator, extractFile],
|
||||
];
|
||||
|
||||
/**
|
||||
* Finds display value mapping function for given indicatorType
|
||||
*/
|
||||
const findMappingRule = (indicatorType: string | null): IndicatorValueExtractor => {
|
||||
const [_, extract = extractStub] = rulesArray.find(([check]) => check(indicatorType)) || [];
|
||||
return extract;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cached rules for indicator types
|
||||
*/
|
||||
const rules: Record<string, IndicatorValueExtractor> = {};
|
||||
|
||||
/**
|
||||
* Find and return indicator display value, if possible
|
||||
*/
|
||||
export const displayValue = (indicator: Indicator): string | null => {
|
||||
const indicatorType = (unwrapValue(indicator, RawIndicatorFieldId.Type) || '').toLowerCase();
|
||||
|
||||
if (!rules[indicatorType]) {
|
||||
rules[indicatorType] = findMappingRule(indicatorType);
|
||||
}
|
||||
|
||||
return rules[indicatorType](indicator);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { RawIndicatorFieldId } from '../../../../common/types/indicator';
|
||||
import { unwrapValue } from './unwrap_value';
|
||||
|
||||
describe('unwrapValue()', () => {
|
||||
it('should return the first field value or null, if not present', () => {
|
||||
expect(unwrapValue({ fields: {} }, RawIndicatorFieldId.Type)).toEqual(null);
|
||||
expect(unwrapValue({} as any, RawIndicatorFieldId.Type)).toEqual(null);
|
||||
expect(unwrapValue(null as any, RawIndicatorFieldId.Type)).toEqual(null);
|
||||
|
||||
expect(
|
||||
unwrapValue({ fields: { [RawIndicatorFieldId.Type]: ['ip'] } }, RawIndicatorFieldId.Type)
|
||||
).toEqual('ip');
|
||||
});
|
||||
});
|
|
@ -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 { Indicator, RawIndicatorFieldId } from '../../../../common/types/indicator';
|
||||
|
||||
/**
|
||||
* Unpacks field value from raw indicator fields. Will return null if fields are missing entirely
|
||||
* or there is no record for given `fieldId`
|
||||
*/
|
||||
export const unwrapValue = <T = string>(
|
||||
indicator: Partial<Indicator> | null | undefined,
|
||||
fieldId: RawIndicatorFieldId
|
||||
): T | null => {
|
||||
if (!indicator) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const valueArray = indicator.fields?.[fieldId];
|
||||
return Array.isArray(valueArray) ? (valueArray[0] as T) : null;
|
||||
};
|
38
x-pack/plugins/threat_intelligence/public/plugin.tsx
Executable file
38
x-pack/plugins/threat_intelligence/public/plugin.tsx
Executable file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { IndicatorsPage } from './modules/indicators/indicators_page';
|
||||
import {
|
||||
ThreatIntelligencePluginSetup,
|
||||
ThreatIntelligencePluginStart,
|
||||
ThreatIntelligencePluginStartDeps,
|
||||
} from './types';
|
||||
|
||||
const createAppComponent = (services: CoreStart) => {
|
||||
return () => (
|
||||
<KibanaContextProvider services={services}>
|
||||
<IndicatorsPage />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export class ThreatIntelligencePlugin implements Plugin<void, void> {
|
||||
public setup(core: CoreSetup): ThreatIntelligencePluginSetup {
|
||||
return {};
|
||||
}
|
||||
public start(
|
||||
core: CoreStart,
|
||||
plugins: ThreatIntelligencePluginStartDeps
|
||||
): ThreatIntelligencePluginStart {
|
||||
const App = createAppComponent({ ...core, ...plugins });
|
||||
return { getComponent: () => App };
|
||||
}
|
||||
public stop() {}
|
||||
}
|
23
x-pack/plugins/threat_intelligence/public/types.ts
Normal file
23
x-pack/plugins/threat_intelligence/public/types.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { VFC } from 'react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
|
||||
export type Services = { data: DataPublicPluginStart } & CoreStart;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ThreatIntelligencePluginSetup {}
|
||||
|
||||
export interface ThreatIntelligencePluginStart {
|
||||
getComponent: () => VFC;
|
||||
}
|
||||
|
||||
export interface ThreatIntelligencePluginStartDeps {
|
||||
data: DataPublicPluginStart;
|
||||
}
|
20
x-pack/plugins/threat_intelligence/tsconfig.json
Normal file
20
x-pack/plugins/threat_intelligence/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target/types",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
},
|
||||
"include": [
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"scripts/**/*",
|
||||
"public/**/*.json",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/data/tsconfig.json" }
|
||||
]
|
||||
}
|
59
x-pack/test/threat_intelligence_cypress/config.ts
Normal file
59
x-pack/test/threat_intelligence_cypress/config.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { FtrConfigProviderContext } from '@kbn/test';
|
||||
|
||||
import { CA_CERT_PATH } from '@kbn/dev-utils';
|
||||
|
||||
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||
const kibanaCommonTestsConfig = await readConfigFile(
|
||||
require.resolve('../../../test/common/config.js')
|
||||
);
|
||||
const xpackFunctionalTestsConfig = await readConfigFile(
|
||||
require.resolve('../functional/config.base.js')
|
||||
);
|
||||
|
||||
return {
|
||||
...kibanaCommonTestsConfig.getAll(),
|
||||
|
||||
esTestCluster: {
|
||||
...xpackFunctionalTestsConfig.get('esTestCluster'),
|
||||
serverArgs: [
|
||||
...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs'),
|
||||
// define custom es server here
|
||||
// API Keys is enabled at the top level
|
||||
'xpack.security.enabled=true',
|
||||
],
|
||||
},
|
||||
|
||||
kbnTestServer: {
|
||||
...xpackFunctionalTestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
'--csp.strict=false',
|
||||
'--csp.warnLegacyBrowsers=false',
|
||||
// define custom kibana server args here
|
||||
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
|
||||
// retrieve rules from the filesystem but not from fleet for Cypress tests
|
||||
'--xpack.securitySolution.prebuiltRulesFromFileSystem=true',
|
||||
'--xpack.securitySolution.prebuiltRulesFromSavedObjects=false',
|
||||
'--xpack.ruleRegistry.write.enabled=true',
|
||||
'--xpack.ruleRegistry.write.cache.enabled=false',
|
||||
'--xpack.ruleRegistry.unsafe.indexUpgrade.enabled=true',
|
||||
// Without below line, default interval for rules is 1m
|
||||
// See https://github.com/elastic/kibana/pull/125396 for details
|
||||
'--xpack.alerting.rules.minimumScheduleInterval.value=1s',
|
||||
'--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true',
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'riskyHostsEnabled',
|
||||
'riskyUsersEnabled',
|
||||
])}`,
|
||||
`--home.disableWelcomeScreen=true`,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue