Skip to content
Snippets Groups Projects
Commit 04e48124 authored by Arne Øslebø's avatar Arne Øslebø
Browse files

Merge branch 'dev2' into dev1

parents 7f74c4b3 daa7a753
No related branches found
No related tags found
No related merge requests found
Showing
with 1350 additions and 16 deletions
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
dslproxy: "<CHANGE_ME:hostname>" dslproxy: "<CHANGE_ME:hostname>"
# TheHive Button plugin
THEHIVE_URL: "https://hive.gn4-3-wp8-soc.sunet.se/"
THEHIVE_API_KEY: "5LymseWiurZBrQN8Kqp8O+9KniTL5cE0"
THEHIVE_OWNER: "admin"
soctools_netname: "soctoolsnet" soctools_netname: "soctoolsnet"
repo: gn43-dsl repo: gn43-dsl
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
src: odfekibana/Dockerfile-kibana.j2 src: odfekibana/Dockerfile-kibana.j2
dest: "{{role_path}}/files/kibanaDockerfile" dest: "{{role_path}}/files/kibanaDockerfile"
- name: Copy tools to build path
command: "cp -av {{role_path}}/templates/odfekibana/thehive_button/ {{role_path}}/files/thehive_button/"
- name: Build kibana image - name: Build kibana image
command: docker build -t {{repo}}/kibana:{{version}}{{suffix}} -f {{role_path}}/files/kibanaDockerfile {{role_path}}/files command: docker build -t {{repo}}/kibana:{{version}}{{suffix}} -f {{role_path}}/files/kibanaDockerfile {{role_path}}/files
......
...@@ -11,5 +11,8 @@ RUN for PLUGIN in \ ...@@ -11,5 +11,8 @@ RUN for PLUGIN in \
https://d3g5vo6xdbdb9a.cloudfront.net/downloads/kibana-plugins/opendistro-index-management/opendistro_index_management_kibana-{{odfeplugin_version}}.zip; \ https://d3g5vo6xdbdb9a.cloudfront.net/downloads/kibana-plugins/opendistro-index-management/opendistro_index_management_kibana-{{odfeplugin_version}}.zip; \
do bin/kibana-plugin install --allow-root ${PLUGIN}; done do bin/kibana-plugin install --allow-root ${PLUGIN}; done
ADD thehive_button /usr/share/kibana/plugins/thehive_button
RUN chown -R kibana:kibana /usr/share/kibana/plugins/thehive_button
USER kibana USER kibana
---
extends: "@elastic/kibana"
settings:
import/resolver:
'@elastic/eslint-import-resolver-kibana':
rootPackageName: 'thehive_button'
{
}
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);
}
});
}
{
"name": "thehive_button",
"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.4.2"
},
"scripts": {
"lint": "eslint .",
"start": "plugin-helpers start",
"build": "plugin-helpers build"
},
"dependencies": {
"request": "^2.88.0",
"@elastic/eui": "10.4.2",
"react": "^16.8.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"
}
}
// 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);
});
}
// 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
import { THEHIVE_API_KEY, THEHIVE_URL, THEHIVE_OWNER } from './env';
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 { DefaultEditorSize } from 'ui/vis/editor_size';
function TheHiveButtonVisProvider(Private) {
const VisFactory = Private(VisFactoryProvider);
//console.log("default URL:", THEHIVE_URL);
//console.log("default API key:", THEHIVE_API_KEY);
return VisFactory.createReactVisualization({
name: 'thehive_button',
title: 'The Hive Case',
icon: 'alert',
description: 'A button to create a new Case in The Hive.',
//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,
obsFields: [], // list of objects, e.g. {name: "clientip", type: "ip", cnt: 100}
}
},
//editor: 'default',
editorConfig: {
optionTabs: [
{
name: "options",
title: "Options",
editor: optionsEditor,
}
],
defaultSize: DefaultEditorSize.LARGE,
},
// optionsTemplate: optionsEditor, //optionsTemplate,
// //enableAutoApply: true,
// },
requestHandler: 'theHiveButtonRequestHandler', // own request handler
responseHandler: 'none', // pass data as returned by requestHandler
});
}
// register the provider with the visTypes registry
VisTypesRegistryProvider.register(TheHiveButtonVisProvider);
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>
}
<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>
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;
}
.myvis-container-div {
padding: 1em;
}
//import { Status } from 'ui/vis/update_status';
import { toastNotifications } from 'ui/notify';
import { createTheHiveCase, addCaseObservables } from './create_case';
//import vis_template from './vis_template.html';
import React, { Component } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiTextArea,
EuiSuperSelect,
EuiBasicTable,
EuiCheckbox,
makeId,
} from '@elastic/eui';
// ********** React components **********
// Main React component - the root of visualization
export class TheHiveButtonVisComponent extends Component {
render() {
//console.log("TheHiveButtonVisComponent.render(), props:", this.props);
return (
<div>
<NewCaseButton params={this.props.vis.params} observables={this.props.visData} />
</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 - 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);
// 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,
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.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.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() {
this.setState({ isModalVisible: false });
}
showModal() {
this.setState({ isModalVisible: true });
}
// Event handlers for change of case parameter
onTitleChange(evt) {
this.setState({title: evt.target.value});
}
onSeverityChange(value) {
this.setState({severity: value});
}
onTLPChange(value) {
this.setState({tlp: value});
}
onDescriptionChange(evt) {
this.setState({description: evt.target.value});
}
// 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) => {
let newObsSel = {...this.state.obsSel};
newObsSel[fieldName] = selectedIndices;
return {obsSel: newObsSel};
});
}
// 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) => {
let newObsData = {...this.state.obsData};
newObsData[fieldName][ix][param] = value;
return {obsData: newObsData};
});
}
// 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
async 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 = [];
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);
// 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
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 = 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, reset form fields and hide spinner
this.closeModal();
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.");
}
}
}
// 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);
// No state here, everything is in the parent class (NewCaseButton)
// "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"},
];
}
// Main render function
render() {
// 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 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 key={this.props.resetCnt}>
<EuiForm style={{width: "800px"}}>
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiFormRow label="Title" fullWidth>
<EuiFieldText name="title" value={this.props.title} onChange={this.props.onTitleChange} required={true} fullWidth />
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow label="Severity">
<EuiSuperSelect
options={this.severityOptions}
valueOfSelected={this.props.severity}
onChange={this.props.onSeverityChange}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow label="TLP">
<EuiSuperSelect
prepend="TLP"
options={this.tlpOptions}
valueOfSelected={this.props.tlp}
onChange={this.props.onTLPChange}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFormRow label="Description" fullWidth>
<EuiTextArea
defaultValue={this.props.description}
onChange={this.props.onDescriptionChange}
rows={4}
fullWidth
/>
</EuiFormRow>
{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}>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 is created.
// Prepare the 'selection' array - it should contain a list of selected row specifications
let selection = [];
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.tableRef.current;
table_node.setState({selection: selection});
}
}
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({
path: '/api/thehive_button/new_case',
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
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,
//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.error("ERROR when trying to send request to The Hive:", error);
resolve({'error': error.message});
}
else {
if (response.statusCode < 200 || response.statusCode >= 300) {
console.error("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()
}
// 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()
}
// Default plugin configuration
export const THEHIVE_URL = '{{THEHIVE_URL}}';
export const THEHIVE_API_KEY = '{{THEHIVE_API_KEY}}';
export const THEHIVE_OWNER = '{{THEHIVE_OWNER}}'; // default owner account of the created cases
...@@ -85,6 +85,17 @@ ...@@ -85,6 +85,17 @@
# tags: # tags:
# - start # - start
- name: Generate configuration for thehive_button plugin
template:
src: files/env.js.j2
dest: "/usr/share/kibana/plugins/thehive_button/public/env.js"
owner: kibana
group: kibana
tags:
- start
- name: Start OpenDistro Kibana for Elasticsearch - name: Start OpenDistro Kibana for Elasticsearch
command: /usr/share/kibana/startkibana.sh command: /usr/share/kibana/startkibana.sh
#shell: exec /usr/share/kibana/bin/kibana -c config/kibana.yml & #shell: exec /usr/share/kibana/bin/kibana -c config/kibana.yml &
...@@ -101,18 +112,14 @@ ...@@ -101,18 +112,14 @@
tags: tags:
- start - start
- name: kibana health - name: Check Kibana health
block: shell: 'curl -k -b /tmp/cookie.txt -c /tmp/cookie.txt -X "GET" "https://{{dslproxy}}:5601/api/status" \
- name: check kibana health
shell: 'curl -k -b /tmp/cookie.txt -c /tmp/cookie.txt -X "GET" "https://{{dslproxy}}:5601/api/status" \
| egrep status....overall....state...green'
rescue:
- name: wait for kibana to become ready
shell: 'sleep 180'
always:
- name: recheck kibana health
shell: 'curl -k -b /tmp/cookie.txt -c /tmp/cookie.txt -X "GET" "https://{{dslproxy}}:5601/api/status" \
| egrep status....overall....state...green' | egrep status....overall....state...green'
register: result
until: task_result.rc == 0
retries: 90
delay: 2
ignore_errors: yes
tags: tags:
- start - start
...@@ -150,11 +157,11 @@ ...@@ -150,11 +157,11 @@
tags: tags:
- start - start
- name: cleanup temporary files for kibana_graph import #- name: cleanup temporary files for kibana_graph import
shell: '/bin/rm -rf /tmp/cookie.txt /tmp/kibana_graphs.ndjson /tmp/tenant.json' # shell: '/bin/rm -rf /tmp/cookie.txt /tmp/kibana_graphs.ndjson /tmp/tenant.json'
ignore_errors: true # ignore_errors: true
tags: # tags:
- start # - start
#- name: check reachable hosts #- name: check reachable hosts
# gather_facts: no # gather_facts: no
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment