Commit 63d694af authored by To-om's avatar To-om

Merge branch 'release/3.3.0-RC6'

parents c91802b6 55adc909
# Change Log
## [3.3.0-RC5](https://github.com/TheHive-Project/TheHive/tree/HEAD) (2019-02-24)
## [3.3.0-RC6](https://github.com/TheHive-Project/TheHive/tree/3.3.0-RC6) (2019-02-07)
[Full Changelog](https://github.com/TheHive-Project/TheHive/compare/3.3.0-RC5...3.3.0-RC6)
**Implemented enhancements:**
- Add Tags to an Alert with Responder [\#912](https://github.com/TheHive-Project/TheHive/issues/912)
- Dashboards - Add text widget [\#908](https://github.com/TheHive-Project/TheHive/issues/908)
- Empty case still available when disabled [\#901](https://github.com/TheHive-Project/TheHive/issues/901)
- Support for filtering Tags by prefix \(using asterisk, % or something\) in search dialog [\#666](https://github.com/TheHive-Project/TheHive/issues/666)
**Closed issues:**
- Dynamic \(auto-refresh\) of cases is break in 3.3.0-RC5 [\#907](https://github.com/TheHive-Project/TheHive/issues/907)
- Hostname Artifact [\#900](https://github.com/TheHive-Project/TheHive/issues/900)
- DOS issue: Firefox crashing TheHive [\#899](https://github.com/TheHive-Project/TheHive/issues/899)
## [3.3.0-RC5](https://github.com/TheHive-Project/TheHive/tree/3.3.0-RC5) (2019-02-23)
[Full Changelog](https://github.com/TheHive-Project/TheHive/compare/3.3.0-RC4...3.3.0-RC5)
**Implemented enhancements:**
......
......@@ -20,7 +20,7 @@ object Dependencies {
val reflections = "org.reflections" % "reflections" % "0.9.11"
val zip4j = "net.lingala.zip4j" % "zip4j" % "1.3.2"
val elastic4play = "org.thehive-project" %% "elastic4play" % "1.8.0-1"
val elastic4play = "org.thehive-project" %% "elastic4play" % "1.9.0"
val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % "2.5.19"
val akkaClusterTools = "com.typesafe.akka" %% "akka-cluster-tools" % "2.5.19"
}
......
......@@ -61,6 +61,10 @@ case class AddLogToTask(content: String, owner: Option[String], status: ActionOp
override def updateStatus(newStatus: Type, newMessage: String): ActionOperation = copy(status = newStatus, message = newMessage)
}
case class AddTagToAlert(tag: String, status: ActionOperationStatus.Type = ActionOperationStatus.Waiting, message: String = "") extends ActionOperation {
override def updateStatus(newStatus: ActionOperationStatus.Type, newMessage: String): AddTagToAlert = copy(status = newStatus, message = newMessage)
}
object ActionOperation {
val addTagToCaseWrites = Json.writes[AddTagToCase]
val addTagToArtifactWrites = Json.writes[AddTagToArtifact]
......@@ -69,6 +73,7 @@ object ActionOperation {
val closeTaskWrites = Json.writes[CloseTask]
val markAlertAsReadWrites = Json.writes[MarkAlertAsRead]
val addLogToTaskWrites = Json.writes[AddLogToTask]
val addTagToAlertWrites = Json.writes[AddTagToAlert]
implicit val actionOperationReads: Reads[ActionOperation] = Reads[ActionOperation](json
(json \ "type").asOpt[String].fold[JsResult[ActionOperation]](JsError("type is missing in action operation")) {
case "AddTagToCase" (json \ "tag").validate[String].map(tag AddTagToCase(tag))
......@@ -85,6 +90,7 @@ object ActionOperation {
content (json \ "content").validate[String]
owner (json \ "owner").validateOpt[String]
} yield AddLogToTask(content, owner)
case "AddTagToAlert" => (json \ "tag").validate[String].map(tag AddTagToAlert(tag))
case other JsError(s"Unknown operation $other")
})
implicit val actionOperationWrites: Writes[ActionOperation] = Writes[ActionOperation] {
......@@ -95,6 +101,7 @@ object ActionOperation {
case a: CloseTask closeTaskWrites.writes(a)
case a: MarkAlertAsRead markAlertAsReadWrites.writes(a)
case a: AddLogToTask addLogToTaskWrites.writes(a)
case a: AddTagToAlert addTagToAlertWrites.writes(a)
case a Json.obj("unsupported operation" a.toString)
}
}
......@@ -198,6 +205,15 @@ class ActionOperationSrv @Inject() (
task findTaskEntity(entity)
_ logSrv.create(task, Fields.empty.set("message", content).set("owner", owner.map(JsString)))
} yield operation.updateStatus(ActionOperationStatus.Success, "")
case AddTagToAlert(tag, _, _) =>
entity match {
case initialAlert: Alert
for {
alert alertSrv.get(initialAlert.id)
_ alertSrv.update(alert.id, Fields.empty.set("tags", Json.toJson((alert.tags() :+ tag).distinct)), ModifyConfig(retryOnConflict = 0, version = Some(alert.version)))
} yield operation.updateStatus(ActionOperationStatus.Success, "")
case _ Future.failed(BadRequestError("Alert not found"))
}
case o Future.successful(operation.updateStatus(ActionOperationStatus.Failure, s"Operation $o not supported"))
}
}
......
......@@ -186,6 +186,7 @@
<script src="scripts/directives/dashboard/item.js"></script>
<script src="scripts/directives/dashboard/line.js"></script>
<script src="scripts/directives/dashboard/multiline.js"></script>
<script src="scripts/directives/dashboard/text.js"></script>
<script src="scripts/directives/dateTimePicker.js"></script>
<script src="scripts/directives/dt-picker.js"></script>
<script src="scripts/directives/entityLink.js"></script>
......
......@@ -57,11 +57,11 @@
this.canEditDashboard = function() {
return (this.createdBy === this.currentUser.id) ||
(this.dashboardStatus = 'Shared' && AuthenticationSrv.isAdmin(this.currentUser));
}
};
this.options = {
dashboardAllowedTypes: ['container'],
containerAllowedTypes: ['bar', 'line', 'donut', 'counter', 'multiline'],
containerAllowedTypes: ['bar', 'line', 'donut', 'counter', 'text', 'multiline'],
maxColumns: 3,
cls: DashboardSrv.typeClasses,
labels: {
......@@ -70,6 +70,7 @@
donut: 'Donut',
line: 'Line',
counter: 'Counter',
text: 'Text',
multiline: 'Multi Lines'
},
editLayout: !_.find(this.definition.items, function(row) {
......@@ -132,9 +133,9 @@
}, 0);
});
}
};
this.itemInserted = function(item, rows, rowIndex, index) {
this.itemInserted = function(item, rows/*, rowIndex, index*/) {
if(!item.id){
item.id = UtilsSrv.guid();
}
......
......@@ -33,7 +33,7 @@
}
return s;
}
};
scope.buildSerie = function(serie, q, index) {
return {
......@@ -166,7 +166,7 @@
};
scope.chart = chart;
}, function(err) {
}, function(/*err*/) {
scope.error = true;
NotificationSrv.log('Failed to fetch data, please edit the widget definition', 'error');
});
......
(function() {
'use strict';
angular.module('theHiveDirectives').directive('dashboardText', function($q, $http, $state, DashboardSrv, GlobalSearchSrv, NotificationSrv) {
return {
restrict: 'E',
scope: {
filter: '=?',
options: '=',
entity: '=',
autoload: '=',
mode: '=',
refreshOn: '@',
resizeOn: '@',
metadata: '='
},
templateUrl: 'views/directives/dashboard/text/view.html',
link: function(scope, elem) {
scope.error = false;
scope.data = null;
scope.globalQuery = null;
scope.load = function() {
if(!scope.options.series || scope.options.series.length === 0) {
scope.error = true;
return;
}
var query = DashboardSrv.buildChartQuery(scope.filter, scope.options.query);
scope.globalQuery = query;
var stats = {
stats: _.map(scope.options.series || [], function(serie, index) {
var s = {
_agg: serie.agg,
_name: serie.name || 'agg_' + (index + 1),
_query: serie.query || {}
};
if(serie.agg !== 'count') {
s._field = serie.field;
}
return {
model: serie.entity,
query: query,
stats: [s]
};
})
};
var statsPromise = $http.post('./api/_stats', stats);
statsPromise.then(function(response) {
scope.error = false;
scope.data = response.data;
var template = scope.options.template;
Object.keys(scope.data).forEach(function(key){
var regex = new RegExp('{{' + key + '}}', 'gi');
template = template.replace(regex, scope.data[key]);
});
scope.content = template;
}, function(/*err*/) {
scope.error = true;
NotificationSrv.log('Failed to fetch data, please edit the widget definition', 'error');
});
};
scope.copyHTML = function() {
var html = elem[0].querySelector('.widget-content').innerHTML;
function listener(e) {
e.clipboardData.setData('text/html', html);
e.clipboardData.setData('text/plain', html);
e.preventDefault();
}
document.addEventListener('copy', listener);
document.execCommand('copy');
document.removeEventListener('copy', listener);
}
if (scope.autoload === true) {
scope.load();
}
if (!_.isEmpty(scope.refreshOn)) {
scope.$on(scope.refreshOn, function(event, filter) {
scope.filter = filter;
scope.load();
});
}
}
};
});
})();
......@@ -74,7 +74,8 @@
donut: 'fa-pie-chart',
line: 'fa-line-chart',
multiline: 'fa-area-chart',
counter: 'fa-calculator'
counter: 'fa-calculator',
text: 'fa-file'
};
this.sortOptions = [{
......@@ -113,6 +114,14 @@
entity: null
}
},
{
type: 'text',
options: {
title: null,
template: null,
entity: null
}
},
{
type: 'donut',
options: {
......@@ -234,6 +243,7 @@
this.hasMinimalConfiguration = function(component) {
switch (component.type) {
case 'multiline':
case 'text':
return component.options.series.length === _.without(_.pluck(component.options.series, 'entity'), undefined).length;
default:
return !!component.options.entity;
......
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">
<i class="mr-xxs fa" ng-class="typeClasses[component.type]"></i>{{component.options.title || 'No title'}} {{mode}}
<i class="mr-xxs fa" ng-class="typeClasses[component.type]"></i>{{component.options.title || 'No title'}}
</h3>
<div class="box-tools pull-right">
......@@ -56,6 +56,14 @@
resize-on="{{resizeOn}}"
mode="mode"></dashboard-multiline>
<dashboard-text ng-switch-when="text"
entity="metadata[component.options.entity]"
options="component.options"
filter="filter"
autoload="autoload"
refresh-on="{{refreshOn}}"
mode="mode"></dashboard-text>
<dashboard-counter ng-switch-when="counter"
entity="metadata[component.options.entity]"
options="component.options"
......
<div class="form-group">
<label>Title</label>
<input type="text" class="form-control" placeholder="Ex: Summary" ng-model="component.options.title">
</div>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label>Template</label>
<textarea class="content-box" name="template"
placeholder="Close summary" markdown-editor="{
'fullscreen': {enable: false},
'iconlibrary': 'fa',
addExtraButtons: true,
resize: 'vertical'}" rows="6" ng-model="component.options.template" required></textarea>
</div>
</div>
</div>
<uib-tabset class="nav-tabs-custom" active="layout.activeTab">
<uib-tab index="0">
<uib-tab-heading>
<i class="fa fa-bars"></i> Basic
</uib-tab-heading>
<ng-include src="'views/directives/dashboard/text/basic.html'"></ng-include>
</uib-tab>
<uib-tab index="1">
<uib-tab-heading>
<i class="fa fa-sort"></i> Series
</uib-tab-heading>
<ng-include src="'views/directives/dashboard/text/series.html'"></ng-include>
</uib-tab>
</uib-tabset>
<div ng-if="serie.filters.length > 0">
<strong>Serie's filter</strong>
</div>
<div class="row mb-xxxs" ng-repeat="filter in serie.filters track by $index">
<div class="col-sm-4">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeSerieFilter(serie, $index)">
<i class="fa fa-times text-danger"></i>
</button>
</span>
<select class="form-control" ng-model="filter.field"
ng-options="item.name as item.name for (key, item) in metadata[serie.entity].attributes"
ng-change="setFilterField(filter, serie.entity)"></select>
</div>
</div>
<div class="col-sm-8">
<filter-editor metadata="metadata" filter="filter" entity="serie.entity"></filter-editor>
</div>
</div>
<div class="mt-xxs">
<a href ng-click="addSerieFilter(serie)">
<i class="fa fa-plus"></i> Add a filter
</a>
</div>
<div ng-if="!component.options.series || component.options.series.length === 0" class="empty-message">
No series defined. <a href ng-click="addSerie()">Add a serie</a>
</div>
<div class="mb-xxxs dashboard-serie" ng-repeat="serie in component.options.series track by $index">
<div class="form-inline mb-xxxs">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeSerie($index)">
<i class="fa fa-times text-danger"></i>
</button>
</span>
<div class="form-group">
<select class="form-control" ng-model="serie.entity"
ng-options="item as metadata[item].label for item in metadata.entities"></select>
</div>
</div>
<div class="form-group">
<select class="form-control" ng-model="serie.agg"
ng-options="item.label as item.id for (key, item) in aggregations"
ng-change="setSerieAgg(serie)">
<option value="" disabled selected></option>
</select>
</div>
<div class="form-group">
<select class="form-control" ng-model="serie.field" ng-disabled="serie.agg === 'count'"
ng-options="item.name as item.name for (key, item) in fieldsForAggregation(metadata[serie.entity].attributes, serie.agg)">
<option value="" disabled selected>-- Select field --</option>
</select>
</div>
<div class="form-group">
<input class="form-control" type="text" ng-model="serie.name" placeholder="Variable name">
</div>
</div>
<div class="ml-m mt-xs">
<ng-include src="'views/directives/dashboard/text/serie.filters.html'"></ng-include>
</div>
</div>
<div ng-if="component.options.series && component.options.series.length > 0" class="mv-xs">
<a href ng-click="addSerie()">
<i class="fa fa-plus"></i> Add a serie
</a>
</div>
<div class="c3-container">
<div class="c3-error" ng-if="error">
<div class="text-center">
<h4 class="text-danger">
<div class="mb-xxs"><i class="fa fa-2x fa-exclamation-triangle"></i></div>
<div>Failed to fetch data, please edit the widget definition</div>
</h4>
</div>
</div>
<div ng-if="!error" class="row">
<div class="col-sm-12">
<div class="clearfix" ng-show="content">
<a class="pull-right ml-xxs" href ng-click="copyHTML()"><i class="fa fa-file-text-o mr-xxxs"></i>HTML</a>
<strong class="text-primary pull-right">Copy</strong>
</div>
<div marked="content" class="markdown widget-content"></div>
</div>
</div>
</div>
......@@ -169,7 +169,7 @@
<option value="" ng-if="dialog.templates.length === 0 || !!!dialog.hideEmptyCaseButton">Empty case</option>
</select>
<span class="input-group-btn">
<button class="btn btn-primary" type="submit" ng-disabled="dialog.loading">Yes, Import</button>
<button class="btn btn-primary" type="submit" ng-disabled="dialog.loading || (!dialog.event.caseTemplate && !!dialog.hideEmptyCaseButton && dialog.templates.length > 0)">Yes, Import</button>
</span>
</div>
......
{
"name": "thehive",
"version": "3.3.0-RC5",
"version": "3.3.0-RC6",
"license": "AGPL-3.0",
"dependencies": {
"angular": "1.5.8",
......
{
"name": "thehive",
"version": "3.3.0-RC5",
"version": "3.3.0-RC6",
"license": "AGPL-3.0",
"repository": {
"type": "git",
......
version in ThisBuild := "3.3.0-RC5"
version in ThisBuild := "3.3.0-RC6"
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment