-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[Security Solution] Investigation guide - insights in markdown #145240
Changes from 19 commits
843821a
a6fb6fa
6963564
dafe3ee
5c75894
e3a0fa9
0096785
2957a12
95b177f
e85e7d1
8f0c80a
3d065c3
119e094
7f55bb7
5fd36e7
3a6b4f6
9b32d75
89eb612
cf46b22
f716b8f
7ca6049
7cac44c
28f00d6
386ac1b
3ad85e3
710f476
4a2b16e
be6cf77
b5fa33d
89bdd49
5558d77
b845243
8273e2f
06c3c18
997bcf5
012863c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
/* | ||
* 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 { Plugin } from 'unified'; | ||
import React, { useContext } from 'react'; | ||
import type { RemarkTokenizer } from '@elastic/eui'; | ||
import { EuiSpacer, EuiCodeBlock, EuiLoadingSpinner, EuiIcon } from '@elastic/eui'; | ||
import type { EuiMarkdownEditorUiPluginEditorProps } from '@elastic/eui/src/components/markdown_editor/markdown_types'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { useAppToasts } from '../../../../hooks/use_app_toasts'; | ||
import { useInsightQuery } from './use_insight_query'; | ||
import { useInsightDataProviders } from './use_insight_data_providers'; | ||
import { BasicAlertDataContext } from '../../../event_details/investigation_guide_view'; | ||
import { InvestigateInTimelineButton } from '../../../event_details/table/investigate_in_timeline_button'; | ||
|
||
interface InsightComponentProps { | ||
label?: string; | ||
description?: string; | ||
providers?: string; | ||
} | ||
|
||
export const parser: Plugin = function () { | ||
const Parser = this.Parser; | ||
const tokenizers = Parser.prototype.inlineTokenizers; | ||
const methods = Parser.prototype.inlineMethods; | ||
const insightPrefix = '!{insight'; | ||
|
||
const tokenizeInsight: RemarkTokenizer = function (eat, value, silent) { | ||
if (value.startsWith(insightPrefix) === false) { | ||
return false; | ||
} | ||
|
||
const nextChar = value[insightPrefix.length]; | ||
if (nextChar !== '{' && nextChar !== '}') return false; | ||
if (silent) { | ||
return true; | ||
} | ||
|
||
// is there a configuration? | ||
const hasConfiguration = nextChar === '{'; | ||
|
||
let configuration: InsightComponentProps = {}; | ||
if (hasConfiguration) { | ||
let configurationString = ''; | ||
let openObjects = 0; | ||
|
||
for (let i = insightPrefix.length; i < value.length; i++) { | ||
const char = value[i]; | ||
if (char === '{') { | ||
openObjects++; | ||
configurationString += char; | ||
} else if (char === '}') { | ||
openObjects--; | ||
if (openObjects === -1) { | ||
break; | ||
} | ||
configurationString += char; | ||
} else { | ||
configurationString += char; | ||
} | ||
} | ||
|
||
try { | ||
configuration = JSON.parse(configurationString); | ||
return eat(value)({ | ||
type: 'insight', | ||
...configuration, | ||
providers: JSON.stringify(configuration.providers), | ||
}); | ||
} catch (err) { | ||
const now = eat.now(); | ||
this.file.fail( | ||
i18n.translate('xpack.securitySolution.markdownEditor.plugins.insightConfigError', { | ||
values: { err }, | ||
defaultMessage: 'Unable to parse insight JSON configuration: {err}', | ||
}), | ||
{ | ||
line: now.line, | ||
column: now.column + insightPrefix.length, | ||
} | ||
); | ||
} | ||
} | ||
return false; | ||
}; | ||
tokenizeInsight.locator = (value: string, fromIndex: number) => { | ||
return value.indexOf(insightPrefix, fromIndex); | ||
}; | ||
tokenizers.insight = tokenizeInsight; | ||
methods.splice(methods.indexOf('text'), 0, 'insight'); | ||
}; | ||
|
||
// receives the configuration from the parser and renders | ||
const InsightComponent = ({ label, description, providers }: InsightComponentProps) => { | ||
const { addError } = useAppToasts(); | ||
let parsedProviders = []; | ||
try { | ||
if (providers !== undefined) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. providers can never be null? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't be, but if they are, we just pass an empty array for data providers, and the button will render but show 0 |
||
parsedProviders = JSON.parse(providers); | ||
} | ||
} catch (err) { | ||
addError(err, { | ||
title: i18n.translate('xpack.securitySolution.markdownEditor.plugins.insightProviderError', { | ||
defaultMessage: 'Unable to parse insight provider configuration', | ||
}), | ||
}); | ||
} | ||
const { data: alertData } = useContext(BasicAlertDataContext); | ||
const dataProviders = useInsightDataProviders({ | ||
providers: parsedProviders, | ||
alertData, | ||
}); | ||
const { totalCount, isQueryLoading, oldestTimestamp } = useInsightQuery({ | ||
dataProviders, | ||
}); | ||
if (isQueryLoading) { | ||
return <EuiLoadingSpinner size="l" />; | ||
} else { | ||
return ( | ||
<InvestigateInTimelineButton | ||
asEmptyButton={false} | ||
dataProviders={dataProviders} | ||
timeRange={oldestTimestamp} | ||
keepDataView={true} | ||
data-test-subj="insight-investigate-in-timeline-button" | ||
> | ||
<EuiIcon type="timeline" /> | ||
{` ${label} (${totalCount}) - ${description}`} | ||
</InvestigateInTimelineButton> | ||
); | ||
} | ||
}; | ||
|
||
export { InsightComponent as renderer }; | ||
|
||
const InsightEditorComponent = ({ | ||
node, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These props aren't used |
||
onSave, | ||
onCancel, | ||
}: EuiMarkdownEditorUiPluginEditorProps<InsightComponentProps>) => { | ||
return ( | ||
<form> | ||
<input type="text" /> | ||
</form> | ||
); | ||
}; | ||
|
||
export const plugin = { | ||
name: 'insight', | ||
button: { | ||
label: 'Insights', | ||
iconType: 'timeline', | ||
}, | ||
helpText: ( | ||
<div> | ||
<EuiCodeBlock language="md" fontSize="l" paddingSize="s" isCopyable> | ||
{'!{insight{options}}'} | ||
</EuiCodeBlock> | ||
<EuiSpacer size="s" /> | ||
</div> | ||
), | ||
editor: InsightEditorComponent, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it always to the current time? and should
timerange
befrom
instead?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ya agree this was weirdly done by me, forgot to go back and change it. updated now so that timerange is passed as a prop or not at all. For the now, I think oldest timestamp -> now is as inclusive as we can be, since a value has to be supplied, not sure what else we could use.