Skip to content
Snippets Groups Projects
Commit da98c4d2 authored by Václav Bartoš's avatar Václav Bartoš
Browse files

Possibility to add Observables, rewritten to use React

parent 4298d16f
No related branches found
No related tags found
No related merge requests found
{ {
"name": "thehive_button", "name": "thehive_button",
"version": "0.2.0", "version": "0.3.0",
"description": "Visualisation plugin which creates a simple button to create a new case in The Hive.", "description": "Visualisation plugin which creates a simple button to create a new case in The Hive.",
"main": "index.js", "main": "index.js",
"kibana": { "kibana": {
"version": "7.2.2" "version": "7.2.2",
}, },
"scripts": { "scripts": {
"lint": "eslint .", "lint": "eslint .",
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
}, },
"dependencies": { "dependencies": {
"request": "^2.88.0", "request": "^2.88.0",
"jquery-ui": "1"
}, },
"devDependencies": { "devDependencies": {
"@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana", "@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana",
......
import './vis.less'; //import './vis.less';
import optionsTemplate from './options_template.html'; //import optionsTemplate from './options_template.html';
import { THEHIVE_API_KEY, THEHIVE_URL, THEHIVE_OWNER } from './env'; import { THEHIVE_API_KEY, THEHIVE_URL, THEHIVE_OWNER } from './env';
import { VisController } from './vis_controller'; //import { VisController } from './vis_controller';
import { TheHiveButtonVisComponent } from './vis_controller';
import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { Status } from 'ui/vis/update_status'; import { Status } from 'ui/vis/update_status';
function TheHiveButtonVisProvider(Private) { function TheHiveButtonVisProvider(Private) {
...@@ -12,62 +15,83 @@ function TheHiveButtonVisProvider(Private) { ...@@ -12,62 +15,83 @@ function TheHiveButtonVisProvider(Private) {
console.log("default URL:", THEHIVE_URL); console.log("default URL:", THEHIVE_URL);
console.log("default API key:", THEHIVE_API_KEY); console.log("default API key:", THEHIVE_API_KEY);
return VisFactory.createBaseVisualization({ return VisFactory.createReactVisualization({
name: 'thehive_button', name: 'thehive_button',
title: 'The Hive Case', title: 'The Hive Case',
icon: 'alert', icon: 'alert',
description: 'A button to create a new Case in The Hive.', description: 'A button to create a new Case in The Hive.',
visualization: VisController, //requiresUpdateStatus: [Status.PARAMS, Status.RESIZE, Status.UI_STATE],
requiresUpdateStatus: [Status.PARAMS, Status.RESIZE, Status.UI_STATE],
visConfig: { visConfig: {
component: TheHiveButtonVisComponent,
defaults: { defaults: {
// add default parameters // add default parameters
url: THEHIVE_URL, url: THEHIVE_URL,
apikey: THEHIVE_API_KEY, apikey: THEHIVE_API_KEY,
owner: THEHIVE_OWNER, owner: THEHIVE_OWNER,
}, }
}, },
editor: 'default', editor: 'default',
// editor: MyEditorController, // editor: MyEditorController,
editorConfig: { editorConfig: {
// optionsTemplate: '<div>TEST</div>', /*collections: {
optionsTemplate: optionsTemplate colorSchemas: [
{id: "test1", label: "Test 1"},
{id: "test2", label: "Test 2"},
],
},*/
optionsTemplate: '<div>TEST</div>',
// optionsTemplate: '<thehivebutton-vis-params></thehivebutton-vis-params>',
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)",
min: 1,
max: 1,
aggFilter: 'count',
defaults: [
{
type: 'count',
schema: 'metric',
},
],
},
{
group: 'buckets',
name: 'group',
title: 'Observables',
min: 1,
max: 1,
aggFilter: 'terms',//['!geohash_grid', '!geotile_grid', '!filter'],
// defaults: [
// {
// type: 'terms',
// //schema: 'group',
// field: 'ip',
// size: 100,
// }
// ]
},
]),
}, },
requestHandler: 'none', //requestHandler: 'courier', // default
responseHandler: 'none', //responseHandler: 'default', // return data as a table, see https://www.elastic.co/guide/en/kibana/7.2/development-visualization-response-handlers.html
}); });
} }
// UNUSED
/*class MyEditorController {
constructor(el, vis) {
this.el = el;
this.vis = vis;
//this.config = vis.type.editorConfig;
this.container = document.createElement('div');
this.container.className = 'myvis-config-container-div';
this.el.appendChild(this.container);
}
async render(visData) {
console.log("MyEditorController render(), config:", this.vis);
// console.log(this.vis.isEditorMode);
// console.log(this.vis.getUiState);
// console.log(this.vis.getState);
// console.log(this.vis.params);
this.container.innerHTML = optionsTemplate;
// this.vis.updateState();
this.vis.dirty = true;
return 'done rendering';
}
// destroy() {
// console.log('destroying');
// }
}*/
// register the provider with the visTypes registry // register the provider with the visTypes registry
VisTypesRegistryProvider.register(TheHiveButtonVisProvider); VisTypesRegistryProvider.register(TheHiveButtonVisProvider);
import { Status } from 'ui/vis/update_status'; //import { Status } from 'ui/vis/update_status';
import chrome from 'ui/chrome'; import chrome from 'ui/chrome';
import { toastNotifications } from 'ui/notify'; import { toastNotifications } from 'ui/notify';
import case_form from './case_form.html'; //import vis_template from './vis_template.html';
import React from 'react';
import $ from 'jquery';
import 'jquery-ui/themes/base/core.css';
import 'jquery-ui/themes/base/theme.css';
import 'jquery-ui/themes/base/resizable.css';
import 'jquery-ui/themes/base/dialog.css';
import 'jquery-ui/ui/core';
import 'jquery-ui/ui/widgets/resizable';
import 'jquery-ui/ui/widgets/dialog';
import React, { Component } from 'react';
import { import {
EuiButton, EuiButton,
// EuiPanel, EuiButtonEmpty,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiTextArea,
EuiSuperSelect,
EuiBasicTable,
EuiCheckbox,
makeId,
} from '@elastic/eui'; } from '@elastic/eui';
function createTheHiveButton(vis) { // ********** React components **********
// Create the button
var button = document.createElement('button'); // Main React component - the root of visualization
button.className = 'euiButton euiButton--danger euiButton--fill'; export class TheHiveButtonVisComponent extends Component {
button.innerHTML = '<span class="euiButton__content"><span class="euiButton__text" title="Create new Case in The Hive">Create new Case</span></span>'; render() {
button.addEventListener('click', hiveButtonOnClick); //console.log(this.props);
button._vis = vis; // store reference to 'vis' to the element let ips = [];
// var button2 = <EuiButton fill iconType="alert" color="danger" onClick={(evt) => hiveButtonOnClick(evt, vis.params)}>Create new Case</EuiButton>; if (this.props.visData) {
// button2.setState({vis: vis}); const first_col_id = this.props.visData.columns[0].id;
ips = this.props.visData.rows.map(row => row[first_col_id]);
// Create the dialog to specify Case parameters //console.log(ips);
var dialog = document.createElement("div"); }
dialog.innerHTML = case_form; // content imported from ext. file return (
$(dialog).dialog({ <div>
autoOpen: false, <NewCaseButton params={this.props.vis.params} observables={ips} />
title: "Create a new Case ...", </div>
width: 600, );
resizable: true, }
buttons: [
componentDidMount() {
this.props.renderComplete();
}
componentDidUpdate() {
this.props.renderComplete();
}
}
// 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
class NewCaseButton extends Component {
constructor(props) {
super(props);
this.state = {
isModalVisible: false,
};
// Each handler function in a class (method) must be "binded" this way
this.closeModal = this.closeModal.bind(this);
this.showModal = this.showModal.bind(this);
}
closeModal() {
this.setState({ isModalVisible: false });
}
showModal() {
this.setState({ isModalVisible: true });
}
render() {
let modal;
if (this.state.isModalVisible) {
modal = <ModalContent close={this.closeModal} params={this.props.params} observables={this.props.observables} />;
}
return (
<div>
<EuiButton fill iconType="alert" color="danger" onClick={this.showModal}>Create new Case ...</EuiButton>
{modal}
</div>
);
}
}
// The popup window with a form
// Props:
// .close - function to close the Modal
// .params - visualization parameters (from vis.params)
// .observables - list of IP addrsses or other observables to add to the Case
class ModalContent extends Component {
constructor(props) {
super(props);
const observables = props.observables;
const n_obs = observables.length;
// Initialize state (all changeable case parameters)
// TODO: move state up to the button, so it persists when modal is closed (and add explicit reset button + reset on submit)
this.state = {
title: "",
description: "\n\n--\nCreated from Kibana",
severity: "2", // medium
tlp: "2", // amber
tags: [],
// 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),
obsIOCs: new Array(n_obs).fill(false),
obsTags: new Array(n_obs).fill([]),
obsSelected: new Array(), // list of indices of selected observables
};
// "Select" options
this.severityOptions = [
{value: "1", inputDisplay: "low"},
{value: "2", inputDisplay: "medium"},
{value: "3", inputDisplay: "high"},
];
this.tlpOptions = [
{value: "0", inputDisplay: "white"},
{value: "1", inputDisplay: "green"},
{value: "2", inputDisplay: "amber"},
{value: "3", inputDisplay: "red"},
];
// Table column definition
this.columns = [
{ {
text: "Cancel", field: "id",
click: function() { name: "Observable",
$( this ).dialog( "close" );
}
}, },
{ {
text: "Submit", field: "descr",
click: function(evt) { name: "Description",
hiveButtonSubmit(dialog, evt.currentTarget); description: "Description of the observable in the context of the case",
} render: (value, item) => (<EuiFieldText
} value={item.descr}
onChange={(e) => this.onChangeDescr(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.onChangeIOC(e, item.i)}
disabled={!item.selected}
/>)
},
/*{
field: "tags",
name: "Tags",
// TODO render and process changes
},*/
] ]
});
// add reference to visualisation parameters to the dialog element // Each handler function in a class (method) must be "bind" this way
dialog._params = button._vis.params; this.submitCase = this.submitCase.bind(this);
this.onChangeDescr = this.onChangeDescr.bind(this);
// add reference to newly created dialog to the button element this.onChangeTLP = this.onChangeTLP.bind(this);
button._dialog_elem = dialog; this.onChangeIOC = this.onChangeIOC.bind(this);
return button; this.onChangeTags = this.onChangeTags.bind(this);
} this.onSelectionChange = this.onSelectionChange.bind(this);
}
function hiveButtonOnClick(evt) {
// Button click -> just open the dialog
var button = evt.currentTarget;
$(button._dialog_elem).dialog("open");
return false;
}
function hiveButtonSubmit(dialog, submit_button) { // Event handlers - change observable parameters
var params = dialog._params; onChangeDescr(evt, rowindex) {
console.log("Submit", submit_button, params); 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};
});
}
onChangeTLP(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};
});
}
onChangeIOC(evt, rowindex) {
const val = evt.target.checked;
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};
});
}
onChangeTags(evt, rowindex) { // TODO
const val = evt.target.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};
});
}
// load data from the <form> // Event handler - a row is (de)selected
var form = $("form", dialog); onSelectionChange(selectedItems) {
console.log(form); // Extract indices from the items and store them into state
var title = $('input[name="case-title"]', form).val(); const selectedIndices = selectedItems.map(item => item.i);
var descr = $('textarea[name="case-descr"]', form).val(); this.setState({obsSelected: selectedIndices});
var severity = 2;
var start_date = null;
var owner = params.owner;
var flag = false;
var tlp = 2;
var tags = [];
// check validity
if (title == "") {
toastNotifications.addDanger("ERROR: Title cannot be empty");
return false;
} }
// Main render function
// TODO should't it be easier to simply store whole table_data in State?
// everything has to be re-rendered on any state change anyway
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.state.obsDescrs[i],
tlp: this.state.obsTLPs[i],
ioc: this.state.obsIOCs[i],
tags: this.state.obsTags[i],
// auxiliary fields, not shown in table:
i: i, // row index
selected: this.state.obsSelected.includes(i),
};
}
//console.log("render(): Table data:", table_data);
return (
<EuiOverlayMask>
<EuiModal onClose={this.props.close} maxWidth={false} initialFocus="[name=title]">
<EuiModalHeader>
<EuiModalHeaderTitle>Create a new case in The Hive</EuiModalHeaderTitle>
</EuiModalHeader>
// disable the button to prevent multiple submits <EuiModalBody>
submit_button.innerHTML = "(working...)"; <EuiForm style={{width: "800px"}}>
submit_button.setAttribute("disabled", true); <EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiFormRow label="Title" fullWidth>
<EuiFieldText name="title" value={this.state.title} onChange={(e) => this.setState({title: e.target.value})} required={true} fullWidth />
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow label="Severity">
<EuiSuperSelect
options={this.severityOptions}
valueOfSelected={this.state.severity}
onChange={(val) => this.setState({severity: val})}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow label="TLP">
<EuiSuperSelect
prepend="TLP"
options={this.tlpOptions}
valueOfSelected={this.state.tlp}
onChange={(val) => this.setState({tlp: val})}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFormRow label="Description" fullWidth>
<EuiTextArea
defaultValue={this.state.description}
onChange={(e) => this.setState({description: e.target.value})}
rows={4}
fullWidth
/>
</EuiFormRow>
<EuiTitle size="s"><h3>Add observables from current query ...</h3></EuiTitle>
<EuiBasicTable
columns={this.columns}
items={table_data}
itemId={(item) => item.id}
selection={ {onSelectionChange: this.onSelectionChange} }
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
}}
/>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={this.props.close}>Cancel</EuiButtonEmpty>
<EuiButton onClick={this.submitCase} fill>Create Case</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);
}
createHiveCase(params.url, params.apikey, title, descr, severity, start_date, owner, flag, tlp, tags) submitCase(evt) {
.then(function(value) { const params = this.props.params;
if ('error' in value) {
// Error contacting The Hive // Get case parameters
console.log("createHiveCase() ERROR:", value.error); const title = this.state.title;
toastNotifications.addDanger("ERROR: " + value.error); const descr = this.state.description;
} const severity = parseInt(this.state.severity);
else { const start_date = null;
// Success - show notification and open the Case in new tab const owner = params.owner;
console.log("createHiveCase() completed:", value); const flag = false;
const case_url = params.url + "index.html#/case/" + value.id + "/details"; const tlp = parseInt(this.state.tlp);
toastNotifications.add({ const tags = this.state.tags;
title: "Case created",
color: "success", if (!title) {
iconType: "checkInCircleFilled", toastNotifications.addDanger("Title can't be empty");
text: ( return;
<div> }
<p><b><a href={case_url} target="_blank">Edit the new Case</a></b></p>
</div> // Get list of selected observables and their params
), let observables = [];
}); let selectionIndices = [...this.state.obsSelected]; // make a copy
window.open(case_url, '_blank'); 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
// re-enable the button // fill in observable definition according to model at
submit_button.innerHTML = "Submit"; // https://github.com/TheHive-Project/TheHiveDocs/blob/master/api/artifact.md
submit_button.removeAttribute("disabled"); const obs = {
dataType: 'ip',
// close the dialog data: this.props.observables[j],
$(dialog).dialog( "close" ); message: this.state.obsDescrs[j],
}); tlp: this.state.obsTLPs[j],
return false; ioc: this.state.obsIOCs[j],
tags: this.state.obsTags[j],
};
observables.push(obs);
}
console.log("Selected observables:", observables);
// Submit request to create the case, handle response
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;
}
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
// Close the popup window
this.props.close();
}
} }
// ********** Functions to send data to Kibana endpoints **********
// Create a new Case in The Hive via its API // 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) // 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) { function createHiveCase(base_url, api_key, title, descr, severity, startDate, owner, flag, tlp, tags) {
...@@ -149,7 +448,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow ...@@ -149,7 +448,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow
"tags": tags, // array of strings "tags": tags, // array of strings
} }
}); });
console.log("Sending request to Kibana API endpoint of thehive_button plugin:", data); console.log("TheHiveButton: Sending request to API endpoint 'new_case':", data);
var kibana_endpoint_url = chrome.addBasePath('/api/thehive_button/new_case'); var kibana_endpoint_url = chrome.addBasePath('/api/thehive_button/new_case');
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
...@@ -163,7 +462,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow ...@@ -163,7 +462,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow
} }
if (this.status == 200) { if (this.status == 200) {
const resp = JSON.parse(this.responseText); const resp = JSON.parse(this.responseText);
console.log("Response from The Hive:", resp); console.log("TheHiveButton: Response from backend:", resp);
if ("error" in resp) { if ("error" in resp) {
resolve({"error": resp.error}); resolve({"error": resp.error});
} }
...@@ -175,7 +474,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow ...@@ -175,7 +474,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow
} }
} }
else { else {
console.log("Error " + this.status + ": " + this.statusText); console.log("TheHiveButton: Error " + this.status + ": " + this.statusText);
resolve({"error": "Error " + this.status + ": " + this.statusText}); resolve({"error": "Error " + this.status + ": " + this.statusText});
} }
} }
...@@ -188,25 +487,43 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow ...@@ -188,25 +487,43 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow
}); });
} }
class VisController { // Add observables to an existing Case in The Hive
constructor(el, vis) { // (send the list of observables to our backend endpoint, it pushes them to The Hive)
this.vis = vis; function addCaseObservables(base_url, api_key, caseid, observables) {
this.el = el; const kibana_endpoint_url = chrome.addBasePath('/api/thehive_button/add_observables');
//console.log('constructor called!'); const data = JSON.stringify({
this.container = document.createElement('div'); "base_url": base_url,
this.container.className = 'myvis-container-div'; "api_key": api_key,
this.button = createTheHiveButton(vis); "caseid": caseid,
this.container.appendChild(this.button); "observables": observables,
this.el.appendChild(this.container); });
} console.log("TheHiveButton: Sending request to API endpoint 'add_observables':", data);
destroy() { return new Promise(function (resolve, reject) {
this.el.innerHTML = ''; // 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});
}
}
async render(visData, status) { // Send the AJAX request
return 'done rendering'; 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
export { VisController }; xhr.send(data);
});
}
const request = require('request'); const request = require('request');
//const fs = require('fs');
//const path = require('path');
//const caFile = path.resolve(__dirname, '../../ca.cert.pem'); // TODO resolve where the CA file should be located / configured
export default function (server) { export default function (server) {
server.route({ server.route({
...@@ -6,6 +10,11 @@ export default function (server) { ...@@ -6,6 +10,11 @@ export default function (server) {
method: 'POST', method: 'POST',
handler: newCaseHandler, handler: newCaseHandler,
}); });
server.route({
path: '/api/thehive_button/add_observables',
method: 'POST',
handler: addObservablesHandler,
});
} }
// Handler of ajax requests to create a new Case in The Hive // Handler of ajax requests to create a new Case in The Hive
...@@ -25,6 +34,7 @@ function newCaseHandler(req, resp) { ...@@ -25,6 +34,7 @@ function newCaseHandler(req, resp) {
//if (!base_url.match(/https?:\/\/.*\//)) { //if (!base_url.match(/https?:\/\/.*\//)) {
return {'error': 'Invalid base URL (it must begin with "http[s]" and end with "/")'}; return {'error': 'Invalid base URL (it must begin with "http[s]" and end with "/")'};
} }
// TODO add "/" to the end automatically
if (!api_key) { if (!api_key) {
return {'error': 'API key not set'}; return {'error': 'API key not set'};
} }
...@@ -36,18 +46,19 @@ function newCaseHandler(req, resp) { ...@@ -36,18 +46,19 @@ function newCaseHandler(req, resp) {
auth: {'bearer': api_key}, auth: {'bearer': api_key},
json: true, json: true,
body: req_body, body: req_body,
//rejectUnauthorized: false, // Disables server certificate check //ca: fs.readFileSync(caFile), // TODO resolve the issue with custom CA, where to get its cert?
rejectUnauthorized: false,
}, },
// handler of the reply from The Hive - just return as reply // handler of the reply from The Hive - just return as reply
function (error, response, body) { function (error, response, body) {
// TODO: find out how to set response code, for now we always return sucess and encode original status code in the content // TODO: find out how to set response code, for now we always return sucess and encode original status code in the content
if (error) { if (error) {
console.log("ERROR when trying to send request to The Hive:", error); console.error("ERROR when trying to send request to The Hive:", error);
resolve({'error': error.message}); resolve({'error': error.message});
} }
else { else {
if (response.statusCode < 200 || response.statusCode >= 300) { if (response.statusCode < 200 || response.statusCode >= 300) {
console.log("ERROR Unexpected reply received from The Hive:", response.statusCode, response.statusMessage, "\n", body) console.error("ERROR Unexpected reply received from The Hive:", response.statusCode, response.statusMessage, "\n", body)
} }
resolve({ resolve({
'status_code': response.statusCode, 'status_code': response.statusCode,
...@@ -60,3 +71,84 @@ function newCaseHandler(req, resp) { ...@@ -60,3 +71,84 @@ function newCaseHandler(req, resp) {
}); // Promise() }); // Promise()
} }
// Note:
// There are two ways to create multiple Observables (artifacts) via The Hive API:
// 1. post one request with an array of observables in "data" field
// - this allows to create all in one request, but doesn't allow to set
// different parameters (IOC, TLP, etc.) to different observables
// 2. post each observable in a separate request
// The second way is used here.
// Handler of ajax requests to add Observables to a Case in The Hive
function addObservablesHandler(req, resp) {
// Parse the request to get connection parameters
// (everything is configured in forntend and sent as part of the request,
// since I don't know how to configure the backend)
var base_url = req.payload['base_url'];
var api_key = req.payload['api_key'];
// check it's a valid URL with slash at the end
if (!base_url) {
return {'error': 'Base URL not set'};
}
if (!base_url.match(/https?:\/\/(([a-z\d.-]+)|((\d{1,3}\.){3}\d{1,3}))(\:\d+)?(\/[-a-z\d%_.~+]*)*\//i)) {
//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'};
}
const caseid = req.payload['caseid'];
const observables = req.payload['observables']; // array of obersvable specifications
return new Promise( async function(resolve, reject) {
// Run one request for each observable
// (A way to run multiple async tasks sequentially inspired by:
// https://jrsinclair.com/articles/2019/how-to-run-async-js-in-parallel-or-sequential/ )
const starterPromise = Promise.resolve(null);
await observables.reduce(
(p, obs) => p.then(() => addObservable(base_url, api_key, caseid, obs)),
starterPromise
).catch((err_msg) => {
console.error(err_msg); // log whole message
resolve({'error': err_msg.split("\n", 1)[0]}); // send the first line to frontend
return;
}
);
resolve({});
});
}
function addObservable(base_url, api_key, caseid, obs) {
return new Promise( function(resolve, reject) {
console.log("Adding observable:", obs);
request({
method: 'POST',
url: base_url + 'api/case/' + caseid + "/artifact",
auth: {'bearer': api_key},
json: true,
body: obs,
//ca: fs.readFileSync(caFile), // TODO resolve the issue with custom CA, where to get its cert?
rejectUnauthorized: false,
},
// handler of the reply from The Hive - just return as reply
function (error, response, body) {
if (error) {
reject("ERROR when trying to send request to The Hive: " + error);
}
else if (response.statusCode < 200 || response.statusCode >= 300) {
reject("ERROR: Unexpected reply received from The Hive: " + response.statusCode + " " + response.statusMessage + "\n" + JSON.stringify(body));
}
else {
// success - continue with the next observable
resolve("OK");
resolve({})
}
} // handler function
); // request()
}); //Promise()
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment