diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fd324029..9beb03fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Change Log +## [3.1.0](https://github.com/TheHive-Project/Cortex/milestone/27) (2020-10-30) + +**Implemented enhancements:** + +- Improve Docker image [\#296](https://github.com/TheHive-Project/Cortex/issues/296) +- Impossible to load catalog through a proxy [\#297](https://github.com/TheHive-Project/Cortex/issues/297) +- Update login page design [\#303](https://github.com/TheHive-Project/Cortex/issues/303) + +**Fixed bugs:** + +- [Bug] Cortex and boolean ConfigurationItems [\#309](https://github.com/TheHive-Project/Cortex/issues/309) + ## [3.1.0-RC1](https://github.com/TheHive-Project/Cortex/milestone/21) (2020-08-13) **Implemented enhancements:** diff --git a/app/org/thp/cortex/controllers/AttachmentCtrl.scala b/app/org/thp/cortex/controllers/AttachmentCtrl.scala index f5fe38258..2f849db8e 100644 --- a/app/org/thp/cortex/controllers/AttachmentCtrl.scala +++ b/app/org/thp/cortex/controllers/AttachmentCtrl.scala @@ -2,23 +2,21 @@ package org.thp.cortex.controllers import java.net.URLEncoder import java.nio.file.Files -import javax.inject.{Inject, Singleton} - -import play.api.http.HttpEntity -import play.api.libs.Files.DefaultTemporaryFileCreator -import play.api.mvc._ -import play.api.{mvc, Configuration} import akka.stream.scaladsl.FileIO +import javax.inject.{Inject, Singleton} import net.lingala.zip4j.core.ZipFile import net.lingala.zip4j.model.ZipParameters import net.lingala.zip4j.util.Zip4jConstants -import org.thp.cortex.models.Roles - import org.elastic4play.Timed -import org.elastic4play.controllers.{Authenticated, Renderer} +import org.elastic4play.controllers.Authenticated import org.elastic4play.models.AttachmentAttributeFormat -import org.elastic4play.services.AttachmentSrv +import org.elastic4play.services.{AttachmentSrv, ExecutionContextSrv} +import org.thp.cortex.models.Roles +import play.api.http.HttpEntity +import play.api.libs.Files.DefaultTemporaryFileCreator +import play.api.mvc._ +import play.api.{mvc, Configuration} /** * Controller used to access stored attachments (plain or zipped) @@ -30,7 +28,7 @@ class AttachmentCtrl( attachmentSrv: AttachmentSrv, authenticated: Authenticated, components: ControllerComponents, - renderer: Renderer + executionContextSrv: ExecutionContextSrv ) extends AbstractController(components) { @Inject() def this( @@ -39,9 +37,9 @@ class AttachmentCtrl( attachmentSrv: AttachmentSrv, authenticated: Authenticated, components: ControllerComponents, - renderer: Renderer + executionContextSrv: ExecutionContextSrv ) = - this(configuration.get[String]("datastore.attachment.password"), tempFileCreator, attachmentSrv, authenticated, components, renderer) + this(configuration.get[String]("datastore.attachment.password"), tempFileCreator, attachmentSrv, authenticated, components, executionContextSrv) /** * Download an attachment, identified by its hash, in plain format @@ -50,16 +48,25 @@ class AttachmentCtrl( */ @Timed("controllers.AttachmentCtrl.download") def download(hash: String, name: Option[String]): Action[AnyContent] = authenticated(Roles.read) { _ => - if (hash.startsWith("{{")) // angularjs hack - NoContent - else if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty) - mvc.Results.BadRequest("File name is invalid") - else - Result( - header = ResponseHeader(200, Map("Content-Disposition" -> s"""attachment; filename="${URLEncoder - .encode(name.getOrElse(hash), "utf-8")}"""", "Content-Transfer-Encoding" -> "binary")), - body = HttpEntity.Streamed(attachmentSrv.source(hash), None, None) - ) + executionContextSrv.withDefault { implicit ec => + if (hash.startsWith("{{")) // angularjs hack + NoContent + else if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty) + mvc.Results.BadRequest("File name is invalid") + else + Result( + header = ResponseHeader( + 200, + Map( + "Content-Disposition" -> + s"""attachment; filename="${URLEncoder + .encode(name.getOrElse(hash), "utf-8")}"""", + "Content-Transfer-Encoding" -> "binary" + ) + ), + body = HttpEntity.Streamed(attachmentSrv.source(hash), None, None) + ) + } } /** @@ -69,33 +76,35 @@ class AttachmentCtrl( */ @Timed("controllers.AttachmentCtrl.downloadZip") def downloadZip(hash: String, name: Option[String]): Action[AnyContent] = authenticated(Roles.read) { _ => - if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty) - BadRequest("File name is invalid") - else { - val f = tempFileCreator.create("zip", hash).path - Files.delete(f) - val zipFile = new ZipFile(f.toFile) - val zipParams = new ZipParameters - zipParams.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_FASTEST) - zipParams.setEncryptFiles(true) - zipParams.setEncryptionMethod(Zip4jConstants.ENC_METHOD_STANDARD) - zipParams.setPassword(password) - zipParams.setFileNameInZip(name.getOrElse(hash)) - zipParams.setSourceExternalStream(true) - zipFile.addStream(attachmentSrv.stream(hash), zipParams) + executionContextSrv.withDefault { implicit ec => + if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty) + BadRequest("File name is invalid") + else { + val f = tempFileCreator.create("zip", hash).path + Files.delete(f) + val zipFile = new ZipFile(f.toFile) + val zipParams = new ZipParameters + zipParams.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_FASTEST) + zipParams.setEncryptFiles(true) + zipParams.setEncryptionMethod(Zip4jConstants.ENC_METHOD_STANDARD) + zipParams.setPassword(password) + zipParams.setFileNameInZip(name.getOrElse(hash)) + zipParams.setSourceExternalStream(true) + zipFile.addStream(attachmentSrv.stream(hash), zipParams) - Result( - header = ResponseHeader( - 200, - Map( - "Content-Disposition" -> s"""attachment; filename="${URLEncoder.encode(name.getOrElse(hash), "utf-8")}.zip"""", - "Content-Type" -> "application/zip", - "Content-Transfer-Encoding" -> "binary", - "Content-Length" -> Files.size(f).toString - ) - ), - body = HttpEntity.Streamed(FileIO.fromPath(f), Some(Files.size(f)), Some("application/zip")) - ) + Result( + header = ResponseHeader( + 200, + Map( + "Content-Disposition" -> s"""attachment; filename="${URLEncoder.encode(name.getOrElse(hash), "utf-8")}.zip"""", + "Content-Type" -> "application/zip", + "Content-Transfer-Encoding" -> "binary", + "Content-Length" -> Files.size(f).toString + ) + ), + body = HttpEntity.Streamed(FileIO.fromPath(f), Some(Files.size(f)), Some("application/zip")) + ) + } } } } diff --git a/app/org/thp/cortex/models/Migration.scala b/app/org/thp/cortex/models/Migration.scala index 77fdd5171..7bbfa9b85 100644 --- a/app/org/thp/cortex/models/Migration.scala +++ b/app/org/thp/cortex/models/Migration.scala @@ -84,5 +84,6 @@ class Migration @Inject() (userSrv: UserSrv, organizationSrv: OrganizationSrv, w ("sequenceCounter" -> counter) } ) + case DatabaseState(4) => Nil } } diff --git a/app/org/thp/cortex/models/package.scala b/app/org/thp/cortex/models/package.scala index 0bbb2c3af..213cb2804 100644 --- a/app/org/thp/cortex/models/package.scala +++ b/app/org/thp/cortex/models/package.scala @@ -1,5 +1,5 @@ package org.thp.cortex package object models { - val modelVersion = 4 + val modelVersion = 5 } diff --git a/app/org/thp/cortex/services/CustomWSAPI.scala b/app/org/thp/cortex/services/CustomWSAPI.scala new file mode 100644 index 000000000..9452066cf --- /dev/null +++ b/app/org/thp/cortex/services/CustomWSAPI.scala @@ -0,0 +1,118 @@ +package org.thp.cortex.services + +import scala.util.control.NonFatal + +import javax.inject.{Inject, Singleton} +import play.api.inject.ApplicationLifecycle +import play.api.libs.ws._ +import play.api.libs.ws.ahc.{AhcWSClient, AhcWSClientConfig, AhcWSClientConfigParser} +import play.api.{Configuration, Environment, Logger} + +import akka.stream.Materializer +import com.typesafe.sslconfig.ssl.TrustStoreConfig + +object CustomWSAPI { + private[CustomWSAPI] lazy val logger = Logger(getClass) + + def parseWSConfig(config: Configuration): AhcWSClientConfig = + new AhcWSClientConfigParser(new WSConfigParser(config.underlying, getClass.getClassLoader).parse(), config.underlying, getClass.getClassLoader) + .parse() + + def parseProxyConfig(config: Configuration): Option[WSProxyServer] = + config.getOptional[Configuration]("play.ws.proxy").map { proxyConfig ⇒ + DefaultWSProxyServer( + proxyConfig.get[String]("host"), + proxyConfig.get[Int]("port"), + proxyConfig.getOptional[String]("protocol"), + proxyConfig.getOptional[String]("user"), + proxyConfig.getOptional[String]("password"), + proxyConfig.getOptional[String]("ntlmDomain"), + proxyConfig.getOptional[String]("encoding"), + proxyConfig.getOptional[Seq[String]]("nonProxyHosts") + ) + } + + def getWS(config: Configuration)(implicit mat: Materializer): AhcWSClient = { + val clientConfig = parseWSConfig(config) + val clientConfigWithTruststore = config.getOptional[String]("play.cert") match { + case Some(p) ⇒ + logger.warn("""Use of "cert" parameter in configuration file is deprecated. Please use: + | ws.ssl { + | trustManager = { + | stores = [ + | { type = "PEM", path = "/path/to/cacert.crt" }, + | { type = "JKS", path = "/path/to/truststore.jks" } + | ] + | } + | } + """.stripMargin) + clientConfig.copy( + wsClientConfig = clientConfig + .wsClientConfig + .copy( + ssl = clientConfig + .wsClientConfig + .ssl + .withTrustManagerConfig( + clientConfig + .wsClientConfig + .ssl + .trustManagerConfig + .withTrustStoreConfigs( + clientConfig.wsClientConfig.ssl.trustManagerConfig.trustStoreConfigs :+ TrustStoreConfig( + filePath = Some(p), + data = None + ) + ) + ) + ) + ) + case None ⇒ clientConfig + } + AhcWSClient(clientConfigWithTruststore, None) + } + + def getConfig(config: Configuration, path: String): Configuration = + Configuration( + config + .getOptional[Configuration](s"play.$path") + .getOrElse(Configuration.empty) + .underlying + .withFallback(config.getOptional[Configuration](path).getOrElse(Configuration.empty).underlying) + ) +} + +@Singleton +class CustomWSAPI( + ws: AhcWSClient, + val proxy: Option[WSProxyServer], + config: Configuration, + environment: Environment, + lifecycle: ApplicationLifecycle, + mat: Materializer + ) extends WSClient { + private[CustomWSAPI] lazy val logger = Logger(getClass) + + @Inject() def this(config: Configuration, environment: Environment, lifecycle: ApplicationLifecycle, mat: Materializer) = + this(CustomWSAPI.getWS(config)(mat), CustomWSAPI.parseProxyConfig(config), config, environment, lifecycle, mat) + + override def close(): Unit = ws.close() + + override def url(url: String): WSRequest = { + val req = ws.url(url) + proxy.fold(req)(req.withProxyServer) + } + + override def underlying[T]: T = ws.underlying[T] + + def withConfig(subConfig: Configuration): CustomWSAPI = { + logger.debug(s"Override WS configuration using $subConfig") + try { + new CustomWSAPI(Configuration(subConfig.underlying.atKey("play").withFallback(config.underlying)), environment, lifecycle, mat) + } catch { + case NonFatal(e) ⇒ + logger.error(s"WSAPI configuration error, use default values", e) + this + } + } +} diff --git a/app/org/thp/cortex/services/DockerJobRunnerSrv.scala b/app/org/thp/cortex/services/DockerJobRunnerSrv.scala index df3424a0e..5931cac12 100644 --- a/app/org/thp/cortex/services/DockerJobRunnerSrv.scala +++ b/app/org/thp/cortex/services/DockerJobRunnerSrv.scala @@ -21,7 +21,13 @@ import org.thp.cortex.models._ import org.elastic4play.utils.RichFuture @Singleton -class DockerJobRunnerSrv(client: DockerClient, autoUpdate: Boolean, implicit val system: ActorSystem) { +class DockerJobRunnerSrv( + client: DockerClient, + autoUpdate: Boolean, + jobBaseDirectory: Path, + dockerJobBaseDirectory: Path, + implicit val system: ActorSystem +) { @Inject() def this(config: Configuration, system: ActorSystem) = @@ -37,6 +43,8 @@ class DockerJobRunnerSrv(client: DockerClient, autoUpdate: Boolean, implicit val .useProxy(config.getOptional[Boolean]("docker.useProxy").getOrElse(false)) .build(), config.getOptional[Boolean]("docker.autoUpdate").getOrElse(true), + Paths.get(config.get[String]("job.directory")), + Paths.get(config.get[String]("job.dockerDirectory")), system: ActorSystem ) @@ -60,7 +68,7 @@ class DockerJobRunnerSrv(client: DockerClient, autoUpdate: Boolean, implicit val .builder() .appendBinds( Bind - .from(jobDirectory.toAbsolutePath.toString) + .from(dockerJobBaseDirectory.resolve(jobBaseDirectory.relativize(jobDirectory)).toAbsolutePath.toString) .to("/job") .readOnly(false) .build() diff --git a/app/org/thp/cortex/services/JobRunnerSrv.scala b/app/org/thp/cortex/services/JobRunnerSrv.scala index b939f56f1..018215d43 100644 --- a/app/org/thp/cortex/services/JobRunnerSrv.scala +++ b/app/org/thp/cortex/services/JobRunnerSrv.scala @@ -42,6 +42,7 @@ class JobRunnerSrv @Inject() ( val logger = Logger(getClass) lazy val analyzerExecutionContext: ExecutionContext = akkaSystem.dispatchers.lookup("analyzer") lazy val responderExecutionContext: ExecutionContext = akkaSystem.dispatchers.lookup("responder") + val jobDirectory: Path = Paths.get(config.get[String]("job.directory")) private val runners: Seq[String] = config .getOptional[Seq[String]]("job.runners") @@ -89,7 +90,7 @@ class JobRunnerSrv @Inject() ( } private def prepareJobFolder(worker: Worker, job: Job): Future[Path] = { - val jobFolder = Files.createTempDirectory(Paths.get(System.getProperty("java.io.tmpdir")), s"cortex-job-${job.id}-") + val jobFolder = Files.createTempDirectory(jobDirectory, s"cortex-job-${job.id}-") val inputJobFolder = Files.createDirectories(jobFolder.resolve("input")) Files.createDirectories(jobFolder.resolve("output")) diff --git a/app/org/thp/cortex/services/OAuth2Srv.scala b/app/org/thp/cortex/services/OAuth2Srv.scala index ae30d3348..27ea7f9b6 100644 --- a/app/org/thp/cortex/services/OAuth2Srv.scala +++ b/app/org/thp/cortex/services/OAuth2Srv.scala @@ -23,7 +23,7 @@ case class OAuth2Config( authorizationUrl: String, tokenUrl: String, userUrl: String, - scope: String, + scope: Seq[String], authorizationHeader: String, autoupdate: Boolean, autocreate: Boolean @@ -41,7 +41,7 @@ object OAuth2Config { authorizationUrl <- configuration.getOptional[String]("auth.oauth2.authorizationUrl") tokenUrl <- configuration.getOptional[String]("auth.oauth2.tokenUrl") userUrl <- configuration.getOptional[String]("auth.oauth2.userUrl") - scope <- configuration.getOptional[String]("auth.oauth2.scope") + scope <- configuration.getOptional[Seq[String]]("auth.oauth2.scope") authorizationHeader = configuration.getOptional[String]("auth.oauth2.authorizationHeader").getOrElse("Bearer") autocreate = configuration.getOptional[Boolean]("auth.sso.autocreate").getOrElse(false) autoupdate = configuration.getOptional[Boolean]("auth.sso.autoupdate").getOrElse(false) @@ -109,7 +109,7 @@ class OAuth2Srv( private def authRedirect(oauth2Config: OAuth2Config): Result = { val state = UUID.randomUUID().toString val queryStringParams = Map[String, Seq[String]]( - "scope" -> Seq(oauth2Config.scope), + "scope" -> Seq(oauth2Config.scope.mkString(" ")), "response_type" -> Seq(oauth2Config.responseType), "redirect_uri" -> Seq(oauth2Config.redirectUri), "client_id" -> Seq(oauth2Config.clientId), diff --git a/app/org/thp/cortex/services/OrganizationSrv.scala b/app/org/thp/cortex/services/OrganizationSrv.scala index c0a62bdfe..c10d0ed54 100644 --- a/app/org/thp/cortex/services/OrganizationSrv.scala +++ b/app/org/thp/cortex/services/OrganizationSrv.scala @@ -2,17 +2,14 @@ package org.thp.cortex.services import javax.inject.{Inject, Singleton} -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration.Duration - import play.api.Configuration import play.api.cache.AsyncCacheApi import play.api.libs.json.JsObject - import akka.NotUsed import akka.stream.scaladsl.Source import org.thp.cortex.models.{Organization, OrganizationModel} - import org.elastic4play.controllers.Fields import org.elastic4play.database.ModifyConfig import org.elastic4play.services._ @@ -38,38 +35,44 @@ class OrganizationSrv( deleteSrv: DeleteSrv, createSrv: CreateSrv, cache: AsyncCacheApi - ) = this(config.get[Duration]("cache.organization"), organizationModel, getSrv, updateSrv, findSrv, deleteSrv, createSrv, cache) + ) = + this(config.get[Duration]("cache.organization"), organizationModel, getSrv, updateSrv, findSrv, deleteSrv, createSrv, cache) - def create(fields: Fields)(implicit authContext: AuthContext): Future[Organization] = + def create(fields: Fields)(implicit authContext: AuthContext, ec: ExecutionContext): Future[Organization] = createSrv[OrganizationModel, Organization](organizationModel, fields) - def get(orgId: String): Future[Organization] = cache.getOrElseUpdate(s"org-$orgId", cacheExpiration) { + def get(orgId: String)(implicit ec: ExecutionContext): Future[Organization] = cache.getOrElseUpdate(s"org-$orgId", cacheExpiration) { getSrv[OrganizationModel, Organization](organizationModel, orgId) } - def update(orgId: String, fields: Fields)(implicit Context: AuthContext): Future[Organization] = + def update(orgId: String, fields: Fields)(implicit Context: AuthContext, ec: ExecutionContext): Future[Organization] = update(orgId, fields, ModifyConfig.default) - def update(orgId: String, fields: Fields, modifyConfig: ModifyConfig)(implicit Context: AuthContext): Future[Organization] = { + def update(orgId: String, fields: Fields, modifyConfig: ModifyConfig)(implicit Context: AuthContext, ec: ExecutionContext): Future[Organization] = { cache.remove(s"org-$orgId") updateSrv[OrganizationModel, Organization](organizationModel, orgId, fields, modifyConfig) } - def update(organization: Organization, fields: Fields)(implicit Context: AuthContext): Future[Organization] = + def update(organization: Organization, fields: Fields)(implicit Context: AuthContext, ec: ExecutionContext): Future[Organization] = update(organization, fields, ModifyConfig.default) - def update(organization: Organization, fields: Fields, modifyConfig: ModifyConfig)(implicit Context: AuthContext): Future[Organization] = { + def update(organization: Organization, fields: Fields, modifyConfig: ModifyConfig)( + implicit Context: AuthContext, + ec: ExecutionContext + ): Future[Organization] = { cache.remove(s"org-${organization.id}") updateSrv(organization, fields, modifyConfig) } - def delete(orgId: String)(implicit Context: AuthContext): Future[Organization] = { + def delete(orgId: String)(implicit Context: AuthContext, ec: ExecutionContext): Future[Organization] = { cache.remove(s"org-$orgId") deleteSrv[OrganizationModel, Organization](organizationModel, orgId) } - def find(queryDef: QueryDef, range: Option[String], sortBy: Seq[String]): (Source[Organization, NotUsed], Future[Long]) = + def find(queryDef: QueryDef, range: Option[String], sortBy: Seq[String])( + implicit ec: ExecutionContext + ): (Source[Organization, NotUsed], Future[Long]) = findSrv[OrganizationModel, Organization](organizationModel, queryDef, range, sortBy) - def stats(queryDef: QueryDef, aggs: Seq[Agg]): Future[JsObject] = findSrv(organizationModel, queryDef, aggs: _*) + def stats(queryDef: QueryDef, aggs: Seq[Agg])(implicit ec: ExecutionContext): Future[JsObject] = findSrv(organizationModel, queryDef, aggs: _*) } diff --git a/app/org/thp/cortex/services/WorkerSrv.scala b/app/org/thp/cortex/services/WorkerSrv.scala index b76fb9b51..690a99e41 100644 --- a/app/org/thp/cortex/services/WorkerSrv.scala +++ b/app/org/thp/cortex/services/WorkerSrv.scala @@ -9,7 +9,6 @@ import scala.io.Codec import scala.util.{Failure, Success, Try} import play.api.libs.json.{JsArray, JsObject, JsString, Json} -import play.api.libs.ws.WSClient import play.api.{Configuration, Logger} import akka.NotUsed @@ -37,7 +36,7 @@ class WorkerSrv @Inject() ( updateSrv: UpdateSrv, deleteSrv: DeleteSrv, findSrv: FindSrv, - ws: WSClient, + ws: CustomWSAPI, implicit val ec: ExecutionContext, implicit val mat: Materializer ) { diff --git a/app/org/thp/cortex/services/mappers/GroupUserMapper.scala b/app/org/thp/cortex/services/mappers/GroupUserMapper.scala index 993f64834..5768d5b30 100644 --- a/app/org/thp/cortex/services/mappers/GroupUserMapper.scala +++ b/app/org/thp/cortex/services/mappers/GroupUserMapper.scala @@ -14,7 +14,6 @@ import org.elastic4play.controllers.Fields class GroupUserMapper( loginAttrName: String, nameAttrName: String, - rolesAttrName: Option[String], groupAttrName: String, organizationAttrName: Option[String], defaultRoles: Seq[String], @@ -29,7 +28,6 @@ class GroupUserMapper( this( configuration.getOptional[String]("auth.sso.attributes.login").getOrElse("login"), configuration.getOptional[String]("auth.sso.attributes.name").getOrElse("name"), - configuration.getOptional[String]("auth.sso.attributes.roles"), configuration.getOptional[String]("auth.sso.attributes.groups").getOrElse(""), configuration.getOptional[String]("auth.sso.attributes.organization"), configuration.getOptional[Seq[String]]("auth.sso.defaultRoles").getOrElse(Seq()), @@ -57,7 +55,7 @@ class GroupUserMapper( .flatMap(o => (jsValue \ o).asOpt[String]) .orElse(defaultOrganization) .fold[JsResult[String]](JsError())(o => JsSuccess(o)) - } yield Fields(Json.obj("login" -> login, "name" -> name, "roles" -> roles, "organization" -> organization)) + } 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}")) diff --git a/app/org/thp/cortex/services/mappers/SimpleUserMapper.scala b/app/org/thp/cortex/services/mappers/SimpleUserMapper.scala index 9e1665e8a..0a317f414 100644 --- a/app/org/thp/cortex/services/mappers/SimpleUserMapper.scala +++ b/app/org/thp/cortex/services/mappers/SimpleUserMapper.scala @@ -42,7 +42,7 @@ class SimpleUserMapper( .flatMap(o => (jsValue \ o).asOpt[String]) .orElse(defaultOrganization) .fold[JsResult[String]](JsError())(o => JsSuccess(o)) - } yield Fields(Json.obj("login" -> login, "name" -> name, "roles" -> roles, "organization" -> organization)) + } 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}")) diff --git a/build.sbt b/build.sbt index b314eb7a7..f25d5d42d 100644 --- a/build.sbt +++ b/build.sbt @@ -15,11 +15,12 @@ libraryDependencies ++= Seq( Dependencies.reflections, Dependencies.zip4j, Dependencies.dockerClient, - Dependencies.akkaCluster + Dependencies.akkaCluster, + Dependencies.akkaClusterTyped ) resolvers += Resolver.sbtPluginRepo("releases") -resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases" +resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases" resolvers += "elasticsearch-releases" at "https://artifacts.elastic.co/maven" publishArtifact in (Compile, packageDoc) := false publishArtifact in packageDoc := false diff --git a/conf/application.sample b/conf/application.sample index de5b68416..c87dc4a8a 100644 --- a/conf/application.sample +++ b/conf/application.sample @@ -32,16 +32,16 @@ search { #} ## Authentication configuration - #search.username = "" - #search.password = "" + #username = "" + #password = "" ## SSL configuration - #search.keyStore { + #keyStore { # path = "/path/to/keystore" # type = "JKS" # or PKCS12 # password = "keystore-password" #} - #search.trustStore { + #trustStore { # path = "/path/to/trustStore" # type = "JKS" # or PKCS12 # password = "trustStore-password" @@ -113,7 +113,7 @@ auth { # URL of the authorization server #clientId = "client-id" #clientSecret = "client-secret" - #redirectUri = "https://my-thehive-instance.example/index.html#!/login" + #redirectUri = "https://my-cortex-instance.example/api/ssoLogin" #responseType = "code" #grantType = "authorization_code" @@ -124,8 +124,6 @@ auth { # 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" - # Type of authorization header - #authorizationHeader = "Bearer" # or token } # Single-Sign On @@ -139,22 +137,16 @@ auth { # Autologin user using SSO? #autologin = false - # Attributes mappings + # Name of mapping class from user resource to backend user ('simple' or 'group') + #mapper = group #attributes { - # login = "login" + # login = "user" # name = "name" # groups = "groups" - # roles = "roles" # list of roles, separated with comma - # organisation = "org" + # organization = "org" #} - - # Name of mapping class from user resource to backend user ('simple' or 'group') - #mapper = group - # Default roles for users with no groups mapped ("read", "analyze", "orgadmin") - #defaultRoles = [] - # Default organization - #defaultOrganization = "MyOrga" - + #defaultRoles = ["read"] + #defaultOrganization = "csirt" #groups { # # URL to retreive groups (leave empty if you are using OIDC) # #url = "https://auth-site.com/api/Groups" @@ -165,6 +157,16 @@ auth { # reader-profile-name = ["read"] # } #} + + #mapper = simple + #attributes { + # login = "user" + # name = "name" + # roles = "roles" + # organization = "org" + #} + #defaultRoles = ["read"] + #defaultOrganization = "csirt" } } @@ -176,7 +178,7 @@ analyzer { # - directory where analyzers are installed # - json file containing the list of analyzer descriptions urls = [ - "https://dl.bintray.com/thehive-project/cortexneurons/analyzers.json" + "https://download.thehive-project.org/analyzers.json" #"/absolute/path/of/analyzers" ] @@ -196,7 +198,7 @@ analyzer { responder { # responder location (same format as analyzer.urls) urls = [ - "https://dl.bintray.com/thehive-project/cortexneurons/responders.json" + "https://download.thehive-project.org/responders.json" #"/absolute/path/of/responders" ] @@ -211,4 +213,10 @@ responder { } } +# Proxy configuration to retrieve catalogs +# play.ws.proxy { +# host = proxy.example.com +# port = 3128 +# } + # It's the end my friend. Happy hunting! diff --git a/conf/reference.conf b/conf/reference.conf index bd04bfaf1..7f457257b 100644 --- a/conf/reference.conf +++ b/conf/reference.conf @@ -12,6 +12,8 @@ cache { job { timeout = 30 minutes runners = [docker, process] + directory = ${java.io.tmpdir} + dockerDirectory = ${job.directory} } # HTTP filters diff --git a/docker.sbt b/docker.sbt index 2349e11ba..b00495861 100644 --- a/docker.sbt +++ b/docker.sbt @@ -29,26 +29,34 @@ dockerCommands := Seq( Cmd("WORKDIR", "/opt/cortex"), // format: off Cmd("RUN", + "wget", "-q", "-O", "-", "https://download.docker.com/linux/static/stable/x86_64/docker-18.09.0.tgz", "|", + "tar", "-xzC", "/usr/local/bin/", "--strip-components", "1", "&&", + "addgroup", "--system", "dockremap", "&&", + "adduser", "--system", "--ingroup", "dockremap", "dockremap", "&&", + "addgroup", "--system", "docker", "&&", + "echo", "dockremap:165536:65536", ">>", "/etc/subuid", "&&", + "echo", "dockremap:165536:65536", ">>", "/etc/subgid", "&&", "apt", "update", "&&", "apt", "upgrade", "-y", "&&", + "apt", "install", "-y", "iptables", "lxc", "&&", "apt", "autoclean", "-y", "-q", "&&", "apt", "autoremove", "-y", "-q", "&&", "rm", "-rf", "/var/lib/apt/lists/*", "&&", "(", "type", "groupadd", "1>/dev/null", "2>&1", "&&", - "groupadd", "-g", "1000", "cortex", "||", - "addgroup", "-g", "1000", "-S", "cortex", + "groupadd", "-g", "1001", "cortex", "||", + "addgroup", "-g", "1001", "-S", "cortex", ")", "&&", "(", "type", "useradd", "1>/dev/null", "2>&1", "&&", - "useradd", "--system", "--uid", "1000", "--gid", "1000", "cortex", "||", - "adduser", "-S", "-u", "1000", "-G", "cortex", "cortex", + "useradd", "--system", "--uid", "1001", "--gid", "1001", "cortex", "||", + "adduser", "-S", "-u", "1001", "-G", "cortex", "cortex", ")"), //format: on Cmd("ADD", "--chown=root:root", "opt", "/opt"), Cmd("ADD", "--chown=cortex:cortex", "var", "/var"), Cmd("ADD", "--chown=cortex:cortex", "etc", "/etc"), + Cmd("VOLUME", "/var/lib/docker"), ExecCmd("RUN", "chmod", "+x", "/opt/cortex/bin/cortex", "/opt/cortex/entrypoint"), Cmd("EXPOSE", "9001"), - Cmd("USER", "thehive"), ExecCmd("ENTRYPOINT", "/opt/cortex/entrypoint"), ExecCmd("CMD") ) diff --git a/docker/cortex/.env b/docker/cortex/.env new file mode 100644 index 000000000..d623e6efd --- /dev/null +++ b/docker/cortex/.env @@ -0,0 +1 @@ +job_directory=/tmp/cortex-jobs diff --git a/docker/cortex/docker-compose.yml b/docker/cortex/docker-compose.yml index f9a628502..be93a4cdd 100644 --- a/docker/cortex/docker-compose.yml +++ b/docker/cortex/docker-compose.yml @@ -8,10 +8,14 @@ services: - script.allowed_types=inline - thread_pool.search.queue_size=100000 - thread_pool.write.queue_size=10000 -path.repo: backup cortex: - image: thehiveproject/cortex:latest + image: thehiveproject/cortex:3.1.0-0.3RC1 + environment: + - job_directory=${job_directory} + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ${job_directory}:${job_directory} depends_on: - elasticsearch ports: - - "0.0.0.0:9001:9001" + - "0.0.0.0:9001:9001" \ No newline at end of file diff --git a/package/docker/entrypoint b/package/docker/entrypoint index 38e9d836f..9d3b00cee 100755 --- a/package/docker/entrypoint +++ b/package/docker/entrypoint @@ -1,122 +1,156 @@ #!/bin/bash -ES_HOSTNAME=elasticsearch -CONFIG_SECRET=1 -CONFIG_ES=1 -CONFIG=1 -CONFIG_FILE=/etc/cortex/application.conf -ANALYZER_PATH=/opt/Cortex-Analyzers/analyzers +test "${no_config:-0}" == 1 +CONFIG=$? +test "${no_config_secret:-0}" == 1 +CONFIG_SECRET=$? +test "${no_config_es:-0}" == 1 +CONFIG_ES=$? +ES_URI=${es_uri:-} +ES_HOSTNAME=${es_hostname:-elasticsearch} +CONFIG_FILE=${config_file:-/etc/cortex/application.conf} +DEFAULT_ANALYZER_URL="https://download.thehive-project.org/analyzers.json" ANALYZER_URLS=() -RESPONDER_PATH=/opt/Cortex-Analyzers/responders +IFS=',' read -r -a ANALYZER_URLS <<< "${analyzer_urls:-$analyzer_url}" +DEFAULT_RESPONDER_URL="https://download.thehive-project.org/responders.json" RESPONDER_URLS=() -START_DOCKER=0 -SHOW_SECRET=0 +IFS=',' read -r -a RESPONDER_URLS <<< "${responder_urls:-$responder_url}" +START_DOCKER=${start_docker:-0} +SHOW_SECRET=${show_secret:-0} +DAEMON_USER=${daemon_user:-cortex} +JOB_DIRECTORY=${job_directory:-/tmp/cortex-jobs} +DOCKER_JOB_DIRECTORY=${docker_job_directory:-} function usage { - cat <<- _EOF_ - Available options: - --no-config | do not try to configure TheHive (add secret and elasticsearch) - --no-config-secret | do not add random secret to configuration - --no-config-es | do not add elasticsearch hosts to configuration - --es-uri | use this string to configure elasticsearch hosts (format: http(s)://host:port,host:port(/prefix)?querystring) - --es-hostname | resolve this hostname to find elasticseach instances - --secret | secret to secure sessions - --show-secret | show the generated secret - --analyzer-url | where analyzers are located (url or path) - --responder-url | where responders are located (url or path) - --start-docker | start a internal docker (inside container) to run analyzers/responders - _EOF_ - exit 1 + cat <<- _EOF_ + Available options: + --no-config | do not configure TheHive (add secret and elasticsearch) + --no-config-secret | do not add random secret to configuration + --no-config-es | do not add elasticsearch hosts to configuration + --es-uri | use this string to configure elasticsearch hosts (format: http(s)://host:port,host:port(/prefix)?querystring) + --es-hostname | resolve this hostname to find elasticsearch instances + --secret | secret to secure sessions + --show-secret | show the generated secret + --job-directory | use this directory to store job files + --docker-job-directory | indicate the job directory in the host (not inside container) + --analyzer-url | where analyzers are located (url or path) + --responder-url | where responders are located (url or path) + --start-docker | start a internal docker (inside container) to run analyzers/responders + --daemon-user | run cortex using this user +_EOF_ + exit 1 } STOP=0 while test $# -gt 0 -o $STOP = 1 do - case "$1" in - "--no-config") CONFIG=0;; - "--no-config-secret") CONFIG_SECRET=0;; - "--no-config-es") CONFIG_ES=0;; - "--es-hosts") echo "--es-hosts is deprecated, please use --es-uri" - usage;; - "--es-uri") shift; ES_URI=$1;; - "--es-hostname") shift; ES_HOSTNAME=$1;; - "--secret") shift; SECRET=$1;; - "--show-secret") SHOW_SECRET=1;; - "--analyzer-path") shift; ANALYZER_PATH=$1;; - "--responder-path") shift; RESPONDER_PATH=$1;; - "--analyzer-url") shift; ANALYZER_URLS+=$1;; - "--responder-url") shift; RESPONDER_URLS+=$1;; - "--start-docker") START_DOCKER=1;; - "--") STOP=1;; - *) echo "unrecognized option: $1"; usage;; - esac - shift + case "$1" in + "--no-config") CONFIG=0;; + "--no-config-secret") CONFIG_SECRET=0;; + "--no-config-es") CONFIG_ES=0;; + "--es-hosts") echo "--es-hosts is deprecated, please use --es-uri" + usage;; + "--es-uri") shift; ES_URI=$1;; + "--es-hostname") shift; ES_HOSTNAME=$1;; + "--secret") shift; SECRET=$1;; + "--show-secret") SHOW_SECRET=1;; + "--job-directory") shift; JOB_DIRECTORY=$1;; + "--docker-job-directory") shift; DOCKER_JOB_DIRECTORY=$1;; + "--analyzer-path") echo "--analyzer-path is deprecated, please use --analyzer-url" + shift; ANALYZER_URLS+=("$1");; + "--responder-path") echo "--responder-path is deprecated, please use --responder-url" + shift; RESPONDER_URLS+=("$1");; + "--analyzer-url") shift; ANALYZER_URLS+=("$1");; + "--responder-url") shift; RESPONDER_URLS+=("$1");; + "--start-docker") START_DOCKER=1;; + "--daemon-user") shift; DAEMON_USER=$1;; + "--") STOP=1;; + *) echo "unrecognized option: $1"; usage;; + esac + shift done if test $CONFIG = 1 then - CONFIG_FILE=$(mktemp).conf - if test $CONFIG_SECRET = 1 - then - if test -z "$SECRET" - then - SECRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 64 | head -n 1) - test $SHOW_SECRET = 1 && echo Using secret: $SECRET - fi - echo play.http.secret.key=\"$SECRET\" >> $CONFIG_FILE - fi + CONFIG_FILE=$(mktemp --tmpdir cortex-XXXXXX.conf) + if test $CONFIG_SECRET = 1 + then + if test -z "$SECRET" + then + SECRET=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 64 | head -n 1) + test $SHOW_SECRET = 1 && echo "Using secret: $SECRET" + fi + echo "play.http.secret.key=\"$SECRET\"" >> "$CONFIG_FILE" + fi - if test $CONFIG_ES = 1 - then - if test -z "$ES_URI" - then - function join_es_hosts { - echo -n $1:9200 - shift - printf "%s," "${@/#/:9200}" - } + if test $CONFIG_ES = 1 + then + if test -z "$ES_URI" + then + function join_es_hosts { + echo -n "$1:9200" + shift + printf "%s," "${@/#/:9200}" + } - ES=$(getent ahostsv4 $ES_HOSTNAME | awk '{ print $1 }' | sort -u) - if test -z "$ES" - then - echo "Warning automatic elasticsearch host config fails" - else - ES_URI=http://$(join_es_hosts $ES) - fi - fi - if test -n "$ES_URI" - then - echo Using elasticsearch uri: $ES_URI - echo search.uri=\"$ES_URI\" >> $CONFIG_FILE - else - echo elasticsearch host not configured - fi - fi + ES=$(getent ahostsv4 "$ES_HOSTNAME" | awk '{ print $1 }' | sort -u) + if test -z "$ES" + then + echo "Warning automatic elasticsearch host config fails" + else + ES_URI=http://$(join_es_hosts "$ES") + fi + fi + if test -n "$ES_URI" + then + echo "Using elasticsearch uri: $ES_URI" + echo "search.uri=\"$ES_URI\"" >> "$CONFIG_FILE" + else + echo elasticsearch host not configured + fi + fi - function join_urls { - echo -n \"$1\" - shift - for U do echo -n ,\"$U\"; done -# printf ",\"%s\"" $@ - } - test ${#ANALYZER_URLS} = 0 && ANALYZER_URLS+=$ANALYZER_PATH - test ${#RESPONDER_URLS} = 0 && RESPONDER_URLS+=$RESPONDER_PATH - - echo analyzer.urls=\[$(join_urls ${ANALYZER_URLS[@]})\] >> $CONFIG_FILE - echo responder.urls=\[$(join_urls ${RESPONDER_URLS[@]})\] >> $CONFIG_FILE + test -n "$JOB_DIRECTORY" && echo "job.directory=\"$JOB_DIRECTORY\"" >> "$CONFIG_FILE" + test -n "$DOCKER_JOB_DIRECTORY" && echo "job.dockerDirectory=\"$DOCKER_JOB_DIRECTORY\"" >> "$CONFIG_FILE" - echo 'include file("/etc/cortex/application.conf")' >> $CONFIG_FILE + function join_urls { + echo -n "\"$1\"" + shift + for U do echo -n ",\"$U\""; done +# printf ",\"%s\"" $@ + } + test ${#ANALYZER_URLS} = 0 && ANALYZER_URLS+=("$DEFAULT_ANALYZER_URL") + echo "analyzer.urls=[$(join_urls "${ANALYZER_URLS[@]}")]" >> "$CONFIG_FILE" + + test ${#RESPONDER_URLS} = 0 && RESPONDER_URLS+=("$DEFAULT_RESPONDER_URL") + echo "responder.urls=[$(join_urls "${RESPONDER_URLS[@]}")]" >> "$CONFIG_FILE" + + echo 'include file("/etc/cortex/application.conf")' >> "$CONFIG_FILE" fi -test $START_DOCKER = 1 && dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 &> /dev/null & -DOCKER_PID=$! +if test $START_DOCKER = 1 +then + usermod --append --groups docker "$DAEMON_USER" + dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 &> /dev/null & + DOCKER_PID=$! +fi echo config file is: -cat $CONFIG_FILE -su -s /bin/sh -c "cd /opt/cortex; bin/cortex \ - -Dconfig.file=$CONFIG_FILE \ - -Dlogger.file=/etc/cortex/logback.xml \ - -Dpidfile.path=/dev/null \ - $@" daemon +cat "$CONFIG_FILE" +chown -R "$DAEMON_USER" /var/log/cortex +chown -R "$DAEMON_USER" /etc/cortex +chown -R "$DAEMON_USER" "$CONFIG_FILE" +test -e /var/run/docker.sock && chown "$DAEMON_USER" /var/run/docker.sock +if test -n "$JOB_DIRECTORY" +then + mkdir -p "$JOB_DIRECTORY" + chown -R "$DAEMON_USER" "$JOB_DIRECTORY" +fi + +su "$DAEMON_USER" -c "bin/cortex \ + -Dconfig.file=$CONFIG_FILE \ + -Dlogger.file=/etc/cortex/logback.xml \ + -Dpidfile.path=/dev/null \ + $*" test $START_DOCKER = 1 && kill ${DOCKER_PID} diff --git a/project/Common.scala b/project/Common.scala index a0c28485d..4cf047169 100644 --- a/project/Common.scala +++ b/project/Common.scala @@ -33,7 +33,8 @@ object Common { javaOptions += "-Xmx1G", // Redirect logs from ElasticSearch (which uses log4j2) to slf4j libraryDependencies += "org.apache.logging.log4j" % "log4j-to-slf4j" % "2.9.1", - excludeDependencies += "org.apache.logging.log4j" % "log4j-core" + excludeDependencies += "org.apache.logging.log4j" % "log4j-core", + dependencyOverrides += "com.typesafe.akka" %% "akka-actor" % play.core.PlayVersion.akkaVersion ) val stableVersion: Regex = "(\\d+\\.\\d+\\.\\d+)-(\\d+)".r diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 5f4a361e3..f4854b8cb 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -1,7 +1,7 @@ import sbt._ object Dependencies { - val scalaVersion = "2.12.8" + val scalaVersion = "2.12.12" object Play { val version = play.core.PlayVersion.current @@ -16,9 +16,10 @@ object Dependencies { val scalaGuice = "net.codingwell" %% "scala-guice" % "4.1.0" - val reflections = "org.reflections" % "reflections" % "0.9.11" - val zip4j = "net.lingala.zip4j" % "zip4j" % "1.3.2" - val elastic4play = "org.thehive-project" %% "elastic4play" % "1.12.1" - val dockerClient = "com.spotify" % "docker-client" % "8.14.4" - val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % play.core.PlayVersion.akkaVersion + val reflections = "org.reflections" % "reflections" % "0.9.11" + val zip4j = "net.lingala.zip4j" % "zip4j" % "1.3.2" + val elastic4play = "org.thehive-project" %% "elastic4play" % "1.12.3" + val dockerClient = "com.spotify" % "docker-client" % "8.14.4" + val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % play.core.PlayVersion.akkaVersion + val akkaClusterTyped = "com.typesafe.akka" %% "akka-cluster-typed" % play.core.PlayVersion.akkaVersion } diff --git a/project/plugins.sbt b/project/plugins.sbt index 531c594a0..e25cbb8ad 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,6 +2,6 @@ logLevel := Level.Info // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.2") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.3") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2") addSbtPlugin("org.thehive-project" % "sbt-github-changelog" % "0.3.0") diff --git a/version.sbt b/version.sbt index 869c6fb5b..d9455fbd5 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "3.1.0-RC1-1" +version in ThisBuild := "3.1.0-1" diff --git a/www/package.json b/www/package.json index 9a3753b2c..19eb7fdf4 100755 --- a/www/package.json +++ b/www/package.json @@ -1,6 +1,6 @@ { "name": "cortex", - "version": "3.1.0-RC1", + "version": "3.1.0", "description": "A powerfull observable analysis engine", "license": "AGPL-3.0-or-later", "homepage": "https://github.com/TheHive-Project/Cortex", diff --git a/www/src/app/components/about/about.html b/www/src/app/components/about/about.html index 678415557..48be657e1 100644 --- a/www/src/app/components/about/about.html +++ b/www/src/app/components/about/about.html @@ -20,8 +20,8 @@
-
Copyright (C) 2014-2019 Thomas Franco, Saâd Kadhi, Jérôme Leonard
-
Copyright (C) 2017-2019 Nabil Adouani
+
Copyright (C) 2014-2020 Thomas Franco, Saâd Kadhi, Jérôme Leonard
+
Copyright (C) 2017-2020 Nabil Adouani
diff --git a/www/src/app/components/footer/footer.html b/www/src/app/components/footer/footer.html index 87a328c66..a7789169e 100755 --- a/www/src/app/components/footer/footer.html +++ b/www/src/app/components/footer/footer.html @@ -4,7 +4,7 @@
- TheHive Project 2016-2019, + TheHive Project 2016-2020, AGPL-V3
diff --git a/www/src/app/pages/admin/organizations/components/analyzers/analyzer.edit.controller.js b/www/src/app/pages/admin/organizations/components/analyzers/analyzer.edit.controller.js index 3fe66ad97..7a37ef011 100644 --- a/www/src/app/pages/admin/organizations/components/analyzers/analyzer.edit.controller.js +++ b/www/src/app/pages/admin/organizations/components/analyzers/analyzer.edit.controller.js @@ -40,10 +40,13 @@ export default class AnalyzerEditController { const property = item.name, configValue = (this.configuration.config || {})[property]; - analyzer.configuration[property] = - configValue || - item.defaultValue || - (item.multi ? [undefined] : undefined); + if(configValue !== undefined) { + analyzer.configuration[property] = configValue; + } else if (item.defaultValue !== undefined) { + analyzer.configuration[property] = item.defaultValue; + } else { + analyzer.configuration[property] = item.multi ? [undefined] : undefined; + } }); // Handle TLP default config diff --git a/www/src/app/pages/admin/organizations/components/analyzers/config-list.controller.js b/www/src/app/pages/admin/organizations/components/analyzers/config-list.controller.js index 967fbbfa8..6a924cd18 100644 --- a/www/src/app/pages/admin/organizations/components/analyzers/config-list.controller.js +++ b/www/src/app/pages/admin/organizations/components/analyzers/config-list.controller.js @@ -30,18 +30,17 @@ export default class OrganizationConfigsController { size: 'lg', resolve: { configuration: () => { - // let defaultValues = { - // string: null, - // number: 0, - // boolean: true - // }; let conf = angular.copy(config); _.forEach(conf.configurationItems, item => { - conf.config[item.name] = - conf.config[item.name] !== undefined ? - conf.config[item.name] : - item.defaultValue || (item.multi ? [undefined] : undefined); + if(conf.config[item.name] === undefined) { + + if (item.defaultValue !== undefined) { + conf.config[item.name] = item.defaultValue; + } else { + conf.config[item.name] = item.multi ? [undefined] : undefined; + } + } }); return conf; diff --git a/www/src/app/pages/admin/organizations/components/responders/responder.edit.controller.js b/www/src/app/pages/admin/organizations/components/responders/responder.edit.controller.js index d0510a64c..151842fd7 100644 --- a/www/src/app/pages/admin/organizations/components/responders/responder.edit.controller.js +++ b/www/src/app/pages/admin/organizations/components/responders/responder.edit.controller.js @@ -39,10 +39,13 @@ export default class ResponderEditController { const property = item.name, configValue = (this.configuration.config || {})[property]; - responder.configuration[property] = - configValue || - item.defaultValue || - (item.multi ? [undefined] : undefined); + if(configValue !== undefined) { + responder.configuration[property] = configValue; + } else if (item.defaultValue !== undefined) { + responder.configuration[property] = item.defaultValue; + } else { + responder.configuration[property] = item.multi ? [undefined] : undefined; + } }); // Handle TLP default config diff --git a/www/src/app/pages/login/login.page.html b/www/src/app/pages/login/login.page.html index 403d860c8..9bd227f1d 100644 --- a/www/src/app/pages/login/login.page.html +++ b/www/src/app/pages/login/login.page.html @@ -15,17 +15,19 @@
-
- +
+ +
+
+ + - -
- diff --git a/www/src/assets/styles/sass/index.scss b/www/src/assets/styles/sass/index.scss index c6b969871..619f0aca5 100755 --- a/www/src/assets/styles/sass/index.scss +++ b/www/src/assets/styles/sass/index.scss @@ -208,4 +208,23 @@ textarea.vertical { .text-capitalized { text-transform: capitalize; +} + +.or-separator { + display: flex; + align-items: center; + text-align: center; + color: #afafaf; +} +.or-separator::before, +.or-separator::after { + content: ''; + flex: 1; + border-bottom: 1px solid #cfcfcf; +} +.or-separator::before { + margin-right: .25em; +} +.or-separator::after { + margin-left: .25em; } \ No newline at end of file