Skip to content

Commit

Permalink
Show surrounding documents when index pattern is available; Finding f…
Browse files Browse the repository at this point in the history
…lyout UI polish (opensearch-project#216) (opensearch-project#245)

* refactored finding flyout

Signed-off-by: Amardeepsingh Siglani <[email protected]>

* updated cypress test

Signed-off-by: Amardeepsingh Siglani <[email protected]>

* show surrounding documents when index-pattern available

Signed-off-by: Amardeepsingh Siglani <[email protected]>

* fixed search filter for log source

Signed-off-by: Amardeepsingh Siglani <[email protected]>

Signed-off-by: Amardeepsingh Siglani <[email protected]>
(cherry picked from commit 98c2b5c)

Co-authored-by: Amardeepsingh Siglani <[email protected]>
Signed-off-by: AWSHurneyt <[email protected]>
  • Loading branch information
2 people authored and AWSHurneyt committed Feb 22, 2023
1 parent 83c4758 commit 35eb3fa
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 109 deletions.
80 changes: 37 additions & 43 deletions cypress/integration/3_alerts.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"]',
Expand Down
13 changes: 7 additions & 6 deletions public/pages/Alerts/components/AlertFlyout/AlertFlyout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -39,17 +39,18 @@ export interface AlertFlyoutProps {
detector: Detector;
findingsService: FindingsService;
ruleService: RuleService;
notifications: NotificationsStart;
opensearchService: OpenSearchService;
onClose: () => void;
onAcknowledge: (selectedItems: AlertItem[]) => void;
notifications: NotificationsStart;
}

export interface AlertFlyoutState {
acknowledged: boolean;
findingFlyoutData?: Finding;
findingItems: Finding[];
loading: boolean;
rules: { [key: string]: Rule };
rules: { [key: string]: RuleSource };
}

export class AlertFlyout extends React.Component<AlertFlyoutProps, AlertFlyoutState> {
Expand Down Expand Up @@ -89,7 +90,7 @@ export class AlertFlyout extends React.Component<AlertFlyoutProps, AlertFlyoutSt
} else {
errorNotificationToast(notifications, 'retrieve', 'findings', findingRes.error);
}
} catch (e) {
} catch (e: any) {
errorNotificationToast(notifications, 'retrieve', 'findings', e);
}
await this.getRules();
Expand Down Expand Up @@ -143,7 +144,7 @@ export class AlertFlyout extends React.Component<AlertFlyoutProps, AlertFlyoutSt
}
this.setState({ rules: allRules });
}
} catch (e) {
} catch (e: any) {
errorNotificationToast(notifications, 'retrieve', 'rules', e);
}
};
Expand Down
3 changes: 2 additions & 1 deletion public/pages/Alerts/containers/Alerts/Alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import AlertsService from '../../../../services/AlertsService';
import DetectorService from '../../../../services/DetectorService';
import { AlertItem } from '../../../../../server/models/interfaces';
import { AlertFlyout } from '../../components/AlertFlyout/AlertFlyout';
import { FindingsService, RuleService } from '../../../../services';
import { FindingsService, RuleService, OpenSearchService } from '../../../../services';
import { Detector } from '../../../../../models/interfaces';
import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers';
import { DISABLE_ACKNOWLEDGED_ALERT_HELP_TEXT } from '../../utils/constants';
Expand All @@ -61,6 +61,7 @@ export interface AlertsProps {
detectorService: DetectorService;
findingService: FindingsService;
ruleService: RuleService;
opensearchService: OpenSearchService;
notifications: NotificationsStart;
match: match;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ export default class DetectorDataSource extends Component<
>
<EuiComboBox
placeholder={'Select an input source for the detector.'}
async={true}
isLoading={loading}
options={indexOptions}
selectedOptions={this.parseOptions(detectorIndices)}
Expand Down
149 changes: 102 additions & 47 deletions public/pages/Findings/components/FindingDetailsFlyout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
EuiAccordion,
EuiBadge,
EuiBadgeGroup,
EuiButton,
EuiButtonIcon,
EuiCodeBlock,
EuiFlexGroup,
Expand All @@ -26,20 +27,23 @@ import { capitalizeFirstLetter, renderTime } from '../../../utils/helpers';
import { DEFAULT_EMPTY_DATA, ROUTES } from '../../../utils/constants';
import { Finding, Query } from '../models/interfaces';
import { RuleViewerFlyout } from '../../Rules/components/RuleViewerFlyout/RuleViewerFlyout';
import { RuleTableItem } from '../../Rules/utils/helpers';
import { RuleSource } from '../../../../server/models/interfaces';
import { RuleItemInfoBase } from '../../Rules/models/types';
import { OpenSearchService } from '../../../services';
import { RuleTableItem } from '../../Rules/utils/helpers';

interface FindingDetailsFlyoutProps {
finding: Finding;
closeFlyout: () => 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<
Expand All @@ -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 || [];
Expand All @@ -68,7 +80,7 @@ export default class FindingDetailsFlyout extends Component<
);
};

showRuleDetails = (fullRule, ruleId: string) => {
showRuleDetails = (fullRule: any, ruleId: string) => {
this.setState({
...this.state,
ruleViewerFlyoutData: {
Expand All @@ -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);
Expand Down Expand Up @@ -166,51 +172,98 @@ export default class FindingDetailsFlyout extends Component<
</EuiFormRow>

<EuiSpacer size={'l'} />
</EuiAccordion>
{rules.length > 1 && <EuiHorizontalRule />}
</div>
);
});
};

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 (
<>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size={'s'}>
<h3>Documents</h3>
</EuiTitle>
<EuiSpacer size={'s'} />

<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
label={'Document ID'}
data-test-subj={'finding-details-flyout-rule-document-id'}
>
<EuiText>{docId || DEFAULT_EMPTY_DATA}</EuiText>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
href={
indexPatternId
? `discover#/context/${indexPatternId}/${related_doc_ids[0]}`
: `#${ROUTES.FINDINGS}`
}
target={indexPatternId ? '_blank' : undefined}
>
View surrounding documents
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>

<EuiFlexItem>
<EuiFormRow
label={'Index'}
data-test-subj={'finding-details-flyout-rule-document-index'}
>
<EuiText>{index || DEFAULT_EMPTY_DATA}</EuiText>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size={'s'} />

<EuiSpacer size={'m'} />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
label={'Document ID'}
data-test-subj={'finding-details-flyout-rule-document-id'}
>
<EuiText>{docId || DEFAULT_EMPTY_DATA}</EuiText>
</EuiFormRow>
</EuiFlexItem>

<EuiFormRow fullWidth={true}>
<EuiCodeBlock
language={'json'}
inline={false}
isCopyable={true}
readOnly={true}
data-test-subj={'finding-details-flyout-rule-document'}
>
{JSON.stringify(document, null, 4)}
</EuiCodeBlock>
<EuiFlexItem>
<EuiFormRow
label={'Index'}
data-test-subj={'finding-details-flyout-rule-document-index'}
>
<EuiText>{index || DEFAULT_EMPTY_DATA}</EuiText>
</EuiFormRow>
</EuiAccordion>
{rules.length > 1 && <EuiHorizontalRule />}
</div>
);
});
};
</EuiFlexItem>
</EuiFlexGroup>

<EuiSpacer size={'m'} />

<EuiFormRow fullWidth={true}>
<EuiCodeBlock
language="json"
isCopyable
data-test-subj={'finding-details-flyout-rule-document'}
>
{JSON.stringify(JSON.parse(document), null, 2)}
</EuiCodeBlock>
</EuiFormRow>
</>
);
}

render() {
const {
Expand Down Expand Up @@ -304,6 +357,8 @@ export default class FindingDetailsFlyout extends Component<
</EuiTitle>
<EuiSpacer size={'m'} />
{this.renderRuleDetails(queries)}
<EuiSpacer size="l" />
{this.renderFindingDocuments()}
</EuiFlyoutBody>
</EuiFlyout>
);
Expand Down
Loading

0 comments on commit 35eb3fa

Please sign in to comment.