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",
"version": "0.2.0",
"version": "0.3.0",
"description": "Visualisation plugin which creates a simple button to create a new case in The Hive.",
"main": "index.js",
"kibana": {
"version": "7.2.2"
"version": "7.2.2",
},
"scripts": {
"lint": "eslint .",
......@@ -13,7 +13,6 @@
},
"dependencies": {
"request": "^2.88.0",
"jquery-ui": "1"
},
"devDependencies": {
"@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana",
......
import './vis.less';
import optionsTemplate from './options_template.html';
//import './vis.less';
//import optionsTemplate from './options_template.html';
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 { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { Status } from 'ui/vis/update_status';
function TheHiveButtonVisProvider(Private) {
......@@ -12,62 +15,83 @@ function TheHiveButtonVisProvider(Private) {
console.log("default URL:", THEHIVE_URL);
console.log("default API key:", THEHIVE_API_KEY);
return VisFactory.createBaseVisualization({
return VisFactory.createReactVisualization({
name: 'thehive_button',
title: 'The Hive Case',
icon: 'alert',
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: {
component: TheHiveButtonVisComponent,
defaults: {
// add default parameters
url: THEHIVE_URL,
apikey: THEHIVE_API_KEY,
owner: THEHIVE_OWNER,
},
}
},
editor: 'default',
// editor: MyEditorController,
editorConfig: {
// optionsTemplate: '<div>TEST</div>',
optionsTemplate: optionsTemplate
/*collections: {
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',
responseHandler: 'none',
//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
});
}
// 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
VisTypesRegistryProvider.register(TheHiveButtonVisProvider);
import { Status } from 'ui/vis/update_status';
//import { Status } from 'ui/vis/update_status';
import chrome from 'ui/chrome';
import { toastNotifications } from 'ui/notify';
import case_form from './case_form.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 vis_template from './vis_template.html';
import React, { Component } from 'react';
import {
EuiButton,
// EuiPanel,
EuiButtonEmpty,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiTextArea,
EuiSuperSelect,
EuiBasicTable,
EuiCheckbox,
makeId,
} from '@elastic/eui';
function createTheHiveButton(vis) {
// Create the button
var button = document.createElement('button');
button.className = 'euiButton euiButton--danger euiButton--fill';
button.innerHTML = '<span class="euiButton__content"><span class="euiButton__text" title="Create new Case in The Hive">Create new Case</span></span>';
button.addEventListener('click', hiveButtonOnClick);
button._vis = vis; // store reference to 'vis' to the element
// var button2 = <EuiButton fill iconType="alert" color="danger" onClick={(evt) => hiveButtonOnClick(evt, vis.params)}>Create new Case</EuiButton>;
// button2.setState({vis: vis});
// Create the dialog to specify Case parameters
var dialog = document.createElement("div");
dialog.innerHTML = case_form; // content imported from ext. file
$(dialog).dialog({
autoOpen: false,
title: "Create a new Case ...",
width: 600,
resizable: true,
buttons: [
// ********** 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);
}
return (
<div>
<NewCaseButton params={this.props.vis.params} observables={ips} />
</div>
);
}
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",
click: function() {
$( this ).dialog( "close" );
}
field: "id",
name: "Observable",
},
{
text: "Submit",
click: function(evt) {
hiveButtonSubmit(dialog, evt.currentTarget);
}
}
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.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
dialog._params = button._vis.params;
// add reference to newly created dialog to the button element
button._dialog_elem = dialog;
return button;
}
function hiveButtonOnClick(evt) {
// Button click -> just open the dialog
var button = evt.currentTarget;
$(button._dialog_elem).dialog("open");
return false;
}
// Each handler function in a class (method) must be "bind" this way
this.submitCase = this.submitCase.bind(this);
this.onChangeDescr = this.onChangeDescr.bind(this);
this.onChangeTLP = this.onChangeTLP.bind(this);
this.onChangeIOC = this.onChangeIOC.bind(this);
this.onChangeTags = this.onChangeTags.bind(this);
this.onSelectionChange = this.onSelectionChange.bind(this);
}
function hiveButtonSubmit(dialog, submit_button) {
var params = dialog._params;
console.log("Submit", submit_button, params);
// Event handlers - change observable parameters
onChangeDescr(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};
});
}
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>
var form = $("form", dialog);
console.log(form);
var title = $('input[name="case-title"]', form).val();
var descr = $('textarea[name="case-descr"]', form).val();
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;
// Event handler - a row is (de)selected
onSelectionChange(selectedItems) {
// Extract indices from the items and store them into state
const selectedIndices = selectedItems.map(item => item.i);
this.setState({obsSelected: selectedIndices});
}
// 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
submit_button.innerHTML = "(working...)";
submit_button.setAttribute("disabled", true);
<EuiModalBody>
<EuiForm style={{width: "800px"}}>
<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)
.then(function(value) {
if ('error' in value) {
// Error contacting The Hive
console.log("createHiveCase() ERROR:", value.error);
toastNotifications.addDanger("ERROR: " + value.error);
}
else {
// Success - show notification and open the Case in new tab
console.log("createHiveCase() completed:", value);
const case_url = params.url + "index.html#/case/" + value.id + "/details";
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>
</div>
),
});
window.open(case_url, '_blank');
}
// re-enable the button
submit_button.innerHTML = "Submit";
submit_button.removeAttribute("disabled");
// close the dialog
$(dialog).dialog( "close" );
});
return false;
submitCase(evt) {
const params = this.props.params;
// Get case parameters
const title = this.state.title;
const descr = this.state.description;
const severity = parseInt(this.state.severity);
const start_date = null;
const owner = params.owner;
const flag = false;
const tlp = parseInt(this.state.tlp);
const tags = this.state.tags;
if (!title) {
toastNotifications.addDanger("Title can't be empty");
return;
}
// 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);
}
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
// 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) {
......@@ -149,7 +448,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow
"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');
return new Promise(function (resolve, reject) {
......@@ -163,7 +462,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow
}
if (this.status == 200) {
const resp = JSON.parse(this.responseText);
console.log("Response from The Hive:", resp);
console.log("TheHiveButton: Response from backend:", resp);
if ("error" in resp) {
resolve({"error": resp.error});
}
......@@ -175,7 +474,7 @@ function createHiveCase(base_url, api_key, title, descr, severity, startDate, ow
}
}
else {
console.log("Error " + this.status + ": " + this.statusText);
console.log("TheHiveButton: 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
});
}
class VisController {
constructor(el, vis) {
this.vis = vis;
this.el = el;
//console.log('constructor called!');
this.container = document.createElement('div');
this.container.className = 'myvis-container-div';
this.button = createTheHiveButton(vis);
this.container.appendChild(this.button);
this.el.appendChild(this.container);
}
destroy() {
this.el.innerHTML = '';
}
// 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});
}
}
async render(visData, status) {
return 'done rendering';
}
};
export { VisController };
// 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);
});
}
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) {
server.route({
......@@ -6,6 +10,11 @@ export default function (server) {
method: 'POST',
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
......@@ -25,6 +34,7 @@ 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'};
}
......@@ -36,18 +46,19 @@ function newCaseHandler(req, resp) {
auth: {'bearer': api_key},
json: true,
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
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
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});
}
else {
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({
'status_code': response.statusCode,
......@@ -60,3 +71,84 @@ function newCaseHandler(req, resp) {
}); // 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