From 7617d340fa9e6558ff868d513b399f1e7aa216e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A1clav=20Barto=C5=A1?= <bartos@cesnet.cz> Date: Wed, 26 Aug 2020 10:59:54 +0200 Subject: [PATCH] Completely reworked to support multiple observable fields Needs some more testing, but everything seems to work well. --- thehive_button/package.json | 4 +- thehive_button/public/create_case.js | 101 +++ thehive_button/public/main.js | 112 +--- thehive_button/public/options_editor.js | 176 +++++ thehive_button/public/options_template.html | 8 - thehive_button/public/request_handler.js | 195 ++++++ thehive_button/public/vis.less | 3 - thehive_button/public/vis_controller.js | 706 +++++++++----------- thehive_button/server/routes/newcase.js | 3 +- 9 files changed, 820 insertions(+), 488 deletions(-) create mode 100644 thehive_button/public/create_case.js create mode 100644 thehive_button/public/options_editor.js delete mode 100644 thehive_button/public/options_template.html create mode 100644 thehive_button/public/request_handler.js delete mode 100644 thehive_button/public/vis.less diff --git a/thehive_button/package.json b/thehive_button/package.json index 5dfa6de..e1c070d 100644 --- a/thehive_button/package.json +++ b/thehive_button/package.json @@ -1,10 +1,10 @@ { "name": "thehive_button", - "version": "0.3.0", + "version": "1.0.0", "description": "Visualisation plugin which creates a simple button to create a new case in The Hive.", "main": "index.js", "kibana": { - "version": "7.2.0" + "version": "7.4.2" }, "scripts": { "lint": "eslint .", diff --git a/thehive_button/public/create_case.js b/thehive_button/public/create_case.js new file mode 100644 index 0000000..fc8edd6 --- /dev/null +++ b/thehive_button/public/create_case.js @@ -0,0 +1,101 @@ +// Functions to send data to Kibana endpoints + +import chrome from 'ui/chrome'; + +// Create a new Case in The Hive via its API +// Return a Promise which resolves to object with ID of the new case ('id' attr) or error message ('error' attr) +export function createTheHiveCase(base_url, api_key, title, descr, severity, startDate, owner, flag, tlp, tags) { + // Prepare data + var data = JSON.stringify({ + "base_url": base_url, + "api_key": api_key, + "body": { + "title": title, + "description": descr, + "severity": severity, // number: 1=low, 2=medium, 3=high + "startDate": startDate, + "owner": owner, // user name the case will be assigned to + "flag": flag, // bool + "tlp": tlp, // number: 0=white, 1=green, 2=amber, 3=red + "tags": tags, // array of strings + } + }); + console.log("TheHiveButton: Sending request to API endpoint 'new_case':", data); + var kibana_endpoint_url = chrome.addBasePath('/api/thehive_button/new_case'); + + return new Promise(function (resolve, reject) { + // Create AJAX request + var xhr = new XMLHttpRequest(); + + // Listener to process reply + xhr.onreadystatechange = function () { + if (this.readyState != 4) { + return; // response not ready yet + } + if (this.status == 200) { + const resp = JSON.parse(this.responseText); + console.log("TheHiveButton: Response from backend:", resp); + if ("error" in resp) { + resolve({"error": resp.error}); + } + else if (resp.status_code != 201) { + resolve({"error": "Unexpected reply received from The Hive: [" + resp.status_code + "] " + resp.status_msg}); + } + else { + resolve({"id": resp.body.id}); // return ID of the new case + } + } + else { + console.log("TheHiveButton: Error " + this.status + ": " + this.statusText); + resolve({"error": "Error " + this.status + ": " + this.statusText}); + } + } + + // Send the AJAX request + xhr.open("POST", kibana_endpoint_url); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.setRequestHeader("kbn-xsrf", "thehive_plugin"); // this header must be set, although its content is probably irrelevant + xhr.send(data); + }); +} + +// Add observables to an existing Case in The Hive +// (send the list of observables to our backend endpoint, it pushes them to The Hive) +export function addCaseObservables(base_url, api_key, caseid, observables) { + const kibana_endpoint_url = chrome.addBasePath('/api/thehive_button/add_observables'); + const data = JSON.stringify({ + "base_url": base_url, + "api_key": api_key, + "caseid": caseid, + "observables": observables, + }); + console.log("TheHiveButton: Sending request to API endpoint 'add_observables':", data); + + return new Promise(function (resolve, reject) { + // Create AJAX request + var xhr = new XMLHttpRequest(); + + // Listener to process reply + xhr.onreadystatechange = function () { + if (this.readyState != 4) { + return; // response not ready yet + } + if (this.status == 200) { + const resp = JSON.parse(this.responseText); + console.log("TheHiveButton: Response from backend:", resp); + resolve(resp); + } + else { + console.log("TheHiveButton: Error " + this.status + ": " + this.statusText); + resolve({"error": "Error " + this.status + ": " + this.statusText}); + } + } + + // Send the AJAX request + xhr.open("POST", kibana_endpoint_url); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.setRequestHeader("kbn-xsrf", "thehive_plugin"); // this header must be set, although its content is probably irrelevant + xhr.send(data); + }); +} + diff --git a/thehive_button/public/main.js b/thehive_button/public/main.js index 3631569..ee46d73 100644 --- a/thehive_button/public/main.js +++ b/thehive_button/public/main.js @@ -1,52 +1,18 @@ -//import './vis.less'; import { THEHIVE_API_KEY, THEHIVE_URL, THEHIVE_OWNER } from './env'; -//import { VisController } from './vis_controller'; import { TheHiveButtonVisComponent } from './vis_controller'; +import { theHiveButtonRequestHandlerProvider } from './request_handler'; +import { optionsEditor } from './options_editor'; import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; -import { Schemas } from 'ui/vis/editors/default/schemas'; -//import { Status } from 'ui/vis/update_status'; import { DefaultEditorSize } from 'ui/vis/editor_size'; -import optionsTemplate from './options_template.html'; - -/* -TODO: It would be better to compose options tab of EUI React elements, -but this probably needs at least kibana 7.4. -See this for example of a custom editor: - https://github.com/elastic/kibana/tree/master/src/plugins/vis_type_markdown/public - -import React from 'react'; -import { - EuiForm, - EuiFormRow, - EuiFieldText, -} from '@elastic/eui'; - -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; - -function optionsEditor({ stateParams, setValue }) { - return ( - <EuiForm> - <EuiFormRow label="Base URL of The Hive"> - <EuiFieldText value={stateParams.url} /> - </EuiFormRow> - <EuiFormRow label="API key"> - <EuiFieldText value={stateParams.apikey} /> - </EuiFormRow> - <EuiFormRow label="Username" helpText="Used as the owner of cases created from here"> - <EuiFieldText value={stateParams.owner} /> - </EuiFormRow> - </EuiForm> -); -*/ function TheHiveButtonVisProvider(Private) { const VisFactory = Private(VisFactoryProvider); - console.log("default URL:", THEHIVE_URL); - console.log("default API key:", THEHIVE_API_KEY); + //console.log("default URL:", THEHIVE_URL); + //console.log("default API key:", THEHIVE_API_KEY); return VisFactory.createReactVisualization({ name: 'thehive_button', @@ -61,65 +27,25 @@ function TheHiveButtonVisProvider(Private) { url: THEHIVE_URL, apikey: THEHIVE_API_KEY, owner: THEHIVE_OWNER, + obsFields: [], // list of objects, e.g. {name: "clientip", type: "ip", cnt: 100} } }, -// editor: optionsEditor, - editor: 'default', - editorConfig: { - optionsTemplate: optionsTemplate, - defaultSize: DefaultEditorSize.MEDIUM, - //enableAutoApply: true, - schemas: new Schemas([ - // TODO all this (metric and bucket "terms") could be pre-defind - // (just a selection of fields to read Observables from would be nice) - // But I don't know how and where to pass the values manually. - /* Allowed params (from /src/legacy/ui/public/vis/editors/default/schemas.d.ts) - aggFilter: string | string[]; - editor: boolean | string; - group: AggGroupNames; - max: number; - min: number; - name: string; - params: AggParam[]; - title: string; - */ - { - group: 'metrics', - name: 'metric', - title: "Metric (not used, ignore this)", - min: 1, - max: 1, - aggFilter: 'count', - defaults: [ - { - type: 'count', - schema: 'metric', - }, - ], - editor: '<div class="hintbox"><i class="fa fa-danger text-info"></i> Some metric must be defined here, but it\'s setting is irrelevant. Just go to "Buckets" below and set up the field to get Observbles from.</div>' - }, + //editor: 'default', + editorConfig: { + optionTabs: [ { - group: 'buckets', - name: 'group', - title: 'Observables', - min: 0, - //max: 1, - aggFilter: ['terms'], - defaults: [ - { - type: 'terms', - schema: 'group', - field: 'ip', - size: 1000, - orderBy: 'alphabetical', - order: 'ascending', - } - ] - }, - ]), + name: "options", + title: "Options", + editor: optionsEditor, + } + ], + defaultSize: DefaultEditorSize.LARGE, }, - //requestHandler: 'courier', // default - //responseHandler: 'default', // return data as a table, see https://www.elastic.co/guide/en/kibana/7.2/development-visualization-response-handlers.html +// optionsTemplate: optionsEditor, //optionsTemplate, +// //enableAutoApply: true, +// }, + requestHandler: 'theHiveButtonRequestHandler', // own request handler + responseHandler: 'none', // pass data as returned by requestHandler }); } diff --git a/thehive_button/public/options_editor.js b/thehive_button/public/options_editor.js new file mode 100644 index 0000000..38762bd --- /dev/null +++ b/thehive_button/public/options_editor.js @@ -0,0 +1,176 @@ +import React from 'react'; +import { + EuiForm, + EuiFormRow, + EuiTitle, + EuiSpacer, + EuiFieldText, + EuiFieldNumber, + EuiSelect, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonIcon, +} from '@elastic/eui'; + +// Default data types in The Hive +const DEFAULT_THE_HIVE_TYPES = [ + '', + 'autonomous-system', + 'domain', + 'file', + 'filename', + 'fqdn', + 'hash', + 'ip', + 'mail', + 'mail_subject', + 'regexp', + 'registry', + 'uri_path', + 'url', + 'user-agent', + 'other', +]; + +// Options for EuiSelect for selection of field's data type in TheHive +const typesOptions = DEFAULT_THE_HIVE_TYPES.map( dt => ({value: dt, text: dt}) ); + +export function optionsEditor(props) { + //console.log("editor render(), props:", props); + const { stateParams, setValue, setValidity, vis } = props; + + // onClick/onChange handlers + const obsAddNew = () => { + const newObsFields = [...stateParams.obsFields, {name: "", type: "", cnt: 100}]; + // For some reason, first click on the button after editor is loaded does + // nothing. Calling setValue twice here fixes it. + setValue("obsFields", newObsFields); + setValue("obsFields", newObsFields); +// setValidity(false); // since new row is empty, form is always invalid + }; + const obsRemove = (ix) => { + let newArray = [...stateParams.obsFields]; + newArray.splice(ix, 1); + setValue("obsFields", newArray); +// validate(); + } + const obsSetName = (ix, name) => { + let newArray = [...stateParams.obsFields]; + newArray[ix].name = name; + setValue("obsFields", newArray); +// validate(); + } + const obsSetType = (ix, type) => { + let newArray = [...stateParams.obsFields]; + newArray[ix].type = type; + setValue("obsFields", newArray); +// validate(); + } + const obsSetCnt = (ix, cnt) => { + let newArray = [...stateParams.obsFields]; + newArray[ix].cnt = parseInt(cnt); + setValue("obsFields", newArray); +// validate(); + } +// const validate = () => { +// let valid = true; +// for (let field of stateParams.obsFields) { +// if (field.name == "" || field.type == "" || field.cnt == "") { +// valid = false; +// break; +// } +// } +// // TODO check for duplicate fields +// setValidity(valid); +// } + + // Get list of all fields in index (except those beginning with "_" or "@") + // and create "options" parameter for EuiSelect. + // Also, fields with "aggregatable=false" are removed, as they can't be used + // with "terms" aggregation we need. + // See this for details: https://www.elastic.co/guide/en/elasticsearch/reference/7.x/fielddata.html + // Empty field is added at the beginning, meaning "no selection yet". + const fieldOptions = [{value: "", text: ""}].concat( + vis.indexPattern.fields.raw.filter( f => (f.name[0] != "_" && f.name[0] != "@" && f.aggregatable) ).map( f => ({value: f.name, text: `${f.name} (${f.type})`}) ) + ); + + return <EuiForm> + <EuiFormRow fullWidth={true} label="Base URL of The Hive"> + <EuiFieldText + fullWidth={true} + value={stateParams.url} + onChange={e => setValue('url', e.target.value)} + isInvalid={stateParams.url == ""} + /> + </EuiFormRow> + <EuiFlexGroup> + <EuiFlexItem grow={1}> + <EuiFormRow label="API key to access The Hive" helpText="API key of a user with write permission."> + <EuiFieldText + fullWidth={true} + value={stateParams.apikey} + onChange={e => setValue('apikey', e.target.value)} + isInvalid={stateParams.apikey == ""} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={1}> + <EuiFormRow label="Assignee" helpText="User to assign created cases to. Must be a valid username from The Hive instance."> + <EuiFieldText + value={stateParams.owner} + onChange={e => setValue('owner', e.target.value)} + isInvalid={stateParams.owner == ""} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiTitle size="s"><h3>Fields to get potential observables from ...</h3></EuiTitle> + <EuiSpacer size="s" /> + {stateParams.obsFields.map( (field, ix) => ( + <EuiFlexGroup key={ix} gutterSize="s"> + <EuiFlexItem grow={3}> + <EuiFormRow label="Field name"> + <EuiSelect + options={fieldOptions} + value={field.name} + onChange={ e => obsSetName(ix, e.target.value) } + isInvalid={field.name == ""} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={2}> + <EuiFormRow label="Data type in The Hive"> + <EuiSelect + options={typesOptions} + value={field.type} + onChange={ e => obsSetType(ix, e.target.value) } + isInvalid={field.type == ""} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={1}> + <EuiFormRow label="Max items shown"> + <EuiFieldNumber + min={1} + max={1000} + value={parseInt(field.cnt)} + onChange={ e => obsSetCnt(ix, e.target.value) } + isInvalid={!(field.cnt > 0)} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButtonIcon iconType="trash" iconSize="m" color="danger" aria-label="Remove field" onClick={ e => obsRemove(ix) } /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + ))} + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton iconType="plusInCircleFilled" color="primary" onClick={obsAddNew}>Add new field ...</EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiForm> +} diff --git a/thehive_button/public/options_template.html b/thehive_button/public/options_template.html deleted file mode 100644 index ef99657..0000000 --- a/thehive_button/public/options_template.html +++ /dev/null @@ -1,8 +0,0 @@ -<div class="form-group"> - <p><label>Base URL of The Hive</label> - <input ng-model="editorState.params.url" class=form-control /></p> - <p><label>API key</label> - <input ng-model="editorState.params.apikey" class=form-control /></p> - <p><label>User name to use as the owner of cases created from here</label> - <input ng-model="editorState.params.owner" class=form-control /></p> -</div> diff --git a/thehive_button/public/request_handler.js b/thehive_button/public/request_handler.js new file mode 100644 index 0000000..bdbb0f4 --- /dev/null +++ b/thehive_button/public/request_handler.js @@ -0,0 +1,195 @@ +import { CourierRequestHandlerProvider as courierRequestHandlerProvider } from 'ui/vis/request_handlers/courier'; +import { SearchSourceProvider } from 'ui/courier/search_source'; +import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; +import { VisRequestHandlersRegistryProvider } from 'ui/registry/vis_request_handlers'; +import { AggConfig } from 'ui/vis/agg_config'; +import { AggConfigs } from 'ui/vis/agg_configs'; +import { getTime } from 'ui/timefilter/get_time'; +import { i18n } from '@kbn/i18n'; +import { has } from 'lodash'; +import { calculateObjectHash } from 'ui/vis/lib/calculate_object_hash'; +import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; +import chrome from 'ui/chrome'; + +// Maximum number of unique values of each field (observables) to fetch +const MAX_NUMBER_OF_TERMS = 5; + +const handleCourierRequest = courierRequestHandlerProvider().handler; + +// Register new RaquestHandlerProvider +const theHiveButtonRequestHandlerProvider = function () { + return { + name: 'theHiveButtonRequestHandler', + handler: theHiveButtonRequestHandler, + } +} +VisRequestHandlersRegistryProvider.register(theHiveButtonRequestHandlerProvider); + +export {theHiveButtonRequestHandlerProvider, theHiveButtonRequestHandler}; + + +// The request handler function itself +async function theHiveButtonRequestHandler(params) { + //console.log("theHiveButtonRequestHandler params:", params); + + let index = params.index; + let partialRows = params.partialRows; + let metricsAtAllLevels = params.metricsAtAllLevels; + let timeRange = params.timeRange; + let query = params.query; + let filters = params.filters; + let inspectorAdapters = params.inspectorAdapters; + let queryFilter = params.queryFilter; + let forceFetch = params.forceFetch; + // our own confiuration: + // list of fields to get potential observables from + // (each "field" is object {name: str, type: str, cnt: int}) + let obsFields = params.visParams.obsFields; + + // filter out invalid field specifications + obsFields = obsFields.filter( f => (f.name != "" && f.type != "" && f.cnt > 0) ); + + if (obsFields.length == 0) { + //console.log("theHiveButtonRequestHandler: Empty obsFields, nothing to do") + return {} // no fields specified, nothing to do + } + + // === Prepare request to ask for unique values of all selected fields === + + // Construct a query for ElasticSearch + // Get "terms" (most common unique values) for each field of obsFields + const aggs_dsl = {} + for (let field of obsFields) { + aggs_dsl[field.name] = { + terms: { + field: field.name, + size: field.cnt, + order: {_count: "desc"} + } + }; + } + //console.log("aggs_dsl:", aggs_dsl); + + // Create empty AggConfigs + // (We could pass specifications of a metric and the buckets here, + // but default processing functions assume multiple buckets are sub-buckets, + // which is not what we want. So we must do a "hack" and manually create + // query directly in format for ElasticSearch) + const aggs = new AggConfigs(params.index, []); + + // === Some magic to get searchSource object === + // (inspired by https://github.com/fbaligand/kibana-enhanced-table/blob/7.4/public/data_load/enhanced-table-request-handler.js) + // (I don't understand it, but it works) + + let $injector = await chrome.dangerouslyGetActiveInjector(); + let Private = $injector.get('Private'); + let SearchSource = Private(SearchSourceProvider); + let searchSource = new SearchSource(); + searchSource.setField('index', index); + searchSource.setField('size', 0); + + inspectorAdapters.requests = new RequestAdapter(); + inspectorAdapters.data = new DataAdapter(); + + + // === Execute query === + // We could call standard "courier" here, but it tries to convert the response + // to a table, which fails in our case, so we copied the main code of courier + // and modified it here. + + const abortSignal = false; + + const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true }); + const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true }); + + aggs.setTimeRange(timeRange); + + // For now we need to mirror the history of the passed search source, since + // the request inspector wouldn't work otherwise. + Object.defineProperty(requestSearchSource, 'history', { + get() { + return searchSource.history; + }, + set(history) { + return searchSource.history = history; + } + }); + + // This has been modified to override DSL format by ours +// requestSearchSource.setField('aggs', function () { +// return aggs.toDsl(metricsAtAllLevels); +// }); + requestSearchSource.setField('aggs', aggs_dsl); + + requestSearchSource.onRequestStart((searchSource, searchRequest) => { + return aggs.onSearchRequestStart(searchSource, searchRequest); + }); + + if (timeRange) { + timeFilterSearchSource.setField('filter', () => { + return getTime(searchSource.getField('index'), timeRange); + }); + } + + requestSearchSource.setField('filter', filters); + requestSearchSource.setField('query', query); + + const reqBody = await requestSearchSource.getSearchRequestBody(); + + const queryHash = calculateObjectHash(reqBody); + // We only need to reexecute the query, if forceFetch was true or the hash of the request body has changed + // since the last request + const shouldQuery = forceFetch || (searchSource.lastQuery !== queryHash); + + if (shouldQuery) { + inspectorAdapters.requests.reset(); + const request = inspectorAdapters.requests.start( + i18n.translate('common.ui.vis.courier.inspector.dataRequest.title', { defaultMessage: 'Data' }), + { + description: i18n.translate('common.ui.vis.courier.inspector.dataRequest.description', + { defaultMessage: 'This request queries Elasticsearch to fetch the data for the visualization.' }), + } + ); + request.stats(getRequestInspectorStats(requestSearchSource)); + + try { + // Abort any in-progress requests before fetching again + if (abortSignal) { + abortSignal.addEventListener('abort', () => requestSearchSource.cancelQueued()); + } + + const response = await requestSearchSource.fetch(); + //console.log("raw response:", response); + + searchSource.lastQuery = queryHash; + + request + .stats(getResponseInspectorStats(searchSource, response)) + .ok({ json: response }); + + searchSource.rawResponse = response; + } catch(e) { + // Log any error during request to the inspector + request.error({ json: e }); + throw e; + } finally { + // Add the request body no matter if things went fine or not + requestSearchSource.getSearchRequestBody().then(req => { + request.json(req); + }); + } + } + + // === Copy of courier code ends here, now we parse the response === + + const resp = searchSource.rawResponse; + // Return as object containing a list of unique values (terms) for each + // requested field + let unique_values_lists = {} + for (let field of obsFields) { + unique_values_lists[field.name] = resp.aggregations[field.name].buckets.map( (x) => x.key ); + } + + //console.log("Final lists:", unique_values_lists); + return unique_values_lists; +} diff --git a/thehive_button/public/vis.less b/thehive_button/public/vis.less deleted file mode 100644 index b6f887a..0000000 --- a/thehive_button/public/vis.less +++ /dev/null @@ -1,3 +0,0 @@ -.myvis-container-div { - padding: 1em; -} diff --git a/thehive_button/public/vis_controller.js b/thehive_button/public/vis_controller.js index 3018390..8b23222 100644 --- a/thehive_button/public/vis_controller.js +++ b/thehive_button/public/vis_controller.js @@ -1,6 +1,6 @@ //import { Status } from 'ui/vis/update_status'; -import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; +import { createTheHiveCase, addCaseObservables } from './create_case'; //import vis_template from './vis_template.html'; import React, { Component } from 'react'; @@ -16,6 +16,7 @@ import { EuiTitle, EuiFlexGroup, EuiFlexItem, + EuiSpacer, EuiForm, EuiFormRow, EuiFieldText, @@ -26,27 +27,16 @@ import { makeId, } from '@elastic/eui'; -// TODO: -// - Explicit reset button -// - Reset when data (query) changes -// - Hide modal when clicked outside - // ********** React components ********** // Main React component - the root of visualization export class TheHiveButtonVisComponent extends Component { render() { - //console.log(this.props); - let ips = []; - if (this.props.visData) { - const first_col_id = this.props.visData.columns[0].id; - ips = this.props.visData.rows.map(row => row[first_col_id]); - //console.log(ips); - } + //console.log("TheHiveButtonVisComponent.render(), props:", this.props); return ( <div> - <NewCaseButton params={this.props.vis.params} observables={ips} /> + <NewCaseButton params={this.props.vis.params} observables={this.props.visData} /> </div> ); } @@ -63,55 +53,86 @@ export class TheHiveButtonVisComponent extends Component { // Button to show the pop-up window (modal) // Props: // .params - visualization parameters (from vis.params) -// .observables - list of IP addrsses or other observables to add to the Case +// .observables - object with lists of potential observables to add to the Case +// for each field in params.obsFields there should be a key in this object +// containing list of observables (this is returned by request_handler) class NewCaseButton extends Component { constructor(props) { super(props); - const n_obs = props.observables.length; - - // initial state of form fields - used to reset form - this.initial_case_state = { - // Case parameters - title: "", - description: "\n\n--\nCreated from Kibana", - severity: "2", // medium - tlp: "2", // amber - tags: [], // TODO (not implemented yet) - // An array of values for each editable part of observables, prefilled with default values - obsDescrs: new Array(n_obs).fill(""), - obsTLPs: new Array(n_obs).fill(2), // TODO (not implemented yet) - obsIOCs: new Array(n_obs).fill(false), - obsTags: new Array(n_obs).fill([]), // TODO (not implemented yet) - obsSelected: new Array(), // list of indices of selected observables - } + // Filter out invalid obsField specifications + this.obsFields = props.params.obsFields.filter( f => (f.name != "" && f.type != "" && f.cnt > 0) ); + //console.log("Filtered field specs:", this.obsFields); // The complete state is here, so it's kept even when modal is closed this.state = { isModalVisible: false, - ...this.initial_case_state, // TODO isn't deep copy needed here (and in reset_form below)? + isWorking: false, // used to show a spinner on submit button + ...this.create_initial_state(), } + + this.resetCnt = 0; // used to change Modal component key on each form reset // Each handler function in a class (method) must be "binded" this way this.closeModal = this.closeModal.bind(this); this.showModal = this.showModal.bind(this); + this.resetForm = this.resetForm.bind(this); this.onTitleChange = this.onTitleChange.bind(this); this.onSeverityChange = this.onSeverityChange.bind(this); this.onTLPChange = this.onTLPChange.bind(this); this.onDescriptionChange = this.onDescriptionChange.bind(this); - this.onObsDescrChange = this.onObsDescrChange.bind(this); - this.onObsTLPChange = this.onObsTLPChange.bind(this); - this.onObsIOCChange = this.onObsIOCChange.bind(this); - this.onObsTagsChange = this.onObsTagsChange.bind(this); this.onObsSelectionChange = this.onObsSelectionChange.bind(this); + this.onObsDataChange = this.onObsDataChange.bind(this); this.submitCase = this.submitCase.bind(this); } + create_initial_state() { + // create a new instance of initial state definition + let initial_state = { + // Case parameters + title: "", + description: "\n\n--\nCreated from Kibana", + severity: "2", // medium + tlp: "2", // amber + tags: [], // TODO (not implemented yet) + obsData: {}, // state of observables form fields (obsData->field->index->{descr,tlp,ioc,tags}) + obsSel: {}, // list of observable selections (obsSel->field->list_of_selected_indices) + } + // pre-fill state of each observable to defaults + const initial_field_data = {descr: "", tlp: 2, ioc: false, tags: []}; + for (let field of this.obsFields) { + const n_obs = this.props.observables[field.name].length; + // fill obsData with new copies of initial_field_data + initial_state.obsData[field.name] = new Array(n_obs).fill().map((_)=>({...initial_field_data})); + // nothing is selected + initial_state.obsSel[field.name] = new Array(); + } + return initial_state; + } + + componentDidUpdate(prevProps) { + // If list of observables was updated or obsFields setting has changed, + // reset the component state and precomputed variables. + if (this.props.observables != prevProps.observables) { + if (this.props.params.obsFields != prevProps.params.obsFields) { + // when obsFields change, observables must change as well, so this "if" + // can be inside the first one. + // Filter out invalid obsField specifications + this.obsFields = this.props.params.obsFields.filter( f => (f.name != "" && f.type != "" && f.cnt && f.cnt > 0) ); + //console.log("Filtered field specs:", this.obsFields); + } + //console.log("New list of observables, resetting form."); + this.resetForm(); + } + } + resetForm() { - this.setState(this.initial_case_state); + this.setState(this.create_initial_state()); + this.resetCnt += 1; // this changes the key of ModalContent, causing it to be replaced by new DOM elelments (otherwise, not all things are reset properly) + this.forceUpdate(); } closeModal() { @@ -136,53 +157,69 @@ class NewCaseButton extends Component { this.setState({description: evt.target.value}); } - // Event handlers for change of observable parameters - onObsDescrChange(evt, rowindex) { - const val = evt.target.value; - this.setState((state, props) => { - // Copy old obsDescrs, update the corresponding item and pass as new state - let descrs = [...state.obsDescrs]; - descrs[rowindex] = val; - return {obsDescrs: descrs}; - }); - } - onObsTLPChange(evt, rowindex) { // TODO - const val = evt.target.value; - this.setState((state, props) => { - // Copy old obsTLPs, update the corresponding item and pass as new state - let tlps = [...state.obsTLPs]; - tlps[rowindex] = val; - return {obsTLPs: tlps}; - }); - } - onObsIOCChange(evt, rowindex) { - const val = evt.target.checked; + // Event handler for observable (de)selection + onObsSelectionChange(fieldName, selectedItems) { + // Extract indices from the items and store them into state + const selectedIndices = selectedItems.map(item4 => item4.i); this.setState((state, props) => { - // Copy old obsIOCs, update the corresponding item and pass as new state - let iocs = [...state.obsIOCs]; - iocs[rowindex] = val; - return {obsIOCs: iocs}; + let newObsSel = {...this.state.obsSel}; + newObsSel[fieldName] = selectedIndices; + return {obsSel: newObsSel}; }); } - onObsTagsChange(evt, rowindex) { // TODO - const val = evt.target.value; + + // Event handler for edit of a form field in observable row + // - fieldName: which field (table of observables) + // - ix: index of the observable in the field's table + // - param: one of: descr,tlp,ioc,tags + // - value: new value of the form field + onObsDataChange(fieldName, ix, param, value) { this.setState((state, props) => { - // Copy old obsTags, update the corresponding item and pass as new state - let tags = [...state.obsTags]; - tags[rowindex] = val; - return {obsTags: tags}; + let newObsData = {...this.state.obsData}; + newObsData[fieldName][ix][param] = value; + return {obsData: newObsData}; }); } - - // Event handler for row (de)selection - onObsSelectionChange(selectedItems) { - // Extract indices from the items and store them into state - const selectedIndices = selectedItems.map(item => item.i); - this.setState({obsSelected: selectedIndices}); + + // Render function + render() { + let modal; + if (this.state.isModalVisible) { + modal = <ModalContent + resetCnt={this.resetCnt} // used to change "key" of modalBody, causing all form fields to be re-created (some things are not reset properly by reseting state only) + close={this.closeModal} + reset={this.resetForm} + fields={this.obsFields} + observables={this.props.observables} + // form state + title={this.state.title} + description={this.state.description} + severity={this.state.severity} + tlp={this.state.tlp} + tags={this.state.tags} + obsData={this.state.obsData} + obsSel={this.state.obsSel} + spinner={this.state.isWorking} + // event handlers + onTitleChange={this.onTitleChange} + onSeverityChange={this.onSeverityChange} + onTLPChange={this.onTLPChange} + onDescriptionChange={this.onDescriptionChange} + onObsSelectionChange={this.onObsSelectionChange} + onObsDataChange={this.onObsDataChange} + submitCase={this.submitCase} + />; + } + return ( + <div> + <EuiButton fill iconType="alert" color="danger" onClick={this.showModal}>Create new Case ...</EuiButton> + {modal} + </div> + ); } - + // Submit case button handler - submitCase(evt) { + async submitCase(evt) { const params = this.props.params; // Get case parameters @@ -202,131 +239,104 @@ class NewCaseButton extends Component { // Get list of selected observables and their params let observables = []; - let selectionIndices = [...this.state.obsSelected]; // make a copy - selectionIndices.sort(); - for (let i = 0; i < selectionIndices.length; i++) { - const j = selectionIndices[i]; // index of a selected obs. in the list of all observables - // fill in observable definition according to model at - // https://github.com/TheHive-Project/TheHiveDocs/blob/master/api/artifact.md - const obs = { - dataType: 'ip', - data: this.props.observables[j], - message: this.state.obsDescrs[j], - tlp: this.state.obsTLPs[j], - ioc: this.state.obsIOCs[j], - tags: this.state.obsTags[j], - }; - observables.push(obs); + for (let field of this.obsFields) { + let selectionIndices = [...this.state.obsSel[field.name]]; // make a copy + selectionIndices.sort(); + for (let i = 0; i < selectionIndices.length; i++) { + const j = selectionIndices[i]; // index of a selected obs. in the list of all observables + // fill in observable definition according to model at + // https://github.com/TheHive-Project/TheHiveDocs/blob/master/api/artifact.md + const obs = { + dataType: field.type, + data: this.props.observables[field.name][j], + message: this.state.obsData[field.name][j].descr, + tlp: this.state.obsData[field.name][j].tlp, + ioc: this.state.obsData[field.name][j].ioc, + tags: this.state.obsData[field.name][j].tags, + }; + observables.push(obs); + } } + //console.log("Selected observables:", observables); - // All data cached in local variables - reset the form fields - this.resetForm(); + // Check '/' at the end of base URL, add it if needed + let base_url = params.url; + if (base_url[base_url.length-1] != "/") { + base_url += "/"; + } + + // Show spinner at submit button + this.setState({isWorking: true}); // Submit request to create the case, handle response - // TODO rewite to await - createHiveCase(params.url, params.apikey, title, descr, severity, start_date, owner, flag, tlp, tags) - .then((resp) => { - if ('error' in resp) { - // Error contacting The Hive - console.error("TheHiveButton: ERROR when trying to create new case:", resp.error); - toastNotifications.addDanger("ERROR: " + resp.error); - return; - } + let resp; + resp = await createTheHiveCase(base_url, params.apikey, title, descr, severity, start_date, owner, flag, tlp, tags); + + if ('error' in resp) { + // Error contacting The Hive + console.error("TheHiveButton: ERROR when trying to create new case:", resp.error); + toastNotifications.addDanger("ERROR: " + resp.error); + this.setState({isWorking: false}); // Hide spinner + return; + } - console.log("TheHiveButton: Case created:", resp); - const case_id = resp.id; - const case_url = params.url + "index.html#/case/" + case_id + "/details"; - - // Show notification - let obs_text; - if (observables.length > 0) { - obs_text = "Adding " + observables.length + " observables in background ..."; - } - else { - obs_text = "(no observables added)"; - } - toastNotifications.add({ - title: "Case created", - color: "success", - iconType: "checkInCircleFilled", - text: ( - <div> - <p><b><a href={case_url} target="_blank">Edit the new Case</a></b></p> - <p>{obs_text}</p> - </div> - ), - }); - - // Open a new window with the case in The Hive - // (adding observables may take some time, so the case is opened first; - // The Hive web is dynamic so the observables appear as they are added) - window.open(case_url, '_blank'); - - if (observables.length == 0) - return; - - // Submit request to add observables - console.log("TheHiveButton: adding " + observables.length + " observables ..."); - addCaseObservables(params.url, params.apikey, case_id, observables) - .then((resp) => { - if ('error' in resp) { - console.error("TheHiveButton: ERROR when trying to add observables: " + resp.error); - toastNotifications.addDanger("ERROR when trying to add observables: " + resp.error); - } - else { - console.log("TheHiveButton: Done, observables added."); - toastNotifications.add("Done, observables added."); - } - }); // addObservables().then() func - }); // createHiveCase.then() func + console.log("TheHiveButton: Case created:", resp); + const case_id = resp.id; + const case_url = base_url + "index.html#/case/" + case_id + "/details"; + + // Show notification + let obs_text; + if (observables.length > 0) { + obs_text = "Adding " + observables.length + " observables in background ..."; + } + else { + obs_text = "(no observables added)"; + } + toastNotifications.add({ + title: "Case created", + color: "success", + iconType: "checkInCircleFilled", + text: ( + <div> + <p><b><a href={case_url} target="_blank">Edit the new Case</a></b></p> + <p>{obs_text}</p> + </div> + ), + }); - // Close the popup window + // Close the popup window, reset form fields and hide spinner this.closeModal(); - } - - // Render function - render() { - let modal; - if (this.state.isModalVisible) { - modal = <ModalContent - close={this.closeModal} - observables={this.props.observables} - // form state - title={this.state.title} - description={this.state.description} - severity={this.state.severity} - tlp={this.state.tlp} - tags={this.state.tags} - obsDescrs={this.state.obsDescrs} - obsTLPs={this.state.obsTLPs} - obsIOCs={this.state.obsIOCs} - obsTags={this.state.obsTags} - obsSelected={this.state.obsSelected} - // event handlers - onTitleChange={this.onTitleChange} - onSeverityChange={this.onSeverityChange} - onTLPChange={this.onTLPChange} - onDescriptionChange={this.onDescriptionChange} - onObsDescrChange={this.onObsDescrChange} - onObsTLPChange={this.onObsTLPChange} - onObsIOCChange={this.onObsIOCChange} - onObsTagsChange={this.onObsTagsChange} - onObsSelectionChange={this.onObsSelectionChange} - submitCase={this.submitCase} - />; + this.resetForm(); + this.setState({isWorking: false}); + + // Open a new window with the case in The Hive + // (adding observables may take some time, so the case is opened first; + // The Hive web is dynamic so the observables appear as they are added) + window.open(case_url, '_blank'); + + if (observables.length == 0) + return; + + // Submit request to add observables + console.log("TheHiveButton: adding " + observables.length + " observables ..."); + resp = await addCaseObservables(base_url, params.apikey, case_id, observables); + + if ('error' in resp) { + console.error("TheHiveButton: ERROR when trying to add observables: " + resp.error); + toastNotifications.addDanger("ERROR when trying to add observables: " + resp.error); + } + else { + console.log("TheHiveButton: Done, observables added."); + toastNotifications.add("Done, observables added."); } - return ( - <div> - <EuiButton fill iconType="alert" color="danger" onClick={this.showModal}>Create new Case ...</EuiButton> - {modal} - </div> - ); } } // The popup window with a form +// props: +// - spinner: when true, disable form and show a spinner over it class ModalContent extends Component { constructor(props) { super(props); @@ -344,79 +354,22 @@ class ModalContent extends Component { {value: "2", inputDisplay: "amber"}, {value: "3", inputDisplay: "red"}, ]; - - // Table columns definition - this.columns = [ - { - field: "id", - name: "Observable", - }, - { - field: "descr", - name: "Description", - description: "Description of the observable in the context of the case", - render: (value, item) => (<EuiFieldText - value={item.descr} - onChange={(e) => this.props.onObsDescrChange(e, item.i)} - disabled={!item.selected} - />) - }, - /*{ - field: "tlp", - name: "TLP", - dataType: "number", - // TODO render and process changes - },*/ - { - field: "ioc", - name: "Is IOC", - dataType: "boolean", - description: "Indicates if the observable is an IOC", - render: (value, item) => (<EuiCheckbox - id={"ioc-checkbox-"+item.id} - checked={item.ioc} - onChange={(e) => this.props.onObsIOCChange(e, item.i)} - disabled={!item.selected} - />) - }, - /*{ - field: "tags", - name: "Tags", - // TODO render and process changes - },*/ - ] - - // Create a reference to observables table, so it's node can be accessed in componentDidMount - this.obsTableRef = React.createRef(); } // Main render function render() { - const obs = this.props.observables; - // Create data for the observables table - let table_data = new Array(obs.length); - for (let i = 0; i < obs.length; i++) { - table_data[i] = { - id: obs[i], - descr: this.props.obsDescrs[i], - tlp: this.props.obsTLPs[i], - ioc: this.props.obsIOCs[i], - tags: this.props.obsTags[i], - // auxiliary fields, not shown in table: - i: i, // row index - selected: this.props.obsSelected.includes(i), - }; - } - this.table_data = table_data; // needed in componentDidMount - + // TODO: replace Modal with Flyout? + + // Note: onClick on EuiOverlayMask causes close of modal when clicked outside, + // implementation inspired by PR: https://github.com/elastic/eui/pull/3462/files#diff-c8fda532e48f75c94c343247cbc6b2d3R53-R60 return ( - <EuiOverlayMask> + <EuiOverlayMask onClick={(evt) => {if (evt.target.classList.contains("euiOverlayMask")) this.props.close();} }> <EuiModal onClose={this.props.close} maxWidth={false} initialFocus="[name=title]"> <EuiModalHeader> <EuiModalHeaderTitle>Create a new case in The Hive</EuiModalHeaderTitle> </EuiModalHeader> - <EuiModalBody> + <EuiModalBody key={this.props.resetCnt}> <EuiForm style={{width: "800px"}}> <EuiFlexGroup> <EuiFlexItem grow={1}> @@ -453,157 +406,150 @@ class ModalContent extends Component { /> </EuiFormRow> - <EuiTitle size="s"><h3>Add observables from current query ...</h3></EuiTitle> - <EuiBasicTable - ref={this.obsTableRef} - columns={this.columns} - items={table_data} - itemId={(item) => item.id} - selection={ {onSelectionChange: this.props.onObsSelectionChange} } - noItemsMessage="No observables found" - rowProps={{ - // Hack to allow selection by clicking anywhere in the table row - // (except input elements) - onClick: (e) => { - if (e.target.tagName != "INPUT") { - // simulate click on the first checkbox in the row to (de)select the row - e.currentTarget.querySelector("input").click(); - e.currentTarget.blur(); // without this the focus remains on the row after click (results in different color) - } - }, - tabIndex: "-1", // prevents focus on row by keyboard navigation - }} - /> + {this.props.fields.length > 0 && <EuiTitle size="s"><h3>Add observables from current query ...</h3></EuiTitle>} + {this.props.fields.map((field,ix) => ( + <ObservablesTable + key={field.name + ":" + this.props.resetCnt} + fieldName={field.name} + observables={this.props.observables[field.name]} + obsData={this.props.obsData[field.name]} + obsSel={this.props.obsSel[field.name]} + onObsSelectionChange={this.props.onObsSelectionChange} + onObsDataChange={this.props.onObsDataChange} + /> + ))} </EuiForm> </EuiModalBody> <EuiModalFooter> - <EuiButtonEmpty onClick={this.props.close}>Cancel</EuiButtonEmpty> - <EuiButton onClick={this.props.submitCase} fill>Create Case</EuiButton> + <EuiButtonEmpty onClick={this.props.close}>Close</EuiButtonEmpty> + <EuiButtonEmpty onClick={this.props.reset}>Reset</EuiButtonEmpty> + <EuiButton onClick={this.props.submitCase} fill isLoading={this.props.spinner}>Create Case</EuiButton> </EuiModalFooter> </EuiModal> </EuiOverlayMask> ); } +} + +// Table of potential observables taken from a given field, allowing to select +// which observables to send to The Hive. +// Props: +// fieldName - name of the field this table is for +// observables - list of observable IDs of this field +// obsData - array of objects specifying state of form fields in the table (.descr, .tlp, ...) +// obsSel - array of indices of selected observables +class ObservablesTable extends Component { + + constructor(props) { + super(props); + + // Table columns definition + this.columns = [ + { + field: "id", + name: "Observable", + }, + { + field: "descr", + name: "Description", + description: "Description of the observable in the context of the case", + render: (value, item1) => (<EuiFieldText + value={item1.descr} + onChange={(e) => this.props.onObsDataChange(props.fieldName, item1.i, "descr", e.target.value)} + disabled={!item1.selected} + />) + }, + /*{ + field: "tlp", + name: "TLP", + dataType: "number", + // TODO render and process changes + },*/ + { + field: "ioc", + name: "Is IOC", + dataType: "boolean", + description: "Indicates if the observable is an IOC", + render: (value, item2) => (<EuiCheckbox + id={"ioc-checkbox-"+item2.id} + checked={item2.ioc} + onChange={(e) => this.props.onObsDataChange(props.fieldName, item2.i, "ioc", e.target.checked)} + disabled={!item2.selected} + />) + }, + /*{ + field: "tags", + name: "Tags", + // TODO render and process changes + },*/ + ] + + // Create a reference to EuiBasicTable, so it's node can be accessed in componentDidMount + this.tableRef = React.createRef(); + } + + render() { + // Table data definition (convert props to format suitable for EuiBasicTable) + const n_obs = this.props.observables.length; + this.table_data = new Array(n_obs); + for (let i = 0; i < n_obs; i++) { + this.table_data[i] = { + id: this.props.observables[i], + descr: this.props.obsData[i].descr, + tlp: this.props.obsData[i].tlp, + ioc: this.props.obsData[i].ioc, + tags: this.props.obsData[i].tags, + // auxiliary fields, not shown in table: + i: i, // row index + selected: this.props.obsSel.includes(i), + }; + } + + return ( + <> + <EuiTitle size="xs"><h4>{this.props.fieldName}</h4></EuiTitle> + <EuiBasicTable + ref={this.tableRef} + columns={this.columns} + items={this.table_data} + itemId={(item3) => item3.id} + selection={ {onSelectionChange: (selectedItems) => this.props.onObsSelectionChange(this.props.fieldName, selectedItems) } } + noItemsMessage="No observables found" + rowProps={{ + // Hack to allow selection by clicking anywhere in the table row + // (except input elements) + onClick: (e) => { + if (e.target.tagName != "INPUT") { + // simulate click on the first checkbox in the row to (de)select the row + e.currentTarget.querySelector("input").click(); + e.currentTarget.blur(); // without this the focus remains on the row after click (results in different color) + } + }, + tabIndex: "-1", // prevents focus on row by keyboard navigation + }} + /> + <EuiSpacer size="l" /> + </> + ) + } componentDidMount() { // There's no way to specify initially selected items in EuiBasicTable by // props, but we may need to select some (in case a user selects some obs., // closes the modal and opens it again). // However, the selection is stored as a 'selection' field of table's state, - // so here we directly edit the state just after the table crated. + // so here we directly edit the state just after the table is created. // Prepare the 'selection' array - it should contain a list of selected row specifications let selection = []; - for (let i = 0; i < this.props.obsSelected.length; i++) { - selection.push(this.table_data[this.props.obsSelected[i]]); + for (let ix of this.props.obsSel) { + selection.push(this.table_data[ix]); } // Get ref to EuiBasicTable element and update its state - const table_node = this.obsTableRef.current; + const table_node = this.tableRef.current; table_node.setState({selection: selection}); } } - -// ********** Functions to send data to Kibana endpoints ********** - - -// Create a new Case in The Hive via its API -// Return a Promise which resolves to object with ID of the new case ('id' attr) or error message ('error' attr) -function createHiveCase(base_url, api_key, title, descr, severity, startDate, owner, flag, tlp, tags) { - // Prepare data - // TODO: check '/' at the end of base URL - var data = JSON.stringify({ - "base_url": base_url, - "api_key": api_key, - "body": { - "title": title, - "description": descr, - "severity": severity, // number: 1=low, 2=medium, 3=high - "startDate": startDate, - "owner": owner, // user name the case will be assigned to - "flag": flag, // bool - "tlp": tlp, // number: 0=white, 1=green, 2=amber, 3=red - "tags": tags, // array of strings - } - }); - console.log("TheHiveButton: Sending request to API endpoint 'new_case':", data); - var kibana_endpoint_url = chrome.addBasePath('/api/thehive_button/new_case'); - - return new Promise(function (resolve, reject) { - // Create AJAX request - var xhr = new XMLHttpRequest(); - - // Listener to process reply - xhr.onreadystatechange = function () { - if (this.readyState != 4) { - return; // response not ready yet - } - if (this.status == 200) { - const resp = JSON.parse(this.responseText); - console.log("TheHiveButton: Response from backend:", resp); - if ("error" in resp) { - resolve({"error": resp.error}); - } - else if (resp.status_code != 201) { - resolve({"error": "Unexpected reply received from The Hive: [" + resp.status_code + "] " + resp.status_msg}); - } - else { - resolve({"id": resp.body.id}); // return ID of the new case - } - } - else { - console.log("TheHiveButton: Error " + this.status + ": " + this.statusText); - resolve({"error": "Error " + this.status + ": " + this.statusText}); - } - } - - // Send the AJAX request - xhr.open("POST", kibana_endpoint_url); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.setRequestHeader("kbn-xsrf", "thehive_plugin"); // this header must be set, although its content is probably irrelevant - xhr.send(data); - }); -} - -// Add observables to an existing Case in The Hive -// (send the list of observables to our backend endpoint, it pushes them to The Hive) -function addCaseObservables(base_url, api_key, caseid, observables) { - const kibana_endpoint_url = chrome.addBasePath('/api/thehive_button/add_observables'); - const data = JSON.stringify({ - "base_url": base_url, - "api_key": api_key, - "caseid": caseid, - "observables": observables, - }); - console.log("TheHiveButton: Sending request to API endpoint 'add_observables':", data); - - return new Promise(function (resolve, reject) { - // Create AJAX request - var xhr = new XMLHttpRequest(); - - // Listener to process reply - xhr.onreadystatechange = function () { - if (this.readyState != 4) { - return; // response not ready yet - } - if (this.status == 200) { - const resp = JSON.parse(this.responseText); - console.log("TheHiveButton: Response from backend:", resp); - resolve(resp); - } - else { - console.log("TheHiveButton: Error " + this.status + ": " + this.statusText); - resolve({"error": "Error " + this.status + ": " + this.statusText}); - } - } - - // Send the AJAX request - xhr.open("POST", kibana_endpoint_url); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.setRequestHeader("kbn-xsrf", "thehive_plugin"); // this header must be set, although its content is probably irrelevant - xhr.send(data); - }); -} - diff --git a/thehive_button/server/routes/newcase.js b/thehive_button/server/routes/newcase.js index 6f796ee..175dee8 100644 --- a/thehive_button/server/routes/newcase.js +++ b/thehive_button/server/routes/newcase.js @@ -34,7 +34,6 @@ function newCaseHandler(req, resp) { //if (!base_url.match(/https?:\/\/.*\//)) { return {'error': 'Invalid base URL (it must begin with "http[s]" and end with "/")'}; } - // TODO add "/" to the end automatically if (!api_key) { return {'error': 'API key not set'}; } @@ -123,7 +122,7 @@ function addObservablesHandler(req, resp) { function addObservable(base_url, api_key, caseid, obs) { return new Promise( function(resolve, reject) { - console.log("Adding observable:", obs); + //console.log("Adding observable:", obs); request({ method: 'POST', url: base_url + 'api/case/' + caseid + "/artifact", -- GitLab