Skip to content

Commit d18c5d1

Browse files
committed
release 4.1.0
1 parent fad1616 commit d18c5d1

41 files changed

Lines changed: 876 additions & 287 deletions

Some content is hidden

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

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ conf/application.conf
2121

2222
sbt-launch.jar
2323
.vscode
24+
.claude
25+
.site

CHANGELOG.md

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

3+
## Unreleased
4+
5+
- [DL-5871] fix: run analyzer/responder jobs on dedicated thread pools to keep the HTTP API responsive under heavy job load
6+
7+
**Upgrade note:** the `analyzer` and `responder` thread pools now use a `thread-pool-executor`
8+
(`fixed-pool-size`) instead of a `fork-join-executor`. Any custom
9+
`analyzer.fork-join-executor` / `responder.fork-join-executor` tuning in `application.conf` is
10+
no longer applied — switch to `analyzer.thread-pool-executor.fixed-pool-size`
11+
(see `conf/application.sample`).
12+
313
## 3.2.0 (2025-06-02)
414

515
- [DL-1231] Add support of Kubernetes

CLAUDE.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Stack
6+
7+
- **Backend:** Scala 2.13.17, Play Framework 3.0.x, Pekko 1.2 (cluster + typed), Guice DI (via `scala-guice`).
8+
- **Build:** sbt 1.11.7 (use the wrapper `./sbt`). JDK 11 (Amazon Corretto in CI).
9+
- **Search/storage:** ElasticSearch 8.19.x via `elastic4s` 8.19. No relational DB. Default HTTP port: `9001`.
10+
- **Frontend:** AngularJS 1.7 + Bootstrap 3, bundled with webpack 3, lives in `www/`. Node 18.16 in CI. Requires `npm install --legacy-peer-deps (--ignore-scripts for macos zsh)` (pinned legacy deps).
11+
- **Job execution:** runs analyzers/responders as Docker containers, Kubernetes pods, or local subprocesses.
12+
13+
## Commands
14+
15+
### Build / run
16+
- `./sbt run` — start the Cortex Play app (port 9001). First run also builds the front-end via the `FrontEnd` sbt plugin (`npm install --legacy-peer-deps && npm run build` inside `www/`).
17+
- `./sbt compile` — backend only.
18+
- `./sbt clean stage` — produces `target/universal/stage` runnable layout.
19+
- `./sbt Universal/packageBin` — full distributable zip (this is what CI runs alongside tests).
20+
- `./sbt Debian/packageBin Rpm/packageBin Docker/publishLocal` — OS packages and local Docker image. `DockerSettings` produces two images: `cortex` (slim) and `cortexWithDeps` (the `target/docker-withdeps` virtual project, used when you want bundled deps; see `build.sbt`).
21+
- Opt-in sbt plugins: `./sbt -Dplugins=sbom,depcheck …` enables `sbt-sbom` and `sbt-dependency-check` (off by default — see `project/plugins.sbt`).
22+
23+
### Frontend (standalone)
24+
- `cd www && npm install --legacy-peer-deps (--ignore-scripts for macos zsh)`
25+
- `npm run dev` — webpack-dev-server with hot reload.
26+
- `npm run build` — production bundle into `www/dist`, which `FrontEnd.scala` then packages into the Play assets.
27+
28+
### Tests
29+
- `./sbt test` — runs the whole suite. Tests are **forked** and **non-parallel** (`Test / fork := true`, `Test / parallelExecution := false` in `project/Common.scala`).
30+
- Single spec: `./sbt "testOnly org.thp.cortex.services.JobRunnerSrvSpec"` (Specs2 with `@RunWith(classOf[JUnitRunner])`).
31+
- Specs2 example filter: `./sbt "testOnly *JobRunnerSrvSpec -- only \"return the original image when prefix is empty\""`.
32+
33+
### Formatting
34+
- `./sbt scalafmtAll` (config in `.scalafmt.conf`, maxColumn 150, sorts imports/modifiers, rewrites unicode arrows). CI does not auto-format; run before committing.
35+
36+
## Architecture
37+
38+
### Multi-project sbt layout
39+
`build.sbt` defines three projects:
40+
- **`cortex`** (root, `app/`) — the Play app. Enables `PlayScala` + packaging plugins.
41+
- **`elastic4play`** (subdir `elastic4play/`) — an in-tree library wrapping ElasticSearch as a Play-friendly data layer: `ModelDef`/`EntityDef`/`AttributeDef` DSL, `CreateSrv`/`UpdateSrv`/`FindSrv`/`AttachmentSrv`, `MigrationCtrl`, and `auth/` provider scaffolding. Cortex `dependsOn(elastic4play)`. Changes to ES models or query plumbing usually live here, not in `app/`.
42+
- **`cortexWithDeps`** — virtual project at `target/docker-withdeps` purely to produce the `cortex-withdeps` Docker tag from the main project's mappings.
43+
44+
### Backend package layout (`app/org/thp/cortex`)
45+
- `Module.scala` — Guice bindings; registered via `play.modules.enabled += org.thp.cortex.Module` in `conf/reference.conf`.
46+
- `controllers/` — Play actions, one file per resource (Analyzer, Responder, Job, User, Organization, Stream, Misp, Auth, …). Routes wired in `conf/routes`.
47+
- `models/` — ES-backed entities built on the elastic4play attribute DSL (`Job`, `Worker`, `Organization`, `User`, `Report`, `Artifact`, `Audit`, `WorkerConfig`, `WorkerDefinition`).
48+
- `services/` — business logic. Most controllers are thin wrappers around a `*Srv`.
49+
- `services/mappers/` — group/role mappers used by external auth (LDAP/AD/OAuth2 → org+role mapping).
50+
51+
### Worker model (analyzers + responders)
52+
- A **Worker** is the running instance of a **WorkerDefinition** (analyzer or responder catalog entry, loaded from URLs in `analyzer.urls` / `responder.urls`). `WorkerSrv` loads definitions on startup and on demand, with `worker.updateDockerImage = true` triggering image refreshes when the catalog changes.
53+
- A **Job** is one execution of a Worker against an artifact; it produces a **Report** and possibly child **Artifacts** (extracted IoCs). See `models/Job.scala` for the `JobStatus` enum (`Waiting`, `InProgress`, `Success`, `Failure`, `Deleted`) and the attribute schema.
54+
- **Caching:** identical `(worker, data)` jobs within `cache.job` (default 10 min) reuse the previous report — the cache key is `cacheTag` on the Job.
55+
56+
### Job runner selection (`JobRunnerSrv`)
57+
At startup, `job.runners` in config (default `[kubernetes, docker, process]`) is filtered down to actually-available runners:
58+
- `kubernetes` requires the fabric8 client to detect a cluster (`K8sJobRunnerSrv.isAvailable`).
59+
- `docker` requires a reachable Docker daemon (`DockerJobRunnerSrv.isAvailable`).
60+
- `process` requires `cortexutils` Python package ≥ 2.0 to be installed (probed for `python`, `python2`, `python3`).
61+
62+
Runners are tried **in the configured order** for each job — first one able to run the worker wins. When editing runner logic, keep in mind the docker image name can be rewritten through `docker.imageRegistryPrefix` (see `JobRunnerSrv.applyImagePrefix` and its spec).
63+
64+
### Auth
65+
`auth.provider` is a **list** evaluated in order (`local`, `ad`, `ldap`, `oauth2`, `key`). `CortexAuthSrv` composes them; multi-valued is the supported way to migrate users between providers. API-key auth (`KeyAuthSrv`) is always available alongside whatever interactive providers are configured.
66+
67+
### Configuration
68+
- `conf/reference.conf` ships defaults; operators override via `conf/application.conf` (template: `conf/application.sample`).
69+
- Job runner, cache TTLs, ES connection, auth providers and per-provider config, and analyzer/responder catalogs all live in HOCON.
70+
71+
## Conventions specific to this codebase
72+
73+
- **Models are not plain case classes** — they extend `ModelDef[…]` + a `*Attributes` trait from elastic4play. Adding a field means editing both the trait and (often) a migration. Look at `models/Job.scala` for the canonical pattern.
74+
- **No DB migrations file** — schema lives in code; the `MigrationCtrl` endpoint (`POST /api/maintenance/migrate`) runs version-aware migrations registered through elastic4play.
75+
- **`organization` is the tenant boundary.** Almost every model carries an `organization` attribute and `*Srv` queries scope by it via `AuthContext`. Don't add cross-organization queries without explicit ACL handling.
76+
- **Routes file is the source of truth** for the public API surface (`conf/routes`) — there's no annotation-based routing.
77+
- **The front-end is legacy AngularJS 1.x** and is *not* under active framework upgrades; keep changes minimal and idiomatic to the existing module structure (`www/src/app/{components,pages,core}`).
78+
79+
## Repository docs / refs
80+
81+
- `README.md` — high-level product description and links to external docs.
82+
- `CHANGELOG.md` — release-by-release feature/fix list (DL-xxxx ticket prefixes match the team's Jira).
83+
- External docs site: <https://docs.strangebee.com/cortex/> (generated from `docs/`, built via `.github/workflows/build.docs.yaml`).
84+
- This repo is mirrored to the public OSS repo at <https://github.com/TheHive-Project/Cortex>. The release process is **manual** — pushes to the mirror and version bumps are not automated.

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
package org.thp.cortex.controllers
22

3-
import javax.inject.{Inject, Singleton}
4-
import scala.concurrent.{ExecutionContext, Future}
5-
3+
import org.elastic4play.BadRequestError
4+
import org.elastic4play.controllers.{Authenticated, Fields, FieldsBodyParser, Renderer}
5+
import org.thp.cortex.models.{BaseConfig, Roles}
6+
import org.thp.cortex.services.AnalyzerConfigSrv
7+
import play.api.Logger
68
import play.api.libs.json.JsObject
79
import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents}
810

9-
import org.thp.cortex.models.{BaseConfig, Roles}
10-
import org.thp.cortex.services.{AnalyzerConfigSrv, UserSrv}
11-
12-
import org.elastic4play.BadRequestError
13-
import org.elastic4play.controllers.{Authenticated, Fields, FieldsBodyParser, Renderer}
11+
import javax.inject.{Inject, Singleton}
12+
import scala.concurrent.{ExecutionContext, Future}
13+
import scala.util.chaining.scalaUtilChainingOps
1414

1515
@Singleton
1616
class AnalyzerConfigCtrl @Inject() (
1717
analyzerConfigSrv: AnalyzerConfigSrv,
18-
userSrv: UserSrv,
1918
authenticated: Authenticated,
2019
fieldsBodyParser: FieldsBodyParser,
2120
renderer: Renderer,
2221
components: ControllerComponents,
2322
implicit val ec: ExecutionContext
2423
) extends AbstractController(components) {
2524

25+
private lazy val logger: Logger = Logger(getClass.getName)
26+
2627
def get(analyzerConfigName: String): Action[AnyContent] = authenticated(Roles.orgAdmin).async { request =>
2728
analyzerConfigSrv
2829
.getForUser(request.userId, analyzerConfigName)
@@ -50,6 +51,7 @@ class AnalyzerConfigCtrl @Inject() (
5051
analyzerConfigSrv
5152
.updateOrCreate(request.userId, analyzerConfigName, config)
5253
.map(renderer.toOutput(OK, _))
54+
.tap(_ => logger.info(s"Analyzer $analyzerConfigName updated with $config by user id ${request.userId}"))
5355
case None => Future.failed(BadRequestError("attribute config has invalid format"))
5456
}
5557
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ trait AssetCtrl {
1313
}
1414

1515
@Singleton
16-
class AssetCtrlProd @Inject() (errorHandler: HttpErrorHandler, meta: AssetsMetadata, env: Environment) extends Assets(errorHandler, meta, env) with AssetCtrl {
16+
class AssetCtrlProd @Inject() (errorHandler: HttpErrorHandler, meta: AssetsMetadata, env: Environment)
17+
extends Assets(errorHandler, meta, env)
18+
with AssetCtrl {
1719
def get(file: String): Action[AnyContent] = at("/www", file)
1820
}
1921

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

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
11
package org.thp.cortex.controllers
22

3-
import javax.inject.{Inject, Singleton}
4-
5-
import scala.concurrent.{ExecutionContext, Future}
6-
7-
import play.api.Logger
8-
import play.api.http.Status
9-
import play.api.mvc._
10-
11-
import org.thp.cortex.models.Roles
12-
import org.thp.cortex.services.{OrganizationSrv, UserSrv}
13-
14-
import org.elastic4play.{BadRequestError, NotFoundError}
153
import org.elastic4play.controllers.{Authenticated, Fields, FieldsBodyParser, Renderer}
164
import org.elastic4play.models.JsonFormat.baseModelEntityWrites
175
import org.elastic4play.services.JsonFormat.{aggReads, queryReads}
186
import org.elastic4play.services.{UserSrv => _, _}
7+
import org.elastic4play.{BadRequestError, NotFoundError}
8+
import org.thp.cortex.models.Roles
9+
import org.thp.cortex.services.{OrganizationSrv, UserSrv}
10+
import play.api.Logger
11+
import play.api.http.Status
12+
import play.api.mvc._
13+
14+
import javax.inject.{Inject, Singleton}
15+
import scala.concurrent.{ExecutionContext, Future}
1916

2017
@Singleton
2118
class OrganizationCtrl @Inject() (
2219
organizationSrv: OrganizationSrv,
23-
authSrv: AuthSrv,
2420
auxSrv: AuxSrv,
2521
userSrv: UserSrv,
2622
authenticated: Authenticated,
@@ -36,7 +32,10 @@ class OrganizationCtrl @Inject() (
3632
def create: Action[Fields] = authenticated(Roles.superAdmin).async(fieldsBodyParser) { implicit request =>
3733
organizationSrv
3834
.create(request.body)
39-
.map(organization => renderer.toOutput(CREATED, organization))
35+
.map { organization =>
36+
logger.info(s"Organization ${organization.id} created by user ${request.userId}")
37+
renderer.toOutput(CREATED, organization)
38+
}
4039
}
4140

4241
def get(organizationId: String): Action[Fields] = authenticated(Roles.superAdmin, Roles.orgAdmin).async(fieldsBodyParser) { implicit request =>
@@ -55,9 +54,12 @@ class OrganizationCtrl @Inject() (
5554
if (organizationId == "cortex")
5655
Future.failed(BadRequestError("Cortex organization can't be updated"))
5756
else
58-
organizationSrv.update(organizationId, request.body).map { organization =>
59-
renderer.toOutput(OK, organization)
60-
}
57+
organizationSrv
58+
.update(organizationId, request.body)
59+
.map { organization =>
60+
logger.info(s"Organization ${organization.id} updated by user ${request.userId}")
61+
renderer.toOutput(OK, organization)
62+
}
6163
}
6264

6365
def delete(organizationId: String): Action[AnyContent] = authenticated(Roles.superAdmin).async { implicit request =>
@@ -66,7 +68,10 @@ class OrganizationCtrl @Inject() (
6668
else
6769
organizationSrv
6870
.delete(organizationId)
69-
.map(_ => NoContent)
71+
.map { organization =>
72+
logger.info(s"Organization ${organization.id} deleted by user ${request.userId}")
73+
NoContent
74+
}
7075
}
7176

7277
def find: Action[Fields] = authenticated(Roles.superAdmin).async(fieldsBodyParser) { implicit request =>

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

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
11
package org.thp.cortex.controllers
22

3-
import scala.concurrent.{ExecutionContext, Future}
4-
3+
import org.elastic4play.BadRequestError
4+
import org.elastic4play.controllers.{Authenticated, Fields, FieldsBodyParser, Renderer}
5+
import org.thp.cortex.models.{BaseConfig, Roles}
6+
import org.thp.cortex.services.ResponderConfigSrv
7+
import play.api.Logger
58
import play.api.libs.json.JsObject
69
import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents}
710

811
import javax.inject.{Inject, Singleton}
9-
import org.thp.cortex.models.{BaseConfig, Roles}
10-
import org.thp.cortex.services.{ResponderConfigSrv, UserSrv}
11-
12-
import org.elastic4play.BadRequestError
13-
import org.elastic4play.controllers.{Authenticated, Fields, FieldsBodyParser, Renderer}
12+
import scala.concurrent.{ExecutionContext, Future}
13+
import scala.util.chaining.scalaUtilChainingOps
1414

1515
@Singleton
1616
class ResponderConfigCtrl @Inject() (
1717
responderConfigSrv: ResponderConfigSrv,
18-
userSrv: UserSrv,
1918
authenticated: Authenticated,
2019
fieldsBodyParser: FieldsBodyParser,
2120
renderer: Renderer,
2221
components: ControllerComponents,
2322
implicit val ec: ExecutionContext
2423
) extends AbstractController(components) {
2524

26-
def get(analyzerConfigName: String): Action[AnyContent] = authenticated(Roles.orgAdmin).async { request =>
25+
private lazy val logger: Logger = Logger(getClass.getName)
26+
27+
def get(responderConfigName: String): Action[AnyContent] = authenticated(Roles.orgAdmin).async { request =>
2728
responderConfigSrv
28-
.getForUser(request.userId, analyzerConfigName)
29+
.getForUser(request.userId, responderConfigName)
2930
.map(renderer.toOutput(OK, _))
3031
}
3132

@@ -44,12 +45,13 @@ class ResponderConfigCtrl @Inject() (
4445
}
4546
}
4647

47-
def update(analyzerConfigName: String): Action[Fields] = authenticated(Roles.orgAdmin).async(fieldsBodyParser) { implicit request =>
48+
def update(responderConfigName: String): Action[Fields] = authenticated(Roles.orgAdmin).async(fieldsBodyParser) { implicit request =>
4849
request.body.getValue("config").flatMap(_.asOpt[JsObject]) match {
4950
case Some(config) =>
5051
responderConfigSrv
51-
.updateOrCreate(request.userId, analyzerConfigName, config)
52+
.updateOrCreate(request.userId, responderConfigName, config)
5253
.map(renderer.toOutput(OK, _))
54+
.tap(_ => logger.info(s"Responder $responderConfigName updated with $config by user id ${request.userId}"))
5355
case None => Future.failed(BadRequestError("attribute config has invalid format"))
5456
}
5557
}

0 commit comments

Comments
 (0)