├── .gitattributes ├── .github ├── actions │ └── get_code_version │ │ └── action.yml ├── issue_template.md └── workflows │ ├── build.docker.yml │ ├── build.packages.yml │ ├── check_code.yml │ ├── deploy.nomad.yml │ ├── setup.fixtures.yml │ └── tests.all.yml ├── .gitignore ├── .scalafmt.conf ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── PGP-PUBLIC-KEY ├── README.md ├── SECURITY.md ├── app └── org │ └── thp │ └── cortex │ ├── Module.scala │ ├── controllers │ ├── AnalyzerConfigCtrl.scala │ ├── AnalyzerCtrl.scala │ ├── AssetCtrl.scala │ ├── AttachmentCtrl.scala │ ├── AuthenticationCtrl.scala │ ├── DBListCtrl.scala │ ├── Home.scala │ ├── JobCtrl.scala │ ├── MispCtrl.scala │ ├── OrganizationCtrl.scala │ ├── ResponderConfigCtrl.scala │ ├── ResponderCtrl.scala │ ├── StatusCtrl.scala │ ├── StreamCtrl.scala │ └── UserCtrl.scala │ ├── models │ ├── Artifact.scala │ ├── Audit.scala │ ├── BaseConfig.scala │ ├── Errors.scala │ ├── Job.scala │ ├── JsonFormat.scala │ ├── Migration.scala │ ├── Organization.scala │ ├── Report.scala │ ├── Roles.scala │ ├── TlpAttributeFormat.scala │ ├── User.scala │ ├── Worker.scala │ ├── WorkerConfig.scala │ ├── WorkerDefinition.scala │ └── package.scala │ ├── services │ ├── AccessLogFilter.scala │ ├── AnalyzerConfigSrv.scala │ ├── AuditActor.scala │ ├── CSRFFilter.scala │ ├── CortexAuthSrv.scala │ ├── CustomWSAPI.scala │ ├── DockerJobRunnerSrv.scala │ ├── ErrorHandler.scala │ ├── JobRunnerSrv.scala │ ├── JobSrv.scala │ ├── K8sJobRunnerSrv.scala │ ├── KeyAuthSrv.scala │ ├── LocalAuthSrv.scala │ ├── MispSrv.scala │ ├── OAuth2Srv.scala │ ├── OrganizationSrv.scala │ ├── ProcessJobRunnerSrv.scala │ ├── ResponderConfigSrv.scala │ ├── StreamMessage.scala │ ├── StreamSrv.scala │ ├── UserSrv.scala │ ├── WorkerConfigSrv.scala │ ├── WorkerSrv.scala │ └── mappers │ │ ├── GroupUserMapper.scala │ │ ├── MultiUserMapperSrv.scala │ │ ├── SimpleUserMapper.scala │ │ └── UserMapper.scala │ └── util │ ├── FunctionalCondition.scala │ ├── JsonConfig.scala │ └── docker │ ├── DockerClient.scala │ └── DockerLogsStringBuilder.scala ├── bin ├── activator └── activator.bat ├── build.sbt ├── builds └── docker │ └── Dockerfile ├── code_of_conduct.md ├── conf ├── application.sample ├── logback.xml ├── reference.conf └── routes ├── contrib └── misp-modules-loader.py ├── deployment └── nomad │ └── packs │ └── cortex │ ├── CHANGELOG.md │ ├── README.md │ ├── metadata.hcl │ ├── outputs.tpl │ ├── templates │ ├── _helpers.tpl │ └── cortex.nomad.tpl │ └── variables.hcl ├── docker └── cortex │ ├── .env │ └── docker-compose.yml ├── elastic4play ├── .drone.yml ├── .gitignore ├── .scalafmt.conf ├── CHANGELOG.md ├── LICENSE ├── app │ └── org │ │ └── elastic4play │ │ ├── ErrorHandler.scala │ │ ├── Errors.scala │ │ ├── JsonFormat.scala │ │ ├── Timed.java │ │ ├── controllers │ │ ├── Authenticated.scala │ │ ├── Fields.scala │ │ ├── JsonFormat.scala │ │ ├── MigrationCtrl.scala │ │ └── Renderer.scala │ │ ├── database │ │ ├── DBConfiguration.scala │ │ ├── DBCreate.scala │ │ ├── DBFind.scala │ │ ├── DBGet.scala │ │ ├── DBIndex.scala │ │ ├── DBModify.scala │ │ ├── DBRemove.scala │ │ ├── DBSequence.scala │ │ └── DBUtils.scala │ │ ├── models │ │ ├── AttachmentAttributeFormat.scala │ │ ├── Attributes.scala │ │ ├── BinaryAttributeFormat.scala │ │ ├── BooleanAttributeFormat.scala │ │ ├── CustomAttributeFormat.scala │ │ ├── DateAttributeFormat.scala │ │ ├── EnumerationAttributeFormat.scala │ │ ├── Errors.scala │ │ ├── HashAttributeFormat.scala │ │ ├── HiveEnumeration.scala │ │ ├── JsonFormat.scala │ │ ├── ListEnumerationAttributeFormat.scala │ │ ├── MetricsAttributeFormat.scala │ │ ├── ModelDef.scala │ │ ├── MultiAttributeFormat.scala │ │ ├── NumberAttributeFormat.scala │ │ ├── ObjectAttributeFormat.scala │ │ ├── OptionalAttributeFormat.scala │ │ ├── RawAttributeFormat.scala │ │ ├── StringAttributeFormat.scala │ │ ├── TextAttributeFormat.scala │ │ ├── UUIDAttributeFormat.scala │ │ └── UserAttributeFormat.scala │ │ ├── services │ │ ├── Aggregations.scala │ │ ├── AttachmentSrv.scala │ │ ├── AuxSrv.scala │ │ ├── CreateSrv.scala │ │ ├── DBList.scala │ │ ├── DeleteSrv.scala │ │ ├── EventSrv.scala │ │ ├── ExecutionContextSrv.scala │ │ ├── FieldsSrv.scala │ │ ├── FindSrv.scala │ │ ├── GetSrv.scala │ │ ├── JsonFormat.scala │ │ ├── MigrationSrv.scala │ │ ├── ModelSrv.scala │ │ ├── QueryDSL.scala │ │ ├── SequenceSrv.scala │ │ ├── TempSrv.scala │ │ ├── UpdateSrv.scala │ │ ├── UserSrv.scala │ │ └── auth │ │ │ ├── ADAuthSrv.scala │ │ │ ├── LdapAuthSrv.scala │ │ │ └── MultiAuthSrv.scala │ │ └── utils │ │ ├── Collection.scala │ │ ├── Hash.scala │ │ ├── Instance.scala │ │ ├── JsonFormat.scala │ │ ├── RetryOnError.scala │ │ └── package.scala ├── conf │ └── reference.conf ├── project │ ├── build.properties │ └── plugins.sbt └── test │ ├── common │ └── Fabricator.scala │ └── org │ └── elastic4play │ ├── database │ ├── DBCreateSpec.scala │ ├── DBFindSpec.scala │ ├── DBGetSpec.scala │ └── DBModifySpec.scala │ ├── models │ └── CustomAttributeSpec.scala │ └── services │ ├── CreateSrvSpec.scala │ ├── DeleteSrvSpec.scala │ ├── FindSrvSpec.scala │ └── UpdateSrvSpec.scala ├── images ├── Architecture.png ├── cortex-analyzers.png └── cortex-logo.png ├── package ├── cortex ├── cortex.service ├── debian │ ├── postinst │ ├── postrm │ └── prerm ├── docker │ └── entrypoint ├── etc_default_cortex ├── logback.xml └── rpm │ ├── post │ ├── postun │ ├── pre │ └── preun ├── project ├── Common.scala ├── Dependencies.scala ├── DockerSettings.scala ├── FrontEnd.scala ├── PackageSettings.scala ├── build.properties └── plugins.sbt ├── sbt ├── test └── resources │ ├── analyzers │ ├── blocker │ │ ├── Dockerfile │ │ └── blocker.sh │ ├── echoAnalyzer │ │ ├── Dockerfile │ │ ├── echoAnalyzer.json │ │ └── echoAnalyzer.sh │ └── testAnalyzer │ │ ├── testAnalyzer.json │ │ └── testAnalyzer.py │ ├── test.check │ └── test.sh ├── version.sbt └── www ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── config └── webpack │ ├── environments │ ├── development.js │ └── production.js │ └── global.js ├── package.json ├── postcss.config.js ├── src ├── app │ ├── components │ │ ├── about │ │ │ ├── about.controller.js │ │ │ └── about.html │ │ ├── container │ │ │ ├── container.component.js │ │ │ ├── container.html │ │ │ └── container.scss │ │ ├── footer │ │ │ ├── footer.component.js │ │ │ ├── footer.html │ │ │ └── footer.scss │ │ ├── header │ │ │ ├── header.component.js │ │ │ ├── header.html │ │ │ └── header.scss │ │ └── user-dialog │ │ │ ├── user.edit.controller.js │ │ │ └── user.edit.modal.html │ ├── core │ │ ├── controllers │ │ │ └── PageController.js │ │ ├── core.module.js │ │ ├── directives │ │ │ ├── autofocus │ │ │ │ └── autofocus.directive.js │ │ │ ├── compare-to │ │ │ │ └── compare-to.directive.js │ │ │ ├── file-chooser │ │ │ │ ├── file-chooser.directive.js │ │ │ │ ├── file-chooser.html │ │ │ │ └── file-chooser.scss │ │ │ ├── fixed-height │ │ │ │ └── fixed-height.directive.js │ │ │ ├── require-roles │ │ │ │ └── require-roles.directive.js │ │ │ ├── taxonomie │ │ │ │ ├── taxonomie.directive.js │ │ │ │ └── taxonomie.html │ │ │ ├── tlp │ │ │ │ ├── tlp.directive.js │ │ │ │ ├── tlp.html │ │ │ │ └── tlp.scss │ │ │ └── user-avatar │ │ │ │ ├── user-avatar.directive.js │ │ │ │ ├── user-avatar.html │ │ │ │ └── user-avatar.scss │ │ ├── filters │ │ │ └── fang.js │ │ └── services │ │ │ ├── common │ │ │ ├── AlertService.js │ │ │ ├── AuthService.js │ │ │ ├── HtmlSanitizer.js │ │ │ ├── ModalService.js │ │ │ ├── NotificationService.js │ │ │ ├── SearchService.js │ │ │ ├── StreamService.js │ │ │ ├── UserService.js │ │ │ ├── UtilsService.js │ │ │ ├── VersionService.js │ │ │ └── modal.confirm.html │ │ │ └── constants.js │ ├── index.bootstrap.js │ ├── index.components.js │ ├── index.config.js │ ├── index.module.js │ ├── index.run.js │ ├── index.vendor.js │ └── pages │ │ ├── admin │ │ ├── admin.module.js │ │ ├── common │ │ │ ├── user-dialog │ │ │ │ ├── user.edit.controller.js │ │ │ │ └── user.edit.modal.html │ │ │ └── user-list │ │ │ │ ├── users-list.controller.js │ │ │ │ └── users-list.html │ │ ├── organizations │ │ │ ├── components │ │ │ │ ├── analyzers │ │ │ │ │ ├── analyzer-config-form.controller.js │ │ │ │ │ ├── analyzer-config-form.html │ │ │ │ │ ├── analyzer.edit.controller.js │ │ │ │ │ ├── analyzer.edit.modal.html │ │ │ │ │ ├── analyzers-list.controller.js │ │ │ │ │ ├── analyzers-list.html │ │ │ │ │ ├── config-list.controller.js │ │ │ │ │ └── config-list.html │ │ │ │ ├── config-form.controller.js │ │ │ │ ├── config-form.html │ │ │ │ ├── config.edit.controller.js │ │ │ │ ├── config.edit.modal.html │ │ │ │ ├── organization.modal.controller.js │ │ │ │ ├── organization.modal.html │ │ │ │ ├── responders │ │ │ │ │ ├── config-list.controller.js │ │ │ │ │ ├── config-list.html │ │ │ │ │ ├── responder-config-form.controller.js │ │ │ │ │ ├── responder-config-form.html │ │ │ │ │ ├── responder.edit.controller.js │ │ │ │ │ ├── responder.edit.modal.html │ │ │ │ │ ├── responders-list.controller.js │ │ │ │ │ └── responders-list.html │ │ │ │ ├── users-list.controller.js │ │ │ │ └── users-list.html │ │ │ ├── details │ │ │ │ ├── organization.page.controller.js │ │ │ │ └── organization.page.html │ │ │ ├── list │ │ │ │ ├── organizations.page.controller.js │ │ │ │ └── organizations.page.html │ │ │ ├── organizations.module.js │ │ │ ├── organizations.scss │ │ │ └── organizations.service.js │ │ └── users │ │ │ ├── list │ │ │ ├── users.page.controller.js │ │ │ └── users.page.html │ │ │ └── users.module.js │ │ ├── analyzers │ │ ├── analyzer.run.controller.js │ │ ├── analyzer.run.modal.html │ │ ├── analyzers.controller.js │ │ ├── analyzers.module.js │ │ ├── analyzers.page.html │ │ ├── analyzers.page.scss │ │ ├── analyzers.service.js │ │ └── components │ │ │ ├── analyzers.list.controller.js │ │ │ └── analyzers.list.html │ │ ├── jobs │ │ ├── components │ │ │ ├── job.details.controller.js │ │ │ ├── job.details.html │ │ │ ├── jobs.list.controller.js │ │ │ ├── jobs.list.html │ │ │ └── jobs.list.scss │ │ ├── job.controller.js │ │ ├── job.page.html │ │ ├── jobs.controller.js │ │ ├── jobs.module.js │ │ ├── jobs.page.html │ │ ├── jobs.page.scss │ │ └── jobs.service.js │ │ ├── login │ │ ├── login.controller.js │ │ ├── login.module.js │ │ ├── login.page.html │ │ └── login.page.scss │ │ ├── main │ │ ├── main.controller.js │ │ ├── main.module.js │ │ └── main.page.html │ │ ├── maintenance │ │ ├── maintenance.controller.js │ │ ├── maintenance.module.js │ │ ├── maintenance.page.html │ │ └── maintenance.service.js │ │ ├── responders │ │ ├── components │ │ │ ├── responders.list.controller.js │ │ │ └── responders.list.html │ │ ├── responders.controller.js │ │ ├── responders.module.js │ │ ├── responders.page.html │ │ └── responders.service.js │ │ └── settings │ │ ├── settings.module.js │ │ ├── settings.page.controller.js │ │ └── settings.page.html ├── assets │ ├── fonts │ │ ├── SIL Open Font License.txt │ │ ├── SourceSansPro-Black.otf │ │ ├── SourceSansPro-BlackIt.otf │ │ ├── SourceSansPro-Bold.otf │ │ ├── SourceSansPro-BoldIt.otf │ │ ├── SourceSansPro-ExtraLight.otf │ │ ├── SourceSansPro-ExtraLightIt.otf │ │ ├── SourceSansPro-It.otf │ │ ├── SourceSansPro-Light.otf │ │ ├── SourceSansPro-LightIt.otf │ │ ├── SourceSansPro-Regular.otf │ │ ├── SourceSansPro-Semibold.otf │ │ └── SourceSansPro-SemiboldIt.otf │ ├── images │ │ ├── angular.png │ │ ├── logo-dark.svg │ │ ├── logo-small.svg │ │ ├── no-avatar.png │ │ └── yeoman.png │ └── styles │ │ └── sass │ │ ├── flex-table.scss │ │ ├── index.scss │ │ ├── paginable-table.scss │ │ └── vendors │ │ ├── AdminLTE-fonts.scss │ │ ├── AdminLTE-skin-blue.scss │ │ └── AdminLTE.scss ├── favicon-128x128.png ├── favicon-16x16.png ├── favicon-196x196.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico └── tpl-index.ejs └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | ui/app/styles/vendors/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/actions/get_code_version/action.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Get latest version of code' 3 | description: 'Simply run sbt command to get the latest app_version of code currently built' 4 | outputs: 5 | app_version: 6 | description: "App version number, like '5.2.1' for example" 7 | value: ${{ steps.get_version.outputs.app_version }} 8 | runs: 9 | using: "composite" 10 | steps: 11 | - uses: coursier/setup-action@v1 12 | with: 13 | jvm: corretto:11 14 | apps: sbt 15 | - name: Get app_version 16 | id: get_version 17 | shell: bash 18 | run: | 19 | V=$(sbt -no-colors --error "print version" | awk 'END{print $1}') 20 | echo "app_version=$V" 21 | echo "app_version=$V" >> $GITHUB_OUTPUT -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | # EDIT THIS TITLE BEFORE POSTING. Use this template for bug reports. If you'd like to request a feature, please be as descriptive as possible and delete the template except the first section (Request Type) 2 | 3 | ### Request Type 4 | (select Bug or Feature Request and **remove this line**) 5 | Bug / Feature Request 6 | 7 | ### Work Environment 8 | 9 | | Question | Answer 10 | |---------------------------|-------------------- 11 | | OS version (server) | Debian, Ubuntu, CentOS, RedHat, ... 12 | | OS version (client) | XP, Seven, 10, Ubuntu, ... 13 | | Cortex version / git hash | 2.x, hash of the commit 14 | | Package Type | Docker, Binary, From source 15 | | Browser type & version | If applicable 16 | 17 | 18 | ### Problem Description 19 | Describe the problem/bug as clearly as possible. 20 | 21 | ### Steps to Reproduce 22 | 1. step 1 23 | 1. step 2 24 | 1. step 3... 25 | 26 | ### Possible Solutions 27 | (keep this section if you have suggestions on how to solve the problem. **Otherwise delete it**) 28 | 29 | ### Complementary information 30 | (add anything that can help identifying the problem such as **log** excerpts, **screenshots**, **configuration dumps** etc.) 31 | -------------------------------------------------------------------------------- /.github/workflows/check_code.yml: -------------------------------------------------------------------------------- 1 | name: Check Code 2 | on: 3 | workflow_dispatch: 4 | workflow_call: 5 | push: 6 | branches: [master, develop] 7 | jobs: 8 | check: 9 | name: Check 10 | runs-on: [ ubuntu-latest ] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: coursier/setup-action@v1 14 | with: 15 | jvm: corretto:11 16 | apps: sbt 17 | - name: Setup node 18 | uses: actions/setup-node@v3 19 | timeout-minutes: 15 20 | continue-on-error: true 21 | with: 22 | node-version: 18.16 23 | - name: Install bower 24 | run: npm install -g bower 25 | - name: Run tests 26 | run: sbt test Universal/packageBin 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.all.yml: -------------------------------------------------------------------------------- 1 | name: Check Code 2 | on: 3 | workflow_dispatch: 4 | workflow_call: 5 | push: 6 | branches: [master, develop] 7 | jobs: 8 | check: 9 | name: Check 10 | runs-on: [ ubuntu-latest ] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Setup node 14 | uses: actions/setup-node@v3 15 | timeout-minutes: 15 16 | continue-on-error: true 17 | with: 18 | node-version: 18.16 19 | - name: Install bower 20 | run: npm install -g bower 21 | - name: Run tests 22 | run: sbt test Universal/packageBin 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | target 3 | /.idea 4 | /.idea_modules 5 | /.classpath 6 | /.project 7 | /.settings 8 | /RUNNING_PID 9 | .cache-main 10 | .cache-tests 11 | .DS_Store 12 | *.py[cod] 13 | /report-templates/*.zip 14 | /.bsp 15 | 16 | /bin/ 17 | !/bin/activator 18 | !/bin/activator.bat 19 | 20 | conf/application.conf 21 | 22 | sbt-launch.jar 23 | .vscode 24 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 2.3.2 2 | project.git = true 3 | align = more # For pretty alignment. 4 | assumeStandardLibraryStripMargin = true 5 | style = defaultWithAlign 6 | maxColumn = 150 7 | 8 | align.openParenCallSite = false 9 | align.openParenDefnSite = false 10 | newlines.alwaysBeforeTopLevelStatements = false 11 | rewrite.rules = [ 12 | RedundantBraces 13 | RedundantParens 14 | SortModifiers 15 | PreferCurlyFors 16 | SortImports 17 | ] 18 | 19 | includeCurlyBraceInSelectChains = true 20 | includeNoParensInSelectChains = true 21 | 22 | rewriteTokens { 23 | "⇒" : "=>" 24 | "←" : "<-" 25 | "→" : "->" 26 | } 27 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors 2 | ------- 3 | 4 | * Thomas Franco (lead developer, back-end) 5 | * Saâd Kadhi (project leader, product management & design) 6 | * Jérôme Leonard (developer, analyzers) 7 | 8 | Contributors 9 | ------------ 10 | 11 | * Nabil Adouani 12 | * CERT Banque de France (CERT-BDF) 13 | 14 | Copyright (C) 2016-2017 Thomas Franco 15 | Copyright (C) 2016-2017 Saâd Kadhi 16 | Copyright (C) 2016-2017 Jérôme Leonard 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # StrangeBee Security Policies 2 | 3 | At [StrangeBee](https://www.strangebee.com) we take the security our software and services seriously, including following applications and projects: 4 | - TheHive (TheHive 5, and [previous open source version](https://github.com/TheHive-Project/TheHive)) 5 | - [Cortex](https://github.com/TheHive-Project/Cortex) 6 | - [Cortex-Analyzers](https://github.com/TheHive-Project/Cortex-Analyzers) 7 | 8 | ## Reporting a vulnerability 9 | If you believe you have found a security vulnerability in our applications and services (TheHive, Cortex, Cortex-Analyzers ...), report it to us. 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 12 | 13 | Instead, please send security vulnerabilities by emailing the StrangeBee Security team: 14 | 15 | ``` 16 | security[@]strangebee.com 17 | ``` 18 | 19 | In this email, please include as much information as possible that can help us better understand and resolve the issue: 20 | - Application and version 21 | - Special configuration and usage required to reproduce the issue 22 | - Step-by-step instructions to reproduce the issue 23 | - Exploit code is any 24 | - Impact of the issue 25 | 26 | This will be very useful and help us triage your report more quickly. 27 | 28 | More information regarding our Security policies and Advisories can be found here: [https://github.com/StrangeBeeCorp/security](). 29 | -------------------------------------------------------------------------------- /app/org/thp/cortex/controllers/AnalyzerConfigCtrl.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.controllers 2 | 3 | import javax.inject.{Inject, Singleton} 4 | import scala.concurrent.{ExecutionContext, Future} 5 | 6 | import play.api.libs.json.JsObject 7 | import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents} 8 | 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} 14 | 15 | @Singleton 16 | class AnalyzerConfigCtrl @Inject() ( 17 | analyzerConfigSrv: AnalyzerConfigSrv, 18 | userSrv: UserSrv, 19 | authenticated: Authenticated, 20 | fieldsBodyParser: FieldsBodyParser, 21 | renderer: Renderer, 22 | components: ControllerComponents, 23 | implicit val ec: ExecutionContext 24 | ) extends AbstractController(components) { 25 | 26 | def get(analyzerConfigName: String): Action[AnyContent] = authenticated(Roles.orgAdmin).async { request => 27 | analyzerConfigSrv 28 | .getForUser(request.userId, analyzerConfigName) 29 | .map(renderer.toOutput(OK, _)) 30 | } 31 | 32 | def list(): Action[AnyContent] = authenticated(Roles.orgAdmin).async { request => 33 | analyzerConfigSrv 34 | .listConfigForUser(request.userId) 35 | .map { bc => 36 | renderer.toOutput( 37 | OK, 38 | bc.sortWith { 39 | case (BaseConfig("global", _, _, _), _) => true 40 | case (_, BaseConfig("global", _, _, _)) => false 41 | case (BaseConfig(a, _, _, _), BaseConfig(b, _, _, _)) => a.compareTo(b) < 0 42 | } 43 | ) 44 | } 45 | } 46 | 47 | def update(analyzerConfigName: String): Action[Fields] = authenticated(Roles.orgAdmin).async(fieldsBodyParser) { implicit request => 48 | request.body.getValue("config").flatMap(_.asOpt[JsObject]) match { 49 | case Some(config) => 50 | analyzerConfigSrv 51 | .updateOrCreate(request.userId, analyzerConfigName, config) 52 | .map(renderer.toOutput(OK, _)) 53 | case None => Future.failed(BadRequestError("attribute config has invalid format")) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/org/thp/cortex/controllers/AssetCtrl.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.controllers 2 | 3 | import javax.inject.{Inject, Singleton} 4 | import play.api.http.{FileMimeTypes, HttpErrorHandler} 5 | import play.api.mvc.{Action, AnyContent} 6 | import controllers.{Assets, AssetsMetadata, ExternalAssets} 7 | import play.api.Environment 8 | 9 | import scala.concurrent.ExecutionContext 10 | 11 | trait AssetCtrl { 12 | def get(file: String): Action[AnyContent] 13 | } 14 | 15 | @Singleton 16 | class AssetCtrlProd @Inject() (errorHandler: HttpErrorHandler, meta: AssetsMetadata, env: Environment) extends Assets(errorHandler, meta, env) with AssetCtrl { 17 | def get(file: String): Action[AnyContent] = at("/www", file) 18 | } 19 | 20 | @Singleton 21 | class AssetCtrlDev @Inject() (environment: Environment)(implicit ec: ExecutionContext, fileMimeTypes: FileMimeTypes) 22 | extends ExternalAssets(environment) 23 | with AssetCtrl { 24 | def get(file: String): Action[AnyContent] = at("www/dist", file) 25 | } 26 | -------------------------------------------------------------------------------- /app/org/thp/cortex/controllers/Home.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.controllers 2 | 3 | import play.api.Configuration 4 | import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents} 5 | 6 | import javax.inject.{Inject, Singleton} 7 | 8 | @Singleton 9 | class Home @Inject() (configuration: Configuration, components: ControllerComponents) extends AbstractController(components) { 10 | 11 | def redirect: Action[AnyContent] = Action { 12 | Redirect(configuration.get[String]("play.http.context").stripSuffix("/") + "/index.html") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/org/thp/cortex/controllers/MispCtrl.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.controllers 2 | 3 | import javax.inject.Inject 4 | import org.elastic4play.controllers.{Authenticated, Fields, FieldsBodyParser, Renderer} 5 | import org.thp.cortex.models.Roles 6 | import org.thp.cortex.services.{MispSrv, WorkerSrv} 7 | import play.api.Logger 8 | import play.api.libs.json.{JsObject, JsValue} 9 | import play.api.mvc._ 10 | 11 | import scala.concurrent.{ExecutionContext, Future} 12 | 13 | class MispCtrl @Inject() ( 14 | mispSrv: MispSrv, 15 | analyzerSrv: WorkerSrv, 16 | authenticated: Authenticated, 17 | fieldsBodyParser: FieldsBodyParser, 18 | renderer: Renderer, 19 | components: ControllerComponents, 20 | implicit val ec: ExecutionContext 21 | ) extends AbstractController(components) { 22 | 23 | private[MispCtrl] lazy val logger = Logger(getClass) 24 | 25 | def modules: Action[Fields] = authenticated(Roles.read).async(fieldsBodyParser) { implicit request => 26 | val (analyzers, analyzerCount) = mispSrv.moduleList 27 | renderer.toOutput(OK, analyzers, analyzerCount) 28 | } 29 | 30 | def query: Action[JsValue] = authenticated(Roles.analyze)(parse.json).async { implicit request => 31 | (request.body \ "module") 32 | .asOpt[String] 33 | .fold(Future.successful(BadRequest("Module parameter is not present in request"))) { module => 34 | request 35 | .body 36 | .as[JsObject] 37 | .fields 38 | .collectFirst { 39 | case kv @ (k, _) if k != "module" => kv 40 | } 41 | .fold(Future.successful(BadRequest("Request doesn't contain data to analyze"))) { 42 | case (mispType, dataJson) => 43 | dataJson.asOpt[String].fold(Future.successful(BadRequest("Data has invalid type (expected string)"))) { data => 44 | mispSrv 45 | .query(module, mispType, data) 46 | .map(Ok(_)) 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/org/thp/cortex/controllers/ResponderConfigCtrl.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.controllers 2 | 3 | import scala.concurrent.{ExecutionContext, Future} 4 | 5 | import play.api.libs.json.JsObject 6 | import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents} 7 | 8 | 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} 14 | 15 | @Singleton 16 | class ResponderConfigCtrl @Inject() ( 17 | responderConfigSrv: ResponderConfigSrv, 18 | userSrv: UserSrv, 19 | authenticated: Authenticated, 20 | fieldsBodyParser: FieldsBodyParser, 21 | renderer: Renderer, 22 | components: ControllerComponents, 23 | implicit val ec: ExecutionContext 24 | ) extends AbstractController(components) { 25 | 26 | def get(analyzerConfigName: String): Action[AnyContent] = authenticated(Roles.orgAdmin).async { request => 27 | responderConfigSrv 28 | .getForUser(request.userId, analyzerConfigName) 29 | .map(renderer.toOutput(OK, _)) 30 | } 31 | 32 | def list(): Action[AnyContent] = authenticated(Roles.orgAdmin).async { request => 33 | responderConfigSrv 34 | .listConfigForUser(request.userId) 35 | .map { bc => 36 | renderer.toOutput( 37 | OK, 38 | bc.sortWith { 39 | case (BaseConfig("global", _, _, _), _) => true 40 | case (_, BaseConfig("global", _, _, _)) => false 41 | case (BaseConfig(a, _, _, _), BaseConfig(b, _, _, _)) => a.compareTo(b) < 0 42 | } 43 | ) 44 | } 45 | } 46 | 47 | def update(analyzerConfigName: String): Action[Fields] = authenticated(Roles.orgAdmin).async(fieldsBodyParser) { implicit request => 48 | request.body.getValue("config").flatMap(_.asOpt[JsObject]) match { 49 | case Some(config) => 50 | responderConfigSrv 51 | .updateOrCreate(request.userId, analyzerConfigName, config) 52 | .map(renderer.toOutput(OK, _)) 53 | case None => Future.failed(BadRequestError("attribute config has invalid format")) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/org/thp/cortex/models/Artifact.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.models 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import play.api.libs.json.JsObject 6 | 7 | import org.elastic4play.models.{AttributeDef, EntityDef, AttributeFormat => F, AttributeOption => O, ChildModelDef} 8 | 9 | trait ArtifactAttributes { _: AttributeDef => 10 | val dataType = attribute("dataType", F.stringFmt, "Type of the artifact", O.readonly) 11 | val data = optionalAttribute("data", F.rawFmt, "Content of the artifact", O.readonly) 12 | val attachment = optionalAttribute("attachment", F.attachmentFmt, "Artifact file content", O.readonly) 13 | val tlp = attribute("tlp", TlpAttributeFormat, "TLP level", 2L) 14 | val tags = multiAttribute("tags", F.stringFmt, "Artifact tags") 15 | val message = optionalAttribute("message", F.textFmt, "Message associated to the analysis") 16 | } 17 | 18 | @Singleton 19 | class ArtifactModel @Inject() (reportModel: ReportModel) 20 | extends ChildModelDef[ArtifactModel, Artifact, ReportModel, Report](reportModel, "artifact", "Artifact", "/artifact") 21 | with ArtifactAttributes {} 22 | 23 | class Artifact(model: ArtifactModel, attributes: JsObject) extends EntityDef[ArtifactModel, Artifact](model, attributes) with ArtifactAttributes 24 | -------------------------------------------------------------------------------- /app/org/thp/cortex/models/Errors.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.models 2 | 3 | abstract class CortexError(message: String) extends Exception(message) 4 | 5 | case class JobNotFoundError(jobId: String) extends CortexError(s"Job $jobId not found") 6 | case class WorkerNotFoundError(analyzerId: String) extends CortexError(s"Worker $analyzerId not found") 7 | case class UnknownConfigurationItem(item: String) extends CortexError(s"Configuration item $item is not known") 8 | case class RateLimitExceeded(analyzer: Worker) 9 | extends CortexError( 10 | s"Rate limit of ${analyzer.rate().getOrElse("(not set ?!)")} per ${analyzer.rateUnit().getOrElse("(not set ?!)")} reached for the analyzer ${analyzer.name()}. Job cannot be started" 11 | ) 12 | -------------------------------------------------------------------------------- /app/org/thp/cortex/models/JsonFormat.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.models 2 | 3 | import play.api.libs.json._ 4 | 5 | import org.elastic4play.models.JsonFormat.enumFormat 6 | import org.elastic4play.services.Role 7 | 8 | object JsonFormat { 9 | private val roleWrites: Writes[Role] = Writes((role: Role) => JsString(role.name.toLowerCase())) 10 | private val roleReads: Reads[Role] = Reads { 11 | case JsString(s) if Roles.isValid(s) => JsSuccess(Roles.withName(s).get) 12 | case _ => JsError(Seq(JsPath -> Seq(JsonValidationError(s"error.expected.role(${Roles.roleNames}")))) 13 | } 14 | implicit val roleFormat: Format[Role] = Format[Role](roleReads, roleWrites) 15 | implicit val workerTypeFormat: Format[WorkerType.Value] = enumFormat(WorkerType) 16 | } 17 | -------------------------------------------------------------------------------- /app/org/thp/cortex/models/Report.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.models 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import play.api.libs.json.JsObject 6 | 7 | import org.elastic4play.models.{AttributeDef, EntityDef, AttributeFormat => F, AttributeOption => O, ChildModelDef} 8 | 9 | trait ReportAttributes { _: AttributeDef => 10 | val full = attribute("full", F.rawFmt, "Full content of the report", O.readonly) 11 | val summary = attribute("summary", F.rawFmt, "Summary of the report", O.readonly) 12 | val operations = attribute("operations", F.rawFmt, "Update operations applied at the end of the job", "[]", O.unaudited) 13 | } 14 | 15 | @Singleton 16 | class ReportModel @Inject() (jobModel: JobModel) 17 | extends ChildModelDef[ReportModel, Report, JobModel, Job](jobModel, "report", "Report", "/report") 18 | with ReportAttributes {} 19 | 20 | class Report(model: ReportModel, attributes: JsObject) extends EntityDef[ReportModel, Report](model, attributes) with ReportAttributes 21 | -------------------------------------------------------------------------------- /app/org/thp/cortex/models/Roles.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.models 2 | 3 | import play.api.libs.json.{JsString, JsValue} 4 | import com.sksamuel.elastic4s.ElasticDsl.keywordField 5 | import com.sksamuel.elastic4s.fields.KeywordField 6 | import org.scalactic.{Every, Good, One, Or} 7 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 8 | import org.elastic4play.controllers.{InputValue, JsonInputValue, StringInputValue} 9 | import org.elastic4play.models.AttributeFormat 10 | import org.elastic4play.services.Role 11 | import org.thp.cortex.models.JsonFormat.roleFormat 12 | 13 | object Roles { 14 | object read extends Role("read") 15 | object analyze extends Role("analyze") 16 | object orgAdmin extends Role("orgadmin") 17 | object superAdmin extends Role("superadmin") 18 | val roles: List[Role] = read :: analyze :: orgAdmin :: superAdmin :: Nil 19 | 20 | val roleNames: List[String] = roles.map(_.name) 21 | def isValid(roleName: String): Boolean = roleNames.contains(roleName.toLowerCase()) 22 | 23 | def withName(roleName: String): Option[Role] = { 24 | val lowerCaseRole = roleName.toLowerCase() 25 | roles.find(_.name == lowerCaseRole) 26 | } 27 | } 28 | 29 | object RoleAttributeFormat extends AttributeFormat[Role]("role") { 30 | 31 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 32 | case JsString(v) if subNames.isEmpty && Roles.isValid(v) => Good(value) 33 | case _ => formatError(JsonInputValue(value)) 34 | } 35 | 36 | override def fromInputValue(subNames: Seq[String], value: InputValue): Role Or Every[AttributeError] = 37 | if (subNames.nonEmpty) 38 | formatError(value) 39 | else 40 | (value match { 41 | case StringInputValue(Seq(v)) => Good(v) 42 | case JsonInputValue(JsString(v)) => Good(v) 43 | case _ => formatError(value) 44 | }).flatMap(v => Roles.withName(v).fold[Role Or Every[AttributeError]](formatError(value))(role => Good(role))) 45 | 46 | override def elasticType(attributeName: String): KeywordField = keywordField(attributeName) 47 | } 48 | -------------------------------------------------------------------------------- /app/org/thp/cortex/models/TlpAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.models 2 | 3 | import play.api.libs.json.{JsNumber, JsValue} 4 | 5 | import org.scalactic.{Every, Good, One, Or} 6 | 7 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 8 | import org.elastic4play.controllers.{InputValue, JsonInputValue, StringInputValue} 9 | import org.elastic4play.models.{Attribute, AttributeDefinition, NumberAttributeFormat} 10 | import org.elastic4play.services.DBLists 11 | 12 | object TlpAttributeFormat extends NumberAttributeFormat { 13 | 14 | def isValidValue(value: Long): Boolean = 0 <= value && value <= 3 15 | 16 | override def definition(dblists: DBLists, attribute: Attribute[Long]): Seq[AttributeDefinition] = 17 | Seq( 18 | AttributeDefinition( 19 | attribute.attributeName, 20 | name, 21 | attribute.description, 22 | Seq(JsNumber(0), JsNumber(1), JsNumber(2), JsNumber(3)), 23 | Seq("white", "green", "amber", "red") 24 | ) 25 | ) 26 | 27 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 28 | case JsNumber(v) if subNames.isEmpty && isValidValue(v.toLong) => Good(value) 29 | case _ => formatError(JsonInputValue(value)) 30 | } 31 | 32 | override def fromInputValue(subNames: Seq[String], value: InputValue): Long Or Every[AttributeError] = 33 | value match { 34 | case StringInputValue(Seq(v)) if subNames.isEmpty => 35 | try { 36 | val longValue = v.toLong 37 | if (isValidValue(longValue)) Good(longValue) 38 | else formatError(value) 39 | } catch { 40 | case _: Throwable => formatError(value) 41 | } 42 | case JsonInputValue(JsNumber(v)) => Good(v.longValue) 43 | case _ => formatError(value) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/org/thp/cortex/models/WorkerConfig.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.models 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import play.api.libs.json.{JsObject, Json} 6 | 7 | import org.elastic4play.models.{AttributeDef, ChildModelDef, EntityDef, AttributeFormat => F, AttributeOption => O} 8 | 9 | import org.thp.cortex.models.JsonFormat.workerTypeFormat 10 | 11 | trait WorkerConfigAttributes { _: AttributeDef => 12 | val name = attribute("name", F.stringFmt, "Worker name") 13 | val config = attribute("config", F.rawFmt, "Configuration of worker", O.sensitive) 14 | val tpe = attribute("type", F.enumFmt(WorkerType), "", O.readonly) 15 | } 16 | 17 | @Singleton 18 | class WorkerConfigModel @Inject() (organizationModel: OrganizationModel) 19 | extends ChildModelDef[WorkerConfigModel, WorkerConfig, OrganizationModel, Organization]( 20 | organizationModel, 21 | "workerConfig", 22 | "WorkerConfig", 23 | "/worker/config" 24 | ) 25 | with WorkerConfigAttributes {} 26 | 27 | class WorkerConfig(model: WorkerConfigModel, attributes: JsObject) 28 | extends EntityDef[WorkerConfigModel, WorkerConfig](model, attributes) 29 | with WorkerConfigAttributes { 30 | def organization = parentId.get 31 | def jsonConfig = Json.parse(config()).as[JsObject] 32 | } 33 | -------------------------------------------------------------------------------- /app/org/thp/cortex/models/package.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex 2 | 3 | package object models { 4 | val modelVersion = 6 5 | } 6 | -------------------------------------------------------------------------------- /app/org/thp/cortex/services/AccessLogFilter.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.services 2 | 3 | import play.api.Logger 4 | import play.api.http.HttpErrorHandler 5 | import play.api.mvc.{EssentialAction, EssentialFilter, RequestHeader} 6 | 7 | import javax.inject.Inject 8 | import scala.concurrent.ExecutionContext 9 | 10 | class AccessLogFilter @Inject() (errorHandler: HttpErrorHandler)(implicit ec: ExecutionContext) extends EssentialFilter { 11 | 12 | val logger: Logger = Logger(getClass) 13 | 14 | override def apply(next: EssentialAction): EssentialAction = 15 | (requestHeader: RequestHeader) => { 16 | val startTime = System.currentTimeMillis 17 | next(requestHeader) 18 | .recoverWith { case error => errorHandler.onServerError(requestHeader, error) } 19 | .map { result => 20 | val endTime = System.currentTimeMillis 21 | val requestTime = endTime - startTime 22 | 23 | logger.info( 24 | s"${requestHeader.remoteAddress} ${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and returned ${result.header.status} ${result 25 | .body 26 | .contentLength 27 | .fold("")(b => s"$b bytes")}" 28 | ) 29 | result.withHeaders("Request-Time" -> requestTime.toString) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/org/thp/cortex/services/AnalyzerConfigSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.services 2 | 3 | import scala.concurrent.{ExecutionContext, Future} 4 | 5 | import play.api.Configuration 6 | 7 | import akka.stream.Materializer 8 | import javax.inject.{Inject, Singleton} 9 | import org.thp.cortex.models.{BaseConfig, WorkerConfigModel, WorkerType} 10 | 11 | import org.elastic4play.services.{CreateSrv, FindSrv, UpdateSrv} 12 | 13 | @Singleton 14 | class AnalyzerConfigSrv @Inject() ( 15 | val configuration: Configuration, 16 | val workerConfigModel: WorkerConfigModel, 17 | val userSrv: UserSrv, 18 | val organizationSrv: OrganizationSrv, 19 | val workerSrv: WorkerSrv, 20 | val createSrv: CreateSrv, 21 | val updateSrv: UpdateSrv, 22 | val findSrv: FindSrv, 23 | implicit val ec: ExecutionContext, 24 | implicit val mat: Materializer 25 | ) extends WorkerConfigSrv { 26 | 27 | override val workerType: WorkerType.Type = WorkerType.analyzer 28 | 29 | def definitions: Future[Map[String, BaseConfig]] = 30 | buildDefinitionMap(workerSrv.listAnalyzerDefinitions._1) 31 | } 32 | -------------------------------------------------------------------------------- /app/org/thp/cortex/services/CSRFFilter.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.services 2 | 3 | import javax.inject.{Inject, Provider, Singleton} 4 | 5 | import play.api.Logger 6 | import play.api.http.SessionConfiguration 7 | import play.api.libs.crypto.CSRFTokenSigner 8 | import play.filters.csrf.{CSRFFilter => PCSRFFilter} 9 | import play.api.mvc.RequestHeader 10 | import play.filters.csrf.CSRF.{ErrorHandler => CSRFErrorHandler, TokenProvider} 11 | import play.filters.csrf.CSRFConfig 12 | 13 | import akka.stream.Materializer 14 | 15 | object CSRFFilter { 16 | private[CSRFFilter] lazy val logger = Logger(getClass) 17 | 18 | def shouldProtect(request: RequestHeader): Boolean = { 19 | val isLogin = request.uri.startsWith("/api/login") 20 | val isApi = request.uri.startsWith("/api") 21 | val isInSession = request.session.data.nonEmpty 22 | val check = !isLogin && isApi && isInSession 23 | logger.debug(s"[csrf] uri ${request.uri} (isLogin=$isLogin, isApi=$isApi, isInSession=$isInSession): ${if (check) "" else "don't"} check") 24 | check 25 | } 26 | 27 | } 28 | 29 | @Singleton 30 | class CSRFFilter @Inject() ( 31 | config: Provider[CSRFConfig], 32 | tokenSignerProvider: Provider[CSRFTokenSigner], 33 | sessionConfiguration: SessionConfiguration, 34 | tokenProvider: TokenProvider, 35 | errorHandler: CSRFErrorHandler 36 | )(mat: Materializer) 37 | extends PCSRFFilter( 38 | config.get.copy(shouldProtect = CSRFFilter.shouldProtect), 39 | tokenSignerProvider.get, 40 | sessionConfiguration, 41 | tokenProvider, 42 | errorHandler 43 | )(mat) 44 | -------------------------------------------------------------------------------- /app/org/thp/cortex/services/CortexAuthSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.services 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import scala.collection.immutable 6 | import scala.concurrent.ExecutionContext 7 | 8 | import play.api.{Configuration, Logger} 9 | 10 | import org.elastic4play.services.AuthSrv 11 | import org.elastic4play.services.auth.MultiAuthSrv 12 | 13 | object CortexAuthSrv { 14 | private[CortexAuthSrv] lazy val logger = Logger(getClass) 15 | 16 | def getAuthSrv(authTypes: Seq[String], authModules: immutable.Set[AuthSrv]): Seq[AuthSrv] = 17 | ("key" +: authTypes.filterNot(_ == "key")) 18 | .flatMap { authType => 19 | authModules 20 | .find(_.name == authType) 21 | .orElse { 22 | logger.error(s"Authentication module $authType not found") 23 | None 24 | } 25 | } 26 | } 27 | 28 | @Singleton 29 | class CortexAuthSrv @Inject() ( 30 | configuration: Configuration, 31 | authModules: immutable.Set[AuthSrv], 32 | userSrv: UserSrv, 33 | implicit override val ec: ExecutionContext 34 | ) extends MultiAuthSrv( 35 | CortexAuthSrv.getAuthSrv(configuration.getDeprecated[Option[Seq[String]]]("auth.provider", "auth.type").getOrElse(Seq("local")), authModules), 36 | ec 37 | ) { 38 | 39 | // Uncomment the following lines if you want to prevent user with key to use password to authenticate 40 | // override def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = 41 | // userSrv.get(username) 42 | // .transformWith { 43 | // case Success(user) if user.key().isDefined ⇒ Future.failed(AuthenticationError("Authentication by password is not permitted for user with key")) 44 | // case _: Success[_] ⇒ super.authenticate(username, password) 45 | // case _: Failure[_] ⇒ Future.failed(AuthenticationError("Authentication failure")) 46 | // } 47 | } 48 | -------------------------------------------------------------------------------- /app/org/thp/cortex/services/LocalAuthSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.services 2 | 3 | import javax.inject.{Inject, Singleton} 4 | import scala.concurrent.{ExecutionContext, Future} 5 | import scala.util.Random 6 | import play.api.mvc.RequestHeader 7 | import akka.stream.Materializer 8 | import org.thp.cortex.models.User 9 | import org.elastic4play.controllers.Fields 10 | import org.elastic4play.services.{AuthCapability, AuthContext, AuthSrv} 11 | import org.elastic4play.utils.Hasher 12 | import org.elastic4play.{AuthenticationError, AuthorizationError} 13 | 14 | @Singleton 15 | class LocalAuthSrv @Inject() (userSrv: UserSrv, implicit val ec: ExecutionContext, implicit val mat: Materializer) extends AuthSrv { 16 | 17 | val name: String = "local" 18 | override val capabilities: Set[AuthCapability.Type] = Set(AuthCapability.changePassword, AuthCapability.setPassword) 19 | 20 | private[services] def doAuthenticate(user: User, password: String): Boolean = 21 | user.password().map(_.split(",", 2)).fold(false) { 22 | case Array(seed, pwd) => 23 | val hash = Hasher("SHA-256").fromString(seed + password).head.toString 24 | hash == pwd 25 | case _ => false 26 | } 27 | 28 | override def authenticate(username: String, password: String)(implicit request: RequestHeader): Future[AuthContext] = 29 | userSrv.get(username).flatMap { user => 30 | if (doAuthenticate(user, password)) userSrv.getFromUser(request, user, name) 31 | else Future.failed(AuthenticationError("Authentication failure")) 32 | } 33 | 34 | override def changePassword(username: String, oldPassword: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = 35 | userSrv.get(username).flatMap { user => 36 | if (doAuthenticate(user, oldPassword)) setPassword(username, newPassword) 37 | else Future.failed(AuthorizationError("Authentication failure")) 38 | } 39 | 40 | override def setPassword(username: String, newPassword: String)(implicit authContext: AuthContext): Future[Unit] = { 41 | val seed = Random.nextString(10).replace(',', '!') 42 | val newHash = seed + "," + Hasher("SHA-256").fromString(seed + newPassword).head.toString 43 | userSrv.update(username, Fields.empty.set("password", newHash)).map(_ => ()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/org/thp/cortex/services/ResponderConfigSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.services 2 | 3 | import scala.concurrent.{ExecutionContext, Future} 4 | 5 | import play.api.Configuration 6 | 7 | import akka.stream.Materializer 8 | import javax.inject.{Inject, Singleton} 9 | import org.thp.cortex.models.{BaseConfig, WorkerConfigModel, WorkerType} 10 | 11 | import org.elastic4play.services.{CreateSrv, FindSrv, UpdateSrv} 12 | 13 | @Singleton 14 | class ResponderConfigSrv @Inject() ( 15 | val configuration: Configuration, 16 | val workerConfigModel: WorkerConfigModel, 17 | val userSrv: UserSrv, 18 | val organizationSrv: OrganizationSrv, 19 | val workerSrv: WorkerSrv, 20 | val createSrv: CreateSrv, 21 | val updateSrv: UpdateSrv, 22 | val findSrv: FindSrv, 23 | implicit val ec: ExecutionContext, 24 | implicit val mat: Materializer 25 | ) extends WorkerConfigSrv { 26 | 27 | override val workerType: WorkerType.Type = WorkerType.responder 28 | def definitions: Future[Map[String, BaseConfig]] = buildDefinitionMap(workerSrv.listResponderDefinitions._1) 29 | } 30 | -------------------------------------------------------------------------------- /app/org/thp/cortex/services/mappers/MultiUserMapperSrv.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.services.mappers 2 | 3 | import scala.collection.immutable 4 | import scala.concurrent.Future 5 | 6 | import play.api.Configuration 7 | import play.api.libs.json.JsValue 8 | 9 | import javax.inject.{Inject, Singleton} 10 | 11 | import org.elastic4play.controllers.Fields 12 | 13 | object MultiUserMapperSrv { 14 | 15 | def getMapper(configuration: Configuration, ssoMapperModules: immutable.Set[UserMapper]): UserMapper = { 16 | val name = configuration.getOptional[String]("auth.sso.mapper").getOrElse("simple") 17 | ssoMapperModules.find(_.name == name).get 18 | } 19 | } 20 | 21 | @Singleton 22 | class MultiUserMapperSrv @Inject() (configuration: Configuration, ssoMapperModules: immutable.Set[UserMapper]) extends UserMapper { 23 | 24 | override val name: String = "usermapper" 25 | private lazy val mapper: UserMapper = MultiUserMapperSrv.getMapper(configuration, ssoMapperModules) 26 | 27 | override def getUserFields(jsValue: JsValue, authHeader: Option[(String, String)]): Future[Fields] = 28 | mapper.getUserFields(jsValue, authHeader) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/org/thp/cortex/services/mappers/SimpleUserMapper.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.services.mappers 2 | 3 | import scala.concurrent.{ExecutionContext, Future} 4 | 5 | import play.api.Configuration 6 | import play.api.libs.json._ 7 | 8 | import javax.inject.Inject 9 | 10 | import org.elastic4play.AuthenticationError 11 | import org.elastic4play.controllers.Fields 12 | 13 | class SimpleUserMapper( 14 | loginAttrName: String, 15 | nameAttrName: String, 16 | rolesAttrName: Option[String], 17 | organizationAttrName: Option[String], 18 | defaultRoles: Seq[String], 19 | defaultOrganization: Option[String], 20 | implicit val ec: ExecutionContext 21 | ) extends UserMapper { 22 | 23 | @Inject() def this(configuration: Configuration, ec: ExecutionContext) = 24 | this( 25 | configuration.getOptional[String]("auth.sso.attributes.login").getOrElse("login"), 26 | configuration.getOptional[String]("auth.sso.attributes.name").getOrElse("name"), 27 | configuration.getOptional[String]("auth.sso.attributes.roles"), 28 | configuration.getOptional[String]("auth.sso.attributes.organization"), 29 | configuration.getOptional[Seq[String]]("auth.sso.defaultRoles").getOrElse(Seq()), 30 | configuration.getOptional[String]("auth.sso.defaultOrganization"), 31 | ec 32 | ) 33 | 34 | override val name: String = "simple" 35 | 36 | override def getUserFields(jsValue: JsValue, authHeader: Option[(String, String)]): Future[Fields] = { 37 | val fields = for { 38 | login <- (jsValue \ loginAttrName).validate[String] 39 | name <- (jsValue \ nameAttrName).validate[String] 40 | roles = rolesAttrName.fold(defaultRoles)(r => (jsValue \ r).asOpt[Seq[String]].getOrElse(defaultRoles)) 41 | organization <- organizationAttrName 42 | .flatMap(o => (jsValue \ o).asOpt[String]) 43 | .orElse(defaultOrganization) 44 | .fold[JsResult[String]](JsError())(o => JsSuccess(o)) 45 | } yield Fields(Json.obj("login" -> login.toLowerCase, "name" -> name, "roles" -> roles, "organization" -> organization)) 46 | fields match { 47 | case JsSuccess(f, _) => Future.successful(f) 48 | case JsError(errors) => Future.failed(AuthenticationError(s"User info fails: ${JsError.toJson(errors)}")) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/org/thp/cortex/services/mappers/UserMapper.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.services.mappers 2 | 3 | import scala.concurrent.Future 4 | 5 | import play.api.libs.json.JsValue 6 | 7 | import org.elastic4play.controllers.Fields 8 | 9 | /** 10 | * User mapper trait to be used when converting a JS response from a third party API to a valid Fields object. Used in 11 | * the SSO process to create new users if the option is selected. 12 | */ 13 | trait UserMapper { 14 | val name: String 15 | def getUserFields(jsValue: JsValue, authHeader: Option[(String, String)] = None): Future[Fields] 16 | } 17 | -------------------------------------------------------------------------------- /app/org/thp/cortex/util/FunctionalCondition.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.util 2 | 3 | object FunctionalCondition { 4 | implicit class When[A](a: A) { 5 | def when(cond: Boolean)(whenTrue: A => A, whenFalse: A => A = identity): A = if (cond) whenTrue(a) else whenFalse(a) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/org/thp/cortex/util/JsonConfig.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.util 2 | 3 | import com.typesafe.config.ConfigValueType.{BOOLEAN, NULL, NUMBER, STRING} 4 | import com.typesafe.config.{ConfigList, ConfigObject, ConfigValue} 5 | import play.api.Configuration 6 | import play.api.libs.json._ 7 | 8 | import scala.jdk.CollectionConverters._ 9 | 10 | object JsonConfig { 11 | implicit val configValueWrites: Writes[ConfigValue] = Writes((value: ConfigValue) => 12 | value match { 13 | case v: ConfigObject => configWrites.writes(Configuration(v.toConfig)) 14 | case v: ConfigList => JsArray(v.asScala.map(x => configValueWrites.writes(x))) 15 | case v if v.valueType == NUMBER => JsNumber(BigDecimal(v.unwrapped.asInstanceOf[Number].toString)) 16 | case v if v.valueType == BOOLEAN => JsBoolean(v.unwrapped.asInstanceOf[Boolean]) 17 | case v if v.valueType == NULL => JsNull 18 | case v if v.valueType == STRING => JsString(v.unwrapped.asInstanceOf[String]) 19 | case _ => JsNull 20 | } 21 | ) 22 | 23 | implicit def configWrites: OWrites[Configuration] = OWrites { (cfg: Configuration) => 24 | JsObject(cfg.subKeys.map(key => key -> configValueWrites.writes(cfg.underlying.getValue(key))).toSeq) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/org/thp/cortex/util/docker/DockerLogsStringBuilder.scala: -------------------------------------------------------------------------------- 1 | package org.thp.cortex.util.docker 2 | 3 | import com.github.dockerjava.api.async.ResultCallback 4 | import com.github.dockerjava.api.model.Frame 5 | 6 | class DockerLogsStringBuilder(var builder: StringBuilder) extends ResultCallback.Adapter[Frame] { 7 | override def onNext(item: Frame): Unit = { 8 | builder.append(new String(item.getPayload)) 9 | super.onNext(item) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /builds/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.9-bookworm 2 | LABEL org.opencontainers.image.source=https://github.com/strangebee/cortex 3 | LABEL org.opencontainers.image.description="Development version of Cortex image, supported by StrangeBee" 4 | LABEL org.opencontainers.image.title="cortex" 5 | WORKDIR /opt/cortex 6 | ENV JAVA_HOME /usr/lib/jvm/java-11-amazon-corretto 7 | RUN apt update && apt upgrade -y && apt install -y curl gnupg && curl -fL https://apt.corretto.aws/corretto.key | gpg --dearmor -o /usr/share/keyrings/corretto.gpg && echo 'deb [signed-by=/usr/share/keyrings/corretto.gpg] https://apt.corretto.aws stable main' > /etc/apt/sources.list.d/corretto.list && apt update && apt install -y java-11-amazon-corretto-jdk && curl -fsSL https://download.docker.com/linux/debian/gpg -o /usr/share/keyrings/docker.asc && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list && apt update && apt install -y docker-ce docker-ce-cli containerd.io docker-ce-rootless-extras uidmap iproute2 fuse-overlayfs && groupadd -g 1001 cortex && useradd --system --uid 1001 --gid 1001 --groups docker cortex -d /opt/cortex && mkdir -m 777 /var/log/cortex && chmod 666 /etc/subuid /etc/subgid && rm -rf /var/lib/apt/lists/* && apt autoclean -y -q && apt autoremove -y -q 8 | ADD --chown=root:root opt /opt 9 | ADD --chown=cortex:cortex --chmod=755 etc /etc 10 | VOLUME /var/lib/docker 11 | RUN ["chmod", "+x", "/opt/cortex/bin/cortex", "/opt/cortex/entrypoint"] 12 | EXPOSE 9001 13 | ENTRYPOINT ["/opt/cortex/entrypoint"] 14 | CMD [] 15 | -------------------------------------------------------------------------------- /conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | ${application.home:-.}/logs/application.log 9 | 10 | %date [%level] from %logger in %thread - %message%n%xException 11 | 12 | 13 | 14 | 15 | 16 | %coloredLevel %logger{15} - %message%n%xException{10} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /contrib/misp-modules-loader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import sys 6 | import os 7 | import getopt 8 | import json 9 | 10 | import misp_modules 11 | 12 | """ 13 | git clone https://github.com/MISP/misp-modules.git 14 | cd misp_modules 15 | pip3 -r REQUIREMENTS 16 | pip3 install . 17 | """ 18 | 19 | 20 | def usage(): 21 | print(__file__ + " --list") 22 | print(__file__ + " --info ") 23 | print(__file__ + " --run ") 24 | 25 | 26 | def run(argv): 27 | mhandlers, modules = misp_modules.load_package_modules() 28 | try: 29 | opts, args = getopt.getopt(argv, 'lh:i:r:', ["list", "help", "info=", "run="]) 30 | except getopt.GetoptError as err: 31 | usage() 32 | print(str(err)) 33 | sys.exit(2) 34 | 35 | for opt, arg in opts: 36 | 37 | # TODO: check if module exist else exit 38 | if opt in ('-h', '--help'): 39 | usage() 40 | sys.exit() 41 | 42 | elif opt in ('-l', '--list'): 43 | modules = [m for m in modules if mhandlers['type:' + m] == "expansion"] 44 | print(json.dumps(modules)) 45 | sys.exit(0) 46 | 47 | elif opt in ('-r', '--run'): 48 | module_name = arg 49 | try: 50 | data = json.load(sys.stdin) 51 | print(json.dumps(mhandlers[module_name].handler(json.dumps(data)))) 52 | except: 53 | error = {'error': sys.exc_info()[1].args[0]} 54 | print(json.dumps(error)) 55 | sys.exit(0) 56 | 57 | elif opt in ('-i', '--info'): 58 | module_name = arg 59 | 60 | try: 61 | config = mhandlers[module_name].moduleconfig 62 | except AttributeError: 63 | config = [] 64 | print(json.dumps({ 65 | 'name': module_name, 66 | 'mispattributes': mhandlers[module_name].mispattributes, 67 | 'moduleinfo': mhandlers[module_name].moduleinfo, 68 | 'config': config 69 | })) 70 | 71 | 72 | if __name__ == '__main__': 73 | if len(sys.argv[1:]) > 0: 74 | run(sys.argv[1:]) 75 | else: 76 | usage() 77 | sys.exit(2) 78 | -------------------------------------------------------------------------------- /deployment/nomad/packs/cortex/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version v0.0.1 (Unreleased) 2 | 3 | Initial Release 4 | -------------------------------------------------------------------------------- /deployment/nomad/packs/cortex/README.md: -------------------------------------------------------------------------------- 1 | # cortex 2 | 3 | 4 | 5 | This pack is a simple Nomad job that runs as a service and can be accessed via 6 | HTTP. 7 | 8 | ## Pack Usage 9 | 10 | 11 | 12 | ### Changing the Message 13 | 14 | To change the message this server responds with, change the "message" variable 15 | when running the pack. 16 | 17 | ``` 18 | nomad-pack run cortex --var message="Hola Mundo!" 19 | ``` 20 | 21 | This tells Nomad Pack to tweak the `MESSAGE` environment variable that the 22 | service reads from. 23 | 24 | ## Variables 25 | 26 | 27 | 28 | - `message` (string:"Hello World!") - The message your application will respond with 29 | - `count` (number:2) - The number of app instances to deploy 30 | - `job_name` (string) - The name to use as the job name which overrides using 31 | the pack name 32 | - `datacenters` (list of strings:["*"]) - A list of datacenters in the region which 33 | are eligible for task placement 34 | - `region` (string) - The region where jobs will be deployed 35 | - `register_service` (bool: true) - If you want to register a Nomad service 36 | for the job 37 | - `service_tags` (list of string) - The service tags for the cortex application 38 | - `service_name` (string) - The service name for the cortex application 39 | 40 | [pack-registry]: https://github.com/hashicorp/nomad-pack-community-registry 41 | -------------------------------------------------------------------------------- /deployment/nomad/packs/cortex/metadata.hcl: -------------------------------------------------------------------------------- 1 | app { 2 | url = "https://github.com/StrangeBee/Cortex" 3 | } 4 | pack { 5 | name = "cortex" 6 | description = "Private repo for Cortex, synced hourly with Thehive-Project/Cortex public repo" 7 | version = "unversioned" 8 | } 9 | 10 | # Optional dependency information. This block can be repeated. 11 | 12 | # dependency "demo_dep" { 13 | # alias = "demo_dep" 14 | # source = "git://source.git/packs/demo_dep" 15 | # } 16 | 17 | # Declared dependencies will be downloaded from their source 18 | # using "nomad-pack vendor deps" and added to ./deps directory. 19 | 20 | # Dependencies in active development can by symlinked in 21 | # the ./deps directory 22 | 23 | # Example dependency source values: 24 | # - "git::https://github.com/org-name/repo-name.git//packs/demo_dep" 25 | # - "git@github.com:org-name/repo-name.git/packs/demo_dep" 26 | -------------------------------------------------------------------------------- /deployment/nomad/packs/cortex/outputs.tpl: -------------------------------------------------------------------------------- 1 | Congrats! You deployed the cortex pack on Nomad. 2 | -------------------------------------------------------------------------------- /deployment/nomad/packs/cortex/variables.hcl: -------------------------------------------------------------------------------- 1 | variable "job_name" { 2 | # If "", the pack name will be used 3 | description = "The name to use as the job name which overrides using the pack name" 4 | type = string 5 | default = "cortex-dev" 6 | } 7 | 8 | variable "datacenters" { 9 | description = "A list of datacenters in the region which are eligible for task placement" 10 | type = list(string) 11 | default = ["dc1"] 12 | } 13 | 14 | variable "count" { 15 | description = "The number of app instances to deploy" 16 | type = number 17 | default = 1 18 | } 19 | 20 | variable "analyzers" { 21 | description = "settings for analyzers" 22 | type = object({ 23 | nb_instances = number 24 | }) 25 | default = { 26 | nb_instances = 11 27 | } 28 | } 29 | 30 | variable "register_service" { 31 | description = "If you want to register a Nomad service for the job" 32 | type = bool 33 | default = true 34 | } 35 | 36 | variable "service_name" { 37 | description = "The service name for the cortex application" 38 | type = string 39 | default = "cortex-dev" 40 | } 41 | 42 | variable "cluster_name" { 43 | description = "The cluster name to be used by Cortex" 44 | type = string 45 | default = "cluster1" 46 | } 47 | 48 | variable "docker_image" { 49 | description = "Docker image to be used by deployment" 50 | type = string 51 | default = "ghcr.io/strangebee/cortex" 52 | } 53 | 54 | variable "docker_image_version" { 55 | description = "Docker image version to be used by deployment" 56 | type = string 57 | default = "devel" 58 | } 59 | 60 | variable "from_docker_hub" { 61 | description = "if we should deploy from Docker hub instead of ghcr.io" 62 | type = bool 63 | default = "false" 64 | } 65 | 66 | variable "service_version" { 67 | description = "Version name used by services and URL" 68 | type = string 69 | default = "latest" 70 | } 71 | 72 | variable "cortex_debug" { 73 | description = "Cortex Debug" 74 | type = string 75 | default = "false" 76 | } 77 | 78 | variable "debug_level" { 79 | description = "Cortex Debug Level" 80 | type = string 81 | default = "INFO" 82 | } 83 | -------------------------------------------------------------------------------- /docker/cortex/.env: -------------------------------------------------------------------------------- 1 | job_directory=/tmp/cortex-jobs 2 | -------------------------------------------------------------------------------- /docker/cortex/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | elasticsearch: 4 | image: elasticsearch:7.8.1 5 | environment: 6 | - http.host=0.0.0.0 7 | - discovery.type=single-node 8 | - script.allowed_types=inline 9 | - thread_pool.search.queue_size=100000 10 | - thread_pool.write.queue_size=10000 11 | cortex: 12 | image: thehiveproject/cortex:3.1.0-0.3RC1 13 | environment: 14 | - job_directory=${job_directory} 15 | volumes: 16 | - /var/run/docker.sock:/var/run/docker.sock 17 | - ${job_directory}:${job_directory} 18 | depends_on: 19 | - elasticsearch 20 | ports: 21 | - "0.0.0.0:9001:9001" -------------------------------------------------------------------------------- /elastic4play/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | !/bin/activator 3 | !/bin/activator.bat 4 | /bin/ 5 | 6 | # sbt specific 7 | .cache 8 | .history 9 | .lib/ 10 | .bsp/ 11 | dist/* 12 | target/ 13 | lib_managed/ 14 | src_managed/ 15 | project/boot/ 16 | project/plugins/project/ 17 | RUNNING_PID 18 | .cache-main 19 | .cache-tests 20 | 21 | # Eclipse 22 | .project 23 | .target 24 | .settings 25 | tmp 26 | .classpath 27 | 28 | # IntelliJ IDEA 29 | /.idea 30 | /*.iml 31 | /out 32 | /.idea_modules 33 | 34 | # Python 35 | __pycache__/ 36 | *.py[cod] 37 | *$py.class 38 | -------------------------------------------------------------------------------- /elastic4play/.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 2.3.2 2 | project.git = true 3 | align = more # For pretty alignment. 4 | assumeStandardLibraryStripMargin = true 5 | style = defaultWithAlign 6 | maxColumn = 150 7 | 8 | align.openParenCallSite = false 9 | align.openParenDefnSite = false 10 | newlines.alwaysBeforeTopLevelStatements = false 11 | rewrite.rules = [ 12 | RedundantBraces 13 | RedundantParens 14 | SortModifiers 15 | PreferCurlyFors 16 | SortImports 17 | ] 18 | 19 | includeCurlyBraceInSelectChains = true 20 | includeNoParensInSelectChains = true 21 | 22 | rewriteTokens { 23 | "⇒": "=>" 24 | "←": "<-" 25 | "→": "->" 26 | } -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/Timed.java: -------------------------------------------------------------------------------- 1 | package org.elastic4play; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface Timed { 11 | String value() default ""; 12 | } -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/controllers/MigrationCtrl.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.controllers 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import scala.concurrent.ExecutionContext 6 | 7 | import play.api.mvc._ 8 | 9 | import org.elastic4play.Timed 10 | import org.elastic4play.services.MigrationSrv 11 | 12 | /** 13 | * Migration controller : start migration process 14 | */ 15 | @Singleton 16 | class MigrationCtrl @Inject() (migrationSrv: MigrationSrv, components: ControllerComponents, implicit val ec: ExecutionContext) 17 | extends AbstractController(components) { 18 | 19 | @Timed("controllers.MigrationCtrl.migrate") 20 | def migrate: Action[AnyContent] = Action.async { 21 | migrationSrv.migrate.map(_ => NoContent) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/controllers/Renderer.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.controllers 2 | 3 | import javax.inject.Inject 4 | 5 | import scala.concurrent.{ExecutionContext, Future} 6 | import scala.util.{Failure, Success, Try} 7 | 8 | import play.api.http.Status 9 | import play.api.libs.json.Json.toJsFieldJsValueWrapper 10 | import play.api.libs.json.{JsValue, Json, Writes} 11 | import play.api.mvc.{Result, Results} 12 | 13 | import akka.stream.Materializer 14 | import akka.stream.scaladsl.Source 15 | 16 | import org.elastic4play.ErrorHandler 17 | 18 | class Renderer @Inject() (errorHandler: ErrorHandler, implicit val ec: ExecutionContext, implicit val mat: Materializer) { 19 | 20 | def toMultiOutput[A](status: Int, objects: Seq[Try[A]])(implicit writes: Writes[A]): Result = { 21 | 22 | val (success, failure) = objects.foldLeft((Seq.empty[JsValue], Seq.empty[JsValue])) { 23 | case ((artifacts, errors), Success(a)) => (Json.toJson(a) +: artifacts, errors) 24 | case ((artifacts, errors), Failure(e)) => 25 | val errorJson = errorHandler.toErrorResult(e) match { 26 | case Some((_, j)) => j 27 | case None => Json.obj("type" -> e.getClass.getName, "error" -> e.getMessage) 28 | } 29 | (artifacts, errorJson +: errors) 30 | 31 | } 32 | if (failure.isEmpty) 33 | toOutput(status, success) 34 | else if (success.isEmpty) 35 | toOutput(Status.BAD_REQUEST, failure) 36 | else 37 | toOutput(Status.MULTI_STATUS, Json.obj("success" -> success, "failure" -> failure)) 38 | } 39 | 40 | def toOutput[C](status: Int, content: C)(implicit writes: Writes[C]): Result = { 41 | val json = Json.toJson(content) 42 | val s = new Results.Status(status) 43 | s(json) 44 | } 45 | 46 | def toOutput[C](status: Int, src: Source[C, _], total: Future[Long])(implicit writes: Writes[C]): Future[Result] = { 47 | val stringSource = src.map(s => Json.toJson(s).toString).intersperse("[", ",", "]") 48 | total.map { t => 49 | new Results.Status(status) 50 | .chunked(stringSource) 51 | .as("application/json") 52 | .withHeaders("X-Total" -> t.toString) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/database/DBGet.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.database 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl._ 4 | import javax.inject.{Inject, Singleton} 5 | import org.elastic4play.NotFoundError 6 | import play.api.libs.json.JsObject 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | 10 | @Singleton 11 | class DBGet @Inject() (db: DBConfiguration) { 12 | 13 | /** 14 | * Retrieve entities from ElasticSearch 15 | * 16 | * @param modelName the name of the model (ie. document type) 17 | * @param id identifier of the entity to retrieve 18 | * @return the entity 19 | */ 20 | def apply(modelName: String, id: String)(implicit ec: ExecutionContext): Future[JsObject] = 21 | db.execute { 22 | // Search by id is not possible on child entity without routing information => id query 23 | search(db.indexName) 24 | .query(idsQuery(id) /*.types(modelName)*/ ) 25 | .size(1) 26 | .seqNoPrimaryTerm(true) 27 | } 28 | .map { searchResponse => 29 | searchResponse 30 | .hits 31 | .hits 32 | .headOption 33 | .fold[JsObject](throw NotFoundError(s"$modelName $id not found")) { hit => 34 | DBUtils.hit2json(hit) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/database/DBRemove.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.database 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl._ 4 | import com.sksamuel.elastic4s.requests.common.RefreshPolicy 5 | import javax.inject.{Inject, Singleton} 6 | import org.elastic4play.models.BaseEntity 7 | import play.api.Logger 8 | 9 | import scala.concurrent.{ExecutionContext, Future} 10 | import scala.util.Success 11 | 12 | @Singleton 13 | class DBRemove @Inject() (db: DBConfiguration) { 14 | 15 | lazy val logger: Logger = Logger(getClass) 16 | 17 | def apply(entity: BaseEntity)(implicit ec: ExecutionContext): Future[Boolean] = { 18 | logger.debug(s"Remove ${entity.model.modelName} ${entity.id}") 19 | db.execute { 20 | deleteById(db.indexName, entity.id) 21 | .routing(entity.routing) 22 | .refresh(RefreshPolicy.WAIT_FOR) 23 | } 24 | .transform(r => Success(r.isSuccess)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/database/DBSequence.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.database 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl._ 4 | import com.sksamuel.elastic4s.requests.common.RefreshPolicy 5 | import javax.inject.{Inject, Singleton} 6 | import org.elastic4play.models.{Attribute, ModelAttributes, AttributeFormat => F, AttributeOption => O} 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | 10 | class SequenceModel extends ModelAttributes("sequence") { 11 | val counter: Attribute[Long] = attribute("sequenceCounter", F.numberFmt, "Value of the sequence", O.model) 12 | } 13 | 14 | @Singleton 15 | class DBSequence @Inject() (db: DBConfiguration) { 16 | 17 | def apply(seqId: String)(implicit ec: ExecutionContext): Future[Int] = 18 | db.execute { 19 | updateById(db.indexName, s"sequence_$seqId") 20 | .upsert("sequenceCounter" -> 1, "relations" -> "sequence") 21 | .script("ctx._source.sequenceCounter += 1") 22 | .retryOnConflict(5) 23 | .fetchSource(true) 24 | .refresh(RefreshPolicy.WAIT_FOR) 25 | } map { updateResponse => 26 | updateResponse.source("sequenceCounter").asInstanceOf[Int] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/BinaryAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl.binaryField 4 | import com.sksamuel.elastic4s.fields.ElasticField 5 | import org.elastic4play.controllers.{InputValue, JsonInputValue} 6 | import org.elastic4play.models.JsonFormat.binaryFormats 7 | import org.elastic4play.services.DBLists 8 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 9 | import org.scalactic._ 10 | import play.api.libs.json.JsValue 11 | 12 | class BinaryAttributeFormat extends AttributeFormat[Array[Byte]]("binary")(binaryFormats) { 13 | override def checkJson(subNames: Seq[String], value: JsValue): Bad[One[InvalidFormatAttributeError]] = formatError(JsonInputValue(value)) 14 | 15 | override def fromInputValue(subNames: Seq[String], value: InputValue): Array[Byte] Or Every[AttributeError] = formatError(value) 16 | 17 | override def elasticType(attributeName: String): ElasticField = binaryField(attributeName) 18 | 19 | override def definition(dblists: DBLists, attribute: Attribute[Array[Byte]]): Seq[AttributeDefinition] = Nil 20 | } 21 | 22 | object BinaryAttributeFormat extends BinaryAttributeFormat 23 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/BooleanAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl.booleanField 4 | import com.sksamuel.elastic4s.fields.ElasticField 5 | import org.elastic4play.controllers.{InputValue, JsonInputValue, StringInputValue} 6 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 7 | import org.scalactic._ 8 | import play.api.libs.json.{JsBoolean, JsValue} 9 | 10 | class BooleanAttributeFormat extends AttributeFormat[Boolean]("boolean") { 11 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 12 | case _: JsBoolean if subNames.isEmpty => Good(value) 13 | case _ => formatError(JsonInputValue(value)) 14 | } 15 | 16 | override def fromInputValue(subNames: Seq[String], value: InputValue): Boolean Or Every[AttributeError] = 17 | if (subNames.nonEmpty) 18 | formatError(value) 19 | else 20 | value match { 21 | case StringInputValue(Seq(v)) => 22 | try Good(v.toBoolean) 23 | catch { 24 | case _: Throwable => formatError(value) 25 | } 26 | case JsonInputValue(JsBoolean(v)) => Good(v) 27 | case _ => formatError(value) 28 | } 29 | 30 | override def elasticType(attributeName: String): ElasticField = booleanField(attributeName) 31 | } 32 | 33 | object BooleanAttributeFormat extends BooleanAttributeFormat 34 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/DateAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.Date 5 | import com.sksamuel.elastic4s.ElasticDsl.dateField 6 | import com.sksamuel.elastic4s.fields.ElasticField 7 | import org.elastic4play.controllers.{InputValue, JsonInputValue, StringInputValue} 8 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 9 | import org.scalactic._ 10 | import play.api.libs.json.{JsNumber, JsString, JsValue} 11 | 12 | import scala.util.Try 13 | 14 | class DateAttributeFormat extends AttributeFormat[Date]("date") { 15 | 16 | def parse(d: String): Option[Date] = 17 | Try { 18 | val datePattern = "yyyyMMdd'T'HHmmssZ" 19 | val df = new SimpleDateFormat(datePattern) 20 | df.setLenient(false) 21 | df.parse(d) 22 | }.orElse(Try(new Date(d.toLong))).toOption 23 | 24 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 25 | case JsString(v) if subNames.isEmpty => parse(v).map(_ => Good(value)).getOrElse(formatError(JsonInputValue(value))) 26 | case JsNumber(_) if subNames.isEmpty => Good(value) 27 | case _ => formatError(JsonInputValue(value)) 28 | } 29 | 30 | override def fromInputValue(subNames: Seq[String], value: InputValue): Date Or Every[AttributeError] = 31 | if (subNames.nonEmpty) 32 | formatError(value) 33 | else { 34 | value match { 35 | case StringInputValue(Seq(v)) => parse(v).map(Good(_)).getOrElse(formatError(value)) 36 | case JsonInputValue(JsString(v)) => parse(v).map(Good(_)).getOrElse(formatError(value)) 37 | case JsonInputValue(JsNumber(v)) => Good(new Date(v.toLong)) 38 | case _ => formatError(value) 39 | } 40 | } 41 | 42 | override def elasticType(attributeName: String): ElasticField = dateField(attributeName).format("epoch_millis||basic_date_time_no_millis") 43 | } 44 | 45 | object DateAttributeFormat extends DateAttributeFormat 46 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/EnumerationAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl.keywordField 4 | import com.sksamuel.elastic4s.fields.ElasticField 5 | import org.elastic4play.controllers.{InputValue, JsonInputValue, StringInputValue} 6 | import org.elastic4play.services.DBLists 7 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 8 | import org.scalactic._ 9 | import play.api.libs.json.{Format, JsString, JsValue} 10 | 11 | case class EnumerationAttributeFormat[T <: Enumeration](e: T)(implicit format: Format[T#Value]) extends AttributeFormat[T#Value](s"enumeration") { 12 | 13 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 14 | case JsString(v) if subNames.isEmpty => 15 | try { 16 | e.withName(v); Good(value) 17 | } catch { 18 | case _: Throwable => formatError(JsonInputValue(value)) 19 | } 20 | case _ => formatError(JsonInputValue(value)) 21 | } 22 | 23 | override def fromInputValue(subNames: Seq[String], value: InputValue): T#Value Or Every[AttributeError] = 24 | if (subNames.nonEmpty) 25 | formatError(value) 26 | else 27 | value match { 28 | case StringInputValue(Seq(v)) => 29 | try Good(e.withName(v)) 30 | catch { 31 | case _: Throwable => formatError(value) 32 | } 33 | case JsonInputValue(JsString(v)) => 34 | try Good(e.withName(v)) 35 | catch { 36 | case _: Throwable => formatError(value) 37 | } 38 | case _ => formatError(value) 39 | } 40 | 41 | override def elasticType(attributeName: String): ElasticField = keywordField(attributeName) 42 | 43 | override def definition(dblists: DBLists, attribute: Attribute[T#Value]): Seq[AttributeDefinition] = 44 | Seq(AttributeDefinition(attribute.attributeName, name, attribute.description, e.values.toSeq.map(v => JsString(v.toString)), Nil)) 45 | } 46 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/Errors.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import scala.util.Try 4 | 5 | import play.api.libs.json.JsValue.jsValueToJsLookup 6 | import play.api.libs.json.{JsObject, JsString, Json} 7 | 8 | case class InvalidEntityAttributes[M <: BaseModelDef, T](model: M, name: String, format: AttributeFormat[T], attributes: JsObject) 9 | extends Exception( 10 | s"Entity is not conform to its model ${model.modelName} : missing attribute $name of type ${format.name}\n" + 11 | s"${Json.prettyPrint(attributes)}\n =⇒ ${Try(format.jsFormat.reads((attributes \ name).as[JsString]))}" 12 | ) 13 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/HashAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl.keywordField 4 | import com.sksamuel.elastic4s.fields.ElasticField 5 | import org.elastic4play.controllers.{InputValue, JsonInputValue, StringInputValue} 6 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 7 | import org.scalactic._ 8 | import play.api.libs.json.{JsString, JsValue} 9 | 10 | object HashAttributeFormat extends AttributeFormat[String]("hash") { 11 | val validDigits = "0123456789abcdefABCDEF" 12 | 13 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 14 | case JsString(v) if subNames.isEmpty && v.forall(c => validDigits.contains(c)) => Good(value) 15 | case _ => formatError(JsonInputValue(value)) 16 | } 17 | 18 | override def fromInputValue(subNames: Seq[String], value: InputValue): String Or Every[AttributeError] = 19 | if (subNames.nonEmpty) 20 | formatError(value) 21 | else 22 | value match { 23 | case StringInputValue(Seq(v)) if v.forall(c => validDigits.contains(c)) => Good(v.toLowerCase) 24 | case JsonInputValue(JsString(v)) if v.forall(c => validDigits.contains(c)) => Good(v.toLowerCase) 25 | case _ => formatError(value) 26 | } 27 | 28 | override def elasticType(attributeName: String): ElasticField = keywordField(attributeName) 29 | } 30 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/HiveEnumeration.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | trait HiveEnumeration { self: Enumeration => 4 | 5 | def getByName(name: String): Value = 6 | try { 7 | withName(name) 8 | } catch { 9 | case _: NoSuchElementException => //throw BadRequestError( 10 | sys.error(s"$name is invalid for $toString. Correct values are ${values.mkString(", ")}") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/JsonFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import play.api.libs.json._ 4 | 5 | object JsonFormat { 6 | implicit def baseModelEntityWrites[E <: BaseEntity]: Writes[E] = Writes((entity: BaseEntity) => entity.toJson) 7 | 8 | implicit def multiFormat[T](implicit jsFormat: Format[T]): Format[Seq[T]] = Format(Reads.seq(jsFormat), Writes.seq(jsFormat)) 9 | 10 | private def optionReads[T](implicit jsReads: Reads[T]) = Reads[Option[T]] { 11 | case JsNull => JsSuccess(None) 12 | case json => jsReads.reads(json).map(v => Some(v)) 13 | } 14 | 15 | implicit def optionFormat[T](implicit jsFormat: Format[T]): Format[Option[T]] = Format(optionReads, Writes.OptionWrites) 16 | 17 | def enumReads[E <: Enumeration with HiveEnumeration](e: E): Reads[E#Value] = 18 | Reads((json: JsValue) => 19 | json match { 20 | case JsString(s) => 21 | import scala.util.Try 22 | Try(JsSuccess(e.getByName(s))) 23 | .orElse(Try(JsSuccess(e.getByName(s.toLowerCase)))) 24 | .getOrElse(JsError(s"Enumeration expected of type: '${e.getClass}', but it does not appear to contain the value: '$s'")) 25 | case _ => JsError("String value expected") 26 | } 27 | ) 28 | 29 | def enumWrites[E <: Enumeration]: Writes[E#Value] = Writes((v: E#Value) => JsString(v.toString)) 30 | 31 | def enumFormat[E <: Enumeration with HiveEnumeration](e: E): Format[E#Value] = 32 | Format(enumReads(e), enumWrites) 33 | 34 | private val binaryReads = Reads.apply { 35 | case JsString(s) => JsSuccess(java.util.Base64.getDecoder.decode(s)) 36 | case _ => JsError("") 37 | } 38 | private val binaryWrites = Writes.apply { bin: Array[Byte] => 39 | JsString(java.util.Base64.getEncoder.encodeToString(bin)) 40 | } 41 | val binaryFormats: Format[Array[Byte]] = Format(binaryReads, binaryWrites) 42 | 43 | implicit val attributeDefinitionWrites: OWrites[AttributeDefinition] = Json.writes[AttributeDefinition] 44 | } 45 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/ListEnumerationAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl.keywordField 4 | import com.sksamuel.elastic4s.fields.ElasticField 5 | import org.elastic4play.controllers.{InputValue, JsonInputValue, StringInputValue} 6 | import org.elastic4play.services.DBLists 7 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 8 | import org.scalactic._ 9 | import play.api.libs.json.{JsString, JsValue} 10 | 11 | case class ListEnumerationAttributeFormat(enumerationName: String)(dblists: DBLists) extends AttributeFormat[String](s"enumeration") { 12 | def items: Set[String] = dblists("list_" + enumerationName).cachedItems.map(_.mapTo[String]).toSet 13 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 14 | case JsString(v) if subNames.isEmpty && items.contains(v) => Good(value) 15 | case _ => formatError(JsonInputValue(value)) 16 | } 17 | 18 | override def fromInputValue(subNames: Seq[String], value: InputValue): String Or Every[AttributeError] = 19 | if (subNames.nonEmpty) 20 | formatError(value) 21 | else 22 | value match { 23 | case StringInputValue(Seq(v)) if items.contains(v) => Good(v) 24 | case JsonInputValue(JsString(v)) if items.contains(v) => Good(v) 25 | case _ => formatError(value) 26 | } 27 | 28 | override def elasticType(attributeName: String): ElasticField = keywordField(attributeName) 29 | 30 | override def definition(dblists: DBLists, attribute: Attribute[String]): Seq[AttributeDefinition] = 31 | Seq(AttributeDefinition(attribute.attributeName, name, attribute.description, items.map(JsString.apply).toSeq, Nil)) 32 | } 33 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/NumberAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl.longField 4 | import com.sksamuel.elastic4s.fields.ElasticField 5 | import org.elastic4play.controllers.{InputValue, JsonInputValue, StringInputValue} 6 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 7 | import org.scalactic._ 8 | import play.api.libs.json.{JsNumber, JsValue} 9 | 10 | class NumberAttributeFormat extends AttributeFormat[Long]("number") { 11 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 12 | case _: JsNumber if subNames.isEmpty => Good(value) 13 | case _ => formatError(JsonInputValue(value)) 14 | } 15 | 16 | override def fromInputValue(subNames: Seq[String], value: InputValue): Long Or Every[AttributeError] = 17 | if (subNames.nonEmpty) 18 | formatError(value) 19 | else 20 | value match { 21 | case StringInputValue(Seq(v)) => 22 | try Good(v.toLong) 23 | catch { 24 | case _: Throwable => formatError(value) 25 | } 26 | case JsonInputValue(JsNumber(v)) => Good(v.longValue) 27 | case _ => formatError(value) 28 | } 29 | 30 | override def elasticType(attributeName: String): ElasticField = longField(attributeName) 31 | 32 | } 33 | 34 | object NumberAttributeFormat extends NumberAttributeFormat 35 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/OptionalAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import com.sksamuel.elastic4s.fields.ElasticField 4 | import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest 5 | import org.elastic4play.AttributeError 6 | import org.elastic4play.controllers.{InputValue, JsonInputValue, NullInputValue} 7 | import org.elastic4play.models.JsonFormat.optionFormat 8 | import org.elastic4play.services.DBLists 9 | import org.scalactic._ 10 | import play.api.libs.json.{JsNull, JsValue} 11 | 12 | case class OptionalAttributeFormat[T](attributeFormat: AttributeFormat[T]) 13 | extends AttributeFormat[Option[T]]("optional-" + attributeFormat.name)(optionFormat(attributeFormat.jsFormat)) { 14 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, Every[AttributeError]] = value match { 15 | case JsNull if subNames.isEmpty => Good(value) 16 | case _ => attributeFormat.checkJson(subNames, value) 17 | } 18 | 19 | override def inputValueToJson(subNames: Seq[String], value: InputValue): JsValue Or Every[AttributeError] = value match { 20 | case NullInputValue | JsonInputValue(JsNull) => Good(JsNull) 21 | case x => attributeFormat.inputValueToJson(subNames, x) 22 | } 23 | 24 | override def fromInputValue(subNames: Seq[String], value: InputValue): Option[T] Or Every[AttributeError] = value match { 25 | case NullInputValue => Good(None) 26 | case x => attributeFormat.fromInputValue(subNames, x).map(v => Some(v)) 27 | } 28 | 29 | override def elasticType(attributeName: String): ElasticField = attributeFormat.elasticType(attributeName) 30 | 31 | override def elasticTemplate(attributePath: Seq[String]): Seq[DynamicTemplateRequest] = attributeFormat.elasticTemplate(attributePath) 32 | 33 | override def definition(dblists: DBLists, attribute: Attribute[Option[T]]): Seq[AttributeDefinition] = 34 | attributeFormat.definition(dblists, attribute.asInstanceOf[Attribute[T]]) 35 | } 36 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/RawAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl.binaryField 4 | import com.sksamuel.elastic4s.fields.ElasticField 5 | import org.elastic4play.controllers.{InputValue, JsonInputValue} 6 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 7 | import org.scalactic._ 8 | import play.api.libs.json.{JsString, JsValue} 9 | 10 | class RawAttributeFormat extends AttributeFormat[String]("raw") { 11 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 12 | case _: JsString if subNames.isEmpty => Good(value) 13 | case _ => formatError(JsonInputValue(value)) 14 | } 15 | override def fromInputValue(subNames: Seq[String], value: InputValue): String Or Every[AttributeError] = 16 | TextAttributeFormat.fromInputValue(subNames, value) match { 17 | case Bad(One(ifae: InvalidFormatAttributeError)) => Bad(One(ifae.copy(format = name))) 18 | case other => other 19 | } 20 | 21 | override def elasticType(attributeName: String): ElasticField = binaryField(attributeName) 22 | } 23 | 24 | object RawAttributeFormat extends RawAttributeFormat 25 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/StringAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl.keywordField 4 | import com.sksamuel.elastic4s.fields.ElasticField 5 | import org.elastic4play.controllers.{InputValue, JsonInputValue} 6 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 7 | import org.scalactic._ 8 | import play.api.libs.json.{JsString, JsValue} 9 | 10 | class StringAttributeFormat extends AttributeFormat[String]("string") { 11 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 12 | case _: JsString if subNames.isEmpty => Good(value) 13 | case _ => formatError(JsonInputValue(value)) 14 | } 15 | 16 | override def fromInputValue(subNames: Seq[String], value: InputValue): String Or Every[AttributeError] = 17 | TextAttributeFormat.fromInputValue(subNames, value) match { 18 | case Bad(One(ifae: InvalidFormatAttributeError)) => Bad(One(ifae.copy(format = name))) 19 | case other => other 20 | } 21 | 22 | override def elasticType(attributeName: String): ElasticField = keywordField(attributeName) 23 | } 24 | 25 | object StringAttributeFormat extends StringAttributeFormat 26 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/TextAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl.textField 4 | import com.sksamuel.elastic4s.fields.ElasticField 5 | import org.elastic4play.controllers.{InputValue, JsonInputValue, StringInputValue} 6 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 7 | import org.scalactic._ 8 | import play.api.libs.json.{JsString, JsValue} 9 | 10 | class TextAttributeFormat extends AttributeFormat[String]("text") { 11 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 12 | case _: JsString if subNames.isEmpty => Good(value) 13 | case _ => formatError(JsonInputValue(value)) 14 | } 15 | 16 | override def fromInputValue(subNames: Seq[String], value: InputValue): String Or Every[AttributeError] = 17 | if (subNames.nonEmpty) 18 | formatError(value) 19 | else 20 | value match { 21 | case StringInputValue(Seq(v)) => Good(v) 22 | case JsonInputValue(JsString(v)) => Good(v) 23 | case _ => formatError(value) 24 | } 25 | 26 | override def elasticType(attributeName: String): ElasticField = textField(attributeName).fielddata(true) 27 | } 28 | 29 | object TextAttributeFormat extends TextAttributeFormat 30 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/UUIDAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import com.sksamuel.elastic4s.ElasticDsl.keywordField 4 | import com.sksamuel.elastic4s.fields.ElasticField 5 | import org.elastic4play.controllers.{InputValue, JsonInputValue, StringInputValue} 6 | import org.elastic4play.{AttributeError, InvalidFormatAttributeError} 7 | import org.scalactic._ 8 | import play.api.libs.json.{JsString, JsValue} 9 | 10 | import java.util.UUID 11 | 12 | class UUIDAttributeFormat extends AttributeFormat[UUID]("uuid") { 13 | override def checkJson(subNames: Seq[String], value: JsValue): Or[JsValue, One[InvalidFormatAttributeError]] = value match { 14 | case JsString(v) if subNames.isEmpty => 15 | try { 16 | UUID.fromString(v); Good(value) 17 | } catch { 18 | case _: Throwable => formatError(JsonInputValue(value)) 19 | } 20 | case _ => formatError(JsonInputValue(value)) 21 | } 22 | 23 | override def fromInputValue(subNames: Seq[String], value: InputValue): UUID Or Every[AttributeError] = 24 | if (subNames.nonEmpty) 25 | formatError(value) 26 | else 27 | value match { 28 | case StringInputValue(Seq(v)) => 29 | try Good(UUID.fromString(v)) 30 | catch { 31 | case _: Throwable => formatError(value) 32 | } 33 | case JsonInputValue(JsString(v)) => 34 | try Good(UUID.fromString(v)) 35 | catch { 36 | case _: Throwable => formatError(value) 37 | } 38 | case _ => formatError(value) 39 | } 40 | 41 | override def elasticType(attributeName: String): ElasticField = keywordField(attributeName) 42 | } 43 | 44 | object UUIDAttributeFormat extends UUIDAttributeFormat 45 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/models/UserAttributeFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | class UserAttributeFormat extends StringAttributeFormat { 4 | override val name: String = "user" 5 | } 6 | 7 | object UserAttributeFormat extends UserAttributeFormat 8 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/services/DeleteSrv.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.services 2 | 3 | import javax.inject.{Inject, Singleton} 4 | import scala.concurrent.{ExecutionContext, Future} 5 | import scala.util.Success 6 | 7 | import play.api.libs.json.JsObject 8 | 9 | import org.elastic4play.NotFoundError 10 | import org.elastic4play.database.{DBRemove, ModifyConfig} 11 | import org.elastic4play.models.{AbstractModelDef, BaseEntity, EntityDef} 12 | 13 | @Singleton 14 | class DeleteSrv @Inject() (updateSrv: UpdateSrv, getSrv: GetSrv, dbremove: DBRemove, eventSrv: EventSrv) { 15 | 16 | def apply[M <: AbstractModelDef[M, E], E <: EntityDef[M, E]]( 17 | model: M, 18 | id: String 19 | )(implicit authContext: AuthContext, ec: ExecutionContext): Future[E] = 20 | getSrv[M, E](model, id).flatMap(entity => apply(entity)) 21 | 22 | def apply[E <: BaseEntity](entity: E)(implicit authContext: AuthContext, ec: ExecutionContext): Future[E] = 23 | updateSrv 24 | .doUpdate(entity, entity.model.removeAttribute, ModifyConfig.default) 25 | .andThen { 26 | case Success(newEntity) => eventSrv.publish(AuditOperation(newEntity, AuditableAction.Delete, JsObject.empty, authContext)) 27 | } 28 | 29 | def realDelete[M <: AbstractModelDef[M, E], E <: EntityDef[M, E]]( 30 | model: M, 31 | id: String 32 | )(implicit authContext: AuthContext, ec: ExecutionContext): Future[Unit] = 33 | getSrv[M, E](model, id).flatMap(entity => realDelete(entity)) 34 | 35 | def realDelete[E <: BaseEntity](entity: E)(implicit authContext: AuthContext, ec: ExecutionContext): Future[Unit] = 36 | dbremove(entity).map { isFound => 37 | if (isFound) eventSrv.publish(AuditOperation(entity, AuditableAction.Delete, entity.toJson, authContext)) 38 | else throw NotFoundError(s"$entity.model.modelName} ${entity.id} not found") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/services/ExecutionContextSrv.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.services 2 | 3 | import akka.actor.ActorSystem 4 | import javax.inject.{Inject, Singleton} 5 | import play.api.Logger 6 | import play.api.cache.SyncCacheApi 7 | 8 | import scala.concurrent.ExecutionContext 9 | import scala.util.Try 10 | 11 | @Singleton 12 | class ExecutionContextSrv @Inject() (system: ActorSystem, syncCacheApi: SyncCacheApi) { 13 | lazy val logger: Logger = Logger(getClass) 14 | val default: ExecutionContext = system.dispatcher 15 | 16 | def get(threadPoolName: String): ExecutionContext = 17 | syncCacheApi.getOrElseUpdate(s"threadPool-$threadPoolName") { 18 | Try(system.dispatchers.lookup(threadPoolName)).getOrElse { 19 | logger.warn(s"""The configuration of thread pool $threadPoolName is not found. Fallback to default thread pool. 20 | |In order to use a dedicated thread pool, add the following configuration in application.conf: 21 | | $threadPoolName { 22 | | fork-join-executor { 23 | | # Number of threads = min(parallelism-max, max(parallelism-min, ceil(available processors * parallelism-factor))) 24 | | parallelism-min = 1 25 | | parallelism-factor = 2.0 26 | | parallelism-max = 4 27 | | } 28 | | } 29 | |""".stripMargin) 30 | default 31 | } 32 | } 33 | def withCustom[A](threadPoolName: String)(body: ExecutionContext => A): A = body(get(threadPoolName)) 34 | def withDefault[A](body: ExecutionContext => A): A = body(default) 35 | } 36 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/services/FieldsSrv.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.services 2 | 3 | import play.api.Logger 4 | import play.api.libs.json._ 5 | 6 | import org.scalactic.Accumulation.convertGenTraversableOnceToValidatable 7 | import org.scalactic._ 8 | 9 | import org.elastic4play.controllers.Fields 10 | import org.elastic4play.controllers.JsonFormat.inputValueFormat 11 | import org.elastic4play.models.BaseModelDef 12 | import org.elastic4play.{AttributeCheckingError, UnknownAttributeError} 13 | 14 | class FieldsSrv { 15 | private[FieldsSrv] lazy val logger = Logger(getClass) 16 | 17 | def parse(fields: Fields, model: BaseModelDef): JsObject Or AttributeCheckingError = 18 | fields 19 | .map { 20 | case (name, value) => 21 | val names = name.split("\\.").toSeq 22 | (name, names, value, model.formAttributes.get(names.head)) 23 | } 24 | .validatedBy { 25 | case (name, _, value, Some(_)) if value.jsonValue == JsNull || value.jsonValue == JsArray(Nil) => Good(name -> value.jsonValue) 26 | case (name, names, value, Some(attr)) => 27 | attr 28 | .format 29 | .inputValueToJson(names.tail, value) 30 | .transform(v => Good(name -> v), es => Bad(es.map(e => e.withName(model.modelName + "." + name)))) 31 | case (_, names, value, None) => Bad(One(UnknownAttributeError(model.modelName + "." + names.mkString("."), Json.toJson(value)))) 32 | } 33 | .transform(attrs => Good(JsObject(attrs.toSeq)), errors => Bad(AttributeCheckingError(model.modelName, errors.toSeq))) 34 | } 35 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/services/GetSrv.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.services 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import scala.concurrent.{ExecutionContext, Future} 6 | 7 | import org.elastic4play.database.DBGet 8 | import org.elastic4play.models.{AbstractModelDef, EntityDef} 9 | 10 | @Singleton 11 | class GetSrv @Inject() (dbGet: DBGet) { 12 | 13 | def apply[M <: AbstractModelDef[M, E], E <: EntityDef[M, E]](model: M, id: String)(implicit ec: ExecutionContext): Future[E] = 14 | dbGet(model.modelName, id).map(attrs => model(attrs)) 15 | } 16 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/services/ModelSrv.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.services 2 | 3 | import javax.inject.{Inject, Provider, Singleton} 4 | 5 | import scala.collection.immutable 6 | 7 | import org.elastic4play.models.BaseModelDef 8 | 9 | @Singleton 10 | class ModelSrv @Inject() (models: Provider[immutable.Set[BaseModelDef]]) { 11 | private[ModelSrv] lazy val modelMap = models.get.map(m => m.modelName -> m).toMap 12 | def apply(modelName: String): Option[BaseModelDef] = modelMap.get(modelName) 13 | lazy val list: Seq[BaseModelDef] = models.get.toSeq 14 | } 15 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/services/SequenceSrv.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.services 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import org.elastic4play.database.{DBConfiguration, DBSequence} 6 | 7 | @Singleton 8 | class SequenceSrv @Inject() (db: DBConfiguration) extends DBSequence(db) 9 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/utils/Collection.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.utils 2 | 3 | import scala.util.{Failure, Success, Try} 4 | 5 | object Collection { 6 | 7 | // def distinctBy[A, B, Repr, That](xs: IterableOnce[A])(f: A => B)(implicit cbf: CanBuildFrom[Repr, A, That]): That = { 8 | // val builder = cbf(xs.repr) 9 | // val i = xs.iterator 10 | // var set = Set[B]() 11 | // while (i.hasNext) { 12 | // val o = i.next 13 | // val b = f(o) 14 | // if (!set(b)) { 15 | // set += b 16 | // builder += o 17 | // } 18 | // } 19 | // builder.result 20 | // } 21 | 22 | // def partitionTry[A, Repr, ThatA, ThatB]( 23 | // xs: TraversableLike[Try[A], Repr] 24 | // )(implicit cbfa: CanBuildFrom[Repr, A, ThatA], cbfb: CanBuildFrom[Repr, Throwable, ThatB]): (ThatA, ThatB) = { 25 | // val aBuilder = cbfa() 26 | // val bBuilder = cbfb() 27 | // xs.foreach { 28 | // case Success(a) => aBuilder += a 29 | // case Failure(b) => bBuilder += b 30 | // } 31 | // (aBuilder.result(), bBuilder.result()) 32 | // } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/utils/Instance.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.utils 2 | 3 | import java.rmi.dgc.VMID 4 | import java.util.concurrent.atomic.AtomicInteger 5 | 6 | import play.api.mvc.RequestHeader 7 | 8 | object Instance { 9 | val id: String = (new VMID).toString 10 | private val counter = new AtomicInteger(0) 11 | def getRequestId(request: RequestHeader): String = s"$id:${request.id}" 12 | def getInternalId: String = s"$id::${counter.incrementAndGet}" 13 | } 14 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/utils/JsonFormat.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.utils 2 | 3 | import play.api.libs.json.{Format, JsString, Reads, Writes} 4 | 5 | object JsonFormat { 6 | private val hashReads: Reads[Hash] = Reads(json => json.validate[String].map(h => Hash(h))) 7 | private val hashWrites: Writes[Hash] = Writes[Hash](h => JsString(h.toString)) 8 | implicit val hashFormat: Format[Hash] = Format(hashReads, hashWrites) 9 | } 10 | -------------------------------------------------------------------------------- /elastic4play/app/org/elastic4play/utils/RetryOnError.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.utils 2 | 3 | import scala.concurrent.{ExecutionContext, Future, Promise} 4 | import scala.concurrent.duration.FiniteDuration 5 | import scala.concurrent.duration.DurationInt 6 | 7 | import play.api.Logger 8 | 9 | import akka.actor.ActorSystem 10 | 11 | object RetryOnError { 12 | 13 | @deprecated("use Retry(Int, FiniteDuration)(Class[_]*)(⇒ Future[A])", "1.6.2") 14 | def apply[A](cond: Throwable => Boolean = _ => true, maxRetry: Int = 5, initialDelay: FiniteDuration = 1.second)( 15 | body: => Future[A] 16 | )(implicit system: ActorSystem, ec: ExecutionContext): Future[A] = 17 | body.recoverWith { 18 | case e if maxRetry > 0 && cond(e) => 19 | val resultPromise = Promise[A] 20 | system.scheduler.scheduleOnce(initialDelay) { 21 | resultPromise.completeWith(apply(cond, maxRetry - 1, initialDelay * 2)(body)) 22 | } 23 | resultPromise.future 24 | } 25 | } 26 | 27 | object Retry { 28 | val logger: Logger = Logger(getClass) 29 | 30 | def exceptionCheck(exceptions: Seq[Class[_]])(t: Throwable): Boolean = 31 | exceptions.exists(_.isAssignableFrom(t.getClass)) || Option(t.getCause).exists(exceptionCheck(exceptions)) 32 | 33 | def apply[T](maxRetry: Int = 5, initialDelay: FiniteDuration = 1.second)( 34 | exceptions: Class[_]* 35 | )(body: => Future[T])(implicit system: ActorSystem, ec: ExecutionContext): Future[T] = 36 | body.recoverWith { 37 | case e: Throwable if maxRetry > 0 && exceptionCheck(exceptions)(e) => 38 | logger.warn(s"An error occurs (${e.getMessage}), retrying ($maxRetry)") 39 | val resultPromise = Promise[T]() 40 | system.scheduler.scheduleOnce(initialDelay) { 41 | resultPromise.completeWith(apply(maxRetry - 1, initialDelay * 2)(exceptions: _*)(body)) 42 | } 43 | resultPromise.future 44 | case e: Throwable if maxRetry > 0 => 45 | logger.error(s"uncatch error, not retrying", e) 46 | throw e 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /elastic4play/conf/reference.conf: -------------------------------------------------------------------------------- 1 | # handler for errors (transform exception to related http status code 2 | play.http.errorHandler = org.elastic4play.ErrorHandler 3 | 4 | -------------------------------------------------------------------------------- /elastic4play/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.5.5 2 | -------------------------------------------------------------------------------- /elastic4play/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Comment to get more information during initialization 2 | logLevel := Level.Info 3 | 4 | // Use the Play sbt plugin for Play projects 5 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.16") 6 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") 7 | addSbtPlugin("org.thehive-project" % "sbt-github-changelog" % "0.4.0") 8 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.10") 9 | -------------------------------------------------------------------------------- /elastic4play/test/common/Fabricator.scala: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import scala.util.Random 4 | import play.api.libs.json.{JsBoolean, JsNumber, JsObject, JsString, JsValue} 5 | 6 | object Fabricator { 7 | def string(prefix: String = "", size: Int = 10): String = prefix + Random.alphanumeric.take(size).mkString 8 | def int: Int = Random.nextInt() 9 | def boolean: Boolean = Random.nextBoolean() 10 | def long: Long = Random.nextLong() 11 | 12 | def jsValue: JsValue = int % 4 match { 13 | case 0 => JsNumber(long) 14 | case 1 => JsBoolean(boolean) 15 | case _ => JsString(string()) 16 | } 17 | 18 | def jsObject(maxSize: Int = 10): JsObject = { 19 | val fields = Seq.fill(int % maxSize)(string() -> jsValue) 20 | JsObject(fields) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /elastic4play/test/org/elastic4play/database/DBGetSpec.scala: -------------------------------------------------------------------------------- 1 | //package org.elastic4play.database 2 | // 3 | //import scala.concurrent.ExecutionContext.Implicits.{ global ⇒ ec } 4 | //import scala.concurrent.Future 5 | // 6 | //import play.api.libs.json.Json.toJsFieldJsValueWrapper 7 | //import play.api.libs.json.{ JsNull, Json } 8 | //import play.api.test.PlaySpecification 9 | // 10 | //import com.sksamuel.elastic4s.{ RichSearchHit, RichSearchResponse, SearchDefinition } 11 | //import org.junit.runner.RunWith 12 | //import org.specs2.mock.Mockito 13 | //import org.specs2.runner.JUnitRunner 14 | // 15 | //import org.elastic4play.utils.RichFuture 16 | // 17 | //@RunWith(classOf[JUnitRunner]) 18 | //class DBGetSpec extends PlaySpecification with Mockito { 19 | // 20 | // "DBGet" should { 21 | // "retrieve document" in { 22 | // val db = mock[DBConfiguration] 23 | // val dbget = new DBGet(db, ec) 24 | // db.indexName returns "testIndex" 25 | // val modelName = "user" 26 | // val entityId = "me" 27 | // 28 | // val searchDefinition = capture[SearchDefinition] 29 | // val response = mock[RichSearchResponse] 30 | // val searchHit = mock[RichSearchHit] 31 | // response.hits returns Array(searchHit) 32 | // searchHit.id returns entityId 33 | // searchHit.`type` returns modelName 34 | // searchHit.fields returns Map.empty 35 | // 36 | // db.execute(searchDefinition.capture) returns Future.successful(response) 37 | // dbget(modelName, entityId).await must_== Json.obj( 38 | // "_type" → modelName, 39 | // "_routing" → entityId, 40 | // "_parent" → JsNull, 41 | // "_id" → entityId) 42 | // 43 | // Json.parse(searchDefinition.value._builder.toString) must_== Json.obj( 44 | // "query" → Json.obj( 45 | // "ids" → Json.obj( 46 | // "type" → "user", 47 | // "values" → Seq("me"))), 48 | // "fields" → Seq("_source", "_routing", "_parent")) 49 | // } 50 | // } 51 | //} 52 | -------------------------------------------------------------------------------- /elastic4play/test/org/elastic4play/database/DBModifySpec.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.database 2 | 3 | import java.util.{Map => JMap} 4 | 5 | import org.elastic4play.models.BaseEntity 6 | import org.junit.runner.RunWith 7 | import org.specs2.matcher.ValueCheck.typedValueCheck 8 | import org.specs2.mock.Mockito 9 | import org.specs2.runner.JUnitRunner 10 | import play.api.libs.json.Json.toJsFieldJsValueWrapper 11 | import play.api.libs.json.{JsArray, JsNull, Json} 12 | import play.api.test.PlaySpecification 13 | 14 | import scala.jdk.CollectionConverters._ 15 | 16 | @RunWith(classOf[JUnitRunner]) 17 | class DBModifySpec extends PlaySpecification with Mockito { 18 | 19 | "DBModify" should { 20 | "build correct update script" in { 21 | val db = mock[DBConfiguration] 22 | val dbmodify = new DBModify(db) 23 | val attributes = Json.obj( 24 | "obj" -> Json.obj("subAttr1" -> 1), 25 | "arr" -> Seq("a", "b", "c"), 26 | "num" -> 42, 27 | "str" -> "blah", 28 | "bool" -> false, 29 | "sub.attr.str" -> "subValue", 30 | "n" -> JsNull, 31 | "sub.attr.remove" -> JsArray(), 32 | "remove" -> JsArray() 33 | ) 34 | val script = dbmodify.buildScript(mock[BaseEntity], attributes) 35 | 36 | script.script must_=== """ 37 | ctx._source["obj"]=params.param0; 38 | ctx._source["arr"]=params.param1; 39 | ctx._source["num"]=params.param2; 40 | ctx._source["str"]=params.param3; 41 | ctx._source["bool"]=params.param4; 42 | ctx._source["sub"]["attr"]["str"]=params.param5; 43 | ctx._source["n"]=null; 44 | ctx._source["sub"]["attr"].remove("remove"); 45 | ctx._source.remove("remove")""".filterNot(c => "\n ".contains(c)) 46 | script.params - "param0" - "param1" must_=== Map("param2" -> 42, "param3" -> "blah", "param4" -> false, "param5" -> "subValue") 47 | script.params("param0").asInstanceOf[JMap[_, _]].asScala must_== Map("subAttr1" -> 1) 48 | script.params("param1").asInstanceOf[Array[Any]].toSeq must contain(exactly[Any]("a", "b", "c")) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /elastic4play/test/org/elastic4play/models/CustomAttributeSpec.scala: -------------------------------------------------------------------------------- 1 | package org.elastic4play.models 2 | 3 | import play.api.libs.json.{JsNumber, Json} 4 | import play.api.test.PlaySpecification 5 | 6 | import org.junit.runner.RunWith 7 | import org.scalactic.Good 8 | import org.specs2.mock.Mockito 9 | import org.specs2.runner.JUnitRunner 10 | 11 | @RunWith(classOf[JUnitRunner]) 12 | class CustomAttributeSpec extends PlaySpecification with Mockito { 13 | "a custom fields attribute" should { 14 | "accept valid JSON object" in { 15 | val js = Json.obj("field1" -> Json.obj("number" -> 12), "field2" -> Json.obj("string" -> "plop"), "field3" -> Json.obj("boolean" -> true)) 16 | CustomAttributeFormat.checkJsonForCreation(Nil, js) must_=== Good(js) 17 | } 18 | 19 | "refuse invalid JSON object" in { 20 | val js = Json.obj("field1" -> Json.obj("number" -> "str"), "field2" -> Json.obj("string" -> 12), "field3" -> Json.obj("boolean" -> 45)) 21 | val result = CustomAttributeFormat.checkJsonForCreation(Nil, js) 22 | result.isBad must_=== true 23 | } 24 | 25 | "accept update a single field" in { 26 | val js = Json.obj("number" -> 14) 27 | CustomAttributeFormat.checkJsonForUpdate(Seq("field-name"), js) must_=== Good(js) 28 | } 29 | 30 | "accept update a single value" in { 31 | val js = JsNumber(15) 32 | CustomAttributeFormat.checkJsonForUpdate(Seq("field-name", "number"), js) must_=== Good(js) 33 | } 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /elastic4play/test/org/elastic4play/services/FindSrvSpec.scala: -------------------------------------------------------------------------------- 1 | //package org.elastic4play.services 2 | // 3 | //import play.api.libs.json.Json 4 | //import play.api.test.PlaySpecification 5 | // 6 | //import com.sksamuel.elastic4s.ElasticDsl.{ matchAllQuery, search } 7 | //import com.sksamuel.elastic4s.IndexesAndTypes.apply 8 | //import org.junit.runner.RunWith 9 | //import org.specs2.mock.Mockito 10 | //import org.specs2.runner.JUnitRunner 11 | // 12 | //import org.elastic4play.models.BaseModelDef 13 | // 14 | //@RunWith(classOf[JUnitRunner]) 15 | //class FindSrvSpec extends PlaySpecification with Mockito { 16 | // 17 | // val indexName = "myIndex" 18 | // val documentType = "myDocument" 19 | // 20 | // "GroupByCategory" should { 21 | // "generate correct elasticsearch query" in { 22 | // import org.elastic4play.services.QueryDSL._ 23 | // val catAgg = new GroupByCategory(Map( 24 | // "debug" → ("level" ~= "DEBUG"), 25 | // "info" → ("level" ~= "INFO"), 26 | // "warn" → ("level" ~= "WARN")), Seq(selectCount)) 27 | // 28 | // val query = search(indexName → documentType).matchAllQuery.aggregations(catAgg(mock[BaseModelDef])) 29 | // 30 | // Json.parse(query._builder.toString) must_== Json.parse(""" 31 | // { 32 | // "query": { 33 | // "match_all": {} 34 | // }, 35 | // "aggregations": { 36 | // "categories": { 37 | // "filters": { 38 | // "filters": { 39 | // "debug": { "term": { "level": "DEBUG" } }, 40 | // "info": { "term": { "level": "INFO" } }, 41 | // "warn": { "term": { "level": "WARN" } } 42 | // } 43 | // }, 44 | // "aggregations": { 45 | // "count": { 46 | // "filter": { 47 | // "match_all": {} 48 | // } 49 | // } 50 | // } 51 | // } 52 | // } 53 | // }""") 54 | // } 55 | // } 56 | //} 57 | -------------------------------------------------------------------------------- /images/Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/images/Architecture.png -------------------------------------------------------------------------------- /images/cortex-analyzers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/images/cortex-analyzers.png -------------------------------------------------------------------------------- /images/cortex-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/images/cortex-logo.png -------------------------------------------------------------------------------- /package/cortex.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=cortex 3 | Documentation=https://thehive-project.org 4 | Wants=network-online.target 5 | After=network-online.target 6 | 7 | [Service] 8 | EnvironmentFile=-/etc/default/cortex 9 | WorkingDirectory=/opt/cortex 10 | 11 | User=cortex 12 | Group=cortex 13 | 14 | ExecStart=/opt/cortex/bin/cortex \ 15 | -Dconfig.file=/etc/cortex/application.conf \ 16 | -Dlogger.file=/etc/cortex/logback.xml \ 17 | -Dpidfile.path=/dev/null 18 | 19 | StandardOutput=null 20 | StandardError=null 21 | 22 | # Specifies the maximum file descriptor number that can be opened by this process 23 | LimitNOFILE=65536 24 | 25 | # Disable timeout logic and wait until process is stopped 26 | TimeoutStopSec=0 27 | 28 | # SIGTERM signal is used to stop the Java process 29 | KillSignal=SIGTERM 30 | 31 | # Java process is never killed 32 | SendSIGKILL=no 33 | 34 | # When a JVM receives a SIGTERM signal it exits with code 143 35 | SuccessExitStatus=143 36 | 37 | [Install] 38 | WantedBy=multi-user.target 39 | -------------------------------------------------------------------------------- /package/etc_default_cortex: -------------------------------------------------------------------------------- 1 | # ##################################### 2 | # ##### Environment Configuration ##### 3 | # ##################################### 4 | 5 | # *WARNING* This file is not read by if you are using systemd 6 | 7 | # This file gets sourced before the actual startscript 8 | # gets executed. You can use this file to provide 9 | # environment variables 10 | 11 | # Define if Cortex service is enabled (no by default) 12 | # ----------------- 13 | ENABLED=no 14 | 15 | # Setting DAEMON_ARGS 16 | # pidfile is disabled (/dev/null) has it is handle by system loader (upstart/sysVinit) 17 | #DAEMON_ARGS=-Dconfig.file=/etc/cortex/cortex.conf -Dlogger.file=/etc/cortex/logback.xml -Dpidfile.path=/dev/null 18 | -------------------------------------------------------------------------------- /package/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | /var/log/cortex/application.log 9 | 10 | /var/log/cortex/application.%i.log.zip 11 | 1 12 | 10 13 | 14 | 15 | 10MB 16 | 17 | 18 | %date [%level] from %logger in %thread - %message%n%xException 19 | 20 | 21 | 22 | 23 | 24 | %coloredLevel %logger{15} - %message%n%xException 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /package/rpm/post: -------------------------------------------------------------------------------- 1 | mkdir -p /var/log/cortex 2 | touch /var/log/cortex/application.log 3 | chown -R cortex:cortex /var/log/cortex 4 | -------------------------------------------------------------------------------- /package/rpm/preun: -------------------------------------------------------------------------------- 1 | # 2 | # Adding service to autostart 3 | # $1 = service name 4 | # 5 | startService() { 6 | app_name=$1 7 | if hash update-rc.d >/dev/null 2>&1; then 8 | echo "Adding $app_name to autostart using update-rc.d" 9 | update-rc.d $app_name defaults 10 | service $app_name start 11 | elif hash chkconfig >/dev/null 2>&1; then 12 | echo "Adding $app_name to autostart using chkconfig" 13 | chkconfig --add cortex 14 | chkconfig $app_name on 15 | service $app_name start 16 | else 17 | echo "WARNING: Could not add $app_name to autostart: neither update-rc nor chkconfig found!" 18 | fi 19 | } 20 | 21 | # 22 | # Removing service from autostart 23 | # $1 = service name 24 | # 25 | stopService() { 26 | app_name=$1 27 | if hash update-rc.d >/dev/null 2>&1; then 28 | echo "Removing $app_name from autostart using update-rc.d" 29 | update-rc.d -f $app_name remove 30 | service $app_name stop 31 | elif hash chkconfig >/dev/null 2>&1; then 32 | echo "Removing $app_name from autostart using chkconfig" 33 | chkconfig $app_name off 34 | chkconfig --del $app_name 35 | service $app_name stop 36 | else 37 | echo "WARNING: Could not remove $app_name from autostart: neither update-rc nor chkconfig found!" 38 | fi 39 | 40 | } 41 | 42 | # 43 | # Restarting the service after package upgrade 44 | # $1 = service name 45 | # 46 | restartService() { 47 | app_name=$1 48 | service $app_name restart 49 | } 50 | 51 | 52 | # Scriptlet syntax: http://fedoraproject.org/wiki/Packaging:ScriptletSnippets#Syntax 53 | # $1 == 1 is upgrade and $1 == 0 is uninstall 54 | if [ $1 -eq 0 ] ; 55 | then 56 | stopService cortex || echo "Could not stop cortex" 57 | fi 58 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | val scalaVersion = "2.13.15" 5 | val dockerJavaVersion = "3.4.0" 6 | 7 | object Play { 8 | val version = play.core.PlayVersion.current 9 | val ws = "com.typesafe.play" %% "play-ws" % version exclude ("com.typesafe.play", "play-ws-standalone-xml") 10 | val ahc = "com.typesafe.play" %% "play-ahc-ws" % version 11 | val cache = "com.typesafe.play" %% "play-ehcache" % version 12 | val test = "com.typesafe.play" %% "play-test" % version 13 | val specs2 = "com.typesafe.play" %% "play-specs2" % version 14 | val filters = "com.typesafe.play" %% "filters-helpers" % version 15 | val guice = "com.typesafe.play" %% "play-guice" % version 16 | val twirl = "com.typesafe.play" %% "twirl-api" % "1.6.8" 17 | } 18 | 19 | val scalaGuice = "net.codingwell" %% "scala-guice" % "6.0.0" 20 | 21 | val reflections = "org.reflections" % "reflections" % "0.10.2" 22 | val zip4j = "net.lingala.zip4j" % "zip4j" % "2.11.5" 23 | val dockerJavaClient = "com.github.docker-java" % "docker-java-core" % dockerJavaVersion 24 | val dockerJavaTransport = "com.github.docker-java" % "docker-java-transport-zerodep" % dockerJavaVersion 25 | val k8sClient = "io.fabric8" % "kubernetes-client" % "5.0.2" 26 | val akkaCluster = "com.typesafe.akka" %% "akka-cluster" % play.core.PlayVersion.akkaVersion 27 | val akkaClusterTyped = "com.typesafe.akka" %% "akka-cluster-typed" % play.core.PlayVersion.akkaVersion 28 | } 29 | -------------------------------------------------------------------------------- /project/FrontEnd.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt._ 3 | import scala.sys.process.Process 4 | import Path.rebase 5 | 6 | object FrontEnd extends AutoPlugin { 7 | 8 | object autoImport { 9 | val frontendFiles = taskKey[Seq[(File, String)]]("Front-end files") 10 | } 11 | 12 | import autoImport._ 13 | 14 | override def trigger = allRequirements 15 | 16 | override def projectSettings = 17 | Seq[Setting[_]](frontendFiles := { 18 | val s = streams.value 19 | s.log.info("Building front-end ...") 20 | s.log.info("npm install") 21 | Process("npm" :: "install" :: "--legacy-peer-deps" :: Nil, baseDirectory.value / "www") ! s.log 22 | s.log.info("npm run build") 23 | Process("npm" :: "run" :: "build" :: Nil, baseDirectory.value / "www") ! s.log 24 | val dir = baseDirectory.value / "www" / "dist" 25 | dir.**(AllPassFilter) pair rebase(dir, "www") 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Comment to get more information during initialization 2 | logLevel := Level.Info 3 | evictionErrorLevel := util.Level.Warn 4 | 5 | // The Play plugin 6 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.5") 7 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") 8 | //addSbtPlugin("org.thehive-project" % "sbt-github-changelog" % "0.4.0") 9 | addSbtPlugin("io.github.siculo" %% "sbt-bom" % "0.3.0") 10 | -------------------------------------------------------------------------------- /test/resources/analyzers/blocker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | 3 | WORKDIR /analyzer 4 | RUN apt update && apt install -y jq 5 | COPY blocker.sh blocker/blocker.sh 6 | ENTRYPOINT ["blocker/blocker.sh"] 7 | -------------------------------------------------------------------------------- /test/resources/analyzers/blocker/blocker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | while true 4 | do 5 | sleep 10 6 | done 7 | -------------------------------------------------------------------------------- /test/resources/analyzers/echoAnalyzer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | 3 | WORKDIR /analyzer 4 | RUN apt update && apt install -y jq 5 | COPY echoAnalyzer.sh echoAnalyzer/echoAnalyzer.sh 6 | ENTRYPOINT ["echoAnalyzer/echoAnalyzer.sh"] -------------------------------------------------------------------------------- /test/resources/analyzers/echoAnalyzer/echoAnalyzer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "echoAnalyzer", 3 | "version": "1.0", 4 | "author": "TheHive-Project", 5 | "url": "https://github.com/thehive-project/thehive", 6 | "license": "AGPL-V3", 7 | "baseConfig": "echoAnalyzer", 8 | "config": { 9 | 10 | }, 11 | "configurationItems": [{ 12 | "name": "multiText", 13 | "description": "config item with multiple text value", 14 | "type": "string", 15 | "multi": true, 16 | "required": false, 17 | "defaultValue": ["c1", "c2"] 18 | }, 19 | { 20 | "name": "num", 21 | "description": "config item with single number value", 22 | "type": "number", 23 | "multi": false, 24 | "required": false 25 | }, 26 | { 27 | "name": "bool", 28 | "description": "config item with required boolean value", 29 | "type": "boolean", 30 | "multi": false, 31 | "required": true, 32 | "defaultValue": true 33 | }, 34 | { 35 | "name": "StringField", 36 | "description": "config item with required string value", 37 | "type": "string", 38 | "multi": false, 39 | "required": true 40 | }, { 41 | "name": "NumberArray", 42 | "description": "config item with multiple number value", 43 | "type": "number", 44 | "multi": true, 45 | "required": false, 46 | "defaultValue": [10, 20] 47 | }, { 48 | "name": "BooleanArray", 49 | "description": "config item with multiple boolean value", 50 | "type": "boolean", 51 | "multi": true, 52 | "required": false, 53 | "defaultValue": [true, true] 54 | } 55 | ], 56 | "description": "Fake analyzer used for functional tests", 57 | "dataTypeList": ["domain", "thehive:case", "thehive:case_task", "thehive:case_artifact", "thehive:alert", "thehive:case_task_log"], 58 | "command": "echoAnalyzer/echoAnalyzer.sh", 59 | "image": "echo_analyzer" 60 | } 61 | -------------------------------------------------------------------------------- /test/resources/analyzers/echoAnalyzer/echoAnalyzer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | echo starting with parameters: $* 6 | for JOB 7 | do 8 | echo executing $JOB 9 | if [[ -d "${JOB}" ]]; then 10 | echo directory $JOB exists 11 | if [[ -r "${JOB}/input/input.json" ]]; then 12 | INPUT=$(cat ${JOB}/input/input.json) 13 | else 14 | INPUT="{}" 15 | fi 16 | echo input is $INPUT 17 | DATA=$(jq .data <<< ${INPUT}) 18 | DATATYPE=$(jq .dataType <<< ${INPUT}) 19 | 20 | echo building output 21 | mkdir -p "${JOB}/output" 22 | cat > "${JOB}/output/output.json" <<- EOF 23 | { 24 | "success": true, 25 | "summary": { 26 | "taxonomies": [ 27 | { "namespace": "test", "predicate": "data", "value": "echo", "level": "info" } 28 | ] 29 | }, 30 | "full": ${INPUT}, 31 | "operations": [ 32 | { "type": "AddTagToCase", "tag": "From Action Operation" }, 33 | { "type": "CreateTask", "title": "task created by action", "description": "yop !" } 34 | ] 35 | } 36 | EOF 37 | echo output is: 38 | cat "${JOB}/output/output.json" 39 | fi 40 | done 41 | -------------------------------------------------------------------------------- /test/resources/analyzers/testAnalyzer/testAnalyzer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testAnalyzer", 3 | "version": "1.0", 4 | "author": "TheHive-Project", 5 | "url": "https://github.com/thehive-project/thehive", 6 | "license": "AGPL-V3", 7 | "baseConfig": "testAnalyzer", 8 | "config": {}, 9 | "configurationItems": [], 10 | "description": "Fake analyzer used for functional tests", 11 | "dataTypeList": ["domain"], 12 | "command": "testAnalyzer/testAnalyzer.py" 13 | } 14 | -------------------------------------------------------------------------------- /test/resources/analyzers/testAnalyzer/testAnalyzer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | 4 | from cortexutils.analyzer import Analyzer 5 | from cortexutils import runner 6 | 7 | 8 | class TestAnalyzer(Analyzer): 9 | 10 | def artifacts(self, raw): 11 | return [ 12 | self.build_artifact("ip", "127.0.0.1", tags=["localhost"]), 13 | self.build_artifact("file", "/etc/issue.net", tlp=3) 14 | ] 15 | 16 | def summary(self, raw): 17 | return {"taxonomies": [self.build_taxonomy("info", "test", "data", "test")]} 18 | 19 | def run(self): 20 | Analyzer.run(self) 21 | 22 | self.report({ 23 | 'data': self.get_data(), 24 | 'input': self._input 25 | }) 26 | 27 | 28 | if __name__ == '__main__': 29 | runner(TestAnalyzer) 30 | -------------------------------------------------------------------------------- /test/resources/test.check: -------------------------------------------------------------------------------- 1 | ################### 2 | # API used by TheHive 3 | [X] GET /api/status 4 | [X] GET /api/analyzer 5 | [X] GET /api/analyzer/:id 6 | [X] GET /api/analyzer/type/:dataType 7 | [X] POST /api/analyzer/:id/run 8 | [X] GET /api/job/:id/waitreport 9 | #################### 10 | 11 | [X] GET /api/job/:id/artifacts 12 | [X] GET /api/analyzerdefinition 13 | [O] POST /api/analyzerdefinition/scan 14 | [X] POST /api/organization/:organizationId/analyzer/:analyzerId 15 | 16 | [ ] GET /api/job 17 | [ ] GET /api/job/:id 18 | [ ] GET /api/job/:id/report 19 | 20 | [ ] GET /api/organization 21 | [X] GET /api/organization/:id 22 | [ ] POST /api/organization 23 | [ ] PATCH /api/organization/:id 24 | [ ] DELETE /api/organization/:id 25 | 26 | [ ] POST /api/stream 27 | [ ] GET /api/stream/status 28 | [ ] GET /api/stream/:streamId 29 | 30 | [X] POST /api/maintenance/migrate 31 | [ ] GET /api/datastore/:hash 32 | [ ] GET /api/datastorezip/:hash 33 | 34 | [ ] GET /api/list 35 | [ ] DELETE /api/list/:itemId 36 | [ ] PATCH /api/list/:itemId 37 | [ ] POST /api/list/:listName 38 | [ ] GET /api/list/:listName 39 | [ ] POST /api/list/:listName/_exists 40 | 41 | [ ] GET /api/user/current 42 | [ ] POST /api/user/_search 43 | [X] POST /api/user 44 | [X] GET /api/user/:userId 45 | [ ] DELETE /api/user/:userId 46 | [ ] PATCH /api/user/:userId 47 | [ ] POST /api/user/:userId/password/set 48 | [ ] POST /api/user/:userId/password/change 49 | [ ] GET /api/user/:userId/key 50 | [ ] DELETE /api/user/:userId/key 51 | [ ] POST /api/user/:userId/key/renew 52 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "3.2.0-1" 2 | -------------------------------------------------------------------------------- /www/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "angularjs-annotate" 4 | ], 5 | "presets": [ 6 | "stage-1", 7 | ["es2015", { "modules": false }], 8 | "es2017" 9 | ] 10 | } -------------------------------------------------------------------------------- /www/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module" 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "eqeqeq": "warn", 9 | "no-fallthrough": "off", 10 | "no-shadow": "warn", 11 | "arrow-body-style": ["warn", "as-needed"], 12 | "arrow-parens": ["warn", "as-needed"], 13 | "no-var": "error" 14 | }, 15 | "globals": { 16 | "angular": false, 17 | "Dropzone": false, 18 | "$": false 19 | }, 20 | "env": { 21 | "browser": true, 22 | "node": true 23 | } 24 | } -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | .sass-cache/ 4 | .idea/ 5 | .tmp/ 6 | true!ng-annotate/ 7 | dist/ 8 | package-lock.json 9 | config/manifest.json 10 | -------------------------------------------------------------------------------- /www/.nvmrc: -------------------------------------------------------------------------------- 1 | v12.18 2 | -------------------------------------------------------------------------------- /www/config/webpack/environments/development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let webpack = require('webpack'); 3 | 4 | module.exports = function (_path) { 5 | return { 6 | context: _path, 7 | devtool: 'source-map', 8 | devServer: { 9 | contentBase: './dist', 10 | hot: true, 11 | inline: true, 12 | proxy: { 13 | '/api': 'http://localhost:9001' 14 | } 15 | }, 16 | plugins: [new webpack.HotModuleReplacementPlugin()] 17 | }; 18 | }; -------------------------------------------------------------------------------- /www/config/webpack/environments/production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let CleanWebpackPlugin = require('clean-webpack-plugin'); 3 | let webpack = require('webpack'); 4 | 5 | module.exports = function (_path) { 6 | return { 7 | context: _path, 8 | devtool: 'source-map', 9 | output: { 10 | publicPath: './', 11 | filename: '[name].[chunkhash].js' 12 | }, 13 | plugins: [ 14 | new CleanWebpackPlugin(['dist'], { 15 | root: _path, 16 | verbose: true, 17 | dry: false 18 | }), 19 | new webpack.optimize.UglifyJsPlugin({ 20 | minimize: true, 21 | warnings: false, 22 | sourceMap: true 23 | }) 24 | ] 25 | }; 26 | }; -------------------------------------------------------------------------------- /www/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /www/src/app/components/about/about.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class AboutController { 4 | constructor($log, $uibModalInstance, config) { 5 | 'ngInject'; 6 | 7 | this.$log = $log; 8 | this.$uibModalInstance = $uibModalInstance; 9 | 10 | this.version = config.versions; 11 | this.connectors = config.connectors; 12 | } 13 | 14 | close() { 15 | this.$uibModalInstance.close(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /www/src/app/components/about/about.html: -------------------------------------------------------------------------------- 1 | 4 | 31 | 38 | -------------------------------------------------------------------------------- /www/src/app/components/container/container.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import containerTpl from './container.html'; 4 | 5 | import './container.scss'; 6 | 7 | class ContainerController { 8 | constructor($log) { 9 | 'ngInject'; 10 | this.$log = $log; 11 | } 12 | } 13 | 14 | export default class ContainerComponent { 15 | constructor() { 16 | this.templateUrl = containerTpl; 17 | this.controller = ContainerController; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /www/src/app/components/container/container.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 |
-------------------------------------------------------------------------------- /www/src/app/components/container/container.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/app/components/container/container.scss -------------------------------------------------------------------------------- /www/src/app/components/footer/footer.component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import footerTpl from './footer.html'; 4 | 5 | class FooterController { 6 | constructor($log, $scope) { 7 | 'ngInject'; 8 | this.$log = $log; 9 | $scope.date = new Date(); 10 | } 11 | } 12 | 13 | export default class FooterComponent { 14 | constructor() { 15 | this.templateUrl = footerTpl; 16 | this.controller = FooterController; 17 | this.require = { 18 | main: '^mainPage' 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /www/src/app/components/footer/footer.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 | TheHive Project 2016-{{date | date:'yyyy'}}, 8 | AGPL-V3 9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /www/src/app/components/footer/footer.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/app/components/footer/footer.scss -------------------------------------------------------------------------------- /www/src/app/components/header/header.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | .avatar.avatar-xs { 3 | line-height: 20px; 4 | 5 | span { 6 | line-height: 20px; 7 | font-weight: bold; 8 | } 9 | 10 | .avatar-icon { 11 | margin-top: -5px; 12 | } 13 | } 14 | } 15 | 16 | .profile-dropdown { 17 | &.open { 18 | .dropdown-menu { 19 | left: auto; 20 | right: 0; 21 | } 22 | } 23 | } 24 | 25 | .notif-badge { 26 | position: relative; 27 | } 28 | 29 | .notif-badge:after { 30 | position: absolute; 31 | content: ""; 32 | width: 10px; 33 | height: 10px; 34 | left: -5px; 35 | top: -5px; 36 | background-color: rgb(240, 43, 43); 37 | border-radius: 50%; 38 | } -------------------------------------------------------------------------------- /www/src/app/core/controllers/PageController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class PageController { 4 | constructor(id) { 5 | 'ngInject'; 6 | 7 | this.id = id; 8 | 9 | this.state = { 10 | filters: {}, 11 | pagination: { 12 | pageSize: 50, 13 | current: 1 14 | } 15 | }; 16 | } 17 | 18 | buildRange() { 19 | let page = this.pagination.current, 20 | size = this.pagination.pageSize; 21 | 22 | return `${(page - 1) * size}-${(page - 1) * size + size}`; 23 | } 24 | 25 | applyFilters() { 26 | this.state.filters = this.filters; 27 | this.localStorageService.set(this.id, this.state); 28 | this.load(1); 29 | } 30 | 31 | clearFilter(filterName) { 32 | this.filters[filterName] = _.isArray(this.filters[filterName]) ? [] : null; 33 | this.applyFilters(); 34 | } 35 | 36 | clearFilters() { 37 | _.forEach(_.keys(this.filters), key => { 38 | this.filters[key] = _.isArray(this.filters[key]) ? [] : null; 39 | }); 40 | 41 | this.applyFilters(); 42 | } 43 | 44 | buildQuery() {} 45 | load() {} 46 | } 47 | -------------------------------------------------------------------------------- /www/src/app/core/core.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const core = angular.module('cortex.core', []); 4 | 5 | import fixedHeightDirective from './directives/fixed-height/fixed-height.directive'; 6 | import fileChooserDirective from './directives/file-chooser/file-chooser.directive'; 7 | import requireRolesDirective from './directives/require-roles/require-roles.directive'; 8 | import compareToDirective from './directives/compare-to/compare-to.directive'; 9 | import userAvatarDirective from './directives/user-avatar/user-avatar.directive'; 10 | import tlpDirective from './directives/tlp/tlp.directive'; 11 | import taxonomieDirective from './directives/taxonomie/taxonomie.directive'; 12 | import autofocusDirective from './directives/autofocus/autofocus.directive'; 13 | 14 | import constants from './services/constants'; 15 | 16 | import notificationService from './services/common/NotificationService'; 17 | import streamService from './services/common/StreamService'; 18 | import versionService from './services/common/VersionService'; 19 | import AlertService from './services/common/AlertService'; 20 | import utilsService from './services/common/UtilsService'; 21 | 22 | import fangFilter from './filters/fang'; 23 | 24 | import AuthService from './services/common/AuthService'; 25 | import HtmlSanitizer from './services/common/HtmlSanitizer'; 26 | import UserService from './services/common/UserService'; 27 | import SearchService from './services/common/SearchService'; 28 | 29 | import ModalService from './services/common/ModalService'; 30 | 31 | core 32 | .service('AuthService', AuthService) 33 | .service('HtmlSanitizer', HtmlSanitizer) 34 | .service('SearchService', SearchService) 35 | .service('UserService', UserService) 36 | .service('ModalService', ModalService) 37 | .service('AlertService', AlertService); 38 | 39 | fixedHeightDirective(core); 40 | fileChooserDirective(core); 41 | requireRolesDirective(core); 42 | compareToDirective(core); 43 | userAvatarDirective(core) 44 | tlpDirective(core); 45 | taxonomieDirective(core); 46 | autofocusDirective(core); 47 | 48 | /* Common services */ 49 | 50 | notificationService(core); 51 | streamService(core); 52 | versionService(core); 53 | utilsService(core); 54 | 55 | /* App services */ 56 | constants(core); 57 | 58 | /* Filters */ 59 | fangFilter(core); 60 | 61 | export default core; -------------------------------------------------------------------------------- /www/src/app/core/directives/autofocus/autofocus.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default function(app) { 4 | app.directive('autoFocus', autofocus); 5 | 6 | function autofocus($timeout) { 7 | 'ngInject'; 8 | 9 | return { 10 | restrict: 'A', 11 | link: linkFn 12 | }; 13 | 14 | function linkFn(scope, element, attrs) { 15 | if (attrs.autoFocus) { 16 | scope.$on(attrs.autoFocus, () => { 17 | $timeout(function() { 18 | element[0].focus(); 19 | }); 20 | }); 21 | } else { 22 | $timeout(function() { 23 | element[0].focus(); 24 | }); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /www/src/app/core/directives/compare-to/compare-to.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default function(app) { 4 | app.directive('compareTo', compareTo); 5 | 6 | function compareTo() { 7 | 'ngInject'; 8 | 9 | return { 10 | require: 'ngModel', 11 | scope: { 12 | otherModelValue: '=compareTo' 13 | }, 14 | link: linkFn 15 | }; 16 | 17 | function linkFn(scope, element, attributes, ngModel) { 18 | ngModel.$validators.compareTo = function(modelValue) { 19 | return modelValue === scope.otherModelValue; 20 | }; 21 | 22 | scope.$watch('otherModelValue', function() { 23 | ngModel.$validate(); 24 | }); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /www/src/app/core/directives/file-chooser/file-chooser.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import $ from 'jquery'; 4 | import Dropzone from 'dropzone'; 5 | 6 | import tpl from './file-chooser.html'; 7 | 8 | import './file-chooser.scss'; 9 | 10 | export default function(app) { 11 | app.directive('fileChooser', fileChooserDirective); 12 | 13 | function fileChooserDirective() { 14 | 'ngInject'; 15 | 16 | return { 17 | restrict: 'A', 18 | templateUrl: tpl, 19 | scope: { 20 | filemodel: '=', 21 | control: '=', 22 | preview: '@?', 23 | accept: '@?' 24 | }, 25 | link: linkFn 26 | }; 27 | 28 | function linkFn(scope, element) { 29 | let dropzone; 30 | element.addClass('dropzone'); 31 | let template = element[0].innerHTML; 32 | $(element[0].children[0]).remove(); 33 | // create a Dropzone for the element with the given options 34 | dropzone = new Dropzone(element[0], { 35 | // 'clickable' : '.dz-clickable', 36 | url: 'dummy', 37 | autoProcessQueue: false, 38 | maxFiles: 1, 39 | // 'addRemoveLinks' : true, 40 | createImageThumbnails: angular.isString(scope.preview) 41 | ? scope.preview === 'true' 42 | : true, 43 | acceptedFiles: angular.isString(scope.accept) 44 | ? scope.accept 45 | : undefined, 46 | previewTemplate: template 47 | }); 48 | 49 | dropzone.on('addedfile', function(file) { 50 | scope.$apply(function() { 51 | scope.filemodel = file; 52 | }); 53 | }); 54 | dropzone.on('removedfile', function() { 55 | setTimeout(function() { 56 | scope.$apply(function() { 57 | delete scope.filemodel; 58 | // scope.filemodel = undefined; 59 | }); 60 | }, 0); 61 | }); 62 | dropzone.on('maxfilesexceeded', function(file) { 63 | this.removeFile(file); 64 | }); 65 | if (angular.isDefined(scope.control)) { 66 | scope.control.removeAllFiles = function() { 67 | dropzone.removeAllFiles(); 68 | }; 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /www/src/app/core/directives/file-chooser/file-chooser.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 8 |
9 |
10 | Drop file or click 11 |
-------------------------------------------------------------------------------- /www/src/app/core/directives/file-chooser/file-chooser.scss: -------------------------------------------------------------------------------- 1 | .dropzone { 2 | height: auto; 3 | min-height: 5em; 4 | padding: 5px; 5 | background-color: #fafafa; 6 | } 7 | 8 | .dropzone .dz-message { 9 | margin: 0 !important; 10 | border: 3px dashed #337ab7 !important; 11 | line-height: 4em; 12 | text-align: center; 13 | cursor: pointer; 14 | } 15 | 16 | .dropzone .dz-message span { 17 | position: inherit !important; 18 | } -------------------------------------------------------------------------------- /www/src/app/core/directives/fixed-height/fixed-height.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import $ from 'jquery'; 4 | 5 | export default function(app) { 6 | app.directive('fixedHeight', fixedHeightDirective); 7 | 8 | function fixedHeightDirective($window, $timeout) { 9 | 'ngInject'; 10 | 11 | return { 12 | restrict: 'A', 13 | link: linkFn 14 | }; 15 | 16 | function linkFn(scope, elem /*, attr*/) { 17 | $timeout(function() { 18 | let windowHeight = $(window).height(); 19 | let footerHeight = $('.main-footer').outerHeight(); 20 | let headerHeight = $('.main-header').height(); 21 | 22 | elem.css('min-height', windowHeight - headerHeight - footerHeight); 23 | }, 500); 24 | 25 | angular.element($window).bind('resize', function() { 26 | let windowHeight = $(window).height(); 27 | let footerHeight = $('.main-footer').outerHeight(); 28 | let headerHeight = $('.main-header').height(); 29 | 30 | elem.css('min-height', windowHeight - headerHeight - footerHeight); 31 | }); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /www/src/app/core/directives/require-roles/require-roles.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default function(app) { 4 | app.directive('requireRoles', requireRoles); 5 | 6 | function requireRoles($log, AuthService) { 7 | 'ngInject'; 8 | 9 | return { 10 | restrict: 'A', 11 | scope: false, 12 | link: linkFn 13 | }; 14 | 15 | function linkFn(scope, elem, attrs) { 16 | let roles = (attrs['requireRoles'] || '').split(','); 17 | 18 | if (!AuthService.hasRole(roles || [])) { 19 | elem.remove(); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /www/src/app/core/directives/taxonomie/taxonomie.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash/core'; 4 | 5 | import tpl from './taxonomie.html'; 6 | 7 | export default function (app) { 8 | app.directive('taxonomie', taxonomie); 9 | 10 | function taxonomie() { 11 | 'ngInject'; 12 | 13 | return { 14 | templateUrl: tpl, 15 | scope: { 16 | taxonomies: '=' 17 | }, 18 | replace: true, 19 | link: linkFn 20 | }; 21 | 22 | function linkFn(scope) { 23 | 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /www/src/app/core/directives/taxonomie/taxonomie.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 | {{taxonomy.namespace}}:{{taxonomy.predicate}}{{(taxonomy.value != undefined && taxonomy.value!= null) ? '="'+taxonomy.value+'"': ''}} 9 | 10 |
11 | 12 |
13 | No summary available 14 |
15 |
-------------------------------------------------------------------------------- /www/src/app/core/directives/tlp/tlp.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash/core'; 4 | 5 | import tpl from './tlp.html'; 6 | import './tlp.scss'; 7 | 8 | export default function (app) { 9 | app.directive('tlp', tlp); 10 | 11 | function tlp(Tlps) { 12 | 'ngInject'; 13 | 14 | return { 15 | templateUrl: tpl, 16 | scope: { 17 | value: '=', 18 | namespace: '@' 19 | }, 20 | replace: true, 21 | link: linkFn 22 | }; 23 | 24 | function linkFn(scope) { 25 | scope.$watch('value', v => { 26 | if (v === undefined) { 27 | scope.tlpClass = 'label-none'; 28 | scope.tlp = 'None'; 29 | } else { 30 | const temp = (_.find(Tlps, { 31 | value: v 32 | }) || {}).key; 33 | 34 | scope.tlpClass = `label-${(temp || '').toLowerCase()}`; 35 | scope.tlp = `${scope.namespace || 'TLP'}:${temp}`; 36 | } 37 | }); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /www/src/app/core/directives/tlp/tlp.html: -------------------------------------------------------------------------------- 1 | {{tlp}} -------------------------------------------------------------------------------- /www/src/app/core/directives/tlp/tlp.scss: -------------------------------------------------------------------------------- 1 | .label.label-none { 2 | background-color: #d2d6de; 3 | color: #444; 4 | } 5 | 6 | .label.label-white { 7 | background-color: #5BC0CE; 8 | } 9 | 10 | .label.label-green { 11 | background-color: #5CB85C; 12 | } 13 | 14 | .label.label-amber { 15 | background-color: #F0AD4E; 16 | } 17 | 18 | .label.label-red { 19 | background-color: #D9534F; 20 | } -------------------------------------------------------------------------------- /www/src/app/core/directives/user-avatar/user-avatar.directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import tpl from './user-avatar.html'; 4 | import './user-avatar.scss'; 5 | 6 | export default function(app) { 7 | app.directive('userAvatar', userAvatar); 8 | 9 | function userAvatar($log, UserService) { 10 | 'ngInject'; 11 | 12 | return { 13 | templateUrl: tpl, 14 | scope: { 15 | user: '=userId', 16 | prefix: '=', 17 | iconOnly: '=', 18 | iconSize: '@' 19 | }, 20 | link: linkFn 21 | }; 22 | 23 | function linkFn(scope) { 24 | scope.userInfo = UserService; 25 | scope.initials = ''; 26 | 27 | scope.$watch('userData.name', value => { 28 | if (!value) { 29 | return; 30 | } 31 | 32 | scope.initials = value 33 | .split(' ') 34 | .map(item => item[0]) 35 | .join('') 36 | .substr(0, 3) 37 | .toUpperCase(); 38 | }); 39 | 40 | scope.$watch('user', value => { 41 | if (!value) { 42 | return; 43 | } 44 | scope.userInfo.getCache(value).then(userData => { 45 | scope.userData = userData; 46 | }); 47 | }); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /www/src/app/core/directives/user-avatar/user-avatar.html: -------------------------------------------------------------------------------- 1 |
2 | {{userData.name}} 3 |
{{initials}}
4 | {{prefix ? userData.organization + '/' + userData.name : userData.name}} 5 |
6 | 7 | -------------------------------------------------------------------------------- /www/src/app/core/directives/user-avatar/user-avatar.scss: -------------------------------------------------------------------------------- 1 | .avatar .avatar-icon { 2 | border-radius: 50px; 3 | } 4 | 5 | .avatar .avatar-name { 6 | margin-left: 8px; 7 | } 8 | 9 | .avatar .avatar-icon { 10 | position: absolute; 11 | } 12 | 13 | .avatar div.avatar-icon { 14 | background-color: #ccc; 15 | font-weight: bold; 16 | float: left; 17 | text-align: center; 18 | color: #000; 19 | } 20 | 21 | .avatar.avatar-xs { 22 | line-height: 30px; 23 | } 24 | 25 | .avatar.avatar-m { 26 | line-height: 40px; 27 | } 28 | 29 | .avatar.avatar-xs .avatar-icon { 30 | width: 30px; 31 | height: 30px; 32 | line-height: 30px; 33 | font-size: 12px; 34 | } 35 | 36 | .avatar.avatar-m .avatar-icon { 37 | width: 40px; 38 | height: 40px; 39 | line-height: 40px; 40 | } 41 | 42 | .avatar.avatar-xs .avatar-name { 43 | margin-left: 35px; 44 | } 45 | 46 | .avatar.avatar-m .avatar-name { 47 | margin-left: 45px; 48 | } 49 | 50 | .avatar-input { 51 | width: 0.1px; 52 | height: 0.1px; 53 | opacity: 0; 54 | overflow: hidden; 55 | position: absolute; 56 | z-index: -1; 57 | } 58 | -------------------------------------------------------------------------------- /www/src/app/core/filters/fang.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default function(app) { 4 | app.filter('fang', UtilsService => function(value) { 5 | if (!value) { 6 | return ''; 7 | } 8 | 9 | return UtilsService.fangValue(value); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /www/src/app/core/services/common/AlertService.js: -------------------------------------------------------------------------------- 1 | 'user strict'; 2 | 3 | import _ from "lodash"; 4 | 5 | export default class AlertService { 6 | constructor($http, $interval) { 7 | 'ngInject'; 8 | 9 | this.$http = $http; 10 | this.$interval = $interval; 11 | this.update = 0; 12 | this.alerts = []; 13 | } 14 | 15 | startUpdate() { 16 | this.update += 1; 17 | if (!this.timer) { 18 | this.timer = this.$interval(this.updateAlerts, 10000, 0, true, this); 19 | } 20 | } 21 | 22 | stopUpdate() { 23 | this.update -= 1; 24 | if (this.update <= 0 && this.$interval) { 25 | this.$interval.cancel(this.timer); 26 | delete this.$interval; 27 | } 28 | } 29 | 30 | updateAlerts(self) { 31 | self.$http.get('./api/alert').then( 32 | response => { 33 | self.alerts = response.data; 34 | }, 35 | () => { 36 | self.alerts = []; 37 | } 38 | ); 39 | } 40 | 41 | contains(alertType) { 42 | return _.find(this.alerts, { type: alertType }); 43 | } 44 | 45 | nonEmpty() { 46 | return this.alerts.length > 0; 47 | } 48 | 49 | isEmpty() { 50 | return this.alerts.length === 0; 51 | } 52 | } -------------------------------------------------------------------------------- /www/src/app/core/services/common/AuthService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash'; 4 | 5 | export default class AuthService { 6 | constructor($http, $log, $q) { 7 | 'ngInject'; 8 | 9 | this.$log = $log; 10 | this.$q = $q; 11 | this.$http = $http; 12 | 13 | this.currentUser = null; 14 | } 15 | 16 | login(username, password) { 17 | let defer = this.$q.defer(); 18 | 19 | this.$http 20 | .post('./api/login', { 21 | user: username, 22 | password: password 23 | }) 24 | .then(response => defer.resolve(response.data)) 25 | .catch(err => defer.reject(err)); 26 | 27 | return defer.promise; 28 | } 29 | 30 | ssoLogin(code) { 31 | let defer = this.$q.defer(); 32 | 33 | this.$http 34 | .post(angular.isDefined(code) ? './api/ssoLogin?code=' + code : './api/ssoLogin') 35 | .then(response => defer.resolve(response)) 36 | .catch(err => defer.reject(err)); 37 | 38 | return defer.promise; 39 | } 40 | 41 | logout() { 42 | return this.$http.get('./api/logout').then( 43 | /*data*/ 44 | () => { 45 | this.currentUser = null; 46 | } 47 | ); 48 | } 49 | 50 | current() { 51 | return this.$http 52 | .get('./api/user/current') 53 | .then(response => { 54 | this.currentUser = response.data; 55 | 56 | return this.$q.resolve(this.currentUser); 57 | }) 58 | .catch(err => { 59 | this.currentUser = null; 60 | 61 | return this.$q.reject(err); 62 | }); 63 | } 64 | 65 | isOrgAdmin(user) { 66 | let re = /orgadmin/i; 67 | 68 | return re.test(user.roles); 69 | } 70 | 71 | isSuperAdmin(user) { 72 | let re = /superadmin/i; 73 | 74 | return re.test(user.roles); 75 | } 76 | 77 | hasRole(roles) { 78 | if (!this.currentUser) { 79 | return false; 80 | } 81 | 82 | return !_.isEmpty(_.intersection(this.currentUser.roles, roles)); 83 | } 84 | } -------------------------------------------------------------------------------- /www/src/app/core/services/common/HtmlSanitizer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class HtmlSanitizer { 4 | constructor($sanitize) { 5 | 'ngInject'; 6 | 7 | this.$sanitize = $sanitize; 8 | this.entityMap = { 9 | '&': '&', 10 | '<': '<', 11 | '>': '>', 12 | '"': '"', 13 | "'": ''', 14 | '/': '/' 15 | }; 16 | } 17 | 18 | sanitize(str) { 19 | return this.$sanitize( 20 | String(str).replace(/[&<>"'\/]/g, s => this.entityMap[s]) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /www/src/app/core/services/common/ModalService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import modalConfirmTpl from './modal.confirm.html'; 4 | 5 | class ModalConfirmController { 6 | constructor($uibModalInstance, title, message, config) { 7 | 'ngInject'; 8 | 9 | this.$uibModalInstance = $uibModalInstance; 10 | this.title = title; 11 | this.message = message; 12 | this.config = config; 13 | } 14 | cancel() { 15 | this.$uibModalInstance.dismiss('cancel'); 16 | } 17 | confirm() { 18 | this.$uibModalInstance.close('ok'); 19 | } 20 | } 21 | 22 | export default class ModalService { 23 | constructor($log, $uibModal) { 24 | 'ngInject'; 25 | 26 | this.$log = $log; 27 | this.$uibModal = $uibModal; 28 | } 29 | 30 | /** 31 | * @param {*} title: Title of the modal 32 | * @param {*} message: Content of the modal 33 | * @param {Object} config: customization of the modal: flavor, okText 34 | */ 35 | confirm(title, message, config) { 36 | return this.$uibModal.open({ 37 | controller: ModalConfirmController, 38 | templateUrl: modalConfirmTpl, 39 | controllerAs: '$modal', 40 | resolve: { 41 | title: function() { 42 | return title; 43 | }, 44 | message: function() { 45 | return message; 46 | }, 47 | config: function() { 48 | return config || {}; 49 | } 50 | } 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /www/src/app/core/services/common/NotificationService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default function(app) { 4 | function NotificationServiceProvider() { 5 | let loginState = 'login'; 6 | let maintenanceState = 'maintenance'; 7 | 8 | this.setLoginState = function(state) { 9 | loginState = state; 10 | }; 11 | 12 | this.setMaintenanceState = function(state) { 13 | maintenanceState = state; 14 | }; 15 | 16 | function NotificationService($state, HtmlSanitizer, Notification) { 17 | 'ngInject'; 18 | 19 | this.success = function(message) { 20 | let sanitized = HtmlSanitizer.sanitize(message); 21 | 22 | return Notification.success(sanitized); 23 | }; 24 | this.error = function(message) { 25 | let sanitized = HtmlSanitizer.sanitize(message); 26 | 27 | return Notification.error(sanitized); 28 | }; 29 | this.log = function(message, type) { 30 | Notification[type || 'error'](HtmlSanitizer.sanitize(message)); 31 | }; 32 | this.handleError = function(moduleName, data, status) { 33 | if (status === 401) { 34 | $state.go(loginState); 35 | } else if (status === 520) { 36 | $state.go(maintenanceState); 37 | } else if (angular.isString(data) && data !== '') { 38 | this.log(moduleName + ': ' + data, 'error'); 39 | } else if (angular.isObject(data)) { 40 | this.log(moduleName + ': ' + data.message, 'error'); 41 | } 42 | }; 43 | } 44 | 45 | this.$get = function($state, HtmlSanitizer, Notification) { 46 | 'ngInject'; 47 | 48 | return new NotificationService($state, HtmlSanitizer, Notification); 49 | }; 50 | } 51 | 52 | app.provider('NotificationService', NotificationServiceProvider); 53 | } 54 | -------------------------------------------------------------------------------- /www/src/app/core/services/common/UtilsService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default function(app) { 4 | app.service('UtilsService', function() { 5 | this.sensitiveTypes = ['url', 'ip', 'mail', 'domain', 'filename']; 6 | 7 | this.fangValue = function(value) { 8 | return value 9 | .replace(/\[\.\]/g, '.') 10 | .replace(/hxxp/gi, 'http') 11 | .replace(/\./g, '[.]') 12 | .replace(/http/gi, 'hxxp'); 13 | }; 14 | 15 | this.fang = function(observable) { 16 | if (this.sensitiveTypes.indexOf(observable.dataType) === -1) { 17 | return observable.data; 18 | } 19 | 20 | return this.fangValue(observable.data); 21 | }; 22 | 23 | this.unfang = function(observable) { 24 | return observable.data.replace(/\[\.\]/g, '.').replace(/hxxp/gi, 'http'); 25 | }; 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /www/src/app/core/services/common/VersionService.js: -------------------------------------------------------------------------------- 1 | 'user strict'; 2 | 3 | export default function(app) { 4 | app.service('VersionService', function($q, $http) { 5 | this.get = function() { 6 | let deferred = $q.defer(); 7 | 8 | $http.get('./api/status').then( 9 | response => { 10 | deferred.resolve(response); 11 | }, 12 | rejection => { 13 | deferred.reject(rejection); 14 | } 15 | ); 16 | return deferred.promise; 17 | }; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /www/src/app/core/services/common/modal.confirm.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 8 | 12 |
13 | -------------------------------------------------------------------------------- /www/src/app/core/services/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default function (app) { 4 | app 5 | .constant('UrlParser', window.url) 6 | .constant('ROUTE_ERRORS', { 7 | auth: 'Authorization has been denied.' 8 | }) 9 | .constant('Roles', { 10 | SUPERADMIN: 'superadmin', 11 | ORGADMIN: 'orgadmin', 12 | ANALYZE: 'analyze', 13 | READ: 'read' 14 | }) 15 | .value('Tlps', [{ 16 | key: 'WHITE', 17 | value: 0 18 | }, 19 | { 20 | key: 'GREEN', 21 | value: 1 22 | }, 23 | { 24 | key: 'AMBER', 25 | value: 2 26 | }, 27 | { 28 | key: 'RED', 29 | value: 3 30 | } 31 | ]); 32 | } -------------------------------------------------------------------------------- /www/src/app/index.bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // index.html page to dist folder 4 | import '!!file-loader?name=[name].[ext]!../favicon.ico'; 5 | import '!!file-loader?name=[name].[ext]!../favicon-16x16.png'; 6 | import '!!file-loader?name=[name].[ext]!../favicon-32x32.png'; 7 | import '!!file-loader?name=[name].[ext]!../favicon-96x96.png'; 8 | import '!!file-loader?name=[name].[ext]!../favicon-128x128.png'; 9 | import '!!file-loader?name=[name].[ext]!../favicon-196x196.png'; 10 | 11 | import '../assets/images/logo-dark.svg'; 12 | 13 | // vendor files 14 | import './index.vendor'; 15 | 16 | // main App module 17 | import './index.module'; 18 | 19 | import '../assets/styles/sass/index.scss'; 20 | 21 | angular.element(document).ready(() => { 22 | angular.bootstrap(document, ['cortex'], { 23 | strictDi: true 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /www/src/app/index.components.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import ContainerComponent from './components/container/container.component'; 4 | import HeaderComponent from './components/header/header.component'; 5 | import FooterComponent from './components/footer/footer.component'; 6 | 7 | const componentsModule = angular.module('index.components', []); 8 | 9 | componentsModule 10 | .component('container', new ContainerComponent()) 11 | .component('header', new HeaderComponent()) 12 | .component('footer', new FooterComponent()); 13 | 14 | export default componentsModule; 15 | -------------------------------------------------------------------------------- /www/src/app/index.config.js: -------------------------------------------------------------------------------- 1 | /*global NODE_ENV*/ 2 | 'use strict'; 3 | 4 | function config( 5 | $logProvider, 6 | $httpProvider, 7 | $compileProvider, 8 | $locationProvider, 9 | MaintenanceServiceProvider, 10 | NotificationProvider, 11 | localStorageServiceProvider 12 | ) { 13 | 'ngInject'; 14 | 15 | $httpProvider.defaults.xsrfCookieName = 'CORTEX-XSRF-TOKEN'; 16 | $httpProvider.defaults.xsrfHeaderName = 'X-CORTEX-XSRF-TOKEN'; 17 | 18 | $logProvider.debugEnabled(false); 19 | 20 | if (NODE_ENV === 'production') { 21 | $logProvider.debugEnabled(false); 22 | $compileProvider.debugInfoEnabled(false); 23 | } 24 | 25 | $locationProvider.html5Mode(false); 26 | 27 | MaintenanceServiceProvider.setSuccessState('index'); 28 | 29 | NotificationProvider.setOptions({ 30 | delay: 10000, 31 | startTop: 20, 32 | startRight: 10, 33 | verticalSpacing: 20, 34 | horizontalSpacing: 20, 35 | positionX: 'left', 36 | positionY: 'bottom' 37 | }); 38 | 39 | // Configure local storage service 40 | localStorageServiceProvider.setPrefix('cortex'); 41 | } 42 | 43 | export default config; 44 | -------------------------------------------------------------------------------- /www/src/app/index.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import config from './index.config'; 4 | import run from './index.run'; 5 | 6 | import uiRouter from '@uirouter/angularjs'; 7 | 8 | import coreModule from './core/core.module'; 9 | import indexComponents from './index.components'; 10 | 11 | import mainModule from './pages/main/main.module'; 12 | import loginModule from './pages/login/login.module'; 13 | import analyzersModule from './pages/analyzers/analyzers.module'; 14 | import respondersModule from './pages/responders/responders.module'; 15 | import jobsModule from './pages/jobs/jobs.module'; 16 | import settingsModule from './pages/settings/settings.module'; 17 | 18 | import adminModule from './pages/admin/admin.module'; 19 | 20 | import maintenanceModule from './pages/maintenance/maintenance.module'; 21 | 22 | const App = angular.module('cortex', [ 23 | // plugins 24 | uiRouter, 25 | 'ngSanitize', 26 | 'ngMessages', 27 | 'ngResource', 28 | 'ui.bootstrap', 29 | 'ui-notification', 30 | 'angularUtils.directives.dirPagination', 31 | 'angularMoment', 32 | 'angular-clipboard', 33 | 'btorfs.multiselect', 34 | 'LocalStorageModule', 35 | 'angularUtils.directives.dirPagination', 36 | 'ui.utils.masks', 37 | 38 | // core 39 | coreModule.name, 40 | 41 | // components 42 | indexComponents.name, 43 | 44 | // pages 45 | mainModule.name, 46 | loginModule.name, 47 | maintenanceModule.name, 48 | analyzersModule.name, 49 | respondersModule.name, 50 | jobsModule.name, 51 | adminModule.name, 52 | settingsModule.name 53 | ]); 54 | 55 | App.config(config).run(run); 56 | 57 | export default App; -------------------------------------------------------------------------------- /www/src/app/index.run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash/core'; 4 | 5 | function runBlock( 6 | $log, 7 | $q, 8 | $transitions, 9 | $state, 10 | AuthService, 11 | Roles, 12 | NotificationService 13 | ) { 14 | 'ngInject'; 15 | 16 | $transitions.onSuccess({}, transition => { 17 | transition 18 | .injector() 19 | .get('$window') 20 | .scrollTo(0, 0); 21 | }); 22 | 23 | $transitions.onBefore({ to: 'index' }, transition => { 24 | const stateService = transition.router.stateService; 25 | 26 | return AuthService.current() 27 | .then(user => { 28 | if (user.roles.indexOf(Roles.SUPERADMIN) !== -1) { 29 | return stateService.target('main.organizations'); 30 | } else { 31 | return stateService.target('main.jobs'); 32 | } 33 | }) 34 | .catch(err => $q.reject(err)); 35 | }); 36 | 37 | $transitions.onBefore({}, transition => { 38 | let roles = (transition.to().data && transition.to().data.allow) || []; 39 | let auth = transition.injector().get('AuthService'); 40 | 41 | if (auth.currentUser === null) { 42 | return; 43 | } 44 | 45 | if (!_.isEmpty(roles) && !auth.hasRole(roles)) { 46 | return transition.router.stateService.target('main.jobs'); 47 | } 48 | }); 49 | 50 | $transitions.onError({}, transition => { 51 | if (!transition.error().detail) { 52 | return; 53 | } 54 | 55 | let status = transition.error().detail.status; 56 | 57 | if (status === 520) { 58 | $state.go('maintenance'); 59 | } else if (status === 401) { 60 | $state.go('login'); 61 | } else { 62 | NotificationService.handleError( 63 | 'Error', 64 | transition.error().detail.data, 65 | status 66 | ); 67 | } 68 | }); 69 | } 70 | 71 | export default runBlock; 72 | -------------------------------------------------------------------------------- /www/src/app/index.vendor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import $ from 'jquery'; 4 | 5 | import 'angular'; 6 | 7 | import '@uirouter/angularjs'; 8 | 9 | import 'angular-resource'; 10 | 11 | import 'angular-sanitize'; 12 | 13 | import 'angular-messages'; 14 | 15 | import 'angular-resource'; 16 | 17 | import 'angular-ui-bootstrap'; 18 | 19 | import 'angular-ui-notification'; 20 | 21 | import 'angular-utils-pagination'; 22 | 23 | import 'moment'; 24 | 25 | import 'angular-moment'; 26 | 27 | import 'dropzone'; 28 | 29 | import 'angular-clipboard'; 30 | 31 | import 'angular-bootstrap-multiselect'; 32 | 33 | import 'angular-local-storage'; 34 | 35 | import 'angular-utils-pagination'; 36 | 37 | import 'angular-images-resizer'; 38 | 39 | import 'angular-base64-upload'; 40 | 41 | import 'angular-input-masks'; 42 | 43 | // local scripts 44 | //import "../assets/js/..."; 45 | 46 | import 'font-awesome/css/font-awesome.css'; 47 | import 'angular-ui-notification/dist/angular-ui-notification.css'; 48 | import 'css-spaces/dist/spaces.css'; 49 | 50 | import 'js-url'; -------------------------------------------------------------------------------- /www/src/app/pages/admin/admin.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import UsersListController from './common/user-list/users-list.controller'; 4 | import UsersListTpl from './common/user-list/users-list.html'; 5 | 6 | import adminOrganizationsModule from './organizations/organizations.module'; 7 | import adminUsersModule from './users/users.module'; 8 | 9 | const adminModule = angular.module('cortex.admin', [ 10 | adminOrganizationsModule.name, 11 | adminUsersModule.name 12 | ]); 13 | 14 | adminModule.component('usersList', { 15 | controller: UsersListController, 16 | templateUrl: UsersListTpl, 17 | bindings: { 18 | organization: '<', 19 | users: '<', 20 | onReload: '&' 21 | }, 22 | require: { 23 | main: '^^mainPage' 24 | } 25 | }); 26 | 27 | export default adminModule; 28 | -------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/components/analyzers/analyzer-config-form.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash/core'; 4 | import omit from 'lodash/omit'; 5 | 6 | export default class AnalyzerConfigFormController { 7 | constructor(Tlps, AnalyzerService) { 8 | 'ngInject'; 9 | 10 | this.AnalyzerService = AnalyzerService; 11 | this.Tlps = Tlps; 12 | this.rateUnits = ['Second', 'Minute', 'Hour', 'Day', 'Month']; 13 | } 14 | 15 | $onInit() { 16 | this.useGlobalCache = 17 | this.analyzer.jobCache === null || this.analyzer.jobCache === undefined; 18 | } 19 | 20 | applyConfig(config) { 21 | _.forEach( 22 | _.keys(config), 23 | k => (this.analyzer.configuration[k] = config[k]) 24 | ); 25 | } 26 | 27 | applyGlobalConfig() { 28 | const props = ['jobCache', 'jobTimeout']; 29 | 30 | this.applyConfig(omit(this.globalConfig.config, props)); 31 | _.each(props, prop => { 32 | this.analyzer[prop] = this.globalConfig.config[prop]; 33 | }); 34 | } 35 | 36 | applyBaseConfig() { 37 | this.applyConfig(this.baseConfig.config); 38 | } 39 | } -------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/components/analyzers/analyzer.edit.modal.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 12 | 18 |
19 | -------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/components/analyzers/config-list.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import _ from 'lodash/core'; 3 | 4 | import ConfigurationEditController from '../config.edit.controller'; 5 | import configurationEditTpl from '../config.edit.modal.html'; 6 | 7 | export default class OrganizationConfigsController { 8 | constructor($log, $uibModal, AnalyzerService, NotificationService) { 9 | 'ngInject'; 10 | 11 | this.$log = $log; 12 | this.$uibModal = $uibModal; 13 | this.AnalyzerService = AnalyzerService; 14 | this.NotificationService = NotificationService; 15 | 16 | this.state = { 17 | filter: '' 18 | }; 19 | } 20 | 21 | isSet(config) { 22 | return _.indexOf([undefined, null, ''], config) === -1; 23 | } 24 | 25 | edit(config) { 26 | let modal = this.$uibModal.open({ 27 | controller: ConfigurationEditController, 28 | templateUrl: configurationEditTpl, 29 | controllerAs: '$modal', 30 | size: 'lg', 31 | resolve: { 32 | configuration: () => { 33 | let conf = angular.copy(config); 34 | 35 | _.forEach(conf.configurationItems, item => { 36 | if(conf.config[item.name] === undefined) { 37 | 38 | if (item.defaultValue !== undefined) { 39 | conf.config[item.name] = item.defaultValue; 40 | } else { 41 | conf.config[item.name] = item.multi ? [undefined] : undefined; 42 | } 43 | } 44 | }); 45 | 46 | return conf; 47 | } 48 | } 49 | }); 50 | 51 | modal.result 52 | .then(configuration => 53 | this.AnalyzerService.saveConfiguration(config.name, { 54 | config: configuration 55 | }) 56 | ) 57 | .then(() => this.AnalyzerService.configurations()) 58 | .then(configs => (this.configurations = configs)) 59 | .then(() => 60 | this.NotificationService.success( 61 | `Configuration ${config.name} updated successfully` 62 | ) 63 | ) 64 | .catch(err => { 65 | if (!_.isString(err)) { 66 | this.NotificationService.error( 67 | `Failed to update configuration ${config.name}` 68 | ); 69 | } 70 | }); 71 | } 72 | } -------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/components/config-form.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class ConfigurationForm { 4 | constructor() {} 5 | 6 | typeOf(config) { 7 | return `${config.multi ? 'multi-' : ''}${config.type}`; 8 | } 9 | 10 | addOption(config) { 11 | let defaultValues = { 12 | string: null, 13 | number: 0, 14 | boolean: true 15 | }; 16 | 17 | if (!this.configuration[config.name]) { 18 | this.configuration[config.name] = []; 19 | } 20 | 21 | this.configuration[config.name].push(defaultValues[config.type]); 22 | } 23 | 24 | removeOption(config, index) { 25 | if (this.configuration[config].length === 1) { 26 | this.configuration[config] = [undefined]; 27 | } else { 28 | this.configuration[config].splice(index, 1); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/components/config.edit.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class ConfigurationEditController { 4 | constructor($log, $uibModalInstance, configuration) { 5 | 'ngInject'; 6 | 7 | this.$log = $log; 8 | this.$uibModalInstance = $uibModalInstance; 9 | this.configuration = configuration; 10 | } 11 | 12 | save() { 13 | this.$uibModalInstance.close(this.configuration.config); 14 | } 15 | cancel() { 16 | this.$uibModalInstance.dismiss('cancel'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/components/config.edit.modal.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 8 | 12 |
-------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/components/organization.modal.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class OrganizationModalController { 4 | constructor($log, $uibModalInstance, organization, mode) { 5 | 'ngInject'; 6 | 7 | this.$log = $log; 8 | this.organization = organization; 9 | this.$uibModalInstance = $uibModalInstance; 10 | this.mode = mode; 11 | 12 | this.initForm(organization); 13 | } 14 | 15 | initForm(organization) { 16 | this.formData = _.defaults( 17 | _.pick(organization || {}, 'id', 'name', 'description', 'status'), 18 | { 19 | name: null, 20 | description: null, 21 | status: 'Active' 22 | } 23 | ); 24 | } 25 | 26 | ok() { 27 | this.$uibModalInstance.close(this.formData); 28 | } 29 | 30 | cancel() { 31 | this.$uibModalInstance.dismiss('cancel'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/components/organization.modal.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 23 | 28 |
-------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/components/responders/config-list.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import _ from 'lodash/core'; 3 | 4 | import ConfigurationEditController from '../config.edit.controller'; 5 | import configurationEditTpl from '../config.edit.modal.html'; 6 | 7 | export default class OrganizationConfigsController { 8 | constructor($log, $uibModal, ResponderService, NotificationService) { 9 | 'ngInject'; 10 | 11 | this.$log = $log; 12 | this.$uibModal = $uibModal; 13 | this.ResponderService = ResponderService; 14 | this.NotificationService = NotificationService; 15 | 16 | this.state = { 17 | filter: '' 18 | }; 19 | } 20 | 21 | isSet(config) { 22 | return _.indexOf([undefined, null, ''], config) === -1; 23 | } 24 | 25 | edit(config) { 26 | let modal = this.$uibModal.open({ 27 | controller: ConfigurationEditController, 28 | templateUrl: configurationEditTpl, 29 | controllerAs: '$modal', 30 | size: 'lg', 31 | resolve: { 32 | configuration: () => { 33 | // let defaultValues = { 34 | // string: null, 35 | // number: 0, 36 | // boolean: true 37 | // }; 38 | let conf = angular.copy(config); 39 | 40 | _.forEach(conf.configurationItems, item => { 41 | conf.config[item.name] = 42 | conf.config[item.name] !== undefined ? 43 | conf.config[item.name] : 44 | item.defaultValue || (item.multi ? [undefined] : undefined); 45 | }); 46 | 47 | return conf; 48 | } 49 | } 50 | }); 51 | 52 | modal.result 53 | .then(configuration => 54 | this.ResponderService.saveConfiguration(config.name, { 55 | config: configuration 56 | }) 57 | ) 58 | .then(() => this.ResponderService.configurations()) 59 | .then(configs => (this.configurations = configs)) 60 | .then(() => 61 | this.NotificationService.success( 62 | `Configuration ${config.name} updated successfully` 63 | ) 64 | ) 65 | .catch(err => { 66 | if (!_.isString(err)) { 67 | this.NotificationService.error( 68 | `Failed to update configuration ${config.name}` 69 | ); 70 | } 71 | }); 72 | } 73 | } -------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/components/responders/responder-config-form.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash/core'; 4 | import omit from 'lodash/omit'; 5 | 6 | export default class ResponderConfigFormController { 7 | constructor(Tlps, ResponderService) { 8 | 'ngInject'; 9 | 10 | this.ResponderService = ResponderService; 11 | this.Tlps = Tlps; 12 | this.rateUnits = ['Second', 'Minute', 'Hour', 'Day', 'Month']; 13 | } 14 | 15 | applyConfig(config) { 16 | _.forEach( 17 | _.keys(config), 18 | k => (this.responder.configuration[k] = config[k]) 19 | ); 20 | } 21 | 22 | applyGlobalConfig() { 23 | const props = ['jobTimeout']; 24 | 25 | this.applyConfig(omit(this.globalConfig.config, props)); 26 | _.each(props, prop => { 27 | this.responder[prop] = this.globalConfig.config[prop]; 28 | }); 29 | } 30 | 31 | applyBaseConfig() { 32 | this.applyConfig(this.baseConfig.config); 33 | } 34 | } -------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/components/responders/responder.edit.modal.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 9 | 15 |
-------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/details/organization.page.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class OrganizationPageController { 4 | constructor( 5 | $log, 6 | $stateParams, 7 | $scope, 8 | AnalyzerService, 9 | ResponderService, 10 | OrganizationService, 11 | AuthService, 12 | AlertService 13 | ) { 14 | 'ngInject'; 15 | 16 | this.$log = $log; 17 | this.$scope = $scope; 18 | this.orgId = $stateParams.id; 19 | this.AnalyzerService = AnalyzerService; 20 | this.ResponderService = ResponderService; 21 | this.OrganizationService = OrganizationService; 22 | this.AuthService = AuthService; 23 | this.AlertService = AlertService; 24 | } 25 | 26 | 27 | $onInit() { 28 | this.AlertService.startUpdate(); 29 | 30 | this.$scope.$on('$destroy', () => { 31 | this.AlertService.stopUpdte(); 32 | }); 33 | } 34 | } -------------------------------------------------------------------------------- /www/src/app/pages/admin/organizations/organizations.scss: -------------------------------------------------------------------------------- 1 | .config-list { 2 | .global-config { 3 | border-left: 4px solid #337ab7; 4 | margin-left: -4px; 5 | margin-bottom: 30px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /www/src/app/pages/admin/users/users.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import angular from 'angular'; 4 | 5 | import UsersPageController from './list/users.page.controller'; 6 | import usersPageTpl from './list/users.page.html'; 7 | 8 | const usersModule = angular 9 | .module('users-module', ['ui.router']) 10 | .config(($stateProvider, Roles) => { 11 | 'ngInject'; 12 | $stateProvider.state('main.users', { 13 | url: 'admin/users', 14 | component: 'usersPage', 15 | resolve: { 16 | users: UserService => UserService.list() 17 | }, 18 | data: { 19 | allow: [Roles.SUPERADMIN, Roles.ORGADMIN] 20 | } 21 | }); 22 | }) 23 | .component('usersPage', { 24 | controller: UsersPageController, 25 | templateUrl: usersPageTpl, 26 | bindings: { 27 | users: '<' 28 | } 29 | }); 30 | 31 | export default usersModule; 32 | -------------------------------------------------------------------------------- /www/src/app/pages/analyzers/analyzer.run.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash/core'; 4 | import uniq from 'lodash/uniq'; 5 | 6 | export default class AnalyzerRunController { 7 | constructor($log, $uibModalInstance, Tlps, analyzers, observable) { 8 | 'ngInject'; 9 | 10 | this.$log = $log; 11 | this.$uibModalInstance = $uibModalInstance; 12 | 13 | this.Tlps = Tlps; 14 | this.analyzers = analyzers; 15 | this.observable = observable; 16 | this.initialAnalyzers = []; 17 | } 18 | 19 | $onInit() { 20 | this.initialAnalyzers = this.getActiveIds(); 21 | this.formData = { 22 | analyzers: this.analyzers, 23 | dataTypes: uniq(_.flatten(_.map(this.analyzers, 'dataTypeList'))), 24 | ids: this.getActiveIds().join(',') 25 | }; 26 | this.observable.tlp = this.observable.tlp || this.Tlps[2].value; 27 | this.observable.pap = this.observable.pap || this.Tlps[2].value; 28 | } 29 | 30 | getActiveIds() { 31 | return _.map(_.filter(this.analyzers, item => item.active === true), 'id'); 32 | } 33 | 34 | isFile() { 35 | return this.observable.dataType === 'file'; 36 | } 37 | 38 | clearData() { 39 | delete this.observable.data; 40 | delete this.observable.attachment; 41 | 42 | _.each(this.analyzers, item => { 43 | if (this.initialAnalyzers.indexOf(item.id) === -1) { 44 | item.active = false; 45 | } 46 | }); 47 | 48 | if (this.initialAnalyzers.length > 0) { 49 | this.formData.ids = this.initialAnalyzers.join(','); 50 | } else { 51 | delete this.formData.ids; 52 | } 53 | } 54 | 55 | toggleAnalyzer(analyzer) { 56 | analyzer.active = !analyzer.active; 57 | 58 | this.formData.ids = this.getActiveIds().join(','); 59 | } 60 | 61 | ok() { 62 | let ids = []; 63 | 64 | if (this.analyzers.length === 1) { 65 | ids = [this.analyzers[0].id]; 66 | } else { 67 | ids = this.formData.ids.split(','); 68 | } 69 | 70 | this.$uibModalInstance.close({ 71 | analyzerIds: ids, 72 | observable: this.observable 73 | }); 74 | } 75 | 76 | cancel() { 77 | this.$uibModalInstance.dismiss('cancel'); 78 | } 79 | } -------------------------------------------------------------------------------- /www/src/app/pages/analyzers/analyzers.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import AnalyzersController from './analyzers.controller'; 4 | import tpl from './analyzers.page.html'; 5 | 6 | import AnalyzersListController from './components/analyzers.list.controller'; 7 | import analyzersListTpl from './components/analyzers.list.html'; 8 | 9 | import analyzerService from './analyzers.service.js'; 10 | 11 | import './analyzers.page.scss'; 12 | 13 | const analyzersModule = angular 14 | .module('analyzers-module', ['ui.router']) 15 | .config(($stateProvider, Roles) => { 16 | 'ngInject'; 17 | 18 | $stateProvider.state('main.analyzers', { 19 | url: 'analyzers', 20 | component: 'analyzersPage', 21 | resolve: { 22 | datatypes: ($q, AnalyzerService) => { 23 | let defer = $q.defer(); 24 | 25 | AnalyzerService.list() 26 | .then(() => { 27 | defer.resolve(AnalyzerService.getTypes()); 28 | }) 29 | .catch(err => defer.reject(err)); 30 | 31 | return defer.promise; 32 | } 33 | }, 34 | data: { 35 | allow: [Roles.SUPERADMIN, Roles.ORGADMIN, Roles.ANALYZE] 36 | } 37 | }); 38 | }) 39 | .component('analyzersPage', { 40 | controller: AnalyzersController, 41 | templateUrl: tpl, 42 | bindings: { 43 | datatypes: '<' 44 | //analyzers: '<', 45 | //definitions: '<' 46 | } 47 | }) 48 | .component('analyzersList', { 49 | controller: AnalyzersListController, 50 | templateUrl: analyzersListTpl, 51 | bindings: { 52 | analyzers: '<' 53 | } 54 | }) 55 | .service('AnalyzerService', analyzerService); 56 | 57 | export default analyzersModule; 58 | -------------------------------------------------------------------------------- /www/src/app/pages/analyzers/analyzers.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/app/pages/analyzers/analyzers.page.scss -------------------------------------------------------------------------------- /www/src/app/pages/analyzers/components/analyzers.list.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class AnalyzerListController { 4 | constructor($state, AnalyzerService, NotificationService) { 5 | 'ngInject'; 6 | 7 | this.$state = $state; 8 | this.AnalyzerService = AnalyzerService; 9 | this.NotificationService = NotificationService; 10 | } 11 | 12 | run(analyzer, dataType) { 13 | analyzer.active = true; 14 | this.AnalyzerService.openRunModal([analyzer], { 15 | dataType: dataType 16 | }) 17 | .then(responses => { 18 | this.$state.go('main.jobs'); 19 | 20 | responses.forEach(resp => { 21 | this.NotificationService.success( 22 | `${resp.data.analyzerName} started successfully on ${resp.data 23 | .data || resp.data.attachment.name}` 24 | ); 25 | }); 26 | }) 27 | .catch(err => { 28 | if (!_.isString(err)) { 29 | this.NotificationService.error( 30 | err.data.message || 31 | `An error occurred: ${err.statusText}` || 32 | 'An unexpected error occurred' 33 | ); 34 | } 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /www/src/app/pages/analyzers/components/analyzers.list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
No analyzers found.
5 |
6 |
7 | 8 |
9 |
10 |
11 |
12 |

13 | {{analyzer.name}} 14 | 15 | Version: {{analyzer.version}} 16 | 17 | Author: {{analyzer.author}} 18 | 19 | License: {{analyzer.license}} 20 |

21 |
22 | {{analyzer.description}} 23 |
24 |
25 | Applies to: 26 | {{type}}  27 |
28 |
29 |
30 | 31 | Run 32 |
33 |
34 |
35 |
36 |
-------------------------------------------------------------------------------- /www/src/app/pages/jobs/components/job.details.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class JobDetailsController { 4 | constructor() {} 5 | } 6 | -------------------------------------------------------------------------------- /www/src/app/pages/jobs/components/job.details.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | {{$ctrl.job.analyzerName}} 5 |
6 |
7 | 8 |
9 | Artifact 10 |

11 | [{{$ctrl.job.dataType | uppercase}}] 12 | {{(($ctrl.job.dataType === 'file') ? $ctrl.job.attachment.name: $ctrl.job.data) | fang}} 14 | {{$ctrl.job.label || 'No Label'}} 15 | 16 |

17 |

18 | [{{$ctrl.job.dataType | uppercase}}] 19 | {{(($ctrl.job.dataType === 'file') ? $ctrl.job.attachment.name : $ctrl.job.data) | fang}} 20 |

21 |
22 |
23 | 24 |
25 | Date 26 |

27 | 28 |

29 |
30 |
31 | 32 |
33 | TLP 34 |

35 | 36 |

37 |
38 |
39 | 40 |
41 | PAP 42 |

43 | 44 |

45 |
46 |
47 | 48 |
49 | Status 50 |

51 | {{$ctrl.job.status}} 53 |

54 |
55 |
56 | 57 |
58 | Report summary 59 |

60 | 61 |

62 |
63 | 64 |
65 |
-------------------------------------------------------------------------------- /www/src/app/pages/jobs/components/jobs.list.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash/core'; 4 | 5 | export default class JobsListController { 6 | constructor($log, JobService, NotificationService, ModalService) { 7 | 'ngInject'; 8 | 9 | this.$log = $log; 10 | this.JobService = JobService; 11 | this.NotificationService = NotificationService; 12 | this.ModalService = ModalService; 13 | } 14 | 15 | deleteJob(id) { 16 | let modalInstance = this.ModalService.confirm( 17 | 'Delete job', 18 | `Are your sure you want to delete this job?`, 19 | { 20 | flavor: 'danger', 21 | okText: 'Yes, delete job' 22 | } 23 | ); 24 | 25 | modalInstance.result 26 | .then(() => this.JobService.remove(id)) 27 | .then( 28 | /*response*/ 29 | () => { 30 | this.onDelete(); 31 | this.NotificationService.success('Job removed successfully'); 32 | } 33 | ) 34 | .catch(err => { 35 | if (!_.isString(err)) { 36 | this.NotificationService.error('Unable to delete the Job.'); 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /www/src/app/pages/jobs/components/jobs.list.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/app/pages/jobs/components/jobs.list.scss -------------------------------------------------------------------------------- /www/src/app/pages/jobs/job.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class JobController { 4 | constructor($log) { 5 | 'ngInject'; 6 | 7 | this.$log = $log; 8 | } 9 | 10 | $onInit() { 11 | this.protectDownloadsWith = this.main.config.config.protectDownloadsWith; 12 | this.hasFileArtifact = (this.job.report.artifacts || []).find(item => item.dataType === 'file'); 13 | } 14 | } -------------------------------------------------------------------------------- /www/src/app/pages/jobs/jobs.page.scss: -------------------------------------------------------------------------------- 1 | pre.error-trace { 2 | color: #a94442; 3 | border: none; 4 | font-size: 10px; 5 | word-wrap: break-word; 6 | word-break: break-all; 7 | white-space: pre-wrap; 8 | background-color: lighten($color: #a94442, $amount: 50%) 9 | } 10 | 11 | pre.responder-input { 12 | color: #367fa9; 13 | border: none; 14 | font-size: 10px; 15 | word-wrap: break-word; 16 | word-break: break-all; 17 | white-space: pre-wrap; 18 | // background-color: lighten($color: #367fa9, $amount: 50%) 19 | background-color: transparent; 20 | 21 | } -------------------------------------------------------------------------------- /www/src/app/pages/jobs/jobs.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class JobService { 4 | constructor($http) { 5 | 'ngInject'; 6 | 7 | this.$http = $http; 8 | } 9 | 10 | list(params) { 11 | return this.$http.post('./api/job/_search', params); 12 | } 13 | 14 | report(jobId) { 15 | return this.$http.get('./api/job/' + jobId + '/report'); 16 | } 17 | 18 | remove(jobId) { 19 | return this.$http.delete('./api/job/' + jobId); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /www/src/app/pages/login/login.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import LoginController from './login.controller'; 4 | import tpl from './login.page.html'; 5 | 6 | import './login.page.scss'; 7 | 8 | const loginPageModule = angular 9 | .module('login-module', ['ui.router']) 10 | .config($stateProvider => { 11 | 'ngInject'; 12 | 13 | $stateProvider.state('login', { 14 | url: '/login', 15 | component: 'loginPage', 16 | resolve: { 17 | config: ($q, VersionService) => VersionService.get() 18 | .then(response => $q.resolve(response.data)) 19 | }, 20 | params: { 21 | autoLogin: false 22 | } 23 | }); 24 | }) 25 | .component('loginPage', { 26 | controller: LoginController, 27 | templateUrl: tpl, 28 | bindings: { 29 | config: '<' 30 | } 31 | }); 32 | 33 | export default loginPageModule; -------------------------------------------------------------------------------- /www/src/app/pages/login/login.page.html: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /www/src/app/pages/login/login.page.scss: -------------------------------------------------------------------------------- 1 | .sso-login-box { 2 | margin-top: 20px; 3 | text-align: center; 4 | } -------------------------------------------------------------------------------- /www/src/app/pages/main/main.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class MainController { 4 | constructor($log, $state) { 5 | 'ngInject'; 6 | this.$log = $log; 7 | this.$state = $state; 8 | } 9 | 10 | $onInit() { 11 | if (this.currentUser === 520) { 12 | this.$state.go('maintenance'); 13 | return; 14 | } else if (!this.currentUser || !this.currentUser.id) { 15 | this.$state.go('login', { 16 | autoLogin: (this.config || {}).ssoAutoLogin 17 | }); 18 | return; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /www/src/app/pages/main/main.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //import MainComponent from './main.component'; 4 | 5 | import MainController from './main.controller'; 6 | import mainTpl from './main.page.html'; 7 | 8 | const mainPageModule = angular 9 | .module('main-module', ['ui.router']) 10 | .config(($stateProvider, $urlRouterProvider) => { 11 | 'ngInject'; 12 | 13 | $urlRouterProvider.otherwise('/index'); 14 | 15 | $stateProvider.state('index', { 16 | url: '/index' 17 | }); 18 | 19 | $stateProvider.state('main', { 20 | abstract: true, 21 | url: '/', 22 | component: 'mainPage', 23 | resolve: { 24 | currentUser: ($q, AuthService, NotificationService) => { 25 | 'ngInject'; 26 | 27 | let deferred = $q.defer(); 28 | 29 | AuthService.current() 30 | .then(userData => deferred.resolve(userData)) 31 | .catch(err => { 32 | NotificationService.handleError('Application', err.data, err.status); 33 | return deferred.reject(err) 34 | }); 35 | 36 | return deferred.promise; 37 | }, 38 | config: ($q, VersionService) => { 39 | let defer = $q.defer(); 40 | 41 | VersionService.get().then(response => { 42 | defer.resolve(response.data); 43 | }); 44 | 45 | return defer.promise; 46 | } 47 | } 48 | }); 49 | }) 50 | .component('mainPage', { 51 | controller: MainController, 52 | templateUrl: mainTpl, 53 | bindings: { 54 | currentUser: '<', 55 | config: '<' 56 | } 57 | }); 58 | 59 | export default mainPageModule; -------------------------------------------------------------------------------- /www/src/app/pages/main/main.page.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/src/app/pages/maintenance/maintenance.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import angular from 'angular'; 4 | import MaintenanceService from './maintenance.service'; 5 | import MaintenanceController from './maintenance.controller'; 6 | 7 | import maintenanceTpl from './maintenance.page.html'; 8 | 9 | const maintenanceModule = angular 10 | .module('thkit.maintenanceModule', []) 11 | .config($stateProvider => { 12 | 'ngInject'; 13 | 14 | $stateProvider.state('maintenance', { 15 | url: '/maintenance', 16 | component: 'maintenancePage' 17 | }); 18 | }) 19 | .component('maintenancePage', { 20 | controller: MaintenanceController, 21 | templateUrl: maintenanceTpl 22 | }) 23 | .provider('MaintenanceService', MaintenanceService); 24 | 25 | export default maintenanceModule; 26 | -------------------------------------------------------------------------------- /www/src/app/pages/maintenance/maintenance.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default function() { 4 | let successState = 'app'; 5 | 6 | this.setSuccessState = function(state) { 7 | successState = state; 8 | }; 9 | 10 | function MaintenanceService() { 11 | this.getSuccessState = function() { 12 | return successState; 13 | }; 14 | } 15 | 16 | this.$get = function() { 17 | return new MaintenanceService(); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /www/src/app/pages/responders/components/responders.list.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class ResponderListController { 4 | constructor($state, ResponderService, NotificationService) { 5 | 'ngInject'; 6 | 7 | this.$state = $state; 8 | this.ResponderService = ResponderService; 9 | this.NotificationService = NotificationService; 10 | } 11 | 12 | // run(analyzer, dataType) { 13 | // analyzer.active = true; 14 | // this.ResponderService.openRunModal([analyzer], { 15 | // dataType: dataType 16 | // }) 17 | // .then(responses => { 18 | // this.$state.go('main.jobs'); 19 | 20 | // responses.forEach(resp => { 21 | // this.NotificationService.success( 22 | // `${resp.data.analyzerName} started successfully on ${resp.data 23 | // .data || resp.data.attachment.name}` 24 | // ); 25 | // }); 26 | // }) 27 | // .catch(err => { 28 | // if (!_.isString(err)) { 29 | // this.NotificationService.error( 30 | // err.data.message || 31 | // `An error occurred: ${err.statusText}` || 32 | // 'An unexpected error occurred' 33 | // ); 34 | // } 35 | // }); 36 | // } 37 | } -------------------------------------------------------------------------------- /www/src/app/pages/responders/components/responders.list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
No responders found.
5 |
6 |
7 | 8 |
9 |
10 |
11 |
12 |

13 | {{responder.name}} 14 | 15 | Version: {{responder.version}} 16 | 17 | Author: {{responder.author}} 18 | 19 | License: {{responder.license}} 20 |

21 |
22 | {{responder.description}} 23 |
24 |
25 | Applies to: 26 | {{type}}  27 |
28 |
29 |
30 |
31 |
32 |
-------------------------------------------------------------------------------- /www/src/app/pages/responders/responders.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import RepondersController from './responders.controller'; 4 | import tpl from './responders.page.html'; 5 | 6 | import RespondersListController from './components/responders.list.controller'; 7 | import respondersListTpl from './components/responders.list.html'; 8 | 9 | import responderService from './responders.service.js'; 10 | 11 | //import './analyzers.page.scss'; 12 | 13 | const respondersModule = angular 14 | .module('responders-module', ['ui.router']) 15 | .config(($stateProvider, Roles) => { 16 | 'ngInject'; 17 | 18 | $stateProvider.state('main.responders', { 19 | url: 'responders', 20 | component: 'respondersPage', 21 | resolve: { 22 | datatypes: ($q, ResponderService) => { 23 | return ResponderService.list() 24 | .then(() => $q.resolve(ResponderService.getTypes())) 25 | .catch(err => $q.reject(err)); 26 | } 27 | }, 28 | data: { 29 | allow: [Roles.SUPERADMIN, Roles.ORGADMIN, Roles.ANALYZE] 30 | } 31 | }); 32 | }) 33 | .component('respondersPage', { 34 | controller: RepondersController, 35 | templateUrl: tpl, 36 | bindings: { 37 | datatypes: '<' 38 | } 39 | }) 40 | .component('respondersList', { 41 | controller: RespondersListController, 42 | templateUrl: respondersListTpl, 43 | bindings: { 44 | responders: '<' 45 | } 46 | }) 47 | .service('ResponderService', responderService); 48 | 49 | export default respondersModule; -------------------------------------------------------------------------------- /www/src/app/pages/settings/settings.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import SettingsPageController from './settings.page.controller'; 4 | import settingsPageTpl from './settings.page.html'; 5 | 6 | const settingsModule = angular 7 | .module('settings-module', ['images-resizer', 'naif.base64']) 8 | .config($stateProvider => { 9 | 'ngInject'; 10 | 11 | $stateProvider.state('main.settings', { 12 | url: 'settings', 13 | component: 'settingsPage' 14 | }); 15 | }) 16 | .component('settingsPage', { 17 | controller: SettingsPageController, 18 | templateUrl: settingsPageTpl, 19 | require: { 20 | main: '^mainPage' 21 | } 22 | }); 23 | 24 | export default settingsModule; 25 | -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-Black.otf -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-BlackIt.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-BlackIt.otf -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-Bold.otf -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-BoldIt.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-BoldIt.otf -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-ExtraLight.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-ExtraLight.otf -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-ExtraLightIt.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-ExtraLightIt.otf -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-It.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-It.otf -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-Light.otf -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-LightIt.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-LightIt.otf -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-Regular.otf -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-Semibold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-Semibold.otf -------------------------------------------------------------------------------- /www/src/assets/fonts/SourceSansPro-SemiboldIt.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/fonts/SourceSansPro-SemiboldIt.otf -------------------------------------------------------------------------------- /www/src/assets/images/angular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/images/angular.png -------------------------------------------------------------------------------- /www/src/assets/images/logo-dark.svg: -------------------------------------------------------------------------------- 1 | logo-dark -------------------------------------------------------------------------------- /www/src/assets/images/no-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/images/no-avatar.png -------------------------------------------------------------------------------- /www/src/assets/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/assets/images/yeoman.png -------------------------------------------------------------------------------- /www/src/assets/styles/sass/paginable-table.scss: -------------------------------------------------------------------------------- 1 | .paginable-page { 2 | .box-tools { 3 | >select { 4 | width: 100px; 5 | } 6 | } 7 | .table-toolbar { 8 | label.form-control { 9 | border: 0; 10 | width: auto; 11 | padding-left: 0; 12 | padding-right: 5px; 13 | } 14 | input.form-control { 15 | width: 300px; 16 | } 17 | select { 18 | &.form-control { 19 | width: 100px; 20 | } 21 | } 22 | multiselect { 23 | width: 150px; 24 | .dropdown-menu { 25 | width: 300px !important; 26 | } 27 | } 28 | .pagination { 29 | margin: 0; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /www/src/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/favicon-128x128.png -------------------------------------------------------------------------------- /www/src/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/favicon-16x16.png -------------------------------------------------------------------------------- /www/src/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/favicon-196x196.png -------------------------------------------------------------------------------- /www/src/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/favicon-32x32.png -------------------------------------------------------------------------------- /www/src/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/favicon-96x96.png -------------------------------------------------------------------------------- /www/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/Cortex/6ef0aa1e8b5eb6cc05f1974f00691b2d3a960799/www/src/favicon.ico -------------------------------------------------------------------------------- /www/src/tpl-index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cortex 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% for(var i=0; i < htmlWebpackPlugin.files.css; i++) { %> 15 | 16 | <% } %> 17 | 18 | <% for(var i=0; i < htmlWebpackPlugin.files.chunks; i++) { %> 19 | 20 | <% } %> 21 | 22 | 23 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /www/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let _ = require('lodash'); 4 | let _configs = { 5 | // global section 6 | global: require(__dirname + '/config/webpack/global'), 7 | 8 | // config by enviroments 9 | production: require(__dirname + '/config/webpack/environments/production'), 10 | development: require(__dirname + '/config/webpack/environments/development') 11 | }; 12 | 13 | let _load = function() { 14 | let ENV = process.env.NODE_ENV ? process.env.NODE_ENV : 'production'; 15 | 16 | //console.log('Current Environment: ', ENV); 17 | 18 | // load config file by environment 19 | return ( 20 | _configs && _.merge(_configs.global(__dirname), _configs[ENV](__dirname)) 21 | ); 22 | }; 23 | 24 | module.exports = _load(); 25 | --------------------------------------------------------------------------------