Skip to content

Commit da75a6e

Browse files
committed
Merge branch 'release/3.1.0'
2 parents a0f9053 + 9be2418 commit da75a6e

31 files changed

+478
-243
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Change Log
22

3+
## [3.1.0](https://github.com/TheHive-Project/Cortex/milestone/27) (2020-10-30)
4+
5+
**Implemented enhancements:**
6+
7+
- Improve Docker image [\#296](https://github.com/TheHive-Project/Cortex/issues/296)
8+
- Impossible to load catalog through a proxy [\#297](https://github.com/TheHive-Project/Cortex/issues/297)
9+
- Update login page design [\#303](https://github.com/TheHive-Project/Cortex/issues/303)
10+
11+
**Fixed bugs:**
12+
13+
- [Bug] Cortex and boolean ConfigurationItems [\#309](https://github.com/TheHive-Project/Cortex/issues/309)
14+
315
## [3.1.0-RC1](https://github.com/TheHive-Project/Cortex/milestone/21) (2020-08-13)
416

517
**Implemented enhancements:**

app/org/thp/cortex/controllers/AttachmentCtrl.scala

Lines changed: 58 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,21 @@ package org.thp.cortex.controllers
22

33
import java.net.URLEncoder
44
import java.nio.file.Files
5-
import javax.inject.{Inject, Singleton}
6-
7-
import play.api.http.HttpEntity
8-
import play.api.libs.Files.DefaultTemporaryFileCreator
9-
import play.api.mvc._
10-
import play.api.{mvc, Configuration}
115

126
import akka.stream.scaladsl.FileIO
7+
import javax.inject.{Inject, Singleton}
138
import net.lingala.zip4j.core.ZipFile
149
import net.lingala.zip4j.model.ZipParameters
1510
import net.lingala.zip4j.util.Zip4jConstants
16-
import org.thp.cortex.models.Roles
17-
1811
import org.elastic4play.Timed
19-
import org.elastic4play.controllers.{Authenticated, Renderer}
12+
import org.elastic4play.controllers.Authenticated
2013
import org.elastic4play.models.AttachmentAttributeFormat
21-
import org.elastic4play.services.AttachmentSrv
14+
import org.elastic4play.services.{AttachmentSrv, ExecutionContextSrv}
15+
import org.thp.cortex.models.Roles
16+
import play.api.http.HttpEntity
17+
import play.api.libs.Files.DefaultTemporaryFileCreator
18+
import play.api.mvc._
19+
import play.api.{mvc, Configuration}
2220

2321
/**
2422
* Controller used to access stored attachments (plain or zipped)
@@ -30,7 +28,7 @@ class AttachmentCtrl(
3028
attachmentSrv: AttachmentSrv,
3129
authenticated: Authenticated,
3230
components: ControllerComponents,
33-
renderer: Renderer
31+
executionContextSrv: ExecutionContextSrv
3432
) extends AbstractController(components) {
3533

3634
@Inject() def this(
@@ -39,9 +37,9 @@ class AttachmentCtrl(
3937
attachmentSrv: AttachmentSrv,
4038
authenticated: Authenticated,
4139
components: ControllerComponents,
42-
renderer: Renderer
40+
executionContextSrv: ExecutionContextSrv
4341
) =
44-
this(configuration.get[String]("datastore.attachment.password"), tempFileCreator, attachmentSrv, authenticated, components, renderer)
42+
this(configuration.get[String]("datastore.attachment.password"), tempFileCreator, attachmentSrv, authenticated, components, executionContextSrv)
4543

4644
/**
4745
* Download an attachment, identified by its hash, in plain format
@@ -50,16 +48,25 @@ class AttachmentCtrl(
5048
*/
5149
@Timed("controllers.AttachmentCtrl.download")
5250
def download(hash: String, name: Option[String]): Action[AnyContent] = authenticated(Roles.read) { _ =>
53-
if (hash.startsWith("{{")) // angularjs hack
54-
NoContent
55-
else if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty)
56-
mvc.Results.BadRequest("File name is invalid")
57-
else
58-
Result(
59-
header = ResponseHeader(200, Map("Content-Disposition" -> s"""attachment; filename="${URLEncoder
60-
.encode(name.getOrElse(hash), "utf-8")}"""", "Content-Transfer-Encoding" -> "binary")),
61-
body = HttpEntity.Streamed(attachmentSrv.source(hash), None, None)
62-
)
51+
executionContextSrv.withDefault { implicit ec =>
52+
if (hash.startsWith("{{")) // angularjs hack
53+
NoContent
54+
else if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty)
55+
mvc.Results.BadRequest("File name is invalid")
56+
else
57+
Result(
58+
header = ResponseHeader(
59+
200,
60+
Map(
61+
"Content-Disposition" ->
62+
s"""attachment; filename="${URLEncoder
63+
.encode(name.getOrElse(hash), "utf-8")}"""",
64+
"Content-Transfer-Encoding" -> "binary"
65+
)
66+
),
67+
body = HttpEntity.Streamed(attachmentSrv.source(hash), None, None)
68+
)
69+
}
6370
}
6471

6572
/**
@@ -69,33 +76,35 @@ class AttachmentCtrl(
6976
*/
7077
@Timed("controllers.AttachmentCtrl.downloadZip")
7178
def downloadZip(hash: String, name: Option[String]): Action[AnyContent] = authenticated(Roles.read) { _ =>
72-
if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty)
73-
BadRequest("File name is invalid")
74-
else {
75-
val f = tempFileCreator.create("zip", hash).path
76-
Files.delete(f)
77-
val zipFile = new ZipFile(f.toFile)
78-
val zipParams = new ZipParameters
79-
zipParams.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_FASTEST)
80-
zipParams.setEncryptFiles(true)
81-
zipParams.setEncryptionMethod(Zip4jConstants.ENC_METHOD_STANDARD)
82-
zipParams.setPassword(password)
83-
zipParams.setFileNameInZip(name.getOrElse(hash))
84-
zipParams.setSourceExternalStream(true)
85-
zipFile.addStream(attachmentSrv.stream(hash), zipParams)
79+
executionContextSrv.withDefault { implicit ec =>
80+
if (!name.getOrElse("").intersect(AttachmentAttributeFormat.forbiddenChar).isEmpty)
81+
BadRequest("File name is invalid")
82+
else {
83+
val f = tempFileCreator.create("zip", hash).path
84+
Files.delete(f)
85+
val zipFile = new ZipFile(f.toFile)
86+
val zipParams = new ZipParameters
87+
zipParams.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_FASTEST)
88+
zipParams.setEncryptFiles(true)
89+
zipParams.setEncryptionMethod(Zip4jConstants.ENC_METHOD_STANDARD)
90+
zipParams.setPassword(password)
91+
zipParams.setFileNameInZip(name.getOrElse(hash))
92+
zipParams.setSourceExternalStream(true)
93+
zipFile.addStream(attachmentSrv.stream(hash), zipParams)
8694

87-
Result(
88-
header = ResponseHeader(
89-
200,
90-
Map(
91-
"Content-Disposition" -> s"""attachment; filename="${URLEncoder.encode(name.getOrElse(hash), "utf-8")}.zip"""",
92-
"Content-Type" -> "application/zip",
93-
"Content-Transfer-Encoding" -> "binary",
94-
"Content-Length" -> Files.size(f).toString
95-
)
96-
),
97-
body = HttpEntity.Streamed(FileIO.fromPath(f), Some(Files.size(f)), Some("application/zip"))
98-
)
95+
Result(
96+
header = ResponseHeader(
97+
200,
98+
Map(
99+
"Content-Disposition" -> s"""attachment; filename="${URLEncoder.encode(name.getOrElse(hash), "utf-8")}.zip"""",
100+
"Content-Type" -> "application/zip",
101+
"Content-Transfer-Encoding" -> "binary",
102+
"Content-Length" -> Files.size(f).toString
103+
)
104+
),
105+
body = HttpEntity.Streamed(FileIO.fromPath(f), Some(Files.size(f)), Some("application/zip"))
106+
)
107+
}
99108
}
100109
}
101110
}

app/org/thp/cortex/models/Migration.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,6 @@ class Migration @Inject() (userSrv: UserSrv, organizationSrv: OrganizationSrv, w
8484
("sequenceCounter" -> counter)
8585
}
8686
)
87+
case DatabaseState(4) => Nil
8788
}
8889
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
package org.thp.cortex
22

33
package object models {
4-
val modelVersion = 4
4+
val modelVersion = 5
55
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package org.thp.cortex.services
2+
3+
import scala.util.control.NonFatal
4+
5+
import javax.inject.{Inject, Singleton}
6+
import play.api.inject.ApplicationLifecycle
7+
import play.api.libs.ws._
8+
import play.api.libs.ws.ahc.{AhcWSClient, AhcWSClientConfig, AhcWSClientConfigParser}
9+
import play.api.{Configuration, Environment, Logger}
10+
11+
import akka.stream.Materializer
12+
import com.typesafe.sslconfig.ssl.TrustStoreConfig
13+
14+
object CustomWSAPI {
15+
private[CustomWSAPI] lazy val logger = Logger(getClass)
16+
17+
def parseWSConfig(config: Configuration): AhcWSClientConfig =
18+
new AhcWSClientConfigParser(new WSConfigParser(config.underlying, getClass.getClassLoader).parse(), config.underlying, getClass.getClassLoader)
19+
.parse()
20+
21+
def parseProxyConfig(config: Configuration): Option[WSProxyServer] =
22+
config.getOptional[Configuration]("play.ws.proxy").map { proxyConfig
23+
DefaultWSProxyServer(
24+
proxyConfig.get[String]("host"),
25+
proxyConfig.get[Int]("port"),
26+
proxyConfig.getOptional[String]("protocol"),
27+
proxyConfig.getOptional[String]("user"),
28+
proxyConfig.getOptional[String]("password"),
29+
proxyConfig.getOptional[String]("ntlmDomain"),
30+
proxyConfig.getOptional[String]("encoding"),
31+
proxyConfig.getOptional[Seq[String]]("nonProxyHosts")
32+
)
33+
}
34+
35+
def getWS(config: Configuration)(implicit mat: Materializer): AhcWSClient = {
36+
val clientConfig = parseWSConfig(config)
37+
val clientConfigWithTruststore = config.getOptional[String]("play.cert") match {
38+
case Some(p)
39+
logger.warn("""Use of "cert" parameter in configuration file is deprecated. Please use:
40+
| ws.ssl {
41+
| trustManager = {
42+
| stores = [
43+
| { type = "PEM", path = "/path/to/cacert.crt" },
44+
| { type = "JKS", path = "/path/to/truststore.jks" }
45+
| ]
46+
| }
47+
| }
48+
""".stripMargin)
49+
clientConfig.copy(
50+
wsClientConfig = clientConfig
51+
.wsClientConfig
52+
.copy(
53+
ssl = clientConfig
54+
.wsClientConfig
55+
.ssl
56+
.withTrustManagerConfig(
57+
clientConfig
58+
.wsClientConfig
59+
.ssl
60+
.trustManagerConfig
61+
.withTrustStoreConfigs(
62+
clientConfig.wsClientConfig.ssl.trustManagerConfig.trustStoreConfigs :+ TrustStoreConfig(
63+
filePath = Some(p),
64+
data = None
65+
)
66+
)
67+
)
68+
)
69+
)
70+
case None clientConfig
71+
}
72+
AhcWSClient(clientConfigWithTruststore, None)
73+
}
74+
75+
def getConfig(config: Configuration, path: String): Configuration =
76+
Configuration(
77+
config
78+
.getOptional[Configuration](s"play.$path")
79+
.getOrElse(Configuration.empty)
80+
.underlying
81+
.withFallback(config.getOptional[Configuration](path).getOrElse(Configuration.empty).underlying)
82+
)
83+
}
84+
85+
@Singleton
86+
class CustomWSAPI(
87+
ws: AhcWSClient,
88+
val proxy: Option[WSProxyServer],
89+
config: Configuration,
90+
environment: Environment,
91+
lifecycle: ApplicationLifecycle,
92+
mat: Materializer
93+
) extends WSClient {
94+
private[CustomWSAPI] lazy val logger = Logger(getClass)
95+
96+
@Inject() def this(config: Configuration, environment: Environment, lifecycle: ApplicationLifecycle, mat: Materializer) =
97+
this(CustomWSAPI.getWS(config)(mat), CustomWSAPI.parseProxyConfig(config), config, environment, lifecycle, mat)
98+
99+
override def close(): Unit = ws.close()
100+
101+
override def url(url: String): WSRequest = {
102+
val req = ws.url(url)
103+
proxy.fold(req)(req.withProxyServer)
104+
}
105+
106+
override def underlying[T]: T = ws.underlying[T]
107+
108+
def withConfig(subConfig: Configuration): CustomWSAPI = {
109+
logger.debug(s"Override WS configuration using $subConfig")
110+
try {
111+
new CustomWSAPI(Configuration(subConfig.underlying.atKey("play").withFallback(config.underlying)), environment, lifecycle, mat)
112+
} catch {
113+
case NonFatal(e)
114+
logger.error(s"WSAPI configuration error, use default values", e)
115+
this
116+
}
117+
}
118+
}

app/org/thp/cortex/services/DockerJobRunnerSrv.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ import org.thp.cortex.models._
2121
import org.elastic4play.utils.RichFuture
2222

2323
@Singleton
24-
class DockerJobRunnerSrv(client: DockerClient, autoUpdate: Boolean, implicit val system: ActorSystem) {
24+
class DockerJobRunnerSrv(
25+
client: DockerClient,
26+
autoUpdate: Boolean,
27+
jobBaseDirectory: Path,
28+
dockerJobBaseDirectory: Path,
29+
implicit val system: ActorSystem
30+
) {
2531

2632
@Inject()
2733
def this(config: Configuration, system: ActorSystem) =
@@ -37,6 +43,8 @@ class DockerJobRunnerSrv(client: DockerClient, autoUpdate: Boolean, implicit val
3743
.useProxy(config.getOptional[Boolean]("docker.useProxy").getOrElse(false))
3844
.build(),
3945
config.getOptional[Boolean]("docker.autoUpdate").getOrElse(true),
46+
Paths.get(config.get[String]("job.directory")),
47+
Paths.get(config.get[String]("job.dockerDirectory")),
4048
system: ActorSystem
4149
)
4250

@@ -60,7 +68,7 @@ class DockerJobRunnerSrv(client: DockerClient, autoUpdate: Boolean, implicit val
6068
.builder()
6169
.appendBinds(
6270
Bind
63-
.from(jobDirectory.toAbsolutePath.toString)
71+
.from(dockerJobBaseDirectory.resolve(jobBaseDirectory.relativize(jobDirectory)).toAbsolutePath.toString)
6472
.to("/job")
6573
.readOnly(false)
6674
.build()

app/org/thp/cortex/services/JobRunnerSrv.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class JobRunnerSrv @Inject() (
4242
val logger = Logger(getClass)
4343
lazy val analyzerExecutionContext: ExecutionContext = akkaSystem.dispatchers.lookup("analyzer")
4444
lazy val responderExecutionContext: ExecutionContext = akkaSystem.dispatchers.lookup("responder")
45+
val jobDirectory: Path = Paths.get(config.get[String]("job.directory"))
4546

4647
private val runners: Seq[String] = config
4748
.getOptional[Seq[String]]("job.runners")
@@ -89,7 +90,7 @@ class JobRunnerSrv @Inject() (
8990
}
9091

9192
private def prepareJobFolder(worker: Worker, job: Job): Future[Path] = {
92-
val jobFolder = Files.createTempDirectory(Paths.get(System.getProperty("java.io.tmpdir")), s"cortex-job-${job.id}-")
93+
val jobFolder = Files.createTempDirectory(jobDirectory, s"cortex-job-${job.id}-")
9394
val inputJobFolder = Files.createDirectories(jobFolder.resolve("input"))
9495
Files.createDirectories(jobFolder.resolve("output"))
9596

app/org/thp/cortex/services/OAuth2Srv.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ case class OAuth2Config(
2323
authorizationUrl: String,
2424
tokenUrl: String,
2525
userUrl: String,
26-
scope: String,
26+
scope: Seq[String],
2727
authorizationHeader: String,
2828
autoupdate: Boolean,
2929
autocreate: Boolean
@@ -41,7 +41,7 @@ object OAuth2Config {
4141
authorizationUrl <- configuration.getOptional[String]("auth.oauth2.authorizationUrl")
4242
tokenUrl <- configuration.getOptional[String]("auth.oauth2.tokenUrl")
4343
userUrl <- configuration.getOptional[String]("auth.oauth2.userUrl")
44-
scope <- configuration.getOptional[String]("auth.oauth2.scope")
44+
scope <- configuration.getOptional[Seq[String]]("auth.oauth2.scope")
4545
authorizationHeader = configuration.getOptional[String]("auth.oauth2.authorizationHeader").getOrElse("Bearer")
4646
autocreate = configuration.getOptional[Boolean]("auth.sso.autocreate").getOrElse(false)
4747
autoupdate = configuration.getOptional[Boolean]("auth.sso.autoupdate").getOrElse(false)
@@ -109,7 +109,7 @@ class OAuth2Srv(
109109
private def authRedirect(oauth2Config: OAuth2Config): Result = {
110110
val state = UUID.randomUUID().toString
111111
val queryStringParams = Map[String, Seq[String]](
112-
"scope" -> Seq(oauth2Config.scope),
112+
"scope" -> Seq(oauth2Config.scope.mkString(" ")),
113113
"response_type" -> Seq(oauth2Config.responseType),
114114
"redirect_uri" -> Seq(oauth2Config.redirectUri),
115115
"client_id" -> Seq(oauth2Config.clientId),

0 commit comments

Comments
 (0)