diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..8fef7c7a8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,28 @@ +# StrangeBee Security Policies + +At [StrangeBee](https://www.strangebee.com) we take the security our software and services seriously, including following applications and projects: +- TheHive (TheHive 5, and [previous open source version](https://github.com/TheHive-Project/TheHive)) +- [Cortex](https://github.com/TheHive-Project/Cortex) +- [Cortex-Analyzers](https://github.com/TheHive-Project/Cortex-Analyzers) + +## Reporting a vulnerability +If you believe you have found a security vulnerability in our applications and services (TheHive, Cortex, Cortex-Analyzers ...), report it to us. + +**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** + +Instead, please send security vulnerabilities by emailing the StrangeBee Security team: + +``` +security[@]strangebee.com +``` + +In this email, please include as much information as possible that can help us better understand and resolve the issue: +- Application and version +- Special configuration and usage required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Exploit code is any +- Impact of the issue + +This will be very useful and help us triage your report more quickly. + +More information regarding our Security policies and Advisories can be found here: [https://github.com/StrangeBeeCorp/security](). diff --git a/app/org/thp/cortex/services/mappers/GroupUserMapper.scala b/app/org/thp/cortex/services/mappers/GroupUserMapper.scala index 5768d5b30..ab50f1c6b 100644 --- a/app/org/thp/cortex/services/mappers/GroupUserMapper.scala +++ b/app/org/thp/cortex/services/mappers/GroupUserMapper.scala @@ -1,24 +1,25 @@ package org.thp.cortex.services.mappers 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 import javax.inject.Inject -import org.elastic4play.AuthenticationError +import org.elastic4play.{AuthenticationError, AuthorizationError} import org.elastic4play.controllers.Fields class GroupUserMapper( loginAttrName: String, nameAttrName: String, - groupAttrName: String, + groupsAttrName: String, organizationAttrName: Option[String], defaultRoles: Seq[String], defaultOrganization: Option[String], - groupsUrl: String, + groupsUrl: Option[String], mappings: Map[String, Seq[String]], ws: WSClient, implicit val ec: ExecutionContext @@ -32,7 +33,7 @@ class GroupUserMapper( configuration.getOptional[String]("auth.sso.attributes.organization"), configuration.getOptional[Seq[String]]("auth.sso.defaultRoles").getOrElse(Seq()), configuration.getOptional[String]("auth.sso.defaultOrganization"), - configuration.getOptional[String]("auth.sso.groups.url").getOrElse(""), + configuration.getOptional[String]("auth.sso.groups.url"), configuration.getOptional[Map[String, Seq[String]]]("auth.sso.groups.mappings").getOrElse(Map()), ws, ec @@ -40,13 +41,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] + } + } + } + override def getUserFields(jsValue: JsValue, authHeader: Option[(String, String)]): Future[Fields] = { - 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 + groupsUrl match { + case Some(groupsEndpointUrl) ⇒ { + logger.debug(s"Retreiving groups from ${groupsEndpointUrl}") + val apiCall = authHeader.fold(ws.url(groupsEndpointUrl))(headers ⇒ ws.url(groupsEndpointUrl).addHttpHeaders(headers)) + apiCall.get.flatMap { r ⇒ extractGroupsThenBuildUserFields(jsValue, r.json) } + } + case None ⇒ { + logger.debug(s"Extracting groups from user info") + extractGroupsThenBuildUserFields(jsValue, jsValue) + } + } + } + + private def extractGroupsThenBuildUserFields(jsValue: JsValue, groupsContainer: JsValue): Future[Fields] = { + (groupsContainer \ groupsAttrName) match { + // Groups received as valid JSON array + case JsDefined(JsArray(groupsList)) ⇒ mapGroupsAndBuildUserFields(jsValue, groupsList.map(_.as[String]).toList) + + // Groups list received as string (invalid JSON, for example: "ROLE" or "['Role 1', ROLE2, 'Role_3']") + case JsDefined(JsString(groupsStr)) ⇒ { + val parser = new RoleListParser + parser.parseAll(parser.expr, groupsStr) match { + case parser.Success(result, _) ⇒ mapGroupsAndBuildUserFields(jsValue, result) + case err: parser.NoSuccess ⇒ Future.failed(AuthenticationError(s"User info fails: can't parse groups list (${err.msg})")) + } + } + + // Invalid group list + case JsDefined(error) ⇒ + Future.failed(AuthenticationError(s"User info fails: invalid groups list received in user info ('${error}' of type ${error.getClass})")) + + // Groups field is undefined + case _: JsUndefined ⇒ + Future.failed(AuthenticationError(s"User info fails: groups attribute ${groupsAttrName} doesn't exist in user info")) + } + } + + 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(AuthorizationError(s"No matched roles for user")) + } else { + logger.debug(s"Computed roles: ${roles.mkString(", ")}") val fields = for { login <- (jsValue \ loginAttrName).validate[String] @@ -58,7 +119,7 @@ class GroupUserMapper( } yield Fields(Json.obj("login" -> login.toLowerCase, "name" -> name, "roles" -> roles, "organization" -> organization)) fields match { case JsSuccess(f, _) => Future.successful(f) - case JsError(errors) => Future.failed(AuthenticationError(s"User info fails: ${errors.map(_._1).mkString}")) + case JsError(errors) ⇒ Future.failed(AuthenticationError(s"User info fails: ${errors.map(_._2).map(_.map(_.messages.mkString(", ")).mkString("; ")).mkString}")) } } }