From a221fc958d347a49925b98fe605b72c66b453a7b Mon Sep 17 00:00:00 2001 From: Zachary Priddy Date: Sun, 24 Mar 2019 21:30:44 -0700 Subject: [PATCH 01/58] Add Critical Status --- thehive-backend/app/models/Alert.scala | 2 +- thehive-backend/app/models/AttributeFormat.scala | 6 +++--- thehive-backend/app/models/Case.scala | 2 +- thehive-backend/app/models/CaseTemplate.scala | 2 +- ui/app/scripts/services/Constants.js | 3 ++- ui/app/views/directives/severity.html | 15 +++++++++------ ui/app/views/partials/alert/list/filters.html | 2 +- ui/app/views/partials/case/list/filters.html | 2 +- 8 files changed, 19 insertions(+), 15 deletions(-) diff --git a/thehive-backend/app/models/Alert.scala b/thehive-backend/app/models/Alert.scala index d2290749d3..26a3205bb9 100644 --- a/thehive-backend/app/models/Alert.scala +++ b/thehive-backend/app/models/Alert.scala @@ -55,7 +55,7 @@ trait AlertAttributes { val caze: A[Option[String]] = optionalAttribute("case", F.stringFmt, "Id of the case, if created") val title: A[String] = attribute("title", F.textFmt, "Title of the alert") val description: A[String] = attribute("description", F.textFmt, "Description of the alert") - val severity: A[Long] = attribute("severity", SeverityAttributeFormat, "Severity if the alert (0-3)", 2L) + val severity: A[Long] = attribute("severity", SeverityAttributeFormat, "Severity if the alert (1-4)", 2L) val tags: A[Seq[String]] = multiAttribute("tags", F.stringFmt, "Alert tags") val tlp: A[Long] = attribute("tlp", TlpAttributeFormat, "TLP level", 2L) val artifacts: A[Seq[JsObject]] = multiAttribute("artifacts", F.objectFmt(artifactAttributes), "Artifact of the alert", O.unaudited) diff --git a/thehive-backend/app/models/AttributeFormat.scala b/thehive-backend/app/models/AttributeFormat.scala index fd98a30140..a6d00d0df7 100644 --- a/thehive-backend/app/models/AttributeFormat.scala +++ b/thehive-backend/app/models/AttributeFormat.scala @@ -11,15 +11,15 @@ import org.elastic4play.{ AttributeError, InvalidFormatAttributeError } object SeverityAttributeFormat extends NumberAttributeFormat { - def isValidValue(value: Long): Boolean = 1 <= value && value <= 3 + def isValidValue(value: Long): Boolean = 1 <= value && value <= 4 override def definition(dblists: DBLists, attribute: Attribute[Long]): Seq[AttributeDefinition] = Seq(AttributeDefinition( attribute.attributeName, name, attribute.description, - Seq(JsNumber(1), JsNumber(2), JsNumber(3)), - Seq("low", "medium", "high"))) + Seq(JsNumber(1), JsNumber(2), JsNumber(3), JsNumber(4)), + Seq("low", "medium", "high", "critical"))) override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = { value match { diff --git a/thehive-backend/app/models/Case.scala b/thehive-backend/app/models/Case.scala index 2c2b4e3c01..5d58d4692a 100644 --- a/thehive-backend/app/models/Case.scala +++ b/thehive-backend/app/models/Case.scala @@ -37,7 +37,7 @@ trait CaseAttributes { _: AttributeDef ⇒ val caseId: A[Long] = attribute("caseId", F.numberFmt, "Id of the case (auto-generated)", O.model) val title: A[String] = attribute("title", F.textFmt, "Title of the case") val description: A[String] = attribute("description", F.textFmt, "Description of the case") - val severity: A[Long] = attribute("severity", SeverityAttributeFormat, "Severity if the case is an incident (0-3)", 2L) + val severity: A[Long] = attribute("severity", SeverityAttributeFormat, "Severity if the case is an incident (1-4)", 2L) val owner: A[String] = attribute("owner", F.userFmt, "Owner of the case") val startDate: A[Date] = attribute("startDate", F.dateFmt, "Creation date", new Date) val endDate: A[Option[Date]] = optionalAttribute("endDate", F.dateFmt, "Resolution date") diff --git a/thehive-backend/app/models/CaseTemplate.scala b/thehive-backend/app/models/CaseTemplate.scala index 144aa4fc61..ead7c77da7 100644 --- a/thehive-backend/app/models/CaseTemplate.scala +++ b/thehive-backend/app/models/CaseTemplate.scala @@ -19,7 +19,7 @@ trait CaseTemplateAttributes { _: AttributeDef ⇒ val templateName: A[String] = attribute("name", F.stringFmt, "Name of the template") val titlePrefix: A[Option[String]] = optionalAttribute("titlePrefix", F.textFmt, "Title of the case") val description: A[Option[String]] = optionalAttribute("description", F.textFmt, "Description of the case") - val severity: A[Option[Long]] = optionalAttribute("severity", SeverityAttributeFormat, "Severity if the case is an incident (0-5)") + val severity: A[Option[Long]] = optionalAttribute("severity", SeverityAttributeFormat, "Severity if the case is an incident (1-4)") val tags: A[Seq[String]] = multiAttribute("tags", F.stringFmt, "Case tags") val flag: A[Option[Boolean]] = optionalAttribute("flag", F.booleanFmt, "Flag of the case") val tlp: A[Option[Long]] = optionalAttribute("tlp", TlpAttributeFormat, "TLP level") diff --git a/ui/app/scripts/services/Constants.js b/ui/app/scripts/services/Constants.js index 0bae8587ac..9ae839dcb1 100644 --- a/ui/app/scripts/services/Constants.js +++ b/ui/app/scripts/services/Constants.js @@ -16,11 +16,12 @@ }) .value('Severity', { keys: { + Critical: 4, High: 3, Medium: 2, Low: 1 }, - values: ['Unknown', 'Low', 'Medium', 'High'] + values: ['Unknown', 'Low', 'Medium', 'High', 'Critical'] }) .value('AlertStatus', { values: ['New', 'Updated', 'Ignored', 'Imported'] diff --git a/ui/app/views/directives/severity.html b/ui/app/views/directives/severity.html index 193495b93a..74f74b3bbb 100644 --- a/ui/app/views/directives/severity.html +++ b/ui/app/views/directives/severity.html @@ -1,11 +1,14 @@
- L - M - H + L + M + H + !! +
- L - M - H + L + M + H + !! ? diff --git a/ui/app/views/partials/alert/list/filters.html b/ui/app/views/partials/alert/list/filters.html index 33c873611f..8f91ead91d 100644 --- a/ui/app/views/partials/alert/list/filters.html +++ b/ui/app/views/partials/alert/list/filters.html @@ -89,7 +89,7 @@

Filters

diff --git a/ui/app/views/partials/case/list/filters.html b/ui/app/views/partials/case/list/filters.html index 9566871ff9..a7208c5fc6 100644 --- a/ui/app/views/partials/case/list/filters.html +++ b/ui/app/views/partials/case/list/filters.html @@ -71,7 +71,7 @@

Filters

From b559d10e12a154fe2d480c3e599d9fc7cbbb74a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9arch?= Date: Tue, 3 Sep 2019 10:34:18 +0200 Subject: [PATCH 02/58] Support for retreiving user groups directly User groups are parsed directly from user infos. The config parameter `auth.sso.groups.url` is useless in that case. Note: an AuthenticationError is raised when no groups are available for the user. --- .../services/mappers/GroupUserMapper.scala | 73 +++++++++++++++++-- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/thehive-backend/app/services/mappers/GroupUserMapper.scala b/thehive-backend/app/services/mappers/GroupUserMapper.scala index 6aafb08965..cb75a04c32 100644 --- a/thehive-backend/app/services/mappers/GroupUserMapper.scala +++ b/thehive-backend/app/services/mappers/GroupUserMapper.scala @@ -3,8 +3,9 @@ package services.mappers import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} +import scala.util.parsing.combinator._ -import play.api.Configuration +import play.api.{Configuration, Logger} import play.api.libs.json._ import play.api.libs.ws.WSClient @@ -38,13 +39,73 @@ class GroupUserMapper( override val name: String = "group" + private[GroupUserMapper] lazy val logger = Logger(getClass) + + private class RoleListParser extends RegexParsers { + val str = "[a-zA-Z0-9_]+".r + val strSpc = "[a-zA-Z0-9_ ]+".r + val realStr = ("\""~>strSpc<~"\"" | "'"~>strSpc<~"'" | str) + + def expr: Parser[Seq[String]] = { + "[" ~ opt(realStr ~ rep(("," | ",") ~ realStr)) ~ "]" ^^ { + case _ ~ Some(firstRole ~ list) ~ _ => list.foldLeft(Seq(firstRole)) { + case (queue, _ ~ role) => role +: queue + } + case _ ~ _ => Seq.empty[String] + } | opt(realStr) ^^ { + case Some(role) => Seq(role) + case None => Seq.empty[String] + } + } + + def apply(input: String, logger: Logger): Seq[String] = parseAll(expr, input) match { + case Success(result, _) => result + case failure : NoSuccess => { + logger.error(failure.msg) + Seq.empty[String] + } + } + } + override def getUserFields(jsValue: JsValue, authHeader: Option[(String, String)]): Future[Fields] = { + if (groupsUrl == "") { + logger.debug(s"No groups endpoint provided (auth.sso.groups.url). Assuming groups were already received.") + + val jsonGroups: Seq[String] = { + val groups: JsValue = (jsValue \ groupAttrName).get + // Groups received as valid JSON array + if (groups.isInstanceOf[JsArray]) { + groups.as[Seq[String]] + // Group list received as string (invalid JSON, for example: "ROLE" or "['Role 1', ROLE2, 'Role_3']") + } else if (groups.isInstanceOf[JsString]) { + (new RoleListParser).apply(groups.as[String], logger) + // Invalid group list + } else { + logger.error(s"Invalid group list (${groupAttrName} attribute, value '${groups}', type ${groups.getClass})") + Seq.empty[String] + } + } + + mapGroupsAndBuildUserFields(jsValue, jsonGroups) + + } else { + val apiCall = authHeader.fold(ws.url(groupsUrl))(headers ⇒ ws.url(groupsUrl).addHttpHeaders(headers)) + apiCall.get.flatMap { r ⇒ + val jsonGroups = (r.json \ groupAttrName).as[Seq[String]] + mapGroupsAndBuildUserFields(jsValue, jsonGroups) + } + } + } + + private def mapGroupsAndBuildUserFields(jsValue: JsValue, jsonGroups: Seq[String]): Future[Fields] = { + val mappedRoles = jsonGroups.flatMap(mappings.get).flatten.toSet + val roles = if (mappedRoles.nonEmpty) mappedRoles else defaultRoles + + if (roles.isEmpty) { + Future.failed(AuthenticationError(s"No matched roles for user.")) - val apiCall = authHeader.fold(ws.url(groupsUrl))(headers ⇒ ws.url(groupsUrl).addHttpHeaders(headers)) - apiCall.get.flatMap { r ⇒ - val jsonGroups = (r.json \ groupAttrName).as[Seq[String]] - val mappedRoles = jsonGroups.flatMap(mappings.get).maxBy(_.length) - val roles = if (mappedRoles.nonEmpty) mappedRoles else defaultRoles + } else { + logger.debug(s"Computed roles : ${roles}") val fields = for { login ← (jsValue \ loginAttrName).validate[String] From 93c2495d760b8be51f122546a5f1f24939089fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9arch?= Date: Tue, 3 Sep 2019 10:40:31 +0200 Subject: [PATCH 03/58] Support for auto-updating SSO user --- thehive-backend/app/services/OAuth2Srv.scala | 21 +++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/thehive-backend/app/services/OAuth2Srv.scala b/thehive-backend/app/services/OAuth2Srv.scala index ea6664f0ef..af8ad47163 100644 --- a/thehive-backend/app/services/OAuth2Srv.scala +++ b/thehive-backend/app/services/OAuth2Srv.scala @@ -24,7 +24,8 @@ case class OAuth2Config( tokenUrl: String, userUrl: String, scope: String, - autocreate: Boolean + autocreate: Boolean, + autoupdate: Boolean ) object OAuth2Config { @@ -41,7 +42,8 @@ object OAuth2Config { tokenUrl ← configuration.getOptional[String]("auth.oauth2.tokenUrl") scope ← configuration.getOptional[String]("auth.oauth2.scope") autocreate = configuration.getOptional[Boolean]("auth.sso.autocreate").getOrElse(false) - } yield OAuth2Config(clientId, clientSecret, redirectUri, responseType, grantType, authorizationUrl, tokenUrl, userUrl, scope, autocreate) + autoupdate = configuration.getOptional[Boolean]("auth.sso.autoupdate").getOrElse(false) + } yield OAuth2Config(clientId, clientSecret, redirectUri, responseType, grantType, authorizationUrl, tokenUrl, userUrl, scope, autocreate, autoupdate) } @Singleton @@ -125,11 +127,24 @@ class OAuth2Srv( userSrv .get(userId) .flatMap(user ⇒ { - userSrv.getFromUser(request, user, name) + if (cfg.autoupdate) { + logger.debug(s"Updating OAuth/OIDC user") + userSrv.inInitAuthContext { implicit authContext ⇒ + // Only update name and roles, not login (can't change it) + userSrv + .update(user, userFields.unset("login")) + .flatMap(user ⇒ { + userSrv.getFromUser(request, user, name) + }) + } + } else { + userSrv.getFromUser(request, user, name) + } }) .recoverWith { case authErr: AuthorizationError ⇒ Future.failed(authErr) case _ if cfg.autocreate ⇒ + logger.debug(s"Creating OAuth/OIDC user") userSrv.inInitAuthContext { implicit authContext ⇒ userSrv .create(userFields) From 03f0137a3ee794f4e500916cf46d1823d7bb4df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9arch?= Date: Tue, 3 Sep 2019 10:42:19 +0200 Subject: [PATCH 04/58] Updated example and default config Default now respects OpenID Connect conventions. --- conf/application.sample | 146 ++++++++++++------ .../services/mappers/GroupUserMapper.scala | 6 +- .../services/mappers/SimpleUserMapper.scala | 4 +- 3 files changed, 103 insertions(+), 53 deletions(-) diff --git a/conf/application.sample b/conf/application.sample index b4c10083f9..64dd2303d5 100644 --- a/conf/application.sample +++ b/conf/application.sample @@ -45,57 +45,109 @@ search { # Authentication auth { - # "provider" parameter contains authentication provider. It can be multi-valued (useful for migration) - # available auth types are: - # services.LocalAuthSrv : passwords are stored in user entity (in Elasticsearch). No configuration is required. - # ad : use ActiveDirectory to authenticate users. Configuration is under "auth.ad" key - # ldap : use LDAP to authenticate users. Configuration is under "auth.ldap" key - provider = [local] + # "provider" parameter contains authentication provider. It can be multi-valued (useful for migration) + # available auth types are: + # services.LocalAuthSrv : passwords are stored in user entity (in Elasticsearch). No configuration is required. + # ad : use ActiveDirectory to authenticate users. Configuration is under "auth.ad" key + # ldap : use LDAP to authenticate users. Configuration is under "auth.ldap" key + # oauth2 : use OAuth/OIDC to authenticate users. Configuration is under "auth.oauth2" and "auth.sso" keys + provider = [local] # By default, basic authentication is disabled. You can enable it by setting "method.basic" to true. #method.basic = true - - ad { - # The Windows domain name in DNS format. This parameter is required if you do not use - # 'serverNames' below. - #domainFQDN = "mydomain.local" - - # Optionally you can specify the host names of the domain controllers instead of using 'domainFQDN - # above. If this parameter is not set, TheHive uses 'domainFQDN'. - #serverNames = [ad1.mydomain.local, ad2.mydomain.local] - - # The Windows domain name using short format. This parameter is required. - #domainName = "MYDOMAIN" - - # If 'true', use SSL to connect to the domain controller. - #useSSL = true - } - - ldap { - # The LDAP server name or address. The port can be specified using the 'host:port' - # syntax. This parameter is required if you don't use 'serverNames' below. - #serverName = "ldap.mydomain.local:389" - - # If you have multiple LDAP servers, use the multi-valued setting 'serverNames' instead. - #serverNames = [ldap1.mydomain.local, ldap2.mydomain.local] - - # Account to use to bind to the LDAP server. This parameter is required. - #bindDN = "cn=thehive,ou=services,dc=mydomain,dc=local" - - # Password of the binding account. This parameter is required. - #bindPW = "***secret*password***" - - # Base DN to search users. This parameter is required. - #baseDN = "ou=users,dc=mydomain,dc=local" - - # Filter to search user in the directory server. Please note that {0} is replaced - # by the actual user name. This parameter is required. - #filter = "(cn={0})" - - # If 'true', use SSL to connect to the LDAP directory server. - #useSSL = true - } + ad { + # The Windows domain name in DNS format. This parameter is required if you do not use + # 'serverNames' below. + #domainFQDN = "mydomain.local" + + # Optionally you can specify the host names of the domain controllers instead of using 'domainFQDN + # above. If this parameter is not set, TheHive uses 'domainFQDN'. + #serverNames = [ad1.mydomain.local, ad2.mydomain.local] + + # The Windows domain name using short format. This parameter is required. + #domainName = "MYDOMAIN" + + # If 'true', use SSL to connect to the domain controller. + #useSSL = true + } + + ldap { + # The LDAP server name or address. The port can be specified using the 'host:port' + # syntax. This parameter is required if you don't use 'serverNames' below. + #serverName = "ldap.mydomain.local:389" + + # If you have multiple LDAP servers, use the multi-valued setting 'serverNames' instead. + #serverNames = [ldap1.mydomain.local, ldap2.mydomain.local] + + # Account to use to bind to the LDAP server. This parameter is required. + #bindDN = "cn=thehive,ou=services,dc=mydomain,dc=local" + + # Password of the binding account. This parameter is required. + #bindPW = "***secret*password***" + + # Base DN to search users. This parameter is required. + #baseDN = "ou=users,dc=mydomain,dc=local" + + # Filter to search user in the directory server. Please note that {0} is replaced + # by the actual user name. This parameter is required. + #filter = "(cn={0})" + + # If 'true', use SSL to connect to the LDAP directory server. + #useSSL = true + } + + oauth2 { + # URL of the authorization server + #clientId = "client-id" + #clientSecret = "client-secret" + #redirectUri = "https://my-thehive-instance.example/index.html#!/login" + #responseType = "code" + #grantType = "authorization_code" + + # URL from where to get the access token + #authorizationUrl = "https://auth-site.com/OAuth/Authorize" + #tokenUrl = "https://auth-site.com/OAuth/Token" + + # The endpoint from which to obtain user details using the OAuth token, after successful login + #userUrl = "https://auth-site.com/api/User" + #scope = "openid profile" + } + + # Single-Sign On + sso { + # Autocreate user in database? + #autocreate = false + + # Autoupdate its profile and roles? + #autoupdate = false + + # Autologin user using SSO? + #autologin = false + # Attributes mappings + #attributes { + # login = "sub" + # name = "name" + # groups = "groups" + # #roles = "roles" + #} + + # Name of mapping class from user resource to backend user ('simple' or 'group') + #mapper = group + # Default roles for users with no groups mapped ("read", "write", "admin") + #defaultRoles = [] + + #groups { + # # URL to retreive groups (leave empty if you are using OIDC) + # #url = "https://auth-site.com/api/Groups" + # # Group mappings, you can have multiple roles for each group: they are merged + # mappings { + # admin-profile-name = ["admin"] + # editor-profile-name = ["write"] + # reader-profile-name = ["read"] + # } + #} + } } # Maximum time between two requests without requesting authentication diff --git a/thehive-backend/app/services/mappers/GroupUserMapper.scala b/thehive-backend/app/services/mappers/GroupUserMapper.scala index cb75a04c32..378e07ee2c 100644 --- a/thehive-backend/app/services/mappers/GroupUserMapper.scala +++ b/thehive-backend/app/services/mappers/GroupUserMapper.scala @@ -15,7 +15,6 @@ import org.elastic4play.controllers.Fields class GroupUserMapper( loginAttrName: String, nameAttrName: String, - rolesAttrName: Option[String], groupAttrName: String, defaultRoles: Seq[String], groupsUrl: String, @@ -26,9 +25,8 @@ class GroupUserMapper( @Inject() def this(configuration: Configuration, ws: WSClient, ec: ExecutionContext) = this( - configuration.getOptional[String]("auth.sso.attributes.login").getOrElse("name"), - configuration.getOptional[String]("auth.sso.attributes.name").getOrElse("username"), - configuration.getOptional[String]("auth.sso.attributes.roles"), + configuration.getOptional[String]("auth.sso.attributes.login").getOrElse("sub"), + configuration.getOptional[String]("auth.sso.attributes.name").getOrElse("name"), configuration.getOptional[String]("auth.sso.attributes.groups").getOrElse(""), configuration.getOptional[Seq[String]]("auth.sso.defaultRoles").getOrElse(Seq()), configuration.getOptional[String]("auth.sso.groups.url").getOrElse(""), diff --git a/thehive-backend/app/services/mappers/SimpleUserMapper.scala b/thehive-backend/app/services/mappers/SimpleUserMapper.scala index b549c9b0ee..323d6f9715 100644 --- a/thehive-backend/app/services/mappers/SimpleUserMapper.scala +++ b/thehive-backend/app/services/mappers/SimpleUserMapper.scala @@ -20,8 +20,8 @@ class SimpleUserMapper( @Inject() def this(configuration: Configuration, ec: ExecutionContext) = this( - configuration.getOptional[String]("auth.sso.attributes.login").getOrElse("name"), - configuration.getOptional[String]("auth.sso.attributes.name").getOrElse("username"), + configuration.getOptional[String]("auth.sso.attributes.login").getOrElse("sub"), + configuration.getOptional[String]("auth.sso.attributes.name").getOrElse("name"), configuration.getOptional[String]("auth.sso.attributes.roles"), configuration.getOptional[Seq[String]]("auth.sso.defaultRoles").getOrElse(Seq()), ec From fa9c778dc063e8e24868a1d352ffc5e14359ff73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9arch?= Date: Tue, 3 Sep 2019 10:45:17 +0200 Subject: [PATCH 05/58] Improved SSO login & autologin --- ui/app/scripts/app.js | 2 +- .../scripts/controllers/AuthenticationCtrl.js | 10 +++++++--- ui/app/scripts/controllers/RootCtrl.js | 4 ++-- ui/app/scripts/services/UtilsSrv.js | 17 ----------------- ui/app/views/login.html | 10 +++++----- 5 files changed, 15 insertions(+), 28 deletions(-) diff --git a/ui/app/scripts/app.js b/ui/app/scripts/app.js index e2873c001d..6fde274b02 100644 --- a/ui/app/scripts/app.js +++ b/ui/app/scripts/app.js @@ -58,7 +58,7 @@ angular.module('thehive', [ } }, params: { - autoLogin: false + disableSsoAutoLogin: false }, title: 'Login' }) diff --git a/ui/app/scripts/controllers/AuthenticationCtrl.js b/ui/app/scripts/controllers/AuthenticationCtrl.js index 4ba859caff..a54abf1bc4 100644 --- a/ui/app/scripts/controllers/AuthenticationCtrl.js +++ b/ui/app/scripts/controllers/AuthenticationCtrl.js @@ -4,18 +4,21 @@ (function() { 'use strict'; angular.module('theHiveControllers') - .controller('AuthenticationCtrl', function($scope, $state, $location, $uibModalStack, $stateParams, AuthenticationSrv, NotificationSrv, UtilsSrv, UrlParser, appConfig) { + .controller('AuthenticationCtrl', function($scope, $state, $location, $uibModalStack, $stateParams, AuthenticationSrv, NotificationSrv, appConfig) { $scope.params = {}; + $scope.ssoLogingIn = false; $uibModalStack.dismissAll(); $scope.ssoLogin = function (code) { + $scope.ssoLogingIn = true; AuthenticationSrv.ssoLogin(code) .then(function(response) { var redirectLocation = response.headers().location; if(angular.isDefined(redirectLocation)) { window.location = redirectLocation; } else { + $location.search('code', null); $state.go('app.cases'); } }) @@ -25,6 +28,7 @@ } else { NotificationSrv.log(err.data.message, 'error'); } + $scope.ssoLogingIn = false; $location.url($location.path()); }); }; @@ -49,8 +53,8 @@ }); }; - var code = UtilsSrv.extractQueryParam('code', UrlParser('query', $location.absUrl())); - if(angular.isDefined(code) || $stateParams.autoLogin) { + var code = $location.search().code; + if(angular.isDefined(code) || (appConfig.config.ssoAutoLogin && !$stateParams.disableSsoAutoLogin)) { $scope.ssoLogin(code); } }); diff --git a/ui/app/scripts/controllers/RootCtrl.js b/ui/app/scripts/controllers/RootCtrl.js index acbd718b83..737f53231a 100644 --- a/ui/app/scripts/controllers/RootCtrl.js +++ b/ui/app/scripts/controllers/RootCtrl.js @@ -9,7 +9,7 @@ angular.module('theHiveControllers').controller('RootCtrl', $state.go('maintenance'); return; }else if(!currentUser || !currentUser.id) { - $state.go('login', {autoLogin: appConfig.config.ssoAutoLogin }); + $state.go('login'); return; } @@ -141,7 +141,7 @@ angular.module('theHiveControllers').controller('RootCtrl', $scope.logout = function() { AuthenticationSrv.logout(function() { - $state.go('login'); + $state.go('login', {disableSsoAutoLogin: true}); }, function(data, status) { NotificationSrv.error('RootCtrl', data, status); }); diff --git a/ui/app/scripts/services/UtilsSrv.js b/ui/app/scripts/services/UtilsSrv.js index 5a4fdb4288..6322ab2cb9 100644 --- a/ui/app/scripts/services/UtilsSrv.js +++ b/ui/app/scripts/services/UtilsSrv.js @@ -101,23 +101,6 @@ scope.value = scope.oldValue; scope.updatable.updating = false; }; - }, - - extractQueryParam: function(paramName, queryString) { - if (!queryString || !paramName) { - return; - } - - var param = $location.search()[paramName]; - - if (param) { - return param; - } else { - var parsedQuery = _.find(queryString.split('&'), function(str) { - return str.startsWith(paramName + '='); - }); - return parsedQuery ? parsedQuery.substr(paramName.length + 1) : undefined; - } } }; diff --git a/ui/app/views/login.html b/ui/app/views/login.html index 0c30c09a84..18bc6d7729 100644 --- a/ui/app/views/login.html +++ b/ui/app/views/login.html @@ -6,25 +6,25 @@
- +
- +
- +
From 57c989f79d04c4bfa93231f3757304506ca63692 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Fri, 30 Aug 2019 15:04:57 +0200 Subject: [PATCH 21/58] #1071 Fix the notification message when exporting cases to MISP --- ui/app/scripts/controllers/case/CaseExportDialogCtrl.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/scripts/controllers/case/CaseExportDialogCtrl.js b/ui/app/scripts/controllers/case/CaseExportDialogCtrl.js index b86458f7f9..e6de0c147d 100644 --- a/ui/app/scripts/controllers/case/CaseExportDialogCtrl.js +++ b/ui/app/scripts/controllers/case/CaseExportDialogCtrl.js @@ -66,7 +66,7 @@ NotificationSrv.log('The case has been successfully exported, but '+ failure +' observable(s) failed', 'warning'); } else { - success = angular.isObject(response.data) ? 1 : response.data.length; + success = angular.isArray(response.data) ? response.data.length : 1 ; NotificationSrv.log('The case has been successfully exported with ' + success+ ' observable(s)', 'success'); $uibModalInstance.close(); } @@ -85,6 +85,6 @@ } self.loading = false; }); - } + }; }); })(); From ebf883c9dbd82bd5ff3d6cbea70a9b8c3e305236 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Fri, 30 Aug 2019 17:02:26 +0200 Subject: [PATCH 22/58] #1065 Add a Preview button to the case alerts list --- ui/app/scripts/controllers/SearchCtrl.js | 4 +++- .../controllers/alert/AlertEventCtrl.js | 3 ++- .../scripts/controllers/alert/AlertListCtrl.js | 3 ++- .../scripts/controllers/case/CaseAlertsCtrl.js | 18 ++++++++++++++++++ ui/app/views/partials/alert/event.dialog.html | 12 ++++++------ ui/app/views/partials/case/case.alerts.html | 6 ++++++ 6 files changed, 37 insertions(+), 9 deletions(-) diff --git a/ui/app/scripts/controllers/SearchCtrl.js b/ui/app/scripts/controllers/SearchCtrl.js index 4daf9305e7..27f760936b 100644 --- a/ui/app/scripts/controllers/SearchCtrl.js +++ b/ui/app/scripts/controllers/SearchCtrl.js @@ -51,7 +51,9 @@ event: event, templates: function() { return CaseTemplateSrv.list(); - } + }, + readonly: true, + isAdmin: false } }).result.then(function(/*response*/) { $scope.searchResults.update(); diff --git a/ui/app/scripts/controllers/alert/AlertEventCtrl.js b/ui/app/scripts/controllers/alert/AlertEventCtrl.js index cde6a84015..265440d64b 100644 --- a/ui/app/scripts/controllers/alert/AlertEventCtrl.js +++ b/ui/app/scripts/controllers/alert/AlertEventCtrl.js @@ -1,10 +1,11 @@ (function() { 'use strict'; angular.module('theHiveControllers') - .controller('AlertEventCtrl', function($scope, $rootScope, $state, $uibModal, $uibModalInstance, ModalUtilsSrv, CustomFieldsCacheSrv, CaseResolutionStatus, AlertingSrv, NotificationSrv, UiSettingsSrv, clipboard, event, templates, isAdmin) { + .controller('AlertEventCtrl', function($scope, $rootScope, $state, $uibModal, $uibModalInstance, ModalUtilsSrv, CustomFieldsCacheSrv, CaseResolutionStatus, AlertingSrv, NotificationSrv, UiSettingsSrv, clipboard, event, templates, isAdmin, readonly) { var self = this; var eventId = event.id; + self.readonly = readonly; self.isAdmin = isAdmin; self.templates = _.pluck(templates, 'name'); self.CaseResolutionStatus = CaseResolutionStatus; diff --git a/ui/app/scripts/controllers/alert/AlertListCtrl.js b/ui/app/scripts/controllers/alert/AlertListCtrl.js index 9d2e4836c3..83740c9e86 100755 --- a/ui/app/scripts/controllers/alert/AlertListCtrl.js +++ b/ui/app/scripts/controllers/alert/AlertListCtrl.js @@ -225,7 +225,8 @@ templates: function() { return CaseTemplateSrv.list(); }, - isAdmin: self.isAdmin + isAdmin: self.isAdmin, + readonly: false } }); }; diff --git a/ui/app/scripts/controllers/case/CaseAlertsCtrl.js b/ui/app/scripts/controllers/case/CaseAlertsCtrl.js index c379fbfc43..12aa928dfe 100644 --- a/ui/app/scripts/controllers/case/CaseAlertsCtrl.js +++ b/ui/app/scripts/controllers/case/CaseAlertsCtrl.js @@ -73,6 +73,24 @@ } }; + $scope.previewEvent = function(event) { + $uibModal.open({ + templateUrl: 'views/partials/alert/event.dialog.html', + controller: 'AlertEventCtrl', + controllerAs: 'dialog', + size: 'max', + resolve: { + event: event, + templates: function() { + //return CaseTemplateSrv.list(); + return []; + }, + isAdmin: false, + readonly: true + } + }); + }; + $scope.alertStats = $scope.initStats($scope.alerts); } ); diff --git a/ui/app/views/partials/alert/event.dialog.html b/ui/app/views/partials/alert/event.dialog.html index 92dd5e8b3a..60425d7aa5 100644 --- a/ui/app/views/partials/alert/event.dialog.html +++ b/ui/app/views/partials/alert/event.dialog.html @@ -143,31 +143,31 @@

- - - -
+
diff --git a/ui/app/views/partials/case/case.alerts.html b/ui/app/views/partials/case/case.alerts.html index 0c4c3b39d3..6ad492473f 100644 --- a/ui/app/views/partials/case/case.alerts.html +++ b/ui/app/views/partials/case/case.alerts.html @@ -91,6 +91,7 @@ + @@ -129,6 +130,11 @@ {{::event.artifacts.length || 0}} {{event.date | showDate}} + + + + + From ed884dcb0b5f940eac6fcf41b7fd69c56cb89529 Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Mon, 2 Sep 2019 16:50:55 +0200 Subject: [PATCH 23/58] #1061 Escape quotes from case list filters --- ui/app/scripts/controllers/case/CaseListCtrl.js | 4 ++-- ui/app/scripts/services/CasesUISrv.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/app/scripts/controllers/case/CaseListCtrl.js b/ui/app/scripts/controllers/case/CaseListCtrl.js index 0997c18634..933bb964f0 100644 --- a/ui/app/scripts/controllers/case/CaseListCtrl.js +++ b/ui/app/scripts/controllers/case/CaseListCtrl.js @@ -246,8 +246,8 @@ self.caseResponders = responders; }) .catch(function(err) { - NotificationSrv.error('CaseList', response.data, response.status); - }) + NotificationSrv.error('CaseList', err.data, err.status); + }); }; this.runResponder = function(responderId, responderName, caze) { diff --git a/ui/app/scripts/services/CasesUISrv.js b/ui/app/scripts/services/CasesUISrv.js index 20080a716f..cae968fdfc 100644 --- a/ui/app/scripts/services/CasesUISrv.js +++ b/ui/app/scripts/services/CasesUISrv.js @@ -165,10 +165,10 @@ // Prepare the filter value if (field === 'keyword') { - query = value; + query = value.replace(/"/gi, '\\"'); } else if (angular.isArray(value) && value.length > 0) { query = _.map(value, function(val) { - return field + ':"' + convertFn(val.text) + '"'; + return field + ':"' + convertFn(val.text.replace(/"/gi, '\\"')) + '"'; }).join(' OR '); query = '(' + query + ')'; } else if (filterDef.type === 'date') { @@ -178,7 +178,7 @@ query = field + ':[ ' + fromDate + ' TO ' + toDate + ' ]'; } else { - query = field + ':' + convertFn(value); + query = field + ':' + convertFn(value.replace(/"/gi, '\\"')); } factory.filters[field] = { From 34a6b5d51dd2602d6aeb2c73f3944956d688d41c Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 3 Sep 2019 11:35:10 +0200 Subject: [PATCH 24/58] #954 Add API to check if a custom field is used --- .../app/controllers/CustomFieldsCtrl.scala | 56 ++++ .../app/controllers/DBListCtrl.scala | 6 +- thehive-backend/conf/routes | 252 +++++++++--------- 3 files changed, 185 insertions(+), 129 deletions(-) create mode 100644 thehive-backend/app/controllers/CustomFieldsCtrl.scala diff --git a/thehive-backend/app/controllers/CustomFieldsCtrl.scala b/thehive-backend/app/controllers/CustomFieldsCtrl.scala new file mode 100644 index 0000000000..abb51ad995 --- /dev/null +++ b/thehive-backend/app/controllers/CustomFieldsCtrl.scala @@ -0,0 +1,56 @@ +package controllers + +import scala.concurrent.{ ExecutionContext, Future } + +import play.api.http.Status +import play.api.libs.json.{ JsNumber, JsObject } +import play.api.mvc.{ AbstractController, Action, AnyContent, ControllerComponents } + +import akka.stream.Materializer +import akka.stream.scaladsl.Sink +import com.sksamuel.elastic4s.http.ElasticDsl.search +import javax.inject.{ Inject, Singleton } +import models.Roles + +import org.elastic4play.NotFoundError +import org.elastic4play.controllers.Authenticated +import org.elastic4play.database.DBFind +import org.elastic4play.services.DBLists +import org.elastic4play.services.QueryDSL._ + +@Singleton +class CustomFieldsCtrl @Inject()( + authenticated: Authenticated, + dbfind: DBFind, + dblists: DBLists, + components: ControllerComponents, + implicit val ec: ExecutionContext, + implicit val mat: Materializer +) extends AbstractController(components) + with Status { + + def useCount(customField: String): Action[AnyContent] = + authenticated(Roles.read) + .async { + dblists("custom_fields") + .getItems[JsObject] + ._1 + .collect { + case (_, value) if (value \ "reference").asOpt[String].contains(customField) ⇒ (value \ "type").as[String] + } + .runWith(Sink.head) + .recoverWith { case _ ⇒ Future.failed(NotFoundError(s"CustomField $customField not found")) } + .flatMap { customFieldType ⇒ + val filter = and("relations" in ("case", "alert", "caseTemplate"), contains(s"customFields.$customField.$customFieldType")) + dbfind( + indexName ⇒ search(indexName).query(filter.query).size(0) + ).map { searchResponse ⇒ + Ok(JsNumber(searchResponse.totalHits)) + } + } + } + + /* +{"query":{"_and":[{"_not":{"_field":"customFields.cf1.string","_value":"ss"}},{"_not":{"status":"Deleted"}}]}} + */ +} diff --git a/thehive-backend/app/controllers/DBListCtrl.scala b/thehive-backend/app/controllers/DBListCtrl.scala index f1b1dbbc03..d4c6a68df3 100644 --- a/thehive-backend/app/controllers/DBListCtrl.scala +++ b/thehive-backend/app/controllers/DBListCtrl.scala @@ -1,14 +1,14 @@ -package org.elastic4play.controllers - -import javax.inject.{Inject, Singleton} +package controllers import scala.concurrent.{ExecutionContext, Future} import play.api.libs.json.{JsValue, Json} import play.api.mvc._ +import javax.inject.{Inject, Singleton} import models.Roles +import org.elastic4play.controllers.{Authenticated, Fields, FieldsBodyParser, Renderer} import org.elastic4play.services.DBLists import org.elastic4play.{MissingAttributeError, Timed} diff --git a/thehive-backend/conf/routes b/thehive-backend/conf/routes index 6d7bae50d6..f0acbc4687 100644 --- a/thehive-backend/conf/routes +++ b/thehive-backend/conf/routes @@ -1,126 +1,126 @@ -# Routes -# This file defines all application routes (Higher priority routes first) -# ~~~~ - -GET / controllers.Home.redirect -GET /api/status controllers.StatusCtrl.get -GET /api/health controllers.StatusCtrl.health -GET /api/logout controllers.AuthenticationCtrl.logout() -POST /api/login controllers.AuthenticationCtrl.login() -POST /api/ssoLogin controllers.AuthenticationCtrl.ssoLogin() - -POST /api/_search controllers.SearchCtrl.find() -POST /api/_stats controllers.SearchCtrl.stats() - -GET /api/case controllers.CaseCtrl.find() -POST /api/case/_search controllers.CaseCtrl.find() -PATCH /api/case/_bulk controllers.CaseCtrl.bulkUpdate() -POST /api/case/_stats controllers.CaseCtrl.stats() -POST /api/case controllers.CaseCtrl.create() -GET /api/case/:caseId controllers.CaseCtrl.get(caseId) -PATCH /api/case/:caseId controllers.CaseCtrl.update(caseId) -DELETE /api/case/:caseId controllers.CaseCtrl.delete(caseId) -DELETE /api/case/:caseId/force controllers.CaseCtrl.realDelete(caseId) -GET /api/case/:caseId/links controllers.CaseCtrl.linkedCases(caseId) -POST /api/case/:caseId1/_merge/:caseId2 controllers.CaseCtrl.merge(caseId1, caseId2) - -POST /api/case/template/_search controllers.CaseTemplateCtrl.find() -POST /api/case/template controllers.CaseTemplateCtrl.create() -GET /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.get(caseTemplateId) -PATCH /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.update(caseTemplateId) -DELETE /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.delete(caseTemplateId) - -POST /api/case/artifact/_search controllers.ArtifactCtrl.find() -POST /api/case/:caseId/artifact/_search controllers.ArtifactCtrl.findInCase(caseId) -POST /api/case/artifact/_stats controllers.ArtifactCtrl.stats() -POST /api/case/:caseId/artifact controllers.ArtifactCtrl.create(caseId) -GET /api/case/artifact/:artifactId controllers.ArtifactCtrl.get(artifactId) -DELETE /api/case/artifact/:artifactId controllers.ArtifactCtrl.delete(artifactId) -PATCH /api/case/artifact/_bulk controllers.ArtifactCtrl.bulkUpdate() -PATCH /api/case/artifact/:artifactId controllers.ArtifactCtrl.update(artifactId) -GET /api/case/artifact/:artifactId/similar controllers.ArtifactCtrl.findSimilar(artifactId) - -POST /api/case/:caseId/task/_search controllers.TaskCtrl.findInCase(caseId) -POST /api/case/task/_search controllers.TaskCtrl.find() -POST /api/case/task/_stats controllers.TaskCtrl.stats() -GET /api/case/task/:taskId controllers.TaskCtrl.get(taskId) -PATCH /api/case/task/:taskId controllers.TaskCtrl.update(taskId) -POST /api/case/:caseId/task controllers.TaskCtrl.create(caseId) - -GET /api/case/task/:taskId/log controllers.LogCtrl.findInTask(taskId) -POST /api/case/task/:taskId/log/_search controllers.LogCtrl.findInTask(taskId) -POST /api/case/task/log/_search controllers.LogCtrl.find() -POST /api/case/task/log/_stats controllers.LogCtrl.stats() -POST /api/case/task/:taskId/log controllers.LogCtrl.create(taskId) -PATCH /api/case/task/log/:logId controllers.LogCtrl.update(logId) -DELETE /api/case/task/log/:logId controllers.LogCtrl.delete(logId) -GET /api/case/task/log/:logId controllers.LogCtrl.get(logId) - -GET /api/alert controllers.AlertCtrl.find() -POST /api/alert/_search controllers.AlertCtrl.find() -PATCH /api/alert/_bulk controllers.AlertCtrl.bulkUpdate() -POST /api/alert/_stats controllers.AlertCtrl.stats() -GET /api/alert/_fixStatus controllers.AlertCtrl.fixStatus() -POST /api/alert controllers.AlertCtrl.create() -GET /api/alert/:alertId controllers.AlertCtrl.get(alertId) -PATCH /api/alert/:alertId controllers.AlertCtrl.update(alertId) -DELETE /api/alert/:alertId controllers.AlertCtrl.delete(alertId, force: Option[Boolean]) -POST /api/alert/:alertId/markAsRead controllers.AlertCtrl.markAsRead(alertId) -POST /api/alert/:alertId/markAsUnread controllers.AlertCtrl.markAsUnread(alertId) -POST /api/alert/:alertId/createCase controllers.AlertCtrl.createCase(alertId) -POST /api/alert/:alertId/follow controllers.AlertCtrl.followAlert(alertId) -POST /api/alert/:alertId/unfollow controllers.AlertCtrl.unfollowAlert(alertId) -POST /api/alert/:alertId/merge/:caseId controllers.AlertCtrl.mergeWithCase(alertId, caseId) -POST /api/alert/merge/_bulk controllers.AlertCtrl.bulkMergeWithCase() -POST /api/alert/delete/_bulk controllers.AlertCtrl.bulkDelete() - -GET /api/flow controllers.AuditCtrl.flow(rootId: Option[String], count: Option[Int]) -GET /api/audit controllers.AuditCtrl.find() -POST /api/audit/_search controllers.AuditCtrl.find() -POST /api/audit/_stats controllers.AuditCtrl.stats() - -GET /api/datastore/:hash controllers.AttachmentCtrl.download(hash, name: Option[String]) -GET /api/datastorezip/:hash controllers.AttachmentCtrl.downloadZip(hash, name: Option[String]) - -POST /api/maintenance/migrate org.elastic4play.controllers.MigrationCtrl.migrate -#POST /api/maintenance/rehash controllers.MaintenanceCtrl.reHash - -GET /api/list org.elastic4play.controllers.DBListCtrl.list() -DELETE /api/list/:itemId org.elastic4play.controllers.DBListCtrl.deleteItem(itemId) -PATCH /api/list/:itemId org.elastic4play.controllers.DBListCtrl.updateItem(itemId) -POST /api/list/:listName org.elastic4play.controllers.DBListCtrl.addItem(listName) -GET /api/list/:listName org.elastic4play.controllers.DBListCtrl.listItems(listName) -POST /api/list/:listName/_exists org.elastic4play.controllers.DBListCtrl.itemExists(listName) - - -GET /api/user/current controllers.UserCtrl.currentUser() -POST /api/user/_search controllers.UserCtrl.find() -POST /api/user controllers.UserCtrl.create() -GET /api/user/:userId controllers.UserCtrl.get(userId) -DELETE /api/user/:userId controllers.UserCtrl.delete(userId) -PATCH /api/user/:userId controllers.UserCtrl.update(userId) -POST /api/user/:userId/password/set controllers.UserCtrl.setPassword(userId) -POST /api/user/:userId/password/change controllers.UserCtrl.changePassword(userId) -GET /api/user/:userId/key controllers.UserCtrl.getKey(userId) -DELETE /api/user/:userId/key controllers.UserCtrl.removeKey(userId) -POST /api/user/:userId/key/renew controllers.UserCtrl.renewKey(userId) - - -POST /api/stream controllers.StreamCtrl.create() -GET /api/stream/status controllers.StreamCtrl.status -GET /api/stream/:streamId controllers.StreamCtrl.get(streamId) - -GET /api/describe/_all controllers.DescribeCtrl.describeAll -GET /api/describe/:modelName controllers.DescribeCtrl.describe(modelName) - -GET /api/dashboard controllers.DashboardCtrl.find() -POST /api/dashboard/_search controllers.DashboardCtrl.find() -POST /api/dashboard/_stats controllers.DashboardCtrl.stats() -POST /api/dashboard controllers.DashboardCtrl.create() -GET /api/dashboard/:dashboardId controllers.DashboardCtrl.get(dashboardId) -PATCH /api/dashboard/:dashboardId controllers.DashboardCtrl.update(dashboardId) -DELETE /api/dashboard/:dashboardId controllers.DashboardCtrl.delete(dashboardId) - --> /api/connector connectors.ConnectorRouter - -GET /*file controllers.AssetCtrl.get(file) +# Routes +# This file defines all application routes (Higher priority routes first) +# ~~~~ + +GET / controllers.Home.redirect +GET /api/status controllers.StatusCtrl.get +GET /api/health controllers.StatusCtrl.health +GET /api/logout controllers.AuthenticationCtrl.logout() +POST /api/login controllers.AuthenticationCtrl.login() +POST /api/ssoLogin controllers.AuthenticationCtrl.ssoLogin() + +POST /api/_search controllers.SearchCtrl.find() +POST /api/_stats controllers.SearchCtrl.stats() + +GET /api/case controllers.CaseCtrl.find() +POST /api/case/_search controllers.CaseCtrl.find() +PATCH /api/case/_bulk controllers.CaseCtrl.bulkUpdate() +POST /api/case/_stats controllers.CaseCtrl.stats() +POST /api/case controllers.CaseCtrl.create() +GET /api/case/:caseId controllers.CaseCtrl.get(caseId) +PATCH /api/case/:caseId controllers.CaseCtrl.update(caseId) +DELETE /api/case/:caseId controllers.CaseCtrl.delete(caseId) +DELETE /api/case/:caseId/force controllers.CaseCtrl.realDelete(caseId) +GET /api/case/:caseId/links controllers.CaseCtrl.linkedCases(caseId) +POST /api/case/:caseId1/_merge/:caseId2 controllers.CaseCtrl.merge(caseId1, caseId2) + +POST /api/case/template/_search controllers.CaseTemplateCtrl.find() +POST /api/case/template controllers.CaseTemplateCtrl.create() +GET /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.get(caseTemplateId) +PATCH /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.update(caseTemplateId) +DELETE /api/case/template/:caseTemplateId controllers.CaseTemplateCtrl.delete(caseTemplateId) + +POST /api/case/artifact/_search controllers.ArtifactCtrl.find() +POST /api/case/:caseId/artifact/_search controllers.ArtifactCtrl.findInCase(caseId) +POST /api/case/artifact/_stats controllers.ArtifactCtrl.stats() +POST /api/case/:caseId/artifact controllers.ArtifactCtrl.create(caseId) +GET /api/case/artifact/:artifactId controllers.ArtifactCtrl.get(artifactId) +DELETE /api/case/artifact/:artifactId controllers.ArtifactCtrl.delete(artifactId) +PATCH /api/case/artifact/_bulk controllers.ArtifactCtrl.bulkUpdate() +PATCH /api/case/artifact/:artifactId controllers.ArtifactCtrl.update(artifactId) +GET /api/case/artifact/:artifactId/similar controllers.ArtifactCtrl.findSimilar(artifactId) + +POST /api/case/:caseId/task/_search controllers.TaskCtrl.findInCase(caseId) +POST /api/case/task/_search controllers.TaskCtrl.find() +POST /api/case/task/_stats controllers.TaskCtrl.stats() +GET /api/case/task/:taskId controllers.TaskCtrl.get(taskId) +PATCH /api/case/task/:taskId controllers.TaskCtrl.update(taskId) +POST /api/case/:caseId/task controllers.TaskCtrl.create(caseId) + +GET /api/case/task/:taskId/log controllers.LogCtrl.findInTask(taskId) +POST /api/case/task/:taskId/log/_search controllers.LogCtrl.findInTask(taskId) +POST /api/case/task/log/_search controllers.LogCtrl.find() +POST /api/case/task/log/_stats controllers.LogCtrl.stats() +POST /api/case/task/:taskId/log controllers.LogCtrl.create(taskId) +PATCH /api/case/task/log/:logId controllers.LogCtrl.update(logId) +DELETE /api/case/task/log/:logId controllers.LogCtrl.delete(logId) +GET /api/case/task/log/:logId controllers.LogCtrl.get(logId) + +GET /api/alert controllers.AlertCtrl.find() +POST /api/alert/_search controllers.AlertCtrl.find() +PATCH /api/alert/_bulk controllers.AlertCtrl.bulkUpdate() +POST /api/alert/_stats controllers.AlertCtrl.stats() +GET /api/alert/_fixStatus controllers.AlertCtrl.fixStatus() +POST /api/alert controllers.AlertCtrl.create() +GET /api/alert/:alertId controllers.AlertCtrl.get(alertId) +PATCH /api/alert/:alertId controllers.AlertCtrl.update(alertId) +DELETE /api/alert/:alertId controllers.AlertCtrl.delete(alertId, force: Option[Boolean]) +POST /api/alert/:alertId/markAsRead controllers.AlertCtrl.markAsRead(alertId) +POST /api/alert/:alertId/markAsUnread controllers.AlertCtrl.markAsUnread(alertId) +POST /api/alert/:alertId/createCase controllers.AlertCtrl.createCase(alertId) +POST /api/alert/:alertId/follow controllers.AlertCtrl.followAlert(alertId) +POST /api/alert/:alertId/unfollow controllers.AlertCtrl.unfollowAlert(alertId) +POST /api/alert/:alertId/merge/:caseId controllers.AlertCtrl.mergeWithCase(alertId, caseId) +POST /api/alert/merge/_bulk controllers.AlertCtrl.bulkMergeWithCase() +POST /api/alert/delete/_bulk controllers.AlertCtrl.bulkDelete() + +GET /api/flow controllers.AuditCtrl.flow(rootId: Option[String], count: Option[Int]) +GET /api/audit controllers.AuditCtrl.find() +POST /api/audit/_search controllers.AuditCtrl.find() +POST /api/audit/_stats controllers.AuditCtrl.stats() + +GET /api/datastore/:hash controllers.AttachmentCtrl.download(hash, name: Option[String]) +GET /api/datastorezip/:hash controllers.AttachmentCtrl.downloadZip(hash, name: Option[String]) + +POST /api/maintenance/migrate org.elastic4play.controllers.MigrationCtrl.migrate +#POST /api/maintenance/rehash controllers.MaintenanceCtrl.reHash + +GET /api/list controllers.DBListCtrl.list() +DELETE /api/list/:itemId controllers.DBListCtrl.deleteItem(itemId) +PATCH /api/list/:itemId controllers.DBListCtrl.updateItem(itemId) +POST /api/list/:listName controllers.DBListCtrl.addItem(listName) +GET /api/list/:listName controllers.DBListCtrl.listItems(listName) +POST /api/list/:listName/_exists controllers.DBListCtrl.itemExists(listName) +GET /api/customFields/:name controllers.CustomFieldsCtrl.useCount(name) + +GET /api/user/current controllers.UserCtrl.currentUser() +POST /api/user/_search controllers.UserCtrl.find() +POST /api/user controllers.UserCtrl.create() +GET /api/user/:userId controllers.UserCtrl.get(userId) +DELETE /api/user/:userId controllers.UserCtrl.delete(userId) +PATCH /api/user/:userId controllers.UserCtrl.update(userId) +POST /api/user/:userId/password/set controllers.UserCtrl.setPassword(userId) +POST /api/user/:userId/password/change controllers.UserCtrl.changePassword(userId) +GET /api/user/:userId/key controllers.UserCtrl.getKey(userId) +DELETE /api/user/:userId/key controllers.UserCtrl.removeKey(userId) +POST /api/user/:userId/key/renew controllers.UserCtrl.renewKey(userId) + + +POST /api/stream controllers.StreamCtrl.create() +GET /api/stream/status controllers.StreamCtrl.status +GET /api/stream/:streamId controllers.StreamCtrl.get(streamId) + +GET /api/describe/_all controllers.DescribeCtrl.describeAll +GET /api/describe/:modelName controllers.DescribeCtrl.describe(modelName) + +GET /api/dashboard controllers.DashboardCtrl.find() +POST /api/dashboard/_search controllers.DashboardCtrl.find() +POST /api/dashboard/_stats controllers.DashboardCtrl.stats() +POST /api/dashboard controllers.DashboardCtrl.create() +GET /api/dashboard/:dashboardId controllers.DashboardCtrl.get(dashboardId) +PATCH /api/dashboard/:dashboardId controllers.DashboardCtrl.update(dashboardId) +DELETE /api/dashboard/:dashboardId controllers.DashboardCtrl.delete(dashboardId) + +-> /api/connector connectors.ConnectorRouter + +GET /*file controllers.AssetCtrl.get(file) From 73b84bab75e5b8a19203d411b16529612711757a Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 3 Sep 2019 11:35:44 +0200 Subject: [PATCH 25/58] Update Play --- project/Dependencies.scala | 4 ++-- project/plugins.sbt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c2ec9c232f..b8ccb66f23 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -21,7 +21,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.11.5-SNAPSHOT" - val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % "2.5.19" - val akkaClusterTools = "com.typesafe.akka" %% "akka-cluster-tools" % "2.5.19" + val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % "2.5.21" + val akkaClusterTools = "com.typesafe.akka" %% "akka-cluster-tools" % "2.5.21" } } diff --git a/project/plugins.sbt b/project/plugins.sbt index c037cfab7e..c8ec87a869 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ // Comment to get more information during initialization logLevel := Level.Info -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.22") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.23") addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0") From 04fd724fe511129dc1f23766a7d5e800ec72e095 Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 3 Sep 2019 11:36:31 +0200 Subject: [PATCH 26/58] Fix MISP attribute deduplication when exporting a case --- .../app/connectors/misp/MispExport.scala | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/thehive-misp/app/connectors/misp/MispExport.scala b/thehive-misp/app/connectors/misp/MispExport.scala index 4e0fe2d44c..3104c06f76 100644 --- a/thehive-misp/app/connectors/misp/MispExport.scala +++ b/thehive-misp/app/connectors/misp/MispExport.scala @@ -51,17 +51,16 @@ class MispExport @Inject()( } def removeDuplicateAttributes(attributes: Seq[ExportedMispAttribute]): Seq[ExportedMispAttribute] = { - val attrIndex = attributes.zipWithIndex - - attrIndex - .filter { - case (ExportedMispAttribute(_, category, tpe, _, value, _), index) ⇒ - attrIndex.exists { - case (ExportedMispAttribute(_, `category`, `tpe`, _, `value`, _), otherIndex) ⇒ otherIndex >= index - case _ ⇒ true - } + var attrSet = Set.empty[(String, String, String)] + val builder = Seq.newBuilder[ExportedMispAttribute] + attributes.foreach { attr ⇒ + val tuple = (attr.category, attr.tpe, attr.value.fold(identity, _.name)) + if (!attrSet.contains(tuple)) { + builder += attr + attrSet += tuple } - .map(_._1) + } + builder.result() } def createEvent( From 6aebecee4af4ed2ab1a5e0d98263b14c3ab9dd2f Mon Sep 17 00:00:00 2001 From: To-om Date: Tue, 3 Sep 2019 14:58:20 +0200 Subject: [PATCH 27/58] #954 Add more details on custom fields use --- .../app/controllers/CustomFieldsCtrl.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/thehive-backend/app/controllers/CustomFieldsCtrl.scala b/thehive-backend/app/controllers/CustomFieldsCtrl.scala index abb51ad995..097b92800b 100644 --- a/thehive-backend/app/controllers/CustomFieldsCtrl.scala +++ b/thehive-backend/app/controllers/CustomFieldsCtrl.scala @@ -8,7 +8,7 @@ import play.api.mvc.{ AbstractController, Action, AnyContent, ControllerComponen import akka.stream.Materializer import akka.stream.scaladsl.Sink -import com.sksamuel.elastic4s.http.ElasticDsl.search +import com.sksamuel.elastic4s.http.ElasticDsl.{ search, termsAggregation } import javax.inject.{ Inject, Singleton } import models.Roles @@ -43,14 +43,14 @@ class CustomFieldsCtrl @Inject()( .flatMap { customFieldType ⇒ val filter = and("relations" in ("case", "alert", "caseTemplate"), contains(s"customFields.$customField.$customFieldType")) dbfind( - indexName ⇒ search(indexName).query(filter.query).size(0) + indexName ⇒ search(indexName).query(filter.query).aggregations(termsAggregation("t").field("relations")) ).map { searchResponse ⇒ - Ok(JsNumber(searchResponse.totalHits)) - } + val result = JsObject(searchResponse.aggregations.terms("t").buckets.map { b ⇒ + b.key → JsNumber(b.docCount) + }) + Ok(result) + } + .recover { case _ ⇒ Ok(JsObject.empty) } } } - - /* -{"query":{"_and":[{"_not":{"_field":"customFields.cf1.string","_value":"ss"}},{"_not":{"status":"Deleted"}}]}} - */ } From fb941c0481c94d3aac8d9a191d71a2974f38fdcc Mon Sep 17 00:00:00 2001 From: To-om Date: Wed, 4 Sep 2019 08:58:45 +0200 Subject: [PATCH 28/58] #954 Add total in custom field use count --- .../app/controllers/AlertCtrl.scala | 9 ++++++--- .../app/controllers/CustomFieldsCtrl.scala | 20 +++++++++---------- .../app/controllers/StatusCtrl.scala | 4 ++-- .../app/controllers/StreamCtrl.scala | 9 +++++---- thehive-backend/app/services/AlertSrv.scala | 20 +++++++++---------- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/thehive-backend/app/controllers/AlertCtrl.scala b/thehive-backend/app/controllers/AlertCtrl.scala index 38a1ee4af6..9e87c3fb0e 100644 --- a/thehive-backend/app/controllers/AlertCtrl.scala +++ b/thehive-backend/app/controllers/AlertCtrl.scala @@ -128,8 +128,9 @@ class AlertCtrl @Inject()( @Timed def bulkDelete(): Action[Fields] = authenticated(Roles.admin).async(fieldsBodyParser) { implicit request ⇒ request.body.getStrings("ids").fold(Future.successful(NoContent)) { ids ⇒ - Future.traverse(ids)(alertSrv.delete(_, request.body.getBoolean("force").getOrElse(false))) - .map(_ => NoContent) + Future + .traverse(ids)(alertSrv.delete(_, request.body.getBoolean("force").getOrElse(false))) + .map(_ ⇒ NoContent) } } @@ -180,7 +181,9 @@ class AlertCtrl @Inject()( def createCase(id: String): Action[Fields] = authenticated(Roles.write).async(fieldsBodyParser) { implicit request ⇒ for { alert ← alertSrv.get(id) - customCaseTemplate = request.body.getString("caseTemplate") + customCaseTemplate = request + .body + .getString("caseTemplate") .orElse(alert.caseTemplate()) caze ← alertSrv.createCase(alert, customCaseTemplate) } yield renderer.toOutput(CREATED, caze) diff --git a/thehive-backend/app/controllers/CustomFieldsCtrl.scala b/thehive-backend/app/controllers/CustomFieldsCtrl.scala index 097b92800b..a5a7b38b35 100644 --- a/thehive-backend/app/controllers/CustomFieldsCtrl.scala +++ b/thehive-backend/app/controllers/CustomFieldsCtrl.scala @@ -1,15 +1,15 @@ package controllers -import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.{ExecutionContext, Future} import play.api.http.Status -import play.api.libs.json.{ JsNumber, JsObject } -import play.api.mvc.{ AbstractController, Action, AnyContent, ControllerComponents } +import play.api.libs.json.{JsNumber, JsObject, Json} +import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents} import akka.stream.Materializer import akka.stream.scaladsl.Sink -import com.sksamuel.elastic4s.http.ElasticDsl.{ search, termsAggregation } -import javax.inject.{ Inject, Singleton } +import com.sksamuel.elastic4s.http.ElasticDsl.{search, termsAggregation} +import javax.inject.{Inject, Singleton} import models.Roles import org.elastic4play.NotFoundError @@ -45,12 +45,12 @@ class CustomFieldsCtrl @Inject()( dbfind( indexName ⇒ search(indexName).query(filter.query).aggregations(termsAggregation("t").field("relations")) ).map { searchResponse ⇒ - val result = JsObject(searchResponse.aggregations.terms("t").buckets.map { b ⇒ - b.key → JsNumber(b.docCount) - }) - Ok(result) + val buckets = searchResponse.aggregations.terms("t").buckets + val total = buckets.map(_.docCount).sum + val result = buckets.map(b ⇒ b.key → JsNumber(b.docCount)) :+ ("total" → JsNumber(total)) + Ok(JsObject(result)) } - .recover { case _ ⇒ Ok(JsObject.empty) } + .recover { case _ ⇒ Ok(Json.obj("total" → 0)) } } } } diff --git a/thehive-backend/app/controllers/StatusCtrl.scala b/thehive-backend/app/controllers/StatusCtrl.scala index 4466a847ec..08ee2f7145 100644 --- a/thehive-backend/app/controllers/StatusCtrl.scala +++ b/thehive-backend/app/controllers/StatusCtrl.scala @@ -33,8 +33,8 @@ class StatusCtrl @Inject()( ) extends AbstractController(components) { private[controllers] def getVersion(c: Class[_]) = Option(c.getPackage.getImplementationVersion).getOrElse("SNAPSHOT") - private var clusterStatusName: String = "Init" - val checkStatusInterval: FiniteDuration = configuration.getOptional[FiniteDuration]("statusCheckInterval").getOrElse(1.minute) + private var clusterStatusName: String = "Init" + val checkStatusInterval: FiniteDuration = configuration.getOptional[FiniteDuration]("statusCheckInterval").getOrElse(1.minute) private def updateStatus(): Unit = { clusterStatusName = Try(dbIndex.clusterStatusName).getOrElse("ERROR") system.scheduler.scheduleOnce(checkStatusInterval)(updateStatus()) diff --git a/thehive-backend/app/controllers/StreamCtrl.scala b/thehive-backend/app/controllers/StreamCtrl.scala index ed4863b3cd..7f3ca03a27 100644 --- a/thehive-backend/app/controllers/StreamCtrl.scala +++ b/thehive-backend/app/controllers/StreamCtrl.scala @@ -23,7 +23,7 @@ import services.StreamActor import services.StreamActor.StreamMessages import org.elastic4play.controllers._ -import org.elastic4play.services.{ AuxSrv, EventSrv, MigrationSrv, UserSrv } +import org.elastic4play.services.{AuxSrv, EventSrv, MigrationSrv, UserSrv} import org.elastic4play.Timed @Singleton @@ -99,9 +99,10 @@ class StreamCtrl( Future.successful(BadRequest("Invalid stream id")) } else { val futureStatus = authenticated.expirationStatus(request) match { - case ExpirationError if !migrationSrv.isMigrating ⇒ userSrv.getInitialUser(request).recoverWith { case _ => authenticated.getFromApiKey(request)}.map(_ ⇒ OK) - case _: ExpirationWarning ⇒ Future.successful(220) - case _ ⇒ Future.successful(OK) + case ExpirationError if !migrationSrv.isMigrating ⇒ + userSrv.getInitialUser(request).recoverWith { case _ ⇒ authenticated.getFromApiKey(request) }.map(_ ⇒ OK) + case _: ExpirationWarning ⇒ Future.successful(220) + case _ ⇒ Future.successful(OK) } // Check if stream actor exists diff --git a/thehive-backend/app/services/AlertSrv.scala b/thehive-backend/app/services/AlertSrv.scala index 6a6ea09102..4b367c3ca0 100644 --- a/thehive-backend/app/services/AlertSrv.scala +++ b/thehive-backend/app/services/AlertSrv.scala @@ -3,27 +3,27 @@ package services import java.nio.file.Files import scala.collection.immutable -import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.{ExecutionContext, Future} import scala.util.matching.Regex -import scala.util.{ Failure, Success, Try } +import scala.util.{Failure, Success, Try} import play.api.libs.json._ -import play.api.{ Configuration, Logger } +import play.api.{Configuration, Logger} import akka.NotUsed import akka.stream.Materializer -import akka.stream.scaladsl.{ Sink, Source } +import akka.stream.scaladsl.{Sink, Source} import connectors.ConnectorRouter -import javax.inject.{ Inject, Singleton } +import javax.inject.{Inject, Singleton} import models._ -import org.elastic4play.controllers.{ Fields, FileInputValue } +import org.elastic4play.controllers.{Fields, FileInputValue} import org.elastic4play.database.ModifyConfig import org.elastic4play.services.JsonFormat.attachmentFormat -import org.elastic4play.services.QueryDSL.{ groupByField, parent, selectCount, withId } +import org.elastic4play.services.QueryDSL.{groupByField, parent, selectCount, withId} import org.elastic4play.services._ import org.elastic4play.utils.Collection -import org.elastic4play.{ ConflictError, InternalError } +import org.elastic4play.{ConflictError, InternalError} trait AlertTransformer { def createCase(alert: Alert, customCaseTemplate: Option[String])(implicit authContext: AuthContext): Future[Case] @@ -312,7 +312,7 @@ class AlertSrv( .create(caze, artifactsFields) .flatMap { artifacts ⇒ Future.traverse(artifacts) { - case Success(_) => Future.successful(()) + case Success(_) ⇒ Future.successful(()) case Failure(ConflictError(_, attributes)) ⇒ // if it already exists, add tags from alert import org.elastic4play.services.QueryDSL._ (for { @@ -340,7 +340,7 @@ class AlertSrv( Future.successful(()) } } - .map(_ => caze) + .map(_ ⇒ caze) updatedCase.onComplete { _ ⇒ // remove temporary files artifactsFields From 0d026b1e15ceb81079f3d4813c53c0f4e5ed145d Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Wed, 4 Sep 2019 15:46:39 +0200 Subject: [PATCH 29/58] #954 Add the possibility to delete a custom field from the admin section --- ui/app/index.html | 1 + .../admin/AdminCustomFieldsCtrl.js | 55 ++++++++++++++++++- .../controllers/case/CaseCloseModalCtrl.js | 9 ++- ui/app/scripts/services/CustomFieldsSrv.js | 17 ++++++ .../admin/case-template/custom-fields.html | 2 +- .../views/partials/admin/custom-fields.html | 3 +- .../views/partials/utils/confirm.modal.html | 3 +- 7 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 ui/app/scripts/services/CustomFieldsSrv.js diff --git a/ui/app/index.html b/ui/app/index.html index f4a81a5a5b..276ad53014 100644 --- a/ui/app/index.html +++ b/ui/app/index.html @@ -246,6 +246,7 @@ + diff --git a/ui/app/scripts/controllers/admin/AdminCustomFieldsCtrl.js b/ui/app/scripts/controllers/admin/AdminCustomFieldsCtrl.js index 9b4a631bd3..021dedf731 100644 --- a/ui/app/scripts/controllers/admin/AdminCustomFieldsCtrl.js +++ b/ui/app/scripts/controllers/admin/AdminCustomFieldsCtrl.js @@ -2,7 +2,7 @@ 'use strict'; angular.module('theHiveControllers').controller('AdminCustomFieldsCtrl', - function($scope, $uibModal, ListSrv, CustomFieldsCacheSrv, NotificationSrv) { + function($scope, $uibModal, ListSrv, CustomFieldsCacheSrv, NotificationSrv, ModalUtilsSrv, CustomFieldsSrv) { var self = this; self.reference = { @@ -66,6 +66,59 @@ }); }; + self.deleteField = function(customField) { + CustomFieldsSrv.usage(customField) + .then(function(response) { + var usage = response.data, + message, + isHtml = false; + + + if (usage.total === 0) { + message = 'Are you sure you want to delete this custom field?'; + } else { + var segs = [ + 'Are you sure you want to delete this custom field?', + '
', + '
', + 'This custom field is used by:', + '
    ' + ]; + + if(usage.case) { + segs.push('
  • ' + usage.case + ' cases
  • '); + } + + if(usage.alert) { + segs.push('
  • ' + usage.alert + ' alerts
  • '); + } + + if(usage.caseTemplate) { + segs.push('
  • ' + usage.caseTemplate + ' case templates
  • '); + } + + segs.push('
'); + + message = segs.join(''); + isHtml = true; + } + + return ModalUtilsSrv.confirm('Remove custom field', message, { + okText: 'Yes, remove it', + flavor: 'danger', + isHtml: isHtml + }); + }) + .then(function(response) { + return CustomFieldsSrv.removeField(customField); + }) + .then(function() { + self.initCustomfields(); + CustomFieldsCacheSrv.clearCache(); + $scope.$emit('custom-fields:refresh'); + }); + }; + self.initCustomfields(); }); })(); diff --git a/ui/app/scripts/controllers/case/CaseCloseModalCtrl.js b/ui/app/scripts/controllers/case/CaseCloseModalCtrl.js index d52daf3d79..9d481674a7 100644 --- a/ui/app/scripts/controllers/case/CaseCloseModalCtrl.js +++ b/ui/app/scripts/controllers/case/CaseCloseModalCtrl.js @@ -43,15 +43,20 @@ }), 'name'); return result; - } + }; $scope.initialize = function() { CustomFieldsCacheSrv.all().then(function(fields) { $scope.orderedFields = getTemplateCustomFields($scope.caze.customFields); - $scope.allCustomFields = fields; + $scope.allCustomFields = fields; $scope.mandatoryFields = _.without(_.map($scope.orderedFields, function(cf) { var fieldDef = fields[cf]; + + if(!fieldDef) { + return; + } + var fieldValue = $scope.caze.customFields[cf][cf.type]; if((fieldValue === undefined || fieldValue === null) && fieldDef.mandatory === true) { diff --git a/ui/app/scripts/services/CustomFieldsSrv.js b/ui/app/scripts/services/CustomFieldsSrv.js new file mode 100644 index 0000000000..e6ac051a2b --- /dev/null +++ b/ui/app/scripts/services/CustomFieldsSrv.js @@ -0,0 +1,17 @@ +(function () { + 'use strict'; + angular.module('theHiveServices') + .factory('CustomFieldsSrv', function ($http) { + + var factory = { + removeField: function (field) { + return $http.delete('./api/list/' + field.id); + }, + usage: function(field) { + return $http.get('./api/customFields/' + field.reference); + } + }; + + return factory; + }); +})(); diff --git a/ui/app/views/partials/admin/case-template/custom-fields.html b/ui/app/views/partials/admin/case-template/custom-fields.html index b1a0ef0531..d85d6440b0 100644 --- a/ui/app/views/partials/admin/case-template/custom-fields.html +++ b/ui/app/views/partials/admin/case-template/custom-fields.html @@ -20,7 +20,7 @@

Delete

- +