Commit 6f4ea3a6 authored by Arne Øslebø's avatar Arne Øslebø
Browse files

moved installation of Kiban plugin from building image to init

parent 616237f4
---
soctoolsproxy: "CHANGE_ME_TO_FQDN"
soctoolsproxy: "arne-centos.cert-labs.uninett.no"
maxmind_key: ""
......@@ -13,6 +13,8 @@ repo: soctools
version: 7
suffix: a20201004
kibana_plugins_version: "v0.7"
haproxy_name: "soctools-haproxy"
haproxy_version: "2.2"
haproxy_img: "{{repo}}/haproxy:{{version}}{{suffix}}"
......
......@@ -24,11 +24,6 @@
dest: "{{docker_build_dir}}/{{item}}/Dockerfile"
with_items: "{{services}}"
- name: Copy thehive_button to build path
copy:
src: "{{role_path}}/templates/odfekibana/thehive_button"
dest: "{{docker_build_dir}}/odfekibana/"
- name: Copy keycloak-tools to build path
copy:
src: "{{role_path}}/templates/keycloak/keycloak-tools"
......
......@@ -11,7 +11,4 @@ RUN for PLUGIN in \
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
ADD thehive_button /usr/share/kibana/plugins/thehive_button
RUN chown -R kibana:kibana /usr/share/kibana/plugins/thehive_button
ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
---
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) {