Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show surrounding documents when index pattern is available; Finding flyout UI polish #216

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
);
amsiglan marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -86,7 +87,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 @@ -140,7 +141,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