Skip to content

Commit

Permalink
[Backport 2.13] Show all related docs for a finding (opensearch-proje…
Browse files Browse the repository at this point in the history
…ct#1011)

* Show all related docs for a finding (opensearch-project#1006)

* refactored Ux to show all documents for a finding

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

* updated snapshots

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

* reverting snapshots

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

* updated cypress test

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

* updated cypress test

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

* refactored code

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

---------

Signed-off-by: Amardeepsingh Siglani <[email protected]>
(cherry picked from commit e16e1aa)
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* updated refs in workflow

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

---------

Signed-off-by: Amardeepsingh Siglani <[email protected]>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Amardeepsingh Siglani <[email protected]>
  • Loading branch information
3 people authored May 9, 2024
1 parent d8c62f2 commit cdacb14
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 94 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/cypress-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:
branches:
- "*"
env:
OPENSEARCH_DASHBOARDS_VERSION: '2.x'
SECURITY_ANALYTICS_BRANCH: '2.x'
OPENSEARCH_DASHBOARDS_VERSION: '2.13.0'
SECURITY_ANALYTICS_BRANCH: '2.13'
jobs:
tests:
name: Run Cypress E2E tests
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches:
- "*"
env:
OPENSEARCH_DASHBOARDS_VERSION: '2.x'
OPENSEARCH_DASHBOARDS_VERSION: '2.13.0'
jobs:
Get-CI-Image-Tag:
uses: opensearch-project/opensearch-build/.github/workflows/get-ci-image-tag.yml@main
Expand Down
11 changes: 5 additions & 6 deletions cypress/integration/3_alerts.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,21 +210,20 @@ describe('Alerts', () => {
});
});

// 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(indexName);

// Confirm there is atleast one row of document
cy.get('tbody > tr').should('have.length.least', 1);

// 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('{"winlog.event_id": 2003}'), null, 2);
const documentLines = document.split('\n');
cy.get('[data-test-subj="finding-details-flyout-rule-document"]')
cy.get('[data-test-subj="finding-details-flyout-document-toggle-0"]').click({ force: true });
cy.get('[data-test-subj="finding-details-flyout-rule-document-0"]')
.get('[class="euiCodeBlock__line"]')
.each((lineElement, lineIndex) => {
let line = lineElement.text();
Expand Down
219 changes: 134 additions & 85 deletions public/pages/Findings/components/FindingDetailsFlyout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
EuiAccordion,
EuiBadge,
EuiBadgeGroup,
EuiButton,
EuiButtonIcon,
EuiCodeBlock,
EuiFlexGroup,
Expand All @@ -27,12 +26,14 @@ import {
EuiSpacer,
EuiText,
EuiTitle,
EuiIcon,
EuiTabs,
EuiTab,
EuiLoadingContent,
EuiEmptyPrompt,
EuiLoadingSpinner,
EuiBasicTableColumn,
EuiInMemoryTable,
EuiToolTip,
EuiEmptyPrompt,
} from '@elastic/eui';
import {
capitalizeFirstLetter,
Expand All @@ -48,7 +49,7 @@ import { OpenSearchService, IndexPatternsService, CorrelationService } from '../
import { RuleTableItem } from '../../Rules/utils/helpers';
import { CreateIndexPatternForm } from './CreateIndexPatternForm';
import { FindingItemType } from '../containers/Findings/Findings';
import { CorrelationFinding, RuleItemInfoBase } from '../../../../types';
import { CorrelationFinding, FindingDocumentItem, RuleItemInfoBase } from '../../../../types';
import { FindingFlyoutTabId, FindingFlyoutTabs } from '../utils/constants';
import { DataStore } from '../../../store/DataStore';
import { CorrelationsTable } from './CorrelationsTable/CorrelationsTable';
Expand All @@ -74,11 +75,12 @@ interface FindingDetailsFlyoutState {
ruleViewerFlyoutData: RuleTableItem | null;
indexPatternId?: string;
isCreateIndexPatternModalVisible: boolean;
selectedTab: { id: string; content: React.ReactNode | null };
selectedTab: { id: FindingFlyoutTabId; content: React.ReactNode | null };
correlatedFindings: CorrelationFinding[];
allRules: { [id: string]: RuleSource };
isDocumentLoading: boolean;
loadingIndexPatternId: boolean;
areCorrelationsLoading: boolean;
docIdToExpandedRowMap: { [id: string]: JSX.Element };
}

export default class FindingDetailsFlyout extends Component<
Expand All @@ -104,9 +106,10 @@ export default class FindingDetailsFlyout extends Component<
),
},
correlatedFindings: [],
isDocumentLoading: true,
loadingIndexPatternId: true,
areCorrelationsLoading: true,
allRules: {},
docIdToExpandedRowMap: {},
};
}

Expand Down Expand Up @@ -155,7 +158,7 @@ export default class FindingDetailsFlyout extends Component<
}
})
.finally(() => {
this.setState({ isDocumentLoading: false });
this.setState({ loadingIndexPatternId: false });
});

this.getCorrelations();
Expand All @@ -174,6 +177,21 @@ export default class FindingDetailsFlyout extends Component<
});
}

componentDidUpdate(
prevProps: Readonly<FindingDetailsFlyoutProps>,
prevState: Readonly<FindingDetailsFlyoutState>,
snapshot?: any
): void {
if (prevState.docIdToExpandedRowMap !== this.state.docIdToExpandedRowMap) {
this.setState({
selectedTab: {
id: this.state.selectedTab.id,
content: this.getTabContent(this.state.selectedTab.id, this.state.loadingIndexPatternId),
},
});
}
}

renderTags = (tags: string[]) => {
return (
tags && (
Expand Down Expand Up @@ -300,97 +318,127 @@ export default class FindingDetailsFlyout extends Component<
return patternId;
};

renderFindingDocuments(isDocumentLoading: boolean) {
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 : '';
toggleDocumentDetails(item: FindingDocumentItem) {
const docIdToExpandedRowMapValues = { ...this.state.docIdToExpandedRowMap };
let formattedDocument = '';
try {
formattedDocument = document ? JSON.stringify(JSON.parse(document), null, 2) : '';
formattedDocument = document ? JSON.stringify(JSON.parse(item.document), null, 2) : '';
} catch {
// no-op
}

const { indexPatternId } = this.state;
if (docIdToExpandedRowMapValues[item.id]) {
delete docIdToExpandedRowMapValues[item.id];
} else {
docIdToExpandedRowMapValues[item.id] = (
<EuiFormRow fullWidth={true}>
<EuiCodeBlock
language="json"
isCopyable
data-test-subj={`finding-details-flyout-rule-document-${item.itemIdx}`}
>
{formattedDocument}
</EuiCodeBlock>
</EuiFormRow>
);
}

this.setState({ docIdToExpandedRowMap: docIdToExpandedRowMapValues });
}

return document ? (
<>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size={'s'}>
<h3>Documents</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
isLoading={isDocumentLoading}
renderFindingDocuments(loadingIndexPatternId: boolean) {
const {
finding: { index, document_list, related_doc_ids },
} = this.props;
const { indexPatternId, docIdToExpandedRowMap } = this.state;
const relatedDocIdsSet = new Set(related_doc_ids);
const relatedDocuments: FindingDocumentItem[] = [];
document_list.forEach((documentInfo) => {
if (documentInfo.found && relatedDocIdsSet.has(documentInfo.id)) {
relatedDocuments.push({ ...documentInfo, itemIdx: relatedDocuments.length });
}
});

if (relatedDocuments.length === 0) {
return (
<>
<EuiTitle size={'s'}>
<h3>Documents</h3>
</EuiTitle>
<EuiSpacer />
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
title={<h2>Document not found</h2>}
body={<p>The document that generated this finding could not be loaded.</p>}
/>
</>
);
}

const actions = [
{
render: ({ id }: FindingDocumentItem) => (
<EuiToolTip title="View surrounding documents">
<EuiButtonIcon
disabled={loadingIndexPatternId}
iconType={'popout'}
data-test-subj={'finding-details-flyout-view-surrounding-documents'}
onClick={() => {
if (indexPatternId) {
window.open(
`discover#/context/${indexPatternId}/${related_doc_ids[0]}`,
'_blank'
);
window.open(`discover#/context/${indexPatternId}/${id}`, '_blank');
} else {
this.setState({ ...this.state, isCreateIndexPatternModalVisible: true });
}
}}
>
View surrounding documents
<EuiIcon type={'popout'} />
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>

<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>
<EuiFormRow
label={'Index'}
data-test-subj={'finding-details-flyout-rule-document-index'}
>
<EuiText>{index || DEFAULT_EMPTY_DATA}</EuiText>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>

<EuiSpacer size={'m'} />
/>
</EuiToolTip>
),
},
];

const documentsColumns: EuiBasicTableColumn<FindingDocumentItem>[] = [
{
name: '',
render: (item: FindingDocumentItem) => (
<EuiButtonIcon
onClick={() => this.toggleDocumentDetails(item)}
aria-label={docIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
iconType={docIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
data-test-subj={`finding-details-flyout-document-toggle-${item.itemIdx}`}
/>
),
width: '30',
isExpander: true,
},
{
field: 'id',
name: 'Document Id',
},
{
name: 'Actions',
actions,
},
];

<EuiFormRow fullWidth={true}>
<EuiCodeBlock
language="json"
isCopyable
data-test-subj={'finding-details-flyout-rule-document'}
>
{formattedDocument}
</EuiCodeBlock>
</EuiFormRow>
</>
) : (
return (
<>
<EuiTitle size={'s'}>
<h3>Documents</h3>
</EuiTitle>
<EuiSpacer />
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
title={<h2>Document not found</h2>}
body={<p>The document that generated this finding could not be loaded.</p>}
<EuiFormRow label={'Index'} data-test-subj={`finding-details-flyout-rule-document-index`}>
<EuiText size="s">{index || DEFAULT_EMPTY_DATA}</EuiText>
</EuiFormRow>
<EuiSpacer />
<EuiInMemoryTable
columns={documentsColumns}
items={relatedDocuments}
itemId="id"
itemIdToExpandedRowMap={docIdToExpandedRowMap}
isExpandable={true}
hasActions={true}
pagination={true}
/>
</>
);
Expand All @@ -400,6 +448,7 @@ export default class FindingDetailsFlyout extends Component<
const {
finding: { related_doc_ids },
} = this.props;

if (this.state.isCreateIndexPatternModalVisible) {
return (
<EuiModal
Expand Down Expand Up @@ -449,7 +498,7 @@ export default class FindingDetailsFlyout extends Component<
}
}

private getTabContent(tabId: FindingFlyoutTabId, isDocumentLoading = false) {
private getTabContent(tabId: FindingFlyoutTabId, loadingIndexPatternId = false) {
switch (tabId) {
case FindingFlyoutTabId.CORRELATIONS:
const logTypes = new Set<string>();
Expand All @@ -473,11 +522,11 @@ export default class FindingDetailsFlyout extends Component<
);
case FindingFlyoutTabId.DETAILS:
default:
return this.createFindingDetails(isDocumentLoading);
return this.createFindingDetails(loadingIndexPatternId);
}
}

private createFindingDetails(isDocumentLoading: boolean) {
private createFindingDetails(loadingIndexPatternId: boolean) {
const {
finding: { queries, detectionType },
} = this.props;
Expand Down Expand Up @@ -536,7 +585,7 @@ export default class FindingDetailsFlyout extends Component<
<EuiSpacer size="l" />
</>
)}
{this.renderFindingDocuments(isDocumentLoading)}
{this.renderFindingDocuments(loadingIndexPatternId)}
</>
);
}
Expand All @@ -546,7 +595,7 @@ export default class FindingDetailsFlyout extends Component<
const {
finding: { id, timestamp, detectionType },
} = this.props;
const { isDocumentLoading } = this.state;
const { loadingIndexPatternId } = this.state;
return (
<EuiFlyout
onClose={DataStore.findings.closeFlyout}
Expand Down Expand Up @@ -626,7 +675,7 @@ export default class FindingDetailsFlyout extends Component<
this.setState({
selectedTab: {
id: tab.id,
content: this.getTabContent(tab.id, isDocumentLoading),
content: this.getTabContent(tab.id, loadingIndexPatternId),
},
});
}}
Expand Down
Loading

0 comments on commit cdacb14

Please sign in to comment.