diff --git a/thehive_button/.eslintrc b/thehive_button/.eslintrc new file mode 100644 index 0000000000000000000000000000000000000000..64eba86220ec489c9c364e9a443941d14a8d3b16 --- /dev/null +++ b/thehive_button/.eslintrc @@ -0,0 +1,7 @@ +--- +extends: "@elastic/kibana" + +settings: + import/resolver: + '@elastic/eslint-import-resolver-kibana': + rootPackageName: 'thehive_button' diff --git a/thehive_button/.gitignore b/thehive_button/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..521e1c7295e90bed131707d9486f5e77fb2569c9 --- /dev/null +++ b/thehive_button/.gitignore @@ -0,0 +1,4 @@ +npm-debug.log* +node_modules +/build/ +/public/app.css diff --git a/thehive_button/.kibana-plugin-helpers.json b/thehive_button/.kibana-plugin-helpers.json new file mode 100644 index 0000000000000000000000000000000000000000..2c63c0851048d8f7bff41ecf0f8cee05f52fd120 --- /dev/null +++ b/thehive_button/.kibana-plugin-helpers.json @@ -0,0 +1,2 @@ +{ +} diff --git a/thehive_button/README.md b/thehive_button/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b728b8e83dcd2159ba75d83dc435316d68619726 --- /dev/null +++ b/thehive_button/README.md @@ -0,0 +1,4 @@ +# The Hive Button + +> Visualisation plugin which creates a simple button to create a new case in The Hive. + diff --git a/thehive_button/index.js b/thehive_button/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fa69c75c30d7ee40f8d7089d6debd6cf69c8d402 --- /dev/null +++ b/thehive_button/index.js @@ -0,0 +1,19 @@ +import newCaseRoute from './server/routes/newcase'; + +export default function (kibana) { + return new kibana.Plugin({ + require: [], //['elasticsearch'], + name: 'thehive_button', + uiExports: { + visTypes: [ + 'plugins/thehive_button/main', + ], + }, + + init(server, options) { // eslint-disable-line no-unused-vars + // Add server routes and initialize the plugin here + newCaseRoute(server); + } + }); +} + diff --git a/thehive_button/package.json b/thehive_button/package.json new file mode 100644 index 0000000000000000000000000000000000000000..9c3124f4d84aa6913e0103900a134702a1fbde84 --- /dev/null +++ b/thehive_button/package.json @@ -0,0 +1,33 @@ +{ + "name": "thehive_button", + "version": "0.1.1", + "description": "Visualisation plugin which creates a simple button to create a new case in The Hive.", + "main": "index.js", + "kibana": { + "version": "7.2.2" + }, + "scripts": { + "lint": "eslint .", + "start": "plugin-helpers start", + "build": "plugin-helpers build" + }, + "dependencies": { + "request": "^2.88.0" + }, + "devDependencies": { + "@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana", + "@elastic/eslint-import-resolver-kibana": "link:../../packages/kbn-eslint-import-resolver-kibana", + "@kbn/plugin-helpers": "link:../../packages/kbn-plugin-helpers", + "babel-eslint": "^9.0.0", + "eslint": "^5.6.0", + "eslint-plugin-babel": "^5.2.0", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-jest": "^21.26.2", + "eslint-plugin-jsx-a11y": "^6.1.2", + "eslint-plugin-mocha": "^5.2.0", + "eslint-plugin-no-unsanitized": "^3.0.2", + "eslint-plugin-prefer-object-spread": "^1.2.1", + "eslint-plugin-react": "^7.11.1", + "expect.js": "^0.3.1" + } +} diff --git a/thehive_button/public/env.js b/thehive_button/public/env.js new file mode 100644 index 0000000000000000000000000000000000000000..4321b85f5ee1682abd17871889a165ae8d96b465 --- /dev/null +++ b/thehive_button/public/env.js @@ -0,0 +1,4 @@ +// Default plugin configuration +export const THEHIVE_URL = 'https://hive.gn4-3-wp8-soc.sunet.se/'; +export const THEHIVE_API_KEY = '5LymseWiurZBrQN8Kqp8O+9KniTL5cE0'; +export const THEHIVE_OWNER = 'admin'; // default owner account of the created cases diff --git a/thehive_button/public/main.js b/thehive_button/public/main.js new file mode 100644 index 0000000000000000000000000000000000000000..96c3bc7b11cc4c1c8c901c23a9fa0399bafa97ef --- /dev/null +++ b/thehive_button/public/main.js @@ -0,0 +1,73 @@ +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 { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; +import { Status } from 'ui/vis/update_status'; + +function TheHiveButtonVisProvider(Private) { + const VisFactory = Private(VisFactoryProvider); + + console.log("default URL:", THEHIVE_URL); + console.log("default API key:", THEHIVE_API_KEY); + + return VisFactory.createBaseVisualization({ + 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], + visConfig: { + 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 + }, + requestHandler: 'none', + responseHandler: 'none', + }); +} + + +// 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); + diff --git a/thehive_button/public/options_template.html b/thehive_button/public/options_template.html new file mode 100644 index 0000000000000000000000000000000000000000..5ec603b3f1e531a18e2bcef0e60e0e54e177ea06 --- /dev/null +++ b/thehive_button/public/options_template.html @@ -0,0 +1,7 @@ +<div class="form-group"> + <p style="margin-bottom: 1em">The plugin is currently not configurable from here. Below are the predefined configuration values. They can be edited in "<tt><KIBANA_PATH>/plugins/thehive_button/public/env.js</tt>" on the backend.</p> + <label>The Hive API URL</label> + <input ng-model=vis.params.url class=form-control disabled=disabled /> + <label>The Hive API key</label> + <input ng-model=vis.params.apikey class=form-control disabled=disabled /> +</div> diff --git a/thehive_button/public/vis.less b/thehive_button/public/vis.less new file mode 100644 index 0000000000000000000000000000000000000000..b6f887afaef57a7674a0d0f06ee6f821a0fc015e --- /dev/null +++ b/thehive_button/public/vis.less @@ -0,0 +1,3 @@ +.myvis-container-div { + padding: 1em; +} diff --git a/thehive_button/public/vis_controller.js b/thehive_button/public/vis_controller.js new file mode 100644 index 0000000000000000000000000000000000000000..87f00dd7e0941d87c6fc3ae035d839ac0ab79763 --- /dev/null +++ b/thehive_button/public/vis_controller.js @@ -0,0 +1,160 @@ +import { Status } from 'ui/vis/update_status'; +import chrome from 'ui/chrome'; +import { toastNotifications } from 'ui/notify'; +//import vis_template from './vis_template.html'; + +import React from 'react'; +import { + EuiButton, +// EuiPanel, +} from '@elastic/eui'; + + +function createTheHiveButton(vis) { + 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}); + return button; +} + +function hiveButtonOnClick(evt) { + var button = evt.currentTarget; + var params = button._vis.params; + //console.log("OnCLick", params); + + // Add "loading" icon and disable the button + var loading_span = document.createElement('span'); + loading_span.className = "euiLoadingSpinner euiLoadingSpinner--medium euiButton__spinner"; + button.firstChild.insertBefore(loading_span, button.firstChild.childNodes[0]); + button.setAttribute("disabled", true); + + var title = "TODO: SET THE TITLE"; + var descr = "(Created from Kibana)\n\n..."; + var severity = 2; + var start_date = null; + var owner = params.owner; + var flag = false; + var tlp = 2; + var tags = []; + + 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'); + } + // remove "loading" icon and re-enable the button + button.firstChild.removeChild(loading_span); + button.removeAttribute("disabled"); + }); + return false; +} + + +// 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("Sending request to Kibana API endpoint of thehive_button plugin:", 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("Response from The Hive:", 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("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); + }); +} + +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 = ''; + } + + async render(visData, status) { + //console.log('in promise!', this.vis) + //console.log(vis_template); + //var vis = this.vis; + //this.container.innerHTML = `${vis_template}`; + //this.container.innerHTML += '<p>Hello World from Promise! ' + JSON.stringify(status) + '</p>'; + //console.log('Render: ' + JSON.stringify(status)); + return 'done rendering'; + } +}; +export { VisController }; + diff --git a/thehive_button/server/routes/newcase.js b/thehive_button/server/routes/newcase.js new file mode 100644 index 0000000000000000000000000000000000000000..7400a5f91cbda1c9e203836dc6a77e17a359ee4f --- /dev/null +++ b/thehive_button/server/routes/newcase.js @@ -0,0 +1,62 @@ +const request = require('request'); + +export default function (server) { + server.route({ + path: '/api/thehive_button/new_case', + method: 'POST', + handler: newCaseHandler, + }); +} + +// Handler of ajax requests to create a new Case in The Hive +function newCaseHandler(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']; + var req_body = req.payload['body']; + + // 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 "/")'}; + } + if (!api_key) { + return {'error': 'API key not set'}; + } + + return new Promise( function(resolve, reject) { + request({ + method: 'POST', + url: base_url + 'api/case', + auth: {'bearer': api_key}, + json: true, + body: req_body, + //rejectUnauthorized: false, // Disables server certificate check + }, + // 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); + 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) + } + resolve({ + 'status_code': response.statusCode, + 'status_msg': response.statusMessage, + 'body': body + }); + } + } // handler function + ); // request() + }); // Promise() +} +