diff --git a/cypress/integration/3_alerts.spec.js b/cypress/integration/3_alerts.spec.js index 374261563..71ed9a628 100644 --- a/cypress/integration/3_alerts.spec.js +++ b/cypress/integration/3_alerts.spec.js @@ -249,51 +249,45 @@ describe('Alerts', () => { ['low', 'windows', 'attack.initial_access', 'attack.t1200'].forEach((tag) => { cy.get('[data-test-subj="finding-details-flyout-rule-tags"]').contains(tag); }); - - // Confirm the rule document ID is present - cy.get('[data-test-subj="finding-details-flyout-rule-document-id"]') - .invoke('text') - .then((text) => expect(text).to.not.equal('-')); - - // Confirm the rule index - cy.get('[data-test-subj="finding-details-flyout-rule-document-index"]').contains(testIndex); - - // Confirm the rule document matches - // The EuiCodeEditor used for this component stores each line of the JSON in an array of elements; - // so this test formats the expected document into an array of strings, - // and matches each entry with the corresponding element line. - const document = JSON.stringify( - [ - { - index: 'sample_alerts_spec_cypress_test_index', - id: '', - found: true, - document: - '{"EventTime":"2020-02-04T14:59:39.343541+00:00","HostName":"EC2AMAZ-EPO7HKA","Keywords":"9223372036854775808","SeverityValue":2,"Severity":"INFO","EventID":2003,"SourceName":"Microsoft-Windows-Sysmon","ProviderGuid":"{5770385F-C22A-43E0-BF4C-06F5698FFBD9}","Version":5,"TaskValue":22,"OpcodeValue":0,"RecordNumber":9532,"ExecutionProcessID":1996,"ExecutionThreadID":2616,"Channel":"Microsoft-Windows-Sysmon/Operational","Domain":"NT AUTHORITY","AccountName":"SYSTEM","UserID":"S-1-5-18","AccountType":"User","Message":"Dns query:\\r\\nRuleName: \\r\\nUtcTime: 2020-02-04 14:59:38.349\\r\\nProcessGuid: {b3c285a4-3cda-5dc0-0000-001077270b00}\\r\\nProcessId: 1904\\r\\nQueryName: EC2AMAZ-EPO7HKA\\r\\nQueryStatus: 0\\r\\nQueryResults: 172.31.46.38;\\r\\nImage: C:\\\\Program Files\\\\nxlog\\\\nxlog.exe","Category":"Dns query (rule: DnsQuery)","Opcode":"Info","UtcTime":"2020-02-04 14:59:38.349","ProcessGuid":"{b3c285a4-3cda-5dc0-0000-001077270b00}","ProcessId":"1904","QueryName":"EC2AMAZ-EPO7HKA","QueryStatus":"0","QueryResults":"172.31.46.38;","Image":"C:\\\\Program Files\\\\nxlog\\\\regsvr32.exe","EventReceivedTime":"2020-02-04T14:59:40.780905+00:00","SourceModuleName":"in","SourceModuleType":"im_msvistalog","CommandLine":"eachtest","Initiated":"true","Provider_Name":"Microsoft-Windows-Kernel-General","TargetObject":"\\\\SOFTWARE\\\\Microsoft\\\\Office\\\\Outlook\\\\Security","EventType":"SetValue"}', - }, - ], - null, - 4 - ); - const documentLines = document.split('\n'); - cy.get('[data-test-subj="finding-details-flyout-rule-document"]') - .get('[class="euiCodeBlock__line"]') - .each((lineElement, lineIndex) => { - let line = lineElement.text(); - let expectedLine = documentLines[lineIndex]; - - // The document ID field is generated when the document is added to the index, - // so this test just checks that the line starts with the ID key. - if (expectedLine.trimStart().startsWith('"id": "')) { - expectedLine = '"id": "'; - expect(line, `document JSON line ${lineIndex}`).to.contain(expectedLine); - } else { - line = line.replaceAll('\n', ''); - expect(line, `document JSON line ${lineIndex}`).to.equal(expectedLine); - } - }); }); + // Confirm the rule document ID is present + cy.get('[data-test-subj="finding-details-flyout-rule-document-id"]') + .invoke('text') + .then((text) => expect(text).to.not.equal('-')); + + // Confirm the rule index + cy.get('[data-test-subj="finding-details-flyout-rule-document-index"]').contains(testIndex); + + // Confirm the rule document matches + // The EuiCodeEditor used for this component stores each line of the JSON in an array of elements; + // so this test formats the expected document into an array of strings, + // and matches each entry with the corresponding element line. + const document = JSON.stringify( + JSON.parse( + '{"EventTime":"2020-02-04T14:59:39.343541+00:00","HostName":"EC2AMAZ-EPO7HKA","Keywords":"9223372036854775808","SeverityValue":2,"Severity":"INFO","EventID":2003,"SourceName":"Microsoft-Windows-Sysmon","ProviderGuid":"{5770385F-C22A-43E0-BF4C-06F5698FFBD9}","Version":5,"TaskValue":22,"OpcodeValue":0,"RecordNumber":9532,"ExecutionProcessID":1996,"ExecutionThreadID":2616,"Channel":"Microsoft-Windows-Sysmon/Operational","Domain":"NT AUTHORITY","AccountName":"SYSTEM","UserID":"S-1-5-18","AccountType":"User","Message":"Dns query:\\r\\nRuleName: \\r\\nUtcTime: 2020-02-04 14:59:38.349\\r\\nProcessGuid: {b3c285a4-3cda-5dc0-0000-001077270b00}\\r\\nProcessId: 1904\\r\\nQueryName: EC2AMAZ-EPO7HKA\\r\\nQueryStatus: 0\\r\\nQueryResults: 172.31.46.38;\\r\\nImage: C:\\\\Program Files\\\\nxlog\\\\nxlog.exe","Category":"Dns query (rule: DnsQuery)","Opcode":"Info","UtcTime":"2020-02-04 14:59:38.349","ProcessGuid":"{b3c285a4-3cda-5dc0-0000-001077270b00}","ProcessId":"1904","QueryName":"EC2AMAZ-EPO7HKA","QueryStatus":"0","QueryResults":"172.31.46.38;","Image":"C:\\\\Program Files\\\\nxlog\\\\regsvr32.exe","EventReceivedTime":"2020-02-04T14:59:40.780905+00:00","SourceModuleName":"in","SourceModuleType":"im_msvistalog","CommandLine":"eachtest","Initiated":"true","Provider_Name":"Microsoft-Windows-Kernel-General","TargetObject":"\\\\SOFTWARE\\\\Microsoft\\\\Office\\\\Outlook\\\\Security","EventType":"SetValue"}' + ), + null, + 2 + ); + const documentLines = document.split('\n'); + cy.get('[data-test-subj="finding-details-flyout-rule-document"]') + .get('[class="euiCodeBlock__line"]') + .each((lineElement, lineIndex) => { + let line = lineElement.text(); + let expectedLine = documentLines[lineIndex]; + + // The document ID field is generated when the document is added to the index, + // so this test just checks that the line starts with the ID key. + if (expectedLine.trimStart().startsWith('"id": "')) { + expectedLine = '"id": "'; + expect(line, `document JSON line ${lineIndex}`).to.contain(expectedLine); + } else { + line = line.replaceAll('\n', ''); + expect(line, `document JSON line ${lineIndex}`).to.equal(expectedLine); + } + }); + // Press the "back" button cy.get( '[data-test-subj="finding-details-flyout-back-button"]', diff --git a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx index ebedd1215..06f45e239 100644 --- a/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx +++ b/public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx @@ -27,9 +27,9 @@ import { errorNotificationToast, renderTime, } from '../../../../utils/helpers'; -import { FindingsService, RuleService } from '../../../../services'; +import { FindingsService, RuleService, OpenSearchService } from '../../../../services'; import FindingDetailsFlyout from '../../../Findings/components/FindingDetailsFlyout'; -import { Detector, Rule } from '../../../../../models/interfaces'; +import { Detector } from '../../../../../models/interfaces'; import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; import { Finding } from '../../../Findings/models/interfaces'; import { NotificationsStart } from 'opensearch-dashboards/public'; @@ -39,9 +39,10 @@ export interface AlertFlyoutProps { detector: Detector; findingsService: FindingsService; ruleService: RuleService; + notifications: NotificationsStart; + opensearchService: OpenSearchService; onClose: () => void; onAcknowledge: (selectedItems: AlertItem[]) => void; - notifications: NotificationsStart; } export interface AlertFlyoutState { @@ -49,7 +50,7 @@ export interface AlertFlyoutState { findingFlyoutData?: Finding; findingItems: Finding[]; loading: boolean; - rules: { [key: string]: Rule }; + rules: { [key: string]: RuleSource }; } export class AlertFlyout extends React.Component { @@ -89,7 +90,7 @@ export class AlertFlyout extends React.Component void; backButton?: React.ReactNode; allRules: { [id: string]: RuleSource }; + opensearchService: OpenSearchService; + closeFlyout: () => void; } interface FindingDetailsFlyoutState { loading: boolean; ruleViewerFlyoutData: RuleTableItem | null; + indexPatternId?: string; } export default class FindingDetailsFlyout extends Component< @@ -54,6 +58,14 @@ export default class FindingDetailsFlyout extends Component< }; } + componentDidMount(): void { + this.getIndexPatternId().then((patternId) => { + if (patternId) { + this.setState({ indexPatternId: patternId }); + } + }); + } + renderTags = () => { const { finding } = this.props; const tags = finding.queries[0].tags || []; @@ -68,7 +80,7 @@ export default class FindingDetailsFlyout extends Component< ); }; - showRuleDetails = (fullRule, ruleId: string) => { + showRuleDetails = (fullRule: any, ruleId: string) => { this.setState({ ...this.state, ruleViewerFlyoutData: { @@ -90,13 +102,7 @@ export default class FindingDetailsFlyout extends Component< }; renderRuleDetails = (rules: Query[] = []) => { - const { - allRules, - finding: { index, related_doc_ids, document_list }, - } = this.props; - const documents = document_list; - const docId = related_doc_ids[0]; - const document = documents.filter((doc) => doc.id === docId); + const { allRules } = this.props; return rules.map((rule, key) => { const fullRule = allRules[rule.id]; const severity = capitalizeFirstLetter(fullRule.level); @@ -166,51 +172,98 @@ export default class FindingDetailsFlyout extends Component< + + {rules.length > 1 && } + + ); + }); + }; + + getIndexPatternId = async () => { + const indexPatterns = await this.props.opensearchService.getIndexPatterns(); + const { + finding: { index }, + } = this.props; + let patternId; + indexPatterns.map((pattern) => { + const patternName = pattern.attributes.title.replaceAll('*', '.*'); + const patternRegex = new RegExp(patternName); + if (index.match(patternRegex)) { + patternId = pattern.id; + } + }); + + return patternId; + }; + renderFindingDocuments() { + const { + finding: { index, document_list, related_doc_ids }, + } = this.props; + const documents = document_list; + const docId = related_doc_ids[0]; + const matchedDocuments = documents.filter((doc) => doc.id === docId); + const document = matchedDocuments.length > 0 ? matchedDocuments[0].document : ''; + const { indexPatternId } = this.state; + + return ( + <> + +

Documents

- - - - - - {docId || DEFAULT_EMPTY_DATA} - - +
+ + + View surrounding documents + + +
- - - {index || DEFAULT_EMPTY_DATA} - - - + - + + + + {docId || DEFAULT_EMPTY_DATA} + + - - - {JSON.stringify(document, null, 4)} - + + + {index || DEFAULT_EMPTY_DATA} - - {rules.length > 1 && } - - ); - }); - }; + + + + + + + + {JSON.stringify(JSON.parse(document), null, 2)} + + + + ); + } render() { const { @@ -304,6 +357,8 @@ export default class FindingDetailsFlyout extends Component< {this.renderRuleDetails(queries)} + + {this.renderFindingDocuments()} ); diff --git a/public/pages/Findings/components/FindingsTable/FindingsTable.tsx b/public/pages/Findings/components/FindingsTable/FindingsTable.tsx index 6ab37b28d..47dbb2a5a 100644 --- a/public/pages/Findings/components/FindingsTable/FindingsTable.tsx +++ b/public/pages/Findings/components/FindingsTable/FindingsTable.tsx @@ -9,10 +9,8 @@ import moment from 'moment'; import { EuiBasicTableColumn, EuiButtonIcon, - EuiEmptyPrompt, EuiInMemoryTable, EuiLink, - EuiText, EuiToolTip, } from '@elastic/eui'; import { FieldValueSelectionFilterConfigType } from '@elastic/eui/src/components/search_bar/filters/field_value_selection_filter'; @@ -26,6 +24,7 @@ import CreateAlertFlyout from '../CreateAlertFlyout'; import { NotificationChannelTypeOptions } from '../../../CreateDetector/components/ConfigureAlerts/models/interfaces'; import { FindingItemType } from '../../containers/Findings/Findings'; import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers'; +import { RuleSource } from '../../../../../server/models/interfaces'; interface FindingsTableProps extends RouteComponentProps { detectorService: DetectorsService; @@ -34,7 +33,7 @@ interface FindingsTableProps extends RouteComponentProps { notificationChannels: NotificationChannelTypeOptions[]; refreshNotificationChannels: () => void; loading: boolean; - rules: any; + rules: { [id: string]: RuleSource }; startTime: string; endTime: string; onRefresh: () => void; diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index 1f0228edd..01fb90318 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -371,6 +371,7 @@ export default class Main extends Component { findingService={services.findingsService} ruleService={services.ruleService} notifications={core?.notifications} + opensearchService={services.opensearchService} /> )} /> diff --git a/public/security_analytics_app.tsx b/public/security_analytics_app.tsx index 78c7beac4..73caff669 100644 --- a/public/security_analytics_app.tsx +++ b/public/security_analytics_app.tsx @@ -21,12 +21,12 @@ import FieldMappingService from './services/FieldMappingService'; import RuleService from './services/RuleService'; export function renderApp(coreStart: CoreStart, params: AppMountParameters, landingPage: string) { - const http = coreStart.http; + const { http, savedObjects } = coreStart; const detectorsService = new DetectorsService(http); const indexService = new IndexService(http); const findingsService = new FindingsService(http); - const opensearchService = new OpenSearchService(http); + const opensearchService = new OpenSearchService(http, savedObjects.client); const fieldMappingService = new FieldMappingService(http); const alertsService = new AlertsService(http); const ruleService = new RuleService(http); diff --git a/public/services/OpenSearchService.ts b/public/services/OpenSearchService.ts index d2be13864..f1b1c6632 100644 --- a/public/services/OpenSearchService.ts +++ b/public/services/OpenSearchService.ts @@ -3,17 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { HttpSetup } from 'opensearch-dashboards/public'; +import { + HttpSetup, + SavedObjectsClientContract, + SimpleSavedObject, +} from 'opensearch-dashboards/public'; import { ServerResponse } from '../../server/models/types'; import { SearchResponse, Plugin } from '../../server/models/interfaces'; import { API } from '../../server/utils/constants'; export default class OpenSearchService { - httpClient: HttpSetup; - - constructor(httpClient: HttpSetup) { - this.httpClient = httpClient; - } + constructor( + private httpClient: HttpSetup, + private savedObjectsClient: SavedObjectsClientContract + ) {} documentIdsQuery = async ( index: string, @@ -41,4 +44,16 @@ export default class OpenSearchService { let url = `..${API.PLUGINS}`; return await this.httpClient.get(url); }; + + getIndexPatterns = async (): Promise[]> => { + const indexPatterns = await this.savedObjectsClient + .find<{ title: string }>({ + type: 'index-pattern', + fields: ['title', 'type'], + perPage: 10000, + }) + .then((response) => response.savedObjects); + + return Promise.resolve(indexPatterns); + }; }