Skip to content

Commit 9c5c12b

Browse files
committed
Merge branch 'release/2.12.0'
Conflicts: CHANGELOG.md ui/bower.json ui/package.json
2 parents d38f28c + 6a0ce0c commit 9c5c12b

File tree

113 files changed

+2937
-740
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

113 files changed

+2937
-740
lines changed

CHANGELOG.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,36 @@
11
# Change Log
22

3+
## [2.12.0](https://github.com/CERT-BDF/TheHive/tree/2.12.0)
4+
5+
[Full Changelog](https://github.com/CERT-BDF/TheHive/compare/2.11.3...2.12.0)
6+
7+
**Implemented enhancements:**
8+
9+
- Sort the analyzers list in observable details page [\#245](https://github.com/CERT-BDF/TheHive/issues/245)
10+
- More options to sort cases [\#243](https://github.com/CERT-BDF/TheHive/issues/243)
11+
- Alert Preview and management improvements [\#232](https://github.com/CERT-BDF/TheHive/issues/232)
12+
- Ability to Reopen Tasks [\#156](https://github.com/CERT-BDF/TheHive/issues/156)
13+
- Display short reports on the Observables tab [\#131](https://github.com/CERT-BDF/TheHive/issues/131)
14+
- Custom fields for case template [\#12](https://github.com/CERT-BDF/TheHive/issues/12)
15+
- Show case status and category \(FP, TP, IND\) in related cases [\#229](https://github.com/CERT-BDF/TheHive/issues/229)
16+
- Open External Links in New Tab [\#228](https://github.com/CERT-BDF/TheHive/issues/228)
17+
- Observable analyzers view reports. [\#191](https://github.com/CERT-BDF/TheHive/issues/191)
18+
- Specifying tags on statistics page or performing a search [\#186](https://github.com/CERT-BDF/TheHive/issues/186)
19+
- Choose case template while importing events from MISP [\#175](https://github.com/CERT-BDF/TheHive/issues/175)
20+
- Use local font files [\#250](https://github.com/CERT-BDF/TheHive/issues/250)
21+
22+
**Fixed bugs:**
23+
24+
- Fix case metrics malformed definitions [\#248](https://github.com/CERT-BDF/TheHive/issues/248)
25+
- Sorting alerts by severity fails [\#242](https://github.com/CERT-BDF/TheHive/issues/242)
26+
- Alerting Panel: Typo Correction [\#240](https://github.com/CERT-BDF/TheHive/issues/240)
27+
- files in alerts are limited to 32kB [\#237](https://github.com/CERT-BDF/TheHive/issues/237)
28+
- Alert can contain inconsistent data [\#234](https://github.com/CERT-BDF/TheHive/issues/234)
29+
- Search do not work with non-latin characters [\#223](https://github.com/CERT-BDF/TheHive/issues/223)
30+
- report status not updated after finish [\#212](https://github.com/CERT-BDF/TheHive/issues/212)
31+
332
## [2.11.3](https://github.com/CERT-BDF/TheHive/tree/2.11.3) (2017-06-14)
4-
[Full Changelog](https://github.com/CERT-BDF/TheHive/compare/debian/2.11.2-2...2.11.3)
33+
[Full Changelog](https://github.com/CERT-BDF/TheHive/compare/debian/2.11.2...2.11.3)
534

635
**Fixed bugs:**
736

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ packageBin := {
104104
(packageBin in Rpm).value
105105
}
106106
// DEB //
107-
version in Debian := version.value + "-2"
107+
version in Debian := version.value + "-1"
108108
debianPackageRecommends := Seq("elasticsearch")
109109
debianPackageDependencies += "openjdk-8-jre-headless"
110110
maintainerScripts in Debian := maintainerScriptsFromDirectory(

contrib/report-templates/File_Info_1_0/long.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<!-- File Indenfication -->
1010
<div class="panel panel-info">
1111
<div class="panel-heading">
12-
<strong>File Idenfitication</strong>
12+
<strong>File Identification</strong>
1313
</div>
1414
<div class="panel-body">
1515
<dl class="dl-horizontal">

project/Dependencies.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ object Dependencies {
3030
val reflections = "org.reflections" % "reflections" % "0.9.10"
3131
val zip4j = "net.lingala.zip4j" % "zip4j" % "1.3.2"
3232
val akkaTest = "com.typesafe.akka" %% "akka-stream-testkit" % "2.4.4"
33-
val elastic4play = "org.cert-bdf" %% "elastic4play" % "1.1.5"
33+
val elastic4play = "org.cert-bdf" %% "elastic4play" % "1.2.1"
3434
}
3535
}

thehive-backend/app/controllers/AlertCtrl.scala

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ import org.elastic4play.services._
1010
import org.elastic4play.{ BadRequestError, Timed }
1111
import play.api.Logger
1212
import play.api.http.Status
13-
import play.api.libs.json.JsArray
13+
import play.api.libs.json.{ JsArray, JsObject, Json }
1414
import play.api.mvc.{ Action, AnyContent, Controller }
15-
import services.AlertSrv
15+
import services.{ AlertSrv, CaseSrv }
16+
import services.JsonFormat.caseSimilarityWrites
1617

1718
import scala.concurrent.{ ExecutionContext, Future }
1819
import scala.util.Try
1920

2021
@Singleton
2122
class AlertCtrl @Inject() (
2223
alertSrv: AlertSrv,
24+
caseSrv: CaseSrv,
2325
auxSrv: AuxSrv,
2426
authenticated: Authenticated,
2527
renderer: Renderer,
@@ -35,17 +37,39 @@ class AlertCtrl @Inject() (
3537
.map(alert renderer.toOutput(CREATED, alert))
3638
}
3739

40+
@Timed
41+
def mergeWithCase(alertId: String, caseId: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request
42+
for {
43+
alert alertSrv.get(alertId)
44+
caze caseSrv.get(caseId)
45+
_ alertSrv.mergeWithCase(alert, caze)
46+
} yield renderer.toOutput(CREATED, caze)
47+
}
48+
3849
@Timed
3950
def get(id: String): Action[AnyContent] = authenticated(Role.read).async { implicit request
40-
val withStats = for {
41-
statsValues request.queryString.get("nstats")
42-
firstValue statsValues.headOption
43-
} yield Try(firstValue.toBoolean).getOrElse(firstValue == "1")
51+
val withStats = request
52+
.queryString
53+
.get("nstats")
54+
.flatMap(_.headOption)
55+
.exists(v Try(v.toBoolean).getOrElse(v == "1"))
56+
57+
val withSimilarity = request
58+
.queryString
59+
.get("similarity")
60+
.flatMap(_.headOption)
61+
.exists(v Try(v.toBoolean).getOrElse(v == "1"))
4462

4563
for {
4664
alert alertSrv.get(id)
47-
alertsWithStats auxSrv.apply(alert, 0, withStats.getOrElse(false), removeUnaudited = false)
48-
} yield renderer.toOutput(OK, alertsWithStats)
65+
alertsWithStats auxSrv.apply(alert, 0, withStats, removeUnaudited = false)
66+
similarCases if (withSimilarity)
67+
alertSrv.similarCases(alert)
68+
.map(sc Json.obj("similarCases" Json.toJson(sc)))
69+
else Future.successful(JsObject(Nil))
70+
} yield {
71+
renderer.toOutput(OK, alertsWithStats ++ similarCases)
72+
}
4973
}
5074

5175
@Timed
@@ -106,11 +130,12 @@ class AlertCtrl @Inject() (
106130
}
107131

108132
@Timed
109-
def createCase(id: String): Action[AnyContent] = authenticated(Role.write).async { implicit request
133+
def createCase(id: String): Action[Fields] = authenticated(Role.write).async(fieldsBodyParser) { implicit request
110134
for {
111135
alert alertSrv.get(id)
112-
updatedAlert alertSrv.createCase(alert)
113-
} yield renderer.toOutput(CREATED, updatedAlert)
136+
customCaseTemplate = request.body.getString("caseTemplate")
137+
caze alertSrv.createCase(alert, customCaseTemplate)
138+
} yield renderer.toOutput(CREATED, caze)
114139
}
115140

116141
@Timed

thehive-backend/app/controllers/ArtifactCtrl.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class ArtifactCtrl @Inject() (
4848

4949
@Timed
5050
def get(id: String): Action[Fields] = authenticated(Role.read).async(fieldsBodyParser) { implicit request
51-
artifactSrv.get(id, request.body.getStrings("fields").map("dataType" +: _))
51+
artifactSrv.get(id)
5252
.map(artifact renderer.toOutput(OK, artifact))
5353
}
5454

thehive-backend/app/models/Alert.scala

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import java.util.Date
44
import javax.inject.{ Inject, Singleton }
55

66
import models.JsonFormat.alertStatusFormat
7-
import org.elastic4play.models.{ Attribute, AttributeDef, BaseEntity, EntityDef, HiveEnumeration, ModelDef, AttributeFormat F, AttributeOption O }
7+
import org.elastic4play.controllers.JsonInputValue
8+
import org.elastic4play.models.{ Attribute, AttributeDef, BaseEntity, EntityDef, HiveEnumeration, ModelDef, MultiAttributeFormat, OptionalAttributeFormat, AttributeFormat F, AttributeOption O }
9+
import org.elastic4play.services.DBLists
810
import org.elastic4play.utils.Hasher
11+
import org.elastic4play.{ AttributeCheckingError, InvalidFormatAttributeError }
912
import play.api.Logger
1013
import play.api.libs.json._
1114
import services.AuditedModel
@@ -20,7 +23,26 @@ object AlertStatus extends Enumeration with HiveEnumeration {
2023

2124
trait AlertAttributes {
2225
_: AttributeDef
23-
def artifactAttributes: Seq[Attribute[_]]
26+
val artifactAttributes: Seq[Attribute[_]] = {
27+
val remoteAttachmentAttributes = Seq(
28+
Attribute("alert", "reference", F.stringFmt, Nil, None, ""),
29+
Attribute("alert", "filename", OptionalAttributeFormat(F.stringFmt), Nil, None, ""),
30+
Attribute("alert", "contentType", OptionalAttributeFormat(F.stringFmt), Nil, None, ""),
31+
Attribute("alert", "size", OptionalAttributeFormat(F.numberFmt), Nil, None, ""),
32+
Attribute("alert", "hash", MultiAttributeFormat(F.stringFmt), Nil, None, ""),
33+
Attribute("alert", "type", OptionalAttributeFormat(F.stringFmt), Nil, None, ""))
34+
35+
Seq(
36+
Attribute("alert", "data", OptionalAttributeFormat(F.stringFmt), Nil, None, ""),
37+
Attribute("alert", "dataType", F.stringFmt, Nil, None, ""),
38+
Attribute("alert", "message", OptionalAttributeFormat(F.stringFmt), Nil, None, ""),
39+
Attribute("alert", "startDate", OptionalAttributeFormat(F.dateFmt), Nil, None, ""),
40+
Attribute("alert", "attachment", OptionalAttributeFormat(F.attachmentFmt), Nil, None, ""),
41+
Attribute("alert", "remoteAttachment", OptionalAttributeFormat(F.objectFmt(remoteAttachmentAttributes)), Nil, None, ""),
42+
Attribute("alert", "tlp", OptionalAttributeFormat(F.numberFmt), Nil, None, ""),
43+
Attribute("alert", "tags", MultiAttributeFormat(F.stringFmt), Nil, None, ""),
44+
Attribute("alert", "ioc", OptionalAttributeFormat(F.stringFmt), Nil, None, ""))
45+
}
2446

2547
val alertId: A[String] = attribute("_id", F.stringFmt, "Alert id", O.readonly)
2648
val tpe: A[String] = attribute("type", F.stringFmt, "Type of the alert", O.readonly)
@@ -41,7 +63,7 @@ trait AlertAttributes {
4163
}
4264

4365
@Singleton
44-
class AlertModel @Inject() (artifactModel: ArtifactModel)
66+
class AlertModel @Inject() (dblists: DBLists)
4567
extends ModelDef[AlertModel, Alert]("alert")
4668
with AlertAttributes
4769
with AuditedModel {
@@ -50,30 +72,38 @@ class AlertModel @Inject() (artifactModel: ArtifactModel)
5072
override val defaultSortBy: Seq[String] = Seq("-date")
5173
override val removeAttribute: JsObject = Json.obj("status" AlertStatus.Ignored)
5274

53-
override def artifactAttributes: Seq[Attribute[_]] = artifactModel.attributes
54-
5575
override def creationHook(parent: Option[BaseEntity], attrs: JsObject): Future[JsObject] = {
56-
Future.successful {
57-
if (attrs.keys.contains("_id"))
58-
attrs
59-
else {
60-
val hasher = Hasher("MD5")
61-
val tpe = (attrs \ "tpe").asOpt[String].getOrElse("<null>")
62-
val source = (attrs \ "source").asOpt[String].getOrElse("<null>")
63-
val sourceRef = (attrs \ "sourceRef").asOpt[String].getOrElse("<null>")
64-
val _id = hasher.fromString(s"$tpe|$source|$sourceRef").head.toString()
65-
attrs + ("_id" JsString(_id))
66-
} - "lastSyncDate" - "case" - "status" - "follow"
67-
}
76+
// check if data attribute is present on all artifacts
77+
val missingDataErrors = (attrs \ "artifacts")
78+
.asOpt[Seq[JsValue]]
79+
.getOrElse(Nil)
80+
.filter { a
81+
((a \ "data").toOption.isEmpty && (a \ "attachment").toOption.isEmpty && (a \ "remoteAttachment").toOption.isEmpty) ||
82+
((a \ "tags").toOption.isEmpty && (a \ "message").toOption.isEmpty)
83+
}
84+
.map(v InvalidFormatAttributeError("artifacts", "artifact", JsonInputValue(v)))
85+
if (missingDataErrors.nonEmpty)
86+
Future.failed(AttributeCheckingError("alert", missingDataErrors))
87+
else
88+
Future.successful {
89+
if (attrs.keys.contains("_id"))
90+
attrs
91+
else {
92+
val hasher = Hasher("MD5")
93+
val tpe = (attrs \ "tpe").asOpt[String].getOrElse("<null>")
94+
val source = (attrs \ "source").asOpt[String].getOrElse("<null>")
95+
val sourceRef = (attrs \ "sourceRef").asOpt[String].getOrElse("<null>")
96+
val _id = hasher.fromString(s"$tpe|$source|$sourceRef").head.toString()
97+
attrs + ("_id" JsString(_id))
98+
} - "lastSyncDate" - "case" - "status" - "follow"
99+
}
68100
}
69101
}
70102

71103
class Alert(model: AlertModel, attributes: JsObject)
72104
extends EntityDef[AlertModel, Alert](model, attributes)
73105
with AlertAttributes {
74106

75-
override def artifactAttributes: Seq[Attribute[_]] = Nil
76-
77107
override def toJson: JsObject = super.toJson +
78108
("artifacts" JsArray(artifacts().map {
79109
// for file artifact, parse data as Json

thehive-backend/app/models/Artifact.scala

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@ package models
33
import java.util.Date
44
import javax.inject.{ Inject, Provider, Singleton }
55

6+
import akka.{ Done, NotUsed }
7+
68
import scala.concurrent.{ ExecutionContext, Future }
79
import scala.language.postfixOps
8-
import akka.stream.Materializer
9-
import play.api.libs.json.{ JsNull, JsObject, JsString, JsValue, JsArray }
10+
import akka.stream.{ IOResult, Materializer }
11+
import play.api.libs.json.{ JsArray, JsNull, JsObject, JsString, JsValue }
1012
import play.api.libs.json.JsLookupResult.jsLookupResultToJsLookup
1113
import play.api.libs.json.JsValue.jsValueToJsLookup
1214
import play.api.libs.json.Json
1315
import play.api.libs.json.Json.toJsFieldJsValueWrapper
14-
import org.elastic4play.BadRequestError
16+
import org.elastic4play.{ BadRequestError, InternalError }
1517
import org.elastic4play.models.{ AttributeDef, BaseEntity, ChildModelDef, EntityDef, HiveEnumeration, AttributeFormat F, AttributeOption O }
16-
import org.elastic4play.services.{ Attachment, DBLists }
18+
import org.elastic4play.services.{ Attachment, AttachmentSrv, DBLists }
1719
import org.elastic4play.utils.MultiHash
1820
import models.JsonFormat.artifactStatusFormat
21+
import play.api.Logger
1922
import services.{ ArtifactSrv, AuditedModel }
2023

24+
import scala.util.Success
25+
2126
object ArtifactStatus extends Enumeration with HiveEnumeration {
2227
type Type = Value
2328
val Ok, Deleted = Value
@@ -28,7 +33,7 @@ trait ArtifactAttributes { _: AttributeDef ⇒
2833
val artifactId: A[String] = attribute("_id", F.stringFmt, "Artifact id", O.model)
2934
val data: A[Option[String]] = optionalAttribute("data", F.stringFmt, "Content of the artifact", O.readonly)
3035
val dataType: A[String] = attribute("dataType", F.listEnumFmt("artifactDataType")(dblists), "Type of the artifact", O.readonly)
31-
val message: A[String] = attribute("message", F.textFmt, "Description of the artifact in the context of the case")
36+
val message: A[Option[String]] = optionalAttribute("message", F.textFmt, "Description of the artifact in the context of the case")
3237
val startDate: A[Date] = attribute("startDate", F.dateFmt, "Creation date", new Date)
3338
val attachment: A[Option[Attachment]] = optionalAttribute("attachment", F.attachmentFmt, "Artifact file content", O.readonly)
3439
val tlp: A[Long] = attribute("tlp", F.numberFmt, "TLP level", 2L)
@@ -42,37 +47,68 @@ trait ArtifactAttributes { _: AttributeDef ⇒
4247
class ArtifactModel @Inject() (
4348
caseModel: CaseModel,
4449
val dblists: DBLists,
50+
attachmentSrv: AttachmentSrv,
4551
artifactSrv: Provider[ArtifactSrv],
4652
implicit val mat: Materializer,
4753
implicit val ec: ExecutionContext) extends ChildModelDef[ArtifactModel, Artifact, CaseModel, Case](caseModel, "case_artifact") with ArtifactAttributes with AuditedModel {
54+
private[ArtifactModel] lazy val logger = Logger(getClass)
4855
override val removeAttribute: JsObject = Json.obj("status" ArtifactStatus.Deleted)
4956

50-
override def apply(attributes: JsObject) = {
57+
override def apply(attributes: JsObject): Artifact = {
5158
val tags = (attributes \ "tags").asOpt[Seq[JsString]].getOrElse(Nil).distinct
5259
new Artifact(this, attributes + ("tags" JsArray(tags)))
5360
}
5461

5562
// this method modify request in order to hash artifact and manager file upload
5663
override def creationHook(parent: Option[BaseEntity], attrs: JsObject): Future[JsObject] = {
5764
val keys = attrs.keys
65+
if (!keys.contains("message") && (attrs \ "tags").asOpt[Seq[JsValue]].forall(_.isEmpty))
66+
throw BadRequestError(s"Artifact must contain a message or on ore more tags")
5867
if (keys.contains("data") == keys.contains("attachment"))
5968
throw BadRequestError(s"Artifact must contain data or attachment (but not both)")
60-
computeId(parent, attrs).map { id
69+
computeId(parent.getOrElse(throw InternalError(s"artifact $attrs has no parent")), attrs).map { id
6170
attrs + ("_id" JsString(id))
6271
}
6372
}
6473

65-
def computeId(parent: Option[BaseEntity], attrs: JsObject): Future[String] = {
74+
override def updateHook(entity: BaseEntity, updateAttrs: JsObject): Future[JsObject] = {
75+
entity match {
76+
case artifact: Artifact
77+
val removeMessage = (updateAttrs \ "message").toOption.exists {
78+
case JsNull true
79+
case JsArray(Nil) true
80+
case _ false
81+
}
82+
val removeTags = (updateAttrs \ "tags").toOption.exists {
83+
case JsNull true
84+
case JsArray(Nil) true
85+
case _ false
86+
}
87+
if ((removeMessage && removeTags) ||
88+
(removeMessage && artifact.tags().isEmpty) ||
89+
(removeTags && artifact.message().isEmpty))
90+
Future.failed(BadRequestError(s"Artifact must contain a message or on ore more tags"))
91+
else
92+
Future.successful(updateAttrs)
93+
}
94+
}
95+
def computeId(parent: BaseEntity, attrs: JsObject): Future[String] = {
6696
// in order to make sure that there is no duplicated artifact, calculate its id from its content (dataType, data, attachment and parent)
6797
val mm = new MultiHash("MD5")
6898
mm.addValue((attrs \ "data").asOpt[JsValue].getOrElse(JsNull))
6999
mm.addValue((attrs \ "dataType").asOpt[JsValue].getOrElse(JsNull))
70-
(attrs \ "attachment" \ "filepath").asOpt[String]
71-
.fold(Future.successful(()))(file mm.addFile(file))
72-
.map { _
73-
mm.addValue(JsString(parent.fold("")(_.id)))
74-
mm.digest.toString
75-
}
100+
for {
101+
IOResult(_, done) (attrs \ "attachment" \ "filepath").asOpt[String]
102+
.fold(Future.successful(IOResult(0, Success(Done))))(file mm.addFile(file))
103+
_ Future.fromTry(done)
104+
_ (attrs \ "attachment" \ "id").asOpt[String]
105+
.fold(Future.successful(NotUsed: NotUsed)) { fileId
106+
mm.addFile(attachmentSrv.source(fileId))
107+
}
108+
} yield {
109+
mm.addValue(JsString(parent.id))
110+
mm.digest.toString
111+
}
76112
}
77113

78114
override def getStats(entity: BaseEntity): Future[JsObject] = {

0 commit comments

Comments
 (0)