diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..d7bb38946 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "composer" + directory: "/app" + schedule: + interval: "daily" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every weekday + interval: "daily" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f2da0572a..f3568770f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3d04f8998..8ee02442a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,7 @@ name: misp # events but only for the 2.4 and develop branches on: push: - branches: [ 2.4, develop, misp-stix ] + branches: [ 2.4, develop, misp-stix, taxii ] pull_request: branches: [ 2.4, develop, misp-stix ] @@ -20,13 +20,13 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04] + os: [ubuntu-22.04] php: ['7.2', '7.3', '7.4'] # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: 'recursive' @@ -75,7 +75,8 @@ jobs: run: | sudo chown $USER:www-data $HOME/.composer pushd app - sudo -H -u $USER php composer.phar install --no-progress + sudo -H -u $USER composer config --no-plugins allow-plugins.composer/installers true + sudo -H -u $USER composer install --no-progress popd cp -fa INSTALL/setup/config.php app/Plugin/CakeResque/Config/config.php # Set perms @@ -138,6 +139,13 @@ jobs: sudo chown -R $USER:www-data `pwd`/app/Config sudo chmod -R 777 `pwd`/app/Config + # fix perms (?) + namei -m /home/runner/work + sudo chmod +x /home/runner/work + sudo chmod +x /home/runner + sudo chmod +x /home + sudo chmod +x / + - name: DB Update run: | sudo -E su $USER -c 'app/Console/cake Admin setSetting "MISP.osuser" $USER' @@ -192,6 +200,9 @@ jobs: - name: Turn MISP live run: sudo -E su $USER -c 'app/Console/cake Live 1' + - name: Check if Redis is ready + run: sudo -E su $USER -c 'app/Console/cake Admin redisReady' + - name: Start workers run: | sudo chmod +x app/Console/worker/start.sh @@ -221,12 +232,11 @@ jobs: echo 'key = "'${AUTH}'"' >> tests/keys.py cat tests/keys.py popd - - - name: Build test - run: | . ./venv/bin/activate pushd tests - ./build-test.sh + bash ./build-test.sh + popd + deactivate - name: Run PHP tests run: | @@ -240,17 +250,15 @@ jobs: popd sudo chmod -R g+ws `pwd`/app/tmp/logs - + . ./venv/bin/activate pushd PyMISP python tests/testlive_comprehensive.py + python tests/test_mispevent.py popd python tests/testlive_security.py -v python tests/testlive_sync.py python tests/testlive_comprehensive_local.py -v - pushd PyMISP - python tests/test_mispevent.py - popd cp PyMISP/tests/keys.py PyMISP/examples/events/ pushd PyMISP/examples/events/ python ./create_massive_dummy_events.py -l 5 -a 30 @@ -264,3 +272,5 @@ jobs: tail -n +1 `pwd`/app/tmp/logs/* tail -n +1 /var/log/apache2/*.log + sudo -u $USER app/Console/cake Log export /tmp/logs.json.gz --without-changes + zcat /tmp/logs.json.gz diff --git a/.gitignore b/.gitignore index 6ac789180..a7ad03a7e 100755 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,7 @@ app/Lib/EventWarning/Custom/* /app/tmp/cached_exports/sha256/* /app/tmp/cached_exports/bro/* /app/Plugin/CakeResque +/app/Plugin/DebugKit .gnupg .smime *.swp diff --git a/CODINGSTYLE.md b/CODINGSTYLE.md index 2c9f26679..29351d34d 100644 --- a/CODINGSTYLE.md +++ b/CODINGSTYLE.md @@ -6,11 +6,11 @@ Maintaining proper coding style is very important for any large software project - It allows others (as well as the future you!) to easily understand fragments of code and what they were supposed to do, and thus makes it easier to later extend them with newer functionality or bug fixes - It allows others to easily review the code and catch bugs - It provides for an aesthetically pleasing experience when one reads the code - + ## General typographic conventions - Maintain a maximum line length of 80 characters. Even though today’s monitors often are very wide and it’s often not a problem to have 120 characters displayed in an editor, maintaining shorter line lengths improves readability. It also allows others to have two parallel windows open, side by side, each with different parts of the source code. - Naming conventions: - - `ClassName`, + - `ClassName`, - `someVariable`, `someFunction`, `someArgument` - Maintain a decent amount of horizontal spacing, e.g. add a space after `if` or before `{` in PHP, Python, JavaScript, and similar in other languages. Whether and where to also use spaces within expressions, such as `(y*4+8)` vs. `(y * 4 + 8)` is left to the developer’s judgment. Do not put spaces immediately after or before the brackets in expressions, so avoid constructs like this: `if ( condition )` and use ones like this: `if (condition)` instead. - Use descriptive names for variables and functions. At a time when most editors have auto-completion features, there is no excuse for using short variable names. @@ -25,7 +25,7 @@ Maintaining proper coding style is very important for any large software project ## File naming conventions - Never use spaces within file names - **PHP:** Write file names in title case ,e.g. `AttachmentTool.php` -- **Python:** Write file names with small letters, use a dash to separate words, rather than underscores, e.g. `load_warninglists.py` +- **Python:** Write file names with small letters, use an underscore to separate words, rather than dashes, e.g. `load_warninglists.py` - **JavaScript:** Write file names with small letters, use dashes to separate words, rather than underscores, e.g. `bootstrap-colorpicker.js` ## General programming style guidelines @@ -41,7 +41,7 @@ Maintaining proper coding style is very important for any large software project return style; } - In production code, there should be little to no commented or disabled code fragments. Do not use comments to disable code fragments, unless you need to. But generally, there is little excuse to keep old, unused code fragments in the code. Instead, use the functionality provided by the source code management system, such as git. For example, create a special branch for storing the old, unused code – this way you will always be able to merge this code into upstream in the future. -- Try not to hardcode values in the code. +- Try not to hardcode values in the code. ## Commit message guidelines diff --git a/INSTALL/INSTALL.sh b/INSTALL/INSTALL.sh index ce2132009..4e01e2c20 100755 --- a/INSTALL/INSTALL.sh +++ b/INSTALL/INSTALL.sh @@ -1509,9 +1509,17 @@ coreCAKE () { ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "GnuPG.homedir" "${PATH_TO_MISP}/.gnupg" ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "GnuPG.password" "${GPG_PASSPHRASE}" ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "GnuPG.obscure_subject" true + ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "GnuPG.key_fetching_disabled" false # FIXME: what if we have not gpg binary but a gpg2 one? ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "GnuPG.binary" "$(which gpg)" + # LinOTP + ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "LinOTPAuth.enabled" false + ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "LinOTPAuth.baseUrl" "https://" + ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "LinOTPAuth.realm" "lino" + ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "LinOTPAuth.verifyssl" true + ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "LinOTPAuth.mixedauth" false + # Enable installer org and tune some configurables ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "MISP.host_org_id" 1 ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "MISP.email" "info@admin.test" @@ -1870,7 +1878,7 @@ mispmodules () { modulesCAKE () { # Enable Enrichment, set better timeouts ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "Plugin.Enrichment_services_enable" true - ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "Plugin.Enrichment_hover_enable" true + ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "Plugin.Enrichment_hover_enable" false ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "Plugin.Enrichment_hover_popover_only" false ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "Plugin.Enrichment_hover_timeout" 150 ${SUDO_WWW} ${RUN_PHP} -- ${CAKE} Admin setSetting "Plugin.Enrichment_timeout" 300 @@ -2543,7 +2551,7 @@ apacheConfig_RHEL7 () { #sudo sed -i "s/SetHandler/\#SetHandler/g" /etc/httpd/conf.d/misp.ssl.conf sudo rm /etc/httpd/conf.d/ssl.conf sudo chmod 644 /etc/httpd/conf.d/misp.ssl.conf - sudo sed -i '/Listen 80/a Listen 443' /etc/httpd/conf/httpd.conf + sudo sed -i '/Listen 443/!s/Listen 80/a Listen 443/' /etc/httpd/conf/httpd.conf # If a valid SSL certificate is not already created for the server, create a self-signed certificate: echo "The Common Name used below will be: ${OPENSSL_CN}" @@ -2591,7 +2599,7 @@ apacheConfig_RHEL8 () { #sudo sed -i "s/SetHandler/\#SetHandler/g" /etc/httpd/conf.d/misp.ssl.conf sudo rm /etc/httpd/conf.d/ssl.conf sudo chmod 644 /etc/httpd/conf.d/misp.ssl.conf - sudo sed -i '/Listen 80/a Listen 443' /etc/httpd/conf/httpd.conf + sudo sed -i '/Listen 443/!s/Listen 80/a Listen 443/' /etc/httpd/conf/httpd.conf # If a valid SSL certificate is not already created for the server, create a self-signed certificate: echo "The Common Name used below will be: ${OPENSSL_CN}" diff --git a/INSTALL/INSTALL.sh.sfv b/INSTALL/INSTALL.sh.sfv index 6f53f2312..1e88b1879 100644 --- a/INSTALL/INSTALL.sh.sfv +++ b/INSTALL/INSTALL.sh.sfv @@ -1,5 +1,5 @@ -; Generated by RHash v1.4.2 on 2022-05-23 at 12:45.34 +; Generated by RHash v1.4.2 on 2023-07-01 at 17:15.04 ; Written by Kravchenko Aleksey (Akademgorodok) - http://rhash.sf.net/ ; -; 160126 12:45.34 2022-05-23 INSTALL.sh -INSTALL.sh 4296D40B11B3002DF3FDFD69A508ED5ECACB8C13 D32E5A4B0F37F4C937CD4F85927E998D917BCBE89E4E0E864FFD7EA09E29ADEF BD093D8018C351E3D3722646E269C4B60E6DA19F42150338CE6FD72FEE293B8B89AA69D48A84B19D3EFDDAE25EC9E646 ECACC3071E130058C3DDECC86E1CBF27DD4F11389D10F43B14293B1915F7A24F02D0DA51E299706A38C00F2D2A7505B0FE46E33B705E53594383CE65461F2B08 +; 160686 17:15.04 2023-07-01 INSTALL.sh +INSTALL.sh 9576C31EC5BD942E1C9B12413E6408E4623252F7 78B708FE1FC6B39BE081B9F05C6AA5E1478F8762CAF5A8A7671A12EBA4D3C1C5 27991471FB5788F42AF3BBF86FC80A95341AA17AE9487016EEC94961A48437172702EB8E2D6CB300387E87D9E8E0E3E5 C1C21FD491AEFD662C87C3EF62837D769E63E9CF2446B9BD607CCEF8AFD72528824A8F408C6892FD51109390104010EF90DA7F4828950A8671D2986A6B8E216F diff --git a/INSTALL/INSTALL.sh.sha1 b/INSTALL/INSTALL.sh.sha1 index 90e04e1ef..e1db6c05b 100644 --- a/INSTALL/INSTALL.sh.sha1 +++ b/INSTALL/INSTALL.sh.sha1 @@ -1 +1 @@ -4296d40b11b3002df3fdfd69a508ed5ecacb8c13 INSTALL.sh +9576c31ec5bd942e1c9b12413e6408e4623252f7 INSTALL.sh diff --git a/INSTALL/INSTALL.sh.sha256 b/INSTALL/INSTALL.sh.sha256 index 80d0ca800..6622f0558 100644 --- a/INSTALL/INSTALL.sh.sha256 +++ b/INSTALL/INSTALL.sh.sha256 @@ -1 +1 @@ -d32e5a4b0f37f4c937cd4f85927e998d917bcbe89e4e0e864ffd7ea09e29adef INSTALL.sh +78b708fe1fc6b39be081b9f05c6aa5e1478f8762caf5a8a7671a12eba4d3c1c5 INSTALL.sh diff --git a/INSTALL/INSTALL.sh.sha384 b/INSTALL/INSTALL.sh.sha384 index 58d22834f..0f9ebbe68 100644 --- a/INSTALL/INSTALL.sh.sha384 +++ b/INSTALL/INSTALL.sh.sha384 @@ -1 +1 @@ -bd093d8018c351e3d3722646e269c4b60e6da19f42150338ce6fd72fee293b8b89aa69d48a84b19d3efddae25ec9e646 INSTALL.sh +27991471fb5788f42af3bbf86fc80a95341aa17ae9487016eec94961a48437172702eb8e2d6cb300387e87d9e8e0e3e5 INSTALL.sh diff --git a/INSTALL/INSTALL.sh.sha512 b/INSTALL/INSTALL.sh.sha512 index e83897162..fa8fc6529 100644 --- a/INSTALL/INSTALL.sh.sha512 +++ b/INSTALL/INSTALL.sh.sha512 @@ -1 +1 @@ -ecacc3071e130058c3ddecc86e1cbf27dd4f11389d10f43b14293b1915f7a24f02d0da51e299706a38c00f2d2a7505b0fe46e33b705e53594383ce65461f2b08 INSTALL.sh +c1c21fd491aefd662c87c3ef62837d769e63e9cf2446b9bd607ccef8afd72528824a8f408c6892fd51109390104010ef90da7f4828950a8671d2986a6b8e216f INSTALL.sh diff --git a/INSTALL/MYSQL.sql b/INSTALL/MYSQL.sql index 44da990b4..150d9aaa5 100644 --- a/INSTALL/MYSQL.sql +++ b/INSTALL/MYSQL.sql @@ -88,6 +88,12 @@ CREATE TABLE IF NOT EXISTS `attribute_tags` ( INDEX `tag_id` (`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- ------------------------------------------------------- + +-- +-- Table structure for table `auth_keys` +-- + CREATE TABLE IF NOT EXISTS `auth_keys` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `uuid` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, @@ -98,6 +104,8 @@ CREATE TABLE IF NOT EXISTS `auth_keys` ( `expiration` int(10) unsigned NOT NULL, `user_id` int(10) unsigned NOT NULL, `comment` text COLLATE utf8mb4_unicode_ci, + `allowed_ips` text COLLATE utf8mb4_unicode_ci, + `unique_ips` text COLLATE utf8mb4_unicode_ci, PRIMARY KEY (`id`), KEY `authkey_start` (`authkey_start`), KEY `authkey_end` (`authkey_end`), @@ -1523,16 +1531,16 @@ INSERT IGNORE INTO `roles` (`id`, `name`, `created`, `modified`, `perm_add`, `pe VALUES (2, 'Org Admin', NOW(), NOW(), 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0); INSERT IGNORE INTO `roles` (`id`, `name`, `created`, `modified`, `perm_add`, `perm_modify`, `perm_modify_org`, `perm_publish`, `perm_publish_zmq`, `perm_publish_kafka`, `perm_sync`, `perm_admin`, `perm_audit`, `perm_full`, `perm_auth`, `perm_regexp_access`, `perm_tagger`, `perm_site_admin`, `perm_template`, `perm_sharing_group`, `perm_tag_editor`, `perm_delegate`, `perm_sighting`, `perm_object_template`, `perm_decaying`, `default_role`) -VALUES (3, 'User', NOW(), NOW(), 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1); +VALUES (3, 'User', NOW(), NOW(), 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1); INSERT IGNORE INTO `roles` (`id`, `name`, `created`, `modified`, `perm_add`, `perm_modify`, `perm_modify_org`, `perm_publish`, `perm_publish_zmq`, `perm_publish_kafka`, `perm_sync`, `perm_admin`, `perm_audit`, `perm_full`, `perm_auth`, `perm_regexp_access`, `perm_tagger`, `perm_site_admin`, `perm_template`, `perm_sharing_group`, `perm_tag_editor`, `perm_delegate`, `perm_sighting`, `perm_object_template`, `perm_decaying`, `default_role`) -VALUES (4, 'Publisher', NOW(), NOW(), 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0); +VALUES (4, 'Publisher', NOW(), NOW(), 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0); INSERT IGNORE INTO `roles` (`id`, `name`, `created`, `modified`, `perm_add`, `perm_modify`, `perm_modify_org`, `perm_publish`, `perm_publish_zmq`, `perm_publish_kafka`, `perm_sync`, `perm_admin`, `perm_audit`, `perm_full`, `perm_auth`, `perm_regexp_access`, `perm_tagger`, `perm_site_admin`, `perm_template`, `perm_sharing_group`, `perm_tag_editor`, `perm_delegate`, `perm_sighting`, `perm_object_template`, `perm_decaying`, `default_role`) -VALUES (5, 'Sync user', NOW(), NOW(), 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0); +VALUES (5, 'Sync user', NOW(), NOW(), 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0); INSERT IGNORE INTO `roles` (`id`, `name`, `created`, `modified`, `perm_add`, `perm_modify`, `perm_modify_org`, `perm_publish`, `perm_publish_zmq`, `perm_publish_kafka`, `perm_sync`, `perm_admin`, `perm_audit`, `perm_full`, `perm_auth`, `perm_regexp_access`, `perm_tagger`, `perm_site_admin`, `perm_template`, `perm_sharing_group`, `perm_tag_editor`, `perm_delegate`, `perm_sighting`, `perm_object_template`, `perm_decaying`, `default_role`) -VALUES (6, 'Read Only', NOW(), NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); +VALUES (6, 'Read Only', NOW(), NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); -- -------------------------------------------------------- @@ -1661,4 +1669,6 @@ INSERT IGNORE INTO `org_blocklists` (`org_uuid`, `created`, `org_name`, `comment ('58d38339-7b24-4386-b4b4-4c0f950d210f', NOW(), 'Setec Astrononomy', 'default example'), ('58d38326-eda8-443a-9fa8-4e12950d210f', NOW(), 'Acme Finance', 'default example'); -INSERT IGNORE INTO `admin_settings` (`setting`, `value`) VALUES ('fix_login', NOW()); +INSERT IGNORE INTO `admin_settings` (`setting`, `value`) VALUES +('fix_login', NOW()), +('default_role', 3); \ No newline at end of file diff --git a/INSTALL/old/INSTALL.ubuntu1604.txt b/INSTALL/old/INSTALL.ubuntu1604.txt deleted file mode 120000 index 19f97ccc3..000000000 --- a/INSTALL/old/INSTALL.ubuntu1604.txt +++ /dev/null @@ -1 +0,0 @@ -../../docs/archive/INSTALL.ubuntu1604.md \ No newline at end of file diff --git a/PyMISP b/PyMISP index 98bb5ebd4..94983c01e 160000 --- a/PyMISP +++ b/PyMISP @@ -1 +1 @@ -Subproject commit 98bb5ebd49cf1ab3eb725922c5bbbc6369657b05 +Subproject commit 94983c01ecced6086df28133a38a297111534142 diff --git a/README.md b/README.md index 8e6dc245a..2a28375fb 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The objective of MISP is to foster the sharing of structured information within MISP, Malware Information Sharing Platform and Threat Sharing, core functionalities are: -- An **efficient IOC and indicators** database allowing to store technical and non-technical information about malware samples, incidents, attackers and intelligence. +- An **efficient IOC and indicators** database, allowing to store technical and non-technical information about malware samples, incidents, attackers and intelligence. - Automatic **correlation** finding relationships between attributes and indicators from malware, attack campaigns or analysis. The correlation engine includes correlation between attributes and more advanced correlations like Fuzzy hashing correlation (e.g. ssdeep) or CIDR block matching. Correlation can also be enabled or event disabled per attribute. - A **flexible data model** where complex [objects](https://www.misp-project.org/objects.html) can be expressed and **linked together to express threat intelligence, incidents or connected elements**. - Built-in **sharing functionality** to ease data sharing using different model of distributions. MISP can automatically synchronize events and attributes among different MISP instances. Advanced filtering functionalities can be used to meet each organization's sharing policy including a **flexible sharing group** capacity and an attribute level distribution mechanisms. @@ -50,9 +50,9 @@ MISP, Malware Information Sharing Platform and Threat Sharing, core functionalit - **export**: generating IDS, OpenIOC, plain text, CSV, MISP XML or JSON output to integrate with other systems (network IDS, host IDS, custom tools), Cache format (used for forensic tools), STIX (XML and JSON) 1 and 2, NIDS export (Suricata, Snort and Bro/Zeek) or RPZ zone. Many other formats can be easily added via the [misp-modules](https://github.com/MISP/misp-modules). - **import**: bulk-import, batch-import, import from OpenIOC, GFI sandbox, ThreatConnect CSV, MISP standard format or STIX 1.1/2.0. Many other formats easily added via the [misp-modules](https://github.com/MISP/misp-modules). - Flexible **free text import** tool to ease the integration of unstructured reports into MISP. -- A gentle system to **collaborate** on events and attributes allowing MISP users to propose changes or updates to attributes/indicators. +- A user-friendly system to **collaborate** on events and attributes allowing MISP users to propose changes or updates to attributes/indicators. - **data-sharing**: automatically exchange and synchronize with other parties and trust-groups using MISP. -- **delegating of sharing**: allows a simple pseudo-anonymous mechanism to delegate publication of event/indicators to another organization. +- **delegating of sharing**: allows for a simple, pseudo-anonymous mechanism to delegate publication of event/indicators to another organization. - Flexible **API** to integrate MISP with your own solutions. MISP is bundled with [PyMISP](https://github.com/MISP/PyMISP) which is a flexible Python Library to fetch, add or update events attributes, handle malware samples or search for attributes. An exhaustive restSearch API to easily search for indicators in MISP and exports those in all the format supported by MISP. - **Adjustable taxonomy** to classify and tag events following your own classification schemes or [existing classification](https://github.com/MISP/misp-taxonomies). The taxonomy can be local to your MISP but also shareable among MISP instances. - **Intelligence vocabularies** called MISP galaxy and bundled with existing [threat actors, malware, RAT, ransomware or MITRE ATT&CK](https://www.misp-project.org/galaxy.html) which can be easily linked with events and attributes in MISP. @@ -108,16 +108,16 @@ License This software is licensed under [GNU Affero General Public License version 3](http://www.gnu.org/licenses/agpl-3.0.html) -* Copyright (C) 2012-2022 Christophe Vandeplas +* Copyright (C) 2012-2023 Christophe Vandeplas * Copyright (C) 2012 Belgian Defence * Copyright (C) 2012 NATO / NCIRC -* Copyright (C) 2013-2022 Andras Iklody -* Copyright (C) 2015-2022 CIRCL - Computer Incident Response Center Luxembourg +* Copyright (C) 2013-2023 Andras Iklody +* Copyright (C) 2015-2023 CIRCL - Computer Incident Response Center Luxembourg * Copyright (C) 2016 Andreas Ziegler -* Copyright (C) 2018-2022 Sami Mokaddem -* Copyright (C) 2018-2022 Christian Studer -* Copyright (C) 2015-2022 Alexandre Dulaunoy +* Copyright (C) 2018-2023 Sami Mokaddem +* Copyright (C) 2018-2023 Christian Studer +* Copyright (C) 2015-2023 Alexandre Dulaunoy * Copyright (C) 2018-2022 Steve Clement -* Copyright (C) 2020-2022 Jakub Onderka +* Copyright (C) 2020-2023 Jakub Onderka For more information, [the list of authors and contributors](AUTHORS) is available. diff --git a/VERSION.json b/VERSION.json index b534235d8..1f1c3a0c9 100644 --- a/VERSION.json +++ b/VERSION.json @@ -1 +1 @@ -{"major":2, "minor":4, "hotfix":164} +{"major":2, "minor":4, "hotfix":174} diff --git a/app/.htaccess b/app/.htaccess index fc3aac4b2..ec36c63bd 100644 --- a/app/.htaccess +++ b/app/.htaccess @@ -1,5 +1,5 @@ RewriteEngine on - RewriteRule ^$ webroot/ [L] - RewriteRule (.*) webroot/$1 [L] - \ No newline at end of file + RewriteRule ^$ webroot/ "[B= ,L]" + RewriteRule (.*) webroot/$1 "[B= ,L]" + diff --git a/app/Config/config.default.php b/app/Config/config.default.php index abe2c3c1e..5e08ff20f 100644 --- a/app/Config/config.default.php +++ b/app/Config/config.default.php @@ -213,12 +213,13 @@ $config = array( // Warning: The following is a 3rd party contribution and still untested (including security) by the MISP-project team. // Feel free to enable it and report back to us if you run into any issues. // - // Uncomment the following to enable Kerberos authentication + // Uncomment the following to enable Kerberos/LDAP authentication // needs PHP LDAP support enabled (e.g. compile flag --with-ldap or Debian package php5-ldap) /* - 'ApacheSecureAuth' => array( // Configuration for kerberos authentication + 'ApacheSecureAuth' => array( // Configuration for kerberos/LDAP authentication 'apacheEnv' => 'REMOTE_USER', // If proxy variable = HTTP_REMOTE_USER, If BasicAuth ldap = PHP_AUTH_USER - 'ldapServer' => 'ldap://example.com', // FQDN or IP + 'ldapServer' => 'ldap://example.com', // FQDN or IP, ldap:// for LDAP or LDAP+STARTTLS, ldaps:// for LDAPS + 'starttls' => true, // true for STARTTLS, ignored for LDAPS 'ldapProtocol' => 3, 'ldapNetworkTimeout' => -1, // use -1 for unlimited network timeout 'ldapReaderUser' => 'cn=userWithReadAccess,ou=users,dc=example,dc=com', // DN ou RDN LDAP with reader user right diff --git a/app/Config/routes.php b/app/Config/routes.php index 673c57177..032267db2 100644 --- a/app/Config/routes.php +++ b/app/Config/routes.php @@ -33,6 +33,7 @@ Router::connect('/roles/admin_index/*', array('controller' => 'roles', 'action' => 'index', 'admin' => true)); Router::connect('/logs/admin_search/*', array('controller' => 'logs', 'action' => 'search', 'admin' => true)); Router::connect('/audit_logs/admin_index/*', array('controller' => 'audit_logs', 'action' => 'index', 'admin' => true)); + Router::connect('/access_logs/admin_index/*', array('controller' => 'access_logs', 'action' => 'index', 'admin' => true)); Router::connect('/logs/admin_index/*', array('controller' => 'logs', 'action' => 'index', 'admin' => true)); Router::connect('/regexp/admin_index/*', array('controller' => 'regexp', 'action' => 'index', 'admin' => true)); diff --git a/app/Console/Command/AdminShell.php b/app/Console/Command/AdminShell.php index 0352c2e46..c7d842f1c 100644 --- a/app/Console/Command/AdminShell.php +++ b/app/Console/Command/AdminShell.php @@ -112,6 +112,18 @@ class AdminShell extends AppShell return $parser; } + public function jobForgot() + { + if (empty($this->args[0])) { + die('Usage: ' . $this->Server->command_line_functions['console_admin_tasks']['data']['Forgot'] . PHP_EOL); + } + + $email = $this->args[0]; + $ip = empty($this->args[1]) ? null : $this->args[1]; + $jobId = empty($this->args[2]) ? null : $this->args[2]; + $this->User->forgot($email, $ip, $jobId); + } + public function jobGenerateCorrelation() { if (empty($this->args[0])) { @@ -524,6 +536,7 @@ class AdminShell extends AppShell $this->out('Executing all updates to bring the database up to date with the current version.'); $processId = empty($this->args[0]) ? false : $this->args[0]; $this->Server->runUpdates(true, false, $processId); + $this->Server->cleanCacheFiles(); $this->out('All updates completed.'); } else { $this->error('This OS user is not allowed to run this command.', 'Run it under `www-data` or `httpd` or `apache` or `wwwrun` or set MISP.osuser in the configuration.' . PHP_EOL . 'You tried to run this command as: ' . $whoami); @@ -554,8 +567,21 @@ class AdminShell extends AppShell public function redisReady() { try { - RedisTool::init()->ping(); - $this->out('Successfully connected to Redis.'); + $redis = RedisTool::init(); + for ($i = 0; $i < 10; $i++) { + $persistence = $redis->info('persistence'); + if (isset($persistence['loading']) && $persistence['loading']) { + $this->out('Redis is still loading...'); + sleep(1); + } else { + break; + } + } + if ($i === 9) { + $this->out('Redis is still loading, but we will continue.'); + } else { + $this->out('Successfully connected to Redis.'); + } } catch (Exception $e) { $this->error('Redis connection is not available', $e->getMessage()); } @@ -845,7 +871,7 @@ class AdminShell extends AppShell ]); foreach ($tables as $table) { - $dataSource->query('OPTIMISE TABLE ' . $dataSource->name($table)); + $dataSource->query('OPTIMIZE TABLE ' . $dataSource->name($table)); $progress->increment(); $progress->draw(); } diff --git a/app/Console/Command/AppShell.php b/app/Console/Command/AppShell.php index d825ea282..77ba8e761 100644 --- a/app/Console/Command/AppShell.php +++ b/app/Console/Command/AppShell.php @@ -89,7 +89,11 @@ abstract class AppShell extends Shell protected function getBackgroundJobsTool() { if (!isset($this->BackgroundJobsTool)) { - $this->BackgroundJobsTool = new BackgroundJobsTool(Configure::read('SimpleBackgroundJobs')); + $settings = ['enabled' => false]; + if (!empty(Configure::read('SimpleBackgroundJobs.enabled'))) { + $settings = Configure::read('SimpleBackgroundJobs'); + } + $this->BackgroundJobsTool = new BackgroundJobsTool($settings); } return $this->BackgroundJobsTool; } diff --git a/app/Console/Command/EventShell.php b/app/Console/Command/EventShell.php index 00ae7253f..9fb01fd45 100644 --- a/app/Console/Command/EventShell.php +++ b/app/Console/Command/EventShell.php @@ -68,7 +68,7 @@ class EventShell extends AppShell $user = $this->getUser($userId); if (!file_exists($path)) { - $this->error("File '$path' does not exists."); + $this->error("File '$path' does not exist."); } if (!is_readable($path)) { $this->error("File '$path' is not readable."); @@ -405,7 +405,17 @@ class EventShell extends AppShell $jobId = $this->args[2]; $userId = $this->args[3]; $user = $this->getUser($userId); - $job = $this->Job->read(null, $jobId); + $job = $this->Job->find('first', [ + 'recursive' => -1, + 'conditions' => [ + 'Job.id' => $jobId + ] + ]); + if (empty($job)) { + $log = ClassRegistry::init('Log'); + $log->createLogEntry($user, 'publish', 'Event', $id, 'Event (' . $id . '): could not be published - valid job not found.', ''); + return true; + } $this->Event->Behaviors->unload('SysLogLogable.SysLogLogable'); $result = $this->Event->publish($id, $passAlong); $job['Job']['progress'] = 100; @@ -632,7 +642,7 @@ class EventShell extends AppShell { $user = $this->User->getAuthUser($userId, true); if (empty($user)) { - $this->error("User with ID $userId does not exists."); + $this->error("User with ID $userId does not exist."); } Configure::write('CurrentUserId', $user['id']); // for audit logging purposes return $user; diff --git a/app/Console/Command/LogShell.php b/app/Console/Command/LogShell.php index 79d1c246d..d4bf5f239 100644 --- a/app/Console/Command/LogShell.php +++ b/app/Console/Command/LogShell.php @@ -3,26 +3,44 @@ /** * @property Log $Log * @property AuditLog $AuditLog + * @property AccessLog $AccessLog * @property Server $Server */ class LogShell extends AppShell { - public $uses = ['Log', 'AuditLog', 'Server']; + public $uses = ['Log', 'AuditLog', 'AccessLog', 'Server']; public function getOptionParser() { $parser = parent::getOptionParser(); $parser->addSubcommand('auditStatistics', [ - 'help' => __('Show statistics from audit logs.'), + 'help' => __('Show statistics for audit logs.'), + ]); + $parser->addSubcommand('accessStatistics', [ + 'help' => __('Show statistics for access logs.'), ]); $parser->addSubcommand('statistics', [ - 'help' => __('Show statistics from logs.'), + 'help' => __('Show statistics for application logs.'), ]); $parser->addSubcommand('export', [ - 'help' => __('Export logs to compressed file in JSON Lines format (one JSON encoded line per entry).'), + 'help' => __('Export application logs to compressed file in JSON Lines format (one JSON encoded line per entry).'), + 'parser' => [ + 'arguments' => [ + 'file' => ['help' => __('Path to output file'), 'required' => true], + ], + 'options' => [ + 'without-changes' => ['boolean' => true, 'help' => __('Do not include add, edit or delete actions.')], + ], + ], + ]); + $parser->addSubcommand('recompress', [ + 'help' => __('Recompress compressed data in logs.'), + ]); + $parser->addSubcommand('accessLogRetention', [ + 'help' => __('Delete logs that are older than specified duration.'), 'parser' => array( 'arguments' => array( - 'file' => ['help' => __('Path to output file'), 'required' => true], + 'duration' => ['help' => __('Duration in days'), 'required' => true], ), ), ]); @@ -32,6 +50,7 @@ class LogShell extends AppShell public function export() { list($path) = $this->args; + $withoutChanges = $this->param('without-changes'); if (file_exists($path)) { $this->error("File $path already exists"); @@ -42,21 +61,24 @@ class LogShell extends AppShell $this->error("Could not open $path for writing"); } - $rows = $this->Log->query("SELECT TABLE_ROWS FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'logs';"); /** @var ProgressShellHelper $progress */ $progress = $this->helper('progress'); $progress->init([ - 'total' => $rows[0]['TABLES']['TABLE_ROWS'], // just estimate, but fast + 'total' => $this->Log->tableRows(), // just estimate, but fast 'width' => 50, ]); $lastId = 0; while (true) { + $conditions = ['Log.id >' => $lastId]; // much faster than offset + if ($withoutChanges) { + $conditions['NOT'] = ['Log.action' => ['add', 'edit', 'delete']]; + } $logs = $this->Log->find('all', [ - 'conditions' => ['id >' => $lastId], // much faster than offset + 'conditions' => $conditions, 'recursive' => -1, 'limit' => 100000, - 'order' => ['id ASC'], + 'order' => ['Log.id ASC'], ]); if (empty($logs)) { break; @@ -77,7 +99,7 @@ class LogShell extends AppShell if ($log['id'] > $lastId) { $lastId = $log['id']; } - $lines .= json_encode($log, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n"; + $lines .= JsonTool::encode($log) . "\n"; } if (gzwrite($file, $lines) === false) { $this->error("Could not write data to $path"); @@ -144,7 +166,48 @@ class LogShell extends AppShell $this->out('Change field:'); $this->out('-------------'); $this->out(str_pad(__('Compressed items:'), 20) . $this->AuditLog->compressionStats['compressed']); + $this->out(str_pad(__('Total size:'), 20) . CakeNumber::toReadableSize($this->AuditLog->compressionStats['bytes_total'])); $this->out(str_pad(__('Uncompressed size:'), 20) . CakeNumber::toReadableSize($this->AuditLog->compressionStats['bytes_uncompressed'])); $this->out(str_pad(__('Compressed size:'), 20) . CakeNumber::toReadableSize($this->AuditLog->compressionStats['bytes_compressed'])); } + + public function accessStatistics() + { + $count = $this->AccessLog->find('count'); + $first = $this->AccessLog->find('first', [ + 'recursive' => -1, + 'fields' => ['created'], + 'order' => ['id ASC'], + ]); + $last = $this->AccessLog->find('first', [ + 'recursive' => -1, + 'fields' => ['created'], + 'order' => ['id DESC'], + ]); + + $this->out(str_pad(__('Count:'), 20) . $count); + $this->out(str_pad(__('First:'), 20) . $first['AccessLog']['created']); + $this->out(str_pad(__('Last:'), 20) . $last['AccessLog']['created']); + + $usage = $this->Server->dbSpaceUsage()['access_logs']; + $this->out(str_pad(__('Data size:'), 20) . CakeNumber::toReadableSize($usage['data_in_bytes'])); + $this->out(str_pad(__('Index size:'), 20) . CakeNumber::toReadableSize($usage['index_in_bytes'])); + $this->out(str_pad(__('Reclaimable size:'), 20) . CakeNumber::toReadableSize($usage['reclaimable_in_bytes']), 2); + } + + public function recompress() + { + $this->AuditLog->recompress(); + } + + public function accessLogRetention() + { + list($duration) = $this->args; + if ($duration <= 0 || !is_numeric($duration)) { + $this->error("Invalid duration specified."); + } + $duration = new DateTime("-$duration days"); + $deleted = $this->AccessLog->deleteOldLogs($duration); + $this->out(__n("Deleted %s entry", "Deleted %s entries", $deleted, $deleted)); + } } diff --git a/app/Console/Command/Ls22Shell.php b/app/Console/Command/Ls22Shell.php index e5791abcb..fd477411b 100644 --- a/app/Console/Command/Ls22Shell.php +++ b/app/Console/Command/Ls22Shell.php @@ -29,6 +29,8 @@ class Ls22Shell extends AppShell public function getOptionParser() { + $this->stdout->styles('green', array('text' => 'green')); + $parser = parent::getOptionParser(); $parser->addSubcommand('enableTaxonomy', [ 'help' => __('Enable a taxonomy with all its tags.'), @@ -101,6 +103,33 @@ class Ls22Shell extends AppShell ), ), ]); + $parser->addSubcommand('setSetting', [ + 'help' => __('Set a setting on the given MISP instance(s).'), + 'parser' => array( + 'options' => array( + 'instances' => [ + 'help' => 'Path to the instance file, by default "instances.csv" from the local directory', + 'short' => 'i', + 'required' => true + ], + 'misp_url_filter' => [ + 'help' => 'The url of the instance to execute changes on. If not set, all are updated.', + 'short' => 'm', + 'required' => false + ], + 'setting' => [ + 'help' => 'The setting to modify', + 'short' => 's', + 'required' => true + ], + 'value' => [ + 'help' => 'The value to set for the given setting', + 'short' => 'v', + 'required' => true + ] + ), + ), + ]); $parser->addSubcommand('addWarninglist', [ 'help' => __('Inject warninglist'), 'parser' => array( @@ -363,6 +392,26 @@ class Ls22Shell extends AppShell } } + public function setSetting() + { + $setting = $this->param('setting'); + $value = $this->param('value'); + $this->__getInstances($this->param('instances')); + foreach ($this->__servers as $server) { + $HttpSocket = $this->Server->setupHttpSocket($server, null); + $request = $this->Server->setupSyncRequest($server, 'Server'); + $payload = ["value" => $value]; + $response = $HttpSocket->post($server['Server']['url'] . '/server/serverSettingsEdit/' . $setting, json_encode($value), $request); + $statusWrapped = sprintf( + '<%s>%s', + $response->isOk() ? 'info' : 'error', + $response->isOk() ? 'OK' : 'Setting updated', + $response->isOk() ? 'info' : 'error' + ); + $this->out($server['Server']['url'] . ': ' . $statusWrapped, 1, Shell::NORMAL); + } + } + public function scores() { $results = []; @@ -379,7 +428,7 @@ class Ls22Shell extends AppShell } $HttpSocket = $this->Server->setupHttpSocket($server, null); $request = $this->Server->setupSyncRequest($server); - $response = $HttpSocket->get($server['Server']['url'] . '/organisations/index/scope:all', false, $request); + $response = $HttpSocket->get($server['Server']['url'] . '/organisations/index/scope:local', false, $request); $orgs = json_decode($response->body(), true); $this->out(__('Organisations fetched. %d found.', count($orgs)), 1, Shell::VERBOSE); $org_mapping = []; @@ -390,21 +439,31 @@ class Ls22Shell extends AppShell if ($org['Organisation']['name'] === 'YT') { continue; } + if ($org['Organisation']['name'] === 'ORGNAME') { + continue; + } $org_mapping[$org['Organisation']['name']] = $org['Organisation']['id']; } - if (!empty($this->param['from'])) { - $time_range[] = $this->param['from']; + $time_range = []; + if (!empty($this->param('from'))) { + $time_range[] = $this->param('from'); } - if (!empty($this->param['to'])) { + if (!empty($this->param('to'))) { if (empty($time_range)) { $time_range[] = '365d'; } - $time_range[] = $this->param['to']; + $time_range[] = $this->param('to'); + } else { + if (!empty($time_range)) { + $time_range[] = '0h'; + } } + $event_extended_uuids = []; + $event_uuid_per_org = []; foreach ($org_mapping as $org_name => $org_id) { - $time_range = []; $params = [ - 'org' => $org_id + 'org' => $org_id, + 'includeWarninglistHits' => true, ]; if (!empty($time_range)) { $params['publish_timestamp'] = $time_range; @@ -415,6 +474,7 @@ class Ls22Shell extends AppShell $results[$org_name] = [ 'attribute_count' => 0, 'object_count' => 0, + 'event_count' => count($events['response']), 'connected_elements' => 0, 'event_tags' => 0, 'attribute_tags' => 0, @@ -423,9 +483,16 @@ class Ls22Shell extends AppShell 'attribute_attack' => 0, 'attribute_other' => 0, 'score' => 0, - 'warnings' => 0 + 'warnings' => 0, + 'events_extended' => 0, + 'extending_events' => 0, ]; foreach ($events['response'] as $event) { + $event_uuid_per_org[$event['Event']['uuid']] = $event['Event']['Orgc']['name']; + if (!empty($event['Event']['extends_uuid'])) { + $event_extended_uuids[$event['Event']['Orgc']['name']][] = $event['Event']['extends_uuid']; + } + if (!empty($event['Event']['Tag'])) { foreach ($event['Event']['Tag'] as $tag) { if (substr($tag['name'], 0, 32) === 'misp-galaxy:mitre-attack-pattern') { @@ -458,7 +525,7 @@ class Ls22Shell extends AppShell } } if (!empty($attribute['warnings'])) { - $result[$org_name]['warnings'] += 1; + $results[$org_name]['warnings'] += 1; } } $results[$org_name]['attribute_count'] += count($event['Event']['Attribute']); @@ -485,6 +552,22 @@ class Ls22Shell extends AppShell } } + + foreach ($event_extended_uuids as $orgc => $uuids) { + foreach ($uuids as $uuid) { + if (!empty($event_uuid_per_org[$uuid])) { + $org_name = $event_uuid_per_org[$uuid]; + if ($orgc != $org_name) { + // Add point for org extending another event + $results[$orgc]['extending_events'] += 1; + // Add point for org getting their event extended + $results[$org_name]['events_extended'] += 1; + } + } + } + } + + $scores = []; foreach ($results as $k => $result) { $totalCount = $result['attribute_count'] + $result['object_count']; @@ -499,8 +582,10 @@ class Ls22Shell extends AppShell $results[$k]['metrics']['connectedness'] = 100 * ($result['connected_elements'] / ($result['attribute_count'] + $result['object_count'])); $results[$k]['metrics']['attack_weight'] = 100 * (2*($result['attack']) + $result['attribute_attack']) / ($result['attribute_count'] + $result['object_count']); $results[$k]['metrics']['other_weight'] = 100 * (2*($result['other']) + $result['attribute_other']) / ($result['attribute_count'] + $result['object_count']); + $results[$k]['metrics']['collaboration'] = 100 * ((2*$result['events_extended'] + $result['extending_events']) / $result['event_count']); + $results[$k]['metrics']['collaboration'] = 100 * (2*(2*$result['events_extended'] + $result['extending_events']) / $result['event_count']); } - foreach (['connectedness', 'attack_weight', 'other_weight', 'warnings'] as $metric) { + foreach (['connectedness', 'attack_weight', 'other_weight', 'warnings', 'collaboration'] as $metric) { if (empty($results[$k]['metrics'][$metric])) { $results[$k]['metrics'][$metric] = 0; } @@ -512,13 +597,15 @@ class Ls22Shell extends AppShell 20 * $results[$k]['metrics']['warnings'] + 20 * $results[$k]['metrics']['connectedness'] + 40 * $results[$k]['metrics']['attack_weight'] + - 20 * $results[$k]['metrics']['other_weight'] + 10 * $results[$k]['metrics']['other_weight'] + + 10 * $results[$k]['metrics']['collaboration'] ) / 100; $scores[$k]['total'] = $results[$k]['score']; $scores[$k]['warnings'] = round(20 * $results[$k]['metrics']['warnings']); $scores[$k]['connectedness'] = round(20 * $results[$k]['metrics']['connectedness']); $scores[$k]['attack_weight'] = round(40 * $results[$k]['metrics']['attack_weight']); - $scores[$k]['other_weight'] = round(20 * $results[$k]['metrics']['other_weight']); + $scores[$k]['other_weight'] = round(10 * $results[$k]['metrics']['other_weight']); + $scores[$k]['collaboration'] = round(10 * $results[$k]['metrics']['collaboration']); } arsort($scores, SORT_DESC); $this->out(str_repeat('=', 128), 1, Shell::NORMAL); @@ -534,15 +621,17 @@ class Ls22Shell extends AppShell $score_string[1] = str_repeat('█', round($score['connectedness']/100)); $score_string[2] = str_repeat('█', round($score['attack_weight']/100)); $score_string[3] = str_repeat('█', round($score['other_weight']/100)); + $score_string[4] = str_repeat('█', round($score['collaboration']/100)); $this->out(sprintf( '| %s | %s | %s |', str_pad($org, 10, ' ', STR_PAD_RIGHT), sprintf( - '%s%s%s%s%s', + '%s%s%s%s%s%s', $score_string[0], $score_string[1], $score_string[2], $score_string[3], + $score_string[4], str_repeat(' ', 100 - mb_strlen(implode('', $score_string))) ), str_pad($score['total'] . '%', 8, ' ', STR_PAD_RIGHT) @@ -555,6 +644,7 @@ class Ls22Shell extends AppShell '█: Connectedness', '█: ATT&CK context', '█: Other Context', + '█: Collaboration', str_repeat(' ', 52) ), 1, Shell::NORMAL); $this->out(str_repeat('=', 128), 1, Shell::NORMAL); diff --git a/app/Console/Command/ServerShell.php b/app/Console/Command/ServerShell.php index 3de14fca3..a82b9d97d 100644 --- a/app/Console/Command/ServerShell.php +++ b/app/Console/Command/ServerShell.php @@ -11,7 +11,7 @@ require_once 'AppShell.php'; */ class ServerShell extends AppShell { - public $uses = array('Server', 'Task', 'Job', 'User', 'Feed'); + public $uses = array('Server', 'Task', 'Job', 'User', 'Feed', 'TaxiiServer'); public function getOptionParser() { @@ -790,4 +790,30 @@ class ServerShell extends AppShell } return $server; } + + public function push_taxii() + { + if (empty($this->args[0]) || empty($this->args[1])) { + die('Usage: ' . $this->Server->command_line_functions['console_automation_tasks']['data']['Push Taxii'] . PHP_EOL); + } + + $userId = $this->args[0]; + $user = $this->getUser($userId); + $serverId = $this->args[1]; + if (!empty($this->args[2])) { + $jobId = $this->args[2]; + } else { + $jobId = $this->Job->createJob($user, Job::WORKER_DEFAULT, 'push_taxii', 'Server: ' . $serverId, 'Pushing.'); + } + $this->Job->read(null, $jobId); + + $result = $this->TaxiiServer->push($serverId, $user, $jobId); + if ($result !== true && !is_array($result)) { + $message = 'Job failed. Reason: ' . $result; + $this->Job->saveStatus($jobId, false, $message); + } else { + $message = 'Job done.'; + $this->Job->saveStatus($jobId, true, $message); + } + } } diff --git a/app/Controller/AccessLogsController.php b/app/Controller/AccessLogsController.php new file mode 100644 index 000000000..372744744 --- /dev/null +++ b/app/Controller/AccessLogsController.php @@ -0,0 +1,228 @@ + -1, + 'limit' => 60, + 'fields' => ['id', 'created', 'user_id', 'org_id', 'authkey_id', 'ip', 'request_method', 'user_agent', 'request_id', 'controller', 'action', 'url', 'response_code', 'memory_usage', 'duration', 'query_count'], + 'contain' => [ + 'User' => ['fields' => ['id', 'email', 'org_id']], + 'Organisation' => ['fields' => ['id', 'name', 'uuid']], + ], + 'order' => [ + 'AccessLog.id' => 'DESC' + ], + ]; + + public function admin_index() + { + $params = $this->IndexFilter->harvestParameters([ + 'created', + 'ip', + 'user', + 'org', + 'request_id', + 'authkey_id', + 'api_request', + 'request_method', + 'controller', + 'action', + 'url', + 'user_agent', + 'memory_usage', + 'duration', + 'query_count', + 'response_code', + ]); + + $conditions = $this->__searchConditions($params); + + if ($this->_isRest()) { + $list = $this->AccessLog->find('all', [ + 'conditions' => $conditions, + 'contain' => $this->paginate['contain'], + ]); + foreach ($list as &$item) { + if (!empty($item['AccessLog']['request'])) { + $item['AccessLog']['request'] = base64_encode($item['AccessLog']['request']); + } + } + return $this->RestResponse->viewData($list, 'json'); + } + if (empty(Configure::read('MISP.log_skip_access_logs_in_application_logs'))) { + $this->Flash->warning(__('Access logs are logged in both application logs and access logs. Make sure you reconfigure your log monitoring tools and update MISP.log_skip_access_logs_in_application_logs.')); + } + + $this->AccessLog->virtualFields['has_query_log'] = 'query_log IS NOT NULL'; + $this->paginate['fields'][] = 'has_query_log'; + $this->paginate['conditions'] = $conditions; + $list = $this->paginate(); + + $this->set('list', $list); + $this->set('title_for_layout', __('Access logs')); + } + + public function admin_request($id) + { + $request = $this->AccessLog->find('first', [ + 'conditions' => ['AccessLog.id' => $id], + 'fields' => ['AccessLog.request'], + ]); + if (empty($request)) { + throw new NotFoundException(__('Access log not found')); + } + + if (empty($request['AccessLog']['request'])) { + throw new NotFoundException(__('Request body is empty')); + } + + $contentType = explode(';', $request['AccessLog']['request_content_type'], 2)[0]; + if ($contentType === 'application/x-www-form-urlencoded' || $contentType === 'multipart/form-data') { + parse_str($request['AccessLog']['request'], $output); + // highlight PHP array + $highlighted = highlight_string("\\|", "", $highlighted, 1); // remove prefix + $highlighted = preg_replace("|\\\$|", "", $highlighted, 1); // remove suffix 1 + $highlighted = trim($highlighted); // remove line breaks + $highlighted = preg_replace("|\\\$|", "", $highlighted, 1); // remove suffix 2 + $highlighted = trim($highlighted); // remove line breaks + $highlighted = preg_replace("|^(\\)(<\\?php )(.*?)(\\)|", "\$1\$3\$4", $highlighted); // remove custom added "set('request', $data); + } + + public function admin_queryLog($id) + { + $request = $this->AccessLog->find('first', [ + 'conditions' => ['AccessLog.id' => $id], + 'fields' => ['AccessLog.query_log'], + ]); + if (empty($request)) { + throw new NotFoundException(__('Access log not found')); + } + + if (empty($request['AccessLog']['query_log'])) { + throw new NotFoundException(__('Query log is empty')); + } + + $this->set('queryLog', $request['AccessLog']['query_log']); + } + + /** + * @param array $params + * @return array + */ + private function __searchConditions(array $params) + { + $qbRules = []; + foreach ($params as $key => $value) { + if ($key === 'created') { + $qbRules[] = [ + 'id' => $key, + 'operator' => is_array($value) ? 'between' : 'greater_or_equal', + 'value' => $value, + ]; + } else { + if (is_array($value)) { + $value = implode('||', $value); + } + $qbRules[] = [ + 'id' => $key, + 'value' => $value, + ]; + } + } + $this->set('qbRules', $qbRules); + + $conditions = []; + if (isset($params['user'])) { + if (is_numeric($params['user'])) { + $conditions['AccessLog.user_id'] = $params['user']; + } else { + $user = $this->User->find('first', [ + 'conditions' => ['User.email' => $params['user']], + 'fields' => ['id'], + ]); + if (!empty($user)) { + $conditions['AccessLog.user_id'] = $user['User']['id']; + } else { + $conditions['AccessLog.user_id'] = -1; + } + } + } + if (isset($params['ip'])) { + $conditions['AccessLog.ip'] = inet_pton($params['ip']); + } + foreach (['authkey_id', 'request_id', 'controller', 'action'] as $field) { + if (isset($params[$field])) { + $conditions['AccessLog.' . $field] = $params[$field]; + } + } + if (isset($params['url'])) { + $conditions['AccessLog.url LIKE'] = "%{$params['url']}%"; + } + if (isset($params['user_agent'])) { + $conditions['AccessLog.user_agent LIKE'] = "%{$params['user_agent']}%"; + } + if (isset($params['memory_usage'])) { + $conditions['AccessLog.memory_usage >='] = ($params['memory_usage'] * 1024); + } + if (isset($params['memory_usage'])) { + $conditions['AccessLog.memory_usage >='] = ($params['memory_usage'] * 1024); + } + if (isset($params['duration'])) { + $conditions['AccessLog.duration >='] = $params['duration']; + } + if (isset($params['query_count'])) { + $conditions['AccessLog.query_count >='] = $params['query_count']; + } + if (isset($params['request_method'])) { + $methodId = array_flip(AccessLog::REQUEST_TYPES)[$params['request_method']] ?? -1; + $conditions['AccessLog.request_method'] = $methodId; + } + if (isset($params['org'])) { + if (is_numeric($params['org'])) { + $conditions['AccessLog.org_id'] = $params['org']; + } else { + $org = $this->AccessLog->Organisation->fetchOrg($params['org']); + if ($org) { + $conditions['AccessLog.org_id'] = $org['id']; + } else { + $conditions['AccessLog.org_id'] = -1; + } + } + } + if (isset($params['created'])) { + $tempData = is_array($params['created']) ? $params['created'] : [$params['created']]; + foreach ($tempData as $k => $v) { + $tempData[$k] = $this->AccessLog->resolveTimeDelta($v); + } + if (count($tempData) === 1) { + $conditions['AccessLog.created >='] = date("Y-m-d H:i:s", $tempData[0]); + } else { + if ($tempData[0] < $tempData[1]) { + $temp = $tempData[1]; + $tempData[1] = $tempData[0]; + $tempData[0] = $temp; + } + $conditions['AND'][] = ['AccessLog.created <=' => date("Y-m-d H:i:s", $tempData[0])]; + $conditions['AND'][] = ['AccessLog.created >=' => date("Y-m-d H:i:s", $tempData[1])]; + } + } + return $conditions; + } +} \ No newline at end of file diff --git a/app/Controller/AppController.php b/app/Controller/AppController.php index 390004fcf..a5212bb63 100755 --- a/app/Controller/AppController.php +++ b/app/Controller/AppController.php @@ -33,8 +33,8 @@ class AppController extends Controller public $helpers = array('OrgImg', 'FontAwesome', 'UserName'); - private $__queryVersion = '146'; - public $pyMispVersion = '2.4.162'; + private $__queryVersion = '153'; + public $pyMispVersion = '2.4.174'; public $phpmin = '7.2'; public $phprec = '7.4'; public $phptoonew = '8.0'; @@ -43,7 +43,6 @@ class AppController extends Controller private $isApiAuthed = false; public $baseurl = ''; - public $sql_dump = false; public $restResponsePayload = null; @@ -102,7 +101,9 @@ class AppController extends Controller { $controller = $this->request->params['controller']; $action = $this->request->params['action']; - + if (empty($this->Session->read('creation_timestamp'))) { + $this->Session->write('creation_timestamp', time()); + } if (Configure::read('MISP.system_setting_db')) { App::uses('SystemSetting', 'Model'); SystemSetting::setGlobalSetting(); @@ -136,17 +137,12 @@ class AppController extends Controller $this->response->header('X-XSS-Protection', '1; mode=block'); } - if (!empty($this->request->params['named']['sql'])) { - $this->sql_dump = intval($this->request->params['named']['sql']); - } - $this->_setupDatabaseConnection(); $this->set('debugMode', Configure::read('debug') >= 1 ? 'debugOn' : 'debugOff'); $isAjax = $this->request->is('ajax'); $this->set('ajax', $isAjax); $this->set('queryVersion', $this->__queryVersion); - $this->User = ClassRegistry::init('User'); $language = Configure::read('MISP.language'); if (!empty($language) && $language !== 'eng') { @@ -155,6 +151,26 @@ class AppController extends Controller Configure::write('Config.language', 'eng'); } + $this->User = ClassRegistry::init('User'); + + if (!empty($this->request->params['named']['disable_background_processing'])) { + Configure::write('MISP.background_jobs', 0); + } + + Configure::write('CurrentController', $controller); + Configure::write('CurrentAction', $action); + $versionArray = $this->User->checkMISPVersion(); + $this->mispVersion = implode('.', $versionArray); + $this->Security->blackHoleCallback = 'blackHole'; + + // send users away that are using ancient versions of IE + // Make sure to update this if IE 20 comes out :) + if (isset($_SERVER['HTTP_USER_AGENT'])) { + if (preg_match('/(?i)msie [2-8]/', $_SERVER['HTTP_USER_AGENT']) && !strpos($_SERVER['HTTP_USER_AGENT'], 'Opera')) { + throw new MethodNotAllowedException('You are using an unsecure and outdated version of IE, please download Google Chrome, Mozilla Firefox or update to a newer version of IE. If you are running IE9 or newer and still receive this error message, please make sure that you are not running your browser in compatibility mode. If you still have issues accessing the site, get in touch with your administration team at ' . Configure::read('MISP.contact')); + } + } + // For fresh installation (salt empty) generate a new salt if (!Configure::read('Security.salt')) { $this->User->Server->serverSettingsSaveValue('Security.salt', $this->User->generateRandomPassword(32)); @@ -165,6 +181,10 @@ class AppController extends Controller $this->User->Server->serverSettingsSaveValue('MISP.uuid', CakeText::uuid()); } + /** + * Authentication related activities + */ + // Check if Apache provides kerberos authentication data $authUserFields = $this->User->describeAuthFields(); $envvar = Configure::read('ApacheSecureAuth.apacheEnv'); @@ -180,22 +200,7 @@ class AppController extends Controller } else { $this->Auth->authenticate[AuthComponent::ALL]['userFields'] = $authUserFields; } - if (!empty($this->request->params['named']['disable_background_processing'])) { - Configure::write('MISP.background_jobs', 0); - } - Configure::write('CurrentController', $controller); - Configure::write('CurrentAction', $action); - $versionArray = $this->User->checkMISPVersion(); - $this->mispVersion = implode('.', $versionArray); - $this->Security->blackHoleCallback = 'blackHole'; - // send users away that are using ancient versions of IE - // Make sure to update this if IE 20 comes out :) - if (isset($_SERVER['HTTP_USER_AGENT'])) { - if (preg_match('/(?i)msie [2-8]/', $_SERVER['HTTP_USER_AGENT']) && !strpos($_SERVER['HTTP_USER_AGENT'], 'Opera')) { - throw new MethodNotAllowedException('You are using an unsecure and outdated version of IE, please download Google Chrome, Mozilla Firefox or update to a newer version of IE. If you are running IE9 or newer and still receive this error message, please make sure that you are not running your browser in compatibility mode. If you still have issues accessing the site, get in touch with your administration team at ' . Configure::read('MISP.contact')); - } - } $userLoggedIn = false; if (Configure::read('Plugin.CustomAuth_enable')) { $userLoggedIn = $this->__customAuthentication($_SERVER); @@ -217,16 +222,19 @@ class AppController extends Controller !$userLoggedIn && ( $controller !== 'users' || - $action !== 'register' || - empty(Configure::read('Security.allow_self_registration')) + ( + ($action !== 'register' || empty(Configure::read('Security.allow_self_registration'))) && + (!in_array($action, ['forgot', 'password_reset']) || empty(Configure::read('Security.allow_password_forgotten'))) + ) ) ) { // REST authentication if ($this->_isRest() || $this->_isAutomation()) { // disable CSRF for REST access $this->Security->csrfCheck = false; - if ($this->__loginByAuthKey() === false || $this->Auth->user() === null) { - if ($this->__loginByAuthKey() === null) { + $loginByAuthKeyResult = $this->__loginByAuthKey(); + if ($loginByAuthKeyResult === false || $this->Auth->user() === null) { + if ($loginByAuthKeyResult === null) { $this->loadModel('Log'); $this->Log->createLogEntry('SYSTEM', 'auth_fail', 'User', 0, "Failed API authentication. No authkey was provided."); } @@ -304,10 +312,14 @@ class AppController extends Controller $this->__accessMonitor($user); } else { - $preAuthActions = array('login', 'register', 'getGpgPublicKey'); + $preAuthActions = array('login', 'register', 'getGpgPublicKey', 'logout401', 'otp'); if (!empty(Configure::read('Security.email_otp_enabled'))) { $preAuthActions[] = 'email_otp'; } + if (!empty(Configure::read('Security.allow_password_forgotten'))) { + $preAuthActions[] = 'forgot'; + $preAuthActions[] = 'password_reset'; + } if (!$this->_isControllerAction(['users' => $preAuthActions, 'servers' => ['cspReport']])) { if ($isAjax) { $response = $this->RestResponse->throwException(401, "Unauthorized"); @@ -411,9 +423,12 @@ class AppController extends Controller } } if ($foundMispAuthKey) { - $authKeyToStore = substr($authKey, 0, 4) + $start = substr($authKey, 0, 4); + $end = substr($authKey, -4); + $authKeyToStore = $start . str_repeat('*', 32) - . substr($authKey, -4); + . $end; + $this->__logApiKeyUse($start . $end); if ($user) { // User found in the db, add the user info to the session if (Configure::read('MISP.log_auth')) { @@ -430,10 +445,7 @@ class AppController extends Controller ); $this->Log->save($log); } - $storeAPITime = Configure::read('MISP.store_api_access_time'); - if (!empty($storeAPITime) && $storeAPITime) { - $this->User->updateAPIAccessTime($user); - } + $this->User->updateAPIAccessTime($user); $this->Session->renew(); $this->Session->write(AuthComponent::$sessionKey, $user); $this->isApiAuthed = true; @@ -447,6 +459,9 @@ class AppController extends Controller } $this->Session->destroy(); } + } else { + $this->loadModel('Log'); + $this->Log->createLogEntry('SYSTEM', 'auth_fail', 'User', 0, "Failed authentication using an API key of incorrect length."); } return false; } @@ -483,7 +498,6 @@ class AppController extends Controller if (!$userFromDb) { $message = __('Something went wrong. Your user account that you are authenticated with doesn\'t exist anymore.'); if ($this->_isRest()) { - // TODO: Why not exception? $response = $this->RestResponse->throwException(401, $message); $response->send(); $this->_stop(); @@ -513,12 +527,24 @@ class AppController extends Controller } $this->Flash->info($message); $this->Auth->logout(); - throw new MethodNotAllowedException($message);//todo this should pb be removed? + $this->_redirectToLogin(); + return false; } else { $this->Flash->error(__('Warning: MISP is currently disabled for all users. Enable it in Server Settings (Administration -> Server Settings -> MISP tab -> live). An update might also be in progress, you can see the progress in ') , array('params' => array('url' => $this->baseurl . '/servers/updateProgress/', 'urlName' => __('Update Progress')), 'clear' => 1)); } } + // kill existing sessions for a user if the admin/instance decides so + // exclude API authentication as it doesn't make sense + if (!$this->isApiAuthed && $this->User->checkForSessionDestruction($user['id'])) { + $this->Auth->logout(); + $this->Session->destroy(); + $message = __('User deauthenticated on administrator request. Please reauthenticate.'); + $this->Flash->warning($message); + $this->_redirectToLogin(); + return false; + } + // Force logout doesn't make sense for API key authentication if (!$this->isApiAuthed && $user['force_logout']) { $this->User->id = $user['id']; @@ -581,6 +607,12 @@ class AppController extends Controller return true; } + // Check if user must create TOTP secret, force them to be on that page as long as needed. + if (empty($user['totp']) && Configure::read('Security.otp_required') && !$this->_isControllerAction(['users' => ['terms', 'change_pw', 'logout', 'login', 'totp_new']])) { // TOTP is mandatory for users, prevent login until the user has configured their TOTP + $this->redirect(array('controller' => 'users', 'action' => 'totp_new', 'admin' => false)); + return false; + } + // Check if user accepted terms and conditions if (!$user['termsaccepted'] && !empty(Configure::read('MISP.terms_file')) && !$this->_isControllerAction(['users' => ['terms', 'logout', 'login', 'downloadTerms']])) { //if ($this->_isRest()) throw new MethodNotAllowedException('You have not accepted the terms of use yet, please log in via the web interface and accept them.'); @@ -621,6 +653,15 @@ class AppController extends Controller return in_array($this->request->params['action'], $actionsToCheck[$controller], true); } + private function __logApiKeyUse($apikey) + { + $redis = $this->User->setupRedis(); + if (!$redis) { + return; + } + $redis->zIncrBy('misp:authkey_log:' . date("Ymd"), 1, $apikey); + } + /** * User access monitoring * @param array $user @@ -665,11 +706,28 @@ class AppController extends Controller { $userMonitoringEnabled = Configure::read('Security.user_monitoring_enabled'); if ($userMonitoringEnabled) { - $redis = $this->User->setupRedis(); - $userMonitoringEnabled = $redis && $redis->sismember('misp:monitored_users', $user['id']); + try { + $userMonitoringEnabled = RedisTool::init()->sismember('misp:monitored_users', $user['id']); + } catch (Exception $e) { + $userMonitoringEnabled = false; + } } - if (Configure::read('MISP.log_paranoid') || $userMonitoringEnabled) { + $shouldBeLogged = $userMonitoringEnabled || + Configure::read('MISP.log_paranoid') || + (Configure::read('MISP.log_paranoid_api') && isset($user['logged_by_authkey']) && $user['logged_by_authkey']); + + if ($shouldBeLogged) { + $includeRequestBody = !empty(Configure::read('MISP.log_paranoid_include_post_body')) || $userMonitoringEnabled; + /** @var AccessLog $accessLog */ + $accessLog = ClassRegistry::init('AccessLog'); + $accessLog->logRequest($user, $this->_remoteIp(), $this->request, $includeRequestBody); + } + + if ( + empty(Configure::read('MISP.log_skip_access_logs_in_application_logs')) && + $shouldBeLogged + ) { $change = 'HTTP method: ' . $_SERVER['REQUEST_METHOD'] . PHP_EOL . 'Target: ' . $this->request->here; if ( ( @@ -684,7 +742,7 @@ class AppController extends Controller $payload = $this->request->input(); $change .= PHP_EOL . 'Request body: ' . $payload; } - $this->Log = ClassRegistry::init('Log'); + $this->loadModel('Log'); $this->Log->createLogEntry($user, 'request', 'User', $user['id'], 'Paranoid log entry', $change); } } @@ -1041,7 +1099,7 @@ class AppController extends Controller $headerNamespace = ''; } if (isset($server[$headerNamespace . $header]) && !empty($server[$headerNamespace . $header])) { - if (Configure::read('Plugin.CustomAuth_only_allow_source') && Configure::read('Plugin.CustomAuth_only_allow_source') !== $server['REMOTE_ADDR']) { + if (Configure::read('Plugin.CustomAuth_only_allow_source') && Configure::read('Plugin.CustomAuth_only_allow_source') !== $this->_remoteIp()) { $this->Log = ClassRegistry::init('Log'); $this->Log->create(); $log = array( @@ -1050,7 +1108,7 @@ class AppController extends Controller 'model_id' => 0, 'email' => 'SYSTEM', 'action' => 'auth_fail', - 'title' => 'Failed authentication using external key (' . trim($server[$headerNamespace . $header]) . ') - the user has not arrived from the expected address. Instead the request came from: ' . $server['REMOTE_ADDR'], + 'title' => 'Failed authentication using external key (' . trim($server[$headerNamespace . $header]) . ') - the user has not arrived from the expected address. Instead the request came from: ' . $this->_remoteIp(), 'change' => null, ); $this->Log->save($log); @@ -1060,7 +1118,7 @@ class AppController extends Controller $user['User'] = $temp; if ($user['User']) { $this->User->updateLoginTimes($user['User']); - $this->Session->renew(); + //$this->Session->renew(); $this->Session->write(AuthComponent::$sessionKey, $user['User']); if (Configure::read('MISP.log_auth')) { $this->Log = ClassRegistry::init('Log'); @@ -1356,9 +1414,10 @@ class AppController extends Controller protected function _remoteIp() { $ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR'; - return isset($_SERVER[$ipHeader]) ? trim($_SERVER[$ipHeader]) : null; + return isset($_SERVER[$ipHeader]) ? trim($_SERVER[$ipHeader]) : $_SERVER['REMOTE_ADDR']; } + /** * @param string $key * @return bool Returns true if the same log defined by $key was not stored in last hour @@ -1452,7 +1511,7 @@ class AppController extends Controller protected function __setPagingParams(int $page, int $limit, int $current, string $type = 'named') { $this->request->params['paging'] = [ - 'Correlation' => [ + $this->modelClass => [ 'page' => $page, 'limit' => $limit, 'current' => $current, diff --git a/app/Controller/AttributesController.php b/app/Controller/AttributesController.php index 2479c5cd1..76eca938e 100644 --- a/app/Controller/AttributesController.php +++ b/app/Controller/AttributesController.php @@ -73,6 +73,9 @@ class AttributesController extends AppController { $user = $this->Auth->user(); $this->paginate['conditions']['AND'][] = $this->Attribute->buildConditions($user); + + $this->__setIndexFilterConditions(); + $attributes = $this->paginate(); if ($this->_isRest()) { @@ -299,7 +302,7 @@ class AttributesController extends AppController $conditions['Attribute.type'] = array('attachment', 'malware-sample'); $attributes = $this->Attribute->fetchAttributes($this->Auth->user(), array('conditions' => $conditions, 'flatten' => true)); if (empty($attributes)) { - throw new UnauthorizedException(__('Attribute does not exists or you do not have the permission to download this attribute.')); + throw new UnauthorizedException(__('Attribute does not exist or you do not have the permission to download this attribute.')); } return $this->__downloadAttachment($attributes[0]['Attribute']); } @@ -1536,6 +1539,7 @@ class AttributesController extends AppController $user = $this->Auth->user(); $exception = null; $filters = $this->__getSearchFilters($exception); + $this->set('passedArgsArray', ['results' => $continue]); if ($this->request->is('post') || !empty($this->request->params['named']['tags'])) { if ($filters === false) { return $exception; @@ -1763,7 +1767,7 @@ class AttributesController extends AppController $conditions['Attribute.type'] = array('attachment', 'malware-sample'); $attributes = $this->Attribute->fetchAttributes($user, array('conditions' => $conditions, 'flatten' => true)); if (empty($attributes)) { - throw new UnauthorizedException(__('Attribute does not exists or you do not have the permission to download this attribute.')); + throw new UnauthorizedException(__('Attribute does not exist or you do not have the permission to download this attribute.')); } return $this->__downloadAttachment($attributes[0]['Attribute']); } @@ -1961,7 +1965,7 @@ class AttributesController extends AppController public function fetchViewValue($id, $field = null) { $user = $this->_closeSession(); - $validFields = ['value', 'comment', 'type', 'category', 'to_ids', 'distribution', 'timestamp', 'first_seen', 'last_seen']; + $validFields = ['value', 'comment', 'type', 'category', 'distribution', 'timestamp', 'first_seen', 'last_seen']; if (!isset($field) || !in_array($field, $validFields, true)) { throw new MethodNotAllowedException(__('Invalid field requested.')); } @@ -1989,9 +1993,7 @@ class AttributesController extends AppController $attribute = $attribute[0]; $result = $attribute['Attribute'][$field]; if ($field === 'distribution') { - $result = $this->Attribute->shortDist[$result]; - } elseif ($field === 'to_ids') { - $result = $result == 0 ? 'No' : 'Yes'; + $this->set('shortDist', $this->Attribute->shortDist); } elseif ($field === 'value') { $this->loadModel('Warninglist'); $attribute['Attribute'] = $this->Warninglist->checkForWarning($attribute['Attribute']); @@ -2006,23 +2008,27 @@ class AttributesController extends AppController public function fetchEditForm($id, $field = null) { - $validFields = array('value', 'comment', 'type', 'category', 'to_ids', 'distribution', 'first_seen', 'last_seen'); - if (!isset($field) || !in_array($field, $validFields)) { - throw new MethodNotAllowedException(__('Invalid field requested.')); - } if (!$this->request->is('ajax')) { throw new MethodNotAllowedException(__('This function can only be accessed via AJAX.')); } - $fields = array('id', 'distribution', 'event_id'); - if ($field == 'category' || $field == 'type') { - $fields[] = 'type'; - $fields[] = 'category'; + + $validFields = array('value', 'comment', 'type', 'category', 'to_ids', 'distribution', 'first_seen', 'last_seen'); + if (!isset($field) || !in_array($field, $validFields, true)) { + throw new NotFoundException(__('Invalid field requested.')); + } + $fieldsToFetch = array('id', 'event_id'); + if ($field === 'category' || $field === 'type') { + $fieldsToFetch[] = 'type'; + $fieldsToFetch[] = 'category'; + if ($field === 'type') { + $fieldsToFetch[] = 'value'; + } } else { - $fields[] = $field; + $fieldsToFetch[] = $field; } $params = array( 'conditions' => array('Attribute.id' => $id), - 'fields' => $fields, + 'fields' => $fieldsToFetch, 'flatten' => 1, 'contain' => array( 'Event' => array( @@ -2044,15 +2050,28 @@ class AttributesController extends AppController unset($distributionLevels[4]); $this->set('distributionLevels', $distributionLevels); } elseif ($field === 'category') { - $typeCategory = array(); + $possibleCategories = []; foreach ($this->Attribute->categoryDefinitions as $k => $category) { - foreach ($category['types'] as $type) { - $typeCategory[$type][] = $k; + if (in_array($attribute['Attribute']['type'], $category['types'], true)) { + $possibleCategories[] = $k; } } - $this->set('typeCategory', $typeCategory); + $this->set('possibleCategories', $possibleCategories); } elseif ($field === 'type') { - $this->set('categoryDefinitions', $this->Attribute->categoryDefinitions); + $possibleTypes = $this->Attribute->categoryDefinitions[$attribute['Attribute']['category']]['types']; + $validTypes = AttributeValidationTool::validTypesForValue($possibleTypes, $this->Attribute->getCompositeTypes(), $attribute['Attribute']['value']); + $options = []; + foreach ($possibleTypes as $possibleType) { + if ($this->Attribute->typeIsAttachment($possibleType)) { + continue; // skip attachment types + } + $options[] = [ + 'name' => $possibleType, + 'value' => $possibleType, + 'disabled' => !in_array($possibleType, $validTypes, true), + ]; + } + $this->set('options', $options); } $this->set('object', $attribute['Attribute']); $fieldURL = ucfirst($field); @@ -2798,10 +2817,7 @@ class AttributesController extends AppController 'fields' => ['Attribute.deleted', 'Attribute.event_id', 'Attribute.id', 'Attribute.object_id', 'Event.orgc_id', 'Event.user_id'], 'contain' => ['Event'], ]); - if (empty($attribute)) { - throw new NotFoundException(__('Invalid attribute')); - } - if ($attribute['Attribute']['deleted']) { + if (empty($attribute) || $attribute['Attribute']['deleted']) { throw new NotFoundException(__('Invalid attribute')); } if (empty($tag_id)) { @@ -2831,19 +2847,19 @@ class AttributesController extends AppController if (!$this->__canModifyTag($attribute, !empty($attributeTag['AttributeTag']['local']))) { return new CakeResponse(array('body'=> json_encode(array('saved' => false, 'errors' => 'You do not have permission to do that.')), 'status' => 200, 'type' => 'json')); } - if (empty($attributeTag)) { return new CakeResponse(array('body'=> json_encode(array('saved' => false, 'errors' => 'Invalid attribute - tag combination.')), 'status' => 200, 'type' => 'json')); } - $tag = $this->Attribute->AttributeTag->Tag->find('first', array( - 'conditions' => array('Tag.id' => $tag_id), - 'recursive' => -1, - 'fields' => array('Tag.name') - )); if ($this->Attribute->AttributeTag->delete($attributeTag['AttributeTag']['id'])) { if (empty($attributeTag['AttributeTag']['local'])) { $this->Attribute->touch($attribute); } + + $tag = $this->Attribute->AttributeTag->Tag->find('first', array( + 'conditions' => array('Tag.id' => $tag_id), + 'recursive' => -1, + 'fields' => array('Tag.name') + )); $log = ClassRegistry::init('Log'); $log->createLogEntry($this->Auth->user(), 'tag', 'Attribute', $id, 'Removed tag (' . $tag_id . ') "' . $tag['Tag']['name'] . '" from attribute (' . $id . ')', 'Attribute (' . $id . ') untagged of Tag (' . $tag_id . ')'); return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'success' => 'Tag removed.', 'check_publish' => empty($attributeTag['AttributeTag']['local']))), 'status' => 200, 'type'=> 'json')); @@ -3005,4 +3021,18 @@ class AttributesController extends AppController $sg = $this->Attribute->Event->SharingGroup->fetchAllAuthorised($this->Auth->user(), 'name', true, $sharingGroupId); return !empty($sg); } + + private function __setIndexFilterConditions() + { + // search by attribute value + if (isset($this->request->params['named']['searchvalue'])) { + $v = $this->request->params['named']['searchvalue']; + $this->paginate['conditions']['AND'][] = [ + 'OR' => [ + ['Attribute.value1' => $v], + ['Attribute.value2' => $v], + ] + ]; + } + } } diff --git a/app/Controller/AuditLogsController.php b/app/Controller/AuditLogsController.php index 298a46e60..4199c748f 100644 --- a/app/Controller/AuditLogsController.php +++ b/app/Controller/AuditLogsController.php @@ -100,7 +100,9 @@ class AuditLogsController extends AppController if ($this->_isRest()) { $this->paginate['fields'][] = 'request_id'; } - + if (!Configure::read('MISP.log_new_audit')) { + $this->Flash->warning(__("Audit log is not enabled. See 'MISP.log_new_audit' in the Server Settings. (Administration -> Server Settings -> MISP tab)")); + } $params = $this->IndexFilter->harvestParameters([ 'ip', 'user', @@ -190,10 +192,13 @@ class AuditLogsController extends AppController $list[$k]['AuditLog']['action_human'] = $this->actions[$item['AuditLog']['action']]; } - $this->set('list', $list); + $this->set('data', $list); $this->set('event', $event); $this->set('mayModify', $this->__canModifyEvent($event)); - $this->set('title_for_layout', __('Audit logs for event #%s', $event['Event']['id'])); + $this->set('menuData', [ + 'menuList' => 'event', + 'menuItem' => 'eventLog' + ]); } public function fullChange($id) diff --git a/app/Controller/AuthKeysController.php b/app/Controller/AuthKeysController.php index dbcb0ed91..ea7289f26 100644 --- a/app/Controller/AuthKeysController.php +++ b/app/Controller/AuthKeysController.php @@ -18,41 +18,13 @@ class AuthKeysController extends AppController ) ); - public function index($id = false) + public function index($user_id = false) { $conditions = $this->__prepareConditions(); - $canCreateAuthkey = true; - if ($id) { - $this->set('user_id', $id); - if ($this->_isAdmin()) { - if ($this->_isSiteAdmin()) { - $canCreateAuthkey = true; - } else { - $user = $this->AuthKey->User->find('first', [ - 'recursive' => -1, - 'conditions' => [ - 'User.id' => $id, - 'User.disabled' => false - ], - 'fields' => ['User.id', 'User.org_id', 'User.disabled'], - 'contain' => [ - 'Role' => [ - 'fields' => [ - 'Role.perm_site_admin', 'Role.perm_admin' - ] - ] - ] - ]); - if ($user['Role']['perm_site_admin'] || ($user['Role']['perm_admin'] && $user['User']['id'] !== $this->Auth->user('id'))) { - $canCreateAuthkey = false; - } else { - $canCreateAuthkey = true; - } - } - } else { - $canCreateAuthkey = (int)$id === (int)$this->Auth->user('id'); - } - $conditions['AND'][] = ['AuthKey.user_id' => $id]; + $canCreateAuthkey = $this->__canCreateAuthKeyForUser($user_id); + if ($user_id) { + $this->set('user_id', $user_id); + $conditions['AND'][] = ['AuthKey.user_id' => $user_id]; } $this->set('canCreateAuthkey', $canCreateAuthkey); $keyUsageEnabled = Configure::read('MISP.log_user_ips') && Configure::read('MISP.log_user_ips_authkeys'); @@ -90,6 +62,9 @@ class AuthKeysController extends AppController public function delete($id) { + if(!$this->__canEditAuthKey($id)) { + throw new MethodNotAllowedException(__('Invalid user or insufficient privileges to interact with an authkey for the given user.')); + } $this->CRUD->delete($id, [ 'conditions' => $this->__prepareConditions(), 'contain' => ['User'], @@ -101,6 +76,9 @@ class AuthKeysController extends AppController public function edit($id) { + if(!$this->__canEditAuthKey($id)) { + throw new MethodNotAllowedException(__('Invalid user or insufficient privileges to interact with an authkey for the given user.')); + } $this->CRUD->edit($id, [ 'conditions' => $this->__prepareConditions(), 'afterFind' => function (array $authKey) { @@ -152,44 +130,16 @@ class AuthKeysController extends AppController $user_id = $this->Auth->user('id'); } $selectConditions = []; - if (!$this->_isSiteAdmin()) { - if ($this->_isAdmin()) { - $role_ids = $this->AuthKey->User->Role->find('column', [ - 'fields' => ['Role.id'], - 'conditions' => [ - 'AND' => [ - 'Role.perm_site_admin' => false, - 'Role.perm_auth' => true, - 'Role.perm_admin' => false - ] - ] - ]); - $user_ids = $this->AuthKey->User->find('column', [ - 'fields' => ['User.id'], - 'conditions' => [ - 'User.org_id' => $this->Auth->user('org_id'), - 'OR' => [ - 'User.role_id' => $role_ids, - 'User.id' => $this->Auth->user('id') - ] - ] - ]); - if (!empty($user_id)) { - if (in_array($user_id, $user_ids)) { - $user_ids = [$user_id]; - } else { - throw new MethodNotAllowedException(__('Invalid user or insufficient privileges to create an authkey for the given user.')); - } - } - $selectConditions['AND'][] = ['User.id' => $user_ids]; - $params['override']['user_id'] = $user_ids[0]; + if ($user_id) { + if ($this->__canCreateAuthKeyForUser($user_id)) { + $selectConditions['AND'][] = ['User.id' => $user_id]; + $params['override']['user_id'] = $user_id; } else { - $selectConditions['AND'][] = ['User.id' => $this->Auth->user('id')]; - $params['override']['user_id'] = $this->Auth->user('id'); + throw new MethodNotAllowedException(__('Invalid user or insufficient privileges to interact with an authkey for the given user.')); } - } else if ($user_id) { - $selectConditions['AND'][] = ['User.id' => $user_id]; - $params['override']['user_id'] = $user_id; + } else { + $selectConditions['AND'][] = ['User.id' => $this->Auth->user('id')]; + $params['override']['user_id'] = $this->Auth->user('id'); } $this->CRUD->add($params); if ($this->IndexFilter->isRest()) { @@ -238,6 +188,33 @@ class AuthKeysController extends AppController ]); } + public function pin($id, $ip) { + if(!$this->__canEditAuthKey($id)) { + throw new MethodNotAllowedException(__('Invalid user or insufficient privileges to interact with an authkey for the given user.')); + } + if ($this->request->is('post')) { + // find entry, to confirm user is authorized + $conditions = $this->__prepareConditions(); + $conditions['AND'][]['AuthKey.id'] = $id; + $authKey = $this->AuthKey->find( + 'first', + ['conditions' => $conditions, + 'recursive'=> 1 + ] + ); + // update the key with the source IP + if ($authKey) { + $authKey['AuthKey']['allowed_ips'] = $ip; + $this->AuthKey->save($authKey, ['fieldList' => ['allowed_ips']]); + $this->Flash->success(__('IP address set as allowed source for the Key.')); + } else { + $this->Flash->error(__('Failed to set IP as source')); + } + } + $this->redirect($this->referer()); + // $this->redirect(['controller' => 'auth_keys', 'view' => 'index']); + } + /** * Return conditions according to current user permission. * @return array @@ -246,7 +223,7 @@ class AuthKeysController extends AppController { $user = $this->Auth->user(); if ($user['Role']['perm_site_admin']) { - $conditions = []; // site admin can see all keys + $conditions = []; // site admin can see/edit all keys } else if ($user['Role']['perm_admin']) { $conditions['AND'][]['User.org_id'] = $user['org_id']; // org admin can see his/her user org auth keys } else { @@ -254,4 +231,54 @@ class AuthKeysController extends AppController } return $conditions; } + + private function __canCreateAuthKeyForUser($user_id) + { + if (!$user_id) + return true; + if ($this->_isAdmin()) { + if ($this->_isSiteAdmin()) { + return true; // site admin is OK for all + } else { + // org admin only for non-admin users and themselves + $user = $this->AuthKey->User->find('first', [ + 'recursive' => -1, + 'conditions' => [ + 'User.id' => $user_id, + 'User.disabled' => false + ], + 'fields' => ['User.id', 'User.org_id', 'User.disabled'], + 'contain' => [ + 'Role' => [ + 'fields' => [ + 'Role.perm_site_admin', 'Role.perm_admin', 'Role.perm_auth' + ] + ] + ] + ]); + if ($user['Role']['perm_site_admin'] || + ($user['Role']['perm_admin'] && $user['User']['id'] !== $this->Auth->user('id')) || + !$user['Role']['perm_auth']) { + // no create/edit for site_admin or other org admin + return false; + } else { + // ok for themselves or users + return true; + } + } + } else { + // user for themselves + return (int)$user_id === (int)$this->Auth->user('id'); + } + } + + private function __canEditAuthKey($key_id) + { + $user_id = $this->AuthKey->find('column', [ + 'fields' => ['AuthKey.user_id'], + 'conditions' => [ + 'AuthKey.id' => $key_id + ]]); + return $this->__canCreateAuthKeyForUser($user_id); + } } diff --git a/app/Controller/CommunitiesController.php b/app/Controller/CommunitiesController.php index 162d7162a..5b7a651bf 100644 --- a/app/Controller/CommunitiesController.php +++ b/app/Controller/CommunitiesController.php @@ -1,15 +1,13 @@ 60, 'maxLimit' => 9999 diff --git a/app/Controller/Component/ACLComponent.php b/app/Controller/Component/ACLComponent.php index c18835589..693703a66 100644 --- a/app/Controller/Component/ACLComponent.php +++ b/app/Controller/Component/ACLComponent.php @@ -72,8 +72,9 @@ class ACLComponent extends Component 'add' => ['AND' => ['perm_auth', 'not_read_only_authkey']], 'delete' => ['AND' => ['perm_auth', 'not_read_only_authkey']], 'edit' => ['AND' => ['perm_auth', 'not_read_only_authkey']], + 'pin' => ['AND' => ['perm_auth', 'not_read_only_authkey']], 'index' => ['perm_auth'], - 'view' => ['perm_auth'] + 'view' => ['perm_auth'], ], 'cerebrates' => [ 'add' => [], @@ -125,7 +126,7 @@ class ACLComponent extends Component 'decayingModel' => array( "update" => array(), "export" => array('*'), - "import" => array('*'), + "import" => array('OR' => array('perm_admin', 'perm_decaying')), "view" => array('*'), "index" => array('*'), "add" => array( 'OR' => array('perm_admin', 'perm_decaying')), @@ -384,23 +385,30 @@ class ACLComponent extends Component 'event_index' => array('*'), 'returnDates' => array('*'), 'testForStolenAttributes' => array(), - 'pruneUpdateLogs' => array() + 'pruneUpdateLogs' => array(), + 'index' => array('perm_audit') ), - 'auditLogs' => [ - 'admin_index' => ['perm_audit'], - 'fullChange' => ['perm_audit'], - 'eventIndex' => ['*'], - 'returnDates' => ['*'], - ], - 'modules' => array( - 'index' => array('perm_auth'), - 'queryEnrichment' => array('perm_auth'), - ), + 'auditLogs' => [ + 'admin_index' => ['perm_audit'], + 'fullChange' => ['perm_audit'], + 'eventIndex' => ['*'], + 'returnDates' => ['*'], + ], + 'accessLogs' => [ + 'admin_index' => [], + 'admin_request' => [], + 'admin_queryLog' => [], + ], + 'modules' => array( + 'index' => array('perm_auth'), + 'queryEnrichment' => array('perm_auth'), + ), 'news' => array( - 'add' => array(), - 'edit' => array(), - 'delete' => array(), - 'index' => array('*'), + 'add' => array(), + 'edit' => array(), + 'delete' => array(), + 'admin_index' => array(), + 'index' => ['*'], ), 'noticelists' => array( 'delete' => array(), @@ -429,6 +437,7 @@ class ACLComponent extends Component 'groupAttributesIntoObject' => array('perm_add'), 'revise_object' => array('perm_add'), 'view' => array('*'), + 'createFromFreetext' => ['perm_add'], ), 'objectReferences' => array( 'add' => array('perm_add'), @@ -446,9 +455,9 @@ class ACLComponent extends Component 'objectChoice' => array('*'), 'objectMetaChoice' => array('perm_add'), 'view' => array('*'), - 'viewElements' => array('*'), 'index' => array('*'), - 'update' => array() + 'update' => array(), + 'possibleObjectTemplates' => ['*'], ), 'objectTemplateElements' => array( 'viewElements' => array('*') @@ -475,9 +484,9 @@ class ACLComponent extends Component 'display' => array('*'), ), 'posts' => array( - 'add' => array('not_read_only_authkey'), - 'delete' => array('not_read_only_authkey'), - 'edit' => array('not_read_only_authkey'), + 'add' => ['AND' => ['not_read_only_authkey', 'discussion_enabled']], + 'delete' => ['AND' => ['not_read_only_authkey', 'discussion_enabled']], + 'edit' => ['AND' => ['not_read_only_authkey', 'discussion_enabled']], 'pushMessageToZMQ' => array() ), 'regexp' => array( @@ -604,7 +613,7 @@ class ACLComponent extends Component ), 'sightings' => array( 'add' => array('perm_sighting'), - 'restSearch' => array('perm_sighting'), + 'restSearch' => array('*'), 'advanced' => array('perm_sighting'), 'delete' => ['AND' => ['perm_sighting', 'perm_modify_org']], 'index' => array('*'), @@ -669,6 +678,7 @@ class ACLComponent extends Component 'taxonomyMassHide' => array('perm_tagger'), 'taxonomyMassUnhide' => array('perm_tagger'), 'toggleRequired' => array(), + 'toggleHighlighted' => array(), 'update' => array(), 'import' => [], 'export' => ['*'], @@ -677,6 +687,19 @@ class ACLComponent extends Component 'hideTag' => array('perm_tagger'), 'normalizeCustomTagsToTaxonomyFormat' => [], ), + 'taxiiServers' => [ + 'add' => ['perm_site_admin'], + 'edit' => ['perm_site_admin'], + 'collectionsIndex' => ['perm_site_admin'], + 'index' => ['perm_site_admin'], + 'objectsIndex' => ['perm_site_admin'], + 'objectView' => ['perm_site_admin'], + 'delete' => ['perm_site_admin'], + 'view' => ['perm_site_admin'], + 'push' => ['perm_site_admin'], + 'getRoot' => ['perm_site_admin'], + 'getCollections' => ['perm_site_admin'] + ], 'templateElements' => array( 'add' => array('perm_template'), 'delete' => array('perm_template'), @@ -698,14 +721,15 @@ class ACLComponent extends Component 'view' => array('*'), ), 'threads' => array( - 'index' => array('*'), - 'view' => array('*'), - 'viewEvent' => array('*'), + 'index' => array('discussion_enabled'), + 'view' => array('discussion_enabled'), + 'viewEvent' => array('discussion_enabled'), ), 'users' => array( 'acceptRegistrations' => array(), 'admin_add' => ['AND' => ['perm_admin', 'add_user_enabled']], 'admin_delete' => array('perm_admin'), + 'admin_destroy' => array(), 'admin_edit' => array('perm_admin'), 'admin_email' => array('perm_admin'), 'admin_filterUserIndex' => array('perm_admin'), @@ -724,13 +748,20 @@ class ACLComponent extends Component 'downloadTerms' => array('*'), 'edit' => array('self_management_enabled'), 'email_otp' => array('*'), + 'forgot' => array('*'), + 'otp' => array('*'), + 'hotp' => array('*'), + 'totp_new' => array('*'), + 'totp_delete' => array('perm_admin'), 'searchGpgKey' => array('*'), 'fetchGpgKey' => array('*'), 'histogram' => array('*'), 'initiatePasswordReset' => ['AND' => ['perm_admin', 'password_change_enabled']], 'login' => array('*'), 'logout' => array('*'), + 'logout401' => array('*'), 'notificationSettings' => ['*'], + 'password_reset' => array('*'), 'register' => array('*'), 'registrations' => array(), 'resetAllSyncAuthKeys' => array(), @@ -759,7 +790,7 @@ class ACLComponent extends Component 'eventIndexColumnToggle' => ['*'], ), 'warninglists' => array( - 'checkValue' => array('perm_auth'), + 'checkValue' => ['*'], 'delete' => ['perm_warninglist'], 'enableWarninglist' => ['perm_warninglist'], 'getToggleField' => ['perm_warninglist'], @@ -849,6 +880,9 @@ class ACLComponent extends Component $this->dynamicChecks['delegation_enabled'] = function (array $user) { return (bool)Configure::read('MISP.delegation'); }; + $this->dynamicChecks['discussion_enabled'] = function (array $user) { + return !Configure::read('MISP.discussion_disable'); + }; // Returns true if current user is not using advanced auth key or if authkey is not read only $this->dynamicChecks['not_read_only_authkey'] = function (array $user) { return !isset($user['authkey_read_only']) || !$user['authkey_read_only']; @@ -998,7 +1032,7 @@ class ACLComponent extends Component */ public function canEditEventReport(array $user, array $eventReport) { - if (!isset($report['Event'])) { + if (!isset($eventReport['Event'])) { throw new InvalidArgumentException('Passed object does not contain an Event.'); } if ($user['Role']['perm_site_admin']) { diff --git a/app/Controller/Component/Auth/ApacheAuthenticate.php b/app/Controller/Component/Auth/ApacheAuthenticate.php index 5fcfd50cb..77979d17e 100644 --- a/app/Controller/Component/Auth/ApacheAuthenticate.php +++ b/app/Controller/Component/Auth/ApacheAuthenticate.php @@ -38,10 +38,10 @@ class ApacheAuthenticate extends BaseAuthenticate } return $returnCode; } - + private function getEmailAddress($ldapEmailField, $ldapUserData) { - // return the email address of an LDAP user if one of the fields in $ldapEmaiLField exists + // return the email address of an LDAP user if one of the fields in $ldapEmaiLField exists foreach($ldapEmailField as $field) { if (isset($ldapUserData[0][$field][0])) { return $ldapUserData[0][$field][0]; @@ -73,6 +73,14 @@ class ApacheAuthenticate extends BaseAuthenticate ldap_set_option($ldapconn, LDAP_OPT_PROTOCOL_VERSION, Configure::read('ApacheSecureAuth.ldapProtocol')); ldap_set_option($ldapconn, LDAP_OPT_REFERRALS, Configure::read('ApacheSecureAuth.ldapAllowReferrals', true)); + if (Configure::read('ApacheSecureAuth.starttls', false) == true) { + # Default is false, sine STARTTLS support is a new feature + # Ignored on ldaps://, but can trigger problems for orgs + # using unencrypted LDAP. Loose comparison allows users to + # use # true / 1 / etc. + ldap_start_tls($ldapconn); + } + if ($ldapconn) { // LDAP bind $ldapbind = ldap_bind($ldapconn, $ldaprdn, $ldappass); @@ -105,7 +113,6 @@ class ApacheAuthenticate extends BaseAuthenticate } else { die("User not found in LDAP"); } - // close LDAP connection ldap_close($ldapconn); } diff --git a/app/Controller/Component/BetterSecurityComponent.php b/app/Controller/Component/BetterSecurityComponent.php index df3540a4d..a6eb76a0e 100644 --- a/app/Controller/Component/BetterSecurityComponent.php +++ b/app/Controller/Component/BetterSecurityComponent.php @@ -13,6 +13,15 @@ class BetterSecurityComponent extends SecurityComponent */ public $doNotGenerateToken = false; + public function blackHole(Controller $controller, $error = '', SecurityException $exception = null) + { + $action = $controller->request->params['action']; + $unlockedActions = JsonTool::encode($this->unlockedActions); + $isRest = $controller->IndexFilter->isRest() ? '1' : '0'; + $this->log("Blackhole exception when accessing $controller->here (isRest: $isRest, action: $action, unlockedActions: $unlockedActions): {$exception->getMessage()}"); // log blackhole exception + return parent::blackHole($controller, $error, $exception); + } + public function generateToken(CakeRequest $request) { if (isset($request->params['requested']) && $request->params['requested'] === 1) { diff --git a/app/Controller/Component/CRUDComponent.php b/app/Controller/Component/CRUDComponent.php index fb276e34d..81ad2bfb1 100644 --- a/app/Controller/Component/CRUDComponent.php +++ b/app/Controller/Component/CRUDComponent.php @@ -114,7 +114,7 @@ class CRUDComponent extends Component $this->Controller->Flash->success($message); if (!empty($params['displayOnSuccess'])) { $this->Controller->set('entity', $data); - $this->Controller->set('referer', $this->Controller->referer()); + $this->Controller->set('referer', $this->Controller->referer(['action' => 'view', $model->id], true)); $this->Controller->render($params['displayOnSuccess']); return; } diff --git a/app/Controller/Component/CompressedRequestHandlerComponent.php b/app/Controller/Component/CompressedRequestHandlerComponent.php index 969303257..4aa1135c0 100644 --- a/app/Controller/Component/CompressedRequestHandlerComponent.php +++ b/app/Controller/Component/CompressedRequestHandlerComponent.php @@ -1,19 +1,16 @@ request->setInput($this->decodeBrotliEncodedContent($controller)); } else if ($contentEncoding === 'gzip') { $controller->request->setInput($this->decodeGzipEncodedContent($controller)); } else { - throw new MethodNotAllowedException("Unsupported content encoding '$contentEncoding'."); + throw new BadRequestException("Unsupported content encoding '$contentEncoding'."); } } } @@ -24,10 +21,10 @@ class CompressedRequestHandlerComponent extends Component public function supportedEncodings() { $supportedEncodings = []; - if (function_exists('gzdecode') || function_exists('inflate_init')) { + if (function_exists('gzdecode')) { $supportedEncodings[] = 'gzip'; } - if (function_exists('brotli_uncompress') || function_exists('brotli_uncompress_init')) { + if (function_exists('brotli_uncompress')) { $supportedEncodings[] = 'br'; } return $supportedEncodings; @@ -36,44 +33,17 @@ class CompressedRequestHandlerComponent extends Component /** * @return string * @throws Exception - * @see CakeRequest::_readInput() */ private function decodeGzipEncodedContent(Controller $controller) { - if (function_exists('inflate_init')) { - // Decompress data on the fly if supported - $resource = inflate_init(ZLIB_ENCODING_GZIP); - if ($resource === false) { - throw new Exception('GZIP incremental uncompress init failed.'); - } - $uncompressed = ''; - foreach ($this->streamInput() as $data) { - $uncompressedChunk = inflate_add($resource, $data); - if ($uncompressedChunk === false) { - throw new MethodNotAllowedException('Invalid compressed data.'); - } - $uncompressed .= $uncompressedChunk; - if (strlen($uncompressed) > self::MAX_SIZE) { - throw new Exception("Uncompressed data are bigger than is limit."); - } - } - $uncompressedChunk = inflate_add($resource, '', ZLIB_FINISH); - if ($uncompressedChunk === false) { - throw new MethodNotAllowedException('Invalid compressed data.'); - } - return $uncompressed . $uncompressedChunk; - - } else if (function_exists('gzdecode')) { - $decoded = gzdecode($controller->request->input(), self::MAX_SIZE); + if (function_exists('gzdecode')) { + $decoded = gzdecode($controller->request->input()); if ($decoded === false) { - throw new MethodNotAllowedException('Invalid compressed data.'); - } - if (strlen($decoded) >= self::MAX_SIZE) { - throw new Exception("Uncompressed data are bigger than is limit."); + throw new BadRequestException('Invalid compressed data.'); } return $decoded; } else { - throw new MethodNotAllowedException("This server doesn't support GZIP compressed requests."); + throw new BadRequestException("This server doesn't support GZIP compressed requests."); } } @@ -81,65 +51,17 @@ class CompressedRequestHandlerComponent extends Component * @param Controller $controller * @return string * @throws Exception - * @see CakeRequest::_readInput() */ private function decodeBrotliEncodedContent(Controller $controller) { - if (function_exists('brotli_uncompress_init')) { - // Decompress data on the fly if supported - $resource = brotli_uncompress_init(); - if ($resource === false) { - throw new Exception('Brotli incremental uncompress init failed.'); - } - $uncompressed = ''; - foreach ($this->streamInput() as $data) { - $uncompressedChunk = brotli_uncompress_add($resource, $data, BROTLI_PROCESS); - if ($uncompressedChunk === false) { - throw new MethodNotAllowedException('Invalid compressed data.'); - } - $uncompressed .= $uncompressedChunk; - if (strlen($uncompressed) > self::MAX_SIZE) { - throw new Exception("Uncompressed data are bigger than is limit."); - } - } - $uncompressedChunk = brotli_uncompress_add($resource, '', BROTLI_FINISH); - if ($uncompressedChunk === false) { - throw new MethodNotAllowedException('Invalid compressed data.'); - } - return $uncompressed . $uncompressedChunk; - - } else if (function_exists('brotli_uncompress')) { - $decoded = brotli_uncompress($controller->request->input(), self::MAX_SIZE); + if (function_exists('brotli_uncompress')) { + $decoded = brotli_uncompress($controller->request->input()); if ($decoded === false) { - throw new MethodNotAllowedException('Invalid compressed data.'); - } - if (strlen($decoded) >= self::MAX_SIZE) { - throw new Exception("Uncompressed data are bigger than is limit."); + throw new BadRequestException('Invalid compressed data.'); } return $decoded; } else { - throw new MethodNotAllowedException("This server doesn't support brotli compressed requests."); + throw new BadRequestException("This server doesn't support brotli compressed requests."); } } - - /** - * @param int $chunkSize - * @return Generator - * @throws Exception - */ - private function streamInput($chunkSize = 8192) - { - $fh = fopen('php://input', 'rb'); - if ($fh === false) { - throw new Exception("Could not open PHP input for reading."); - } - while (!feof($fh)) { - $data = fread($fh, $chunkSize); - if ($data === false) { - throw new Exception("Could not read PHP input."); - } - yield $data; - } - fclose($fh); - } } diff --git a/app/Controller/Component/IOCImportComponent.php b/app/Controller/Component/IOCImportComponent.php index f265d1ed2..163b17c5a 100644 --- a/app/Controller/Component/IOCImportComponent.php +++ b/app/Controller/Component/IOCImportComponent.php @@ -190,7 +190,7 @@ class IOCImportComponent extends Component $duplicateFilter = array(); // check if we have any attributes, if yes, add their UUIDs to our list of success-array - if (count($event['Attribute']) > 0) { + if (isset($event['Attribute']) && count($event['Attribute']) > 0) { foreach ($event['Attribute'] as $k => $attribute) { $condensed = strtolower($attribute['value']) . $attribute['category'] . $attribute['type']; if (!in_array($condensed, $duplicateFilter)) { diff --git a/app/Controller/Component/IndexFilterComponent.php b/app/Controller/Component/IndexFilterComponent.php index fb0d62553..8d78351aa 100644 --- a/app/Controller/Component/IndexFilterComponent.php +++ b/app/Controller/Component/IndexFilterComponent.php @@ -44,15 +44,21 @@ class IndexFilterComponent extends Component } } } + + $data = $this->__massageData($data, $request, $paramArray); + + $this->Controller->set('passedArgs', json_encode($this->Controller->passedArgs)); + return $data; + } + + private function __massageData($data, $request, $paramArray) + { + $data = array_filter($data, function($paramName) use ($paramArray) { + return !empty($paramArray[$paramName]); + }, ARRAY_FILTER_USE_KEY); + if (!empty($paramArray)) { foreach ($paramArray as $p) { - if ( - isset($options['ordered_url_params'][$p]) && - (!in_array(strtolower((string)$options['ordered_url_params'][$p]), array('null', '0', false, 'false', null))) - ) { - $data[$p] = $options['ordered_url_params'][$p]; - $data[$p] = str_replace(';', ':', $data[$p]); - } if (isset($request->params['named'][$p])) { $data[$p] = str_replace(';', ':', $request->params['named'][$p]); } @@ -67,28 +73,8 @@ class IndexFilterComponent extends Component } } unset($v); - if (!empty($options['additional_delimiters'])) { - if (!is_array($options['additional_delimiters'])) { - $options['additional_delimiters'] = array($options['additional_delimiters']); - } - foreach ($data as $k => $v) { - $found = false; - foreach ($options['additional_delimiters'] as $delim) { - if (strpos($v, $delim) !== false) { - $found = true; - break; - } - } - if ($found) { - $data[$k] = explode($options['additional_delimiters'][0], str_replace($options['additional_delimiters'], $options['additional_delimiters'][0], $v)); - foreach ($data[$k] as $k2 => $value) { - $data[$k][$k2] = trim($data[$k][$k2]); - } - } - } - } - $this->Controller->set('passedArgs', json_encode($this->Controller->passedArgs)); return $data; + } public function isRest() diff --git a/app/Controller/Component/RestResponseComponent.php b/app/Controller/Component/RestResponseComponent.php index 28d4a60e0..0e3f4d09d 100644 --- a/app/Controller/Component/RestResponseComponent.php +++ b/app/Controller/Component/RestResponseComponent.php @@ -608,37 +608,47 @@ class RestResponseComponent extends Component $type = 'csv'; } else { $type = $format; - $dumpSql = !empty($this->Controller->sql_dump) && Configure::read('debug') > 1; + + $dumpSql = intval($this->Controller->request->params['named']['sql'] ?? 0); + if ($dumpSql && Configure::read('debug') < 2) { + $dumpSql = 0; // disable dumping SQL if debugging is off + } + if (!$raw) { if (is_string($response)) { $response = array('message' => $response); } if ($dumpSql) { - $this->Log = ClassRegistry::init('Log'); - if ($this->Controller->sql_dump === 2) { - $response = array('sql_dump' => $this->Log->getDataSource()->getLog(false, false)); + if ($dumpSql === 2) { + $response = ['sql_dump' => $this->getSqlLog()]; } else { - $response['sql_dump'] = $this->Log->getDataSource()->getLog(false, false); + $response['sql_dump'] = $this->getSqlLog(); } } - $flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; - if (!$this->isAutomaticTool()) { - $flags |= JSON_PRETTY_PRINT; // Do not pretty print response for automatic tools + // If response is big array, encode items separately to save memory + if (is_array($response) && count($response) > 10000) { + $output = new TmpFileTool(); + $output->write('['); + + foreach ($response as $item) { + $output->writeWithSeparator(JsonTool::encode($item), ','); + } + + $output->write(']'); + $response = $output; + } else { + $prettyPrint = !$this->isAutomaticTool(); // Do not pretty print response for automatic tools + $response = JsonTool::encode($response, $prettyPrint); } - if (defined('JSON_THROW_ON_ERROR')) { - $flags |= JSON_THROW_ON_ERROR; // Throw exception on error if supported - } - $response = json_encode($response, $flags); } else { if ($dumpSql) { - $this->Log = ClassRegistry::init('Log'); - if ($this->Controller->sql_dump === 2) { - $response = json_encode(array('sql_dump' => $this->Log->getDataSource()->getLog(false, false))); + if ($dumpSql === 2) { + $response = JsonTool::encode(['sql_dump' => $this->getSqlLog()]); } else { $response = substr_replace( $response, - sprintf(', "sql_dump": %s}', json_encode($this->Log->getDataSource()->getLog(false, false))), + sprintf(', "sql_dump": %s}', JsonTool::encode($this->getSqlLog())), -2 ); } @@ -2104,4 +2114,12 @@ class RestResponseComponent extends Component } return '/' . $admin_routing . $controller . '/' . $action; } + + /** + * @return array + */ + private function getSqlLog() + { + return $this->Controller->User->getDataSource()->getLog(false, false); + } } diff --git a/app/Controller/DashboardsController.php b/app/Controller/DashboardsController.php index 9b87e8afd..25df8ec0f 100644 --- a/app/Controller/DashboardsController.php +++ b/app/Controller/DashboardsController.php @@ -88,22 +88,24 @@ class DashboardsController extends AppController public function getForm($action = 'edit') { - if ($this->request->is('post') || $this->request->is('put')) { + if ($this->request->is(['post', 'put'])) { $data = $this->request->data; - if ($action === 'edit' && !isset($data['widget'])) { - throw new InvalidArgumentException(__('No widget name passed.')); - } if (empty($data['config'])) { $data['config'] = ''; } if ($action === 'add') { $data['widget_options'] = $this->Dashboard->loadAllWidgets($this->Auth->user()); - } else { + } else if ($action === 'edit') { + if (!isset($data['widget'])) { + throw new BadRequestException(__('No widget name passed.')); + } $dashboardWidget = $this->Dashboard->loadWidget($this->Auth->user(), $data['widget']); $data['description'] = empty($dashboardWidget->description) ? '' : $dashboardWidget->description; $data['params'] = empty($dashboardWidget->params) ? array() : $dashboardWidget->params; $data['params'] = array_merge($data['params'], array('widget_config' => __('Configuration of the widget that will be passed to the render. Check the view for more information'))); $data['params'] = array_merge(array('alias' => __('Alias to use as the title of the widget')), $data['params']); + } else { + throw new BadRequestException(__('Invalid action provided, just add or edit is supported.')); } $this->set('data', $data); $this->layout = false; @@ -183,13 +185,17 @@ class DashboardsController extends AppController } else { $data = $dashboardWidget->handler($user, $valueConfig); } - + $renderer = method_exists($dashboardWidget, 'getRenderer') ? $dashboardWidget->getRenderer($valueConfig) : $dashboardWidget->render; $config = array( - 'render' => $dashboardWidget->render, + 'render' => $renderer, 'autoRefreshDelay' => empty($dashboardWidget->autoRefreshDelay) ? false : $dashboardWidget->autoRefreshDelay, 'widget_config' => empty($valueConfig['widget_config']) ? array() : $valueConfig['widget_config'] ); + if (!empty($this->request->params['named']['exportjson'])) { + return $this->RestResponse->viewData($data); + } + $this->layout = false; $this->set('title', $dashboardWidget->title); $this->set('widget_id', $widget_id); @@ -316,6 +322,8 @@ class DashboardsController extends AppController public function listTemplates() { $conditions = array(); + // load all widgets for internal use, won't be displayed to the user. Thus we circumvent the ACL on it. + $accessible_widgets = array_keys($this->Dashboard->loadAllWidgets($this->Auth->user())); if (!$this->_isSiteAdmin()) { $permission_flags = array(); foreach ($this->Auth->user('Role') as $perm => $value) { @@ -394,6 +402,15 @@ class DashboardsController extends AppController } $element['Dashboard']['widgets'] = array_keys($widgets); sort($element['Dashboard']['widgets']); + $temp = []; + foreach ($element['Dashboard']['widgets'] as $widget) { + if (in_array($widget, $accessible_widgets)) { + $temp['allow'][] = $widget; + } else { + $temp['deny'][] = $widget; + } + } + $element['Dashboard']['widgets'] = $temp; if ($element['Dashboard']['user_id'] != $this->Auth->user('id')) { $element['User']['email'] = ''; } diff --git a/app/Controller/DecayingModelController.php b/app/Controller/DecayingModelController.php index 3b0ac6377..0d1394ea1 100644 --- a/app/Controller/DecayingModelController.php +++ b/app/Controller/DecayingModelController.php @@ -377,13 +377,15 @@ class DecayingModelController extends AppController $decaying_model['DecayingModel']['enabled'] = 1; if ($this->DecayingModel->save($decaying_model)) { + $model = $this->DecayingModel->fetchModel($this->Auth->user(), $id, true, array(), true); + if (empty($model)) { + throw new NotFoundException(__('No Decaying Model with the provided ID exists')); + } + $response = array('data' => $model, 'action' => 'enable'); if ($this->request->is('ajax')) { - $model = $this->DecayingModel->fetchModel($this->Auth->user(), $id, true, array(), true); - if (empty($model)) { - throw new NotFoundException(__('No Decaying Model with the provided ID exists')); - } - $response = array('data' => $model, 'action' => 'enable'); return $this->RestResponse->viewData($response, $this->response->type()); + } else if ($this->_isRest()) { + return $this->RestResponse->successResponse($id, __('Decaying model enabled'), $model); } $this->Flash->success(__('Decaying Model enabled.')); } else { @@ -400,7 +402,7 @@ class DecayingModelController extends AppController } $this->Flash->error(__('Error while enabling decaying model')); } - $this->redirect($this->referer()); + $this->redirect(array('action' => 'index')); } else { $this->set('model', $decaying_model['DecayingModel']); $this->render('ajax/enable_form'); @@ -420,13 +422,15 @@ class DecayingModelController extends AppController $decaying_model['DecayingModel']['enabled'] = 0; if ($this->DecayingModel->save($decaying_model)) { + $model = $this->DecayingModel->fetchModel($this->Auth->user(), $id, true, array(), true); + if (empty($model)) { + throw new NotFoundException(__('No Decaying Model with the provided ID exists')); + } + $response = array('data' => $model, 'action' => 'disable'); if ($this->request->is('ajax')) { - $model = $this->DecayingModel->fetchModel($this->Auth->user(), $id, true, array(), true); - if (empty($model)) { - throw new NotFoundException(__('No Decaying Model with the provided ID exists')); - } - $response = array('data' => $model, 'action' => 'disable'); return $this->RestResponse->viewData($response, $this->response->type()); + } else if ($this->_isRest()) { + return $this->RestResponse->successResponse($id, __('Decaying model disabled'), $model); } $this->Flash->success(__('Decaying Model disabled.')); } else { diff --git a/app/Controller/EventReportsController.php b/app/Controller/EventReportsController.php index d4d972d36..0a32b5f8a 100644 --- a/app/Controller/EventReportsController.php +++ b/app/Controller/EventReportsController.php @@ -492,9 +492,9 @@ class EventReportsController extends AppController $this->set('sharingGroups', $sgs); } - private function __injectPermissionsToViewContext($user, $report) + private function __injectPermissionsToViewContext(array $user, array $report) { - $canEdit = $this->EventReport->canEditReport($user, $report) === true; + $canEdit = $this->ACL->canEditEventReport($user, $report); $this->set('canEdit', $canEdit); } diff --git a/app/Controller/EventsController.php b/app/Controller/EventsController.php index d430cfbf1..b2905f6ec 100644 --- a/app/Controller/EventsController.php +++ b/app/Controller/EventsController.php @@ -111,6 +111,10 @@ class EventsController extends AppController if (in_array($this->request->action, ['checkLocks', 'getDistributionGraph'], true)) { $this->Security->doNotGenerateToken = true; } + + if (Configure::read('Plugin.CustomAuth_enable') && in_array($this->request->action, ['saveFreeText'], true)) { + $this->Security->csrfCheck = false; + } } /** @@ -665,6 +669,25 @@ class EventsController extends AppController [$eventReportQuery] ] ]; + break; + case 'value': + if ($v == "") { + continue 2; + } + $conditions['OR'] = [ + ['Attribute.value1' => $v], + ['Attribute.value2' => $v], + ]; + + $eventIds = $this->Event->Attribute->fetchAttributes($this->Auth->user(), array( + 'conditions' => $conditions, + 'flatten' => true, + 'event_ids' => true, + 'list' => true, + )); + + $this->paginate['conditions']['AND'][] = array('Event.id' => $eventIds); + break; default: continue 2; @@ -678,7 +701,7 @@ class EventsController extends AppController { // list the events $urlparams = ""; - $overrideAbleParams = array('all', 'attribute', 'published', 'eventid', 'datefrom', 'dateuntil', 'org', 'eventinfo', 'tag', 'tags', 'distribution', 'sharinggroup', 'analysis', 'threatlevel', 'email', 'hasproposal', 'timestamp', 'publishtimestamp', 'publish_timestamp', 'minimal'); + $overrideAbleParams = array('all', 'attribute', 'published', 'eventid', 'datefrom', 'dateuntil', 'org', 'eventinfo', 'tag', 'tags', 'distribution', 'sharinggroup', 'analysis', 'threatlevel', 'email', 'hasproposal', 'timestamp', 'publishtimestamp', 'publish_timestamp', 'minimal', 'value'); $paginationParams = array('limit', 'page', 'sort', 'direction', 'order'); $passedArgs = $this->passedArgs; if (!empty($this->request->data)) { @@ -990,7 +1013,7 @@ class EventsController extends AppController $possibleColumns[] = 'proposals'; } - if (Configure::read('MISP.showDiscussionsCountOnIndex')) { + if (Configure::read('MISP.showDiscussionsCountOnIndex') && !Configure::read('MISP.discussion_disable')) { $possibleColumns[] = 'discussion'; } @@ -1022,6 +1045,7 @@ class EventsController extends AppController if (in_array('tags', $columns, true) || in_array('clusters', $columns, true)) { $events = $this->Event->attachTagsToEvents($events); $events = $this->GalaxyCluster->attachClustersToEventIndex($user, $events, true); + $events = $this->__attachHighlightedTagsToEvents($events); } if (in_array('correlations', $columns, true)) { @@ -1036,7 +1060,7 @@ class EventsController extends AppController $events = $this->Event->attachProposalsCountToEvents($user, $events); } - if (in_array('discussion', $columns, true)) { + if (in_array('discussion', $columns, true) && !Configure::read('MISP.discussion_disable')) { $events = $this->Event->attachDiscussionsCountToEvents($user, $events); } @@ -1302,6 +1326,7 @@ class EventsController extends AppController } // remove galaxies tags + $containsProposals = !empty($event['ShadowAttribute']);; $this->loadModel('Taxonomy'); foreach ($event['Object'] as $k => $object) { if (isset($object['Attribute'])) { @@ -1312,6 +1337,9 @@ class EventsController extends AppController $tagConflicts = $this->Taxonomy->checkIfTagInconsistencies($attribute['AttributeTag']); $event['Object'][$k]['Attribute'][$k2]['tagConflicts'] = $tagConflicts; } + if (!$containsProposals && !empty($attribute['ShadowAttribute'])) { + $containsProposals = true; + } } } } @@ -1323,6 +1351,9 @@ class EventsController extends AppController $tagConflicts = $this->Taxonomy->checkIfTagInconsistencies($attribute['AttributeTag']); $attribute['tagConflicts'] = $tagConflicts; } + if (!$containsProposals && !empty($attribute['ShadowAttribute'])) { + $containsProposals = true; + } } if (empty($this->passedArgs['sort'])) { $filters['sort'] = 'timestamp'; @@ -1337,6 +1368,7 @@ class EventsController extends AppController } $this->params->params['paging'] = array($this->modelClass => $params); $this->set('event', $event); + $this->set('includeOrgColumn', (isset($conditions['extended']) || $containsProposals)); $this->set('includeSightingdb', (!empty($filters['includeSightingdb']) && Configure::read('Plugin.Sightings_sighting_db_enable'))); $this->set('deleted', isset($filters['deleted']) && $filters['deleted'] != 0); $this->set('attributeFilter', isset($filters['attributeFilter']) ? $filters['attributeFilter'] : 'all'); @@ -1386,32 +1418,11 @@ class EventsController extends AppController $emptyEvent = (empty($event['Object']) && empty($event['Attribute'])); $this->set('emptyEvent', $emptyEvent); - $attributeCount = isset($event['Attribute']) ? count($event['Attribute']) : 0; - $objectCount = isset($event['Object']) ? count($event['Object']) : 0; - $oldest_timestamp = false; + // set the data for the contributors / history field $contributors = $this->Event->ShadowAttribute->getEventContributors($event['Event']['id']); $this->set('contributors', $contributors); - if ($this->__canPublishEvent($event, $user)) { - $proposalStatus = false; - if (isset($event['ShadowAttribute']) && !empty($event['ShadowAttribute'])) { - $proposalStatus = true; - } - if (!$proposalStatus && !empty($event['Attribute'])) { - foreach ($event['Attribute'] as $temp) { - if (!empty($temp['ShadowAttribute'])) { - $proposalStatus = true; - break; - } - } - } - $mess = $this->Session->read('Message'); - if ($proposalStatus && empty($mess)) { - $this->Flash->info('This event has active proposals for you to accept or discard.'); - } - } - // set the pivot data $this->helpers[] = 'Pivot'; if ($continue) { @@ -1435,7 +1446,7 @@ class EventsController extends AppController } } foreach ($relatedEventCorrelationCount as $key => $relation) { - $relatedEventCorrelationCount[$key] = count($relatedEventCorrelationCount[$key]); + $relatedEventCorrelationCount[$key] = count($relation); } $this->Event->removeGalaxyClusterTags($event); @@ -1449,11 +1460,15 @@ class EventsController extends AppController } $this->set('tagConflicts', $tagConflicts); + $attributeCount = isset($event['Attribute']) ? count($event['Attribute']) : 0; + $objectCount = isset($event['Object']) ? count($event['Object']) : 0; + $oldestTimestamp = PHP_INT_MAX; + $containsProposals = !empty($event['ShadowAttribute']); $modDate = date("Y-m-d", $event['Event']['timestamp']); $modificationMap = array($modDate => 1); foreach ($event['Attribute'] as $k => $attribute) { - if ($oldest_timestamp === false || $oldest_timestamp > $attribute['timestamp']) { - $oldest_timestamp = $attribute['timestamp']; + if ($oldestTimestamp > $attribute['timestamp']) { + $oldestTimestamp = $attribute['timestamp']; } $modDate = date("Y-m-d", $attribute['timestamp']); $modificationMap[$modDate] = !isset($modificationMap[$modDate]) ? 1 : $modificationMap[$modDate] + 1; @@ -1470,10 +1485,10 @@ class EventsController extends AppController } $event['Attribute'][$k]['tagConflicts'] = $tagConflicts; } + if (!$containsProposals && !empty($attribute['ShadowAttribute'])) { + $containsProposals = true; + } } - $attributeTagsName = $this->Event->Attribute->AttributeTag->extractAttributeTagsNameFromEvent($event); - $this->set('attributeTags', array_values($attributeTagsName['tags'])); - $this->set('attributeClusters', array_values($attributeTagsName['clusters'])); foreach ($event['Object'] as $k => $object) { $modDate = date("Y-m-d", $object['timestamp']); @@ -1481,8 +1496,8 @@ class EventsController extends AppController if (!empty($object['Attribute'])) { $attributeCount += count($object['Attribute']); foreach ($object['Attribute'] as $k2 => $attribute) { - if ($oldest_timestamp === false || $oldest_timestamp > $attribute['timestamp']) { - $oldest_timestamp = $attribute['timestamp']; + if ($oldestTimestamp > $attribute['timestamp']) { + $oldestTimestamp = $attribute['timestamp']; } $modDate = date("Y-m-d", $attribute['timestamp']); @@ -1500,9 +1515,24 @@ class EventsController extends AppController } $event['Object'][$k]['Attribute'][$k2]['tagConflicts'] = $tagConflicts; } + if (!$containsProposals && !empty($attribute['ShadowAttribute'])) { + $containsProposals = true; + } } } } + + if ($containsProposals && $this->__canPublishEvent($event, $user)) { + $mess = $this->Session->read('Message'); + if (empty($mess)) { + $this->Flash->info(__('This event has active proposals for you to accept or discard.')); + } + } + + $attributeTagsName = $this->Event->Attribute->AttributeTag->extractAttributeTagsNameFromEvent($event); + $this->set('attributeTags', array_values($attributeTagsName['tags'])); + $this->set('attributeClusters', array_values($attributeTagsName['clusters'])); + $this->set('warningTagConflicts', $warningTagConflicts); $filters['sort'] = 'timestamp'; $filters['direction'] = 'desc'; @@ -1584,9 +1614,10 @@ class EventsController extends AppController if (!empty($filters['includeSightingdb']) && Configure::read('Plugin.Sightings_sighting_db_enable')) { $this->set('sightingdbs', $this->Sightingdb->getSightingdbList($user)); } + $this->set('includeOrgColumn', $this->viewVars['extended'] || $containsProposals); $this->set('includeSightingdb', !empty($filters['includeSightingdb']) && Configure::read('Plugin.Sightings_sighting_db_enable')); $this->set('relatedEventCorrelationCount', $relatedEventCorrelationCount); - $this->set('oldest_timestamp', $oldest_timestamp); + $this->set('oldest_timestamp', $oldestTimestamp === PHP_INT_MAX ? false : $oldestTimestamp); $this->set('missingTaxonomies', $this->Event->missingTaxonomies($event)); $this->set('currentUri', $attributeUri); $this->set('filters', $filters); @@ -1645,6 +1676,7 @@ class EventsController extends AppController $cortex_modules = $this->Module->getEnabledModules($user, false, 'Cortex'); $this->set('cortex_modules', $cortex_modules); } + $this->set('sightingsDbEnabled', (bool)Configure::read('Plugin.Sightings_sighting_db_enable')); } public function view($id = null, $continue = false, $fromEvent = null) @@ -1805,6 +1837,8 @@ class EventsController extends AppController $this->set('includeRelatedTags', (!empty($namedParams['includeRelatedTags'])) ? 1 : 0); $this->set('includeDecayScore', (!empty($namedParams['includeDecayScore'])) ? 1 : 0); + $this->__setHighlightedTags($event); + if ($this->_isSiteAdmin() && $event['Event']['orgc_id'] !== $this->Auth->user('org_id')) { $this->Flash->info(__('You are currently logged in as a site administrator and about to edit an event not belonging to your organisation. This goes against the sharing model of MISP. Use a normal user account for day to day work.')); } @@ -2273,7 +2307,12 @@ class EventsController extends AppController foreach ($analysisLevels as $key => $value) { $fieldDesc['analysis'][$key] = $this->Event->analysisDescriptions[$key]['formdesc']; } - $this->Flash->info(__('The event created will be visible to the organisations having an account on this platform, but not synchronised to other MISP instances until it is published.')); + + if (Configure::read('MISP.unpublishedprivate')) { + $this->Flash->info(__('The event created will be visible only to your organisation until it is published.')); + } else { + $this->Flash->info(__('The event created will be visible to the organisations having an account on this platform, but not synchronised to other MISP instances until it is published.')); + } $this->set('fieldDesc', $fieldDesc); if (isset($this->params['named']['extends'])) { $this->set('extends_uuid', $this->params['named']['extends']); @@ -2300,7 +2339,7 @@ class EventsController extends AppController // set the id $this->set('id', $id); // set whether it is published or not - $this->set('published', $this->Event->data['Event']['published']); + $this->set('published', $this->Event->data['Event']['published'] ?? false); } public function add_misp_export() @@ -2308,6 +2347,9 @@ class EventsController extends AppController if ($this->request->is('post')) { $results = array(); if (!empty($this->request->data)) { + if (empty($this->request->data['Event'])) { + $this->request->data['Event'] = $this->request->data; + } if (!empty($this->request->data['Event']['filecontent'])) { $data = $this->request->data['Event']['filecontent']; $isXml = $data[0] === '<'; @@ -2341,29 +2383,64 @@ class EventsController extends AppController $results = $this->Event->addMISPExportFile($this->Auth->user(), $data, $isXml, $takeOwnership, $publish); } catch (Exception $e) { $this->log("Exception during processing MISP file import: {$e->getMessage()}"); - $this->Flash->error(__('Could not process MISP export file. Probably file content is invalid.')); + $this->Flash->error(__('Could not process MISP export file. %s.', $e->getMessage())); $this->redirect(['controller' => 'events', 'action' => 'add_misp_export']); } } $this->set('results', $results); $this->render('add_misp_export_result'); } + $this->set('title_for_layout', __('Import from MISP Export File')); } - public function upload_stix($stix_version = '1', $publish = false) + public function upload_stix($stix_version = '1', $publish = false, $galaxies_as_tags = true, $debug = false) { + $sgs = $this->Event->SharingGroup->fetchAllAuthorised($this->Auth->user(), 'name', 1); + $initialDistribution = 0; + if (Configure::read('MISP.default_event_distribution') != null) { + $initialDistribution = Configure::read('MISP.default_event_distribution'); + } + $distributionLevels = $this->Event->distributionLevels; if ($this->request->is('post')) { if ($this->_isRest()) { if (isset($this->params['named']['publish'])) { $publish = $this->params['named']['publish']; } + if (isset($this->params['named']['distribution'])) { + $distribution = intval($this->params['named']['distribution']); + if (array_key_exists($distribution, $distributionLevels)) { + $initialDistribution = $distribution; + } else { + throw new MethodNotAllowedException(__('Wrong distribution level')); + } + } + $sharingGroupId = null; + if ($initialDistribution == 4) { + if (!isset($this->params['named']['sharing_group_id'])) { + throw new MethodNotAllowedException(__('The sharing group id is needed when the distribution is set to 4 ("Sharing group").')); + } + $sharingGroupId = intval($this->params['named']['sharing_group_id']); + if (!array_key_exists($sharingGroupId, $sgs)) { + throw new MethodNotAllowedException(__('Please select a valid sharing group id.')); + } + } + if (isset($this->params['named']['galaxies_as_tags'])) { + $galaxies_as_tags = $this->params['named']['galaxies_as_tags']; + } + if (isset($this->params['named']['debugging'])) { + $debug = $this->params['named']['debugging']; + } $filePath = FileAccessTool::writeToTempFile($this->request->input()); $result = $this->Event->upload_stix( $this->Auth->user(), $filePath, $stix_version, 'uploaded_stix_file.' . ($stix_version == '1' ? 'xml' : 'json'), - $publish + $publish, + $initialDistribution, + $sharingGroupId, + $galaxies_as_tags, + $debug ); if (is_numeric($result)) { $event = $this->Event->fetchEvent($this->Auth->user(), array('eventid' => $result)); @@ -2382,12 +2459,19 @@ class EventsController extends AppController if (!move_uploaded_file($this->data['Event']['stix']['tmp_name'], $filePath)) { throw new Exception("Could not move uploaded STIX file."); } + if (isset($this->data['Event']['debug'])) { + $debug = $this->data['Event']['debug']; + } $result = $this->Event->upload_stix( $this->Auth->user(), $filePath, $stix_version, $original_file, - $this->data['Event']['publish'] + $this->data['Event']['publish'], + $this->data['Event']['distribution'], + $this->data['Event']['sharing_group_id'], + !boolval($this->data['Event']['galaxies_parsing']), + $debug ); if (is_numeric($result)) { $this->Flash->success(__('STIX document imported.')); @@ -2405,6 +2489,20 @@ class EventsController extends AppController } } $this->set('stix_version', $stix_version == 2 ? '2.x JSON' : '1.x XML'); + $this->set('initialDistribution', $initialDistribution); + $distributions = array_keys($this->Event->distributionDescriptions); + $distributions = $this->_arrayToValuesIndexArray($distributions); + $this->set('distributions', $distributions); + $fieldDesc = array(); + if (empty($sgs)) { + unset($distributionLevels[4]); + } + $this->set('distributionLevels', $distributionLevels); + foreach ($distributionLevels as $key => $value) { + $fieldDesc['distribution'][$key] = $this->Event->distributionDescriptions[$key]['formdesc']; + } + $this->set('sharingGroups', $sgs); + $this->set('fieldDesc', $fieldDesc); } public function merge($target_id=null, $source_id=null) @@ -2540,7 +2638,7 @@ class EventsController extends AppController } } - public function populate($id) + public function populate($id, $regenerateUUIDs=false) { if ($this->request->is('get') && $this->_isRest()) { return $this->RestResponse->describe('Events', 'populate', false, $this->response->type()); @@ -2562,15 +2660,31 @@ class EventsController extends AppController } if ($this->request->is('post') || $this->request->is('put')) { if (isset($this->request->data['Event'])) { + $regenerateUUIDs = $this->request->data['Event']['regenerate_uuids'] ?? false; $this->request->data = $this->request->data['Event']; } if (isset($this->request->data['json'])) { $this->request->data = $this->_jsonDecode($this->request->data['json']); } + if (isset($this->request->data['Event'])) { + $this->request->data = $this->request->data['Event']; + } $eventToSave = $event; $capturedObjects = ['Attribute', 'Object', 'Tag', 'Galaxy', 'EventReport']; foreach ($capturedObjects as $objectType) { if (!empty($this->request->data[$objectType])) { + if (!empty($regenerateUUIDs)) { + foreach ($this->request->data[$objectType] as $i => $obj) { + unset($this->request->data[$objectType][$i]['id']); + unset($this->request->data[$objectType][$i]['uuid']); + if ($objectType === 'Object' && !empty($this->request->data[$objectType][$i]['Attribute'])) { + foreach ($this->request->data[$objectType][$i]['Attribute'] as $j => $attr) { + unset($this->request->data[$objectType][$i]['Attribute'][$j]['id']); + unset($this->request->data[$objectType][$i]['Attribute'][$j]['uuid']); + } + } + } + } $eventToSave['Event'][$objectType] = $this->request->data[$objectType]; } } @@ -3494,6 +3608,7 @@ class EventsController extends AppController 'category' => 'External analysis', 'uuid' => CakeText::uuid(), 'type' => 'attachment', + 'sharing_group_id' => '0', 'value' => $this->data['Event']['submittedioc']['name'], 'to_ids' => false, 'distribution' => $dist, @@ -3508,7 +3623,7 @@ class EventsController extends AppController $fieldList = array( 'Event' => array('published', 'timestamp'), - 'Attribute' => array('event_id', 'category', 'type', 'value', 'value1', 'value2', 'to_ids', 'uuid', 'distribution', 'timestamp', 'comment') + 'Attribute' => array('event_id', 'category', 'type', 'value', 'value1', 'value2', 'to_ids', 'uuid', 'distribution', 'timestamp', 'comment', 'sharing_group_id') ); // Save it all $saveResult = $this->Event->saveAssociated($saveEvent, array('validate' => true, 'fieldList' => $fieldList)); @@ -3985,6 +4100,7 @@ class EventsController extends AppController $this->set('mayModify', $this->__canModifyEvent($event)); $this->set('typeDefinitions', $this->Event->Attribute->typeDefinitions); $this->set('typeCategoryMapping', $typeCategoryMapping); + $this->set('defaultAttributeDistribution', $this->Event->Attribute->defaultDistribution()); $this->set('resultArray', $resultArray); $this->set('importComment', ''); $this->set('title_for_layout', __('Freetext Import Results')); @@ -4290,7 +4406,7 @@ class EventsController extends AppController ), 'bro' => array( 'url' => $this->baseurl . '/attributes/bro/download/all/false/' . $id, - // 'url' => '/attributes/restSearch/returnFormat:bro/published:1||0/eventid:' . $id, + // 'url' => $this->baseurl . '/attributes/restSearch/returnFormat:bro/published:1||0/eventid:' . $id, 'text' => __('Bro rules'), 'requiresPublished' => false, 'checkbox' => false, @@ -4396,12 +4512,12 @@ class EventsController extends AppController ), 'STIX' => array( 'url' => $this->baseurl . '/events/upload_stix', - 'text' => __('STIX 1.1.1 format (lossy)'), + 'text' => __('STIX 1.x format (lossy)'), 'ajax' => false, ), 'STIX2' => array( 'url' => $this->baseurl . '/events/upload_stix/2', - 'text' => __('STIX 2.0 format (lossy)'), + 'text' => __('STIX 2.x format (lossy)'), 'ajax' => false, ) ); @@ -5094,42 +5210,82 @@ class EventsController extends AppController $this->render('index'); } - // expects an attribute ID and the module to be used - public function queryEnrichment($attribute_id, $module = false, $type = 'Enrichment') + // expects a model ID, model type, the module to be used (optional) and the type of enrichment (optional) + public function queryEnrichment($id, $module = false, $type = 'Enrichment', $model = 'Attribute') { if (!Configure::read('Plugin.' . $type . '_services_enable')) { throw new MethodNotAllowedException(__('%s services are not enabled.', $type)); } - $attribute = $this->Event->Attribute->fetchAttributes($this->Auth->user(), [ - 'conditions' => [ - 'Attribute.id' => $attribute_id - ], - 'flatten' => 1, - 'includeEventTags' => 1, - 'contain' => ['Event' => ['fields' => ['distribution', 'sharing_group_id']]], - ]); - if (empty($attribute)) { - throw new MethodNotAllowedException(__('Attribute not found or you are not authorised to see it.')); + + if (!in_array($model, array('Attribute', 'ShadowAttribute', 'Object', 'Event'))) { + throw new MethodNotAllowedException(__('Invalid model.')); } + $this->loadModel('Module'); $enabledModules = $this->Module->getEnabledModules($this->Auth->user(), false, $type); + if (!is_array($enabledModules) || empty($enabledModules)) { - throw new MethodNotAllowedException(__('No valid %s options found for this attribute.', $type)); + throw new MethodNotAllowedException(__('No valid %s options found for this %s.', $type, strtolower($model))); } + + if ($model === 'Attribute' || $model === 'ShadowAttribute') { + $attribute = $this->Event->Attribute->fetchAttributes($this->Auth->user(), [ + 'conditions' => [ + 'Attribute.id' => $id + ], + 'flatten' => 1, + 'includeEventTags' => 1, + 'contain' => ['Event' => ['fields' => ['distribution', 'sharing_group_id']]], + ]); + if (empty($attribute)) { + throw new MethodNotAllowedException(__('Attribute not found or you are not authorised to see it.')); + } + } + + if ($model === 'Object') { + $object = $this->Event->Object->fetchObjects($this->Auth->user(), [ + 'conditions' => [ + 'Object.id' => $id + ], + 'flatten' => 1, + 'includeEventTags' => 1, + 'contain' => ['Event' => ['fields' => ['distribution', 'sharing_group_id']]], + ]); + if (empty($object)) { + throw new MethodNotAllowedException(__('Object not found or you are not authorised to see it.')); + } + } + if ($this->request->is('ajax')) { - $modules = array(); - foreach ($enabledModules['modules'] as $module) { - if (in_array($attribute[0]['Attribute']['type'], $module['mispattributes']['input'])) { - $modules[] = array('name' => $module['name'], 'description' => $module['meta']['description']); + $modules = []; + + if ($model === 'Attribute' || $model === 'ShadowAttribute') { + foreach ($enabledModules['modules'] as $module) { + if (in_array($attribute[0]['Attribute']['type'], $module['mispattributes']['input'])) { + $modules[] = array('name' => $module['name'], 'description' => $module['meta']['description']); + } } } - foreach (array('attribute_id', 'modules') as $viewVar) { - $this->set($viewVar, $$viewVar); + + if ($model === 'Object') { + foreach ($enabledModules['modules'] as $module) { + if ( + in_array($object[0]['Object']['name'], $module['mispattributes']['input']) || + in_array($object[0]['Object']['uuid'], $module['mispattributes']['input']) + ) { + $modules[] = array('name' => $module['name'], 'description' => $module['meta']['description']); + } + } } + + $this->set('id', $id); + $this->set('modules', $modules); $this->set('type', $type); + $this->set('model', $model); $this->render('ajax/enrichmentChoice'); } else { - $options = array(); + $options = []; + $format = 'simplified'; foreach ($enabledModules['modules'] as $temp) { if ($temp['name'] == $module) { $format = !empty($temp['mispattributes']['format']) ? $temp['mispattributes']['format'] : 'simplified'; @@ -5151,7 +5307,13 @@ class EventsController extends AppController $this->set('title_for_layout', __('Enrichment Results')); $this->set('title', __('Enrichment Results')); if ($format == 'misp_standard') { - $this->__queryEnrichment($attribute, $module, $options, $type); + if ($model === 'Attribute' || $model === 'ShadowAttribute') { + $this->__queryEnrichment($attribute, $module, $options, $type); + } + + if ($model === 'Object') { + $this->__queryObjectEnrichment($object, $module, $options, $type); + } } else { $this->__queryOldEnrichment($attribute, $module, $options, $type); } @@ -5205,6 +5367,57 @@ class EventsController extends AppController } } + private function __queryObjectEnrichment($object, $module, $options, $type) + { + $object[0]['Object']['Attribute'] = $object[0]['Attribute']; + foreach($object[0]['Object']['Attribute'] as &$attribute) { + if ($this->Event->Attribute->typeIsAttachment($attribute['type'])) { + $attribute['data'] = $this->Event->Attribute->base64EncodeAttachment($attribute); + } + } + + $event_id = $object[0]['Event']['id']; + $data = array('module' => $module, 'object' => $object[0]['Object'], 'event_id' => $event_id); + if (!empty($options)) { + $data['config'] = $options; + } + $result = $this->Module->queryModuleServer($data, false, $type, false, $object[0]); + if (!$result) { + throw new InternalErrorException(__('%s service not reachable.', $type)); + } + if (isset($result['error'])) { + $this->Flash->error($result['error']); + } + if (!is_array($result)) { + throw new Exception($result); + } + $event = $this->Event->handleMispFormatFromModuleResult($result); + if (empty($event['Attribute']) && empty($event['Object'])) { + throw new NotImplementedException(__('No Attribute or Object returned by the module.')); + } else { + $importComment = !empty($result['comment']) ? $result['comment'] : $object[0]['Object']['value'] . __(': Enriched via the ') . $module . ($type != 'Enrichment' ? ' ' . $type : '') . ' module'; + $this->set('importComment', $importComment); + $event['Event'] = $object[0]['Event']; + $org_name = $this->Event->Orgc->find('first', array( + 'conditions' => array('Orgc.id' => $event['Event']['orgc_id']), + 'fields' => array('Orgc.name') + )); + $event['Event']['orgc_name'] = $org_name['Orgc']['name']; + if ($attribute[0]['Object']['id']) { + $object_id = $attribute[0]['Object']['id']; + $initial_object = $this->Event->fetchInitialObject($event_id, $object_id); + if (!empty($initial_object)) { + $event['initialObject'] = $initial_object; + } + } + $this->set('event', $event); + $this->set('menuItem', 'enrichmentResults'); + $this->set('title_for_layout', __('Enrichment Results')); + $this->set('title', __('Enrichment Results')); + $this->render('resolved_misp_format'); + } + } + private function __queryOldEnrichment($attribute, $module, $options, $type) { $data = array('module' => $module, $attribute[0]['Attribute']['type'] => $attribute[0]['Attribute']['value'], 'event_id' => $attribute[0]['Attribute']['event_id'], 'attribute_uuid' => $attribute[0]['Attribute']['uuid']); @@ -5258,6 +5471,7 @@ class EventsController extends AppController $this->set('resultArray', $resultArray); $this->set('typeDefinitions', $this->Event->Attribute->typeDefinitions); $this->set('typeCategoryMapping', $typeCategoryMapping); + $this->set('defaultAttributeDistribution', $this->Event->Attribute->defaultDistribution()); $this->set('importComment', $importComment); $this->render('resolved_attributes'); } @@ -5292,7 +5506,7 @@ class EventsController extends AppController } } - public function importModule($module, $eventId) + public function importModule($moduleName, $eventId) { $event = $this->Event->fetchSimpleEvent($this->Auth->user(), $eventId); if (!$event) { @@ -5302,8 +5516,7 @@ class EventsController extends AppController $eventId = $event['Event']['id']; $this->loadModel('Module'); - $moduleName = $module; - $module = $this->Module->getEnabledModule($module, 'Import'); + $module = $this->Module->getEnabledModule($moduleName, 'Import'); if (!is_array($module)) { throw new MethodNotAllowedException($module); } @@ -5311,10 +5524,11 @@ class EventsController extends AppController $module['mispattributes']['inputSource'] = array('paste'); } if ($this->request->is('post')) { + $requestData = $this->request->data['Event']; $fail = false; $modulePayload = array( - 'module' => $module['name'], - 'event_id' => $eventId, + 'module' => $module['name'], + 'event_id' => $eventId, ); if (isset($module['meta']['config'])) { foreach ($module['meta']['config'] as $conf) { @@ -5322,11 +5536,11 @@ class EventsController extends AppController } } if ($moduleName === 'csvimport') { - if (empty($this->request->data['Event']['config']['header']) && $this->request->data['Event']['config']['has_header'] === '1') { - $this->request->data['Event']['config']['header'] = ' '; + if (empty($requestData['config']['header']) && $requestData['config']['has_header'] === '1') { + $requestData['config']['header'] = ' '; } - if (empty($this->request->data['Event']['config']['special_delimiter'])) { - $this->request->data['Event']['config']['special_delimiter'] = ' '; + if (empty($requestData['config']['special_delimiter'])) { + $requestData['config']['special_delimiter'] = ' '; } } if (isset($module['mispattributes']['userConfig'])) { @@ -5337,18 +5551,19 @@ class EventsController extends AppController $validation = true; } } else { - $validation = call_user_func_array(array($this->Module, $this->Module->configTypes[$config['type']]['validation']), array($this->request->data['Event']['config'][$configName])); + $validationMethod = Module::CONFIG_TYPES[$config['type']]['validation']; + $validation = $this->Module->{$validationMethod}($requestData['config'][$configName]); } if ($validation !== true) { $fail = ucfirst($configName) . ': ' . $validation; } else { if (isset($config['regex']) && !empty($config['regex'])) { - $fail = preg_match($config['regex'], $this->request->data['Event']['config'][$configName]) ? false : ucfirst($configName) . ': ' . 'Invalid setting' . ($config['errorMessage'] ? ' - ' . $config['errorMessage'] : ''); + $fail = preg_match($config['regex'], $requestData['config'][$configName]) ? false : ucfirst($configName) . ': ' . 'Invalid setting' . ($config['errorMessage'] ? ' - ' . $config['errorMessage'] : ''); if (!empty($fail)) { - $modulePayload['config'][$configName] = $this->request->data['Event']['config'][$configName]; + $modulePayload['config'][$configName] = $requestData['config'][$configName]; } } else { - $modulePayload['config'][$configName] = $this->request->data['Event']['config'][$configName]; + $modulePayload['config'][$configName] = $requestData['config'][$configName]; } } } @@ -5356,31 +5571,29 @@ class EventsController extends AppController } if (!$fail) { if (!empty($module['mispattributes']['inputSource'])) { - if (!isset($this->request->data['Event']['source'])) { + if (!isset($requestData['source'])) { if (in_array('paste', $module['mispattributes']['inputSource'])) { - $this->request->data['Event']['source'] = '0'; + $requestData['source'] = '0'; } else { - $this->request->data['Event']['source'] = '1'; + $requestData['source'] = '1'; } } - if ($this->request->data['Event']['source'] == '1') { - if (isset($this->request->data['Event']['data'])) { - $modulePayload['data'] = base64_decode($this->request->data['Event']['data']); - } elseif (!isset($this->request->data['Event']['fileupload']) || empty($this->request->data['Event']['fileupload'])) { - $fail = 'Invalid file upload.'; + if ($requestData['source'] == '1') { + if (isset($requestData['data'])) { + $modulePayload['data'] = base64_decode($requestData['data']); + } elseif (empty($requestData['fileupload'])) { + $fail = __('Invalid file upload.'); } else { - $fileupload = $this->request->data['Event']['fileupload']; - $tmpfile = new File($fileupload['tmp_name']); - if ((isset($fileupload['error']) && $fileupload['error'] == 0) || (!empty($fileupload['tmp_name']) && $fileupload['tmp_name'] != 'none') && is_uploaded_file($tmpfile->path)) { + $fileupload = $requestData['fileupload']; + if ((isset($fileupload['error']) && $fileupload['error'] == 0) || (!empty($fileupload['tmp_name']) && $fileupload['tmp_name'] != 'none') && is_uploaded_file($fileupload['tmp_name'])) { $filename = basename($fileupload['name']); - App::uses('FileAccessTool', 'Tools'); - $modulePayload['data'] = FileAccessTool::readFromFile($fileupload['tmp_name'], $fileupload['size']); + $modulePayload['data'] = FileAccessTool::readAndDelete($fileupload['tmp_name']); } else { - $fail = 'Invalid file upload.'; + $fail = __('Invalid file upload.'); } } } else { - $modulePayload['data'] = $this->request->data['Event']['paste']; + $modulePayload['data'] = $requestData['paste']; } } else { $modulePayload['data'] = ''; @@ -5402,13 +5615,13 @@ class EventsController extends AppController } $importComment = !empty($result['comment']) ? $result['comment'] : 'Enriched via the ' . $module['name'] . ' module'; if (!empty($module['mispattributes']['format']) && $module['mispattributes']['format'] === 'misp_standard') { - $event = $this->Event->handleMispFormatFromModuleResult($result); - $event['Event'] = array('id' => $eventId); + $resolvedEvent = $this->Event->handleMispFormatFromModuleResult($result); + $resolvedEvent['Event'] = $event['Event']; if ($this->_isRest()) { - $this->Event->processModuleResultsDataRouter($this->Auth->user(), $event, $eventId, $importComment); - return $this->RestResponse->viewData($event, $this->response->type()); + $this->Event->processModuleResultsDataRouter($this->Auth->user(), $resolvedEvent, $eventId, $importComment); + return $this->RestResponse->viewData($resolvedEvent, $this->response->type()); } - $this->set('event', $event); + $this->set('event', $resolvedEvent); $this->set('menuItem', 'importResults'); $render_name = 'resolved_misp_format'; } else { @@ -5433,15 +5646,13 @@ class EventsController extends AppController $this->set('resultArray', $resultArray); $this->set('typeDefinitions', $this->Event->Attribute->typeDefinitions); $this->set('typeCategoryMapping', $typeCategoryMapping); + $this->set('defaultAttributeDistribution', $this->Event->Attribute->defaultDistribution()); $render_name = 'resolved_attributes'; } - $distributions = $this->Event->Attribute->distributionLevels; - $sgs = $this->Event->SharingGroup->fetchAllAuthorised($this->Auth->user(), 'name', 1); - if (empty($sgs)) { - unset($distributions[4]); - } - $this->set('distributions', $distributions); - $this->set('sgs', $sgs); + + $distributionData = $this->Event->Attribute->fetchDistributionData($this->Auth->user()); + $this->set('distributions', $distributionData['levels']); + $this->set('sgs', $distributionData['sgs']); $this->set('title', __('Import Results')); $this->set('title_for_layout', __('Import Results')); $this->set('importComment', $importComment); @@ -5452,7 +5663,7 @@ class EventsController extends AppController $this->Flash->error($fail); } } - $this->set('configTypes', $this->Module->configTypes); + $this->set('configTypes', Module::CONFIG_TYPES); $this->set('module', $module); $this->set('eventId', $eventId); $this->set('event', $event); @@ -6217,4 +6428,31 @@ class EventsController extends AppController $this->render('/genericTemplates/confirm'); } } + + /** + * @param array $event + * @return void + */ + private function __setHighlightedTags($event) + { + $this->loadModel('Taxonomy'); + $highlightedTags = $this->Taxonomy->getHighlightedTags($this->Taxonomy->getHighlightedTaxonomies(), $event['EventTag']); + $this->set('highlightedTags', $highlightedTags); + } + + /** + * + * @param array $events + * @return array + */ + private function __attachHighlightedTagsToEvents($events) + { + $this->loadModel('Taxonomy'); + $highlightedTaxonomies = $this->Taxonomy->getHighlightedTaxonomies(); + foreach ($events as $k => $event) { + $events[$k]['Event']['highlightedTags'] = $this->Taxonomy->getHighlightedTags($highlightedTaxonomies, $event['EventTag']); + } + + return $events; + } } diff --git a/app/Controller/FeedsController.php b/app/Controller/FeedsController.php index b7764d5fa..5eafbf435 100644 --- a/app/Controller/FeedsController.php +++ b/app/Controller/FeedsController.php @@ -311,6 +311,7 @@ class FeedsController extends AppController ]; $this->set('allAttributeTypes', $allTypes['attribute']); $this->set('allObjectTypes', $allTypes['object']); + $this->set('supportedUrlparams', Feed::SUPPORTED_URL_PARAM_FILTERS); $this->set(compact('dropdownData')); $this->set('defaultPullRules', json_encode(Feed::DEFAULT_FEED_PULL_RULES)); $this->set('menuData', array('menuList' => 'feeds', 'menuItem' => 'add')); diff --git a/app/Controller/GalaxiesController.php b/app/Controller/GalaxiesController.php index 42898630c..58fb77eed 100644 --- a/app/Controller/GalaxiesController.php +++ b/app/Controller/GalaxiesController.php @@ -22,8 +22,16 @@ class GalaxiesController extends AppController public function index() { $aclConditions = array(); - $filters = $this->IndexFilter->harvestParameters(array('value', 'enabled')); - $searchConditions = array(); + $filterData = array( + 'request' => $this->request, + 'named_params' => $this->params['named'], + 'paramArray' => ['value', 'enabled'], + 'ordered_url_params' => [], + 'additional_delimiters' => PHP_EOL + ); + $exception = false; + $filters = $this->_harvestParameters($filterData, $exception); + $searchConditions = []; if (empty($filters['value'])) { $filters['value'] = ''; } else { @@ -220,14 +228,15 @@ class GalaxiesController extends AppController } else { $data = $this->request->data['Galaxy']; $text = FileAccessTool::getTempUploadedFile($data['submittedjson'], $data['json']); - $clusters = json_decode($text, true); - if ($clusters === null) { - throw new MethodNotAllowedException(__('Error while decoding JSON')); + try { + $clusters = JsonTool::decodeArray($text); + } catch (Exception $e) { + throw new BadRequestException(__('Error while decoding JSON')); } } $saveResult = $this->Galaxy->importGalaxyAndClusters($this->Auth->user(), $clusters); if ($saveResult['success']) { - $message = sprintf(__('Galaxy clusters imported. %s imported, %s ignored, %s failed. %s'), $saveResult['imported'], $saveResult['ignored'], $saveResult['failed'], !empty($saveResult['errors']) ? implode(', ', $saveResult['errors']) : ''); + $message = __('Galaxy clusters imported. %s imported, %s ignored, %s failed. %s', $saveResult['imported'], $saveResult['ignored'], $saveResult['failed'], !empty($saveResult['errors']) ? implode(', ', $saveResult['errors']) : ''); if ($this->_isRest()) { return $this->RestResponse->saveSuccessResponse('Galaxy', 'import', false, $this->response->type(), $message); } else { @@ -235,7 +244,7 @@ class GalaxiesController extends AppController $this->redirect(array('controller' => 'galaxies', 'action' => 'index')); } } else { - $message = sprintf(__('Could not import galaxy clusters. %s imported, %s ignored, %s failed. %s'), $saveResult['imported'], $saveResult['ignored'], $saveResult['failed'], !empty($saveResult['errors']) ? implode(', ', $saveResult['errors']) : ''); + $message = __('Could not import galaxy clusters. %s imported, %s ignored, %s failed. %s', $saveResult['imported'], $saveResult['ignored'], $saveResult['failed'], !empty($saveResult['errors']) ? implode(', ', $saveResult['errors']) : ''); if ($this->_isRest()) { return $this->RestResponse->saveFailResponse('Galaxy', 'import', false, $message); } else { @@ -525,7 +534,7 @@ class GalaxiesController extends AppController } } - $result = $this->Galaxy->attachCluster($user, $target_type, $target_id, $cluster_id, $local); + $result = $this->Galaxy->attachCluster($user, $target_type, $target, $cluster_id, $local); return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'success' => $result, 'check_publish' => true)), 'status'=>200, 'type' => 'json')); } diff --git a/app/Controller/GalaxyClustersController.php b/app/Controller/GalaxyClustersController.php index 52d5721e7..0a6c5e29f 100644 --- a/app/Controller/GalaxyClustersController.php +++ b/app/Controller/GalaxyClustersController.php @@ -37,7 +37,8 @@ class GalaxyClustersController extends AppController public function index($galaxyId) { - $filters = $this->IndexFilter->harvestParameters(array('context', 'searchall')); + $galaxyId = $this->Toolbox->findIdByUuid($this->GalaxyCluster->Galaxy, $galaxyId); + $filters = $this->_harvestParameters(array('context', 'searchall')); $aclConditions = $this->GalaxyCluster->buildConditions($this->Auth->user()); $contextConditions = array(); if (empty($filters['context'])) { @@ -47,7 +48,7 @@ class GalaxyClustersController extends AppController } if ($filters['context'] == 'default') { - $contextConditions['GalaxyCluster.default'] = true; + $contextConditions['GalaxyCluster.default'] = true; } elseif ($filters['context'] == 'custom') { $contextConditions['GalaxyCluster.default'] = false; } elseif ($filters['context'] == 'org') { @@ -146,9 +147,8 @@ class GalaxyClustersController extends AppController 'GalaxyCluster.default' => 0, ] ]); - $this->loadModel('Attribute'); - $distributionLevels = $this->Attribute->distributionLevels; - unset($distributionLevels[5]); + $this->loadModel('Event'); + $distributionLevels = $this->Event->shortDist; $this->set('distributionLevels', $distributionLevels); $this->set('list', $clusters); $this->set('galaxy_id', $galaxyId); diff --git a/app/Controller/GalaxyElementsController.php b/app/Controller/GalaxyElementsController.php index c529a19a6..c09c80fcf 100644 --- a/app/Controller/GalaxyElementsController.php +++ b/app/Controller/GalaxyElementsController.php @@ -1,6 +1,9 @@ _closeSession(); $filters = $this->IndexFilter->harvestParameters(array('context', 'searchall')); - $aclConditions = $this->GalaxyElement->buildClusterConditions($this->Auth->user(), $clusterId); + $aclConditions = $this->GalaxyElement->buildClusterConditions($user, $clusterId); if (empty($filters['context'])) { $filters['context'] = 'all'; } @@ -44,18 +48,15 @@ class GalaxyElementsController extends AppController 'context' => $filters['context'], 'searchall' => isset($filters['searchall']) ? $filters['searchall'] : '' ])); - $cluster = $this->GalaxyElement->GalaxyCluster->fetchIfAuthorized($this->Auth->user(), $clusterId, array('edit', 'delete'), false, false); + $cluster = $this->GalaxyElement->GalaxyCluster->fetchIfAuthorized($user, $clusterId, array('edit', 'delete'), false, false); $canModify = !empty($cluster['authorized']); - $canModify = true; $this->set('canModify', $canModify); - if ($filters['context'] == 'JSONView') { + if ($filters['context'] === 'JSONView') { $expanded = $this->GalaxyElement->getExpandedJSONFromElements($elements); $this->set('JSONElements', $expanded); } - if ($this->request->is('ajax')) { - $this->layout = false; - $this->render('ajax/index'); - } + $this->layout = false; + $this->render('ajax/index'); } public function delete($elementId) diff --git a/app/Controller/LogsController.php b/app/Controller/LogsController.php index 13ee4c3de..b86db15cb 100644 --- a/app/Controller/LogsController.php +++ b/app/Controller/LogsController.php @@ -1,7 +1,9 @@ request->params['action']) { + if ('admin_search' === $this->request->params['action']) { $this->Security->csrfCheck = false; } } - public function admin_index() + public function index() { $paramArray = array('id', 'title', 'created', 'model', 'model_id', 'action', 'user_id', 'change', 'email', 'org', 'description', 'ip'); $filterData = array( 'request' => $this->request, - 'named_params' => $this->params['named'], + 'named_params' => $this->request->params['named'], 'paramArray' => $paramArray, 'ordered_url_params' => func_get_args() ); $exception = false; $filters = $this->_harvestParameters($filterData, $exception); unset($filterData); + if ($this->_isRest()) { if ($filters === false) { return $exception; @@ -71,8 +74,14 @@ class LogsController extends AppController } } if (!$this->_isSiteAdmin()) { - $orgRestriction = $this->Auth->user('Organisation')['name']; - $conditions['AND']['Log.org'] = $orgRestriction; + if ($this->_isAdmin()) { + // ORG admins can see their own org info + $orgRestriction = $this->Auth->user('Organisation')['name']; + $conditions['Log.org'] = $orgRestriction; + } else { + // users can see their own info + $conditions['Log.user_id'] = $this->Auth->user('id'); + } } $params = array( 'conditions' => $conditions, @@ -86,30 +95,42 @@ class LogsController extends AppController } $log_entries = $this->Log->find('all', $params); return $this->RestResponse->viewData($log_entries, 'json'); - } else { - $this->set('isSearch', 0); - $this->recursive = 0; - $validFilters = $this->Log->logMeta; - if (!$this->_isSiteAdmin()) { - $orgRestriction = $this->Auth->user('Organisation')['name']; - $conditions['Log.org'] = $orgRestriction; - $this->paginate['conditions'] = $conditions; - } else { - $validFilters = array_merge_recursive($validFilters, $this->Log->logMetaAdmin); - } - if (isset($this->params['named']['filter']) && in_array($this->params['named']['filter'], array_keys($validFilters))) { - $this->paginate['conditions']['Log.action'] = $validFilters[$this->params['named']['filter']]['values']; - } - foreach ($filters as $key => $value) { - if ($key === 'created') { - $key = 'created >='; - } - $this->paginate['conditions']["Log.$key"] = $value; - } - $this->set('validFilters', $validFilters); - $this->set('filter', isset($this->params['named']['filter']) ? $this->params['named']['filter'] : false); - $this->set('list', $this->paginate()); } + + $this->set('isSearch', 0); + $this->recursive = 0; + $validFilters = $this->Log->logMeta; + if ($this->_isSiteAdmin()) { + $validFilters = array_merge_recursive($validFilters, $this->Log->logMetaAdmin); + } + else if (!$this->_isSiteAdmin() && $this->_isAdmin()) { + // ORG admins can see their own org info + $orgRestriction = $this->Auth->user('Organisation')['name']; + $conditions['Log.org'] = $orgRestriction; + $this->paginate['conditions'] = $conditions; + } else { + // users can see their own info + $conditions['Log.email'] = $this->Auth->user('email'); + $this->paginate['conditions'] = $conditions; + } + if (isset($this->params['named']['filter']) && in_array($this->params['named']['filter'], array_keys($validFilters))) { + $this->paginate['conditions']['Log.action'] = $validFilters[$this->params['named']['filter']]['values']; + } + foreach ($filters as $key => $value) { + if ($key === 'created') { + $key = 'created >='; + } + $this->paginate['conditions']["Log.$key"] = $value; + } + $this->set('validFilters', $validFilters); + $this->set('filter', isset($this->params['named']['filter']) ? $this->params['named']['filter'] : false); + $this->set('list', $this->paginate()); + } + + public function admin_index() + { + $this->view = 'index'; + return $this->index(); } // Shows a minimalistic history for the currently selected event @@ -313,7 +334,7 @@ class LogsController extends AppController } // set the same view as the index page - $this->render('admin_index'); + $this->render('index'); } } else { // get from Session @@ -356,7 +377,7 @@ class LogsController extends AppController $this->set('list', $list); // set the same view as the index page - $this->render('admin_index'); + $this->render('index'); } } else { // no search keyword is given, show the search form diff --git a/app/Controller/NewsController.php b/app/Controller/NewsController.php index e7ce8c257..f4df8cbd2 100755 --- a/app/Controller/NewsController.php +++ b/app/Controller/NewsController.php @@ -1,6 +1,9 @@ 5, 'maxLimit' => 9999, // LATER we will bump here on a problem once we have more than 9999 events <- no we won't, this is the max a user van view/page. - 'order' => array( + 'order' => [ 'News.id' => 'DESC' - ), + ], + 'contain' => [ + 'User' => ['fields' => ['User.email']], + ] ); public function index() { - $this->paginate['contain'] = array('User' => array('fields' => array('User.email'))); + $user = $this->Auth->user(); $newsItems = $this->paginate(); - $newsread = $this->Auth->user('newsread'); - foreach ($newsItems as $key => $item) { - if ($item['News']['date_created'] > $newsread) { - $newsItems[$key]['News']['new'] = true; - } else { - $newsItems[$key]['News']['new'] = false; + $newsread = $user['newsread']; + $hasUnreadNews = false; + foreach ($newsItems as &$item) { + $isNew = $item['News']['date_created'] > $newsread; + $item['News']['new'] = $isNew; + if ($isNew) { + $hasUnreadNews = true; } } $this->set('newsItems', $newsItems); + $this->set('hasUnreadNews', $hasUnreadNews); - $this->loadModel('User'); - $this->User->updateField($this->Auth->user(), 'newsread', time()); + if ($hasUnreadNews) { + $homepage = $this->User->UserSetting->getValueForUser($user['id'], 'homepage'); + if (!empty($homepage)) { + $this->set('homepage', $homepage); + } else { + $this->set('homepage', "{$this->baseurl}/events/index"); + } + + $this->User->updateField($user, 'newsread', time()); + } + } + + public function admin_index() + { + $user = $this->Auth->user(); + $this->paginate['limit'] = 25; + $newsItems = $this->paginate(); + + $this->set('newsItems', $newsItems); + $this->set('user', $user); } public function add() @@ -74,7 +100,6 @@ class NewsController extends AppController public function delete($id) { - $this->defaultModel = 'News'; $this->CRUD->delete($id); if ($this->IndexFilter->isRest()) { return $this->restResponsePayload; diff --git a/app/Controller/ObjectTemplatesController.php b/app/Controller/ObjectTemplatesController.php index f1f52f338..f536b978b 100644 --- a/app/Controller/ObjectTemplatesController.php +++ b/app/Controller/ObjectTemplatesController.php @@ -22,9 +22,10 @@ class ObjectTemplatesController extends AppController public function beforeFilter() { parent::beforeFilter(); - if (in_array($this->request->action, ['objectMetaChoice', 'objectChoice'], true)) { + if (in_array($this->request->action, ['objectMetaChoice', 'objectChoice', 'possibleObjectTemplates'], true)) { $this->Security->doNotGenerateToken = true; } + $this->Security->unlockedActions[] = 'possibleObjectTemplates'; } public function objectMetaChoice($eventId) @@ -162,16 +163,6 @@ class ObjectTemplatesController extends AppController $this->redirect($this->referer()); } - public function viewElements($id, $context = 'all') - { - $elements = $this->ObjectTemplate->ObjectTemplateElement->find('all', array( - 'conditions' => array('ObjectTemplateElement.object_template_id' => $id) - )); - $this->set('list', $elements); - $this->layout = false; - $this->render('ajax/view_elements'); - } - public function index($all = false) { $passedArgsArray = array(); @@ -183,11 +174,12 @@ class ObjectTemplatesController extends AppController $this->set('all', true); } if (!empty($this->params['named']['searchall'])) { + $searchTerm = '%' . strtolower($this->request->params['named']['searchall']) . '%'; $this->paginate['conditions']['AND']['OR'] = array( - 'ObjectTemplate.uuid LIKE' => '%' . strtolower($this->params['named']['searchall']) . '%', - 'LOWER(ObjectTemplate.name) LIKE' => '%' . strtolower($this->params['named']['searchall']) . '%', - 'ObjectTemplate.meta-category LIKE' => '%' . strtolower($this->params['named']['searchall']) . '%', - 'LOWER(ObjectTemplate.description) LIKE' => '%' . strtolower($this->params['named']['searchall']) . '%' + 'ObjectTemplate.uuid LIKE' => $searchTerm, + 'LOWER(ObjectTemplate.name) LIKE' => $searchTerm, + 'ObjectTemplate.meta-category LIKE' => $searchTerm, + 'LOWER(ObjectTemplate.description) LIKE' => $searchTerm, ); } if ($this->_isRest()) { @@ -196,11 +188,11 @@ class ObjectTemplatesController extends AppController unset($rules['order']); $objectTemplates = $this->ObjectTemplate->find('all', $rules); return $this->RestResponse->viewData($objectTemplates, $this->response->type()); - } else { - $this->paginate['order'] = array('ObjectTemplate.name' => 'ASC'); - $objectTemplates = $this->paginate(); - $this->set('list', $objectTemplates); } + + $this->paginate['order'] = array('ObjectTemplate.name' => 'ASC'); + $objectTemplates = $this->paginate(); + $this->set('list', $objectTemplates); $this->set('passedArgs', json_encode($passedArgs)); $this->set('passedArgsArray', $passedArgsArray); } @@ -315,4 +307,28 @@ class ObjectTemplatesController extends AppController } return $this->RestResponse->viewData($template, $this->response->type()); } + + public function possibleObjectTemplates() + { + session_abort(); + $this->request->allowMethod(['post']); + + $attributeTypes = $this->request->data['attributeTypes']; + $templates = $this->ObjectTemplate->fetchPossibleTemplatesBasedOnTypes($attributeTypes)['templates']; + + $results = []; + foreach ($templates as $template) { + $template = $template['ObjectTemplate']; + if ($template['compatibility'] === true && empty($template['invalidTypes'])) { + $results[] = [ + 'id' => $template['id'], + 'name' => $template['name'], + 'description' => $template['description'], + 'meta-category' => $template['meta-category'], + ]; + } + } + + return $this->RestResponse->viewData($results, 'json'); + } } diff --git a/app/Controller/ObjectsController.php b/app/Controller/ObjectsController.php index b5faafaa5..c58aca671 100644 --- a/app/Controller/ObjectsController.php +++ b/app/Controller/ObjectsController.php @@ -60,33 +60,6 @@ class ObjectsController extends AppController $sgs = $this->MispObject->SharingGroup->fetchAllAuthorised($this->Auth->user(), 'name', false, array_keys($sharing_groups)); $this->set('sharing_groups', $sgs); } - $multiple_template_elements = Hash::extract($template['ObjectTemplateElement'], sprintf('{n}[multiple=true]')); - $multiple_attribute_allowed = array(); - foreach ($multiple_template_elements as $template_element) { - $relation_type = $template_element['object_relation'] . ':' . $template_element['type']; - $multiple_attribute_allowed[$relation_type] = true; - } - $this->set('multiple_attribute_allowed', $multiple_attribute_allowed); - // try to fetch similar objects - $cur_attrs = Hash::extract($this->request->data, 'Attribute.{n}.value'); - $conditions = array( - 'event_id' => $event_id, - 'value1' => $cur_attrs, - 'object_id !=' => '0' - ); - $similar_objects = $this->MispObject->Attribute->find('all', array( - 'conditions' => $conditions, - 'recursive' => -1, - 'fields' => 'object_id, count(object_id) as similarity_amount', - 'group' => 'object_id', - 'order' => 'similarity_amount DESC' - )); - $similar_object_ids = array(); - $similar_object_similarity_amount = array(); - foreach ($similar_objects as $obj) { - $similar_object_ids[] = $obj['Attribute']['object_id']; - $similar_object_similarity_amount[$obj['Attribute']['object_id']] = $obj[0]['similarity_amount']; - } if (isset($this->request->data['Attribute'])) { foreach ($this->request->data['Attribute'] as &$attribute) { @@ -113,33 +86,35 @@ class ObjectsController extends AppController 'cur_object_tmp_uuid' => $curObjectTmpUuid, 'data' => $this->request->data )); - if (!empty($similar_object_ids)) { - $this->set('similar_objects_count', count($similar_object_ids)); - $similar_object_ids = array_slice($similar_object_ids, 0, $similar_objects_display_threshold); // slice to honor the threshold - $similar_objects = $this->MispObject->fetchObjects($this->Auth->user(), array( - 'conditions' => array( - 'Object.id' => $similar_object_ids, - 'Object.template_uuid' => $template['ObjectTemplate']['uuid'] - ) - )); - foreach ($similar_objects as $key => $obj) { - $similar_objects[$key]['Object']['similarity_amount'] = $similar_object_similarity_amount[$obj['Object']['id']]; // sorting function cannot use external variables - } - usort($similar_objects, function ($a, $b) { // fetch Object returns object sorted by IDs, force the sort by the similarity amount - if ($a['Object']['similarity_amount'] == $b['Object']['similarity_amount']) { - return 0; + + if ($action === 'add') { + list($similar_objects_count, $similar_objects, $simple_flattened_attribute, $simple_flattened_attribute_noval) = $this->MispObject->findSimilarObjects( + $this->Auth->user(), + $event_id, + $this->request->data['Attribute'], + $template, + $similar_objects_display_threshold + ); + if ($similar_objects_count) { + $this->set('similar_objects_count', $similar_objects_count); + $this->set('similar_objects', $similar_objects); + $this->set('similar_objects_display_threshold', $similar_objects_display_threshold); + $this->set('simple_flattened_attribute', $simple_flattened_attribute); + $this->set('simple_flattened_attribute_noval', $simple_flattened_attribute_noval); + + $multiple_template_elements = Hash::extract($template['ObjectTemplateElement'],'{n}[multiple=true]'); + $multiple_attribute_allowed = array(); + foreach ($multiple_template_elements as $template_element) { + $relation_type = $template_element['object_relation'] . ':' . $template_element['type']; + $multiple_attribute_allowed[$relation_type] = true; } - return ($a['Object']['similarity_amount'] > $b['Object']['similarity_amount']) ? -1 : 1; - }); - $this->set('similar_objects', $similar_objects); - $this->set('similar_object_similarity_amount', $similar_object_similarity_amount); - $this->set('similar_objects_display_threshold', $similar_objects_display_threshold); + $this->set('multiple_attribute_allowed', $multiple_attribute_allowed); + } } } - /** - * Create an object using a template + * Create an object using a template * POSTing will take the input and validate it against the template * GETing will return the template */ @@ -343,10 +318,16 @@ class ObjectsController extends AppController $template = $this->MispObject->prepareTemplate($template); $element = array(); foreach ($template['ObjectTemplateElement'] as $templateElement) { - if ($templateElement['object_relation'] == $object_relation) { + if ($templateElement['object_relation'] === $object_relation) { $element = $templateElement; + break; } } + + if (empty($element)) { + throw new NotFoundException(__("Object template do not contains object relation $object_relation")); + } + $distributionData = $this->MispObject->Event->Attribute->fetchDistributionData($this->Auth->user()); $this->layout = false; $this->set('distributionData', $distributionData); @@ -431,7 +412,7 @@ class ObjectsController extends AppController $savedObject = array(); if (!is_numeric($objectToSave)) { $object_validation_errors = array(); - foreach($objectToSave as $field => $field_errors) { + foreach ($objectToSave as $field => $field_errors) { $object_validation_errors[] = sprintf('%s: %s', $field, implode(', ', $field_errors)); } $error_message = __('Object could not be saved.') . PHP_EOL . implode(PHP_EOL, $object_validation_errors); @@ -458,12 +439,10 @@ class ObjectsController extends AppController return $this->RestResponse->saveFailResponse('Objects', 'edit', false, $id, $this->response->type()); } } else { - $message = __('Object attributes saved.'); if ($this->request->is('ajax')) { - $this->autoRender = false; if (is_numeric($objectToSave)) { $this->MispObject->Event->unpublishEvent($event); - return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'success' => $message)), 'status'=>200, 'type' => 'json')); + return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'success' => __('Object attributes saved.'))), 'status'=>200, 'type' => 'json')); } else { return new CakeResponse(array('body'=> json_encode(array('saved' => true, 'errors' => $error_message)), 'status'=>200, 'type' => 'json')); } @@ -597,7 +576,7 @@ class ObjectsController extends AppController $object = $object[0]; $result = $object['Object'][$field]; if ($field === 'distribution') { - $result = $this->MispObject->shortDist[$result]; + $this->set('shortDist', $this->MispObject->Attribute->shortDist); } $this->set('value', $result); $this->set('field', $field); @@ -634,7 +613,7 @@ class ObjectsController extends AppController throw new NotFoundException(__('Invalid object')); } $this->layout = false; - if ($field == 'distribution') { + if ($field === 'distribution') { $distributionLevels = $this->MispObject->shortDist; unset($distributionLevels[4]); $this->set('distributionLevels', $distributionLevels); @@ -732,7 +711,7 @@ class ObjectsController extends AppController 'fields' => array('template_uuid', 'template_version', 'id', 'event_id'), 'flatten' => 1, 'contain' => array( - 'Event' + 'Event' => ['fields' => ['id', 'user_id', 'org_id', 'orgc_id']] ) ); // fetchObjects restrict access based on user @@ -1137,23 +1116,8 @@ class ObjectsController extends AppController $selectedAttributes = $this->_jsonDecode($selectedAttributes); $res = $this->MispObject->validObjectsFromAttributeTypes($this->Auth->user(), $eventId, $selectedAttributes); - $potentialTemplates = $res['templates']; - $attributeTypes = $res['types']; - usort($potentialTemplates, function($a, $b) { - if ($a['ObjectTemplate']['id'] == $b['ObjectTemplate']['id']) { - return 0; - } else if (is_array($a['ObjectTemplate']['compatibility']) && is_array($b['ObjectTemplate']['compatibility'])) { - return count($a['ObjectTemplate']['compatibility']) > count($b['ObjectTemplate']['compatibility']) ? 1 : -1; - } else if (is_array($a['ObjectTemplate']['compatibility']) && !is_array($b['ObjectTemplate']['compatibility'])) { - return 1; - } else if (!is_array($a['ObjectTemplate']['compatibility']) && is_array($b['ObjectTemplate']['compatibility'])) { - return -1; - } else { // sort based on invalidTypes count - return count($a['ObjectTemplate']['invalidTypes']) > count($b['ObjectTemplate']['invalidTypes']) ? 1 : -1; - } - }); - $this->set('potential_templates', $potentialTemplates); - $this->set('selected_types', $attributeTypes); + $this->set('potential_templates', $res['templates']); + $this->set('selected_types', $res['types']); $this->set('event_id', $eventId); } @@ -1230,7 +1194,8 @@ class ObjectsController extends AppController if (empty($template)) { throw new NotFoundException(__('Invalid template.')); } - $conformity_result = $this->MispObject->ObjectTemplate->checkTemplateConformityBasedOnTypes($template, $selected_attributes); + $attributeTypes = array_column(array_column($selected_attributes, 'Attribute'), 'type'); + $conformity_result = $this->MispObject->ObjectTemplate->checkTemplateConformityBasedOnTypes($template, $attributeTypes); $skipped_attributes = 0; foreach ($selected_attributes as $i => $attribute) { if (in_array($attribute['Attribute']['type'], $conformity_result['invalidTypes'], true)) { @@ -1253,8 +1218,18 @@ class ObjectsController extends AppController )); foreach ($object_references as $i => $object_reference) { - $temp_object = $this->MispObject->find('first', array('id' => $object_reference['ObjectReference']['object_id'], 'recursive' => -1)); - $temp_attribute = $this->MispObject->Attribute->find('first', array('id' => $object_reference['ObjectReference']['referenced_id'], 'recursive' => -1)); + $temp_object = $this->MispObject->find('first', [ + 'conditions' => [ + 'id' => $object_reference['ObjectReference']['object_id'] + ], + 'recursive' => -1 + ]); + $temp_attribute = $this->MispObject->Attribute->find('first', [ + 'conditions' => [ + 'id' => $object_reference['ObjectReference']['referenced_id'], + ], + 'recursive' => -1 + ]); if (!empty($temp_object) && !empty($temp_attribute)) { $temp_object = $temp_object['Object']; $temp_attribute = $temp_attribute['Attribute']; @@ -1278,6 +1253,151 @@ class ObjectsController extends AppController } } + public function createFromFreetext($eventId) + { + $this->request->allowMethod(['post']); + + $event = $this->MispObject->Event->find('first', array( + 'recursive' => -1, + 'fields' => array('Event.id', 'Event.uuid', 'Event.orgc_id', 'Event.user_id', 'Event.publish_timestamp'), + 'conditions' => array('Event.id' => $eventId) + )); + if (empty($event)) { + throw new NotFoundException(__('Invalid event.')); + } + if (!$this->__canModifyEvent($event)) { + throw new ForbiddenException(__('You do not have permission to do that.')); + } + + $requestData = $this->request->data['Object']; + $selectedTemplateId = $requestData['selectedTemplateId']; + $template = $this->MispObject->ObjectTemplate->find('first', array( + 'recursive' => -1, + 'conditions' => array( + 'ObjectTemplate.id' => $selectedTemplateId, + 'ObjectTemplate.active' => true, + ), + 'contain' => ['ObjectTemplateElement'], + )); + if (empty($template)) { + throw new NotFoundException(__('Invalid template.')); + } + + if (isset($requestData['selectedObjectRelationMapping'])) { + $distribution = $requestData['distribution']; + $sharingGroupId = $requestData['sharing_group_id'] ?? 0; + $comment = $requestData['comment']; + if ($distribution == 4) { + $sg = $this->MispObject->SharingGroup->fetchSG($sharingGroupId, $this->Auth->user()); + if (empty($sg)) { + throw new NotFoundException(__('Invalid sharing group.')); + } + } else { + $sharingGroupId = 0; + } + + $attributes = $this->_jsonDecode($requestData['attributes']); + $selectedObjectRelationMapping = $this->_jsonDecode($requestData['selectedObjectRelationMapping']); + + // Attach object relation to attributes and fix tag format + foreach ($attributes as $k => &$attribute) { + $attribute['object_relation'] = $selectedObjectRelationMapping[$k]; + if (!empty($attribute['tags'])) { + $attribute['Tag'] = []; + foreach (explode(",", $attribute['tags']) as $tagName) { + $attribute['Tag'][] = [ + 'name' => trim($tagName), + ]; + } + unset($attribute['tags']); + } + } + + $object = [ + 'Object' => [ + 'event_id' => $eventId, + 'distribution' => $distribution, + 'sharing_group_id' => $sharingGroupId, + 'comment' => $comment, + 'Attribute' => $attributes, + ], + ]; + + $object = $this->MispObject->fillObjectDataFromTemplate($object, $template); + $result = $this->MispObject->captureObject($object, $eventId, $this->Auth->user(), true, false, $event); + if ($result === true) { + return $this->RestResponse->saveSuccessResponse('Objects', 'Created from Attributes', $result, 'json'); + } else { + $error = __('Failed to create an Object from Attributes. Error: ') . PHP_EOL . h($result); + return $this->RestResponse->saveFailResponse('Objects', 'Created from Attributes', false, $error, 'json'); + } + } else { + $attributes = $this->_jsonDecode($requestData['attributes']); + + $processedAttributes = []; + foreach ($attributes as $attribute) { + if ($attribute['type'] === 'ip-src/ip-dst') { + $types = array('ip-src', 'ip-dst'); + } elseif ($attribute['type'] === 'ip-src|port/ip-dst|port') { + $types = array('ip-src|port', 'ip-dst|port'); + } else { + $types = [$attribute['type']]; + } + foreach ($types as $type) { + $attribute['type'] = $type; + $processedAttributes[] = $attribute; + } + } + + $attributeTypes = array_column($processedAttributes, 'type'); + $conformityResult = $this->MispObject->ObjectTemplate->checkTemplateConformityBasedOnTypes($template, $attributeTypes); + + if ($conformityResult['valid'] !== true || !empty($conformityResult['invalidTypes'])) { + throw new NotFoundException(__('Invalid template.')); + } + + $objectRelations = []; + foreach ($template['ObjectTemplateElement'] as $templateElement) { + $objectRelations[$templateElement['type']][] = $templateElement; + } + + // Attach first object_relation according to attribute type that will be considered as default + foreach ($processedAttributes as &$attribute) { + $attribute['object_relation'] = $objectRelations[$attribute['type']][0]['object_relation']; + } + + $distributionData = $this->MispObject->Event->Attribute->fetchDistributionData($this->Auth->user()); + $this->set('event', $event); + $this->set('distributionData', $distributionData); + $this->set('distributionLevels', $this->MispObject->Attribute->distributionLevels); + $this->set('template', $template); + $this->set('objectRelations', $objectRelations); + $this->set('attributes', $processedAttributes); + + list($similar_objects_count, $similar_objects, $simple_flattened_attribute, $simple_flattened_attribute_noval) = $this->MispObject->findSimilarObjects( + $this->Auth->user(), + $eventId, + $processedAttributes, + $template + ); + if ($similar_objects_count) { + $this->set('similar_objects_count', $similar_objects_count); + $this->set('similar_objects', $similar_objects); + $this->set('similar_objects_display_threshold', 15); + $this->set('simple_flattened_attribute', $simple_flattened_attribute); + $this->set('simple_flattened_attribute_noval', $simple_flattened_attribute_noval); + + $multiple_template_elements = Hash::extract($template['ObjectTemplateElement'],'{n}[multiple=true]'); + $multiple_attribute_allowed = array(); + foreach ($multiple_template_elements as $template_element) { + $relation_type = $template_element['object_relation'] . ':' . $template_element['type']; + $multiple_attribute_allowed[$relation_type] = true; + } + $this->set('multiple_attribute_allowed', $multiple_attribute_allowed); + } + } + } + private function __objectIdToConditions($id) { if (is_numeric($id)) { diff --git a/app/Controller/ServersController.php b/app/Controller/ServersController.php index d5ab9f747..67699cbf4 100644 --- a/app/Controller/ServersController.php +++ b/app/Controller/ServersController.php @@ -26,7 +26,7 @@ class ServersController extends AppController 'fields' => array('RemoteOrg.name', 'RemoteOrg.id'), ), ), - 'maxLimit' => 9999, // LATER we will bump here on a problem once we have more than 9999 events + 'maxLimit' => 9999, 'order' => array( 'Server.priority' => 'ASC' ), @@ -57,6 +57,14 @@ class ServersController extends AppController unset($fields['authkey']); $fields = array_keys($fields); + $filters = $this->IndexFilter->harvestParameters(['search']); + $conditions = []; + if (!empty($filters['search'])) { + $strSearch = '%' . trim(strtolower($filters['search'])) . '%'; + $conditions['OR'][]['LOWER(Server.name) LIKE'] = $strSearch; + $conditions['OR'][]['LOWER(Server.url) LIKE'] = $strSearch; + } + if ($this->_isRest()) { $params = array( 'fields' => $fields, @@ -72,12 +80,14 @@ class ServersController extends AppController 'fields' => array('RemoteOrg.id', 'RemoteOrg.name', 'RemoteOrg.uuid', 'RemoteOrg.nationality', 'RemoteOrg.sector', 'RemoteOrg.type'), ), ), + 'conditions' => $conditions, ); $servers = $this->Server->find('all', $params); $servers = $this->Server->attachServerCacheTimestamps($servers); return $this->RestResponse->viewData($servers, $this->response->type()); } else { $this->paginate['fields'] = $fields; + $this->paginate['conditions'] = $conditions; $servers = $this->paginate(); $servers = $this->Server->attachServerCacheTimestamps($servers); $this->set('servers', $servers); @@ -905,30 +915,52 @@ class ServersController extends AppController App::uses('File', 'Utility'); App::uses('Folder', 'Utility'); App::uses('FileAccessTool', 'Tools'); + App::uses('SyncTool', 'Tools'); if (isset($server['Server'][$subm]['name'])) { if ($this->request->data['Server'][$subm]['size'] != 0) { if (!$this->Server->checkFilename($server['Server'][$subm]['name'])) { throw new Exception(__('Filename not allowed')); } - $file = new File($server['Server'][$subm]['name']); - $ext = $file->ext(); + + if (!is_uploaded_file($server['Server'][$subm]['tmp_name'])) { + throw new Exception(__('File not uploaded correctly')); + } + + $ext = pathinfo($server['Server'][$subm]['name'], PATHINFO_EXTENSION); + if (!in_array($ext, SyncTool::ALLOWED_CERT_FILE_EXTENSIONS)) { + $this->Flash->error(__('Invalid extension.')); + $this->redirect(array('action' => 'index')); + } + if (!$server['Server'][$subm]['size'] > 0) { $this->Flash->error(__('Incorrect extension or empty file.')); $this->redirect(array('action' => 'index')); } - // read pem file data - $pemData = FileAccessTool::readFromFile($server['Server'][$subm]['tmp_name'], $server['Server'][$subm]['size']); + // read certificate file data + $certData = FileAccessTool::readFromFile($server['Server'][$subm]['tmp_name'], $server['Server'][$subm]['size']); } else { return true; } } else { - $pemData = base64_decode($server['Server'][$subm]); + $ext = 'pem'; + $certData = base64_decode($server['Server'][$subm]); } + + // check if the file is a valid x509 certificate + try { + $cert = openssl_x509_parse($certData); + if (!$cert) { + throw new Exception(__('Invalid certificate.')); + } + } catch (Exception $e) { + $this->Flash->error(__('Invalid certificate.')); + $this->redirect(array('action' => 'index')); + } + $destpath = APP . "files" . DS . "certs" . DS; - $dir = new Folder(APP . "files" . DS . "certs", true); $pemfile = new File($destpath . $id . $ins . '.' . $ext); - $result = $pemfile->write($pemData); + $result = $pemfile->write($certData); $s = $this->Server->read(null, $id); $s['Server'][$attr] = $s['Server']['id'] . $ins . '.' . $ext; if ($result) { @@ -1075,6 +1107,9 @@ class ServersController extends AppController $this->set('correlation_metrics', $correlation_metrics); } if ($tab === 'files') { + if (!empty(Configure::read('Security.disable_instance_file_uploads'))) { + throw new MethodNotAllowedException(__('This functionality is disabled.')); + } $files = $this->Server->grabFiles(); $this->set('files', $files); } @@ -1624,6 +1659,9 @@ class ServersController extends AppController if (!$this->request->is('post')) { throw new MethodNotAllowedException(); } + if (!empty(Configure::read('Security.disable_instance_file_uploads'))) { + throw new MethodNotAllowedException(__('Feature disabled.')); + } $validItems = $this->Server->getFileRules(); // Check if there were problems with the file upload @@ -1685,8 +1723,9 @@ class ServersController extends AppController if (!function_exists('getallheaders')) { $headers = []; foreach ($_SERVER as $name => $value) { - if (substr($name, 0, 5) === 'HTTP_') { - $headers[strtolower(str_replace('_', '-', substr($name, 5)))] = $value; + $name = strtolower($name); + if (substr($name, 0, 5) === 'http_') { + $headers[str_replace('_', '-', substr($name, 5))] = $value; } } } else { @@ -1719,6 +1758,7 @@ class ServersController extends AppController if (!$server) { throw new NotFoundException(__('Invalid server')); } + @session_write_close(); // close session to allow concurrent requests $result = $this->Server->runConnectionTest($server); if ($result['status'] == 1) { if (isset($result['info']['version']) && preg_match('/^[0-9]+\.+[0-9]+\.[0-9]+$/', $result['info']['version'])) { diff --git a/app/Controller/ShadowAttributesController.php b/app/Controller/ShadowAttributesController.php index 48b448091..bfaf2be34 100644 --- a/app/Controller/ShadowAttributesController.php +++ b/app/Controller/ShadowAttributesController.php @@ -62,8 +62,10 @@ class ShadowAttributesController extends AppController // If the old_id is set to anything but 0 then we're dealing with a proposed edit to an existing attribute if ($shadow['old_id'] != 0) { // Find the live attribute by the shadow attribute's uuid, so we can begin editing it - $this->Attribute->contain = 'Event'; - $activeAttribute = $this->Attribute->findByUuid($shadow['uuid']); + $activeAttribute = $this->Attribute->find('first', [ + 'conditions' => ['Attribute.uuid' => $shadow['uuid']], + 'contain' => ['Event'], + ]); // Send those away that shouldn't be able to edit this if (!$this->__canModifyEvent($activeAttribute)) { @@ -76,7 +78,7 @@ class ShadowAttributesController extends AppController } if (isset($shadow['proposal_to_delete']) && $shadow['proposal_to_delete']) { - $this->Attribute->delete($activeAttribute['Attribute']['id']); + $this->Attribute->deleteAttribute($activeAttribute['Attribute']['id'], $this->Auth->user(), false); } else { // Update the live attribute with the shadow data $fieldsToUpdate = array('value1', 'value2', 'value', 'type', 'category', 'comment', 'to_ids', 'first_seen', 'last_seen'); @@ -597,8 +599,8 @@ class ShadowAttributesController extends AppController // if any of these fields is set, it will create a proposal public function edit($id = null) { - $existingAttribute = $this->ShadowAttribute->Event->Attribute->fetchAttributes($this->Auth->user(), array( - 'contain' => array('Event' => array('fields' => array('Event.id', 'Event.orgc_id', 'Event.org_id', 'Event.distribution', 'Event.uuid'))), + $existingAttribute = $this->ShadowAttribute->Attribute->fetchAttributes($this->Auth->user(), array( + 'contain' => ['Event' => ['fields' => ['Event.id', 'Event.orgc_id', 'Event.org_id', 'Event.distribution', 'Event.uuid', 'Event.user_id']]], 'conditions' => $this->__attributeIdToConditions($id), 'flatten' => 1 )); @@ -673,9 +675,9 @@ class ShadowAttributesController extends AppController $sa = $this->ShadowAttribute->find( 'first', array( - 'conditions' => array('ShadowAttribute.id' => $this->ShadowAttribute->id), - 'recursive' => -1, - 'fields' => array('id', 'old_id', 'event_id', 'type', 'category', 'value', 'comment','to_ids', 'uuid', 'event_org_id', 'email', 'deleted', 'timestamp', 'first_seen', 'last_seen') + 'conditions' => array('ShadowAttribute.id' => $this->ShadowAttribute->id), + 'recursive' => -1, + 'fields' => array('id', 'old_id', 'event_id', 'type', 'category', 'value', 'comment','to_ids', 'uuid', 'event_org_id', 'email', 'deleted', 'timestamp', 'first_seen', 'last_seen') ) ); $this->set('ShadowAttribute', $sa['ShadowAttribute']); @@ -904,7 +906,24 @@ class ShadowAttributesController extends AppController } $params = array( 'conditions' => $conditions, - 'fields' => array('ShadowAttribute.id', 'ShadowAttribute.old_id', 'ShadowAttribute.event_id', 'ShadowAttribute.type', 'ShadowAttribute.category', 'ShadowAttribute.uuid', 'ShadowAttribute.to_ids', 'ShadowAttribute.value', 'ShadowAttribute.comment', 'ShadowAttribute.org_id', 'ShadowAttribute.timestamp', 'ShadowAttribute.first_seen', 'ShadowAttribute.last_seen'), + 'fields' => array( + 'ShadowAttribute.id', + 'ShadowAttribute.old_id', + 'ShadowAttribute.event_id', + 'ShadowAttribute.type', + 'ShadowAttribute.category', + 'ShadowAttribute.uuid', + 'ShadowAttribute.to_ids', + 'ShadowAttribute.value', + 'ShadowAttribute.comment', + 'ShadowAttribute.org_id', + 'ShadowAttribute.timestamp', + 'ShadowAttribute.first_seen', + 'ShadowAttribute.last_seen', + 'ShadowAttribute.deleted', + 'ShadowAttribute.proposal_to_delete', + 'ShadowAttribute.disable_correlation' + ), 'contain' => array( 'Event' => array( 'fields' => array('id', 'org_id', 'info', 'orgc_id', 'uuid'), diff --git a/app/Controller/SightingsController.php b/app/Controller/SightingsController.php index bb5259072..7ba059918 100644 --- a/app/Controller/SightingsController.php +++ b/app/Controller/SightingsController.php @@ -66,7 +66,8 @@ class SightingsController extends AppController $filters = !empty($this->request->data['filters']) ? $this->request->data['filters'] : false; } if (!$error) { - $result = $this->Sighting->saveSightings($id, $values, $timestamp, $this->Auth->user(), $type, $source, false, true, false, $filters); + $publish_sighting = !empty(Configure::read('Sightings_enable_realtime_publish')); + $result = $this->Sighting->saveSightings($id, $values, $timestamp, $this->Auth->user(), $type, $source, false, $publish_sighting, false, $filters); } if (!is_numeric($result)) { $error = $result; diff --git a/app/Controller/TagsController.php b/app/Controller/TagsController.php index 89bec4f2d..d1cabe53f 100644 --- a/app/Controller/TagsController.php +++ b/app/Controller/TagsController.php @@ -60,7 +60,7 @@ class TagsController extends AppController $this->paginate['conditions']['AND'][] = ['LOWER(Tag.name) LIKE' => '%' . strtolower($passedArgsArray['searchall']) . '%']; } foreach (['name', 'filter', 'search'] as $f) { - if (!empty($passedArgsArray['name'])) { + if (!empty($passedArgsArray[$f])) { $this->paginate['conditions']['AND'][] = ['LOWER(Tag.name)' => strtolower($passedArgsArray[$f])]; } } @@ -367,6 +367,9 @@ class TagsController extends AppController // Remove galaxy tags $event = $this->Tag->removeGalaxyClusterTags($user, $event); + $highlightedTags = $this->Taxonomy->getHighlightedTags($this->Taxonomy->getHighlightedTaxonomies(), $event['EventTag']); + $this->set('highlightedTaxonomies', $highlightedTags); + $this->set('tags', $event['EventTag']); $this->set('missingTaxonomies', $this->Tag->EventTag->Event->missingTaxonomies($event)); $tagConflicts = $this->Taxonomy->checkIfTagInconsistencies($event['EventTag']); diff --git a/app/Controller/TaxiiServersController.php b/app/Controller/TaxiiServersController.php new file mode 100644 index 000000000..69fa9c4a6 --- /dev/null +++ b/app/Controller/TaxiiServersController.php @@ -0,0 +1,231 @@ +request->params['action'] || 'getCollections' == $this->request->params['action']) { + $this->Security->csrfCheck = false; + } + if ($this->request->params['action'] === 'add' || $this->request->params['action'] === 'edit') { + $this->Security->unlockedFields = ['api_root', 'collection']; + } + parent::beforeFilter(); + } + + public $paginate = array( + 'limit' => 60, + 'maxLimit' => 9999 + ); + + public function index() + { + $params = [ + 'filters' => ['name', 'url', 'uuid'], + 'quickFilters' => ['name'] + ]; + $this->CRUD->index($params); + if ($this->IndexFilter->isRest()) { + return $this->restResponsePayload; + } + $this->set('menuData', array('menuList' => 'sync', 'menuItem' => 'list_taxii')); + } + + public function add() + { + $params = []; + $this->CRUD->add($params); + if ($this->restResponsePayload) { + return $this->restResponsePayload; + } + $dropdownData = []; + $this->set(compact('dropdownData')); + $this->set('menuData', array('menuList' => 'sync', 'menuItem' => 'add_taxii')); + } + + public function edit($id) + { + $this->set('menuData', array('menuList' => 'sync', 'menuItem' => 'edit_taxii')); + $this->set('id', $id); + $params = []; + $this->CRUD->edit($id, $params); + if ($this->IndexFilter->isRest()) { + return $this->restResponsePayload; + } + $dropdownData = []; + $this->set(compact('dropdownData')); + $this->render('add'); + } + + public function delete($id) + { + $this->CRUD->delete($id); + if ($this->IndexFilter->isRest()) { + return $this->restResponsePayload; + } + } + + public function view($id) + { + $this->set('menuData', ['menuList' => 'sync', 'menuItem' => 'view_taxii']); + $this->CRUD->view($id); + if ($this->IndexFilter->isRest()) { + return $this->restResponsePayload; + } + $this->set('id', $id); + } + + public function push($id) + { + $this->set('menuData', ['menuList' => 'sync', 'menuItem' => 'push_taxii']); + $taxii_server = $this->TaxiiServer->find('first', [ + 'recursive' => -1, + 'conditions' => ['TaxiiServer.id' => $id] + ]); + if (empty($taxii_server)) { + throw new NotFoundException(__('Invalid Taxii Server ID provided.')); + } + + if ($this->request->is('post')) { + $result = $this->TaxiiServer->pushRouter($taxii_server['TaxiiServer']['id'], $this->Auth->user()); + $message = __('Taxii push initiated.'); + if ($this->_isRest()) { + return $this->RestResponse->saveSuccessResponse('TaxiiServers', 'push', $id, false, $message); + } else { + $this->Flash->success($message); + $this->redirect($this->referer()); + } + } else { + $this->set('id', $taxii_server['TaxiiServer']['id']); + $this->set('title', __('Push data to TAXII server')); + $this->set('question', __('Are you sure you want to Push data as configured in the filters to the TAXII server?')); + $this->set('actionName', __('Push')); + $this->layout = 'ajax'; + $this->render('/genericTemplates/confirm'); + } + } + + public function getRoot() + { + if (empty($this->request->data['baseurl'])) { + return $this->RestResponse->saveFailResponse( + 'TaxiiServers', 'getRoot', null, __('No baseurl set.'), $this->response->type() + ); + } else { + $this->request->data['uri'] = '/taxii2/'; + $result = $this->TaxiiServer->queryInstance( + [ + 'TaxiiServer' => $this->request->data, + 'type' => 'get' + ] + ); + if (is_array($result)) { + $results = []; + foreach ($result['api_roots'] as $api_root) { + $api_root = explode('/', trim($api_root, '/')); + $api_root = end($api_root); + $results[$api_root] = $this->request->data['baseurl'] . '/' . $api_root . '/'; + } + return $this->RestResponse->viewData($results, 'json'); + } else { + return $this->RestResponse->saveFailResponse( + 'TaxiiServers', 'getRoot', null, $result, $this->response->type() + ); + } + } + } + + public function getCollections() + { + if (empty($this->request->data['baseurl'])) { + return $this->RestResponse->saveFailResponse( + 'TaxiiServers', 'getCollections', null, __('No baseurl set.'), $this->response->type() + ); + } + if (empty($this->request->data['api_root'])) { + return $this->RestResponse->saveFailResponse( + 'TaxiiServers', 'getCollections', null, __('No api_root set.'), $this->response->type() + ); + } + $this->request->data['uri'] = '/' . $this->request->data['api_root'] . '/collections/'; + $result = $this->TaxiiServer->queryInstance( + [ + 'TaxiiServer' => $this->request->data, + 'type' => 'get' + ] + ); + if (is_array($result)) { + $results = []; + foreach ($result['collections'] as $collection) { + if (!empty($collection['can_write'])) { + $versions = ''; + if (!empty($collection['media_types'])) { + if (!is_array(($collection['media_types']))) { + $collection['media_types'] = [$collection['media_types']]; + } + $versions = []; + foreach ($collection['media_types'] as $media_type) { + $media_type = explode('=', $media_type); + $media_type = end($media_type); + $versions[$media_type] = true; + } + $versions = implode(', ', array_keys($versions)); + } + $text = (empty($versions) ? '' : '[' . $versions . '] ') . $collection['title']; + $results[$collection['id']] = $text; + } + } + return $this->RestResponse->viewData($results, 'json'); + } else { + return $this->RestResponse->saveFailResponse( + 'TaxiiServers', 'getRoot', null, $result, $this->response->type() + ); + } + } + + public function collectionsIndex($id) + { + $result = $this->TaxiiServer->getCollections($id); + if ($this->_isRest()) { + return $this->RestResponse->viewData($result, $this->response->type()); + } else { + App::uses('CustomPaginationTool', 'Tools'); + $customPagination = new CustomPaginationTool(); + $customPagination->truncateAndPaginate($result, $this->params, false, true); + $this->set('data', $result); + $this->set('id', $id); + $this->set('menuData', array('menuList' => 'sync', 'menuItem' => 'list_taxii_collections')); + } + + } + + public function objectsIndex($id, $collection_id, $next = null) + { + $result = $this->TaxiiServer->getObjects($id, $collection_id, $next); + if ($this->_isRest()) { + return $this->RestResponse->viewData($result, $this->response->type()); + } else { + $this->set('data', $result['objects']); + $this->set('more', $result['more']); + $this->set('next', isset($result['next']) ? $result['next'] : null); + $this->set('id', $id); + $this->set('collection_id', $collection_id); + $this->set('menuData', array('menuList' => 'sync', 'menuItem' => 'list_taxii_collection_objects')); + } + } + + public function objectView($server_id, $collection_id, $id) + { + $result = $this->TaxiiServer->getObject($id, $server_id, $collection_id); + $result = json_encode($result, JSON_PRETTY_PRINT); + $this->layout = false; + $this->set('title', h($id)); + $this->set('json', $result); + $this->render('/genericTemplates/display'); + } +} diff --git a/app/Controller/TaxonomiesController.php b/app/Controller/TaxonomiesController.php index afbb6c22c..b47b80617 100644 --- a/app/Controller/TaxonomiesController.php +++ b/app/Controller/TaxonomiesController.php @@ -59,7 +59,7 @@ class TaxonomiesController extends AppController public function view($id) { - $taxonomy = $this->Taxonomy->getTaxonomy($id, ['full' => $this->_isRest()]); + $taxonomy = $this->Taxonomy->getTaxonomy($id, $this->_isRest()); if (empty($taxonomy)) { throw new NotFoundException(__('Taxonomy not found.')); } @@ -498,6 +498,32 @@ class TaxonomiesController extends AppController $this->render('ajax/toggle_required'); } + public function toggleHighlighted($id) + { + $taxonomy = $this->Taxonomy->find('first', array( + 'recursive' => -1, + 'conditions' => array('Taxonomy.id' => $id) + )); + if (empty($taxonomy)) { + return $this->RestResponse->saveFailResponse('Taxonomy', 'toggleHighlighted', $id, 'Invalid Taxonomy', $this->response->type()); + } + if ($this->request->is('post')) { + $taxonomy['Taxonomy']['highlighted'] = $this->request->data['Taxonomy']['highlighted']; + $result = $this->Taxonomy->save($taxonomy); + if ($result) { + return $this->RestResponse->saveSuccessResponse('Taxonomy', 'toggleHighlighted', $id, $this->response->type()); + } else { + return $this->RestResponse->saveFailResponse('Taxonomy', 'toggleHighlighted', $id, $this->validationError, $this->response->type()); + } + } + + $this->set('highlighted', !$taxonomy['Taxonomy']['highlighted']); + $this->set('id', $id); + $this->autoRender = false; + $this->layout = false; + $this->render('ajax/toggle_highlighted'); + } + /** * @param string $action * @param int $modelId diff --git a/app/Controller/TemplatesController.php b/app/Controller/TemplatesController.php index 2ea4e9108..05dbd8879 100644 --- a/app/Controller/TemplatesController.php +++ b/app/Controller/TemplatesController.php @@ -18,7 +18,7 @@ class TemplatesController extends AppController public function beforeFilter() { // TODO REMOVE parent::beforeFilter(); - $this->Security->unlockedActions = array('uploadFile', 'deleteTemporaryFile'); + $this->Security->unlockedActions = array('uploadFile', 'deleteTemporaryFile', 'saveElementSorting'); } public function index() @@ -188,7 +188,7 @@ class TemplatesController extends AppController $this->request->onlyAllow('ajax'); $orderedElements = $this->request->data; foreach ($orderedElements as $key => $e) { - $orderedElements[$key] = ltrim($e, 'id_'); + $orderedElements[$key] = (int)ltrim($e, 'id_'); } $extractedIds = array(); foreach ($orderedElements as $element) { diff --git a/app/Controller/UsersController.php b/app/Controller/UsersController.php index b8f75c610..ee3f20cb9 100644 --- a/app/Controller/UsersController.php +++ b/app/Controller/UsersController.php @@ -1,5 +1,5 @@ json_encode(array('saved' => false, 'errors' => 'Something went wrong, please try again later.')), 'status'=>200, 'type' => 'json')); } - public function unsubscribe($code) + public function unsubscribe($code, $type = null) { + if ($type === null) { + $type = 'autoalert'; + } else if (!in_array($type, ['autoalert', 'notification_daily', 'notification_weekly', 'notification_monthly'], true)) { + throw new NotFoundException("Invalid type $type."); + } + $user = $this->Auth->user(); if (!hash_equals($this->User->unsubscribeCode($user), rtrim($code, '.'))) { @@ -126,11 +137,11 @@ class UsersController extends AppController $this->redirect(['action' => 'view', 'me']); } - if ($user['autoalert']) { - $this->User->updateField($this->Auth->user(), 'autoalert', false); - $this->Flash->success(__('Successfully unsubscribed from event alert.')); + if ($user[$type]) { + $this->User->updateField($user, $type, false); + $this->Flash->success(__('Successfully unsubscribed from notification.')); } else { - $this->Flash->info(__('Already unsubscribed from event alert.')); + $this->Flash->info(__('Already unsubscribed from notification.')); } $this->redirect(['action' => 'view', 'me']); } @@ -255,6 +266,77 @@ class UsersController extends AppController $this->set('canFetchPgpKey', $this->__canFetchPgpKey()); } + private function __pw_change($user, $source, &$abortPost, $token = false) + { + if (!isset($this->request->data['User'])) { + $this->request->data = array('User' => $this->request->data); + } + if (Configure::read('Security.require_password_confirmation')) { + if (!empty($this->request->data['User']['current_password'])) { + $hashed = $this->User->verifyPassword($this->Auth->user('id'), $this->request->data['User']['current_password']); + if (!$hashed) { + $message = __('Invalid password. Please enter your current password to continue.'); + if ($this->_isRest()) { + return $this->RestResponse->saveFailResponse('Users', $source, false, $message, $this->response->type()); + } + $abortPost = true; + $this->Flash->error($message); + } + unset($this->request->data['User']['current_password']); + } else if (!$this->_isRest()) { + $message = __('Please enter your current password to continue.'); + if ($this->_isRest()) { + return $this->RestResponse->saveFailResponse('Users', $source, false, $message, $this->response->type()); + } + $abortPost = true; + $this->Flash->info($message); + } + } + $hashed = $this->User->verifyPassword($this->Auth->user('id'), $this->request->data['User']['password']); + if ($hashed) { + $message = __('Submitted new password cannot be the same as the current one'); + $abortPost = true; + } + if (!$abortPost) { + // What fields should be saved (allowed to be saved) + $user['User']['change_pw'] = 0; + $user['User']['password'] = $this->request->data['User']['password']; + if ($this->_isRest()) { + $user['User']['confirm_password'] = $this->request->data['User']['password']; + } else { + $user['User']['confirm_password'] = $this->request->data['User']['confirm_password']; + } + $temp = $user['User']['password']; + // Save the data + if ($this->User->save($user)) { + if ($token) { + $this->User->purgeForgetToken($token); + } + $message = __('Password Changed.'); + // log as System if the reset comes from an unauthed user using password_reset tokens + $logUser = empty($this->Auth->user()) ? 'SYSTEM' : $this->Auth->user(); + $this->User->extralog($logUser, $source, null, null, $user); + if ($this->_isRest()) { + return $this->RestResponse->saveSuccessResponse('User', $source, false, $this->response->type(), $message); + } + $this->Flash->success($message); + $this->redirect(array('action' => 'view', $user['User']['id'])); + } else { + $message = __('The password could not be updated. Make sure you meet the minimum password length / complexity requirements.'); + if ($this->_isRest()) { + return $this->RestResponse->saveFailResponse('Users', $source, false, $message, $this->response->type()); + } + $this->Flash->error($message); + } + } else { + if ($this->_isRest()) { + return $this->RestResponse->saveFailResponse('Users', $source, false, $message, $this->response->type()); + } else { + $this->Flash->error($message); + } + } + } + public function change_pw() { $id = $this->Auth->user('id'); @@ -263,69 +345,8 @@ class UsersController extends AppController 'recursive' => -1 )); if ($this->request->is('post') || $this->request->is('put')) { - if (!isset($this->request->data['User'])) { - $this->request->data = array('User' => $this->request->data); - } $abortPost = false; - if (Configure::read('Security.require_password_confirmation')) { - if (!empty($this->request->data['User']['current_password'])) { - $hashed = $this->User->verifyPassword($this->Auth->user('id'), $this->request->data['User']['current_password']); - if (!$hashed) { - $message = __('Invalid password. Please enter your current password to continue.'); - if ($this->_isRest()) { - return $this->RestResponse->saveFailResponse('Users', 'change_pw', false, $message, $this->response->type()); - } - $abortPost = true; - $this->Flash->error($message); - } - unset($this->request->data['User']['current_password']); - } else if (!$this->_isRest()) { - $message = __('Please enter your current password to continue.'); - if ($this->_isRest()) { - return $this->RestResponse->saveFailResponse('Users', 'change_pw', false, $message, $this->response->type()); - } - $abortPost = true; - $this->Flash->info($message); - } - } - $hashed = $this->User->verifyPassword($this->Auth->user('id'), $this->request->data['User']['password']); - if ($hashed) { - $message = __('Submitted new password cannot be the same as the current one'); - $abortPost = true; - } - if (!$abortPost) { - // What fields should be saved (allowed to be saved) - $user['User']['change_pw'] = 0; - $user['User']['password'] = $this->request->data['User']['password']; - if ($this->_isRest()) { - $user['User']['confirm_password'] = $this->request->data['User']['password']; - } else { - $user['User']['confirm_password'] = $this->request->data['User']['confirm_password']; - } - $temp = $user['User']['password']; - // Save the data - if ($this->User->save($user)) { - $message = __('Password Changed.'); - $this->User->extralog($this->Auth->user(), "change_pw", null, null, $user); - if ($this->_isRest()) { - return $this->RestResponse->saveSuccessResponse('User', 'change_pw', false, $this->response->type(), $message); - } - $this->Flash->success($message); - $this->redirect(array('action' => 'view', $id)); - } else { - $message = __('The password could not be updated. Make sure you meet the minimum password length / complexity requirements.'); - if ($this->_isRest()) { - return $this->RestResponse->saveFailResponse('Users', 'change_pw', false, $message, $this->response->type()); - } - $this->Flash->error($message); - } - } else { - if ($this->_isRest()) { - return $this->RestResponse->saveFailResponse('Users', 'change_pw', false, $message, $this->response->type()); - } else { - $this->Flash->error($message); - } - } + return $this->__pw_change($user, 'change_pw', $abortPost); } if ($this->_isRest()) { return $this->RestResponse->describe('Users', 'change_pw', false, $this->response->type()); @@ -410,6 +431,12 @@ class UsersController extends AppController $this->paginate['conditions']['AND'][] = $test; } } + } elseif ("inactive" == $searchTerm) { + if ($v == "1") { + $this->paginate['conditions']['AND'][] = array('User.last_login <' => time() - 60*60*24*30); // older than a month + $this->paginate['conditions']['AND'][] = array('User.current_login <' => time() - 60*60*24*30); // older than a month + $this->paginate['conditions']['AND'][] = array('User.last_api_access <' => time() - 60*60*24*30); // older than a month + } } $passedArgsArray[$searchTerm] = $v; } @@ -487,7 +514,7 @@ class UsersController extends AppController public function admin_filterUserIndex() { $passedArgsArray = array(); - $booleanFields = array('autoalert', 'contactalert', 'termsaccepted', 'disabled'); + $booleanFields = array('autoalert', 'contactalert', 'termsaccepted', 'disabled', 'inactive'); $textFields = array('role', 'email'); if (empty(Configure::read('Security.advanced_authkeys'))) { $textFields[] = 'authkey'; @@ -559,7 +586,7 @@ class UsersController extends AppController { $user = $this->User->find('first', array( 'recursive' => -1, - 'conditions' => $this->__adminFetchConditions($id), + 'conditions' => $this->__adminFetchConditions($id, $edit=False), 'contain' => [ 'UserSetting', 'Role', @@ -582,17 +609,7 @@ class UsersController extends AppController unset($user['User']['authkey']); } if ($this->_isRest()) { - $user['User']['password'] = '*****'; - $temp = array(); - foreach ($user['UserSetting'] as $v) { - $temp[$v['setting']] = $v['value']; - } - $user['UserSetting'] = $temp; - return $this->RestResponse->viewData(array( - 'User' => $user['User'], - 'Role' => $user['Role'], - 'UserSetting' => $user['UserSetting'] - ), $this->response->type()); + return $this->RestResponse->viewData($this->__massageUserObject($user), $this->response->type()); } $this->set('user', $user); @@ -848,9 +865,6 @@ class UsersController extends AppController // MISP automatically chooses the first available option for the user as the selected setting (usually user) // Org admin is downgraded to a user // Now we make an exception for the already assigned role, both in the form and the actual edit. - if (!empty($userToEdit['Role']['perm_site_admin'])) { - throw new NotFoundException(__('Invalid user')); - } $allowedRole = $userToEdit['User']['role_id']; $params = array('conditions' => array( 'OR' => array( @@ -1080,27 +1094,36 @@ class UsersController extends AppController public function admin_delete($id = null) { - $this->request->allowMethod(['post', 'delete']); - - $user = $this->User->find('first', array( - 'conditions' => $this->__adminFetchConditions($id), - 'recursive' => -1 - )); - if (empty($user)) { - throw new NotFoundException(__('Invalid user')); - } - if ($this->User->delete($id)) { - $fieldsDescrStr = 'User (' . $id . '): ' . $user['User']['email']; - $this->User->extralog($this->Auth->user(), "delete", $fieldsDescrStr, ''); - if ($this->_isRest()) { - return $this->RestResponse->saveSuccessResponse('User', 'admin_delete', $id, $this->response->type(), 'User deleted.'); - } else { - $this->Flash->success(__('User deleted')); - $this->redirect(array('action' => 'index')); + if ($this->request->is('post') || $this->request->is('delete')) { + $user = $this->User->find('first', array( + 'conditions' => $this->__adminFetchConditions($id), + 'recursive' => -1, + 'contain' => array('Role') + )); + if (empty($user)) { + throw new NotFoundException(__('Invalid user')); } + if ($this->User->delete($id)) { + $fieldsDescrStr = 'User (' . $id . '): ' . $user['User']['email']; + $this->User->extralog($this->Auth->user(), "delete", $fieldsDescrStr, ''); + if ($this->_isRest()) { + return $this->RestResponse->saveSuccessResponse('User', 'admin_delete', $id, $this->response->type(), 'User deleted.'); + } else { + $this->Flash->success(__('User deleted')); + $this->redirect(array('action' => 'index')); + } + } + $this->Flash->error(__('User was not deleted')); + $this->redirect(array('action' => 'index')); + } else { + $this->set( + 'question', + __('Are you sure you want to delete the user? It is highly recommended to never delete users but to disable them instead.') + ); + $this->set('title', __('Delete user')); + $this->set('actionName', 'Delete'); + $this->render('/genericTemplates/confirm'); } - $this->Flash->error(__('User was not deleted')); - $this->redirect(array('action' => 'index')); } public function admin_massToggleField($fieldName, $enabled) @@ -1115,6 +1138,7 @@ class UsersController extends AppController 'conditions' => $this->__adminFetchConditions($ids), 'recursive' => -1, 'fields' => ['id', $fieldName], + 'contain' => array('Role') ]); if (empty($users)) { throw new NotFoundException(__('Invalid users')); @@ -1167,23 +1191,35 @@ class UsersController extends AppController if ($this->request->is(['post', 'put'])) { $this->Bruteforce = ClassRegistry::init('Bruteforce'); if (!empty($this->request->data['User']['email'])) { - if ($this->Bruteforce->isBlocklisted($_SERVER['REMOTE_ADDR'], $this->request->data['User']['email'])) { + if ($this->Bruteforce->isBlocklisted($this->request->data['User']['email'])) { $expire = Configure::check('SecureAuth.expire') ? Configure::read('SecureAuth.expire') : 300; throw new ForbiddenException('You have reached the maximum number of login attempts. Please wait ' . $expire . ' seconds and try again.'); } } - // Check the length of the user's authkey match old format. This can be removed in future. - $userPass = $this->User->find('first', [ + $unauth_user = $this->User->find('first', [ 'conditions' => ['User.email' => $this->request->data['User']['email']], - 'fields' => ['User.password'], + 'fields' => ['User.password', 'User.totp', 'User.hotp_counter'], 'recursive' => -1, ]); - if (!empty($userPass) && strlen($userPass['User']['password']) === 40) { - $oldHash = true; - unset($this->Auth->authenticate['Form']['passwordHasher']); // use default password hasher - $this->Auth->constructAuthenticate(); + if ($unauth_user) { + // Check the length of the user's authkey match old format. This can be removed in future. + $userPass = $unauth_user['User']['password']; + if (!empty($userPass) && strlen($userPass) === 40) { + $oldHash = true; + unset($this->Auth->authenticate['Form']['passwordHasher']); // use default password hasher + $this->Auth->constructAuthenticate(); + } + // user has TOTP token, check creds and redirect to TOTP validation + if (!empty($unauth_user['User']['totp']) && !$unauth_user['User']['disabled'] && class_exists('\OTPHP\TOTP')) { + $user = $this->Auth->identify($this->request, $this->response); + if ($user && !$user['disabled']) { + $this->Session->write('otp_user', $user); + return $this->redirect('otp'); + } + } } } + // if instance requires email OTP if ($this->request->is('post') && Configure::read('Security.email_otp_enabled')) { $user = $this->Auth->identify($this->request, $this->response); if ($user && !$user['disabled']) { @@ -1213,9 +1249,14 @@ class UsersController extends AppController if ($this->request->is('post') || $this->request->is('put')) { $this->Flash->error(__('Invalid username or password, try again')); if (isset($this->request->data['User']['email'])) { - $this->Bruteforce->insert($_SERVER['REMOTE_ADDR'], $this->request->data['User']['email']); + $this->Bruteforce->insert($this->request->data['User']['email']); } } + + // + // Actions needed for the first access, when the database is not populated yet. + // + // populate the DB with the first role (site admin) if it's empty if (!$this->User->Role->hasAny()) { $siteAdmin = array('Role' => array( @@ -1265,7 +1306,6 @@ class UsersController extends AppController } $org_id = $this->User->Organisation->id; } - // populate the DB with the first user if it's empty if (!$this->User->hasAny()) { if (!isset($org_id)) { @@ -1277,7 +1317,6 @@ class UsersController extends AppController $org_id = $firstOrg['Organisation']['id']; } } - $this->User->runUpdates(); $this->User->createInitialUser($org_id); } @@ -1286,25 +1325,25 @@ class UsersController extends AppController private function _postlogin() { - $this->User->extralog($this->Auth->user(), "login"); - $this->User->Behaviors->disable('SysLogLogable.SysLogLogable'); - $this->User->id = $this->Auth->user('id'); - $user = $this->User->find('first', array( - 'conditions' => array( - 'User.id' => $this->Auth->user('id') - ), - 'recursive' => -1 - )); - unset($user['User']['password']); - $this->User->updateLoginTimes($user['User']); - $lastUserLogin = $user['User']['last_login']; - $this->User->Behaviors->enable('SysLogLogable.SysLogLogable'); - if ($lastUserLogin) { - $readableDatetime = (new DateTime())->setTimestamp($lastUserLogin)->format('D, d M y H:i:s O'); // RFC822 - $this->Flash->info(__('Welcome! Last login was on %s', $readableDatetime)); - } - // no state changes are ever done via GET requests, so it is safe to return to the original page: - $this->redirect($this->Auth->redirectUrl()); + $this->User->extralog($this->Auth->user(), "login"); + $this->User->Behaviors->disable('SysLogLogable.SysLogLogable'); + $this->User->id = $this->Auth->user('id'); + $user = $this->User->find('first', array( + 'conditions' => array( + 'User.id' => $this->Auth->user('id') + ), + 'recursive' => -1 + )); + unset($user['User']['password']); + $this->User->updateLoginTimes($user['User']); + $lastUserLogin = $user['User']['last_login']; + $this->User->Behaviors->enable('SysLogLogable.SysLogLogable'); + if ($lastUserLogin) { + $readableDatetime = (new DateTime())->setTimestamp($lastUserLogin)->format('D, d M y H:i:s O'); // RFC822 + $this->Flash->info(__('Welcome! Last login was on %s', $readableDatetime)); + } + // no state changes are ever done via GET requests, so it is safe to return to the original page: + $this->redirect($this->Auth->redirectUrl()); } public function routeafterlogin() @@ -1344,6 +1383,7 @@ class UsersController extends AppController unset($user['User']['password']); $user['User']['action'] = 'logout'; $this->User->save($user['User'], true, array('id')); + $this->Session->write('otp_secret', null); $this->redirect($this->Auth->logout()); } @@ -1536,8 +1576,9 @@ class UsersController extends AppController public function admin_quickEmail($user_id) { $user = $this->User->find('first', array( - 'conditions' => $this->__adminFetchConditions($user_id), - 'recursive' => -1 + 'conditions' => $this->__adminFetchConditions($user_id, $edit=False), + 'recursive' => -1, + 'contain' => array('Role') )); $error = false; if (empty($user)) { @@ -1701,7 +1742,8 @@ class UsersController extends AppController { $user = $this->User->find('first', array( 'conditions' => $this->__adminFetchConditions($id), - 'recursive' => -1 + 'recursive' => -1, + 'contain' => array('Role') )); if (empty($user)) { throw new NotFoundException(__('Invalid user')); @@ -1731,6 +1773,178 @@ class UsersController extends AppController } } + public function otp() + { + $user = $this->Session->read('otp_user'); + if (empty($user)) { + $this->redirect('login'); + } + if ($this->request->is('post') && isset($this->request->data['User']['otp'])) { + $this->Bruteforce = ClassRegistry::init('Bruteforce'); + if ($this->Bruteforce->isBlocklisted($user['email'])) { + $expire = Configure::check('SecureAuth.expire') ? Configure::read('SecureAuth.expire') : 300; + throw new ForbiddenException('You have reached the maximum number of login attempts. Please wait ' . $expire . ' seconds and try again.'); + } + $secret = $user['totp']; + $totp = \OTPHP\TOTP::create($secret); + $hotp = \OTPHP\HOTP::create($secret); + if ($totp->verify(trim($this->request->data['User']['otp']))) { + // OTP is correct, we login the user with CakePHP + $this->Auth->login($user); + $this->_postlogin(); + } elseif (isset($user['hotp_counter']) && $hotp->verify(trim($this->request->data['User']['otp']), $user['hotp_counter'])) { + // HOTP is correct, update the counter and login + $this->User->id = $user['id']; + $this->User->saveField('hotp_counter', $user['hotp_counter']+1); + $this->Auth->login($user); + $this->_postlogin(); + } else { + $this->Flash->error(__("The OTP is incorrect or has expired")); + $fieldsDescrStr = 'User (' . $user['id'] . '): ' . $user['email']. ' wrong OTP token'; + $this->User->extralog($user, "login_fail", $fieldsDescrStr, ''); + $this->Bruteforce->insert($user['email']); + } + } + // GET Request or wrong OTP, just show the form + $this->set('totp', $user['totp']? true : false); + $this->set('hotp_counter', $user['hotp_counter']); + } + + public function hotp() + { + if (!class_exists('\OTPHP\HOTP')) { + $this->Flash->error(__("The required PHP libraries to support OTP are not installed. Please contact your administrator to address this.")); + $this->redirect($this->referer()); + } + + $user = $this->User->find('first', array( + 'recursive' => -1, + 'conditions' => array('User.id' => $this->Auth->user('id')), + 'fields' => array( + 'totp', 'email', 'id', 'hotp_counter' + ) + )); + $hotp = \OTPHP\HOTP::create($user['User']['totp'], $user['User']['hotp_counter']); + $hotp_codes = []; + for ($i=$user['User']['hotp_counter']; $i < $user['User']['hotp_counter']+50 ; $i++) { + $hotp_codes[$i] = $hotp->at($i); + } + $this->set('hotp_codes', $hotp_codes); + } + + public function totp_new() + { + if (Configure::read('LinOTPAuth.enabled')) { + $this->Flash->error(__("LinOTP is enabled for this instance. Built-in TOTP should not be used.")); + $this->redirect($this->referer()); + } + if (!class_exists('\OTPHP\TOTP') || !class_exists('\BaconQrCode\Writer')) { + $this->Flash->error(__("The required PHP libraries to support TOTP are not installed. Please contact your administrator to address this.")); + $this->redirect($this->referer()); + } + // only allow the users themselves to generate a TOTP secret. + // If TOTP is enforced they will be invited to generate it at first login + $user = $this->User->find('first', array( + 'recursive' => -1, + 'conditions' => array('User.id' => $this->Auth->user('id')), + 'fields' => array( + 'totp', 'email', 'id' + ) + )); + if (empty($user)) { + throw new NotFoundException(__('Invalid user')); + } + // do not allow this page to be accessed if the current already has a TOTP. Just redirect to the users details page with a Flash->error() + if ($user['User']['totp']) { + $this->Flash->error(__("Your account already has a TOTP. Please contact your organisational administrator to change or delete it.")); + $this->redirect($this->referer()); + } + + if ($this->request->is('get')) { + $totp = \OTPHP\TOTP::create(); + $secret = $totp->getSecret(); + $this->Session->write('otp_secret', $secret); // Store in session, we want to create a new secret each time the totp_new() function is queried via a GET (this will not impede incorrect confirmation attempty) + } else { + $secret = $this->Session->read('otp_secret'); // Reload secret from session. + if ($secret) { + $totp = \OTPHP\TOTP::create($secret); + } else { + $totp = \OTPHP\TOTP::create(); + $secret = $totp->getSecret(); + $this->Session->write('otp_secret', $secret); // Store in session, we want to keep reusing the same QR code until the user correctly enters the generated key on their authenticator + } + } + if ($this->request->is('post') && isset($this->request->data['User']['otp'])) { + if ($totp->verify(trim($this->request->data['User']['otp']))) { + // we know the user can generate TOTP tokens, save the new TOTP to the database + $this->User->id = $user['User']['id']; + $this->User->saveField('totp', $secret); + $this->User->saveField('hotp_counter', 0); + $this->_refreshAuth(); + $this->Flash->info(__('The OTP is correct and now active for your account.')); + $fieldsDescrStr = 'User (' . $user['User']['id'] . '): ' . $user['User']['email']. ' TOTP token created'; + $this->User->extralog($this->Auth->user(), "update", $fieldsDescrStr, ''); + // redirect to a page that gives the next 50 HOTP + $this->redirect(array('controller' => 'users', 'action'=> 'hotp')); + } else { + $this->Flash->error(__("The OTP is incorrect or has expired.")); + } + } else { + // GET Request, just show the form + } + // generate QR code with the secret + $renderer = new \BaconQrCode\Renderer\ImageRenderer( + new \BaconQrCode\Renderer\RendererStyle\RendererStyle(200), + new \BaconQrCode\Renderer\Image\SvgImageBackEnd() + ); + $writer = new \BaconQrCode\Writer($renderer); + $totp->setLabel($user['User']['email']); + if (Configure::read('Security.otp_issuer')) { + $totp->setIssuer(Configure::read('Security.otp_issuer')); + } else { + $totp->setIssuer(Configure::read('MISP.org') . ' MISP'); + } + $qrcode = $writer->writeString($totp->getProvisioningUri()); + $qrcode = preg_replace('/^.+\n/', '', $qrcode); // ignore first set('qrcode', $qrcode); + $this->set('secret', $secret); + } + + public function totp_delete($id) { + if ($this->request->is('post') || $this->request->is('delete')) { + $user = $this->User->find('first', array( + 'conditions' => $this->__adminFetchConditions($id), + 'recursive' => -1, + 'contain' => array('Role') + )); + if (empty($user)) { + throw new NotFoundException(__('Invalid user')); + } + $this->User->id = $id; + if ($this->User->saveField('totp', null)) { + $fieldsDescrStr = 'User (' . $id . '): ' . $user['User']['email'] . ' TOTP deleted'; + $this->User->extralog($this->Auth->user(), "update", $fieldsDescrStr, ''); + if ($this->_isRest()) { + return $this->RestResponse->saveSuccessResponse('User', 'admin_totp_delete', $id, $this->response->type(), 'User TOTP deleted.'); + } else { + $this->Flash->success(__('User TOTP deleted')); + $this->redirect('/admin/users/index'); + } + } + $this->Flash->error(__('User TOTP was not deleted')); + $this->redirect('/admin/users/index'); + } else { + $this->set( + 'question', + __('Are you sure you want to delete the TOTP of the user?.') + ); + $this->set('title', __('Delete user TOTP')); + $this->set('actionName', 'Delete'); + $this->render('/genericTemplates/confirm'); + } + } + public function email_otp() { $user = $this->Session->read('email_otp_user'); @@ -1750,6 +1964,8 @@ class UsersController extends AppController $this->_postlogin(); } else { $this->Flash->error(__("The OTP is incorrect or has expired")); + $fieldsDescrStr = 'User (' . $user['id'] . '): ' . $user['email']. ' wrong email OTP token'; + $this->User->extralog($user, "login_fail", $fieldsDescrStr, ''); } } else { // GET Request @@ -1784,7 +2000,7 @@ class UsersController extends AppController $body = $this->__replaceEmailVariables($body); $body = str_replace('$validity', $validity, $body); $body = str_replace('$otp', $otp, $body); - $body = str_replace('$ip', $this->__getClientIP(), $body); + $body = str_replace('$ip', $this->_remoteIp(), $body); $body = str_replace('$username', $user['email'], $body); // Fetch user that contains also PGP or S/MIME keys for e-mail encryption @@ -1800,22 +2016,6 @@ class UsersController extends AppController } } - /** - * Helper function to determine the IP of a client (proxy aware) - */ - private function __getClientIP() { - $x_forwarded = filter_input(INPUT_SERVER, 'HTTP_X_FORWARDED_FOR', FILTER_SANITIZE_STRING); - $client_ip = filter_input(INPUT_SERVER, 'HTTP_CLIENT_IP', FILTER_SANITIZE_STRING); - if (!empty($x_forwarded)) { - $x_forwarded = explode(",", $x_forwarded); - return $x_forwarded[0]; - } elseif(!empty($client_ip)){ - return $client_ip; - } else { - return filter_input(INPUT_SERVER, 'REMOTE_ADDR', FILTER_SANITIZE_STRING); - } - } - // shows some statistics about the instance public function statistics($page = 'data') { @@ -2831,7 +3031,7 @@ class UsersController extends AppController * @return array * @throws NotFoundException */ - private function __adminFetchConditions($id) + private function __adminFetchConditions($id, $edit = True) { if (empty($id)) { throw new NotFoundException(__('Invalid user')); @@ -2841,7 +3041,133 @@ class UsersController extends AppController $user = $this->Auth->user(); if (!$user['Role']['perm_site_admin']) { $conditions['User.org_id'] = $user['org_id']; // org admin + if ($edit) { + $conditions['Role.perm_site_admin'] = False; + } } return $conditions; } + + public function admin_destroy($id = null) + { + $conditionFields = ['id', 'email']; + $params = $this->IndexFilter->harvestParameters(['id', 'email']); + if (!empty($id)) { + $params['id'] = $id; + } + $conditions = []; + foreach ($conditionFields as $conditionField) { + if (!empty($params[$conditionField])) { + $conditions[$conditionField . ' LIKE'] = $params[$conditionField]; + } + } + if (!empty($conditions)) { + $user_ids = $this->User->find('list', [ + 'recursive' => -1, + 'fields' => ['email', 'id'], + 'conditions' => $conditions + ]); + } else { + $user_ids = [__('Every user') => 'all']; + } + if ($this->request->is('post')) { + $redis = RedisTool::init(); + $kill_before = time(); + foreach (array_values($user_ids) as $user_id) { + $redis->set('misp:session_destroy:' . $user_id, $kill_before); + } + $message = __( + 'Session destruction cutoff set to the current timestamp for the given selection (%s). Session(s) will be destroyed on the next user interaction.', + implode(', ', array_keys($user_ids)) + ); + if ($this->_isRest()) { + return $this->RestResponse->saveSuccessResponse('User', 'admin_destroy', false, $this->response->type(), $message); + } + $this->Flash->success($message); + $this->redirect($this->referer()); + } else { + $this->set( + 'question', + __( + 'Do you really wish to destroy the session for: %s ? The session destruction will occur when the users try to interact with MISP the next time.', + implode(', ', array_keys($user_ids)) + ) + ); + $this->set('title', __('Destroy sessions')); + $this->set('actionName', 'Destroy'); + $this->render('/genericTemplates/confirm'); + } + } + public function logout401() { + # You should read the documentation in docs/CONFIG.ApacheSecureAuth.md + # before using this endpoint. It is not useful without webserver config + # changes. + # To use this, set Plugin.CustomAuth_custom_logout to /users/logout401 + $this->response->statusCode(401); + } + + public function forgot() + { + if (empty(Configure::read('Security.allow_password_forgotten'))) { + $this->Flash->error(__('This feature is disabled.')); + $this->redirect('/'); + } + if (!empty($this->Auth->user()) && !$this->_isRest()) { + $this->Flash->info(__('You are already logged in, no need to ask for a password reset. Log out first.')); + $this->redirect('/'); + } + if ($this->request->is('post')) { + if (empty($this->request->data['User'])) { + $this->request->data = ['User' => $this->request->data]; + } + if (empty($this->request->data['User']['email'])) { + throw new MethodNotAllowedException(__('No email provided, cannot generate password reset message.')); + } + $user = [ + 'id' => 0, + 'email' => 'SYSTEM', + 'Organisation' => [ + 'name' => 'SYSTEM' + ] + ]; + $this->loadModel('Log'); + $this->Log->createLogEntry($user, 'forgot', 'User', 0, 'Password reset requested for: ' . $this->request->data['User']['email']); + $this->User->forgotRouter($this->request->data['User']['email'], $this->_remoteIp()); + $message = __('Password reset request submitted. If a valid user is found, you should receive an e-mail with a temporary reset link momentarily. Please be advised that this link is only valid for 10 minutes.'); + if ($this->_isRest()) { + return $this->RestResponse->saveSuccessResponse('User', 'forgot', false, $this->response->type(), $message); + } + $this->Flash->info($message); + $this->redirect('/'); + } + } + + public function password_reset($token) + { + if (empty(Configure::read('Security.allow_password_forgotten'))) { + $this->Flash->error(__('This feature is disabled.')); + $this->redirect('/'); + } + $this->loadModel('Server'); + $this->set('complexity', !empty(Configure::read('Security.password_policy_complexity')) ? Configure::read('Security.password_policy_complexity') : $this->Server->serverSettings['Security']['password_policy_complexity']['value']); + $this->set('length', !empty(Configure::read('Security.password_policy_length')) ? Configure::read('Security.password_policy_length') : $this->Server->serverSettings['Security']['password_policy_length']['value']); + if (!empty($this->Auth->user()) && !$this->_isRest()) { + $this->redirect('/'); + } + $user = $this->User->fetchForgottenPasswordUser($token); + if (empty($user)) { + $message = __('Invalid token, or password request token already expired.'); + if ($this->_isRest()) { + throw new MethodNotAllowedException($message); + } else { + $this->Flash->error($message); + $this->redirect('/'); + } + } + if ($this->request->is('post') || $this->request->is('put')) { + $abortPost = false; + return $this->__pw_change(['User' => $user], 'password_reset', $abortPost, $token); + } + } + } diff --git a/app/Controller/WorkflowsController.php b/app/Controller/WorkflowsController.php index a70962c4b..b935bfee4 100644 --- a/app/Controller/WorkflowsController.php +++ b/app/Controller/WorkflowsController.php @@ -67,6 +67,7 @@ class WorkflowsController extends AppController } else { $successMessage = __('Workflow saved.'); $savedWorkflow = $result['saved']; + $savedWorkflow = $this->Workflow->attachLabelToConnections($savedWorkflow); return $this->__getSuccessResponseBasedOnContext($successMessage, $savedWorkflow, 'edit', false, $redirectTarget); } } else { @@ -101,6 +102,9 @@ class WorkflowsController extends AppController } } $this->CRUD->view($id, [ + 'afterFind' => function($workflow) { + return $this->Workflow->attachLabelToConnections($workflow); + } ]); if ($this->IndexFilter->isRest()) { return $this->restResponsePayload; @@ -151,6 +155,7 @@ class WorkflowsController extends AppController } else { $workflow = $this->Workflow->fetchWorkflow($workflow_id); } + $workflow = $this->Workflow->attachLabelToConnections($workflow, $trigger_id); $modules = $this->Workflow->attachNotificationToModules($modules, $workflow); $this->loadModel('WorkflowBlueprint'); $workflowBlueprints = $this->WorkflowBlueprint->find('all'); @@ -285,6 +290,8 @@ class WorkflowsController extends AppController if ($this->_isRest()) { return $this->RestResponse->viewData($module, $this->response->type()); } + if (!isset($module['Workflow'])) + $module['Workflow'] = ['counter' => false, 'id' => false]; $this->set('data', $module); $this->set('menuData', ['menuList' => 'workflows', 'menuItem' => 'view_module']); } diff --git a/app/Lib/Dashboard/APIActivityWidget.php b/app/Lib/Dashboard/APIActivityWidget.php new file mode 100644 index 000000000..3ff0d7e54 --- /dev/null +++ b/app/Lib/Dashboard/APIActivityWidget.php @@ -0,0 +1,137 @@ + 'A list of filters by organisation meta information (sector, type, nationality, id, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + 'limit' => 'Limits the number of displayed APIkeys. (-1 will list all) Default: -1', + 'days' => 'How many days back should the list go - for example, setting 7 will only show contributions in the past 7 days. (integer)', + 'month' => 'Who contributed most this month? (boolean)', + 'previous_month' => 'Who contributed most the previous, finished month? (boolean)', + 'year' => 'Which contributed most this year? (boolean)', + ]; + public $description = 'Basic widget showing some server statistics in regards to MISP.'; + public $cacheLifetime = 10; + public $autoRefreshDelay = null; + private $User = null; + private $AuthKey = null; + + + private function getDates($options) + { + if (!empty($options['days'])) { + $begin = new DateTime(date('Y-m-d', strtotime(sprintf("-%s days", $options['days'])))); + } else if (!empty($options['month'])) { + $begin = new DateTime(date('Y-m-d', strtotime('first day of this month 00:00:00', time()))); + } else if (!empty($options['previous_month'])) { + $begin = new DateTime(date('Y-m-d', strtotime('first day of last month 00:00:00', time()))); + $end = new DateTime(date('Y-m-d', strtotime('last day of last month 23:59:59', time()))); + } else if (!empty($options['year'])) { + $begin = new DateTime(date('Y-m-d', strtotime('first day of this year 00:00:00', time()))); + } else { + $begin = new DateTime(date('Y-m-d', strtotime('-7 days', time())));; + } + + $end = isset($end) ? $end : new DateTime(); + $dates = new DatePeriod( + $begin, + new DateInterval('P1D'), + $end + ); + $results = []; + foreach ($dates as $date) { + $results[] = $date->format('Ymd'); + } + return $results; + } + + public function handler($user, $options = array()) + { + $this->User = ClassRegistry::init('User'); + $this->AuthKey = ClassRegistry::init('AuthKey'); + $redis = $this->User->setupRedis(); + if (!$redis) { + throw new NotFoundException(__('No redis connection found.')); + } + + $params = ['conditions' => []]; + $dates = $this->getDates($options); + $pipe = $redis->pipeline(); + foreach ($dates as $date) { + $pipe->zrange('misp:authkey_log:' . $date, 0, -1, true); + } + $temp = $pipe->exec(); + $raw_results = []; + $counts = []; + foreach ($dates as $k => $date) { + $raw_results[$date] = $temp[$k]; + if (!empty($temp[$k])) { + foreach ($temp[$k] as $key => $count) { + if (isset($counts[$key])) { + $counts[$key] += (int)$count; + } else { + $counts[$key] = (int)$count; + } + } + } + } + arsort($counts); + $this->AuthKey->Behaviors->load('Containable'); + $temp_apikeys = array_flip(array_keys($counts)); + foreach ($temp_apikeys as $apikey => $value) { + $temp_apikeys[$apikey] = $this->AuthKey->find('first', [ + 'conditions' => [ + 'AuthKey.authkey_start' => substr($apikey, 0, 4), + 'AuthKey.authkey_end' => substr($apikey, 4) + ], + 'fields' => ['AuthKey.authkey_start', 'AuthKey.authkey_end', 'AuthKey.id', 'User.id', 'User.email'], + 'recursive' => 1 + ]); + } + $results = []; + $baseurl = empty(Configure::read('MISP.external_baseurl')) ? h(Configure::read('MISP.baseurl')) : Configure::read('MISP.external_baseurl'); + foreach ($counts as $key => $junk) { + $data = $temp_apikeys[$key]; + if (!empty($data)) { + $results[] = [ + 'html_title' => sprintf( + '%s', + h($baseurl), + h($data['AuthKey']['id']), + $key + ), + 'html' => sprintf( + '%s (%s)', + h($counts[$key]), + h($baseurl), + h($data['User']['id']), + h($data['User']['email']) + ) + ]; + } else { + $results[] = [ + 'title' => $key, + 'html' => sprintf( + '%s (%s)', + h($counts[$key]), + __('An unknown key can be caused by the given key having been permanently deleted or falsely mis-identified (for the purposes of this widget) on instances using legacy API key authentication.'), + __('Unknown key') + ) + ]; + } + } + return $results; + } + + public function checkPermissions($user) + { + if (empty($user['Role']['perm_site_admin'])) { + return false; + } + return true; + } +} diff --git a/app/Lib/Dashboard/AttackWidget.php b/app/Lib/Dashboard/AttackWidget.php new file mode 100644 index 000000000..3c4f38952 --- /dev/null +++ b/app/Lib/Dashboard/AttackWidget.php @@ -0,0 +1,38 @@ + 'A list of restsearch filters to apply to the heatmap. (dictionary, prepending values with ! uses them as a negation)' + ]; + public $cacheLifetime = 1200; + public $autoRefreshDelay = false; + private $validFilterKeys = [ + 'filters' + ]; + private $Event = null; + public $placeholder = +'{ + "filters": { + "attackGalaxy": "mitre-attack-pattern", + "timestamp": ["2023-01-01", "2023-03-31"], + "published": [0,1] + } +}'; + + public function handler($user, $options = array()) + { + $this->Event = ClassRegistry::init('Event'); + $data = null; + if (!empty($options['filters'])) { + $data = $this->Event->restSearch($user, 'attack', $options['filters']); + $data = JsonTool::decode($data->intoString()); + } + return $data; + } +} +?> diff --git a/app/Lib/Dashboard/EventEvolutionLineWidget.php b/app/Lib/Dashboard/EventEvolutionLineWidget.php new file mode 100644 index 000000000..e8d188813 --- /dev/null +++ b/app/Lib/Dashboard/EventEvolutionLineWidget.php @@ -0,0 +1,136 @@ + 'A list of filters by organisation meta information (nationality, sector, type, name, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + 'start_date' => 'Start date, expressed in Y-m-d format (e.g. 2012-10-01)' + ]; + private $validFilterKeys = [ + 'nationality', + 'sector', + 'type', + 'name', + 'uuid' + ]; + + public $placeholder = + '{ + "filter": { + "sector": "financial" + }, + "start_date": "2017-01", +}'; + + private $Organisation = null; + private $Event = null; + + private function timeConditions($options) + { + if (!empty($options['start_date'])) { + $condition = strtotime($options['start_date']); + } else { + $condition = strtotime('2012-10-01'); + } + $datetime = new DateTime(); + $datetime->setTimestamp($condition); + return $datetime->format('Y-m-d H:i:s'); + } + + public function handler($user, $options = array()) + { + $this->Organisation = ClassRegistry::init('Organisation'); + $this->Event = ClassRegistry::init('Event'); + $oparams = [ + 'conditions' => [ + 'AND' => ['Organisation.local' => !isset($options['local']) ? 1 : $options['local']] + ], + 'limit' => 10, + 'recursive' => -1 + ]; + $eparams = []; + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $oparams['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + $timeConditions = $this->timeConditions($options); + if ($timeConditions) { + $eparams['conditions']['AND'][] = ['Event.publish_timestamp >=' => strtotime($timeConditions)]; + } + $org_ids = $this->Organisation->find('list', [ + 'recursive' => -1, + 'conditions' => $oparams['conditions'], + 'fields' => ['id'] + ]); + $this->Event->virtualFields = [ + 'published_date' => null + ]; + $raw = $this->Event->find('all', [ + 'recursive' => -1, + 'conditions' => $eparams['conditions'], + 'fields' => ['DATE_FORMAT(FROM_UNIXTIME(Event.publish_timestamp), "%Y-%m") AS date', 'count(id) AS count'], + 'group' => 'MONTH(FROM_UNIXTIME(Event.publish_timestamp)), YEAR(FROM_UNIXTIME(Event.publish_timestamp))' + + ]); + + usort($raw, [$this, 'sortByCreationDate']); + $raw_padded = []; + $total = 0; + $default_start_date = empty($raw) ? '2012-10-01' : ($raw[0][0]['date'] . '-01'); + $start = new DateTime(empty($options['start_date']) ? $default_start_date : $options['start_date']); + $end = new DateTime(date('Y-m') . '-01'); + $interval = DateInterval::createFromDateString('1 month'); + $period = new DatePeriod($start, $interval, $end); + foreach ($period as $dt) { + $raw_padded[$dt->format('Y-m') . '-01'] = 0; + } + foreach ($raw as $datapoint) { + $raw_padded[$datapoint[0]['date'] . '-01'] = (int)$datapoint[0]['count']; + } + $total = 0; + foreach ($raw_padded as $date => $count) { + $total += $count; + $raw_padded[$date] = $total; + } + $data = []; + foreach ($raw_padded as $date => $count) { + $data['data'][] = [ + 'Events' => (int)$count, + 'date' => $date + ]; + } + return $data; + } + + private function sortByCreationDate($a, $b) { + if ($a[0]['date'] > $b[0]['date']) { + return 1; + } else { + return -1; + } + return 0; + } +} diff --git a/app/Lib/Dashboard/LoginsWidget.php b/app/Lib/Dashboard/LoginsWidget.php new file mode 100644 index 000000000..cb17b8273 --- /dev/null +++ b/app/Lib/Dashboard/LoginsWidget.php @@ -0,0 +1,99 @@ + 'A list of filters by organisation meta information (sector, type, nationality, id, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + 'limit' => 'Limits the number of displayed APIkeys. (-1 will list all) Default: -1', + 'days' => 'How many days back should the list go - for example, setting 7 will only show contributions in the past 7 days. (integer)', + 'month' => 'Who contributed most this month? (boolean)', + 'previous_month' => 'Who contributed most the previous, finished month? (boolean)', + 'year' => 'Which contributed most this year? (boolean)', + ]; + public $description = 'Basic widget showing some server statistics in regards to MISP.'; + public $cacheLifetime = 10; + public $autoRefreshDelay = null; + private $User = null; + private $Log = null; + + + private function getDates($options) + { + if (!empty($options['days'])) { + $begin = date('Y-m-d H:i:s', strtotime(sprintf("-%s days", $options['days']))); + } else if (!empty($options['month'])) { + $begin = date('Y-m-d H:i:s', strtotime('first day of this month 00:00:00', time())); + } else if (!empty($options['previous_month'])) { + $begin = date('Y-m-d H:i:s', strtotime('first day of last month 00:00:00', time())); + $end = date('Y-m-d H:i:s', strtotime('last day of last month 23:59:59', time())); + } else if (!empty($options['year'])) { + $begin = date('Y-m-d', strtotime('first day of this year 00:00:00', time())); + } else { + $begin = date('Y-m-d H:i:s', strtotime('-7 days', time())); + } + $params = []; + if (!empty($end)) { + $params['Log.created <='] = $end; + } + if (!empty($begin)) { + $params['Log.created >='] = $begin; + } + return $params; + } + + public function handler($user, $options = array()) + { + $this->User = ClassRegistry::init('User'); + $this->Log = ClassRegistry::init('Log'); + $conditions = $this->getDates($options); + $conditions['Log.action'] = 'login'; + $this->Log->Behaviors->load('Containable'); + $this->Log->bindModel([ + 'belongsTo' => [ + 'User' + ] + ]); + $this->Log->virtualFields['count'] = 0; + $this->Log->virtualFields['email'] = ''; + $logs = $this->Log->find('all', [ + 'recursive' => -1, + 'conditions' => $conditions, + 'fields' => ['Log.user_id', 'COUNT(Log.id) AS Log__count', 'User.email AS Log__email'], + 'contain' => ['User'], + 'group' => ['Log.user_id'] + ]); + $counts = []; + $emails = []; + foreach ($logs as $log) { + $counts[$log['Log']['user_id']] = $log['Log']['count']; + $emails[$log['Log']['user_id']] = $log['Log']['email']; + } + $results = []; + arsort($counts); + $baseurl = empty(Configure::read('MISP.external_baseurl')) ? h(Configure::read('MISP.baseurl')) : Configure::read('MISP.external_baseurl'); + foreach ($counts as $user_id => $count) { + $results[] = [ + 'html_title' => sprintf( + '%s', + h($baseurl), + h($user_id), + h($emails[$user_id]) + ), + 'value' => $count + ]; + } + return $results; + } + + public function checkPermissions($user) + { + if (empty($user['Role']['perm_site_admin'])) { + return false; + } + return true; + } +} diff --git a/app/Lib/Dashboard/NewOrgsWidget.php b/app/Lib/Dashboard/NewOrgsWidget.php new file mode 100644 index 000000000..141b6adf8 --- /dev/null +++ b/app/Lib/Dashboard/NewOrgsWidget.php @@ -0,0 +1,171 @@ + 'Maximum number of joining organisations shown. (integer, defaults to 10 if not set)', + 'filter' => 'A list of filters by organisation meta information (nationality, sector, type, name, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + 'days' => 'How many days back should the list go - for example, setting 7 will only show the organisations that were added in the past 7 days. (integer)', + 'month' => 'Which organisations have been added this month? (boolean)', + 'previous_month' => 'Who contributed most the previous, finished month? (boolean)', + 'year' => 'Which organisations have been added this year? (boolean)', + 'local' => 'Should the list only show local organisations? (boolean or list of booleans, defaults to 1. To get both sets, use [0,1])', + 'fields' => 'Which fields should be displayed, by default all are selected. Pass a list with the following options: [id, uuid, name, sector, type, nationality, creation_date]' + ]; + private $validFilterKeys = [ + 'nationality', + 'sector', + 'type', + 'name', + 'uuid' + ]; + + public $placeholder = + '{ + "limit": 5, + "filter": { + "nationality": [ + "Hungary", + "Russia", + "North Korea" + ] + }, + "month": true +}'; + + private $Organisation = null; + + private function timeConditions($options) + { + $limit = empty($options['limit']) ? 10 : $options['limit']; + if (!empty($options['days'])) { + $condition = strtotime(sprintf("-%s days", $options['days'])); + $this->tableDescription = __('The %d newest organisations created in the past %d days', $limit, (int)$options['days']); + } else if (!empty($options['month'])) { + $condition = strtotime('first day of this month 00:00:00', time()); + $this->tableDescription = __('The %d newest organisations created during the current month', $limit); + } else if (!empty($options['previous_month'])) { + $condition = strtotime('first day of last month 00:00:00', time()); + $end_condition = strtotime('last day of last month 23:59:59', time()); + $this->tableDescription = __('The %d newest organisations created during the previous month', $limit); + } else if (!empty($options['year'])) { + $condition = strtotime('first day of this year 00:00:00', time()); + $this->tableDescription = __('The %d newest organisations created during the current year', $limit); + } else { + $this->tableDescription = __('The %d newest organisations created', $limit); + return null; + } + $conditions = []; + if (!empty($condition)) { + $datetime = new DateTime(); + $datetime->setTimestamp($condition); + $conditions['Organisation.date_created >='] = $datetime->format('Y-m-d H:i:s'); + } + if (!empty($end_condition)) { + $datetime = new DateTime(); + $datetime->setTimestamp($end_condition); + $conditions['Organisation.date_created <='] = $datetime->format('Y-m-d H:i:s'); + } + return $conditions; + } + + public function handler($user, $options = array()) + { + $this->Organisation = ClassRegistry::init('Organisation'); + $field_options = [ + 'id' => [ + 'name' => '#', + 'url' => Configure::read('MISP.baseurl') . '/organisations/view', + 'element' => 'links', + 'data_path' => 'Organisation.id', + 'url_params_data_paths' => 'Organisation.id' + ], + 'date_created' => [ + 'name' => 'Creation date', + 'data_path' => 'Organisation.date_created' + ], + 'name' => [ + 'name' => 'Name', + 'data_path' => 'Organisation.name', + ], + 'uuid' => [ + 'name' => 'UUID', + 'data_path' => 'Organisation.uuid', + ], + 'sector' => [ + 'name' => 'Sector', + 'data_path' => 'Organisation.sector', + ], + 'nationality' => [ + 'name' => 'Nationality', + 'data_path' => 'Organisation.nationality', + ], + 'type' => [ + 'name' => 'Type', + 'data_path' => 'Organisation.type', + ] + ]; + $params = [ + 'conditions' => [ + 'AND' => ['Organisation.local' => !isset($options['local']) ? 1 : $options['local']] + ], + 'limit' => 10, + 'recursive' => -1 + ]; + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + $timeConditions = $this->timeConditions($options); + if ($timeConditions) { + $params['conditions']['AND'][] = ['Organisation.date_created >=' => $timeConditions]; + } + if (isset($options['fields'])) { + $fields = []; + foreach ($options['fields'] as $field) { + if (isset($field_options[$field])) { + $fields[$field] = $field_options[$field]; + } + } + } else { + $fields = $field_options; + } + $data = $this->Organisation->find('all', [ + 'recursive' => -1, + 'conditions' => $params['conditions'], + 'limit' => isset($options['limit']) ? (int)$options['limit'] : 10, + 'fields' => array_keys($fields), + 'order' => 'Organisation.date_created DESC' + ]); + + return [ + 'data' => $data, + 'fields' => $fields, + 'description' => $this->tableDescription + ]; + } +} diff --git a/app/Lib/Dashboard/NewUsersWidget.php b/app/Lib/Dashboard/NewUsersWidget.php new file mode 100644 index 000000000..5c66ff6e6 --- /dev/null +++ b/app/Lib/Dashboard/NewUsersWidget.php @@ -0,0 +1,187 @@ + 'Maximum number of joining users shown. (integer, defaults to 10 if not set)', + 'filter' => 'A list of filters for the organisations (nationality, sector, type, name, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + 'days' => 'How many days back should the list go - for example, setting 7 will only show the organisations that were added in the past 7 days. (integer)', + 'month' => 'Which organisations have been added this month? (boolean)', + 'previous_month' => 'Who contributed most the previous, finished month? (boolean)', + 'year' => 'Which organisations have been added this year? (boolean)', + 'fields' => 'Which fields should be displayed, by default all are selected. Pass a list with the following options: [id, email, Organisation.name, Role.name, date_created]' + ]; + private $validFilterKeys = [ + 'id', + 'email', + 'Organisation.name', + 'Role.name', + 'date_created' + ]; + + public $placeholder = + '{ + "limit": 10, + "filter": { + "Organisation.name": [ + "!FSB", + "!GRU", + "!Kaspersky" + ], + "email": [ + "!andras.iklody@circl.lu" + ], + "Role.name": [ + "Publisher", + "User" + ] + }, + "year": true +}'; + + private $User = null; + + private function timeConditions($options) + { + $limit = empty($options['limit']) ? 10 : $options['limit']; + if (!empty($options['days'])) { + $condition = strtotime(sprintf("-%s days", $options['days'])); + $this->tableDescription = __('The %d newest users created in the past %d days', $limit, (int)$options['days']); + } else if (!empty($options['month'])) { + $condition = strtotime('first day of this month 00:00:00', time()); + $this->tableDescription = __('The %d newest users created during the current month', $limit); + } else if (!empty($options['previous_month'])) { + $condition = strtotime('first day of last month 00:00:00', time()); + $end_condition = strtotime('last day of last month 23:59:59', time()); + $this->tableDescription = __('The %d newest organisations created during the previous month', $limit); + } else if (!empty($options['year'])) { + $condition = strtotime('first day of this year 00:00:00', time()); + $this->tableDescription = __('The %d newest users created during the current year', $limit); + } else { + $this->tableDescription = __('The %d newest users created', $limit); + return null; + } + $conditions = []; + if (!empty($condition)) { + $datetime = new DateTime(); + $datetime->setTimestamp($condition); + $conditions['Organisation.date_created >='] = $datetime->format('Y-m-d H:i:s'); + } + if (!empty($end_condition)) { + $datetime = new DateTime(); + $datetime->setTimestamp($end_condition); + $conditions['Organisation.date_created <='] = $datetime->format('Y-m-d H:i:s'); + } + return $conditions; + } + + public function handler($user, $options = array()) + { + $this->User = ClassRegistry::init('User'); + $field_options = [ + 'id' => [ + 'name' => '#', + 'url' => empty($user['Role']['perm_site_admin']) ? null : Configure::read('MISP.baseurl') . '/admin/users/view', + 'element' => 'links', + 'data_path' => 'User.id', + 'url_params_data_paths' => 'User.id' + ], + 'date_created' => [ + 'name' => 'Creation date', + 'data_path' => 'User.date_created' + ], + 'email' => [ + 'name' => 'E-mail', + 'data_path' => 'User.email', + ], + 'Organisation.name' => [ + 'name' => 'Organisation', + 'data_path' => 'Organisation.name', + ], + 'Role.name' => [ + 'name' => 'Role', + 'data_path' => 'Role.name', + ] + ]; + $params = [ + 'conditions' => [], + 'limit' => 10, + 'recursive' => -1 + ]; + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + $filterName = strpos($filterKey, '.') ? $filterKey : 'User.' . $filterKey; + if ($value[0] === '!') { + $tempConditionBucket[$filterName . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket[$filterName . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + $timeConditions = $this->timeConditions($options); + if ($timeConditions) { + $params['conditions']['AND'][] = $timeConditions; + } + if (isset($options['fields'])) { + $fields = []; + foreach ($options['fields'] as $field) { + if (isset($field_options[$field])) { + $fields[$field] = $field_options[$field]; + } + } + } else { + $fields = $field_options; + } + + // redact e-mails for non site admins unless specifically allowed + if ( + empty($user['Role']['perm_site_admin']) && + !Configure::read('Security.disclose_user_emails') && + isset($fields['email']) + ) { + unset($fields['email']); + } + $data = $this->User->find('all', [ + 'recursive' => -1, + 'contain' => ['Organisation.name', 'Role.name'], + 'conditions' => $params['conditions'], + 'limit' => isset($options['limit']) ? $options['limit'] : 10, + 'fields' => array_keys($fields), + 'order' => 'User.date_created DESC' + ]); + + foreach ($data as &$u) { + if (empty($u['User']['date_created'])) { + continue; + } + $tempDate = new DateTime(); + $tempDate->setTimestamp($u['User']['date_created']); + $u['User']['date_created'] = $tempDate->format('Y-m-d H:i:s'); + } + + return [ + 'data' => $data, + 'fields' => $fields, + 'description' => $this->tableDescription + ]; + } +} diff --git a/app/Lib/Dashboard/OrgContributionToplistWidget.php b/app/Lib/Dashboard/OrgContributionToplistWidget.php new file mode 100644 index 000000000..da0e92619 --- /dev/null +++ b/app/Lib/Dashboard/OrgContributionToplistWidget.php @@ -0,0 +1,122 @@ + 'How many days back should the list go - for example, setting 7 will only show contributions in the past 7 days. (integer)', + 'month' => 'Who contributed most this month? (boolean)', + 'previous_month' => 'Who contributed most the previous, finished month? (boolean)', + 'year' => 'Which contributed most this year? (boolean)', + 'filter' => 'A list of filters by organisation meta information (nationality, sector, type, name, uuid, local (- expects a boolean or a list of boolean values)) to include. (dictionary, prepending values with ! uses them as a negation)', + 'limit' => 'Limits the number of displayed tags. Default: 10' + ]; + public $cacheLifetime = null; + public $autoRefreshDelay = false; + private $validFilterKeys = [ + 'nationality', + 'sector', + 'type', + 'name', + 'uuid' + ]; + public $placeholder = +'{ + "days": "7d", + "threshold": 15, + "filter": { + "sector": "Financial" + } +}'; + private $Org = null; + private $Event = null; + + + private function timeConditions($options) + { + $limit = empty($options['limit']) ? 10 : $options['limit']; + if (!empty($options['days'])) { + $condition = strtotime(sprintf("-%s days", $options['days'])); + } else if (!empty($options['month'])) { + $condition = strtotime('first day of this month 00:00:00', time()); + } else if (!empty($options['previous_month'])) { + $condition = strtotime('first day of previous month 00:00:00', time()); + $end_condition = strtotime('last day of last month 23:59:59', time()); + } else if (!empty($options['year'])) { + $condition = strtotime('first day of this year 00:00:00', time()); + } else { + return null; + } + $conditions = []; + if (!empty($condition)) { + $datetime = new DateTime(); + $datetime->setTimestamp($condition); + $conditions['Event.timestamp >='] = $datetime->format('Y-m-d H:i:s'); + } + if (!empty($end_condition)) { + $datetime = new DateTime(); + $datetime->setTimestamp($end_condition); + $conditions['Event.timestamp <='] = $datetime->format('Y-m-d H:i:s'); + } + return $conditions; + } + + + public function handler($user, $options = array()) + { + $params = ['conditions' => []]; + $timeConditions = $this->timeConditions($options); + if ($timeConditions) { + $params['conditions']['AND'][] = $timeConditions; + } + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + if (isset($options['filter']['local'])) { + $params['conditions']['AND']['local'] = $options['filter']['local']; + } + + $this->Org = ClassRegistry::init('Organisation'); + $org_ids = $this->Org->find('list', [ + 'fields' => ['Organisation.id', 'Organisation.name'], + 'conditions' => $params['conditions'] + ]); + $conditions = ['Event.orgc_id IN' => array_keys($org_ids)]; + $this->Event = ClassRegistry::init('Event'); + $this->Event->virtualFields['frequency'] = 0; + $orgs = $this->Event->find('all', [ + 'recursive' => -1, + 'fields' => ['orgc_id', 'count(Event.orgc_id) as Event__frequency'], + 'group' => ['orgc_id'], + 'conditions' => $conditions, + 'order' => 'count(Event.orgc_id) desc', + 'limit' => empty($options['limit']) ? 10 : $options['limit'] + ]); + $results = []; + foreach($orgs as $org) { + $results[$org_ids[$org['Event']['orgc_id']]] = $org['Event']['frequency']; + } + return ['data' => $results]; + } +} +?> diff --git a/app/Lib/Dashboard/OrgEventsWidget.php b/app/Lib/Dashboard/OrgEventsWidget.php new file mode 100644 index 000000000..cf74e9a88 --- /dev/null +++ b/app/Lib/Dashboard/OrgEventsWidget.php @@ -0,0 +1,121 @@ + 'A list of organisation names to filter out', + 'months' => 'Number of past months to consider for the graph', + 'logarithmic' => 'Visualize data on logarithmic scale' + ); + + public $placeholder = +'{ + "blocklist_orgs": ["Orgs to filter"], + "months": "6", + "logarithmic": "true" +}'; + + + + + + /* + * Target_month must be from 1 to 12 + * Target year must be 4 digits + */ + private function org_events_count($user, $org, $target_month, $target_year) { + $events_count = 0; + + $start_date = $target_year.'-'.$target_month.'-01'; + if($target_month == 12) { + $end_date = ($target_year+1).'-01-01'; + } else { + $end_date = $target_year.'-'.($target_month+1).'-01'; + } + $conditions = array('Event.orgc_id' => $org['Organisation']['id'], 'Event.date >=' => $start_date, 'Event.date <' => $end_date); + + //This is required to enforce the ACL (not pull directly from the DB) + $eventIds = $this->Event->fetchSimpleEventIds($user, array('conditions' => $conditions)); + + if(!empty($eventIds)) { + $params = array('Event.id' => $eventIds); + $events = $this->Event->find('all', array('conditions' => array('AND' => $params))); + foreach($events as $event) { + $events_count+= 1; + } + } + return $events_count; + } + + private function filter_ghost_orgs(&$data, $orgs){ + foreach ($data['data'] as &$item) { + foreach(array_keys($orgs) as $org_name) { + unset($item[$org_name]); + } + } + } + + public function handler($user, $options = array()) + { + $this->Log = ClassRegistry::init('Log'); + $this->Org = ClassRegistry::init('Organisation'); + $this->Event = ClassRegistry::init('Event'); + $orgs = $this->Org->find('all', array( 'conditions' => array('Organisation.local' => 1))); + $current_month = date('n'); + $current_year = date('Y'); + $limit = 6; // months + if(!empty($options['months'])) { + $limit = (int) ($options['months']); + } + $offset = 0; + $ghost_orgs = array(); // track orgs without any contribution + // We start by putting all orgs_id in there: + foreach($orgs as $org) { + // We check for blocklisted orgs + if(!empty($options['blocklist_orgs']) && in_array($org['Organisation']['name'], $options['blocklist_orgs'])) { + unset($orgs[$offset]); + } else { + $ghost_orgs[$org['Organisation']['name']] = true; + } + $offset++; + } + $data = array(); + $data['data'] = array(); + for ($i=0; $i < $limit; $i++) { + $target_month = $current_month - $i; + $target_year = $current_year; + if ($target_month < 1) { + $target_month += 12; + $target_year -= 1; + } + $item = array(); + $item ['date'] = $target_year.'-'.$target_month.'-01'; + foreach($orgs as $org) { + $count = $this->org_events_count($user, $org, $target_month, $target_year); + if($options['logarithmic'] === "true" || $options['logarithmic'] === "1") { + $item[$org['Organisation']['name']] = (int) round(log($count, 1.1)); // taking the logarithmic view + } else if(empty($options['logarithmic']) || $options['logarithmic'] === "true" || $options['logarithmic'] === "1"){ + $item[$org['Organisation']['name']] = $count; + } + // if a positive score is detected at least once it's enough to be + // considered for the graph + if($count > 0) { + unset($ghost_orgs[$org['Organisation']['name']]); + } + } + $data['data'][] = $item; + } + $this->filter_ghost_orgs($data, $ghost_orgs); + return $data; + } +} diff --git a/app/Lib/Dashboard/OrgEvolutionLineWidget.php b/app/Lib/Dashboard/OrgEvolutionLineWidget.php new file mode 100644 index 000000000..bdc78a4e8 --- /dev/null +++ b/app/Lib/Dashboard/OrgEvolutionLineWidget.php @@ -0,0 +1,126 @@ + 'A list of filters by organisation meta information (nationality, sector, type, name, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + 'start_date' => 'Start date, expressed in Y-m-d format (e.g. 2012-10-01)', + 'local' => 'Should the list only show local organisations? (boolean or list of booleans, defaults to 1. To get both sets, use [0,1])' + ]; + private $validFilterKeys = [ + 'nationality', + 'sector', + 'type', + 'name', + 'uuid' + ]; + + public $placeholder = + '{ + "filter": { + "sector": "financial" + }, + "start_date": "2017-01", +}'; + + private $Organisation = null; + + private function timeConditions($options) + { + if (!empty($options['start_date'])) { + $condition = strtotime($options['start_date']); + } else { + $condition = strtotime('2012-10-01'); + } + $datetime = new DateTime(); + $datetime->setTimestamp($condition); + return $datetime->format('Y-m-d H:i:s'); + } + + public function handler($user, $options = array()) + { + $this->Organisation = ClassRegistry::init('Organisation'); + $params = [ + 'conditions' => [ + 'AND' => ['Organisation.local' => !isset($options['local']) ? 1 : $options['local']] + ], + 'limit' => 10, + 'recursive' => -1 + ]; + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + $timeConditions = $this->timeConditions($options); + if ($timeConditions) { + $params['conditions']['AND'][] = ['Organisation.date_created >=' => $timeConditions]; + } + $raw = $this->Organisation->find('all', [ + 'recursive' => -1, + 'conditions' => $params['conditions'], + 'fields' => ['DATE_FORMAT(date_created, "%Y-%m") AS date', 'count(id) AS count'], + 'group' => 'MONTH(date_created), YEAR(date_created)' + + ]); + usort($raw, [$this, 'sortByCreationDate']); + $raw_padded = []; + $total = 0; + $default_start_date = empty($raw) ? '2012-10-01' : ($raw[0][0]['date'] . '-01'); + $start = new DateTime(empty($options['start_date']) ? $default_start_date : $options['start_date']); + $end = new DateTime(date('Y-m') . '-01'); + $interval = DateInterval::createFromDateString('1 month'); + $period = new DatePeriod($start, $interval, $end); + foreach ($period as $dt) { + $raw_padded[$dt->format('Y-m') . '-01'] = 0; + } + foreach ($raw as $datapoint) { + $raw_padded[$datapoint[0]['date'] . '-01'] = (int)$datapoint[0]['count']; + } + $total = 0; + foreach ($raw_padded as $date => $count) { + $total += $count; + $raw_padded[$date] = $total; + } + $data = []; + foreach ($raw_padded as $date => $count) { + $data['data'][] = [ + 'Organisations' => (int)$count, + 'date' => $date + ]; + } + return $data; + } + + private function sortByCreationDate($a, $b) { + if ($a[0]['date'] > $b[0]['date']) { + return 1; + } else { + return -1; + } + return 0; + } +} diff --git a/app/Lib/Dashboard/OrganisationListWidget.php b/app/Lib/Dashboard/OrganisationListWidget.php new file mode 100644 index 000000000..db988b5a0 --- /dev/null +++ b/app/Lib/Dashboard/OrganisationListWidget.php @@ -0,0 +1,81 @@ + 'A list of filters by organisation meta information (sector, type, local (- expects a boolean or a list of boolean values)) to include. (dictionary, prepending values with ! uses them as a negation)', + 'limit' => 'Limits the number of displayed tags. Default: 10' + ]; + public $cacheLifetime = null; + public $autoRefreshDelay = false; + private $validFilterKeys = [ + 'sector', + 'type', + 'local' + ]; + public $placeholder = +'{ + "filter": { + "type": "Member", + "local": [0,1] + } +}'; + private $Organisation = null; + + public $countryCodes = []; + + public function handler($user, $options = array()) + { + App::uses('WidgetToolkit', 'Lib/Dashboard/Tools'); + $WidgetToolkit = new WidgetToolkit(); + $this->countryCodes = $WidgetToolkit->getCountryCodeMapping(); + $params = [ + 'conditions' => [ + 'Nationality !=' => '' + ] + ]; + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + $this->Organisation = ClassRegistry::init('Organisation'); + $orgs = $this->Organisation->find('all', [ + 'recursive' => -1, + 'fields' => ['Organisation.nationality', 'COUNT(Organisation.nationality) AS frequency'], + 'conditions' => $params['conditions'], + 'group' => ['Organisation.nationality'] + ]); + $results = []; + foreach($orgs as $org) { + $country = $org['Organisation']['nationality']; + $count = $org['0']['frequency']; + if (isset($this->countryCodes[$country])) { + $countryCode = $this->countryCodes[$country]; + $results[$countryCode] = $count; + } + } + arsort($results); + return ['data' => $results]; + } +} +?> diff --git a/app/Lib/Dashboard/OrganisationMapWidget.php b/app/Lib/Dashboard/OrganisationMapWidget.php new file mode 100644 index 000000000..4929ac013 --- /dev/null +++ b/app/Lib/Dashboard/OrganisationMapWidget.php @@ -0,0 +1,80 @@ + 'A list of filters by organisation meta information (sector, type, local (- expects a boolean or a list of boolean values)) to include. (dictionary, prepending values with ! uses them as a negation)', + 'limit' => 'Limits the number of displayed tags. Default: 10' + ]; + public $cacheLifetime = null; + public $autoRefreshDelay = false; + private $validFilterKeys = [ + 'sector', + 'type', + 'local' + ]; + public $placeholder = +'{ + "filter": { + "type": "Member", + "local": [0,1] + } +}'; + private $Organisation = null; + + public $countryCodes = []; + + public function handler($user, $options = array()) + { + App::uses('WidgetToolkit', 'Lib/Dashboard/Tools'); + $WidgetToolkit = new WidgetToolkit(); + $this->countryCodes = $WidgetToolkit->getCountryCodeMapping(); + $params = [ + 'conditions' => [ + 'Nationality !=' => '' + ] + ]; + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + $this->Organisation = ClassRegistry::init('Organisation'); + $orgs = $this->Organisation->find('all', [ + 'recursive' => -1, + 'fields' => ['Organisation.nationality', 'COUNT(Organisation.nationality) AS frequency'], + 'conditions' => $params['conditions'], + 'group' => ['Organisation.nationality'] + ]); + $results = ['data' => [], 'scope' => 'Organisations']; + foreach($orgs as $org) { + $country = $org['Organisation']['nationality']; + $count = $org['0']['frequency']; + if (isset($this->countryCodes[$country])) { + $countryCode = $this->countryCodes[$country]; + $results['data'][$countryCode] = $count; + } + } + return $results; + } +} +?> diff --git a/app/Lib/Dashboard/RecentSightingsWidget.php b/app/Lib/Dashboard/RecentSightingsWidget.php index 7a34a6c6d..eeb64ab53 100644 --- a/app/Lib/Dashboard/RecentSightingsWidget.php +++ b/app/Lib/Dashboard/RecentSightingsWidget.php @@ -35,21 +35,21 @@ class RecentSightingsWidget $data = array(); $count = 0; - foreach (JsonTool::decode($Sighting->restSearch($user, 'json', $filters)->intoString())->{'response'} as $el) { - $sighting = $el->{'Sighting'}; - $event = $sighting->{'Event'}; - $attribute = $sighting->{'Attribute'}; + foreach (JsonTool::decode($Sighting->restSearch($user, 'json', $filters)->intoString())['response'] as $el) { + $sighting = $el['Sighting']; + $event = $sighting['Event']; + $attribute = $sighting['Attribute']; - if ($sighting->{'type'} == 0) $type = "Sighting"; - elseif ($sighting->{'type'} == 1) $type = "False positive"; + if ($sighting['type'] == 0) $type = "Sighting"; + elseif ($sighting['type'] == 1) $type = "False positive"; else $type = "Expiration"; - $output = $attribute->{'value'} . " (id: " . $attribute->{'id'} . ") in " . $event->{'info'} . " (id: " . $event->{'id'} . ")"; + $output = $attribute['value'] . " (id: " . $attribute['id'] . ") in " . $event['info'] . " (id: " . $event['id'] . ")"; $data[] = array( 'title' => $type, 'value' => $output, 'html' => sprintf( ' (Event %s)', - Configure::read('MISP.baseurl') . '/events/view/', $event->{'id'}, - $event->{'id'} + Configure::read('MISP.baseurl') . '/events/view/', $event['id'], + $event['id'] ) ); ++$count; diff --git a/app/Lib/Dashboard/Tools/WidgetToolkit.php b/app/Lib/Dashboard/Tools/WidgetToolkit.php new file mode 100644 index 000000000..1da149e94 --- /dev/null +++ b/app/Lib/Dashboard/Tools/WidgetToolkit.php @@ -0,0 +1,189 @@ + 'AF', + 'Albania' => 'AL', + 'Algeria' => 'DZ', + 'Angola' => 'AO', + 'Argentina' => 'AR', + 'Armenia' => 'AM', + 'Australia' => 'AU', + 'Austria' => 'AT', + 'Azerbaijan' => 'AZ', + 'Bahamas' => 'BS', + 'Bangladesh' => 'BD', + 'Belarus' => 'BY', + 'Belgium' => 'BE', + 'Belize' => 'BZ', + 'Benin' => 'BJ', + 'Bhutan' => 'BT', + 'Bolivia' => 'BO', + 'Bosnia and Herz.' => 'BA', + 'Botswana' => 'BW', + 'Brazil' => 'BR', + 'Brunei' => 'BN', + 'Bulgaria' => 'BG', + 'Burkina Faso' => 'BF', + 'Burundi' => 'BI', + 'Cambodia' => 'KH', + 'Cameroon' => 'CM', + 'Canada' => 'CA', + 'Central African Rep.' => 'CF', + 'Chad' => 'TD', + 'Chile' => 'CL', + 'China' => 'CN', + 'Colombia' => 'CO', + 'Congo' => 'CG', + 'Costa Rica' => 'CR', + 'Croatia' => 'HR', + 'Cuba' => 'CU', + 'Cyprus' => 'CY', + 'Czech Rep.' => 'CZ', + 'Czech Republic' => 'CZ', + 'Côte d\'Ivoire' => 'CI', + 'Dem. Rep. Congo' => 'CD', + 'Dem. Rep. Korea' => 'KP', + 'Denmark' => 'DK', + 'Djibouti' => 'DJ', + 'Dominican Rep.' => 'DO', + 'Ecuador' => 'EC', + 'Egypt' => 'EG', + 'El Salvador' => 'SV', + 'Eq. Guinea' => 'GQ', + 'Eritrea' => 'ER', + 'Estonia' => 'EE', + 'Ethiopia' => 'ET', + 'Falkland Is.' => 'FK', + 'Fiji' => 'FJ', + 'Finland' => 'FI', + 'Fr. S. Antarctic Lands' => 'TF', + 'France' => 'FR', + 'Gabon' => 'GA', + 'Gambia' => 'GM', + 'Georgia' => 'GE', + 'Germany' => 'DE', + 'Ghana' => 'GH', + 'Greece' => 'GR', + 'Greenland' => 'GL', + 'Guatemala' => 'GT', + 'Guinea' => 'GN', + 'Guinea-Bissau' => 'GW', + 'Guyana' => 'GY', + 'Haiti' => 'HT', + 'Honduras' => 'HN', + 'Hungary' => 'HU', + 'Iceland' => 'IS', + 'India' => 'IN', + 'Indonesia' => 'ID', + 'Iran' => 'IR', + 'Iraq' => 'IQ', + 'Ireland' => 'IE', + 'Ireland {Republic}' => 'IE', + 'Israel' => 'IL', + 'Italy' => 'IT', + 'Jamaica' => 'JM', + 'Japan' => 'JP', + 'Jordan' => 'JO', + 'Kazakhstan' => 'KZ', + 'Kenya' => 'KE', + 'Korea' => 'KR', + 'Kuwait' => 'KW', + 'Kyrgyzstan' => 'KG', + 'Lao PDR' => 'LA', + 'Latvia' => 'LV', + 'Lebanon' => 'LB', + 'Lesotho' => 'LS', + 'Liberia' => 'LR', + 'Libya' => 'LY', + 'Lithuania' => 'LT', + 'Luxembourg' => 'LU', + 'Macedonia' => 'MK', + 'Madagascar' => 'MG', + 'Mainland China' => 'CN', + 'Malawi' => 'MW', + 'Malaysia' => 'MY', + 'Mali' => 'ML', + 'Malta' => 'MT', + 'Mauritania' => 'MR', + 'Mexico' => 'MX', + 'Moldova' => 'MD', + 'Mongolia' => 'MN', + 'Montenegro' => 'ME', + 'Morocco' => 'MA', + 'Mozamb' => 'MZ', + 'Myanmar' => 'MM', + 'Namibia' => 'NA', + 'Nepal' => 'NP', + 'Netherlands' => 'NL', + 'New Caledonia' => 'NC', + 'New Zealand' => 'NZ', + 'Nicaragua' => 'NI', + 'Niger' => 'NE', + 'Nigeria' => 'NG', + 'Norway' => 'NO', + 'Oman' => 'OM', + 'Pakistan' => 'PK', + 'Palestine' => 'PS', + 'Panama' => 'PA', + 'Papua New Guinea' => 'PG', + 'Paraguay' => 'PY', + 'Peru' => 'PE', + 'Philippines' => 'PH', + 'Poland' => 'PL', + 'Portugal' => 'PT', + 'Puerto Rico' => 'PR', + 'Qatar' => 'QA', + 'Romania' => 'RO', + 'Russia' => 'RU', + 'Russian Federation' => 'RU', + 'Rwanda' => 'RW', + 'S. Sudan' => 'SS', + 'Saudi Arabia' => 'SA', + 'Senegal' => 'SN', + 'Serbia' => 'RS', + 'Sierra Leone' => 'SL', + 'Slovakia' => 'SK', + 'Slovenia' => 'SI', + 'Solomon Is.' => 'SB', + 'Somalia' => 'SO', + 'South Africa' => 'ZA', + 'Spain' => 'ES', + 'Sri Lanka' => 'LK', + 'Sudan' => 'SD', + 'Suriname' => 'SR', + 'Swaziland' => 'SZ', + 'Sweden' => 'SE', + 'Switzerland' => 'CH', + 'Syria' => 'SY', + 'Taiwan' => 'TW', + 'Tajikistan' => 'TJ', + 'Tanzania' => 'TZ', + 'Thailand' => 'TH', + 'Timor-Leste' => 'TL', + 'Togo' => 'TG', + 'Trinidad and Tobago' => 'TT', + 'Tunisia' => 'TN', + 'Turkey' => 'TR', + 'Turkmenistan' => 'TM', + 'Uganda' => 'UG', + 'Ukraine' => 'UA', + 'United Arab Emirates' => 'AE', + 'United Kingdom' => 'GB', + 'United States' => 'US', + 'Uruguay' => 'UY', + 'Uzbekistan' => 'UZ', + 'Vanuatu' => 'VU', + 'Venezuela' => 'VE', + 'Vietnam' => 'VN', + 'W. Sahara' => 'EH', + 'Yemen' => 'YE', + 'Zambia' => 'ZM', + 'Zimbabwe' => 'ZW' + ]; + } +} \ No newline at end of file diff --git a/app/Lib/Dashboard/TrendingAttributesWidget.php b/app/Lib/Dashboard/TrendingAttributesWidget.php new file mode 100644 index 000000000..957b6d7cc --- /dev/null +++ b/app/Lib/Dashboard/TrendingAttributesWidget.php @@ -0,0 +1,137 @@ + 'The time window, going back in seconds, that should be included. (allows for filtering by days - example: 5d. -1 Will fetch all historic data)', + 'exclude' => 'List of values to exclude - for example "8.8.8.8".', + 'threshold' => 'Limits the number of displayed attribute values. Default: 10', + 'type' => 'List of Attribute types to include', + 'category' => 'List of Attribute categories to exclude', + 'to_ids' => 'A list of to_ids settings accepted for the data displayed ([0], [1], [0,1])', + 'org_filter' => 'List of organisation filters to exclude events by, based on organisation meta-data (Organisation.sector, Organisation.type, Organisation.nationality). Pre-pending a value with a "!" negates it.' + ); + private $validOrgFilters = [ + 'sector', + 'type', + 'national', + 'uuid', + 'local' + ]; + public $placeholder = + '{ + "time_window": "7d", + "threshold": 15, + "org_filter": { + "sector": ["Financial"] + } +}'; + public $description = 'Widget showing the trending tags over the past x seconds, along with the possibility to include/exclude tags.'; + public $cacheLifetime = 3; + + private function getOrgList($options) + { + $organisationModel = ClassRegistry::init('Organisation'); + if (!empty($options['org_filter']) && is_array($options['org_filter'])) { + foreach ($this->validOrgFilters as $filterKey) { + if (isset($options['org_filter'][$filterKey])) { + if ($filterKey === 'local') { + $tempConditionBucket['Organisation.local'] = $options['org_filter']['local']; + } else { + if (!is_array($options['org_filter'][$filterKey])) { + $options['org_filter'][$filterKey] = [$options['org_filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['org_filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + } + if (!empty($tempConditionBucket)) { + $orgConditions[] = $tempConditionBucket; + } + } + } + return $organisationModel->find('column', [ + 'recursive' => -1, + 'conditions' => $orgConditions, + 'fields' => ['Organisation.id'] + ]); + } + } + + public function handler($user, $options = array()) + { + /** @var Event $eventModel */ + $attributeModel = ClassRegistry::init('Attribute'); + $threshold = empty($options['threshold']) ? 10 : $options['threshold']; + if (is_string($options['time_window']) && substr($options['time_window'], -1) === 'd') { + $time_window = ((int)substr($options['time_window'], 0, -1)) * 24 * 60 * 60; + } else { + $time_window = empty($options['time_window']) ? (7 * 24 * 60 * 60) : (int)$options['time_window']; + } + $conditions = $time_window === -1 ? [] : ['Attribute.timestamp >=' => time() - $time_window]; + $conditions['Attribute.deleted'] = 0; + $conditionsToParse = ['type', 'category', 'to_ids']; + foreach ($conditionsToParse as $parsedCondition) { + if (!empty($options[$parsedCondition])) { + $conditions['Attribute.' . $parsedCondition] = $options[$parsedCondition]; + } + } + if (!empty($options['exclude'])) { + $conditions['Attribute.value1 NOT IN'] = $options['exclude']; + } + if (!empty($options['org_filter'])) { + $conditions['Event.orgc_id IN'] = $this->getOrgList($options); + if (empty($conditions['Event.orgc_id IN'])) { + $conditions['Event.orgc_id IN'] = [-1]; + } + } + $attributeModel->virtualFields['frequency'] = 0; + if (!empty($user['Role']['perm_site_admin'])) { + $values = $attributeModel->find('all', [ + 'recursive' => -1, + 'fields' => ['Attribute.value1', 'count(Attribute.value1) as Attribute__frequency'], + 'group' => ['Attribute.value1'], + 'conditions' => $conditions, + 'contain' => ['Event.orgc_id'], + 'order' => 'count(Attribute.value1) desc', + 'limit' => empty($options['threshold']) ? 10 : $options['threshold'] + ]); + } else { + $conditions['AND'][] = [ + 'OR' => [ + 'Event.orgc_id' => $user['org_id'], + + ] + ]; + $values = $attributeModel->find('all', [ + 'recursive' => -1, + 'fields' => ['Attribute.value1', 'count(Attribute.value1) as Attribute__frequency', 'Attribute.distribution', 'Attribute.sharing_group_id'], + 'group' => 'Attribute.value1', + 'contain' => [ + 'Event.org_id', + 'Event.distribution', + 'Event.sharing_group_id', + 'Object.distribution', + 'Object.sharing_group_id' + ], + 'conditions' => $conditions, + 'order' => 'count(Attribute.value1) desc', + 'limit' => empty($options['threshold']) ? 10 : $options['threshold'] + ]); + } + $data = []; + foreach ($values as $value) { + $data[$value['Attribute']['value1']] = $value['Attribute']['frequency']; + } + return ['data' => $data]; + } +} diff --git a/app/Lib/Dashboard/TrendingTagsWidget.php b/app/Lib/Dashboard/TrendingTagsWidget.php index b9bdc9b7f..b9d61ce41 100644 --- a/app/Lib/Dashboard/TrendingTagsWidget.php +++ b/app/Lib/Dashboard/TrendingTagsWidget.php @@ -7,56 +7,115 @@ class TrendingTagsWidget public $width = 3; public $height = 4; public $params = array( - 'time_window' => 'The time window, going back in seconds, that should be included.', + 'time_window' => 'The time window, going back in seconds, that should be included. (allows for filtering by days - example: 5d. -1 Will fetch all historic data)', 'exclude' => 'List of substrings to exclude tags by - for example "sofacy" would exclude any tag containing sofacy.', 'include' => 'List of substrings to include tags by - for example "sofacy" would include any tag containing sofacy.', - 'threshold' => 'Limits the number of displayed tags. Default: 10' + 'threshold' => 'Limits the number of displayed tags. Default: 10', + 'filter_event_tags' => 'Filters to be applied on event tags', + 'over_time' => 'Toggle the trending to be over time', ); public $placeholder = -'{ - "time_window": "86400", + '{ + "time_window": "7d", "threshold": 15, "exclude": ["tlp:", "pap:"], - "include": ["misp-galaxy:", "my-internal-taxonomy"] + "include": ["misp-galaxy:", "my-internal-taxonomy"], + "filter_event_tags": ["misp-galaxy:threat-actor="APT 29"], }'; public $description = 'Widget showing the trending tags over the past x seconds, along with the possibility to include/exclude tags.'; - public $cacheLifetime = 600; + public $cacheLifetime = 3; public function handler($user, $options = array()) { /** @var Event $eventModel */ $eventModel = ClassRegistry::init('Event'); $threshold = empty($options['threshold']) ? 10 : $options['threshold']; - $params = [ - 'timestamp' => time() - (empty($options['time_window']) ? 8640000 : $options['time_window']), - ]; + if (is_string($options['time_window']) && substr($options['time_window'], -1) === 'd') { + $time_window = ((int)substr($options['time_window'], 0, -1)) * 24 * 60 * 60; + } else { + $time_window = empty($options['time_window']) ? (7 * 24 * 60 * 60) : (int)$options['time_window']; + } + $params = $time_window === -1 ? [] : ['timestamp' => time() - $time_window]; + + if (!empty($options['filter_event_tags'])) { + $params['event_tags'] = $options['filter_event_tags']; + } $eventIds = $eventModel->filterEventIds($user, $params); - $tags = []; $tagColours = []; - if (!empty($eventIds)) { - $eventTags = $eventModel->EventTag->find('all', [ - 'conditions' => ['EventTag.event_id' => $eventIds], - 'contain' => ['Tag' => ['fields' => ['name', 'colour']]], - 'recursive' => -1, - 'fields' => ['id'], - ]); + $allTags = []; + $this->render = $this->getRenderer($options); + if (!empty($options['over_time'])) { - foreach ($eventTags as $eventTag) { - $tagName = $eventTag['Tag']['name']; - if (isset($tags[$tagName])) { - $tags[$tagName]++; - } else if ($this->checkTag($options, $tagName)) { - $tags[$tagName] = 1; - $tagColours[$tagName] = $eventTag['Tag']['colour']; + $tagOvertime = []; + if (!empty($eventIds)) { + $events = $eventModel->fetchEvent($user, [ + 'eventid' => $eventIds, + 'order' => 'Event.timestamp', + 'metadata' => 1 + ]); + + foreach ($events as $event) { + $timestamp = $event['Event']['timestamp']; + $timestamp = strftime('%Y-%m-%d', $timestamp); + foreach ($event['EventTag'] as $tag) { + $tagName = $tag['Tag']['name']; + if (isset($tagOvertime[$timestamp][$tagName])) { + $tagOvertime[$timestamp][$tagName]++; + } else if ($this->checkTag($options, $tagName)) { + $tagOvertime[$timestamp][$tagName] = 1; + $tagColours[$tagName] = $tag['Tag']['colour']; + $allTags[$tagName] = $tagName; + } + } } } - arsort($tags); + $data = []; + $data['data'] = []; + foreach($tagOvertime as $date => $tagCount) { + $item = []; + $item['date'] = $date; + foreach ($allTags as $tagName) { + if (!empty($tagCount[$tagName])) { + $item[$tagName] = $tagCount[$tagName]; + } else { + $item[$tagName] = 0; + } + } + $data['data'][] = $item; + } + uasort($data['data'], function ($a, $b) { + return ($a['date'] < $b['date']) ? -1 : 1; + }); + $data['data'] = array_values($data['data']); + return $data; + } else { + $tags = []; + if (!empty($eventIds)) { + $eventTags = $eventModel->EventTag->find('all', [ + 'conditions' => ['EventTag.event_id' => $eventIds], + 'contain' => ['Tag' => ['fields' => ['name', 'colour']]], + 'recursive' => -1, + 'fields' => ['id'], + ]); + + foreach ($eventTags as $eventTag) { + $tagName = $eventTag['Tag']['name']; + if (isset($tags[$tagName])) { + $tags[$tagName]++; + } else if ($this->checkTag($options, $tagName)) { + $tags[$tagName] = 1; + $tagColours[$tagName] = $eventTag['Tag']['colour']; + } + } + + arsort($tags); + $data['data'] = array_slice($tags, 0, $threshold); + $data['colours'] = $tagColours; + } + } - - $data['data'] = array_slice($tags, 0, $threshold); - $data['colours'] = $tagColours; return $data; } @@ -80,4 +139,9 @@ class TrendingTagsWidget return true; } } + + public function getRenderer(array $options) + { + return !empty($options['over_time']) ? 'MultiLineChart' : 'BarChart'; + } } diff --git a/app/Lib/Dashboard/TresholdSightingsWidget.php b/app/Lib/Dashboard/TresholdSightingsWidget.php index 95fdf969a..d74dafeb4 100644 --- a/app/Lib/Dashboard/TresholdSightingsWidget.php +++ b/app/Lib/Dashboard/TresholdSightingsWidget.php @@ -31,21 +31,21 @@ class TresholdSightingsWidget $data = array(); $sightings_score = array(); - $restSearch = JsonTool::decode($Sighting->restSearch($user, 'json', $filters)->intoString())->{'response'}; + $restSearch = JsonTool::decode($Sighting->restSearch($user, 'json', $filters)->intoString())['response']; foreach ($restSearch as $el) { - $sighting = $el->{'Sighting'}; - $attribute = $sighting->{'Attribute'}; - $event = $sighting->{'Event'}; + $sighting = $el['Sighting']; + $attribute = $sighting['Attribute']; + $event = $sighting['Event']; - if (!array_key_exists($attribute->{'id'}, $sightings_score)) $sightings_score[$attribute->{'id'}] = array( 'value' => $attribute->{'value'}, + if (!array_key_exists($attribute['id'], $sightings_score)) $sightings_score[$attribute['id']] = array( 'value' => $attribute['value'], 'score' => 0, - 'event_title' => $event->{'info'}, - 'event_id' => $event->{'id'}); + 'event_title' => $event['info'], + 'event_id' => $event['id']); # Sighting - if ($sighting->{'type'} == 0) $sightings_score[$attribute->{'id'}]['score'] = $sightings_score[$attribute->{'id'}]['score'] - 1; + if ($sighting['type'] == 0) $sightings_score[$attribute['id']]['score'] = $sightings_score[$attribute['id']]['score'] - 1; # False Positive - elseif ($sighting->{'type'} == 1) $sightings_score[$attribute->{'id'}]['score'] = $sightings_score[$attribute->{'id'}]['score'] + 1; + elseif ($sighting['type'] == 1) $sightings_score[$attribute['id']]['score'] = $sightings_score[$attribute['id']]['score'] + 1; } foreach ($sightings_score as $attribute_id => $s) { diff --git a/app/Lib/Dashboard/UsageDataWidget.php b/app/Lib/Dashboard/UsageDataWidget.php index caebbae05..4741722ad 100644 --- a/app/Lib/Dashboard/UsageDataWidget.php +++ b/app/Lib/Dashboard/UsageDataWidget.php @@ -5,40 +5,87 @@ class UsageDataWidget public $render = 'SimpleList'; public $width = 2; public $height = 5; - public $params = array(); public $description = 'Shows usage data / statistics.'; public $cacheLifetime = false; - public $autoRefreshDelay = 3; + public $autoRefreshDelay = false; + public $params = [ + 'filter' => 'A list of filters by organisation meta information (nationality, sector, type, name, uuid) to include. (dictionary, prepending values with ! uses them as a negation)', + ]; + private $User = null; + private $Event = null; + private $Correlation = null; + private $Thread = null; + private $AuthKey = null; + private $redis = null; + + private $validFilterKeys = [ + 'nationality', + 'sector', + 'type', + 'name', + 'uuid' + ]; - public function handler($user, $options = array()){ + private $validFields = [ + 'Events', + 'Attributes', + 'Attributes / event', + 'Correlations', + 'Active proposals', + 'Users', + 'Users with PGP keys', + 'Organisations', + 'Local organisations', + 'Event creator orgs', + 'Average users / org', + 'Discussion threads', + 'Discussion posts' + ]; + + public function handler($user, $options = array()) { $this->User = ClassRegistry::init('User'); - - $orgsCount = $this->User->Organisation->find('count'); - $localOrgsParams['conditions']['Organisation.local'] = 1; - $localOrgsCount = $this->User->Organisation->find('count', $localOrgsParams); - - $thisMonth = strtotime('first day of this month'); + $this->redis = $this->User->setupRedis(); + if (!$this->redis) { + throw new NotFoundException(__('No redis connection found.')); + } $this->Event = ClassRegistry::init('Event'); - $eventsCount = $this->Event->find('count', array('recursive' => -1)); - $eventsCountMonth = $this->Event->find('count', array('conditions' => array('Event.timestamp >' => $thisMonth), 'recursive' => -1)); - - $this->Attribute = ClassRegistry::init('Attribute'); - $attributesCount = $this->Attribute->find('count', array('conditions' => array('Attribute.deleted' => 0), 'recursive' => -1)); - $attributesCountMonth = $this->Attribute->find('count', array('conditions' => array('Attribute.timestamp >' => $thisMonth, 'Attribute.deleted' => 0), 'recursive' => -1)); - $attributesPerEvent = round($attributesCount / $eventsCount); - - $this->Correlation = ClassRegistry::init('Correlation'); - $correlationsCount = $this->Correlation->find('count', array('recursive' => -1)) / 2; - - $proposalsCount = $this->Event->ShadowAttribute->find('count', array('recursive' => -1, 'conditions' => array('deleted' => 0))); - - $usersCount = $this->User->find('count', array('recursive' => -1)); - $usersCountPgp = $this->User->find('count', array('recursive' => -1, 'conditions' => array('User.gpgkey !=' => ''))); - $usersCountPgpPercentage = round(100* ($usersCountPgp / $usersCount), 1); - $contributingOrgsCount = $this->Event->find('count', array('recursive' => -1, 'group' => array('Event.orgc_id'))); - $averageUsersPerOrg = round($usersCount / $localOrgsCount, 1); - $this->Thread = ClassRegistry::init('Thread'); + $this->Correlation = ClassRegistry::init('Correlation'); + $thisMonth = strtotime('first day of this month'); + $orgConditions = []; + $orgIdList = null; + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $orgConditions[] = $tempConditionBucket; + } + } + } + $orgIdList = $this->User->Organisation->find('column', [ + 'recursive' => -1, + 'conditions' => $orgConditions, + 'fields' => ['Organisation.id'] + ]); + } + $eventsCount = $this->getEventsCount($orgConditions, $orgIdList, $thisMonth); + $attributesCount = $this->getAttributesCount($orgConditions, $orgIdList, $thisMonth); + $usersCount = $this->getUsersCount($orgConditions, $orgIdList, $thisMonth); + $usersCountPgp = $this->getUsersCountPgp($orgConditions, $orgIdList, $thisMonth); + $localOrgsCount = $this->getLocalOrgsCount($orgConditions, $orgIdList, $thisMonth); + + $threadCount = $this->Thread->find('count', array('conditions' => array('Thread.post_count >' => 0), 'recursive' => -1)); $threadCountMonth = $this->Thread->find('count', array('conditions' => array('Thread.date_created >' => date("Y-m-d H:i:s", $thisMonth), 'Thread.post_count >' => 0), 'recursive' => -1)); @@ -47,21 +94,69 @@ class UsageDataWidget //Monhtly data is not added to the widget at the moment, could optionally add these later and give user choice? - $statistics = array( - array('title' => 'Events', 'value' => $eventsCount), - array('title' => 'Attributes', 'value' => $attributesCount), - array('title' => 'Attributes / event', 'value' => $attributesPerEvent), - array('title' => 'Correlations', 'value' => $correlationsCount), - array('title' => 'Active proposals', 'value' => $proposalsCount), - array('title' => 'Users', 'value' => $usersCount), - array('title' => 'Users with PGP keys', 'value' => $usersCountPgp . ' (' . $usersCountPgpPercentage . '%)'), - array('title' => 'Organisations', 'value' => $orgsCount), - array('title' => 'Local organisations', 'value' => $localOrgsCount), - array('title' => 'Event creator orgs', 'value' => $contributingOrgsCount), - array('title' => 'Average users / org', 'value' => $averageUsersPerOrg), - array('title' => 'Discussions threads', 'value' => $threadCount), - array('title' => 'Discussion posts', 'value' => $postCount) - ); + $statistics = [ + 'Events' => [ + 'title' => 'Events', + 'value' => $eventsCount, + 'change' => $this->getEventsCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Attributes' => [ + 'title' => 'Attributes', + 'value' => $attributesCount, + 'change' => $this->getAttributesCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Attributes / event' => [ + 'title' => 'Attributes / event', + 'value' => $eventsCount ? round($attributesCount / $eventsCount) : 0 + ], + 'Correlations' => [ + 'title' => 'Correlations', + 'value' => $this->getCorrelationsCount($orgConditions, $orgIdList, $thisMonth) + ], + 'Active proposals' => [ + 'title' => 'Active proposals', + 'value' => $this->getProposalsCount($orgConditions, $orgIdList, $thisMonth) + ], + 'Users' => [ + 'title' => 'Users', + 'value' => $usersCount, + 'change' => $this->getUsersCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Users with PGP keys' => [ + 'title' => 'Users with PGP keys', + 'value' => sprintf( + '%s (%s %%)', + $usersCountPgp, + $usersCount ? round(100* ($usersCountPgp / $usersCount), 1) : 0 + ) + ], + 'Organisations' => [ + 'title' => 'Organisations', + 'value' => $this->getOrgsCount($orgConditions, $orgIdList, $thisMonth), + 'change' => $this->getOrgsCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Local organisations' => [ + 'title' => 'Local organisations', + 'value' => $localOrgsCount, + 'change' => $this->getLocalOrgsCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Event creator orgs' => [ + 'title' => 'Event creator orgs', 'value' => $this->getContributingOrgsCount($orgConditions, $orgIdList, $thisMonth) + ], + 'Average users / org' => [ + 'title' => 'Average users / org', 'value' => round($usersCount / $localOrgsCount, 1) + ], + 'Discussion threads' => [ + 'title' => 'Discussions threads', + 'value' => $this->getThreadsCount($orgConditions, $orgIdList, $thisMonth), + 'change' => $this->getThreadsCountMonth($orgConditions, $orgIdList, $thisMonth) + ], + 'Discussion posts' => [ + 'title' => 'Discussion posts', + 'value' => $this->getPostsCount($orgConditions, $orgIdList, $thisMonth), + 'change' => $this->getPostsCountMonth($orgConditions, $orgIdList, $thisMonth) + ] + ]; if(!empty(Configure::read('Security.advanced_authkeys'))){ $this->AuthKey = ClassRegistry::init('AuthKey'); $authkeysCount = $this->AuthKey->find('count', array('recursive' => -1)); @@ -70,6 +165,239 @@ class UsageDataWidget return $statistics; } + private function getEventsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = []; + if (!empty($orgIdList)) { + $conditions['AND'][] = ['Event.orgc_id IN' => $orgIdList]; + } + return $this->Event->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getCorrelationsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = []; + if (!empty($orgIdList)) { + $conditions['AND']['OR'][] = ['Correlation.org_id IN' => $orgIdList]; + $conditions['AND']['OR'][] = ['Correlation.1_org_id IN' => $orgIdList]; + } + return $this->Correlation->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getEventsCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['Event.timestamp >' => $thisMonth]; + if (!empty($orgIdList)) { + $conditions['AND'][] = ['Event.orgc_id IN' => $orgIdList]; + } + return $this->Event->find('count', [ + 'conditions' => $conditions, + 'recursive' => -1 + ]); + } + + private function getAttributesCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['Attribute.deleted' => 0]; + if (!empty($orgIdList)) { + $conditions['AND'][] = ['Event.orgc_id IN' => $orgIdList]; + } + $hash = hash('sha256', json_encode($orgIdList)); + $count = $this->redis->get('misp:dashboard:attribute_count:' . $hash); + if (empty($count)) { + $count = $this->Event->Attribute->find('count', [ + 'conditions' => $conditions, + 'contain' => ['Event'], + 'recursive' => -1 + ]); + $this->redis->setEx('misp:dashboard:attribute_count:' . $hash, 3600, $count); + } + return $count; + } + + private function getAttributesCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['Attribute.timestamp >' => $thisMonth, 'Attribute.deleted' => 0]; + if (!empty($orgIdList)) { + $conditions['AND'][] = ['Event.orgc_id IN' => $orgIdList]; + } + return $this->Event->Attribute->find('count', [ + 'conditions' => $conditions, + 'contain' => 'Event.orgc_id', + 'recursive' => -1 + ]); + } + + private function getOrgsCount($orgConditions, $orgIdList, $thisMonth) + { + return $this->User->Organisation->find('count', [ + 'conditions' => [ + 'AND' => $orgConditions + ] + ]); + } + + private function getOrgsCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $datetime = new DateTime(); + $datetime->setTimestamp($thisMonth); + $thisMonth = $datetime->format('Y-m-d H:i:s'); + return $this->User->Organisation->find('count', [ + 'conditions' => [ + 'AND' => $orgConditions, + 'Organisation.date_created >' => $thisMonth + ] + ]); + } + + private function getLocalOrgsCount($orgConditions, $orgIdList, $thisMonth) + { + return $this->User->Organisation->find('count', [ + 'conditions' => [ + 'Organisation.local' => 1, + 'AND' => $orgConditions + ] + ]); + } + + private function getLocalOrgsCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $datetime = new DateTime(); + $datetime->setTimestamp($thisMonth); + $thisMonth = $datetime->format('Y-m-d H:i:s'); + return $this->User->Organisation->find('count', [ + 'conditions' => [ + 'Organisation.local' => 1, + 'AND' => $orgConditions, + 'Organisation.date_created >' => $thisMonth + ] + ]); + } + + private function getProposalsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['deleted' => 0]; + if (!empty($orgIdList)) { + $conditions['ShadowAttribute.org_id IN'] = $orgIdList; + } + return $this->Event->ShadowAttribute->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getUsersCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = []; + if (!empty($orgIdList)) { + $conditions['User.org_id IN'] = $orgIdList; + } + return $this->User->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getUsersCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['User.date_created >' => $thisMonth]; + if (!empty($orgIdList)) { + $conditions['User.org_id IN'] = $orgIdList; + } + return $this->User->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getUsersCountPgp($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['User.gpgkey !=' => '']; + if (!empty($orgIdList)) { + $conditions['User.org_id IN'] = $orgIdList; + } + return $this->User->find('count', [ + 'recursive' => -1, + 'conditions' => $conditions + ]); + } + + private function getContributingOrgsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = []; + if ($orgConditions) { + $conditions['AND'][] = ['Event.orgc_id IN' => $orgIdList]; + } + return $this->Event->find('count', [ + 'recursive' => -1, + 'group' => ['Event.orgc_id'], + 'conditions' => $conditions + ]); + } + + private function getThreadsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = ['Thread.post_count >' => 0]; + if ($orgConditions) { + $conditions['AND'][] = ['Thread.org_id IN' => $orgIdList]; + } + return $this->Thread->find('count', [ + 'conditions' => $conditions, + 'recursive' => -1 + ]); + } + + private function getThreadsCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $conditions = [ + 'Thread.post_count >' => 0, + 'Thread.date_created >=' => $thisMonth + ]; + if ($orgConditions) { + $conditions['AND'][] = ['Thread.org_id IN' => $orgIdList]; + } + return $this->Thread->find('count', [ + 'conditions' => $conditions, + 'recursive' => -1 + ]); + } + + private function getPostsCount($orgConditions, $orgIdList, $thisMonth) + { + $conditions = []; + if ($orgConditions) { + $conditions['AND'][] = ['User.org_id IN' => $orgIdList]; + } + return $this->Thread->Post->find('count', [ + 'conditions' => $conditions, + 'contain' => ['User.org_id'], + 'recursive' => -1 + ]); + } + + private function getPostsCountMonth($orgConditions, $orgIdList, $thisMonth) + { + $conditions = [ + 'Post.date_created >=' => $thisMonth + ]; + if ($orgConditions) { + $conditions['AND'][] = ['User.org_id IN' => $orgIdList]; + } + return $this->Thread->Post->find('count', [ + 'conditions' => $conditions, + 'contain' => ['User.org_id'], + 'recursive' => -1 + ]); + } + + +/* There is nothing sensitive in here. public function checkPermissions($user) { if (empty($user['Role']['perm_site_admin'])) { @@ -77,4 +405,5 @@ class UsageDataWidget } return true; } +*/ } diff --git a/app/Lib/Dashboard/UserContributionToplistWidget.php b/app/Lib/Dashboard/UserContributionToplistWidget.php new file mode 100644 index 000000000..ec434c82b --- /dev/null +++ b/app/Lib/Dashboard/UserContributionToplistWidget.php @@ -0,0 +1,138 @@ + 'How many days back should the list go - for example, setting 7 will only show contributions in the past 7 days. (integer)', + 'month' => 'Who contributed most this month? (boolean)', + 'previous_month' => 'Who contributed most the previous, finished month? (boolean)', + 'year' => 'Which contributed most this year? (boolean)', + 'filter' => 'A list of filters by organisation meta information (nationality, sector, type, name, uuid, local (- expects a boolean or a list of boolean values)) to include. (dictionary, prepending values with ! uses them as a negation)', + 'limit' => 'Limits the number of displayed tags. Default: 10' + ]; + public $cacheLifetime = null; + public $autoRefreshDelay = false; + private $validFilterKeys = [ + 'nationality', + 'sector', + 'type', + 'name', + 'uuid' + ]; + public $placeholder = +'{ + "days": "7d", + "threshold": 15, + "filter": { + "sector": "Financial" + } +}'; + private $Org = null; + private $Event = null; + + + private function timeConditions($options) + { + $limit = empty($options['limit']) ? 10 : $options['limit']; + if (!empty($options['days'])) { + $condition = strtotime(sprintf("-%s days", $options['days'])); + } else if (!empty($options['month'])) { + $condition = strtotime('first day of this month 00:00:00', time()); + } else if (!empty($options['previous_month'])) { + $condition = strtotime('first day of previous month 00:00:00', time()); + $end_condition = strtotime('last day of last month 23:59:59', time()); + } else if (!empty($options['year'])) { + $condition = strtotime('first day of this year 00:00:00', time()); + } else { + return null; + } + $conditions = []; + if (!empty($condition)) { + $datetime = new DateTime(); + $datetime->setTimestamp($condition); + $conditions['Event.timestamp >='] = $datetime->format('Y-m-d H:i:s'); + } + if (!empty($end_condition)) { + $datetime = new DateTime(); + $datetime->setTimestamp($end_condition); + $conditions['Event.timestamp <='] = $datetime->format('Y-m-d H:i:s'); + } + return $conditions; + } + + + public function handler($user, $options = array()) + { + $params = ['conditions' => []]; + $timeConditions = $this->timeConditions($options); + if ($timeConditions) { + $params['conditions']['AND'][] = $timeConditions; + } + if (!empty($options['filter']) && is_array($options['filter'])) { + foreach ($this->validFilterKeys as $filterKey) { + if (!empty($options['filter'][$filterKey])) { + if (!is_array($options['filter'][$filterKey])) { + $options['filter'][$filterKey] = [$options['filter'][$filterKey]]; + } + $tempConditionBucket = []; + foreach ($options['filter'][$filterKey] as $value) { + if ($value[0] === '!') { + $tempConditionBucket['Organisation.' . $filterKey . ' NOT IN'][] = mb_substr($value, 1); + } else { + $tempConditionBucket['Organisation.' . $filterKey . ' IN'][] = $value; + } + } + if (!empty($tempConditionBucket)) { + $params['conditions']['AND'][] = $tempConditionBucket; + } + } + } + } + if (isset($options['filter']['local'])) { + $params['conditions']['AND']['local'] = $options['filter']['local']; + } + + $this->Org = ClassRegistry::init('Organisation'); + $org_ids = $this->Org->find('list', [ + 'fields' => ['Organisation.id', 'Organisation.name'], + 'conditions' => $params['conditions'] + ]); + $userConditions = []; + if (!empty($org_ids)) { + $userConditions = ['User.org_id IN' => array_keys($org_ids)]; + } + $user_ids = $this->Org->User->find('list', [ + 'fields' => ['User.id', 'User.email'], + 'conditions' => $userConditions + ]); + $conditions = empty($user_ids) ? [] : ['Event.user_id IN' => array_keys($user_ids)]; + $this->Event = ClassRegistry::init('Event'); + $this->Event->virtualFields['frequency'] = 0; + $users = $this->Event->find('all', [ + 'recursive' => -1, + 'fields' => ['user_id', 'count(Event.user_id) as Event__frequency'], + 'group' => ['user_id'], + 'conditions' => $conditions, + 'order' => 'count(Event.user_id) desc', + 'limit' => empty($options['limit']) ? 10 : $options['limit'] + ]); + $results = []; + foreach($users as $user) { + $results[$user_ids[$user['Event']['user_id']]] = $user['Event']['frequency']; + } + return ['data' => $results]; + } + + public function checkPermissions($user) + { + if (empty(Configure::read('Security.disclose_user_emails')) && empty($user['Role']['perm_site_admin'])) { + return false; + } + return true; + } +} +?> diff --git a/app/Lib/Dashboard/WhoamiWidget.php b/app/Lib/Dashboard/WhoamiWidget.php index a95620908..40281df22 100644 --- a/app/Lib/Dashboard/WhoamiWidget.php +++ b/app/Lib/Dashboard/WhoamiWidget.php @@ -28,7 +28,7 @@ class WhoamiWidget array('title' => 'Email', 'value' => $user['email']), array('title' => 'Role', 'value' => $user['Role']['name']), array('title' => 'Organisation', 'value' => $user['Organisation']['name']), - array('title' => 'IP', 'value' => empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['REMOTE_ADDR'] : $_SERVER['HTTP_X_FORWARDED_FOR']), + array('title' => 'IP', 'value' => $this->Log->_remoteIp()), array('title' => 'Last logins', 'value' => $entries) ); } diff --git a/app/Lib/Export/Stix2Export.php b/app/Lib/Export/Stix2Export.php index a3c81f82a..66e1fdebc 100644 --- a/app/Lib/Export/Stix2Export.php +++ b/app/Lib/Export/Stix2Export.php @@ -4,7 +4,7 @@ App::uses('StixExport', 'Export'); class Stix2Export extends StixExport { protected $__attributes_limit = 15000; - protected $__default_version = '2.0'; + protected $__default_version = '2.1'; protected $__sane_versions = array('2.0', '2.1'); protected function __initiate_framing_params() diff --git a/app/Lib/Export/StixExport.php b/app/Lib/Export/StixExport.php index 6abc20b75..4c759e991 100644 --- a/app/Lib/Export/StixExport.php +++ b/app/Lib/Export/StixExport.php @@ -83,6 +83,12 @@ abstract class StixExport if ($this->__empty_file) { $this->__tmp_file->close(); $this->__tmp_file->delete(); + if (empty($this->__filenames)) { + $framing = $this->getFraming(); + $tmpFile = new TmpFileTool(); + $tmpFile->write($framing['header'] . $framing['footer']); + return $tmpFile; + } } else { if (!empty($this->__event_galaxies)) { $this->__write_event_galaxies(); diff --git a/app/Lib/Tools/AttachmentTool.php b/app/Lib/Tools/AttachmentTool.php index 52f227832..d95cdd6cd 100644 --- a/app/Lib/Tools/AttachmentTool.php +++ b/app/Lib/Tools/AttachmentTool.php @@ -157,7 +157,7 @@ class AttachmentTool $filepath = $this->attachmentDir() . DS . $path; $file = new File($filepath); if (!is_file($file->path)) { - throw new NotFoundException("File '$filepath' does not exists."); + throw new NotFoundException("File '$filepath' does not exist."); } } diff --git a/app/Lib/Tools/AttributeValidationTool.php b/app/Lib/Tools/AttributeValidationTool.php index 739d3c01d..752fbe1e0 100644 --- a/app/Lib/Tools/AttributeValidationTool.php +++ b/app/Lib/Tools/AttributeValidationTool.php @@ -202,7 +202,7 @@ class AttributeValidationTool $value = substr($value, 2); // remove 'AS' } if (strpos($value, '.') !== false) { // maybe value is in asdot notation - $parts = explode('.', $value); + $parts = explode('.', $value, 2); if (self::isPositiveInteger($parts[0]) && self::isPositiveInteger($parts[1])) { return $parts[0] * 65536 + $parts[1]; } @@ -224,7 +224,6 @@ class AttributeValidationTool switch ($type) { case 'md5': case 'imphash': - case 'telfhash': case 'sha1': case 'sha224': case 'sha256': @@ -255,6 +254,11 @@ class AttributeValidationTool return true; } return __('Checksum has an invalid length or format (expected: at least 35 hexadecimal characters, optionally starting with t1 instead of hexadecimal characters). Please double check the value or select type "other".'); + case 'telfhash': + if (self::isTelfhashValid($value)) { + return true; + } + return __('Checksum has an invalid length or format (expected: %s or %s hexadecimal characters). Please double check the value or select type "other".', 70, 72); case 'pehash': if (self::isHashValid('pehash', $value)) { return true; @@ -412,12 +416,12 @@ class AttributeValidationTool } return __('Email address has an invalid format. Please double check the value or select type "other".'); case 'vulnerability': - if (preg_match("#^(CVE-)[0-9]{4}(-)[0-9]{4,}$#", $value)) { + if (preg_match("#^CVE-[0-9]{4}-[0-9]{4,}$#", $value)) { return true; } return __('Invalid format. Expected: CVE-xxxx-xxxx...'); case 'weakness': - if (preg_match("#^(CWE-)[0-9]{1,}$#", $value)) { + if (preg_match("#^CWE-[0-9]+$#", $value)) { return true; } return __('Invalid format. Expected: CWE-x...'); @@ -526,6 +530,7 @@ class AttributeValidationTool case 'favicon-mmh3': case 'chrome-extension-id': case 'mobile-application-id': + case 'azure-application-id': case 'named pipe': if (strpos($value, "\n") !== false) { return __('Value must not contain new line character.'); @@ -582,6 +587,28 @@ class AttributeValidationTool throw new InvalidArgumentException("Unknown type $type."); } + /** + * This method will generate all valid types for given value. + * @param array $types Typos to check + * @param array $compositeTypes Composite types + * @param string $value Values to check + * @return array + */ + public static function validTypesForValue(array $types, array $compositeTypes, $value) + { + $possibleTypes = []; + foreach ($types as $type) { + if (in_array($type, $compositeTypes, true) && substr_count($value, '|') !== 1) { + continue; // value is not in composite format + } + $modifiedValue = AttributeValidationTool::modifyBeforeValidation($type, $value); + if (AttributeValidationTool::validate($type, $modifiedValue) === true) { + $possibleTypes[] = $type; + } + } + return $possibleTypes; + } + /** * @param string $value * @return bool @@ -612,6 +639,15 @@ class AttributeValidationTool return strlen($value) > 35 && ctype_xdigit($value); } + /** + * @param string $value + * @return bool + */ + private static function isTelfhashValid($value) + { + return strlen($value) == 70 || strlen($value) == 72; + } + /** * @param string $type diff --git a/app/Lib/Tools/BackgroundJobsTool.php b/app/Lib/Tools/BackgroundJobsTool.php index 19d56dada..37e53883e 100644 --- a/app/Lib/Tools/BackgroundJobsTool.php +++ b/app/Lib/Tools/BackgroundJobsTool.php @@ -161,7 +161,7 @@ class BackgroundJobsTool } RedisTool::unlink($this->RedisConnection, self::DATA_CONTENT_PREFIX . ':' . $uuid); return $data; - } else if ($path[0] === '/') { // deprecated storage location when not full path is provided + } else if ($path[0] !== '/') { // deprecated storage location when not full path is provided $path = APP . 'tmp/cache/ingest' . DS . $path; } return JsonTool::decode(FileAccessTool::readAndDelete($path)); diff --git a/app/Lib/Tools/CustomPaginationTool.php b/app/Lib/Tools/CustomPaginationTool.php index 525de8201..ad052af38 100644 --- a/app/Lib/Tools/CustomPaginationTool.php +++ b/app/Lib/Tools/CustomPaginationTool.php @@ -27,6 +27,8 @@ class CustomPaginationTool $params['options'][$v] = $options[$v]; } } + $params['page'] = is_numeric($params['page']) ? $params['page'] : 1; + $params['limit'] = is_numeric($params['limit']) ? $params['limit'] : 60; $maxPage = floor($params['count'] / $params['limit']); if ($params['count'] % $params['limit'] != 0) { $maxPage += 1; diff --git a/app/Lib/Tools/EventTimelineTool.php b/app/Lib/Tools/EventTimelineTool.php index 145ab9909..d684d8882 100644 --- a/app/Lib/Tools/EventTimelineTool.php +++ b/app/Lib/Tools/EventTimelineTool.php @@ -57,6 +57,12 @@ $event['Attribute'] = array(); } + if (!empty($fullevent[0]['Sighting'])) { + $event['Sighting'] = $fullevent[0]['Sighting']; + } else { + $event['Sighting'] = array(); + } + return $event; } @@ -81,6 +87,11 @@ $attribute = array(); } + $sightingsAttributeMap = []; + foreach ($event['Sighting'] as $sighting) { + $sightingsAttributeMap[$sighting['attribute_id']][] = $sighting['date_sighting']; + } + // extract links and node type foreach ($attribute as $attr) { $toPush = array( @@ -93,6 +104,7 @@ 'first_seen' => $attr['first_seen'], 'last_seen' => $attr['last_seen'], 'attribute_type' => $attr['type'], + 'date_sighting' => $sightingsAttributeMap[$attr['id']] ?? [], 'is_image' => $this->__eventModel->Attribute->isImage($attr), ); $this->__json['items'][] = $toPush; @@ -134,6 +146,7 @@ 'group' => 'object_attribute', 'timestamp' => $obj_attr['timestamp'], 'attribute_type' => $obj_attr['type'], + 'date_sighting' => $sightingsAttributeMap[$attr['id']] ?? [], 'is_image' => $this->__eventModel->Attribute->isImage($obj_attr), ); $toPush_obj['Attribute'][] = $toPush_attr; diff --git a/app/Lib/Tools/FileAccessTool.php b/app/Lib/Tools/FileAccessTool.php index 3e3aa01cd..b56338a21 100644 --- a/app/Lib/Tools/FileAccessTool.php +++ b/app/Lib/Tools/FileAccessTool.php @@ -107,8 +107,14 @@ class FileAccessTool } if (file_put_contents($file, $content, LOCK_EX | (!empty($append) ? FILE_APPEND : 0)) === false) { - $freeSpace = disk_free_space($dir); - throw new Exception("An error has occurred while attempt to write to file `$file`. Maybe not enough space? ($freeSpace bytes left)"); + if (file_exists($file) && !is_writable($file)) { + $errorMessage = 'File is not writeable.'; + } else { + $freeSpace = disk_free_space($dir); + $errorMessage = "Maybe not enough space? ($freeSpace bytes left)"; + } + + throw new Exception("An error has occurred while attempt to write to file `$file`. $errorMessage"); } } diff --git a/app/Lib/Tools/RedisTool.php b/app/Lib/Tools/RedisTool.php index 4159322fd..8a68dfe20 100644 --- a/app/Lib/Tools/RedisTool.php +++ b/app/Lib/Tools/RedisTool.php @@ -1,7 +1,7 @@ connect($host, (int) $port)) { + $connection = empty($socket) ? $redis->connect($host, (int) $port) : $redis->connect($host); + if (!$connection) { throw new Exception("Could not connect to Redis: {$redis->getLastError()}"); } if (!empty($pass)) { diff --git a/app/Lib/Tools/SecurityAudit.php b/app/Lib/Tools/SecurityAudit.php index 944b63b24..29c7cc81e 100644 --- a/app/Lib/Tools/SecurityAudit.php +++ b/app/Lib/Tools/SecurityAudit.php @@ -138,7 +138,7 @@ class SecurityAudit if (!Configure::read('MISP.log_new_audit')) { $output['Logging'][] = [ 'hint', - __('New audit log stores more information, like used authkey ID or request ID that can help when analysing or correlating audit logs.'), + __('New audit log stores more information, like used authkey ID or request ID that can help when analysing or correlating audit logs. Set `MISP.log_new_audit` to `true` to enable.'), ]; } diff --git a/app/Lib/Tools/SyncTool.php b/app/Lib/Tools/SyncTool.php index aa621879b..157123d21 100644 --- a/app/Lib/Tools/SyncTool.php +++ b/app/Lib/Tools/SyncTool.php @@ -2,6 +2,9 @@ class SyncTool { + + const ALLOWED_CERT_FILE_EXTENSIONS = ['pem', 'crt']; + /** * Take a server as parameter and return a HttpSocket object using the ssl options defined in the server settings * @param array|null $server @@ -15,10 +18,10 @@ class SyncTool $params = ['compress' => true]; if (!empty($server)) { if (!empty($server[$model]['cert_file'])) { - $params['ssl_cafile'] = APP . "files" . DS . "certs" . DS . $server[$model]['id'] . '.pem'; + $params['ssl_cafile'] = APP . "files" . DS . "certs" . DS . $server[$model]['cert_file']; } if (!empty($server[$model]['client_cert_file'])) { - $params['ssl_local_cert'] = APP . "files" . DS . "certs" . DS . $server[$model]['id'] . '_client.pem'; + $params['ssl_local_cert'] = APP . "files" . DS . "certs" . DS . $server[$model]['client_cert_file']; } if (!empty($server[$model]['self_signed'])) { $params['ssl_allow_self_signed'] = true; diff --git a/app/Lib/Tools/WorkflowFormatConverterTool.php b/app/Lib/Tools/WorkflowFormatConverterTool.php index caa00e57a..267c317be 100644 --- a/app/Lib/Tools/WorkflowFormatConverterTool.php +++ b/app/Lib/Tools/WorkflowFormatConverterTool.php @@ -44,6 +44,17 @@ class WorkflowFormatConverterTool { $converted = []; $converted = JSONConverterTool::convert($event, false, true); + $eventTags = !empty($converted['Event']['Tag']) ? $converted['Event']['Tag'] : []; + if (!empty($converted['Event']['Attribute'])) { + foreach ($converted['Event']['Attribute'] as $i => $attribute) { + $converted['Event']['Attribute'][$i] = self::__propagateTagToAttributes($attribute, $eventTags); + } + } + if (!empty($converted['Event']['Object'])) { + foreach ($converted['Event']['Object'] as $i => $object) { + $converted['Event']['Object'][$i] = self::__propagateTagToObjectAttributes($object, $eventTags); + } + } return $converted; } @@ -101,6 +112,33 @@ class WorkflowFormatConverterTool return $converted; } + private static function __propagateTagToAttributes(array $attribute, array $eventTags): array + { + $allTags = []; + if (!empty($eventTags)) { + foreach ($eventTags as $eventTag) { + $eventTag['inherited'] = true; + $allTags[] = $eventTag; + } + } + if (!empty($attribute['Tag'])) { + foreach ($attribute['Tag'] as $tag) { + $tag['inherited'] = false; + $allTags[] = $tag; + } + } + $attribute['_allTags'] = $allTags; + return $attribute; + } + + private static function __propagateTagToObjectAttributes(array $object, array $eventTags): array + { + foreach ($object['Attribute'] as $i => $attribute) { + $object['Attribute'][$i] = self::__propagateTagToAttributes($attribute, $eventTags); + } + return $object; + } + private static function __encapsulateEntityWithEvent(array $data): array { $eventModel = ClassRegistry::init('Event'); @@ -112,12 +150,11 @@ class WorkflowFormatConverterTool if (empty($event)) { return []; } - $event = self::__convertEvent($event); - $event = $event['Event']; reset($data); $entityType = key($data); - $event[$entityType][] = $data[$entityType]; - return ['Event' => $event]; + $event['Event'][$entityType][] = $data[$entityType]; + $event = self::__convertEvent($event); + return $event; } private static function __includeFlattenedAttributes(array $event): array diff --git a/app/Lib/Tools/WorkflowGraphTool.php b/app/Lib/Tools/WorkflowGraphTool.php index 5762d10bc..60c7b8382 100644 --- a/app/Lib/Tools/WorkflowGraphTool.php +++ b/app/Lib/Tools/WorkflowGraphTool.php @@ -6,16 +6,18 @@ class GraphUtil { public function __construct($graphData) { - $this->graph = $graphData; + $this->graph = array_filter($graphData, function($i) { + return $i != '_frames'; + }, ARRAY_FILTER_USE_KEY); $this->numberNodes = count($this->graph); - $this->edgeList = $this->_buildEdgeList($graphData); + $this->edgeList = $this->_buildEdgeList($this->graph); $this->properties = []; } private function _buildEdgeList($graphData): array { $list = []; - foreach ($graphData as $node) { + foreach ($graphData as $i => $node) { $list[(int)$node['id']] = []; foreach (($node['outputs'] ?? []) as $output_id => $outputs) { foreach ($outputs as $connections) { @@ -162,6 +164,12 @@ class GraphWalker } else if ($node['data']['id'] == 'concurrent-task') { $this->_evaluateConcurrentTask($node, $roamingData, $outputs['output_1']); return ['output_1' => []]; + } else if ($node['data']['id'] == 'generic-filter-data') { + $this->_evaluateFilterAddLogic($node, $roamingData, $outputs['output_1']); + return ['output_1' => $outputs['output_1']]; + } else if ($node['data']['id'] == 'generic-filter-reset') { + $this->_evaluateFilterRemoveLogic($node, $roamingData, $outputs['output_1']); + return ['output_1' => $outputs['output_1']]; } else { $useFirstOutput = $this->_evaluateCustomLogicCondition($node, $roamingData); return $useFirstOutput ? ['output_1' => $outputs['output_1']] : ['output_2' => $outputs['output_2']]; @@ -175,6 +183,18 @@ class GraphWalker return $result; } + private function _evaluateFilterAddLogic($node, WorkflowRoamingData $roamingData): bool + { + $result = $this->WorkflowModel->executeNode($node, $roamingData); + return $result; + } + + private function _evaluateFilterRemoveLogic($node, WorkflowRoamingData $roamingData): bool + { + $result = $this->WorkflowModel->executeNode($node, $roamingData); + return $result; + } + private function _evaluateCustomLogicCondition($node, WorkflowRoamingData $roamingData): bool { $result = $this->WorkflowModel->executeNode($node, $roamingData); @@ -254,6 +274,7 @@ class WorkflowRoamingData private $data; private $workflow; private $current_node; + private $workflowModel; public function __construct(array $workflow_user, array $data, array $workflow, int $current_node) { @@ -270,9 +291,50 @@ class WorkflowRoamingData public function getData(): array { + if (!empty($this->getEnabledFilters())) { + return $this->filterDataIfNeeded(); + } return $this->data; } + public function filterDataIfNeeded(): array + { + $filteredData = $this->data; + $filters = $this->getEnabledFilters(); + foreach ($filters as $filteringLabel => $filteringOptions) { + $filteredData = $this->applyFilter($filteredData, $filteringOptions); + } + return $filteredData; + } + + private function applyFilter(array $data, array $filteringOptions): array + { + if (substr($filteringOptions['selector'], -4) === '.{n}') { + $filteringOptions['selector'] = substr($filteringOptions['selector'], 0, -4); + } + $baseModule = $this->getFilteringModule(); + $extracted = $baseModule->extractData($data, $filteringOptions['selector']); + if ($extracted === false) { + $filteredData = false; + } + $filteredData = $baseModule->getItemsMatchingCondition($extracted, $filteringOptions['value'], $filteringOptions['operator'], $filteringOptions['path']); + $newData = Hash::remove($data, $filteringOptions['selector']); + $newData = Hash::insert($data, $filteringOptions['selector'], $filteredData); + return $newData; + } + + private function getFilteringModule() + { + $this->workflowModel = ClassRegistry::init('Workflow'); + $moduleClass = $this->workflowModel->getModuleClassByType('logic', 'generic-filter-data'); + return $moduleClass; + } + + public function getEnabledFilters(): array + { + return !empty($this->data['enabledFilters']) ? $this->data['enabledFilters'] : []; + } + public function getWorkflow(): array { return $this->workflow; @@ -296,6 +358,20 @@ class WorkflowRoamingData class WorkflowGraphTool { + + /** + * cleanGraphData Remove frame nodes from the graph data + * + * @param array $graphData + * @return array + */ + public static function cleanGraphData(array $graphData): array + { + return array_filter($graphData, function($i) { + return $i != '_frames'; + }, ARRAY_FILTER_USE_KEY); + } + /** * extractTriggerFromWorkflow Return the trigger id (or full module) that are specified in the workflow * @@ -322,8 +398,9 @@ class WorkflowGraphTool */ public static function extractTriggersFromWorkflow(array $graphData, bool $fullNode = false): array { + $graphData = self::cleanGraphData($graphData); $triggers = []; - foreach ($graphData as $node) { + foreach ($graphData as $i => $node) { if ($node['data']['module_type'] == 'trigger') { if (!empty($fullNode)) { $triggers[] = $node; @@ -344,8 +421,9 @@ class WorkflowGraphTool */ public static function extractConcurrentTasksFromWorkflow(array $graphData, bool $fullNode = false): array { + $graphData = self::cleanGraphData($graphData); $nodes = []; - foreach ($graphData as $node) { + foreach ($graphData as $i => $node) { if ($node['data']['module_type'] == 'logic' && $node['data']['id'] == 'concurrent-task') { if (!empty($fullNode)) { $nodes[] = $node; @@ -357,6 +435,52 @@ class WorkflowGraphTool return $nodes; } + /** + * extractFilterNodesFromWorkflow Return the list of generic-filter-data's id (or full module) that are included in the workflow + * + * @param array $workflow + * @param bool $fullNode + * @return array + */ + public static function extractFilterNodesFromWorkflow(array $graphData, bool $fullNode = false): array + { + $graphData = self::cleanGraphData($graphData); + $nodes = []; + foreach ($graphData as $i => $node) { + if ($node['data']['module_type'] == 'logic' && $node['data']['id'] == 'generic-filter-data') { + if (!empty($fullNode)) { + $nodes[] = $node; + } else { + $nodes[] = $node['data']['id']; + } + } + } + return $nodes; + } + + /** + * extractResetFilterFromWorkflow Return the list of generic-filter-reset's id (or full module) that are included in the workflow + * + * @param array $workflow + * @param bool $fullNode + * @return array + */ + public static function extractResetFilterFromWorkflow(array $graphData, bool $fullNode = false): array + { + $graphData = self::cleanGraphData($graphData); + $nodes = []; + foreach ($graphData as $i => $node) { + if ($node['data']['module_type'] == 'logic' && $node['data']['id'] == 'generic-filter-reset') { + if (!empty($fullNode)) { + $nodes[] = $node; + } else { + $nodes[] = $node['data']['id']; + } + } + } + return $nodes; + } + /** * isAcyclic Return if the graph contains a cycle * diff --git a/app/Locale/ara/LC_MESSAGES/default.po b/app/Locale/ara/LC_MESSAGES/default.po index d669fcdb5..ccaa20c24 100644 --- a/app/Locale/ara/LC_MESSAGES/default.po +++ b/app/Locale/ara/LC_MESSAGES/default.po @@ -725,7 +725,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "" #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/cake_resque.pot b/app/Locale/cake_resque.pot index fdbc5ab59..a105801bb 100644 --- a/app/Locale/cake_resque.pot +++ b/app/Locale/cake_resque.pot @@ -578,7 +578,7 @@ msgid "Workers number [%s] is not valid. Please enter a valid number" msgstr "" #: Plugin/CakeResque/Console/Command/CakeResqueShell.php:1282 -msgid "User [%s] does not exists. Please enter a valid system user" +msgid "User [%s] does not exist. Please enter a valid system user" msgstr "" #: Plugin/CakeResque/Console/Command/CakeResqueShell.php:1304 diff --git a/app/Locale/cze/LC_MESSAGES/default.po b/app/Locale/cze/LC_MESSAGES/default.po index b1c6c1d00..fa32e5969 100644 --- a/app/Locale/cze/LC_MESSAGES/default.po +++ b/app/Locale/cze/LC_MESSAGES/default.po @@ -723,7 +723,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "Neplatná Skupina sdílení nebo není autorizovaná." #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/dan/LC_MESSAGES/default.po b/app/Locale/dan/LC_MESSAGES/default.po index 073394e95..8de07eca4 100644 --- a/app/Locale/dan/LC_MESSAGES/default.po +++ b/app/Locale/dan/LC_MESSAGES/default.po @@ -720,7 +720,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "Ugyldig Delingsgruppe, eller ingen godkendelse." #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/default.pot b/app/Locale/default.pot index 40c15ea69..8800c29b4 100644 --- a/app/Locale/default.pot +++ b/app/Locale/default.pot @@ -848,7 +848,7 @@ msgid "Add attribute" msgstr "" #: Controller/AttributesController.php:287;1725 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:305 diff --git a/app/Locale/deu/LC_MESSAGES/default.po b/app/Locale/deu/LC_MESSAGES/default.po index a447de5b5..0704a865b 100644 --- a/app/Locale/deu/LC_MESSAGES/default.po +++ b/app/Locale/deu/LC_MESSAGES/default.po @@ -720,7 +720,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "Ungültige Freigabegruppe oder nicht berechtigt." #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/fra/LC_MESSAGES/default.po b/app/Locale/fra/LC_MESSAGES/default.po index ece0e4027..4383c9239 100644 --- a/app/Locale/fra/LC_MESSAGES/default.po +++ b/app/Locale/fra/LC_MESSAGES/default.po @@ -723,7 +723,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "Groupe de partage invalide ou non autorisé." #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/hun/LC_MESSAGES/default.po b/app/Locale/hun/LC_MESSAGES/default.po index 29959ad24..060ff002d 100644 --- a/app/Locale/hun/LC_MESSAGES/default.po +++ b/app/Locale/hun/LC_MESSAGES/default.po @@ -720,7 +720,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "" #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/ita/LC_MESSAGES/default.po b/app/Locale/ita/LC_MESSAGES/default.po index 83ce00e04..3fd797c47 100644 --- a/app/Locale/ita/LC_MESSAGES/default.po +++ b/app/Locale/ita/LC_MESSAGES/default.po @@ -721,7 +721,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "Sharing Group non valido o non autorizzato." #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/jpn/LC_MESSAGES/default.po b/app/Locale/jpn/LC_MESSAGES/default.po index ba835e83c..8d8b227ac 100644 --- a/app/Locale/jpn/LC_MESSAGES/default.po +++ b/app/Locale/jpn/LC_MESSAGES/default.po @@ -719,7 +719,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "無効な共有グループ、もしくは権限がありません。" #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/kor/LC_MESSAGES/default.po b/app/Locale/kor/LC_MESSAGES/default.po index d8348242d..702b70330 100644 --- a/app/Locale/kor/LC_MESSAGES/default.po +++ b/app/Locale/kor/LC_MESSAGES/default.po @@ -720,7 +720,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "잘못된 공유 그룹이거나 권한이 없습니다" #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/no/LC_MESSAGES/default.po b/app/Locale/no/LC_MESSAGES/default.po index 1615ea775..2ebb74bc6 100644 --- a/app/Locale/no/LC_MESSAGES/default.po +++ b/app/Locale/no/LC_MESSAGES/default.po @@ -720,7 +720,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "Ugyldig delingsgruppe eller ikke autorisert." #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/pol/LC_MESSAGES/default.po b/app/Locale/pol/LC_MESSAGES/default.po index 275617176..348da1637 100644 --- a/app/Locale/pol/LC_MESSAGES/default.po +++ b/app/Locale/pol/LC_MESSAGES/default.po @@ -723,7 +723,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "" #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/pt_BR/LC_MESSAGES/default.po b/app/Locale/pt_BR/LC_MESSAGES/default.po index 14425a49e..e78b80293 100644 --- a/app/Locale/pt_BR/LC_MESSAGES/default.po +++ b/app/Locale/pt_BR/LC_MESSAGES/default.po @@ -721,7 +721,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "Grupo de compartilhamento inválido ou não autorizado." #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/ro/LC_MESSAGES/default.po b/app/Locale/ro/LC_MESSAGES/default.po index 00e817fce..0032d1140 100644 --- a/app/Locale/ro/LC_MESSAGES/default.po +++ b/app/Locale/ro/LC_MESSAGES/default.po @@ -721,7 +721,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "" #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/rus/LC_MESSAGES/default.po b/app/Locale/rus/LC_MESSAGES/default.po index b3bfb931e..12d435b47 100644 --- a/app/Locale/rus/LC_MESSAGES/default.po +++ b/app/Locale/rus/LC_MESSAGES/default.po @@ -722,7 +722,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "" #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/si-LK/LC_MESSAGES/default.po b/app/Locale/si-LK/LC_MESSAGES/default.po index 53160e3aa..ba4d74a52 100644 --- a/app/Locale/si-LK/LC_MESSAGES/default.po +++ b/app/Locale/si-LK/LC_MESSAGES/default.po @@ -723,7 +723,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "වලංගු නොවන බෙදාගැනීමේ කණ්ඩායමක් හෝ අවසරයක් නැත." #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "උපලක්ෂණයක් නොපවතී හෝ ඔබට මෙම උපලක්ෂණ බාගැනීමට අවසර නැත." #: Controller/AttributesController.php:334 diff --git a/app/Locale/spa/LC_MESSAGES/default.po b/app/Locale/spa/LC_MESSAGES/default.po index 548973e37..618975630 100644 --- a/app/Locale/spa/LC_MESSAGES/default.po +++ b/app/Locale/spa/LC_MESSAGES/default.po @@ -722,7 +722,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "Grupo de uso no válido o no autorizado." #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Locale/th_TH/LC_MESSAGES/default.po b/app/Locale/th_TH/LC_MESSAGES/default.po index 45589c869..c22c08e09 100644 --- a/app/Locale/th_TH/LC_MESSAGES/default.po +++ b/app/Locale/th_TH/LC_MESSAGES/default.po @@ -722,7 +722,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "กลุ่มการแบ่งปันไม่ถูกต้องหรือไม่ได้รับอนุญาต" #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "ไม่มีแอตทริบิวต์หรือคุณไม่ได้รับอนุญาตให้ดาวน์โหลดแอตทริบิวต์นี้" #: Controller/AttributesController.php:334 diff --git a/app/Locale/zh-s/LC_MESSAGES/default.po b/app/Locale/zh-s/LC_MESSAGES/default.po index 9d554618d..b8a044794 100644 --- a/app/Locale/zh-s/LC_MESSAGES/default.po +++ b/app/Locale/zh-s/LC_MESSAGES/default.po @@ -719,7 +719,7 @@ msgid "Invalid Sharing Group or not authorised." msgstr "无效的共享组或未授权." #: Controller/AttributesController.php:316;1772 -msgid "Attribute does not exists or you do not have the permission to download this attribute." +msgid "Attribute does not exist or you do not have the permission to download this attribute." msgstr "" #: Controller/AttributesController.php:334 diff --git a/app/Model/AccessLog.php b/app/Model/AccessLog.php new file mode 100644 index 000000000..f1f70b453 --- /dev/null +++ b/app/Model/AccessLog.php @@ -0,0 +1,345 @@ + 'Unknown', + 1 => 'GET', + 2 => 'HEAD', + 3 => 'POST', + 4 => 'PUT', + 5 => 'DELETE', + 6 => 'OPTIONS', + 7 => 'TRACE', + 8 => 'PATCH', + ]; + + public $actsAs = [ + 'Containable', + ]; + + public $belongsTo = [ + 'User' => [ + 'className' => 'User', + 'foreignKey' => 'user_id', + ], + 'Organisation' => [ + 'className' => 'Organisation', + 'foreignKey' => 'org_id', + ], + ]; + + public function afterFind($results, $primary = false) + { + foreach ($results as &$result) { + if (isset($result['AccessLog']['ip'])) { + $result['AccessLog']['ip'] = inet_ntop($result['AccessLog']['ip']); + } + if (isset($result['AccessLog']['request_method'])) { + $result['AccessLog']['request_method'] = self::REQUEST_TYPES[$result['AccessLog']['request_method']]; + } + if (!empty($result['AccessLog']['request'])) { + $decoded = $this->decodeRequest($result['AccessLog']['request']); + if ($decoded) { + list($contentType, $encoding, $data) = $decoded; + $result['AccessLog']['request'] = $data; + $result['AccessLog']['request_content_type'] = $contentType; + $result['AccessLog']['request_content_encoding'] = $encoding; + } else { + $result['AccessLog']['request'] = false; + } + } + if (!empty($result['AccessLog']['query_log'])) { + $result['AccessLog']['query_log'] = JsonTool::decode($this->decompress($result['AccessLog']['query_log'])); + } + if (!empty($result['AccessLog']['memory_usage'])) { + $result['AccessLog']['memory_usage'] = $result['AccessLog']['memory_usage'] * 1024; + } + } + return $results; + } + + public function beforeSave($options = []) + { + $accessLog = &$this->data['AccessLog']; + + $this->externalLog($accessLog); + + if (Configure::read('MISP.log_paranoid_skip_db')) { + return; + } + + // Truncate + foreach (['request_id', 'user_agent', 'url'] as $field) { + if (isset($accessLog[$field]) && strlen($accessLog[$field]) > 255) { + $accessLog[$field] = substr($accessLog[$field], 0, 255); + } + } + + if (isset($accessLog['ip'])) { + $accessLog['ip'] = inet_pton($accessLog['ip']); + } + + if (isset($accessLog['request_method'])) { + $requestMethodIds = array_flip(self::REQUEST_TYPES); + $accessLog['request_method'] = $requestMethodIds[$accessLog['request_method']] ?? 0; + } + + if (!empty($accessLog['request'])) { + $accessLog['request'] = $this->compress($accessLog['request']); + } + + if (!empty($accessLog['query_log'])) { + $accessLog['query_log'] = $this->compress(JsonTool::encode($accessLog['query_log'])); + } + + // In database save size in kb to avoid overflow signed int type + if (isset($accessLog['memory_usage'])) { + $accessLog['memory_usage'] = $accessLog['memory_usage'] >> 10; // same as /= 1024 + } + } + + /** + * @param array $user + * @param string $remoteIp + * @param CakeRequest $request + * @param bool $includeRequestBody + * @return bool + * @throws Exception + */ + public function logRequest(array $user, $remoteIp, CakeRequest $request, $includeRequestBody = true) + { + $requestTime = $this->requestTime(); + $logClientIp = Configure::read('MISP.log_client_ip'); + $includeSqlQueries = Configure::read('MISP.log_paranoid_include_sql_queries'); + + if ($includeSqlQueries) { + $this->getDataSource()->fullDebug = true; // Enable SQL logging + } + + $dataToSave = [ + 'created' => $requestTime->format('Y-m-d H:i:s.u'), + 'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? null, + 'user_id' => (int)$user['id'], + 'org_id' => (int)$user['org_id'], + 'authkey_id' => isset($user['authkey_id']) ? (int)$user['authkey_id'] : null, + 'ip' => $logClientIp ? $remoteIp : null, + 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null, + 'request_method' => $_SERVER['REQUEST_METHOD'], + 'controller' => $request->params['controller'], + 'action' => $request->params['action'], + 'url' => $request->here, + ]; + + if ($includeRequestBody && $request->is(['post', 'put', 'delete'])) { + $dataToSave['request'] = $this->requestBody($request); + } + + // Save data on shutdown + register_shutdown_function(function () use ($dataToSave, $requestTime, $includeSqlQueries) { + session_write_close(); // close session to allow concurrent requests + $this->saveOnShutdown($dataToSave, $requestTime, $includeSqlQueries); + }); + + return true; + } + + /** + * @param DateTime $duration + * @return int Number of deleted entries + */ + public function deleteOldLogs(DateTime $duration) + { + $this->deleteAll([ + ['created <' => $duration->format('Y-m-d H:i:s.u')], + ], false); + + $deleted = $this->getAffectedRows(); + if ($deleted > 100) { + $dataSource = $this->getDataSource(); + $dataSource->query('OPTIMIZE TABLE ' . $dataSource->name($this->table)); + } + return $deleted; + } + + /** + * @param CakeRequest $request + * @return string + */ + private function requestBody(CakeRequest $request) + { + $requestContentType = $_SERVER['CONTENT_TYPE'] ?? null; + $requestEncoding = $_SERVER['HTTP_CONTENT_ENCODING'] ?? null; + + if (substr($requestContentType, 0, 19) === 'multipart/form-data') { + $input = http_build_query($request->data, '', '&'); + } else { + $input = $request->input(); + } + + return "$requestContentType\n$requestEncoding\n$input"; + } + + /** + * @param array $data + * @param DateTime $requestTime + * @param bool $includeSqlQueries + * @return bool + * @throws Exception + */ + private function saveOnShutdown(array $data, DateTime $requestTime, $includeSqlQueries) + { + $sqlLog = $this->getDataSource()->getLog(false, false); + $queryCount = $sqlLog['count']; + + if ($includeSqlQueries && !empty($sqlLog['log'])) { + foreach ($sqlLog['log'] as &$log) { + $log['query'] = $this->escapeNonUnicode($log['query']); + unset($log['affected']); // affected is the same as numRows + unset($log['params']); // no need to save for your use case + } + $data['query_log'] = ['time' => $sqlLog['time'], 'log' => $sqlLog['log']]; + } + + $data['response_code'] = http_response_code(); + $data['memory_usage'] = memory_get_peak_usage(); + $data['query_count'] = $queryCount; + $data['duration'] = (int)((microtime(true) - $requestTime->format('U.u')) * 1000); // in milliseconds + + try { + return $this->save($data, ['atomic' => false]); + } catch (Exception $e) { + $this->logException("Could not insert access log to database", $e, LOG_WARNING); + return false; + } + } + + /** + * @param array $data + * @return void + */ + public function externalLog(array $data) + { + if ($this->pubToZmq('audit')) { + $this->getPubSubTool()->publish($data, 'audit', 'log'); + } + + $this->publishKafkaNotification('audit', $data, 'log'); + // In future add support for sending logs to elastic + } + + /** + * @return DateTime + */ + private function requestTime() + { + $requestTime = $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true); + $requestTime = (string) $requestTime; + // Fix string if float value doesnt contain decimal part + if (strpos($requestTime, '.') === false) { + $requestTime .= '.0'; + } + return DateTime::createFromFormat('U.u', $requestTime); + } + + /** + * @param string $request + * @return array|false + */ + private function decodeRequest($request) + { + $request = $this->decompress($request); + if ($request === false) { + return false; + } + + list($contentType, $encoding, $data) = explode("\n", $request, 3); + + if ($encoding === 'gzip') { + $data = gzdecode($data); + } elseif ($encoding === 'br') { + if (function_exists('brotli_uncompress')) { + $data = brotli_uncompress($data); + } else { + $data = false; + } + } + + return [$contentType, $encoding, $data]; + } + + /** + * @param string $data + * @return false|string + */ + private function decompress($data) + { + $header = substr($data, 0, 4); + if ($header === self::BROTLI_HEADER) { + if (function_exists('brotli_uncompress')) { + $data = brotli_uncompress(substr($data, 4)); + if ($data === false) { + return false; + } + } else { + return false; + } + } + return $data; + } + + /** + * @param string $data + * @return string + */ + private function compress($data) + { + $compressionEnabled = Configure::read('MISP.log_new_audit_compress') && + function_exists('brotli_compress'); + + if ($compressionEnabled && strlen($data) >= self::COMPRESS_MIN_LENGTH) { + return self::BROTLI_HEADER . brotli_compress($data, 4, BROTLI_TEXT); + } + return $data; + } + + /** + * @param $string + * @return string + */ + private function escapeNonUnicode($string) + { + if (json_encode($string, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS) !== false) { + return $string; // string is valid unicode + } + + if (function_exists('mb_str_split')) { + $result = mb_str_split($string); + } else { + $result = []; + $length = mb_strlen($string); + for ($i = 0; $i < $length; $i++) { + $result[] = mb_substr($string, $i, 1); + } + } + + $string = ''; + foreach ($result as $char) { + if (strlen($char) === 1 && !preg_match('/[[:print:]]/', $char)) { + $string .= '\x' . bin2hex($char); + } else { + $string .= $char; + } + } + + return $string; + } +} \ No newline at end of file diff --git a/app/Model/AppModel.php b/app/Model/AppModel.php index 8465e5794..b62cadca0 100644 --- a/app/Model/AppModel.php +++ b/app/Model/AppModel.php @@ -41,8 +41,6 @@ class AppModel extends Model private $__profiler = array(); - public $elasticSearchClient; - /** @var AttachmentTool|null */ private $attachmentTool; @@ -85,7 +83,9 @@ class AppModel extends Model 81 => false, 82 => false, 83 => false, 84 => false, 85 => false, 86 => false, 87 => false, 88 => false, 89 => false, 90 => false, 91 => false, 92 => false, 93 => false, 94 => false, 95 => true, 96 => false, 97 => true, 98 => false, - 99 => false + 99 => false, 100 => false, 101 => false, 102 => false, 103 => false, 104 => false, + 105 => false, 106 => false, 107 => false, 108 => false, 109 => false, 110 => false, + 111 => false, 112 => false, 113 => true, 114 => false ); const ADVANCED_UPDATES_DESCRIPTION = array( @@ -1882,6 +1882,97 @@ class AppModel extends Model $sqlArray[] = "ALTER TABLE `event_tags` ADD `relationship_type` varchar(191) NULL DEFAULT '';"; $sqlArray[] = "ALTER TABLE `attribute_tags` ADD `relationship_type` varchar(191) NULL DEFAULT '';"; break; + case 100: + $sqlArray[] = "CREATE TABLE IF NOT EXISTS `access_logs` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `created` datetime(4) NOT NULL, + `user_id` int(11) NOT NULL, + `org_id` int(11) NOT NULL, + `authkey_id` int(11) DEFAULT NULL, + `ip` varbinary(16) DEFAULT NULL, + `request_method` tinyint NOT NULL, + `user_agent` varchar(255) DEFAULT NULL, + `request_id` varchar(255) DEFAULT NULL, + `controller` varchar(20) NOT NULL, + `action` varchar(20) NOT NULL, + `url` varchar(255) NOT NULL, + `request` blob, + `response_code` smallint NOT NULL, + `memory_usage` int(11) NOT NULL, + `duration` int(11) NOT NULL, + `query_count` int(11) NOT NULL, + PRIMARY KEY (`id`), + INDEX `user_id` (`user_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"; + break; + case 101: + $sqlArray[] = "CREATE TABLE IF NOT EXISTS `taxii_servers` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `uuid` varchar(40) COLLATE utf8_bin NOT NULL , + `name` varchar(191) NOT NULL, + `owner` varchar(191) NOT NULL, + `baseurl` varchar(191) NOT NULL, + `api_root` varchar(191) NOT NULL DEFAULT 0, + `description` text, + `filters` text, + `api_key` varchar(255)COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`id`), + INDEX `uuid` (`uuid`), + INDEX `name` (`name`), + INDEX `baseurl` (`baseurl`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"; + break; + case 102: + $sqlArray[] = "UPDATE roles SET perm_audit = 1;"; + break; + case 103: + $sqlArray[] = "ALTER TABLE `taxonomies` ADD `highlighted` tinyint(1) DEFAULT 0;"; + break; + case 104: + $sqlArray[] = "ALTER TABLE `access_logs` ADD `query_log` blob DEFAULT NULL"; + break; + case 105: + // set a default role if there is none + if (!$this->AdminSetting->getSetting('default_role')) { + $role = ClassRegistry::init('Role')->findByName('User'); + if ($role) { + $sqlArray[] = "INSERT INTO `admin_settings` (setting, value) VALUES ('default_role', '".$role['Role']['id']."');"; + } else { + // there is no role called User, do nothing + } + } + break; + case 106: + $sqlArray[] = "ALTER TABLE `taxii_servers` MODIFY `baseurl` varchar(191) NOT NULL;"; + break; + case 107: + $sqlArray[] = "ALTER TABLE `auth_keys` ADD `unique_ips` text COLLATE utf8mb4_unicode_ci"; + break; + case 108: + $sqlArray[] = "ALTER TABLE `workflows` MODIFY `data` LONGTEXT;"; + break; + case 109: + $sqlArray[] = "UPDATE `over_correlating_values` SET `value` = LOWER(`value`) COLLATE utf8mb4_unicode_ci;"; + break; + case 110: + $sqlArray[] = "ALTER TABLE `users` ADD `totp` varchar(255) DEFAULT NULL;"; + $sqlArray[] = "ALTER TABLE `users` ADD `hotp_counter` int(11) DEFAULT NULL;"; + break; + case 111: + $sqlArray[] = "ALTER TABLE `taxii_servers` ADD `collection` varchar(40) CHARACTER SET ascii DEFAULT NULL;"; + break; + case 112: + $sqlArray[] = "ALTER TABLE `roles` ADD `perm_view_feed_correlations` tinyint(1) NOT NULL DEFAULT 0;"; + break; + case 113: + // we only want to update the existing roles - going forward the default is still 0 + // Also, we want to execute it as a separate update to ensure that cache clearing is done correctly + $this->cleanCacheFiles(); + $sqlArray[] = "UPDATE roles SET perm_view_feed_correlations = 1;"; + break; + case 114: + $indexArray[] = ['object_references', 'uuid']; + break; case 'fixNonEmptySharingGroupID': $sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;'; $sqlArray[] = 'UPDATE `attributes` SET `sharing_group_id` = 0 WHERE `distribution` != 4;'; @@ -2361,7 +2452,7 @@ class AppModel extends Model 'action' => 'update_db_worker', 'user_id' => 0, 'title' => __('Issues executing run_updates'), - 'change' => __('Database updates are locked. Worker not spawned') + 'change' => __('Database updates are locked. Make sure that you have an update worker running. If you do, it might be related to an update\'s execution repeatedly failing or still being in progress.') )); if (!empty($job)) { // if multiple prio worker is enabled, want to mark them as done $job['Job']['progress'] = 100; @@ -2952,17 +3043,6 @@ class AppModel extends Model return self::$loadedPubSubTool; } - protected function getElasticSearchTool() - { - if (!$this->elasticSearchClient) { - App::uses('ElasticSearchClient', 'Tools'); - $client = new ElasticSearchClient(); - $client->initTool(); - $this->elasticSearchClient = $client; - } - return $this->elasticSearchClient; - } - /** * @return BackgroundJobsTool */ @@ -3023,6 +3103,16 @@ class AppModel extends Model return [$subQuery]; } + /** + * Returns estimated number of table rows + * @return int + */ + public function tableRows() + { + $rows = $this->query("SELECT TABLE_ROWS FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{$this->table}';"); + return $rows[0]['TABLES']['TABLE_ROWS']; + } + // start a benchmark run for the given bench name public function benchmarkInit($name = 'default') { @@ -3244,10 +3334,12 @@ class AppModel extends Model $filter = array(); foreach ($temp as $f) { $f = strval($f); - if ($f[0] === '!') { - $filter['NOT'][] = substr($f, 1); - } else { - $filter['OR'][] = $f; + if ($f !== '') { + if ($f[0] === '!') { + $filter['NOT'][] = substr($f, 1); + } else { + $filter['OR'][] = $f; + } } } return $filter; @@ -3523,6 +3615,11 @@ class AppModel extends Model */ protected function logException($message, Exception $exception, $type = LOG_ERR) { + // If Sentry is installed, send exception to Sentry + if (function_exists('\Sentry\captureException') && $type === LOG_ERR) { + \Sentry\captureException($exception); + } + $message .= "\n"; do { @@ -3860,4 +3957,35 @@ class AppModel extends Model ); "); } + + public function findOrder($order, $order_model, $valid_order_fields) + { + if (!is_array($order)) { + $order_rules = explode(' ', strtolower($order)); + $order_field = explode('.', $order_rules[0]); + $order_field = end($order_field); + if (in_array($order_field, $valid_order_fields)) { + $direction = 'asc'; + if (!empty($order_rules[1]) && trim($order_rules[1]) === 'desc') { + $direction = 'desc'; + } + } else { + return null; + } + return $order_model . '.' . $order_field . ' ' . $direction; + } + return null; + } + + /** + * @return string|null + */ + public function _remoteIp() + { + $ipHeader = Configure::read('MISP.log_client_ip_header') ?: null; + if ($ipHeader && isset($_SERVER[$ipHeader])) { + return trim($_SERVER[$ipHeader]); + } + return $_SERVER['REMOTE_ADDR'] ?? null; + } } diff --git a/app/Model/Attribute.php b/app/Model/Attribute.php index 93a89e50b..ca2c6658a 100644 --- a/app/Model/Attribute.php +++ b/app/Model/Attribute.php @@ -25,7 +25,7 @@ class Attribute extends AppModel { public $combinedKeys = array('event_id', 'category', 'type'); - public $name = 'Attribute'; // TODO general + public $name = 'Attribute'; public $actsAs = array( 'AuditLog', @@ -36,13 +36,14 @@ class Attribute extends AppModel 'Trim', 'Containable', 'Regexp' => array('fields' => array('value')), + 'LightPaginator' ); public $displayField = 'value'; public $virtualFields = array( 'value' => "CASE WHEN Attribute.value2 = '' THEN Attribute.value1 ELSE CONCAT(Attribute.value1, '|', Attribute.value2) END", - ); // TODO hardcoded + ); // explanations of certain fields to be used in various views public $fieldDescriptions = array( @@ -736,29 +737,9 @@ class Attribute extends AppModel return true; } - $type = $this->data['Attribute']['type']; - $conditions = array( - 'Attribute.event_id' => $this->data['Attribute']['event_id'], - 'Attribute.type' => $type, - 'Attribute.category' => $this->data['Attribute']['category'], - 'Attribute.deleted' => 0, - 'Attribute.object_id' => 0, - ); + $existingAttribute = $this->findAttributeByValue($this->data['Attribute']); - $value = $fields['value']; - if (in_array($type, $this->getCompositeTypes(), true)) { - $value = explode('|', $value); - $conditions['Attribute.value1'] = $value[0]; - $conditions['Attribute.value2'] = $value[1]; - } else { - $conditions['Attribute.value1'] = $value; - } - - if (isset($this->data['Attribute']['id'])) { - $conditions['Attribute.id !='] = $this->data['Attribute']['id']; - } - - return !$this->hasAny($conditions); + return empty($existingAttribute); } public function validateTypeValue($fields) @@ -1606,7 +1587,7 @@ class Attribute extends AppModel * @return array * @throws Exception */ - public function fetchAttributes(array $user, array $options = [], &$result_count = false) + public function fetchAttributes(array $user, array $options = [], &$result_count = false, $real_count = false) { $params = array( 'conditions' => $this->buildConditions($user), @@ -1705,7 +1686,14 @@ class Attribute extends AppModel if (empty($options['flatten'])) { $params['conditions']['AND'][] = array('Attribute.object_id' => 0); } - $params['order'] = isset($options['order']) ? $options['order'] : []; + $params['order'] = []; + if (!empty($options['order'])) { + $options['order'] = $this->findOrder( + $options['order'], + 'Attribute', + ['id', 'event_id', 'object_id', 'type', 'category', 'value', 'distribution', 'timestamp', 'object_relation'] + ); + } if (!isset($options['withAttachments'])) { $options['withAttachments'] = false; } @@ -1780,7 +1768,7 @@ class Attribute extends AppModel } // Do not fetch result count when `$result_count` is false - if ($result_count !== false) { + if ($result_count !== false && $real_count == true) { $find_params = $params; unset($find_params['limit']); $result_count = $this->find('count', $find_params); @@ -1792,11 +1780,15 @@ class Attribute extends AppModel $eventTags = []; // tag cache $attributes = []; do { + $continue = true; $results = $this->find('all', $params); if (empty($results)) { break; } - + $iteration_result_count = count($results); + if ($real_count !== true) { + $result_count += count($results); + } if (!empty($options['includeContext'])) { $eventIds = []; foreach ($results as $result) { @@ -1875,7 +1867,7 @@ class Attribute extends AppModel unset($attribute); if ($loop) { - if (count($results) < $loopLimit) { // we fetched less results than limit, so we can skip next query + if ($iteration_result_count < $loopLimit) { // we fetched fewer results than the limit, so we can exit the loop break; } $params['page']++; @@ -2491,6 +2483,10 @@ class Attribute extends AppModel if (!isset($attribute['distribution'])) { $attribute['distribution'] = $this->defaultDistribution(); } + $breakOnDuplicate = true; + if (isset($params['breakOnDuplicate'])) { + $breakOnDuplicate = (bool)$params['breakOnDuplicate']; + } $params = array( 'fieldList' => self::CAPTURE_FIELDS, ); @@ -2504,6 +2500,16 @@ class Attribute extends AppModel unset($attribute['sharing_group_id']); } } + // if breakOnDuplicate=false, try to find the existing attribute by value and set the id and uuid + if ($breakOnDuplicate === false) { + unset($this->validate['value']['uniqueValue']); + $existingAttribute = $this->findAttributeByValue($attribute); + if (!empty($existingAttribute)) { + $attribute['id'] = $existingAttribute['Attribute']['id']; + $attribute['uuid'] = $existingAttribute['Attribute']['uuid']; + $this->id = $attribute['id']; + } + } if (!$this->save(['Attribute' => $attribute], $params)) { $this->logDropped($user, $attribute); } else { @@ -2916,6 +2922,13 @@ class Attribute extends AppModel if (!empty($filters['score'])) { $params['score'] = $filters['score']; } + if (!empty($filters['order'])) { + $params['order'] = $this->findOrder( + $filters['order'], + 'Attribute', + ['id', 'event_id', 'object_id', 'type', 'category', 'value', 'distribution', 'timestamp', 'object_relation'] + ); + } if ($paramsOnly) { return $params; } @@ -2935,7 +2948,8 @@ class Attribute extends AppModel $exportTool->additional_params ); } - + ClassRegistry::init('ConnectionManager'); + $db = ConnectionManager::getDataSource('default'); $tmpfile = new TmpFileTool(); $tmpfile->write($exportTool->header($exportToolParams)); $loop = false; @@ -2969,9 +2983,15 @@ class Attribute extends AppModel $this->Allowedlist = ClassRegistry::init('Allowedlist'); $separator = $exportTool->separator($exportToolParams); $elementCounter = 0; + $real_count = false; + $incrementTotalBy = $loop || $real_count ? 0 : 1; do { - $results = $this->fetchAttributes($user, $params, $elementCounter); - $totalCount = $elementCounter; + $results = $this->fetchAttributes($user, $params, $elementCounter, $real_count); + if (!$real_count) { + $totalCount = $params['limit'] * ($params['page'] - 1) + $elementCounter; + } else { + $totalCount = $elementCounter; + } $elementCounter = false; // do not call `count` again if (empty($results)) { break; // nothing found, skip rest @@ -2987,13 +3007,109 @@ class Attribute extends AppModel $tmpfile->writeWithSeparator($handlerResult, $separator); } } - if ($loop && count($results) < $params['limit']) { - break; // do not continue if we received less results than limit + if (count($results) < $params['limit']) { + $incrementTotalBy = 0; + if ($loop) { + break; // do not continue if we received less results than limit + } } $params['page'] += 1; } while ($loop); + return $totalCount + $incrementTotalBy; + } - return $totalCount; + public function bro($user, $type, $tags = false, $eventId = false, $from = false, $to = false, $last = false, $enforceWarninglist = false, $skipHeader = false) + { + App::uses('BroExport', 'Export'); + $export = new BroExport(); + if ($type == 'all') { + $types = array_keys($export->mispTypes); + } else { + $types = array($type); + } + $intel = array(); + foreach ($types as $type) { + //restricting to non-private or same org if the user is not a site-admin. + $conditions['AND'] = array('Attribute.to_ids' => 1, 'Event.published' => 1); + if ($from) { + $conditions['AND']['Event.date >='] = $from; + } + if ($to) { + $conditions['AND']['Event.date <='] = $to; + } + if ($last) { + $conditions['AND']['Event.publish_timestamp >='] = $last; + } + if ($eventId !== false) { + $temp = array(); + $args = $this->dissectArgs($eventId); + foreach ($args[0] as $accepted) { + $temp['OR'][] = array('Event.id' => $accepted); + } + $conditions['AND'][] = $temp; + $temp = array(); + foreach ($args[1] as $rejected) { + $temp['AND'][] = array('Event.id !=' => $rejected); + } + $conditions['AND'][] = $temp; + } + if ($tags !== false) { + // If we sent any tags along, load the associated tag names for each attribute + $tag = ClassRegistry::init('Tag'); + $args = $this->dissectArgs($tags); + $tagArray = $tag->fetchEventTagIds($args[0], $args[1]); + $temp = array(); + foreach ($tagArray[0] as $accepted) { + $temp['OR'][] = array('Event.id' => $accepted); + } + $conditions['AND'][] = $temp; + $temp = array(); + foreach ($tagArray[1] as $rejected) { + $temp['AND'][] = array('Event.id !=' => $rejected); + } + $conditions['AND'][] = $temp; + } + $this->Allowedlist = ClassRegistry::init('Allowedlist'); + $this->allowedlist = $this->Allowedlist->getBlockedValues(); + $instanceString = 'MISP'; + if (Configure::read('MISP.host_org_id') && Configure::read('MISP.host_org_id') > 0) { + $this->Event->Orgc->id = Configure::read('MISP.host_org_id'); + if ($this->Event->Orgc->exists()) { + $instanceString = $this->Event->Orgc->field('name') . ' MISP'; + } + } + $mispTypes = $export->getMispTypes($type); + foreach ($mispTypes as $mispType) { + $conditions['AND']['Attribute.type'] = $mispType[0]; + $intel = array_merge($intel, $this->__bro($user, $conditions, $mispType[1], $export, $this->allowedlist, $instanceString, $enforceWarninglist)); + } + } + natsort($intel); + $intel = array_unique($intel); + if (empty($skipHeader)) { + array_unshift($intel, $export->header); + } + return $intel; + } + + private function __bro($user, $conditions, $valueField, $export, $allowedlist, $instanceString, $enforceWarninglist) + { + $attributes = $this->fetchAttributes( + $user, + array( + 'conditions' => $conditions, // array of conditions + 'order' => 'Attribute.value' . $valueField . ' ASC', + 'recursive' => -1, // int + 'fields' => array('Attribute.id', 'Attribute.event_id', 'Attribute.type', 'Attribute.category', 'Attribute.comment', 'Attribute.to_ids', 'Attribute.value', 'Attribute.value' . $valueField), + 'contain' => array('Event' => array('fields' => array('Event.id', 'Event.threat_level_id', 'Event.orgc_id', 'Event.uuid'))), + 'enforceWarninglist' => $enforceWarninglist, + 'flatten' => 1 + ) + ); + $orgs = $this->Event->Orgc->find('list', array( + 'fields' => array('Orgc.id', 'Orgc.name') + )); + return $export->export($attributes, $orgs, $valueField, $allowedlist, $instanceString); } public function set_filter_uuid(&$params, $conditions, $options) @@ -3139,7 +3255,7 @@ class Attribute extends AppModel 'Payload delivery' => array( 'desc' => __('Information about how the malware is delivered'), 'formdesc' => __('Information about the way the malware payload is initially delivered, for example information about the email or web-site, vulnerability used, originating IP etc. Malware sample itself should be attached here.'), - 'types' => array('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'sha512/224', 'sha512/256', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512', 'ssdeep', 'imphash', 'telfhash', 'impfuzzy', 'authentihash', 'vhash', 'pehash', 'tlsh', 'cdhash', 'filename', 'filename|md5', 'filename|sha1', 'filename|sha224', 'filename|sha256', 'filename|sha384', 'filename|sha512', 'filename|sha512/224', 'filename|sha512/256', 'filename|sha3-224', 'filename|sha3-256', 'filename|sha3-384', 'filename|sha3-512', 'filename|authentihash', 'filename|vhash', 'filename|ssdeep', 'filename|tlsh', 'filename|imphash','filename|impfuzzy', 'filename|pehash', 'mac-address', 'mac-eui-64', 'ip-src', 'ip-dst', 'ip-dst|port', 'ip-src|port', 'hostname', 'domain', 'email', 'email-src', 'email-dst', 'email-subject', 'email-attachment', 'email-body', 'url', 'user-agent', 'AS', 'pattern-in-file', 'pattern-in-traffic', 'filename-pattern', 'stix2-pattern', 'yara', 'sigma', 'mime-type', 'attachment', 'malware-sample', 'link', 'malware-type', 'comment', 'text', 'hex', 'vulnerability', 'cpe', 'weakness', 'x509-fingerprint-sha1', 'x509-fingerprint-md5', 'x509-fingerprint-sha256', 'ja3-fingerprint-md5', 'jarm-fingerprint', 'hassh-md5', 'hasshserver-md5', 'other', 'hostname|port', 'email-dst-display-name', 'email-src-display-name', 'email-header', 'email-reply-to', 'email-x-mailer', 'email-mime-boundary', 'email-thread-index', 'email-message-id', 'mobile-application-id', 'chrome-extension-id', 'whois-registrant-email', 'anonymised') + 'types' => array('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'sha512/224', 'sha512/256', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512', 'ssdeep', 'imphash', 'telfhash', 'impfuzzy', 'authentihash', 'vhash', 'pehash', 'tlsh', 'cdhash', 'filename', 'filename|md5', 'filename|sha1', 'filename|sha224', 'filename|sha256', 'filename|sha384', 'filename|sha512', 'filename|sha512/224', 'filename|sha512/256', 'filename|sha3-224', 'filename|sha3-256', 'filename|sha3-384', 'filename|sha3-512', 'filename|authentihash', 'filename|vhash', 'filename|ssdeep', 'filename|tlsh', 'filename|imphash','filename|impfuzzy', 'filename|pehash', 'mac-address', 'mac-eui-64', 'ip-src', 'ip-dst', 'ip-dst|port', 'ip-src|port', 'hostname', 'domain', 'email', 'email-src', 'email-dst', 'email-subject', 'email-attachment', 'email-body', 'url', 'user-agent', 'AS', 'pattern-in-file', 'pattern-in-traffic', 'filename-pattern', 'stix2-pattern', 'yara', 'sigma', 'mime-type', 'attachment', 'malware-sample', 'link', 'malware-type', 'comment', 'text', 'hex', 'vulnerability', 'cpe', 'weakness', 'x509-fingerprint-sha1', 'x509-fingerprint-md5', 'x509-fingerprint-sha256', 'ja3-fingerprint-md5', 'jarm-fingerprint', 'hassh-md5', 'hasshserver-md5', 'other', 'hostname|port', 'email-dst-display-name', 'email-src-display-name', 'email-header', 'email-reply-to', 'email-x-mailer', 'email-mime-boundary', 'email-thread-index', 'email-message-id', 'azure-application-id', 'mobile-application-id', 'chrome-extension-id', 'whois-registrant-email', 'anonymised') ), 'Artifacts dropped' => array( 'desc' => __('Any artifact (files, registry keys etc.) dropped by the malware or other modifications to the system'), @@ -3148,7 +3264,7 @@ class Attribute extends AppModel 'Payload installation' => array( 'desc' => __('Info on where the malware gets installed in the system'), 'formdesc' => __('Location where the payload was placed in the system and the way it was installed. For example, a filename|md5 type attribute can be added here like this: c:\\windows\\system32\\malicious.exe|41d8cd98f00b204e9800998ecf8427e.'), - 'types' => array('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'sha512/224', 'sha512/256', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512', 'ssdeep', 'imphash', 'telfhash', 'impfuzzy', 'authentihash', 'vhash', 'pehash', 'tlsh', 'cdhash', 'filename', 'filename|md5', 'filename|sha1', 'filename|sha224', 'filename|sha256', 'filename|sha384', 'filename|sha512', 'filename|sha512/224', 'filename|sha512/256', 'filename|sha3-224', 'filename|sha3-256', 'filename|sha3-384', 'filename|sha3-512', 'filename|authentihash', 'filename|vhash', 'filename|ssdeep', 'filename|tlsh', 'filename|imphash', 'filename|impfuzzy', 'filename|pehash', 'pattern-in-file', 'pattern-in-traffic', 'pattern-in-memory', 'filename-pattern', 'stix2-pattern', 'yara', 'sigma', 'vulnerability', 'cpe','weakness', 'attachment', 'malware-sample', 'malware-type', 'comment', 'text', 'hex', 'x509-fingerprint-sha1', 'x509-fingerprint-md5', 'x509-fingerprint-sha256', 'mobile-application-id', 'chrome-extension-id', 'other', 'mime-type', 'anonymised') + 'types' => array('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'sha512/224', 'sha512/256', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512', 'ssdeep', 'imphash', 'telfhash', 'impfuzzy', 'authentihash', 'vhash', 'pehash', 'tlsh', 'cdhash', 'filename', 'filename|md5', 'filename|sha1', 'filename|sha224', 'filename|sha256', 'filename|sha384', 'filename|sha512', 'filename|sha512/224', 'filename|sha512/256', 'filename|sha3-224', 'filename|sha3-256', 'filename|sha3-384', 'filename|sha3-512', 'filename|authentihash', 'filename|vhash', 'filename|ssdeep', 'filename|tlsh', 'filename|imphash', 'filename|impfuzzy', 'filename|pehash', 'pattern-in-file', 'pattern-in-traffic', 'pattern-in-memory', 'filename-pattern', 'stix2-pattern', 'yara', 'sigma', 'vulnerability', 'cpe','weakness', 'attachment', 'malware-sample', 'malware-type', 'comment', 'text', 'hex', 'x509-fingerprint-sha1', 'x509-fingerprint-md5', 'x509-fingerprint-sha256', 'azure-application-id', 'azure-application-id', 'mobile-application-id', 'chrome-extension-id', 'other', 'mime-type', 'anonymised') ), 'Persistence mechanism' => array( 'desc' => __('Mechanisms used by the malware to start at boot'), @@ -3206,20 +3322,20 @@ class Attribute extends AppModel private function generateTypeDefinitions() { return array( - 'md5' => array('desc' => __('A checksum in md5 format'), 'formdesc' => __("You are encouraged to use filename|md5 instead. A checksum in md5 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha1' => array('desc' => __('A checksum in sha1 format'), 'formdesc' => __("You are encouraged to use filename|sha1 instead. A checksum in sha1 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha256' => array('desc' => __('A checksum in sha256 format'), 'formdesc' => __("You are encouraged to use filename|sha256 instead. A checksum in sha256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'md5' => array('desc' => __('A checksum in MD5 format'), 'formdesc' => __("You are encouraged to use filename|md5 instead. A checksum in md5 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha1' => array('desc' => __('A checksum in SHA1 format'), 'formdesc' => __("You are encouraged to use filename|sha1 instead. A checksum in sha1 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha256' => array('desc' => __('A checksum in SHA256 format'), 'formdesc' => __("You are encouraged to use filename|sha256 instead. A checksum in sha256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename' => array('desc' => __('Filename'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'pdb' => array('desc' => __('Microsoft Program database (PDB) path information'), 'default_category' => 'Artifacts dropped', 'to_ids' => 0), - 'filename|md5' => array('desc' => __('A filename and an md5 hash separated by a |'), 'formdesc' => __("A filename and an md5 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha1' => array('desc' => __('A filename and an sha1 hash separated by a |'), 'formdesc' => __("A filename and an sha1 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha256' => array('desc' => __('A filename and an sha256 hash separated by a |'), 'formdesc' => __("A filename and an sha256 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|md5' => array('desc' => __('A filename and an MD5 hash separated by a |'), 'formdesc' => __("A filename and an md5 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha1' => array('desc' => __('A filename and an SHA1 hash separated by a |'), 'formdesc' => __("A filename and an sha1 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha256' => array('desc' => __('A filename and an SHA256 hash separated by a |'), 'formdesc' => __("A filename and an sha256 hash separated by a | (no spaces)"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'ip-src' => array('desc' => __("A source IP address of the attacker"), 'default_category' => 'Network activity', 'to_ids' => 1), 'ip-dst' => array('desc' => __('A destination IP address of the attacker or C&C server'), 'formdesc' => __("A destination IP address of the attacker or C&C server. Also set the IDS flag on when this IP is hardcoded in malware"), 'default_category' => 'Network activity', 'to_ids' => 1), 'hostname' => array('desc' => __('A full host/dnsname of an attacker'), 'formdesc' => __("A full host/dnsname of an attacker. Also set the IDS flag on when this hostname is hardcoded in malware"), 'default_category' => 'Network activity', 'to_ids' => 1), 'domain' => array('desc' => __('A domain name used in the malware'), 'formdesc' => __("A domain name used in the malware. Use this instead of hostname when the upper domain is important or can be used to create links between events."), 'default_category' => 'Network activity', 'to_ids' => 1), 'domain|ip' => array('desc' => __('A domain name and its IP address (as found in DNS lookup) separated by a |'),'formdesc' => __("A domain name and its IP address (as found in DNS lookup) separated by a | (no spaces)"), 'default_category' => 'Network activity', 'to_ids' => 1), - 'email' => array('desc' => ('An e-mail address'), 'default_category' => 'Social network', 'to_ids' => 1), + 'email' => array('desc' => ('An email address'), 'default_category' => 'Social network', 'to_ids' => 1), 'email-src' => array('desc' => __("The source email address. Used to describe the sender when describing an e-mail."), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'eppn' => array('desc' => __("eduPersonPrincipalName - eppn - the NetId of the person for the purposes of inter-institutional authentication. Should be stored in the form of user@univ.edu, where univ.edu is the name of the local security domain."), 'default_category' => 'Network activity', 'to_ids' => 1), 'email-dst' => array('desc' => __("The destination email address. Used to describe the recipient when describing an e-mail."), 'default_category' => 'Network activity', 'to_ids' => 1), @@ -3227,8 +3343,8 @@ class Attribute extends AppModel 'email-attachment' => array('desc' => __("File name of the email attachment."), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'email-body' => array('desc' => __('Email body'), 'default_category' => 'Payload delivery', 'to_ids' => 0), 'float' => array('desc' => __("A floating point value."), 'default_category' => 'Other', 'to_ids' => 0), - 'git-commit-id' => array('desc' => __("A git commit ID."), 'default_category' => 'Internal reference', 'to_ids' => 0), - 'url' => array('desc' => __('url'), 'default_category' => 'Network activity', 'to_ids' => 1), + 'git-commit-id' => array('desc' => __("A Git commit ID."), 'default_category' => 'Internal reference', 'to_ids' => 0), + 'url' => array('desc' => __('Uniform Resource Locator'), 'default_category' => 'Network activity', 'to_ids' => 1), 'http-method' => array('desc' => __("HTTP method used by the malware (e.g. POST, GET, ...)."), 'default_category' => 'Network activity', 'to_ids' => 0), 'user-agent' => array('desc' => __("The user-agent used by the malware in the HTTP request."), 'default_category' => 'Network activity', 'to_ids' => 0), 'ja3-fingerprint-md5' => array('desc' => __("JA3 is a method for creating SSL/TLS client fingerprints that should be easy to produce on any platform and can be easily shared for threat intelligence."), 'default_category' => 'Network activity', 'to_ids' => 1), @@ -3242,7 +3358,7 @@ class Attribute extends AppModel 'snort' => array('desc' => __('An IDS rule in Snort rule-format'), 'formdesc' => __("An IDS rule in Snort rule-format. This rule will be automatically rewritten in the NIDS exports."), 'default_category' => 'Network activity', 'to_ids' => 1), 'bro' => array('desc' => __('An NIDS rule in the Bro rule-format'), 'formdesc' => __("An NIDS rule in the Bro rule-format."), 'default_category' => 'Network activity', 'to_ids' => 1), 'zeek' => array('desc' => __('An NIDS rule in the Zeek rule-format'), 'formdesc' => __("An NIDS rule in the Zeek rule-format."), 'default_category' => 'Network activity', 'to_ids' => 1), - 'community-id' => array('desc' => __('a community ID flow hashing algorithm to map multiple traffic monitors into common flow id'), 'formdesc' => __("a community ID flow hashing algorithm to map multiple traffic monitors into common flow id"), 'default_category' => 'Network activity', 'to_ids' => 1), + 'community-id' => array('desc' => __('A community ID flow hashing algorithm to map multiple traffic monitors into common flow id'), 'formdesc' => __("a community ID flow hashing algorithm to map multiple traffic monitors into common flow id"), 'default_category' => 'Network activity', 'to_ids' => 1), 'pattern-in-file' => array('desc' => __('Pattern in file that identifies the malware'), 'default_category' => 'Payload installation', 'to_ids' => 1), 'pattern-in-traffic' => array('desc' => __('Pattern in network traffic that identifies the malware'), 'default_category' => 'Network activity', 'to_ids' => 1), 'pattern-in-memory' => array('desc' => __('Pattern in memory dump that identifies the malware'), 'default_category' => 'Payload installation', 'to_ids' => 1), @@ -3250,7 +3366,7 @@ class Attribute extends AppModel 'pgp-public-key' => array('desc' => __('A PGP public key'), 'default_category' => 'Person', 'to_ids' => 0), 'pgp-private-key' => array('desc' => __('A PGP private key'), 'default_category' => 'Person', 'to_ids' => 0), 'ssh-fingerprint' => array('desc' => __('A fingerprint of SSH key material'), 'default_category' => 'Network activity', 'to_ids' => 0), - 'yara' => array('desc' => __('Yara signature'), 'default_category' => 'Payload installation', 'to_ids' => 1), + 'yara' => array('desc' => __('YARA signature'), 'default_category' => 'Payload installation', 'to_ids' => 1), 'stix2-pattern' => array('desc' => __('STIX 2 pattern'), 'default_category' => 'Payload installation', 'to_ids' => 1), 'sigma' => array('desc' => __('Sigma - Generic Signature Format for SIEM Systems'), 'default_category' => 'Payload installation', 'to_ids' => 1), 'gene' => array('desc' => __('GENE - Go Evtx sigNature Engine'), 'default_category' => 'Artifacts dropped', 'to_ids' => 0), @@ -3260,7 +3376,7 @@ class Attribute extends AppModel 'cookie' => array('desc' => __('HTTP cookie as often stored on the user web client. This can include authentication cookie or session cookie.'), 'default_category' => 'Network activity', 'to_ids' => 0), 'vulnerability' => array('desc' => __('A reference to the vulnerability used in the exploit'), 'default_category' => 'External analysis', 'to_ids' => 0), 'cpe' => array('desc' => __('Common Platform Enumeration - structured naming scheme for information technology systems, software, and packages.'), 'default_category' => 'External analysis', 'to_ids' => 0), - 'weakness' => array('desc'=> __('A reference to the weakness used in the exploit'), 'default_category' => 'External analysis', 'to_ids' => 0), + 'weakness' => array('desc'=> __('A reference to the weakness (CWE) used in the exploit'), 'default_category' => 'External analysis', 'to_ids' => 0), 'attachment' => array('desc' => __('Attachment with external information'), 'formdesc' => __("Please upload files using the Upload Attachment button."), 'default_category' => 'External analysis', 'to_ids' => 0), 'malware-sample' => array('desc' => __('Attachment containing encrypted malware sample'), 'formdesc' => __("Please upload files using the Upload Attachment button."), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'link' => array('desc' => __('Link to an external information'), 'default_category' => 'External analysis', 'to_ids' => 0), @@ -3298,34 +3414,34 @@ class Attribute extends AppModel 'ssdeep' => array('desc' => __('A checksum in ssdeep format'), 'formdesc' => __("You are encouraged to use filename|ssdeep instead. A checksum in the SSDeep format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'imphash' => array('desc' => __('Import hash - a hash created based on the imports in the sample.'), 'formdesc' => __("You are encouraged to use filename|imphash instead. A hash created based on the imports in the sample, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'telfhash' => array('desc' => __('telfhash is symbol hash for ELF files, just like imphash is imports hash for PE files.'), 'formdesc' => __("You are encouraged to use a file object with telfash"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'pehash' => array('desc' => __('PEhash - a hash calculated based of certain pieces of a PE executable file'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'pehash' => array('desc' => __('peHash - a hash calculated based of certain pieces of a PE executable file'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'impfuzzy' => array('desc' => __('A fuzzy hash of import table of Portable Executable format'), 'formdesc' => __("You are encouraged to use filename|impfuzzy instead. A fuzzy hash created based on the imports in the sample, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha224' => array('desc' => __('A checksum in sha-224 format'), 'formdesc' => __("You are encouraged to use filename|sha224 instead. A checksum in sha224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha384' => array('desc' => __('A checksum in sha-384 format'), 'formdesc' => __("You are encouraged to use filename|sha384 instead. A checksum in sha384 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha512' => array('desc' => __('A checksum in sha-512 format'), 'formdesc' => __("You are encouraged to use filename|sha512 instead. A checksum in sha512 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha512/224' => array('desc' => __('A checksum in the sha-512/224 format'), 'formdesc' => __("You are encouraged to use filename|sha512/224 instead. A checksum in sha512/224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha512/256' => array('desc' => __('A checksum in the sha-512/256 format'), 'formdesc' => __("You are encouraged to use filename|sha512/256 instead. A checksum in sha512/256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha3-224' => array('desc' => __('A checksum in sha3-224 format'), 'formdesc' => __("You are encouraged to use filename|sha3-224 instead. A checksum in sha3-224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha3-256' => array('desc' => __('A checksum in sha3-256 format'), 'formdesc' => __("You are encouraged to use filename|sha3-256 instead. A checksum in sha3-256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha3-384' => array('desc' => __('A checksum in sha3-384 format'), 'formdesc' => __("You are encouraged to use filename|sha3-384 instead. A checksum in sha3-384 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'sha3-512' => array('desc' => __('A checksum in sha3-512 format'), 'formdesc' => __("You are encouraged to use filename|sha3-512 instead. A checksum in sha3-512 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha224' => array('desc' => __('A checksum in SHA-224 format'), 'formdesc' => __("You are encouraged to use filename|sha224 instead. A checksum in sha224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha384' => array('desc' => __('A checksum in SHA-384 format'), 'formdesc' => __("You are encouraged to use filename|sha384 instead. A checksum in sha384 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha512' => array('desc' => __('A checksum in SHA-512 format'), 'formdesc' => __("You are encouraged to use filename|sha512 instead. A checksum in sha512 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha512/224' => array('desc' => __('A checksum in the SHA-512/224 format'), 'formdesc' => __("You are encouraged to use filename|sha512/224 instead. A checksum in sha512/224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha512/256' => array('desc' => __('A checksum in the SHA-512/256 format'), 'formdesc' => __("You are encouraged to use filename|sha512/256 instead. A checksum in sha512/256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha3-224' => array('desc' => __('A checksum in SHA3-224 format'), 'formdesc' => __("You are encouraged to use filename|sha3-224 instead. A checksum in sha3-224 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha3-256' => array('desc' => __('A checksum in SHA3-256 format'), 'formdesc' => __("You are encouraged to use filename|sha3-256 instead. A checksum in sha3-256 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha3-384' => array('desc' => __('A checksum in SHA3-384 format'), 'formdesc' => __("You are encouraged to use filename|sha3-384 instead. A checksum in sha3-384 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'sha3-512' => array('desc' => __('A checksum in SHA3-512 format'), 'formdesc' => __("You are encouraged to use filename|sha3-512 instead. A checksum in sha3-512 format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'tlsh' => array('desc' => __('A checksum in the Trend Micro Locality Sensitive Hash format'), 'formdesc' => __("You are encouraged to use filename|tlsh instead. A checksum in the Trend Micro Locality Sensitive Hash format, only use this if you don't know the correct filename"), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'cdhash' => array('desc' => __('An Apple Code Directory Hash, identifying a code-signed Mach-O executable file'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|authentihash' => array('desc' => __('A checksum in md5 format'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|authentihash' => array('desc' => __('A filename and Authenticode executable signature hash'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename|vhash' => array('desc' => __('A filename and a VirusTotal hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename|ssdeep' => array('desc' => __('A checksum in ssdeep format'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename|imphash' => array('desc' => __('Import hash - a hash created based on the imports in the sample.'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename|impfuzzy' => array('desc' => __('Import fuzzy hash - a fuzzy hash created based on the imports in the sample.'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|pehash' => array('desc' => __('A filename and a PEhash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha224' => array('desc' => __('A filename and a sha-224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha384' => array('desc' => __('A filename and a sha-384 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha512' => array('desc' => __('A filename and a sha-512 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha512/224' => array('desc' => __('A filename and a sha-512/224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha512/256' => array('desc' => __('A filename and a sha-512/256 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha3-224' => array('desc' => __('A filename and an sha3-224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha3-256' => array('desc' => __('A filename and an sha3-256 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha3-384' => array('desc' => __('A filename and an sha3-384 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), - 'filename|sha3-512' => array('desc' => __('A filename and an sha3-512 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|pehash' => array('desc' => __('A filename and a peHash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha224' => array('desc' => __('A filename and a SHA-224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha384' => array('desc' => __('A filename and a SHA-384 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha512' => array('desc' => __('A filename and a SHA-512 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha512/224' => array('desc' => __('A filename and a SHa-512/224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha512/256' => array('desc' => __('A filename and a SHA-512/256 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha3-224' => array('desc' => __('A filename and an SHA3-224 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha3-256' => array('desc' => __('A filename and an SHA3-256 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha3-384' => array('desc' => __('A filename and an SHA3-384 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'filename|sha3-512' => array('desc' => __('A filename and an SHA3-512 hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'filename|tlsh' => array('desc' => __('A filename and a Trend Micro Locality Sensitive Hash separated by a |'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'windows-scheduled-task' => array('desc' => __('A scheduled task in windows'), 'default_category' => 'Artifacts dropped', 'to_ids' => 0), 'windows-service-name' => array('desc' => __('A windows service name. This is the name used internally by windows. Not to be confused with the windows-service-displayname.'), 'default_category' => 'Artifacts dropped', 'to_ids' => 0), @@ -3343,7 +3459,7 @@ class Attribute extends AppModel 'x509-fingerprint-sha1' => array('desc' => __('X509 fingerprint in SHA-1 format'), 'default_category' => 'Network activity', 'to_ids' => 1), 'x509-fingerprint-md5' => array('desc' => __('X509 fingerprint in MD5 format'), 'default_category' => 'Network activity', 'to_ids' => 1), 'x509-fingerprint-sha256' => array('desc' => __('X509 fingerprint in SHA-256 format'), 'default_category' => 'Network activity', 'to_ids' => 1), - 'dns-soa-email' => array('desc' => __('RFC1035 mandates that DNS zones should have a SOA (Statement Of Authority) record that contains an email address where a PoC for the domain could be contacted. This can sometimes be used for attribution/linkage between different domains even if protected by whois privacy'), 'default_category' => 'Attribution', 'to_ids' => 0), + 'dns-soa-email' => array('desc' => __('RFC 1035 mandates that DNS zones should have a SOA (Statement Of Authority) record that contains an email address where a PoC for the domain could be contacted. This can sometimes be used for attribution/linkage between different domains even if protected by whois privacy'), 'default_category' => 'Attribution', 'to_ids' => 0), 'size-in-bytes' => array('desc' => __('Size expressed in bytes'), 'default_category' => 'Other', 'to_ids' => 0), 'counter' => array('desc' => __('An integer counter, generally to be used in objects'), 'default_category' => 'Other', 'to_ids' => 0), 'datetime' => array('desc' => __('Datetime in the ISO 8601 format'), 'default_category' => 'Other', 'to_ids' => 0), @@ -3351,8 +3467,8 @@ class Attribute extends AppModel 'ip-dst|port' => array('desc' => __('IP destination and port number separated by a |'), 'default_category' => 'Network activity', 'to_ids' => 1), 'ip-src|port' => array('desc' => __('IP source and port number separated by a |'), 'default_category' => 'Network activity', 'to_ids' => 1), 'hostname|port' => array('desc' => __('Hostname and port number separated by a |'), 'default_category' => 'Network activity', 'to_ids' => 1), - 'mac-address' => array('desc' => __('Mac address'), 'default_category' => 'Network activity', 'to_ids' => 0), - 'mac-eui-64' => array('desc' => __('Mac EUI-64 address'), 'default_category' => 'Network activity', 'to_ids' => 0), + 'mac-address' => array('desc' => __('MAC address'), 'default_category' => 'Network activity', 'to_ids' => 0), + 'mac-eui-64' => array('desc' => __('MAC EUI-64 address'), 'default_category' => 'Network activity', 'to_ids' => 0), // verify IDS flag defaults for these 'email-dst-display-name' => array('desc' => __('Email destination display name'), 'default_category' => 'Payload delivery', 'to_ids' => 0), 'email-src-display-name' => array('desc' => __('Email source display name'), 'default_category' => 'Payload delivery', 'to_ids' => 0), @@ -3362,9 +3478,9 @@ class Attribute extends AppModel 'email-mime-boundary' => array('desc' => __('The email mime boundary separating parts in a multipart email'), 'default_category' => 'Payload delivery', 'to_ids' => 0), 'email-thread-index' => array('desc' => __('The email thread index header'), 'default_category' => 'Payload delivery', 'to_ids' => 0), 'email-message-id' => array('desc' => __('The email message ID'), 'default_category' => 'Payload delivery', 'to_ids' => 0), - 'github-username' => array('desc' => __('A github user name'), 'default_category' => 'Social network', 'to_ids' => 0), - 'github-repository' => array('desc' => __('A github repository'), 'default_category' => 'Social network', 'to_ids' => 0), - 'github-organisation' => array('desc' => __('A github organisation'), 'default_category' => 'Social network', 'to_ids' => 0), + 'github-username' => array('desc' => __('A GitHub user name'), 'default_category' => 'Social network', 'to_ids' => 0), + 'github-repository' => array('desc' => __('A Github repository'), 'default_category' => 'Social network', 'to_ids' => 0), + 'github-organisation' => array('desc' => __('A GitHub organisation'), 'default_category' => 'Social network', 'to_ids' => 0), 'jabber-id' => array('desc' => __('Jabber ID'), 'default_category' => 'Social network', 'to_ids' => 0), 'twitter-id' => array('desc' => __('Twitter ID'), 'default_category' => 'Social network', 'to_ids' => 0), 'dkim' => array('desc' => __('DKIM public key'), 'default_category' => 'Network activity', 'to_ids' => 0), @@ -3396,6 +3512,7 @@ class Attribute extends AppModel 'place-port-of-onward-foreign-destination' => array('desc' => __('A Port where the passenger is transiting to'), 'default_category' => 'Person', 'to_ids' => 0), 'passenger-name-record-locator-number' => array('desc' => __('The Passenger Name Record Locator is a key under which the reservation for a trip is stored in the system. The PNR contains, among other data, the name, flight segments and address of the passenger. It is defined by a combination of five or six letters and numbers.'), 'default_category' => 'Person', 'to_ids' => 0), 'mobile-application-id' => array('desc' => __('The application id of a mobile application'), 'default_category' => 'Payload delivery', 'to_ids' => 1), + 'azure-application-id' => array('desc' => __('Azure Application ID.'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'chrome-extension-id' => array('desc' => __('Chrome extension id'), 'default_category' => 'Payload delivery', 'to_ids' => 1), 'cortex' => array('desc' => __('Cortex analysis result'), 'default_category' => 'External analysis', 'to_ids' => 0), 'boolean' => array('desc' => __('Boolean value - to be used in objects'), 'default_category' => 'Other', 'to_ids' => 0), @@ -3404,4 +3521,37 @@ class Attribute extends AppModel //'url-regex' => array('desc' => '', 'default_category' => 'Person', 'to_ids' => 0), ); } + + private function findAttributeByValue($attribute) + { + $type = $attribute['type']; + $conditions = [ + 'Attribute.event_id' => $attribute['event_id'], + 'Attribute.type' => $type, + 'Attribute.deleted' => 0, + 'Attribute.object_id' => 0, + ]; + + if (isset($attribute['category'])) { + $conditions['Attribute.category'] = $attribute['category']; + } + + if (in_array($type, $this->getCompositeTypes(), true)) { + $value = explode('|', $attribute['value']); + $conditions['Attribute.value1'] = $value[0]; + $conditions['Attribute.value2'] = $value[1]; + } else { + $conditions['Attribute.value1'] = $attribute['value']; + } + + if (isset($attribute['id'])) { + $conditions['Attribute.id !='] = $attribute['id']; + } + + return $this->find('first', [ + 'recursive' => -1, + 'conditions' => $conditions, + 'fields' => ['Attribute.id', 'Attribute.uuid'] + ]); + } } diff --git a/app/Model/AuditLog.php b/app/Model/AuditLog.php index 51cd1542a..83f0093f9 100644 --- a/app/Model/AuditLog.php +++ b/app/Model/AuditLog.php @@ -9,7 +9,7 @@ App::uses('AppModel', 'Model'); class AuditLog extends AppModel { const BROTLI_HEADER = "\xce\xb2\xcf\x81"; - const BROTLI_MIN_LENGTH = 200; + const COMPRESS_MIN_LENGTH = 256; const ACTION_ADD = 'add', ACTION_EDIT = 'edit', @@ -25,7 +25,11 @@ class AuditLog extends AppModel ACTION_REMOVE_GALAXY = 'remove_galaxy', ACTION_REMOVE_GALAXY_LOCAL = 'remove_local_galaxy', ACTION_PUBLISH = 'publish', - ACTION_PUBLISH_SIGHTINGS = 'publish_sightings'; + ACTION_PUBLISH_SIGHTINGS = 'publish_sightings', + ACTION_LOGIN = 'login', + ACTION_PASSWDCHANGE = 'password_change', + ACTION_LOGOUT = 'logout', + ACTION_LOGIN_FAILED = 'login_failed'; const REQUEST_TYPE_DEFAULT = 0, REQUEST_TYPE_API = 1, @@ -45,9 +49,6 @@ class AuditLog extends AppModel /** @var bool */ private $pubToZmq; - /** @var bool */ - private $elasticLogging; - /** @var bool */ private $logClientIp; @@ -59,6 +60,7 @@ class AuditLog extends AppModel public $compressionStats = [ 'compressed' => 0, + 'bytes_total' => 0, 'bytes_compressed' => 0, 'bytes_uncompressed' => 0, ]; @@ -81,9 +83,9 @@ class AuditLog extends AppModel public function __construct($id = false, $table = null, $ds = null) { parent::__construct($id, $table, $ds); - $this->compressionEnabled = Configure::read('MISP.log_new_audit_compress') && function_exists('brotli_compress'); + $this->compressionEnabled = Configure::read('MISP.log_new_audit_compress') && + function_exists('brotli_compress'); $this->pubToZmq = $this->pubToZmq('audit'); - $this->elasticLogging = Configure::read('Plugin.ElasticSearch_logging_enable'); $this->logClientIp = Configure::read('MISP.log_client_ip'); } @@ -145,6 +147,20 @@ class AuditLog extends AppModel return ''; } + /** + * @param mixed $change + * @return string + * @throws JsonException + */ + private function encodeChange($change) + { + $change = JsonTool::encode($change); + if ($this->compressionEnabled && strlen($change) >= self::COMPRESS_MIN_LENGTH) { + return self::BROTLI_HEADER . brotli_compress($change, 4, BROTLI_TEXT); + } + return $change; + } + /** * @param string $change * @return array|string @@ -152,10 +168,13 @@ class AuditLog extends AppModel */ private function decodeChange($change) { - if (substr($change, 0, 4) === self::BROTLI_HEADER) { + $len = strlen($change); + $this->compressionStats['bytes_total'] += $len; + $header = substr($change, 0, 4); + if ($header === self::BROTLI_HEADER) { $this->compressionStats['compressed']++; if (function_exists('brotli_uncompress')) { - $this->compressionStats['bytes_compressed'] += strlen($change); + $this->compressionStats['bytes_compressed'] += $len; $change = brotli_uncompress(substr($change, 4)); $this->compressionStats['bytes_uncompressed'] += strlen($change); if ($change === false) { @@ -179,10 +198,7 @@ class AuditLog extends AppModel { $auditLog = &$this->data['AuditLog']; if (!isset($auditLog['ip']) && $this->logClientIp) { - $ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR'; - if (isset($_SERVER[$ipHeader])) { - $auditLog['ip'] = $_SERVER[$ipHeader]; - } + $auditLog['ip'] = $this->_remoteIp(); } if (!isset($auditLog['user_id'])) { @@ -222,11 +238,7 @@ class AuditLog extends AppModel } if (isset($auditLog['change'])) { - $change = JsonTool::encode($auditLog['change']); - if ($this->compressionEnabled && strlen($change) >= self::BROTLI_MIN_LENGTH) { - $change = self::BROTLI_HEADER . brotli_compress($change, 4, BROTLI_TEXT); - } - $auditLog['change'] = $change; + $auditLog['change'] = $this->encodeChange($auditLog['change']); } } @@ -243,12 +255,7 @@ class AuditLog extends AppModel $this->publishKafkaNotification('audit', $data, 'log'); - if ($this->elasticLogging) { - // send off our logs to distributed /dev/null - $logIndex = Configure::read("Plugin.ElasticSearch_log_index"); - $elasticSearchClient = $this->getElasticSearchTool(); - $elasticSearchClient->pushDocument($logIndex, "log", $data); - } + // In future add support for sending logs to elastic // write to syslogd as well if enabled if ($this->syslog === null) { @@ -334,15 +341,34 @@ class AuditLog extends AppModel } } + /** + * @throws JsonException + * @throws Exception + */ public function recompress() { $changes = $this->find('all', [ 'fields' => ['AuditLog.id', 'AuditLog.change'], 'recursive' => -1, - 'conditions' => ['length(AuditLog.change) >=' => self::BROTLI_MIN_LENGTH], + 'conditions' => ['OR' => [ + ['length(AuditLog.change) >=' => self::COMPRESS_MIN_LENGTH], + ['AuditLog.change LIKE' => self::BROTLI_HEADER . '%'], + ]], ]); - foreach ($changes as $change) { - $this->save($change, true, ['id', 'change']); + + $options = [ + 'validate' => false, + 'callbacks' => false, + 'fieldList' => ['change'], + ]; + + foreach (array_chunk($changes, 100) as $chunk) { + $toSave = []; + foreach ($chunk as $change) { + $change['AuditLog']['change'] = $this->encodeChange($change['AuditLog']['change']); + $toSave[] = $change; + } + $this->saveMany($toSave, $options); } } diff --git a/app/Model/AuthKey.php b/app/Model/AuthKey.php index e1caa4e10..fbbd80f1e 100644 --- a/app/Model/AuthKey.php +++ b/app/Model/AuthKey.php @@ -55,6 +55,7 @@ class AuthKey extends AppModel $this->data['AuthKey']['authkey_raw'] = $authkey; } + $validAllowedIpFound = false; if (!empty($this->data['AuthKey']['allowed_ips'])) { $allowedIps = &$this->data['AuthKey']['allowed_ips']; if (is_string($allowedIps)) { @@ -70,12 +71,18 @@ class AuthKey extends AppModel if (!is_array($allowedIps)) { $this->invalidate('allowed_ips', 'Allowed IPs must be array'); } + foreach ($allowedIps as $cidr) { if (!CidrTool::validate($cidr)) { $this->invalidate('allowed_ips', "$cidr is not valid IP range"); + } else { + $validAllowedIpFound = true; } } } + if (!empty(Configure::read('Security.mandate_ip_allowlist_advanced_authkeys')) && $validAllowedIpFound === false){ + $this->invalidate('allowed_ips', "Setting an ip allowlist is mandatory on this instance."); + } $creationTime = isset($this->data['AuthKey']['created']) ? $this->data['AuthKey']['created'] : time(); $validity = Configure::read('Security.advanced_authkeys_validity'); @@ -104,6 +111,12 @@ class AuthKey extends AppModel if (isset($val['AuthKey']['allowed_ips'])) { $results[$key]['AuthKey']['allowed_ips'] = JsonTool::decode($val['AuthKey']['allowed_ips']); } + if (isset($val['AuthKey']['unique_ips'])) { + $results[$key]['AuthKey']['unique_ips'] = JsonTool::decode($val['AuthKey']['unique_ips']); + } else { + $results[$key]['AuthKey']['unique_ips'] = []; + } + } return $results; } @@ -117,6 +130,13 @@ class AuthKey extends AppModel $this->data['AuthKey']['allowed_ips'] = JsonTool::encode($this->data['AuthKey']['allowed_ips']); } } + if (isset($this->data['AuthKey']['unique_ips'])) { + if (empty($this->data['AuthKey']['unique_ips'])) { + $this->data['AuthKey']['unique_ips'] = null; + } else { + $this->data['AuthKey']['unique_ips'] = JsonTool::encode($this->data['AuthKey']['unique_ips']); + } + } return true; } @@ -162,12 +182,27 @@ class AuthKey extends AppModel $possibleAuthkeys = $this->find('all', [ 'recursive' => -1, - 'fields' => ['id', 'authkey', 'user_id', 'expiration', 'allowed_ips', 'read_only'], + 'fields' => ['id', 'authkey', 'user_id', 'expiration', 'allowed_ips', 'read_only', 'unique_ips'], 'conditions' => $conditions, ]); $passwordHasher = $this->getHasher(); foreach ($possibleAuthkeys as $possibleAuthkey) { - if ($passwordHasher->check($authkey, $possibleAuthkey['AuthKey']['authkey'])) { + if ($passwordHasher->check($authkey, $possibleAuthkey['AuthKey']['authkey'])) { // valid authkey + // store IP in db if not there yet + $remote_ip = $this->_remoteIp(); + $update_db_ip = true; + if (in_array($remote_ip, $possibleAuthkey['AuthKey']['unique_ips'])) { + $update_db_ip = false; // IP already seen, skip saving in DB + } else { // first time this IP is seen for this API key + $possibleAuthkey['AuthKey']['unique_ips'][] = $remote_ip; + } + if ($update_db_ip) { + // prevent double entries due to race condition + $possibleAuthkey['AuthKey']['unique_ips'] = array_unique($possibleAuthkey['AuthKey']['unique_ips']); + // save in db + $this->save($possibleAuthkey, ['fieldList' => ['unique_ips']]); + } + // fetch user $user = $this->User->getAuthUser($possibleAuthkey['AuthKey']['user_id']); if ($user) { $user = $this->setUserData($user, $possibleAuthkey); @@ -191,9 +226,9 @@ class AuthKey extends AppModel $user['authkey_read_only'] = (bool)$authkey['AuthKey']['read_only']; if ($authkey['AuthKey']['read_only']) { - // Disable all permissions, keep just `perm_auth` unchanged + // Disable all permissions, keep just `perm_auth` and `perm_audit` unchanged foreach ($user['Role'] as $key => &$value) { - if (substr($key, 0, 5) === 'perm_' && $key !== 'perm_auth') { + if (substr($key, 0, 5) === 'perm_' && $key !== 'perm_auth' && $key !== 'perm_audit') { $value = 0; } } diff --git a/app/Model/Behavior/AuditLogBehavior.php b/app/Model/Behavior/AuditLogBehavior.php index 9355373d3..bc87e8e0b 100644 --- a/app/Model/Behavior/AuditLogBehavior.php +++ b/app/Model/Behavior/AuditLogBehavior.php @@ -26,9 +26,10 @@ class AuditLogBehavior extends ModelBehavior 'current_login' => true, // User 'last_login' => true, // User 'newsread' => true, // User + 'unique_ips' => true, // User 'proposal_email_lock' => true, // Event 'enable_password' => true, - 'confirm_password' => true + 'confirm_password' => true, ]; private $modelInfo = [ @@ -62,18 +63,18 @@ class AuditLogBehavior extends ModelBehavior { // Generate model info for attribute and proposals $attributeInfo = function (array $new, array $old) { - $category = isset($new['category']) ? $new['category'] : $old['category']; - $type = isset($new['type']) ? $new['type'] : $old['type']; - $value1 = trim(isset($new['value1']) ? $new['value1'] : $old['value1']); - $value2 = trim(isset($new['value2']) ? $new['value2'] : $old['value2']); + $category = $new['category'] ?? $old['category']; + $type = $new['type'] ?? $old['type']; + $value1 = trim($new['value1'] ?? $old['value1']); + $value2 = trim($new['value2'] ?? $old['value2']); $value = $value1 . (empty($value2) ? '' : '|' . $value2); return "$category/$type $value"; }; $this->modelInfo['Attribute'] = $attributeInfo; $this->modelInfo['ShadowAttribute'] = $attributeInfo; $this->modelInfo['AuthKey'] = function (array $new, array $old) { - $start = isset($new['authkey_start']) ? $new['authkey_start'] : $old['authkey_start']; - $end = isset($new['authkey_end']) ? $new['authkey_end'] : $old['authkey_end']; + $start = $new['authkey_start'] ?? $old['authkey_start']; + $end = $new['authkey_end'] ?? $old['authkey_end']; return "$start********************************$end"; }; } @@ -171,7 +172,7 @@ class AuditLogBehavior extends ModelBehavior if (isset($this->modelInfo[$model->name])) { $modelTitleField = $this->modelInfo[$model->name]; if (is_callable($modelTitleField)) { - $modelTitle = $modelTitleField($data, isset($this->beforeSave[$model->alias]) ? $this->beforeSave[$model->alias] : []); + $modelTitle = $modelTitleField($data, $this->beforeSave[$model->alias] ?? []); } else if (isset($data[$modelTitleField])) { $modelTitle = $data[$modelTitleField]; } else if ($this->beforeSave[$model->alias][$modelTitleField]) { @@ -182,7 +183,7 @@ class AuditLogBehavior extends ModelBehavior $modelName = $model->name === 'MispObject' ? 'Object' : $model->name; if ($modelName === 'AttributeTag' || $modelName === 'EventTag') { - $isLocal = isset($data['local']) ? $data['local'] : false; + $isLocal = $data['local'] ?? false; $action = $isLocal ? AuditLog::ACTION_TAG_LOCAL : AuditLog::ACTION_TAG; $tagInfo = $this->getTagInfo($model, $data['tag_id']); if ($tagInfo) { @@ -242,7 +243,7 @@ class AuditLogBehavior extends ModelBehavior if ($model->name === 'Event') { $eventId = $model->id; } else { - $eventId = isset($model->data[$model->alias]['event_id']) ? $model->data[$model->alias]['event_id'] : null; + $eventId = $model->data[$model->alias]['event_id'] ?? null; } $modelTitle = null; @@ -260,7 +261,7 @@ class AuditLogBehavior extends ModelBehavior $id = $model->id; if ($modelName === 'AttributeTag' || $modelName === 'EventTag') { - $isLocal = isset($model->data[$model->alias]['local']) ? $model->data[$model->alias]['local'] : false; + $isLocal = $model->data[$model->alias]['local'] ?? false; $action = $isLocal ? AuditLog::ACTION_REMOVE_TAG_LOCAL : AuditLog::ACTION_REMOVE_TAG; $tagInfo = $this->getTagInfo($model, $model->data[$model->alias]['tag_id']); if ($tagInfo) { diff --git a/app/Model/Behavior/DefaultCorrelationBehavior.php b/app/Model/Behavior/DefaultCorrelationBehavior.php index 29beaba68..a61271440 100644 --- a/app/Model/Behavior/DefaultCorrelationBehavior.php +++ b/app/Model/Behavior/DefaultCorrelationBehavior.php @@ -95,7 +95,7 @@ class DefaultCorrelationBehavior extends ModelBehavior return [ $value, (int) $a['Event']['id'], - (int) $a['Attribute']['object_id'], + (int) ($a['Attribute']['object_id'] ?? 0), (int) $a['Attribute']['id'], (int) $a['Event']['org_id'], (int) $a['Attribute']['distribution'], @@ -105,7 +105,7 @@ class DefaultCorrelationBehavior extends ModelBehavior (int) $a['Event']['sharing_group_id'], empty($a['Attribute']['object_id']) ? 0 : (int) $a['Object']['sharing_group_id'], (int) $b['Event']['id'], - (int) $b['Attribute']['object_id'], + (int) ($b['Attribute']['object_id'] ?? 0), (int) $b['Attribute']['id'], (int) $b['Event']['org_id'], (int) $b['Attribute']['distribution'], @@ -527,7 +527,6 @@ class DefaultCorrelationBehavior extends ModelBehavior $correlation[$prefix . 'object_id'] && ( $correlation[$prefix . 'object_distribution'] == 0 || - $correlation[$prefix . 'object_distribution'] == 5 || ( $correlation[$prefix . 'object_distribution'] == 4 && !in_array($correlation[$prefix . 'object_sharing_group_id'], $sgids) diff --git a/app/Model/Bruteforce.php b/app/Model/Bruteforce.php index 2c7894ba9..46c8342f7 100644 --- a/app/Model/Bruteforce.php +++ b/app/Model/Bruteforce.php @@ -5,31 +5,50 @@ App::uses('Sanitize', 'Utility'); class Bruteforce extends AppModel { - public function insert($ip, $username) + public function insert($username) { $this->Log = ClassRegistry::init('Log'); $this->Log->create(); + $ip = $this->_remoteIp(); $expire = Configure::check('SecureAuth.expire') ? Configure::read('SecureAuth.expire') : 300; $amount = Configure::check('SecureAuth.amount') ? Configure::read('SecureAuth.amount') : 5; - $expire = time() + $expire; - $expire = date('Y-m-d H:i:s', $expire); + $expireTime = time() + $expire; + $expireTime = date('Y-m-d H:i:s', $expireTime); $bruteforceEntry = array( 'ip' => $ip, 'username' => trim(strtolower($username)), - 'expire' => $expire + 'expire' => $expireTime ); $this->save($bruteforceEntry); - $title = 'Failed login attempt using username ' . $username . ' from IP: ' . $_SERVER['REMOTE_ADDR'] . '.'; - if ($this->isBlocklisted($ip, $username)) { - $title .= 'This has tripped the bruteforce protection after ' . $amount . ' failed attempts. The user is now blocklisted for ' . $expire . ' seconds.'; + $title = 'Failed login attempt using username ' . $username . ' from IP: ' . $ip . '.'; + if ($this->isBlocklisted($username)) { + $change = 'This has tripped the bruteforce protection after ' . $amount . ' failed attempts. The source IP/username is now blocklisted for ' . $expire . ' seconds.'; + } else { + $change = ''; } + // lookup the real user details + $this->User = ClassRegistry::init('User'); + $user = $this->User->find('first', array( + 'conditions' => array('User.email' => $username), + 'fields' => array('User.id', 'Organisation.name'), + 'recursive' => 0)); + if ($user) { + $org = $user['Organisation']['name']; + $userId = $user['User']['id']; + } else { + $org = 'SYSTEM'; + $userId = 0; + } + $log = array( - 'org' => 'SYSTEM', + 'org' => $org, 'model' => 'User', - 'model_id' => 0, + 'model_id' => $userId, 'email' => $username, + 'user_id' => $userId, 'action' => 'login_fail', - 'title' => $title + 'title' => $title, + 'change' => $change ); $this->Log->save($log); } @@ -45,11 +64,12 @@ class Bruteforce extends AppModel $this->query($sql); } - public function isBlocklisted($ip, $username) + public function isBlocklisted($username) { // first remove old expired rows $this->clean(); // count + $ip = $this->_remoteIp(); $params = array( 'conditions' => array( 'Bruteforce.ip' => $ip, diff --git a/app/Model/Community.php b/app/Model/Community.php index 60429a0b7..870657685 100644 --- a/app/Model/Community.php +++ b/app/Model/Community.php @@ -4,37 +4,20 @@ class Community extends AppModel { public $useTable = false; - public $recursive = -1; - - public $actsAs = array( - 'Containable', - ); - - public $validate = array( - ); - - public function beforeValidate($options = array()) - { - parent::beforeValidate(); - return true; - } - + /** + * @param string $context + * @param string|null $value + * @return array + */ public function getCommunityList($context, $value) { - $community_file = new File(APP . 'files/community-metadata/defaults.json'); - if (!$community_file->exists()) { - throw new NotFoundException(__('Default community list not found.')); - } - $community_list = $community_file->read(); - if (empty($community_list)) { - throw new NotFoundException(__('Default community list empty.')); - } try { - $community_list = json_decode($community_list, true); + $community_list = FileAccessTool::readJsonFromFile(APP . 'files/community-metadata/defaults.json'); } catch (Exception $e) { throw new NotFoundException(__('Default community list not in the expected format.')); } - $fieldsToCheck = array('name', 'uuid', 'description', 'url', 'sector', 'nationality', 'type', 'org_uuid', 'org_name'); + + $fieldsToCheck = ['name', 'uuid', 'description', 'url', 'sector', 'nationality', 'type', 'org_uuid', 'org_name']; foreach ($community_list as $k => $v) { if ($v['misp_project_vetted'] === ($context === 'vetted')) { $community_list[$k]['id'] = $k + 1; @@ -44,11 +27,12 @@ class Community extends AppModel continue; } if (!empty($value)) { + $value = mb_strtolower($value); $found = false; foreach ($fieldsToCheck as $field) { - if (strpos(strtolower($v[$field]), $value) !== false) { + if (strpos(mb_strtolower($v[$field]), $value) !== false) { $found = true; - continue; + break; } } if (!$found) { @@ -56,42 +40,32 @@ class Community extends AppModel } } } - $community_list = array_values($community_list); - return $community_list; + return array_values($community_list); } + /** + * @param int|string $id Community ID or UUID + * @return array + */ public function getCommunity($id) { - $community_file = new File(APP . 'files/community-metadata/defaults.json'); - if (!$community_file->exists()) { - throw new NotFoundException(__('Default community list not found.')); - } - $community_list = $community_file->read(); - if (empty($community_list)) { - throw new NotFoundException(__('Default community list empty.')); - } try { - $community_list = json_decode($community_list, true); + $community_list = FileAccessTool::readJsonFromFile(APP . 'files/community-metadata/defaults.json'); } catch (Exception $e) { throw new NotFoundException(__('Default community list not in the expected format.')); } + foreach ($community_list as $k => $v) { $community_list[$k]['id'] = $k + 1; $community_list[$k]['Org'] = array('uuid' => $v['org_uuid'], 'name' => $v['org_name']); } - $community = false; - $lookupField = 'id'; - if (Validation::uuid($id)) { - $lookupField = 'uuid'; - } + + $lookupField = Validation::uuid($id) ? 'uuid' : 'id'; foreach ($community_list as $s) { - if ($s[$lookupField === 'uuid' ? 'uuid' : 'id'] === $id) { - $community = $s; + if ($s[$lookupField === 'uuid' ? 'uuid' : 'id'] == $id) { + return $s; } } - if (empty($community)) { - throw new NotFoundException(__('Community not found.')); - } - return $community; + throw new NotFoundException(__('Community not found.')); } } diff --git a/app/Model/Correlation.php b/app/Model/Correlation.php index c1ad604c6..3bff78c84 100644 --- a/app/Model/Correlation.php +++ b/app/Model/Correlation.php @@ -818,6 +818,11 @@ class Correlation extends AppModel return true; } + /** + * @param array $query + * @return array|false + * @throws RedisException + */ public function findTop(array $query) { try { @@ -825,21 +830,23 @@ class Correlation extends AppModel } catch (Exception $e) { return false; } + $start = $query['limit'] * ($query['page'] -1); $end = $query['limit'] * $query['page'] - 1; $list = $redis->zRevRange(self::CACHE_NAME, $start, $end, true); $results = []; - foreach ($list as $value => $count) { - $realValue = $this->CorrelationValue->find('first', - [ - 'recursive' => -1, - 'conditions' => ['CorrelationValue.id' => $value], - 'fields' => 'CorrelationValue.value' - ] - ); + + $realValues = $this->CorrelationValue->find('list', [ + 'recursive' => -1, + 'conditions' => ['CorrelationValue.id' => array_keys($list)], + 'fields' => ['CorrelationValue.id', 'CorrelationValue.value'], + ]); + + foreach ($list as $valueId => $count) { + $value = $realValues[$valueId] ?? null; $results[] = [ 'Correlation' => [ - 'value' => $realValue['CorrelationValue']['value'], + 'value' => $value, 'count' => $count, 'excluded' => $this->__preventExcludedCorrelations($value), ] diff --git a/app/Model/Dashboard.php b/app/Model/Dashboard.php index c74698530..0503b2bd1 100644 --- a/app/Model/Dashboard.php +++ b/app/Model/Dashboard.php @@ -97,6 +97,7 @@ class Dashboard extends AppModel } } } + ksort($widgets); return $widgets; } diff --git a/app/Model/Datasource/Database/MysqlExtended.php b/app/Model/Datasource/Database/MysqlExtended.php index 9e7eafc29..df20c4281 100644 --- a/app/Model/Datasource/Database/MysqlExtended.php +++ b/app/Model/Datasource/Database/MysqlExtended.php @@ -119,6 +119,33 @@ class MysqlExtended extends Mysql return isset($forceIndexHint) ? ('FORCE INDEX ' . $forceIndexHint) : null; } + /** + * - Do not call microtime when not necessary + * - Count query count even when logging is disabled + * + * @param string $sql + * @param array $options + * @param array $params + * @return mixed + */ + public function execute($sql, $options = [], $params = []) + { + $log = $options['log'] ?? $this->fullDebug; + + if ($log) { + $t = microtime(true); + $this->_result = $this->_execute($sql, $params); + $this->took = round((microtime(true) - $t) * 1000); + $this->numRows = $this->affected = $this->lastAffected(); + $this->logQuery($sql, $params); + } else { + $this->_result = $this->_execute($sql, $params); + $this->_queriesCnt++; + } + + return $this->_result; + } + /** * Reduce memory usage for insertMulti * diff --git a/app/Model/DecayingModelsFormulas/PolynomialExtended.php b/app/Model/DecayingModelsFormulas/PolynomialExtended.php index 4fa8ecfbb..71c98a9a3 100644 --- a/app/Model/DecayingModelsFormulas/PolynomialExtended.php +++ b/app/Model/DecayingModelsFormulas/PolynomialExtended.php @@ -18,7 +18,7 @@ class PolynomialExtended extends Polynomial } else { $retention_taxonomy_id = $retention_taxonomy_id['Taxonomy']['id']; } - $taxonomy = $this->Taxonomy->getTaxonomy($retention_taxonomy_id, array('full' => true)); + $taxonomy = $this->Taxonomy->getTaxonomy($retention_taxonomy_id); $this->retention_taxonomy = array(); foreach ($taxonomy['entries'] as $k => $entry) { $this->retention_taxonomy[$entry['tag']] = $entry['numerical_value']; diff --git a/app/Model/Event.php b/app/Model/Event.php index 9e18c47b2..06bb7bf7e 100755 --- a/app/Model/Event.php +++ b/app/Model/Event.php @@ -540,7 +540,8 @@ class Event extends AppModel 'local' => $local, 'relationship_type' => $relationship, ]; - $success = $success || $this->EventTag->attachTagToEvent($event_id, $tag, $nothingToChange); + $attachSuccess = $this->EventTag->attachTagToEvent($event_id, $tag, $nothingToChange); + $success = $success || $attachSuccess; $touchEvent = $touchEvent || !$nothingToChange; } if ($touchEvent) { @@ -562,7 +563,8 @@ class Event extends AppModel $success = $success || true; continue; } - $success = $success || $this->EventTag->detachTagFromEvent($event_id, $tag_id, $local, $nothingToChange); + $detachSuccess = $this->EventTag->detachTagFromEvent($event_id, $tag_id, $local, $nothingToChange); + $success = $success || $detachSuccess; $touchEvent = $touchEvent || !$nothingToChange; } if ($touchEvent) { @@ -1526,7 +1528,11 @@ class Event extends AppModel 'recursive' => -1, ); if (isset($params['order'])) { - $find_params['order'] = $params['order']; + $find_params['order'] = $this->findOrder( + $params['order'], + 'Event', + ['id', 'info', 'analysis', 'threat_level_id', 'distribution', 'timestamp', 'publish_timestamp'] + ); } if (isset($params['limit'])) { // Get the count (but not the actual data) of results for paginators @@ -2006,7 +2012,11 @@ class Event extends AppModel $params['page'] = $options['page']; } if (!empty($options['order'])) { - $params['order'] = $options['order']; + $options['order'] = $this->findOrder( + $options['order'], + 'Event', + ['id', 'info', 'analysis', 'threat_level_id', 'distribution', 'timestamp', 'publish_timestamp'] + ); } $results = $this->find('all', $params); if (empty($results)) { @@ -2089,7 +2099,7 @@ class Event extends AppModel // Include information about event creator user email. This information is included for: // - users from event creator org // - site admins - // In export, this information will be included in `event_creator_email` field just for auditors of event creator org. + // In export, this information will be included in `event_creator_email` field for auditors of event creator org and site admins. $sameOrg = $event['Event']['orgc_id'] === $user['org_id']; if ($sameOrg || $user['Role']['perm_site_admin']) { if (!isset($userEmails[$event['Event']['user_id']])) { @@ -2097,7 +2107,7 @@ class Event extends AppModel } $userEmail = $userEmails[$event['Event']['user_id']]; - if ($sameOrg && $user['Role']['perm_audit']) { + if ($sameOrg && $user['Role']['perm_audit'] || $user['Role']['perm_site_admin']) { $event['Event']['event_creator_email'] = $userEmail; } $event['User']['email'] = $userEmail; @@ -2852,10 +2862,35 @@ class Event extends AppModel return $conditions; } + /** + * @param string $value + * @return string + */ + private static function compressIpv6($value) + { + if (strpos($value, ':') && $converted = inet_pton($value)) { + return inet_ntop($converted); + } + return $value; + } + public function set_filter_value(&$params, $conditions, $options) { if (!empty($params['value'])) { $params[$options['filter']] = $this->convert_filters($params['value']); + foreach (['OR', 'AND', 'NOT'] as $operand) { + if (!empty($params[$options['filter']][$operand])) { + foreach ($params[$options['filter']][$operand] as $k => $v) { + if ($operand === 'NOT') { + $v = mb_substr($v, 1); + } + if (filter_var($v, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $v = $this->compressIpv6($v); + } + $params[$options['filter']][$operand][$k] = $operand === 'NOT' ? '!' . $v : $v; + } + } + } $conditions = $this->generic_add_filter($conditions, $params['value'], ['Attribute.value1', 'Attribute.value2']); } @@ -3365,7 +3400,11 @@ class Event extends AppModel foreach ($event['EventTag'] as $tag) { $tagId = $this->captureTagWithCache($tag['Tag'], $user, $capturedTags); if ($tagId && !in_array($tagId, $event_tag_ids)) { - $eventTags[] = array('tag_id' => $tagId); + $eventTags[] = array( + 'tag_id' => $tagId, + 'local' => isset($tag['local']) ? $tag['local'] : 0, + 'relationship_type' => isset($tag['relationship_type']) ? $tag['relationship_type'] : '', + ); $event_tag_ids[] = $tagId; } } @@ -3380,6 +3419,7 @@ class Event extends AppModel $eventTags[] = [ 'tag_id' => $tag_id, 'local' => isset($tag['local']) ? $tag['local'] : 0, + 'relationship_type' => isset($tag['relationship_type']) ? $tag['relationship_type'] : '', ]; $event_tag_ids[] = $tag_id; } @@ -3440,7 +3480,11 @@ class Event extends AppModel $a['AttributeTag'] = array($a['AttributeTag']); } foreach ($a['AttributeTag'] as $tag) { - $attributeTags[] = array('tag_id' => $this->captureTagWithCache($tag['Tag'], $user, $capturedTags)); + $attributeTags[] = array( + 'tag_id' => $this->captureTagWithCache($tag['Tag'], $user, $capturedTags), + 'local' => isset($tag['local']) ? $tag['local'] : 0, + 'relationship_type' => isset($tag['relationship_type']) ? $tag['relationship_type'] : '', + ); } } if (isset($a['Tag'])) { @@ -3453,6 +3497,7 @@ class Event extends AppModel $attributeTags[] = [ 'tag_id' => $tagId, 'local' => isset($tag['local']) ? $tag['local'] : 0, + 'relationship_type' => isset($tag['relationship_type']) ? $tag['relationship_type'] : '', ]; } } @@ -3605,20 +3650,12 @@ class Event extends AppModel if (!isset($data['Event']['orgc_id']) && !isset($data['Event']['orgc'])) { $data['Event']['orgc_id'] = $data['Event']['org_id']; } else { - if (!isset($data['Event']['Orgc'])) { - if (isset($data['Event']['orgc_id']) && $data['Event']['orgc_id'] != $user['org_id'] && !$user['Role']['perm_sync'] && !$user['Role']['perm_site_admin']) { + $orgc_id = $data['Event']['orgc_id'] ?? null; + $orgc_uuid = $data['Event']['Orgc']['uuid'] ?? null; + if (!$user['Role']['perm_sync'] && !$user['Role']['perm_site_admin']) { + if ($orgc_uuid != $user['Organisation']['uuid'] && $orgc_id != $user['org_id']) { throw new MethodNotAllowedException('Event cannot be created as you are not a member of the creator organisation.'); } - } else { - if ($data['Event']['Orgc']['uuid'] != $user['Organisation']['uuid'] && !$user['Role']['perm_sync'] && !$user['Role']['perm_site_admin']) { - throw new MethodNotAllowedException('Event cannot be created as you are not a member of the creator organisation.'); - } - if (isset($data['Event']['orgc']) && $data['Event']['orgc'] != $user['Organisation']['name'] && !$user['Role']['perm_sync'] && !$user['Role']['perm_site_admin']) { - throw new MethodNotAllowedException('Event cannot be created as you are not a member of the creator organisation.'); - } - } - if (isset($data['Event']['orgc_id']) && $data['Event']['orgc_id'] != $user['org_id'] && !$user['Role']['perm_sync'] && !$user['Role']['perm_site_admin']) { - throw new MethodNotAllowedException('Event cannot be created as you are not a member of the creator organisation.'); } } if (!Configure::check('MISP.enableOrgBlocklisting') || Configure::read('MISP.enableOrgBlocklisting') !== false) { @@ -3631,6 +3668,7 @@ class Event extends AppModel $this->OrgBlocklist = ClassRegistry::init('OrgBlocklist'); } if ($this->OrgBlocklist->isBlocked($orgc)) { + $this->OrgBlocklist->saveEventBlocked($orgc); return 'blocked'; } } @@ -4465,7 +4503,7 @@ class Event extends AppModel /** @var Job $job */ $job = ClassRegistry::init('Job'); $message = empty($sightingUuids) ? __('Publishing sightings.') : __('Publishing %s sightings.', count($sightingUuids)); - $jobId = $job->createJob($user, Job::WORKER_PRIO, 'publish_event', "Event ID: $id", $message); + $jobId = $job->createJob($user, Job::WORKER_DEFAULT, 'publish_event', "Event ID: $id", $message); $args = ['publish_sightings', $id, $passAlong, $jobId, $user['id']]; if (!empty($sightingUuids)) { @@ -4473,7 +4511,7 @@ class Event extends AppModel } return $this->getBackgroundJobsTool()->enqueue( - BackgroundJobsTool::PRIO_QUEUE, + BackgroundJobsTool::DEFAULT_QUEUE, BackgroundJobsTool::CMD_EVENT, $args, true, @@ -5586,6 +5624,10 @@ class Event extends AppModel return $resultArray; } + /** + * @param array $result + * @return array + */ public function handleMispFormatFromModuleResult(&$result) { $defaultDistribution = $this->Attribute->defaultDistribution(); @@ -5599,7 +5641,7 @@ class Event extends AppModel $event['Attribute'] = $attributes; } if (!empty($result['results']['Object'])) { - $object = array(); + $objects = array(); foreach ($result['results']['Object'] as $tmp_object) { $tmp_object['distribution'] = (isset($tmp_object['distribution']) ? (int)$tmp_object['distribution'] : $defaultDistribution); $tmp_object['sharing_group_id'] = (isset($tmp_object['sharing_group_id']) ? (int)$tmp_object['sharing_group_id'] : 0); @@ -5623,6 +5665,11 @@ class Event extends AppModel return $event; } + /** + * @param array $attribute + * @param int $defaultDistribution + * @return array + */ private function __fillAttribute($attribute, $defaultDistribution) { if (is_array($attribute['type'])) { @@ -5818,41 +5865,79 @@ class Event extends AppModel * @throws InvalidArgumentException * @throws Exception */ - public function upload_stix(array $user, $file, $stix_version, $original_file, $publish) + public function upload_stix(array $user, $file, $stix_version, $original_file, $publish, $distribution, $sharingGroupId, $galaxiesAsTags, $debug = false) { $scriptDir = APP . 'files' . DS . 'scripts'; - if ($stix_version == '2') { + if ($stix_version == '2' || $stix_version == '2.0' || $stix_version == '2.1') { $scriptFile = $scriptDir . DS . 'stix2' . DS . 'stix2misp.py'; - $output_path = $file . '.stix2'; - $stix_version = "STIX 2.0"; + $output_path = $file . '.out'; + $shell_command = [ + ProcessTool::pythonBin(), + $scriptFile, + '-i', $file, + '--distribution', $distribution + ]; + if ($distribution == 4) { + array_push($shell_command, '--sharing_group_id', $sharingGroupId); + } + if ($galaxiesAsTags) { + $shell_command[] = '--galaxies_as_tags'; + } + if ($debug) { + $shell_command[] = '--debug'; + } + $stix_version = "STIX 2.1"; } elseif ($stix_version == '1' || $stix_version == '1.1' || $stix_version == '1.2') { $scriptFile = $scriptDir . DS . 'stix2misp.py'; $output_path = $file . '.json'; + $shell_command = [ + ProcessTool::pythonBin(), + $scriptFile, + $file, + Configure::read('MISP.default_event_distribution'), + Configure::read('MISP.default_attribute_distribution'), + $this->__getTagNamesFromSynonyms($scriptDir) + ]; $stix_version = "STIX 1.1"; } else { throw new InvalidArgumentException('Invalid STIX version'); } - $shell_command = [ - ProcessTool::pythonBin(), - $scriptFile, - $file, - Configure::read('MISP.default_event_distribution'), - Configure::read('MISP.default_attribute_distribution'), - $this->__getTagNamesFromSynonyms($scriptDir), - ]; - $result = ProcessTool::execute($shell_command, null, true); $result = preg_split("/\r\n|\n|\r/", trim($result)); $result = trim(end($result)); $tempFile = file_get_contents($file); unlink($file); - if ($result === '1') { + $decoded = JsonTool::decode($result); + if (!empty($decoded['success'])) { $data = FileAccessTool::readAndDelete($output_path); $data = $this->jsonDecode($data); if (empty($data['Event'])) { $data = array('Event' => $data); } + if (!$galaxiesAsTags) { + if (!isset($this->GalaxyCluster)) { + $this->GalaxyCluster = ClassRegistry::init('GalaxyCluster'); + } + $this->__handleGalaxiesAndClusters($user, $data['Event']); + if (!empty($data['Event']['Attribute'])) { + foreach ($data['Event']['Attribute'] as &$attribute) { + $this->__handleGalaxiesAndClusters($user, $attribute); + } + } + if (!empty($data['Event']['Object'])) { + foreach ($data['Event']['Object'] as &$misp_object) { + if (!empty($misp_object['Attribute'])) { + foreach ($misp_object['Attribute'] as &$attribute) { + $this->__handleGalaxiesAndClusters($user, $attribute); + } + } + } + } + } + if (!empty($decoded['stix_version'])) { + $stix_version = 'STIX ' . $decoded['stix_version']; + } $created_id = false; $validationIssues = false; $result = $this->_add($data, true, $user, '', null, false, null, $created_id, $validationIssues); @@ -5870,13 +5955,8 @@ class Event extends AppModel return $result; } return $validationIssues; - } else if ($result === '2') { - $response = __('Issues while loading the stix file.'); - } elseif ($result === '3') { - $response = __('Issues with the maec library.'); - } else { - $response = __('Issues executing the ingestion script or invalid input.'); } + $response = __($decoded['error']); if (!$user['Role']['perm_site_admin']) { $response .= ' ' . __('Please ask your administrator to'); } else { @@ -5886,6 +5966,19 @@ class Event extends AppModel return $response; } + private function __handleGalaxiesAndClusters($user, &$data) + { + if (!empty($data['Galaxy'])) { + $tag_names = $this->GalaxyCluster->convertGalaxyClustersToTags($user, $data['Galaxy']); + if (empty($data['Tag'])) { + $data['Tag'] = []; + } + foreach ($tag_names as $tag_name) { + $data['Tag'][] = array('name' => $tag_name); + } + } + } + /** * @param string $scriptDir * @return string @@ -6319,11 +6412,33 @@ class Event extends AppModel return $message; } - public function processModuleResultsData($user, $resolved_data, $id, $default_comment = '', $jobId = false, $adhereToWarninglists = false, $event_level = false) + /** + * @param array $user + * @param array $resolved_data + * @param int $id + * @param string $default_comment + * @param int|false $jobId + * @param bool $adhereToWarninglists + * @param bool $event_level + * @return int|string + * @throws JsonException + */ + public function processModuleResultsData(array $user, $resolved_data, $id, $default_comment = '', $jobId = false, $adhereToWarninglists = false, $event_level = false) { + $event = $this->find('first', [ + 'recursive' => -1, + 'conditions' => ['id' => $id], + ]); + if (empty($event)) { + throw new Exception("Event with ID `$id` not found."); + } if ($jobId) { $this->Job = ClassRegistry::init('Job'); $this->Job->id = $jobId; + + /** @var EventLock $eventLock */ + $eventLock = ClassRegistry::init('EventLock'); + $eventLock->insertLockBackgroundJob($event['Event']['id'], $jobId); } $failed_attributes = $failed_objects = $failed_object_attributes = $failed_reports = 0; $saved_attributes = $saved_objects = $saved_object_attributes = $saved_reports = 0; @@ -6369,6 +6484,7 @@ class Event extends AppModel } } } else { + $this->Attribute->logDropped($user, $attribute); $failed_attributes++; $lastAttributeError = $this->Attribute->validationErrors; $original_uuid = $this->__findOriginalUUID( @@ -6384,8 +6500,7 @@ class Event extends AppModel } if ($jobId) { $processedAttributes++; - $this->Job->saveField('message', 'Attribute ' . $processedAttributes . '/' . $total_attributes); - $this->Job->saveField('progress', ($processedAttributes * 100 / $items_count)); + $this->Job->saveProgress($jobId, "Attribute $processedAttributes/$total_attributes", $processedAttributes * 100 / $items_count); } } } else { @@ -6430,7 +6545,7 @@ class Event extends AppModel if (isset($initial_attributes[$object_relation]) && in_array($object_attribute['value'], $initial_attributes[$object_relation])) { continue; } - if ($this->__saveObjectAttribute($object_attribute, $default_comment, $id, $initial_object_id, $user)) { + if ($this->__saveObjectAttribute($object_attribute, null, $event, $initial_object_id, $user)) { $saved_object_attributes++; } else { $failed_object_attributes++; @@ -6463,7 +6578,7 @@ class Event extends AppModel if ($this->Object->save($object)) { $object_id = $this->Object->id; foreach ($object['Attribute'] as $object_attribute) { - if ($this->__saveObjectAttribute($object_attribute, $default_comment, $id, $object_id, $user)) { + if ($this->__saveObjectAttribute($object_attribute, null, $event, $object_id, $user)) { $saved_object_attributes++; } else { $failed_object_attributes++; @@ -6498,8 +6613,7 @@ class Event extends AppModel } if ($jobId) { $processedObjects++; - $this->Job->saveField('message', 'Object ' . $processedObjects . '/' . $total_objects); - $this->Job->saveField('progress', (($processedObjects + $total_attributes) * 100 / $items_count)); + $this->Job->saveProgress($jobId, "Object $processedObjects/$total_objects", ($processedObjects + $total_attributes) * 100 / $items_count); } } @@ -6566,14 +6680,13 @@ class Event extends AppModel } if ($jobId) { $current = ($i + 1); - $this->Job->saveField('message', 'EventReport ' . $current . '/' . $total_reports); - $this->Job->saveField('progress', ($current * 100 / $items_count)); + $this->Job->saveProgress($jobId, "EventReport $current/$total_reports", $current * 100 / $items_count); } } } if ($saved_attributes > 0 || $saved_objects > 0 || $saved_reports > 0) { - $this->unpublishEvent($id); + $this->unpublishEvent($event); } if ($event_level) { return $saved_attributes + $saved_object_attributes + $saved_reports; @@ -6634,8 +6747,8 @@ class Event extends AppModel $message .= $failed_reports . $reason; } if ($jobId) { - $this->Job->saveField('message', 'Processing complete. ' . $message); - $this->Job->saveField('progress', 100); + $this->Job->saveStatus($jobId, true, 'Processing complete. ' . $message); + $eventLock->deleteBackgroundJobLock($event['Event']['id'], $jobId); } return $message; } @@ -6722,28 +6835,39 @@ class Event extends AppModel return (!empty($original_uuid)) ? $original_uuid['Object']['uuid'] : $original_uuid; } - private function __saveObjectAttribute($attribute, $default_comment, $event_id, $object_id, $user) + /** + * @param array $attribute + * @param string|null $default_comment + * @param array $event + * @param int $object_id + * @param array $user + * @return array|bool|mixed + * @throws Exception + */ + private function __saveObjectAttribute(array $attribute, $default_comment, array $event, $object_id, array $user) { $attribute['object_id'] = $object_id; - $attribute['event_id'] = $event_id; - if (empty($attribute['comment'])) { + $attribute['event_id'] = $event['Event']['id']; + if (empty($attribute['comment']) && $default_comment) { $attribute['comment'] = $default_comment; } if (!empty($attribute['data']) && !empty($attribute['encrypt'])) { $attribute = $this->Attribute->onDemandEncrypt($attribute); } $this->Attribute->create(); - $attribute_save = $this->Attribute->save($attribute); + $attribute_save = $this->Attribute->save($attribute, ['parentEvent' => $event]); if ($attribute_save) { if (!empty($attribute['Tag'])) { foreach ($attribute['Tag'] as $tag) { $tag_id = $this->Attribute->AttributeTag->Tag->captureTag($tag, $user); $relationship_type = empty($tag['relationship_type']) ? false : $tag['relationship_type']; if ($tag_id) { - $this->Attribute->AttributeTag->attachTagToAttribute($this->Attribute->id, $event_id, $tag_id, !empty($tag['local']), $relationship_type); + $this->Attribute->AttributeTag->attachTagToAttribute($this->Attribute->id, $event['Event']['id'], $tag_id, !empty($tag['local']), $relationship_type); } } } + } else { + $this->Attribute->logDropped($user, $attribute); } return $attribute_save; } @@ -7047,6 +7171,47 @@ class Event extends AppModel } } + + public function restSearchFilterMassage($filters, $non_restrictive_export, $user) + { + if (!empty($filters['ignore'])) { + $filters['to_ids'] = array(0, 1); + $filters['published'] = array(0, 1); + } + if (!empty($filters['quickFilter'])) { + $filters['searchall'] = $filters['quickFilter']; + if (!empty($filters['value'])) { + unset($filters['value']); + } + } + if (isset($filters['searchall'])) { + if (!empty($filters['value'])) { + $filters['wildcard'] = $filters['value']; + } else { + $filters['wildcard'] = $filters['searchall']; + } + } + + if (isset($filters['tag']) and !isset($filters['tags'])) { + $filters['tags'] = $filters['tag']; + } + if (!empty($filters['withAttachments'])) { + $filters['includeAttachments'] = 1; + } + if (empty($non_restrictive_export)) { + if (!isset($filters['to_ids'])) { + $filters['to_ids'] = 1; + } + if (!isset($filters['published'])) { + $filters['published'] = 1; + } + $filters['allow_proposal_blocking'] = 1; + } + $subqueryElements = $this->harvestSubqueryElements($filters); + $filters = $this->addFiltersFromSubqueryElements($filters, $subqueryElements, $user); + return $filters; + } + /** * @param array $user * @param string $returnFormat @@ -7075,49 +7240,18 @@ class Event extends AppModel $exportTool->setDefaultFilters($filters); } - if (empty($exportTool->non_restrictive_export)) { - if (!isset($filters['to_ids'])) { - $filters['to_ids'] = 1; - } - if (!isset($filters['published'])) { - $filters['published'] = 1; - } - $filters['allow_proposal_blocking'] = 1; - } - if (!empty($exportTool->renderView)) { $renderView = $exportTool->renderView; } + $non_restrictive_export = !empty($exportTool->non_restrictive_export); + $filters = $this->restSearchFilterMassage($filters, $non_restrictive_export, $user); - if (!empty($filters['ignore'])) { - $filters['to_ids'] = array(0, 1); - $filters['published'] = array(0, 1); - } - if (!empty($filters['quickFilter'])) { - $filters['searchall'] = $filters['quickFilter']; - if (!empty($filters['value'])) { - unset($filters['value']); - } - } - if (isset($filters['searchall'])) { - if (!empty($filters['value'])) { - $filters['wildcard'] = $filters['value']; - } else { - $filters['wildcard'] = $filters['searchall']; - } - } - - if (isset($filters['tag']) and !isset($filters['tags'])) { - $filters['tags'] = $filters['tag']; - } - $subqueryElements = $this->harvestSubqueryElements($filters); - $filters = $this->addFiltersFromSubqueryElements($filters, $subqueryElements, $user); $filters = $this->addFiltersFromUserSettings($user, $filters); if (empty($exportTool->mock_query_only)) { $filters['include_attribute_count'] = 1; $eventid = $this->filterEventIds($user, $filters, $elementCounter); $eventCount = count($eventid); - $eventids_chunked = $this->__clusterEventIds($exportTool, $eventid); + $eventids_chunked = $this->clusterEventIds($exportTool, $eventid); unset($eventid); } else { $eventids_chunked = array(); @@ -7143,9 +7277,6 @@ class Event extends AppModel $tmpfile = new TmpFileTool(); $tmpfile->write($exportTool->header($exportToolParams)); $i = 0; - if (!empty($filters['withAttachments'])) { - $filters['includeAttachments'] = 1; - } $this->Allowedlist = ClassRegistry::init('Allowedlist'); $separator = $exportTool->separator($exportToolParams); unset($filters['page']); @@ -7184,7 +7315,7 @@ class Event extends AppModel * Chunk them by the attribute count to fit the memory limits * */ - private function __clusterEventIds($exportTool, $eventIds) + public function clusterEventIds($exportTool, $eventIds) { $memory_in_mb = $this->convert_to_memory_limit_to_mb(ini_get('memory_limit')); $default_attribute_memory_coefficient = Configure::check('MISP.default_attribute_memory_coefficient') ? Configure::read('MISP.default_attribute_memory_coefficient') : 80; diff --git a/app/Model/EventReport.php b/app/Model/EventReport.php index d0c074693..1ab7f3dfe 100644 --- a/app/Model/EventReport.php +++ b/app/Model/EventReport.php @@ -408,7 +408,7 @@ class EventReport extends AppModel return $report; } else { if (in_array('edit', $authorizations) || in_array('delete', $authorizations)) { - $checkResult = $this->canEditReport($user, $report); + $checkResult = $user['Role']['perm_site_admin'] || ($report['Event']['orgc_id'] === $user['org_id']); if ($checkResult !== true) { if ($throwErrors) { throw new UnauthorizedException($checkResult); @@ -420,20 +420,6 @@ class EventReport extends AppModel } } - public function canEditReport(array $user, array $report) - { - if ($user['Role']['perm_site_admin']) { - return true; - } - if (empty($report['Event'])) { - return __('Could not find associated event'); - } - if ($report['Event']['orgc_id'] != $user['org_id']) { - return __('Only the creator organisation of the event can modify the report'); - } - return true; - } - public function reArrangeReport(array $report) { $rearrangeObjects = array('Event', 'SharingGroup'); diff --git a/app/Model/EventTag.php b/app/Model/EventTag.php index f60ba2574..c855de995 100644 --- a/app/Model/EventTag.php +++ b/app/Model/EventTag.php @@ -91,7 +91,7 @@ class EventTag extends AppModel /** * @param int $event_id - * @param int $tagId + * @param array $tag * @param bool $nothingToChange * @return bool * @throws Exception diff --git a/app/Model/Feed.php b/app/Model/Feed.php index d3d16140f..413733880 100644 --- a/app/Model/Feed.php +++ b/app/Model/Feed.php @@ -462,6 +462,9 @@ class Feed extends AppModel */ public function attachFeedCorrelations(array $attributes, array $user, array &$event, $overrideLimit = false, $scope = 'Feed') { + if (!isset($user['Role']['perm_view_feed_correlations']) || $user['Role']['perm_view_feed_correlations'] != true) { + return $attributes; + } if (empty($attributes)) { return $attributes; } diff --git a/app/Model/Galaxy.php b/app/Model/Galaxy.php index 40ef276d7..72a001b57 100644 --- a/app/Model/Galaxy.php +++ b/app/Model/Galaxy.php @@ -278,10 +278,9 @@ class Galaxy extends AppModel /** * Capture the Galaxy * - * @param $user * @param array $user * @param array $galaxy The galaxy to be captured - * @return array the captured galaxy + * @return array|false the captured galaxy or false on error */ public function captureGalaxy(array $user, array $galaxy) { @@ -312,21 +311,20 @@ class Galaxy extends AppModel * Import all clusters into the Galaxy they are shipped with, creating the galaxy if not existant. * * This function is meant to be used with manual import or push from remote instance - * @param $user + * @param array $user * @param array $clusters clusters to import * @return array The import result with errors if any */ - public function importGalaxyAndClusters($user, array $clusters) + public function importGalaxyAndClusters(array $user, array $clusters) { $results = array('success' => false, 'imported' => 0, 'ignored' => 0, 'failed' => 0, 'errors' => array()); - foreach ($clusters as $k => $cluster) { - $conditions = array(); + foreach ($clusters as $cluster) { if (!empty($cluster['GalaxyCluster']['Galaxy'])) { $existingGalaxy = $this->captureGalaxy($user, $cluster['GalaxyCluster']['Galaxy']); } elseif (!empty($cluster['GalaxyCluster']['type'])) { $existingGalaxy = $this->find('first', array( 'recursive' => -1, - 'fields' => array('id', 'version'), + 'fields' => array('id'), 'conditions' => array('Galaxy.type' => $cluster['GalaxyCluster']['type']), )); if (empty($existingGalaxy)) { // We don't have enough info to create the galaxy @@ -380,16 +378,16 @@ class Galaxy extends AppModel /** * @param array $user - * @param string $target_type + * @param string $targetType Can be 'event', 'attribute' or 'tag_collection' * @param array $target * @param int $cluster_id * @param bool $local * @return string * @throws Exception */ - public function attachCluster(array $user, $target_type, array $target, $cluster_id, $local = false) + public function attachCluster(array $user, $targetType, array $target, $cluster_id, $local = false) { - $connectorModel = Inflector::camelize($target_type) . 'Tag'; + $connectorModel = Inflector::camelize($targetType) . 'Tag'; $local = $local == 1 || $local === true ? 1 : 0; $cluster_alias = $this->GalaxyCluster->alias; $galaxy_alias = $this->alias; @@ -409,36 +407,36 @@ class Galaxy extends AppModel } $this->Tag = ClassRegistry::init('Tag'); $tag_id = $this->Tag->captureTag(array('name' => $cluster['GalaxyCluster']['tag_name'], 'colour' => '#0088cc', 'exportable' => 1, 'local_only' => $local_only), $user, true); - if ($target_type === 'event') { + if ($targetType === 'event') { $target_id = $target['Event']['id']; - } elseif ($target_type === 'attribute') { + } elseif ($targetType === 'attribute') { $target_id = $target['Attribute']['id']; } else { $target_id = $target['TagCollection']['id']; } - $existingTag = $this->Tag->$connectorModel->hasAny(array($target_type . '_id' => $target_id, 'tag_id' => $tag_id)); + $existingTag = $this->Tag->$connectorModel->hasAny(array($targetType . '_id' => $target_id, 'tag_id' => $tag_id)); if ($existingTag) { return 'Cluster already attached.'; } $this->Tag->$connectorModel->create(); - $toSave = array($target_type . '_id' => $target_id, 'tag_id' => $tag_id, 'local' => $local); - if ($target_type === 'attribute') { + $toSave = array($targetType . '_id' => $target_id, 'tag_id' => $tag_id, 'local' => $local); + if ($targetType === 'attribute') { $toSave['event_id'] = $target['Attribute']['event_id']; } $result = $this->Tag->$connectorModel->save($toSave); if ($result) { if (!$local) { - if ($target_type === 'attribute') { + if ($targetType === 'attribute') { $this->Tag->AttributeTag->Attribute->touch($target); - } elseif ($target_type === 'event') { + } elseif ($targetType === 'event') { $this->Tag->EventTag->Event->unpublishEvent($target); } } - if ($target_type === 'attribute' || $target_type === 'event') { + if ($targetType === 'attribute' || $targetType === 'event') { $this->Tag->EventTag->Event->insertLock($user, $target['Event']['id']); } - $logTitle = 'Attached ' . $cluster['GalaxyCluster']['value'] . ' (' . $cluster['GalaxyCluster']['id'] . ') to ' . $target_type . ' (' . $target_id . ')'; - $this->loadLog()->createLogEntry($user, 'galaxy', ucfirst($target_type), $target_id, $logTitle); + $logTitle = 'Attached ' . $cluster['GalaxyCluster']['value'] . ' (' . $cluster['GalaxyCluster']['id'] . ') to ' . $targetType . ' (' . $target_id . ')'; + $this->loadLog()->createLogEntry($user, 'galaxy', ucfirst($targetType), $target_id, $logTitle); return 'Cluster attached.'; } return 'Could not attach the cluster'; diff --git a/app/Model/GalaxyCluster.php b/app/Model/GalaxyCluster.php index 89a47bcae..a2321079e 100644 --- a/app/Model/GalaxyCluster.php +++ b/app/Model/GalaxyCluster.php @@ -720,14 +720,14 @@ class GalaxyCluster extends AppModel /** * Gets a cluster then save it. * - * @param $user + * @param array $user * @param array $cluster Cluster to be saved * @param bool $fromPull If the current capture is performed from a PULL sync * @param int $orgId The organisation id that should own the cluster * @param array $server The server for which to capture is ongoing * @return array Result of the capture including successes, fails and errors */ - public function captureCluster($user, $cluster, $fromPull=false, $orgId=0, $server=false) + public function captureCluster(array $user, $cluster, $fromPull=false, $orgId=0, $server=false) { $results = array('success' => false, 'imported' => 0, 'ignored' => 0, 'failed' => 0, 'errors' => array()); @@ -1063,8 +1063,12 @@ class GalaxyCluster extends AppModel if (isset($options['group'])) { $params['group'] = $options['group']; } - if (isset($options['order'])) { - $params['order'] = $options['order']; + if (!empty($options['order'])) { + $options['order'] = $this->findOrder( + $options['order'], + 'GalaxyCluster', + ['id', 'event_id', 'version', 'type', 'value', 'distribution', 'orgc_id', 'org_id', 'tag_name', 'galaxy_id'] + ); } if (isset($options['page'])) { $params['page'] = $options['page']; @@ -1667,7 +1671,7 @@ class GalaxyCluster extends AppModel } try { - if (!$serverSync->isSupported(ServerSyncTool::PERM_SYNC) || $serverSync->isSupported(ServerSyncTool::PERM_GALAXY_EDITOR)) { + if (!$serverSync->isSupported(ServerSyncTool::PERM_SYNC) || !$serverSync->isSupported(ServerSyncTool::PERM_GALAXY_EDITOR)) { return __('The remote user does not have the permission to manipulate galaxies - the upload of the galaxy clusters has been blocked.'); } $serverSync->pushGalaxyCluster($cluster)->json(); @@ -1855,7 +1859,7 @@ class GalaxyCluster extends AppModel } /** - * getClusterIdListBasedOnPullTechnique Collect the list of remote cluster IDs to be pulled based on the technique + * Collect the list of remote cluster IDs to be pulled based on the technique * * @param array $user * @param string|int $technique @@ -1895,15 +1899,13 @@ class GalaxyCluster extends AppModel $clusterIds = $this->Server->getElligibleClusterIdsFromServerForPull($serverSync, $onlyUpdateLocalCluster = false); } } catch (HttpSocketHttpException $e) { - if ($e->getCode() === 403) { - return array('error' => array(1, null)); - } else { + if ($e->getCode() !== 403) { $this->logException("Could not get eligible cluster IDs from server {$serverSync->serverId()} for pull.", $e); - return array('error' => array(2, $e->getMessage())); } + return []; } catch (Exception $e) { $this->logException("Could not get eligible cluster IDs from server {$serverSync->serverId()} for pull.", $e); - return array('error' => array(2, $e->getMessage())); + return []; } return $clusterIds; } @@ -2067,4 +2069,31 @@ class GalaxyCluster extends AppModel } return $CyCatRelations; } + + /** + * convertGalaxyClustersToTags + * + * @param array $user + * @param array $galaxies + * @return array The tag names extracted from galaxy clusters + */ + public function convertGalaxyClustersToTags($user, $galaxies) + { + $galaxyClusters = []; + $tag_names = []; + foreach ($galaxies as $galaxy) { + if (empty($galaxy['GalaxyCluster'])) { + continue; + } + $clusters = $galaxy['GalaxyCluster']; + unset($galaxy['GalaxyCluster']); + foreach ($clusters as $cluster) { + $cluster['Galaxy'] = $galaxy; + $galaxyClusters[] = array('GalaxyCluster' => $cluster); + $tag_names[] = !empty($cluster['tag_name']) ? $cluster['tag_name'] : 'misp-galaxy:' . $cluster['type'] . '="' . $cluster['uuid'] . '"'; + } + } + $this->Galaxy->importGalaxyAndClusters($user, $galaxyClusters); + return $tag_names; + } } diff --git a/app/Model/GalaxyClusterBlocklist.php b/app/Model/GalaxyClusterBlocklist.php index fdf87b1d2..8a874c197 100644 --- a/app/Model/GalaxyClusterBlocklist.php +++ b/app/Model/GalaxyClusterBlocklist.php @@ -1,5 +1,6 @@ array( 'unique' => array( - 'rule' => 'isUnique', - 'message' => 'Galaxy Cluster already blocklisted.' + 'rule' => 'isUnique', + 'message' => 'Galaxy Cluster already blocklisted.' ), 'uuid' => array( - 'rule' => array('uuid'), - 'message' => 'Please provide a valid UUID' + 'rule' => array('uuid'), + 'message' => 'Please provide a valid UUID' ), ) ); @@ -35,9 +36,8 @@ class GalaxyClusterBlocklist extends AppModel public function beforeValidate($options = array()) { parent::beforeValidate(); - $date = date('Y-m-d H:i:s'); if (empty($this->data['GalaxyClusterBlocklist']['id'])) { - $this->data['GalaxyClusterBlocklist']['date_created'] = $date; + $this->data['GalaxyClusterBlocklist']['date_created'] = date('Y-m-d H:i:s'); } if (empty($this->data['GalaxyClusterBlocklist']['comment'])) { $this->data['GalaxyClusterBlocklist']['comment'] = ''; @@ -45,12 +45,14 @@ class GalaxyClusterBlocklist extends AppModel return true; } + /** + * @param string $clusterUUID + * @return bool + */ public function checkIfBlocked($clusterUUID) { - $entry = $this->find('first', array('conditions' => array('cluster_uuid' => $clusterUUID))); - if (!empty($entry)) { - return true; - } - return false; + return $this->hasAny([ + 'cluster_uuid' => $clusterUUID, + ]); } } diff --git a/app/Model/GalaxyClusterRelation.php b/app/Model/GalaxyClusterRelation.php index 7fb182064..6f20fba88 100644 --- a/app/Model/GalaxyClusterRelation.php +++ b/app/Model/GalaxyClusterRelation.php @@ -223,7 +223,7 @@ class GalaxyClusterRelation extends AppModel } if (!$force) { $targetCluster = $this->TargetCluster->fetchIfAuthorized($user, $relation['GalaxyClusterRelation']['referenced_galaxy_cluster_uuid'], 'view', $throwErrors=false, $full=false); - if (isset($targetCluster['authorized']) && !$targetCluster['authorized']) { // do not save the relation if referenced cluster is not accessible by the user (or does not exists) + if (isset($targetCluster['authorized']) && !$targetCluster['authorized']) { // do not save the relation if referenced cluster is not accessible by the user (or does not exist) $errors[] = array(__('Invalid referenced galaxy cluster')); return $errors; } @@ -315,7 +315,7 @@ class GalaxyClusterRelation extends AppModel return $errors; } $targetCluster = $this->TargetCluster->fetchIfAuthorized($user, $relation['GalaxyClusterRelation']['referenced_galaxy_cluster_uuid'], 'view', $throwErrors=false, $full=false); - if (isset($targetCluster['authorized']) && !$targetCluster['authorized']) { // do not save the relation if referenced cluster is not accessible by the user (or does not exists) + if (isset($targetCluster['authorized']) && !$targetCluster['authorized']) { // do not save the relation if referenced cluster is not accessible by the user (or does not exist) $errors[] = array(__('Invalid referenced galaxy cluster')); return $errors; } diff --git a/app/Model/GalaxyElement.php b/app/Model/GalaxyElement.php index 58d60e4fc..b648ce605 100644 --- a/app/Model/GalaxyElement.php +++ b/app/Model/GalaxyElement.php @@ -1,5 +1,9 @@ data['Inbox']['uuid'] = CakeText::uuid(); $this->data['Inbox']['timestamp'] = time(); - $this->data['Inbox']['ip'] = $_SERVER['REMOTE_ADDR']; + $this->data['Inbox']['ip'] = $this->_remoteIp(); $this->data['Inbox']['user_agent'] = $_SERVER['HTTP_USER_AGENT']; $this->data['Inbox']['user_agent_sha256'] = hash('sha256', $_SERVER['HTTP_USER_AGENT']); return true; diff --git a/app/Model/Log.php b/app/Model/Log.php index 2d6b983f2..cd0130432 100644 --- a/app/Model/Log.php +++ b/app/Model/Log.php @@ -44,6 +44,7 @@ class Log extends AppModel 'export', 'fetchEvent', 'file_upload', + 'forgot', 'galaxy', 'include_formula', 'load_module', @@ -51,6 +52,7 @@ class Log extends AppModel 'login_fail', 'logout', 'merge', + 'password_reset', 'pruneUpdateLogs', 'publish', 'publish_sightings', @@ -109,6 +111,8 @@ class Log extends AppModel public $actsAs = ['LightPaginator']; + private $elasticSearchClient; + /** * Null when not defined, false when not enabled * @var Syslog|null|false @@ -121,10 +125,7 @@ class Log extends AppModel return false; } if (Configure::read('MISP.log_client_ip')) { - $ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR'; - if (isset($_SERVER[$ipHeader])) { - $this->data['Log']['ip'] = $_SERVER[$ipHeader]; - } + $this->data['Log']['ip'] = $this->_remoteIp(); } $setEmpty = array('title' => '', 'model' => '', 'model_id' => 0, 'action' => '', 'user_id' => 0, 'change' => '', 'email' => '', 'org' => '', 'description' => '', 'ip' => ''); foreach ($setEmpty as $field => $empty) { @@ -151,6 +152,22 @@ class Log extends AppModel return true; } + public function afterSave($created, $options = array()) + { + // run workflow if needed, but skip workflow for certain types, to prevent loops + if (!in_array($this->data['Log']['model'], ['Log', 'Workflow'])) { + $trigger_id = 'log-after-save'; + $workflowErrors = []; + $logging = [ + 'model' => 'Log', + 'action' => 'execute_workflow', + 'id' => $this->data['Log']['user_id'] + ]; + $this->executeTrigger($trigger_id, $this->data, $workflowErrors); + } + return true; + } + public function returnDates($org = 'all') { $conditions = array(); @@ -349,9 +366,8 @@ class Log extends AppModel public function logData($data) { - if (Configure::read('Plugin.ZeroMQ_enable') && Configure::read('Plugin.ZeroMQ_audit_notifications_enable')) { - $pubSubTool = $this->getPubSubTool(); - $pubSubTool->publish($data, 'audit', 'log'); + if ($this->pubToZmq('audit')) { + $this->getPubSubTool()->publish($data, 'audit', 'log'); } $this->publishKafkaNotification('audit', $data, 'log'); @@ -1147,4 +1163,15 @@ class Log extends AppModel break; } } + + private function getElasticSearchTool() + { + if (!$this->elasticSearchClient) { + App::uses('ElasticSearchClient', 'Tools'); + $client = new ElasticSearchClient(); + $client->initTool(); + $this->elasticSearchClient = $client; + } + return $this->elasticSearchClient; + } } diff --git a/app/Model/MispObject.php b/app/Model/MispObject.php index fa1df66cd..778b763e1 100644 --- a/app/Model/MispObject.php +++ b/app/Model/MispObject.php @@ -400,15 +400,7 @@ class MispObject extends AppModel $attributeArray = $object['Attribute']; } foreach ($attributeArray as $attribute) { - if ($attribute['type'] === 'malware-sample') { - if (strpos($attribute['value'], '|') === false && !empty($attribute['data'])) { - $attribute['value'] = $attribute['value'] . '|' . md5(base64_decode($attribute['data'])); - } - } - $attributeValueAfterModification = AttributeValidationTool::modifyBeforeValidation($attribute['type'], $attribute['value']); - $attributeValueAfterModification = $this->Attribute->runRegexp($attribute['type'], $attributeValueAfterModification); - - $newObjectAttributes[] = sha1($attribute['object_relation'] . $attribute['category'] . $attribute['type'] . $attributeValueAfterModification, true); + $newObjectAttributes[] = $this->getObjectAttributeHash($attribute); } $newObjectAttributeCount = count($newObjectAttributes); if (!empty($this->__objectDuplicationCheckCache['new'][$object['Object']['template_uuid']])) { @@ -433,7 +425,7 @@ class MispObject extends AppModel 'conditions' => array('Attribute.deleted' => 0) ) ), - 'fields' => array('template_uuid'), + 'fields' => array('template_uuid', 'uuid'), 'conditions' => array('template_uuid' => $object['Object']['template_uuid'], 'Object.deleted' => 0, 'event_id' => $eventId) )); } @@ -441,10 +433,12 @@ class MispObject extends AppModel $temp = array(); if (!empty($existingObject['Attribute']) && $newObjectAttributeCount === count($existingObject['Attribute'])) { foreach ($existingObject['Attribute'] as $existingAttribute) { - $temp[] = sha1($existingAttribute['object_relation'] . $existingAttribute['category'] . $existingAttribute['type'] . $existingAttribute['value'], true); + $temp[] = $this->getObjectAttributeHash($existingAttribute); } + if (empty(array_diff($temp, $newObjectAttributes))) { $duplicatedObjectId = $existingObject['Object']['id']; + $duplicateObjectUuid = $existingObject['Object']['uuid']; return true; } } @@ -452,7 +446,37 @@ class MispObject extends AppModel return false; } - public function saveObject(array $object, $eventId, $template = false, $user, $errorBehaviour = 'drop', $breakOnDuplicate = false) + /** + * @param array $object + * @param array $template + * @return array + */ + public function fillObjectDataFromTemplate(array $object, array $template) + { + $templateFields = array( + 'name' => 'name', + 'meta-category' => 'meta-category', + 'description' => 'description', + 'template_version' => 'version', + 'template_uuid' => 'uuid' + ); + foreach ($templateFields as $objectField => $templateField) { + $object['Object'][$objectField] = $template['ObjectTemplate'][$templateField]; + } + return $object; + } + + /** + * @param array $object + * @param int $eventId + * @param array $template + * @param array $user + * @param string $errorBehaviour + * @param bool $breakOnDuplicate + * @return array|array[]|bool|int|mixed|string + * @throws Exception + */ + public function saveObject(array $object, $eventId, $template = false, array $user, $errorBehaviour = 'drop', $breakOnDuplicate = false) { $templateFields = array( 'name' => 'name', @@ -562,7 +586,7 @@ class MispObject extends AppModel // conditions // order // group - public function fetchObjects($user, $options = array()) + public function fetchObjects(array $user, array $options = array()) { $attributeConditions = array(); if (!$user['Role']['perm_site_admin']) { @@ -608,8 +632,6 @@ class MispObject extends AppModel } if (isset($options['contain'])) { $params['contain'] = array_merge_recursive($params['contain'], $options['contain']); - } else { - $option['contain']['Event']['fields'] = array('id', 'info', 'org_id', 'orgc_id'); } if ( empty($options['metadata']) && @@ -662,7 +684,6 @@ class MispObject extends AppModel if ($options['enforceWarninglist'] && !isset($this->Warninglist)) { $this->Warninglist = ClassRegistry::init('Warninglist'); } - $results = array_values($results); $proposals_block_attributes = Configure::read('MISP.proposals_block_attributes'); if (empty($options['metadata'])) { foreach ($results as $key => $object) { @@ -764,7 +785,7 @@ class MispObject extends AppModel /** * Clean the attribute list up from artifacts introduced by the object form * @param array $attributes - * @return string|array + * @return array * @throws InternalErrorException * @throws Exception */ @@ -812,6 +833,70 @@ class MispObject extends AppModel return $attributes; } + /** + * @param array $user + * @param int $eventId + * @param array $attributes + * @param array $template + * @param int $threshold + * @return array + */ + public function findSimilarObjects(array $user, $eventId, array $attributes, array $template, $threshold = 15) + { + $attributeValues = array_column($attributes, 'value'); + $conditions = array( + 'event_id' => $eventId, + 'value1' => $attributeValues, + 'object_id !=' => 0, + ); + $similarObjects = $this->Attribute->find('all', array( + 'conditions' => $conditions, + 'recursive' => -1, + 'fields' => 'object_id, count(object_id) as similarity_amount', + 'group' => 'object_id', + 'order' => 'similarity_amount DESC' + )); + + if (empty($similarObjects)) { + return [0, [], [], []]; + } + + $similar_object_ids = array(); + $similar_object_similarity_amount = array(); + foreach ($similarObjects as $obj) { + $similar_object_ids[] = $obj['Attribute']['object_id']; + $similar_object_similarity_amount[$obj['Attribute']['object_id']] = (int)$obj[0]['similarity_amount']; + } + $similar_objects_count = count($similar_object_ids); + $similar_object_ids = array_slice($similar_object_ids, 0, $threshold); // slice to honor the threshold + $similar_objects = $this->fetchObjects($user, array( + 'conditions' => array( + 'Object.id' => $similar_object_ids, + 'Object.template_uuid' => $template['ObjectTemplate']['uuid'] + ) + )); + foreach ($similar_objects as $key => $obj) { + $similar_objects[$key]['Object']['similarity_amount'] = $similar_object_similarity_amount[$obj['Object']['id']]; // sorting function cannot use external variables + } + usort($similar_objects, function ($a, $b) { // fetch Object returns object sorted by IDs, force the sort by the similarity amount + if ($a['Object']['similarity_amount'] == $b['Object']['similarity_amount']) { + return 0; + } + return ($a['Object']['similarity_amount'] > $b['Object']['similarity_amount']) ? -1 : 1; + }); + + $simple_flattened_attribute = []; + $simple_flattened_attribute_noval = []; + foreach ($attributes as $k => $attribute) { + $curFlat = $attribute['object_relation'] . '.' . $attribute['type'] . '.' .$attribute['value']; + $simple_flattened_attribute[$curFlat] = $k; + $curFlatNoval = $attribute['object_relation'] . '.' . $attribute['type']; + $simple_flattened_attribute_noval[$curFlatNoval] = $k; + } + + return [$similar_objects_count, $similar_objects, $simple_flattened_attribute, $simple_flattened_attribute_noval]; + } + // Set Object's *-seen (and ObjectAttribute's *-seen and ObjectAttribute's value if requested) to the provided *-seen value // Therefore, synchronizing the 3 values public function syncObjectAndAttributeSeen($object, $forcedSeenOnElements, $applyOnAttribute=True) { @@ -851,6 +936,14 @@ class MispObject extends AppModel return $object; } + /** + * @param array $object + * @param array $objectToSave + * @param bool $onlyAddNewAttribute + * @param array $user + * @return array|int + * @throws JsonException + */ public function deltaMerge(array $object, array $objectToSave, $onlyAddNewAttribute=false, array $user) { if (!isset($objectToSave['Object'])) { @@ -965,7 +1058,6 @@ class MispObject extends AppModel } } else { // we only add the new attribute $newAttribute = $objectToSave['Attribute'][0]; - $this->Event->Attribute->create(); $newAttribute['event_id'] = $object['Object']['event_id']; $newAttribute['object_id'] = $object['Object']['id']; // Set seen of object at attribute level @@ -980,10 +1072,9 @@ class MispObject extends AppModel (!array_key_exists('last_seen', $object['Object']) && !is_null($object['Object']['last_seen'])) ) { $newAttribute['last_seen'] = $object['Object']['last_seen']; - $different = true; } $saveAttributeResult = $this->Attribute->saveAttributes(array($newAttribute), $user); - return $saveAttributeResult ? $this->id : $this->validationErrors; + return $saveAttributeResult ? $this->id : $this->Attribute->validationErrors; } return $this->id; } @@ -1232,51 +1323,14 @@ class MispObject extends AppModel 'Attribute.event_id' => $eventId, 'Attribute.object_id' => 0, ], + 'fields' => ['Attribute.type'], ]); if (empty($attributes)) { return array('templates' => array(), 'types' => array()); } - $attributeTypes = array(); - foreach ($attributes as $i => $attribute) { - $attributeTypes[$attribute['Attribute']['type']] = true; - $attributes[$i]['Attribute']['object_relation'] = $attribute['Attribute']['type']; - } - $attributeTypes = array_keys($attributeTypes); - $potentialTemplateIds = $this->ObjectTemplate->find('column', array( - 'recursive' => -1, - 'fields' => array( - 'ObjectTemplate.id', - ), - 'conditions' => array( - 'ObjectTemplate.active' => true, - 'ObjectTemplateElement.type' => $attributeTypes, - ), - 'joins' => array( - array( - 'table' => 'object_template_elements', - 'alias' => 'ObjectTemplateElement', - 'type' => 'RIGHT', - 'fields' => array('ObjectTemplateElement.object_relation', 'ObjectTemplateElement.type'), - 'conditions' => array('ObjectTemplate.id = ObjectTemplateElement.object_template_id') - ) - ), - 'group' => 'ObjectTemplate.id', - )); - - $templates = $this->ObjectTemplate->find('all', [ - 'recursive' => -1, - 'conditions' => ['id' => $potentialTemplateIds], - 'contain' => ['ObjectTemplateElement' => ['fields' => ['object_relation', 'type', 'multiple']]] - ]); - - foreach ($templates as $i => $template) { - $res = $this->ObjectTemplate->checkTemplateConformityBasedOnTypes($template, $attributes); - $templates[$i]['ObjectTemplate']['compatibility'] = $res['valid'] ? true : $res['missingTypes']; - $templates[$i]['ObjectTemplate']['invalidTypes'] = $res['invalidTypes']; - $templates[$i]['ObjectTemplate']['invalidTypesMultiple'] = $res['invalidTypesMultiple']; - } - return array('templates' => $templates, 'types' => $attributeTypes); + $attributeTypes = array_column(array_column($attributes, 'Attribute'), 'type'); + return $this->ObjectTemplate->fetchPossibleTemplatesBasedOnTypes($attributeTypes); } public function groupAttributesIntoObject(array $user, $event_id, array $object, $template, array $selected_attribute_ids, array $selected_object_relation_mapping, $hard_delete_attribute) @@ -1647,4 +1701,17 @@ class MispObject extends AppModel return $newValue != $originalValue; } } + + private function getObjectAttributeHash($attribute) + { + if ($attribute['type'] === 'malware-sample') { + if (strpos($attribute['value'], '|') === false && !empty($attribute['data'])) { + $attribute['value'] = $attribute['value'] . '|' . md5(base64_decode($attribute['data'])); + } + } + $attributeValueAfterModification = AttributeValidationTool::modifyBeforeValidation($attribute['type'], $attribute['value']); + $attributeValueAfterModification = $this->Attribute->runRegexp($attribute['type'], $attributeValueAfterModification); + + return sha1($attribute['object_relation'] . $attribute['category'] . $attribute['type'] . $attributeValueAfterModification, true); + } } diff --git a/app/Model/Module.php b/app/Model/Module.php index 325408ec2..cb99a10fd 100644 --- a/app/Model/Module.php +++ b/app/Model/Module.php @@ -6,7 +6,8 @@ class Module extends AppModel { public $useTable = false; - private $__validTypes = array( + // private + const VALID_TYPES = array( 'Enrichment' => array('hover', 'expansion'), 'Import' => array('import'), 'Export' => array('export'), @@ -14,6 +15,7 @@ class Module extends AppModel 'Cortex' => array('cortex') ); + // private const TYPE_TO_FAMILY = array( 'Import' => 'Import', 'Export' => 'Export', @@ -23,7 +25,7 @@ class Module extends AppModel 'Cortex' => 'Cortex' ); - public $configTypes = array( + const CONFIG_TYPES = array( 'IP' => array( 'validation' => 'validateIPField', 'field' => 'text', @@ -351,7 +353,7 @@ class Module extends AppModel $result = array(); if (is_array($modules)) { foreach ($modules as $module) { - if (array_intersect($this->__validTypes[$moduleFamily], $module['meta']['module-type'])) { + if (array_intersect(self::VALID_TYPES[$moduleFamily], $module['meta']['module-type'])) { $moduleSettings = [ [ 'name' => 'enabled', diff --git a/app/Model/ObjectTemplate.php b/app/Model/ObjectTemplate.php index f82b89681..0062d231b 100644 --- a/app/Model/ObjectTemplate.php +++ b/app/Model/ObjectTemplate.php @@ -207,11 +207,69 @@ class ObjectTemplate extends AppModel } /** - * @param array $template - * @param array $attributes + * @param array $attributeTypes Array of attribute types to check, can contains multiple types * @return array */ - public function checkTemplateConformityBasedOnTypes(array $template, array $attributes) + public function fetchPossibleTemplatesBasedOnTypes(array $attributeTypes) + { + $uniqueAttributeTypes = array_unique($attributeTypes, SORT_REGULAR); + $potentialTemplateIds = $this->find('column', array( + 'recursive' => -1, + 'fields' => array( + 'ObjectTemplate.id', + ), + 'conditions' => array( + 'ObjectTemplate.active' => true, + 'ObjectTemplateElement.type' => $uniqueAttributeTypes, + ), + 'joins' => array( + array( + 'table' => 'object_template_elements', + 'alias' => 'ObjectTemplateElement', + 'type' => 'RIGHT', + 'fields' => array('ObjectTemplateElement.object_relation', 'ObjectTemplateElement.type'), + 'conditions' => array('ObjectTemplate.id = ObjectTemplateElement.object_template_id') + ) + ), + 'group' => 'ObjectTemplate.id', + )); + + $templates = $this->find('all', [ + 'recursive' => -1, + 'conditions' => ['id' => $potentialTemplateIds], + 'contain' => ['ObjectTemplateElement' => ['fields' => ['object_relation', 'type', 'multiple']]] + ]); + + foreach ($templates as $i => $template) { + $res = $this->checkTemplateConformityBasedOnTypes($template, $attributeTypes); + $templates[$i]['ObjectTemplate']['compatibility'] = $res['valid'] ? true : $res['missingTypes']; + $templates[$i]['ObjectTemplate']['invalidTypes'] = $res['invalidTypes']; + $templates[$i]['ObjectTemplate']['invalidTypesMultiple'] = $res['invalidTypesMultiple']; + } + + usort($templates, function($a, $b) { + if ($a['ObjectTemplate']['id'] == $b['ObjectTemplate']['id']) { + return 0; + } else if (is_array($a['ObjectTemplate']['compatibility']) && is_array($b['ObjectTemplate']['compatibility'])) { + return count($a['ObjectTemplate']['compatibility']) > count($b['ObjectTemplate']['compatibility']) ? 1 : -1; + } else if (is_array($a['ObjectTemplate']['compatibility']) && !is_array($b['ObjectTemplate']['compatibility'])) { + return 1; + } else if (!is_array($a['ObjectTemplate']['compatibility']) && is_array($b['ObjectTemplate']['compatibility'])) { + return -1; + } else { // sort based on invalidTypes count + return count($a['ObjectTemplate']['invalidTypes']) > count($b['ObjectTemplate']['invalidTypes']) ? 1 : -1; + } + }); + + return array('templates' => $templates, 'types' => $uniqueAttributeTypes); + } + + /** + * @param array $template + * @param array $attributeTypes Array of attribute types to check, can contains multiple types + * @return array + */ + public function checkTemplateConformityBasedOnTypes(array $template, array $attributeTypes) { $to_return = array('valid' => true, 'missingTypes' => array()); if (!empty($template['ObjectTemplate']['requirements'])) { @@ -222,13 +280,7 @@ class ObjectTemplate extends AppModel if (!empty($template['ObjectTemplate']['requirements']['required'])) { foreach ($template['ObjectTemplate']['requirements']['required'] as $requiredField) { $requiredType = $elementsByObjectRelationName[$requiredField]['type']; - $found = false; - foreach ($attributes as $attribute) { - if ($attribute['Attribute']['type'] === $requiredType) { - $found = true; - break; - } - } + $found = in_array($requiredType, $attributeTypes, true); if (!$found) { $to_return = array('valid' => false, 'missingTypes' => array($requiredType)); } @@ -241,11 +293,8 @@ class ObjectTemplate extends AppModel foreach ($template['ObjectTemplate']['requirements']['requiredOneOf'] as $requiredField) { $requiredType = $elementsByObjectRelationName[$requiredField]['type'] ?? null; $allRequiredTypes[] = $requiredType; - foreach ($attributes as $attribute) { - if ($attribute['Attribute']['type'] === $requiredType) { - $found = true; - break; - } + if (!$found) { + $found = in_array($requiredType, $attributeTypes, true); } } if (!$found) { @@ -262,17 +311,17 @@ class ObjectTemplate extends AppModel $valid_types[$templateElement['type']] = $templateElement['multiple']; } $check_for_multiple_type = array(); - foreach ($attributes as $attribute) { - if (isset($valid_types[$attribute['Attribute']['type']])) { - if (!$valid_types[$attribute['Attribute']['type']]) { // is not multiple - if (isset($check_for_multiple_type[$attribute['Attribute']['type']])) { - $to_return['invalidTypesMultiple'][] = $attribute['Attribute']['type']; + foreach ($attributeTypes as $attributeType) { + if (isset($valid_types[$attributeType])) { + if (!$valid_types[$attributeType]) { // is not multiple + if (isset($check_for_multiple_type[$attributeType])) { + $to_return['invalidTypesMultiple'][] = $attributeType; } else { - $check_for_multiple_type[$attribute['Attribute']['type']] = 1; + $check_for_multiple_type[$attributeType] = 1; } } } else { - $to_return['invalidTypes'][] = $attribute['Attribute']['type']; + $to_return['invalidTypes'][] = $attributeType; } } $to_return['invalidTypes'] = array_unique($to_return['invalidTypes'], SORT_REGULAR); diff --git a/app/Model/OrgBlocklist.php b/app/Model/OrgBlocklist.php index 531917390..ba69e1940 100644 --- a/app/Model/OrgBlocklist.php +++ b/app/Model/OrgBlocklist.php @@ -43,6 +43,24 @@ class OrgBlocklist extends AppModel return true; } + public function afterDelete() + { + parent::afterDelete(); + if (!empty($this->data['OrgBlocklist']['org_uuid'])) { + $this->cleanupBlockedCount($this->data['OrgBlocklist']['org_uuid']); + } + } + + public function afterFind($results, $primary = false) + { + foreach ($results as $k => $result) { + if (isset($result['OrgBlocklist']['org_uuid'])) { + $results[$k]['OrgBlocklist']['blocked_data'] = $this->getBlockedData($result['OrgBlocklist']['org_uuid']); + } + } + return $results; + } + /** * @param array $eventArray */ @@ -74,16 +92,7 @@ class OrgBlocklist extends AppModel } if (is_numeric($orgIdOrUuid)) { - $this->Organisation = ClassRegistry::init('Organisation'); - $orgUuid = $this->Organisation->find('first', [ - 'conditions' => ['Organisation.id' => $orgIdOrUuid], - 'fields' => ['Organisation.uuid'], - 'recursive' => -1, - ]); - if (empty($orgUuid)) { - return false; // org not found by ID, so it is not blocked - } - $orgUuid = $orgUuid['Organisation']['uuid']; + $orgUuid = $this->getUUIDFromID($orgIdOrUuid); } else { $orgUuid = $orgIdOrUuid; } @@ -92,4 +101,67 @@ class OrgBlocklist extends AppModel $this->blockedCache[$orgIdOrUuid] = $isBlocked; return $isBlocked; } + + private function getUUIDFromID($orgID) + { + $this->Organisation = ClassRegistry::init('Organisation'); + $orgUuid = $this->Organisation->find('first', [ + 'conditions' => ['Organisation.id' => $orgID], + 'fields' => ['Organisation.uuid'], + 'recursive' => -1, + ]); + if (empty($orgUuid)) { + return false; // org not found by ID, so it is not blocked + } + $orgUuid = $orgUuid['Organisation']['uuid']; + return $orgUuid; + } + + public function saveEventBlocked($orgIdOrUUID) + { + if (is_numeric($orgIdOrUUID)) { + $orgcUUID = $this->getUUIDFromID($orgIdOrUUID); + } else { + $orgcUUID = $orgIdOrUUID; + } + $lastBlockTime = time(); + $redisKeyBlockAmount = "misp:blocklist_blocked_amount:{$orgcUUID}"; + $redisKeyBlockLastTime = "misp:blocklist_blocked_last_time:{$orgcUUID}"; + $redis = RedisTool::init(); + if ($redis !== false) { + $pipe = $redis->multi(Redis::PIPELINE) + ->incr($redisKeyBlockAmount) + ->set($redisKeyBlockLastTime, $lastBlockTime); + $pipe->exec(); + } + } + + private function cleanupBlockedCount($orgcUUID) + { + $redisKeyBlockAmount = "misp:blocklist_blocked_amount:{$orgcUUID}"; + $redisKeyBlockLastTime = "misp:blocklist_blocked_last_time:{$orgcUUID}"; + $redis = RedisTool::init(); + if ($redis !== false) { + $pipe = $redis->multi(Redis::PIPELINE) + ->del($redisKeyBlockAmount) + ->del($redisKeyBlockLastTime); + $pipe->exec(); + } + } + + public function getBlockedData($orgcUUID) + { + $redisKeyBlockAmount = "misp:blocklist_blocked_amount:{$orgcUUID}"; + $redisKeyBlockLastTime = "misp:blocklist_blocked_last_time:{$orgcUUID}"; + $blockData = [ + 'blocked_amount' => false, + 'blocked_last_time' => false, + ]; + $redis = RedisTool::init(); + if ($redis !== false) { + $blockData['blocked_amount'] = $redis->get($redisKeyBlockAmount); + $blockData['blocked_last_time'] = $redis->get($redisKeyBlockLastTime); + } + return $blockData; + } } diff --git a/app/Model/OverCorrelatingValue.php b/app/Model/OverCorrelatingValue.php index 8dd898a95..e252540f1 100644 --- a/app/Model/OverCorrelatingValue.php +++ b/app/Model/OverCorrelatingValue.php @@ -10,6 +10,7 @@ class OverCorrelatingValue extends AppModel public static function truncate(string $value): string { + $value = mb_strtolower($value); return mb_substr($value, 0, 191); } @@ -45,11 +46,15 @@ class OverCorrelatingValue extends AppModel if (!$this->isBlocked($value)) { $value = self::truncate($value); $this->create(); - $this->save([ - 'value' => $value, - 'occurrence' => 0 - ]); - $this->blockedValues[$value] = true; + try { + $this->save([ + 'value' => mb_strtolower($value), + 'occurrence' => 0 + ]); + $this->blockedValues[$value] = true; + } catch (Exception $e) { + //most likely we ran into an issue with capitalisation, there's no reason to break the process for this + } } } diff --git a/app/Model/Role.php b/app/Model/Role.php index d75345067..b4274e33b 100644 --- a/app/Model/Role.php +++ b/app/Model/Role.php @@ -14,7 +14,7 @@ class Role extends AppModel 'name' => array( 'unique' => array( 'rule' => 'isUnique', - 'message' => 'A role with this name already exists.' // TODO i18n? + 'message' => 'A role with this name already exists.' ), 'valueNotEmpty' => array( 'rule' => array('valueNotEmpty'), @@ -324,7 +324,13 @@ class Role extends AppModel 'text' => 'Warninglist Editor', 'readonlyenabled' => false, 'title' => __('Allow to manage warninglists.'), - ) + ), + 'perm_view_feed_correlations' => array( + 'id' => 'RolePermViewFeedCorrelations', + 'text' => 'View Feed Correlations', + 'readonlyenabled' => true, + 'title' => __('Allow the viewing of feed correlations. Enabling this can come at a performance cost.'), + ), ); } } diff --git a/app/Model/Server.php b/app/Model/Server.php index 0d756b991..2c36b9b02 100644 --- a/app/Model/Server.php +++ b/app/Model/Server.php @@ -746,13 +746,13 @@ class Server extends AppModel } /** - * getElligibleClusterIdsFromServerForPull Get a list of cluster IDs that are present on the remote server and returns clusters that should be pulled + * Get a list of cluster IDs that are present on the remote server and returns clusters that should be pulled * * @param ServerSyncTool $serverSync * @param bool $onlyUpdateLocalCluster If set to true, only cluster present locally will be returned * @param array $eligibleClusters Array of cluster present locally that could potentially be updated. Linked to $onlyUpdateLocalCluster * @param array $conditions Conditions to be sent to the remote server while fetching accessible clusters IDs - * @return array List of cluster IDs to be pulled + * @return array List of cluster UUIDs to be pulled * @throws HttpSocketHttpException * @throws HttpSocketJsonException * @throws JsonException @@ -760,25 +760,47 @@ class Server extends AppModel public function getElligibleClusterIdsFromServerForPull(ServerSyncTool $serverSync, $onlyUpdateLocalCluster=true, array $eligibleClusters=array(), array $conditions=array()) { $this->log("Fetching eligible clusters from server #{$serverSync->serverId()} for pull: " . JsonTool::encode($conditions), LOG_INFO); + + if ($onlyUpdateLocalCluster && empty($eligibleClusters)) { + return []; // no clusters for update + } + $clusterArray = $this->fetchCustomClusterIdsFromServer($serverSync, $conditions=$conditions); if (empty($clusterArray)) { - return []; + return []; // empty remote clusters } + + /** @var GalaxyClusterBlocklist $GalaxyClusterBlocklist */ + $GalaxyClusterBlocklist = ClassRegistry::init('GalaxyClusterBlocklist'); + + if (!$onlyUpdateLocalCluster) { + /** @var GalaxyCluster $GalaxyCluster */ + $GalaxyCluster = ClassRegistry::init('GalaxyCluster'); + // Do not fetch clusters with the same or newer version that already exists on local instance + $eligibleClusters = $GalaxyCluster->find('list', [ + 'conditions' => ['GalaxyCluster.uuid' => array_column(array_column($clusterArray, 'GalaxyCluster'), 'uuid')], + 'fields' => ['GalaxyCluster.uuid', 'GalaxyCluster.version'], + ]); + } + + $clustersForPull = []; foreach ($clusterArray as $cluster) { - if (isset($eligibleClusters[$cluster['GalaxyCluster']['uuid']])) { - $localVersion = $eligibleClusters[$cluster['GalaxyCluster']['uuid']]; - if ($localVersion >= $cluster['GalaxyCluster']['version']) { - unset($eligibleClusters[$cluster['GalaxyCluster']['uuid']]); - } - } else { - if ($onlyUpdateLocalCluster) { - unset($eligibleClusters[$cluster['GalaxyCluster']['uuid']]); - } else { - $eligibleClusters[$cluster['GalaxyCluster']['uuid']] = true; + $clusterUuid = $cluster['GalaxyCluster']['uuid']; + + if ($GalaxyClusterBlocklist->checkIfBlocked($clusterUuid)) { + continue; // skip blocked clusters + } + + if (isset($eligibleClusters[$clusterUuid])) { + $localVersion = $eligibleClusters[$clusterUuid]; + if ($localVersion < $cluster['GalaxyCluster']['version']) { + $clustersForPull[] = $clusterUuid; } + } elseif (!$onlyUpdateLocalCluster) { + $clustersForPull[] = $clusterUuid; } } - return array_keys($eligibleClusters); + return $clustersForPull; } /** @@ -2132,7 +2154,7 @@ class Server extends AppModel return true; } - public function otpBeforeHook($setting, $value) + public function email_otpBeforeHook($setting, $value) { if ($value && !empty(Configure::read('MISP.disable_emailing'))) { return __('Emailing is currently disabled. Enabling OTP without e-mailing being configured would lock all users out.'); @@ -2140,6 +2162,17 @@ class Server extends AppModel return true; } + public function otpBeforeHook($setting, $value) + { + if ($value && (!class_exists('\OTPHP\TOTP') || !class_exists('\BaconQrCode\Writer'))) { + return __('The TOTP and QR code generation libraries are not installed. Enabling OTP without those libraries installed would lock all users out.'); + } + if ($value && Configure::read('LinOTPAuth.enabled')) { + return __('The TOTP and LinOTPAuth should not be used at the same time.'); + } + return true; + } + public function testForRPZSerial($value) { if ($this->testForEmpty($value) !== true) { @@ -3249,7 +3282,7 @@ class Server extends AppModel $indexDiff = array(); foreach ($expectedIndex as $tableName => $indexes) { if (!array_key_exists($tableName, $actualIndex)) { - continue; // If table does not exists, it is covered by the schema diagnostic + continue; // If table does not exist, it is covered by the schema diagnostic } $tableIndexDiff = array_diff(array_keys($indexes), array_keys($actualIndex[$tableName])); // check for missing indexes foreach ($tableIndexDiff as $columnDiff) { @@ -3996,21 +4029,29 @@ class Server extends AppModel try { $composer = FileAccessTool::readJsonFromFile(APP . DS . 'composer.json'); $extensions = []; + $dependencies = []; foreach ($composer['require'] as $require => $foo) { if (substr($require, 0, 4) === 'ext-') { $extensions[substr($require, 4)] = true; } + else if (mb_strpos($require, '/') !== false) { // external dependencies have namespaces, so a / + $dependencies[$require] = true; + } } foreach ($composer['suggest'] as $suggest => $reason) { if (substr($suggest, 0, 4) === 'ext-') { $extensions[substr($suggest, 4)] = $reason; } + else if (mb_strpos($suggest, '/') !== false) { // external dependencies have namespaces, so a / + $dependencies[$suggest] = $reason; + } } } catch (Exception $e) { $this->logException('Could not load extensions from composer.json', $e, LOG_NOTICE); $extensions = ['redis' => '', 'gd' => '', 'ssdeep' => '', 'zip' => '', 'intl' => '']; // Default extensions } + // check PHP extensions $results = ['cli' => false]; foreach ($extensions as $extension => $reason) { $results['extensions'][$extension] = [ @@ -4022,9 +4063,9 @@ class Server extends AppModel 'info' => $reason === true ? null : $reason, ]; } - if (is_readable(APP . '/files/scripts/selftest.php')) { + if (is_readable(APP . DS . 'files' . DS . 'scripts' . DS . 'selftest.php')) { try { - $execResult = ProcessTool::execute(['php', APP . '/files/scripts/selftest.php', json_encode(array_keys($extensions))]); + $execResult = ProcessTool::execute(['php', APP . DS . 'files' . DS . 'scripts' . DS . 'selftest.php', json_encode(array_keys($extensions))]); } catch (Exception $e) { // pass } @@ -4037,6 +4078,7 @@ class Server extends AppModel } } + // version check $minimalVersions = [ 'redis' => '2.2.8', // because of sAddArray method ]; @@ -4052,6 +4094,24 @@ class Server extends AppModel } } } + + // check PHP dependencies, installed in the Vendor directory, just check presence of the folder + if (class_exists('\Composer\InstalledVersions')) { + foreach ($dependencies as $dependency => $reason) { + try { + $version = \Composer\InstalledVersions::getVersion($dependency); + } catch (Exception $e) { + $version = false; + } + $results['dependencies'][$dependency] = [ + 'version' => $version, + 'version_outdated' => false, + 'required' => $reason === true, + 'info' => $reason === true ? null : $reason, + ]; + } + } + return $results; } @@ -4075,7 +4135,11 @@ class Server extends AppModel $current = implode('.', $version_array); $upToDate = version_compare($current, substr($newest, 1)); - if ($upToDate === 0) { + if ($newest === null && (Configure::read('MISP.online_version_check') || !Configure::check('MISP.online_version_check'))) { + $upToDate = 'error'; + } elseif ($newest === null && (!Configure::read('MISP.online_version_check') && Configure::check('MISP.online_version_check'))) { + $upToDate = 'disabled'; + } elseif ($upToDate === 0) { $upToDate = 'same'; } else { $upToDate = $upToDate === -1 ? 'older' : 'newer'; @@ -4111,11 +4175,15 @@ class Server extends AppModel */ public function getCurrentGitStatus($checkVersion = false) { - $HttpSocket = $this->setupHttpSocket(null, null, 3); - try { - $latestCommit = GitTool::getLatestCommit($HttpSocket); - } catch (Exception $e) { - $latestCommit = false; + $latestCommit = false; + + if (Configure::read('MISP.online_version_check') || !Configure::check('MISP.online_version_check')) { + $HttpSocket = $this->setupHttpSocket(null, null, 3); + try { + $latestCommit = GitTool::getLatestCommit($HttpSocket); + } catch (Exception $e) { + $latestCommit = false; + } } $output = [ @@ -4124,7 +4192,7 @@ class Server extends AppModel 'latestCommit' => $latestCommit, ]; if ($checkVersion) { - $output['version'] = $latestCommit ? $this->checkRemoteVersion($HttpSocket) : false; + $output['version'] = $latestCommit ? $this->checkRemoteVersion($HttpSocket) : $this->checkVersion(null); } return $output; } @@ -5539,7 +5607,7 @@ class Server extends AppModel ), 'log_client_ip_header' => array( 'level' => 1, - 'description' => __('If log_client_ip is enabled, you can customize which header field contains the client\'s IP address. This is generally used when you have a reverse proxy infront of your MISP instance.'), + 'description' => __('If log_client_ip is enabled, you can customize which header field contains the client\'s IP address. This is generally used when you have a reverse proxy in front of your MISP instance. Prepend the variable with "HTTP_", for example "HTTP_X_FORWARDED_FOR".'), 'value' => 'REMOTE_ADDR', 'test' => 'testForEmpty', 'type' => 'string', @@ -5568,6 +5636,15 @@ class Server extends AppModel 'type' => 'boolean', 'null' => true ), + 'log_skip_access_logs_in_application_logs' => [ + 'level' => 0, + 'description' => __('Skip adding the access log entries to the /logs/ application logs. This is **HIGHLY** recommended as your instance will be logging these entries twice otherwise, however, for compatibility reasons for auditing we maintain this behaviour until confirmed otherwise.'), + 'value' => false, + 'errorMessage' => __('Access logs are logged twice. This is generally not recommended, make sure you update your tooling.'), + 'test' => 'testBoolTrue', + 'type' => 'boolean', + 'null' => true + ], 'log_paranoid' => array( 'level' => 0, 'description' => __('If this functionality is enabled all page requests will be logged. Keep in mind this is extremely verbose and will become a burden to your database.'), @@ -5576,9 +5653,17 @@ class Server extends AppModel 'type' => 'boolean', 'null' => true ), + 'log_paranoid_api' => array( + 'level' => 0, + 'description' => __('If this functionality is enabled all API requests will be logged.'), + 'value' => false, + 'test' => 'testBoolFalse', + 'type' => 'boolean', + 'null' => true + ), 'log_paranoid_skip_db' => array( 'level' => 0, - 'description' => __('You can decide to skip the logging of the paranoid logs to the database.'), + 'description' => __('You can decide to skip the logging of the paranoid logs to the database. Logs will be just published to ZMQ or Kafka.'), 'value' => false, 'test' => 'testParanoidSkipDb', 'type' => 'boolean', @@ -5592,6 +5677,14 @@ class Server extends AppModel 'type' => 'boolean', 'null' => true ), + 'log_paranoid_include_sql_queries' => [ + 'level' => 0, + 'description' => __('If paranoid logging is enabled, include the SQL queries in the entries.'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true + ], 'log_user_ips' => array( 'level' => 0, 'description' => __('Log user IPs on each request. 30 day retention for lookups by IP to get the last authenticated user ID for the given IP, whilst on the reverse, indefinitely stores all associated IPs for a user ID.'), @@ -5632,6 +5725,14 @@ class Server extends AppModel 'type' => 'boolean', 'null' => true ), + 'discussion_disable' => [ + 'level' => 1, + 'description' => __('Completely disable ability for user to add discussion to events.'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true + ], 'showCorrelationsOnIndex' => array( 'level' => 1, 'description' => __('When enabled, the number of correlations visible to the currently logged in user will be visible on the event index UI. This comes at a performance cost but can be very useful to see correlating events at a glance.'), @@ -6015,12 +6116,30 @@ class Server extends AppModel ], 'thumbnail_in_redis' => [ 'level' => self::SETTING_OPTIONAL, - 'description' => __('Store image thumbnails in Redis insteadof file system.'), + 'description' => __('Store image thumbnails in Redis instead of file system.'), 'value' => false, 'test' => 'testBool', 'type' => 'boolean', 'null' => true, ], + 'self_update' => [ + 'level' => self::SETTING_CRITICAL, + 'description' => __('Enable the GUI button for MISP self-update on the Diagnostics page.'), + 'value' => true, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true, + 'cli_only' => true, + ], + 'online_version_check' => [ + 'level' => self::SETTING_CRITICAL, + 'description' => __('Enable the online MISP version check when loading the Diagnostics page.'), + 'value' => true, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true, + 'cli_only' => true, + ], ), 'GnuPG' => array( 'branch' => 1, @@ -6280,6 +6399,14 @@ class Server extends AppModel 'type' => 'boolean', 'null' => true ), + 'mandate_ip_allowlist_advanced_authkeys' => array( + 'level' => 2, + 'description' => __('If enabled, setting an ip allowlist will be mandatory when adding or editing an advanced authkey.'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true + ), 'disable_browser_cache' => array( 'level' => 0, 'description' => __('If enabled, HTTP headers that block browser cache will be send. Static files (like images or JavaScripts) will still be cached, but not generated pages.'), @@ -6304,12 +6431,29 @@ class Server extends AppModel 'type' => 'boolean', 'null' => true, ], + 'otp_required' => array( + 'level' => 2, + 'description' => __('Require authentication with OTP. Users that do not have (T/H)OTP configured will be forced to create a token at first login. You cannot use it in combination with external authentication plugins.'), + 'value' => false, + 'test' => 'testBool', + 'beforeHook' => 'otpBeforeHook', + 'type' => 'boolean', + 'null' => true + ), + 'otp_issuer' => array( + 'level' => 2, + 'description' => __('If OTP is enabled, set the issuer string to an arbitrary value. Otherwise, MISP will default to "[MISP.org] MISP".'), + 'value' => false, + 'test' => 'testForEmpty', + 'type' => 'string', + 'null' => true + ), 'email_otp_enabled' => array( 'level' => 2, 'description' => __('Enable two step authentication with a OTP sent by email. Requires e-mailing to be enabled. Warning: You cannot use it in combination with external authentication plugins.'), 'value' => false, 'test' => 'testBool', - 'beforeHook' => 'otpBeforeHook', + 'beforeHook' => 'email_otpBeforeHook', 'type' => 'boolean', 'null' => true ), @@ -6355,6 +6499,14 @@ class Server extends AppModel 'type' => 'boolean', 'null' => true ), + 'allow_password_forgotten' => array( + 'level' => 1, + 'description' => __('Enabling this setting will allow users to request automated password reset tokens via mail and initiate a reset themselves. Users with no encryption keys will not be able to use this feature.'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true + ), 'self_registration_message' => array( 'level' => 1, 'bigField' => true, @@ -6515,7 +6667,24 @@ class Server extends AppModel 'test' => 'testBool', 'type' => 'boolean', 'null' => true - ] + ], + 'disable_instance_file_uploads' => [ + 'level' => self::SETTING_RECOMMENDED, + 'description' => __('When enabled, the "Manage files" menu is disabled on the server settings. You can still copy files via ssh to the appropriate location and link them using MISP.settings.'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true, + 'cli_only' => true + ], + 'disclose_user_emails' => array( + 'level' => 0, + 'description' => __('Enable this setting to allow for the user e-mail addresses to be shown to non site-admin users. Keep in mind that in broad communities this can be abused.'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean', + 'null' => true + ), ), 'SecureAuth' => array( 'branch' => 1, @@ -7124,6 +7293,13 @@ class Server extends AppModel 'test' => 'testBool', 'type' => 'boolean' ), + 'Sightings_enable_realtime_publish' => array( + 'level' => 1, + 'description' => __('By default, sightings will not be immediately pushed to connected instances, as this can have a heavy impact on the performance of sighting attributes. Enable realtime publishing to trigger the publishing of sightings immediately as they are added.'), + 'value' => false, + 'test' => 'testBool', + 'type' => 'boolean' + ), 'CustomAuth_enable' => array( 'level' => 2, 'description' => __('Enable this functionality if you would like to handle the authentication via an external tool and authenticate with MISP using a custom header.'), diff --git a/app/Model/ShadowAttribute.php b/app/Model/ShadowAttribute.php index 3c9e1ad2a..a6f4478ec 100644 --- a/app/Model/ShadowAttribute.php +++ b/app/Model/ShadowAttribute.php @@ -17,7 +17,7 @@ class ShadowAttribute extends AppModel { public $combinedKeys = array('event_id', 'category', 'type'); - public $name = 'ShadowAttribute'; // TODO general + public $name = 'ShadowAttribute'; public $recursive = -1; diff --git a/app/Model/Sighting.php b/app/Model/Sighting.php index 67907925d..2562b404d 100644 --- a/app/Model/Sighting.php +++ b/app/Model/Sighting.php @@ -220,18 +220,7 @@ class Sighting extends AppModel return []; } - // Create conditions for fetching just sightings that user can see according to sightings policy - $conditions = $this->createConditionsByAttributes($user, $attributes); - if (empty($conditions)) { - return []; - } - - $conditions['Sighting.id'] = $ids; - $sightings = $this->find('all', [ - 'recursive' => -1, - 'conditions' => $conditions, - 'order' => 'Sighting.id', - ]); + $sightings = $this->filterSightingsByAttributeACL($user, $attributes, $ids); if (empty($sightings)) { return []; @@ -369,6 +358,51 @@ class Sighting extends AppModel return $this->generateStatistics($groupedSightings, $csvWithFalsePositive); } + /** + * @param array $user + * @param array $attributes Attributes with `Attribute.id`, `Event.id` and `Event.org_id` fields + * @param array $ids + * @return array + */ + private function filterSightingsByAttributeACL(array $user, array $attributes, array $ids) + { + $sightingsPolicy = $this->sightingsPolicy(); + $attributesKeyed = []; + $hostOrgId = Configure::read('MISP.host_org_id'); + $userOrgId = $user['org_id']; + foreach ($attributes as $attribute) { + $attributesKeyed[$attribute['Attribute']['id']] = $attribute; + } + unset($attributes); + $sightings = $this->find('all', [ + 'recursive' => -1, + 'conditions' => [ + 'Sighting.id' => $ids + ], + 'order' => 'Sighting.id' + ]); + foreach ($sightings as $k => $sighting) { + $attribute = $attributesKeyed[$sighting['Sighting']['attribute_id']]; + $ownEvent = $attribute['Event']['org_id'] == $userOrgId; + if (!$ownEvent) { + if ($sightingsPolicy === self::SIGHTING_POLICY_EVENT_OWNER) { + if ($sighting['Sighting']['org_id'] != $userOrgId) { + unset($sightings[$k]); + } + } else if ($sightingsPolicy === self::SIGHTING_POLICY_SIGHTING_REPORTER) { + if (!$this->isreporter($attribute['Event']['id'], $userOrgId)) { + unset($sightings[$k]); + } + } else if ($sightingsPolicy === self::SIGHTING_POLICY_HOST_ORG) { + if (!in_array($sighting['Sighting']['org_id'], [$userOrgId, $hostOrgId])) { + unset($sightings[$k]); + } + } + } + } + return array_values($sightings); + } + /** * @param array $user * @param array $attributes Attributes with `Attribute.id`, `Event.id` and `Event.org_id` fields @@ -752,7 +786,7 @@ class Sighting extends AppModel foreach ($values as $value) { foreach (array('value1', 'value2') as $field) { $conditions['OR'][] = array( - 'LOWER(Attribute.' . $field . ') LIKE' => strtolower($value) + 'Attribute.' . $field => $value ); } } @@ -1357,10 +1391,7 @@ class Sighting extends AppModel $sightingsToSave = []; foreach ($sightings as $sighting) { $sighting = $sighting['Sighting']; - $attributeUuid = $sighting['Attribute']['uuid']; - $eventUuid = $sighting['Event']['uuid']; - unset($sighting['Event'], $sighting['Attribute']); - $sighting['attribute_uuid'] = $attributeUuid; + $eventUuid = $sighting['event_uuid']; $sightingsToSave[$eventUuid][] = $sighting; } diff --git a/app/Model/SystemSetting.php b/app/Model/SystemSetting.php index bf98c49f0..d30eefcd4 100644 --- a/app/Model/SystemSetting.php +++ b/app/Model/SystemSetting.php @@ -21,6 +21,8 @@ class SystemSetting extends AppModel 'MISP.tmpdir', 'MISP.system_setting_db', 'MISP.attachments_dir', + 'MISP.self_update', + 'MISP.online_version_check', ]; // Allow to set config values just for these categories diff --git a/app/Model/Tag.php b/app/Model/Tag.php index d3c67fe9a..25388043a 100644 --- a/app/Model/Tag.php +++ b/app/Model/Tag.php @@ -459,7 +459,7 @@ class Tag extends AppModel $tags_temp = $this->find('all', $tag_params); $tags = array(); foreach ($tags_temp as $temp) { - $tags[strtoupper($temp['Tag']['name'])] = $temp; + $tags[mb_strtolower($temp['Tag']['name'])] = $temp; } return $tags; } diff --git a/app/Model/TaxiiServer.php b/app/Model/TaxiiServer.php new file mode 100644 index 000000000..343619965 --- /dev/null +++ b/app/Model/TaxiiServer.php @@ -0,0 +1,251 @@ + [ + 'roleModel' => 'Role', + 'roleKey' => 'role_id', + 'change' => 'full' + ], + 'Containable' + ]; + + private $Job = null; + private $Event = null; + private $Allowedlist = null; + + public function beforeValidate($options = array()) + { + parent::beforeValidate(); + if (empty($this->id) && empty($this->data['TaxiiServer']['uuid'])) { + $this->data['TaxiiServer']['uuid'] = CakeText::uuid(); + } + return true; + } + + public function pushRouter($id, $user) + { + if (Configure::read('MISP.background_jobs')) { + /** @var Job $job */ + $job = ClassRegistry::init('Job'); + $jobId = $job->createJob($user, Job::WORKER_DEFAULT, 'push_taxii', "Taxii Server ID: $id", 'Pushing.'); + + return $this->getBackgroundJobsTool()->enqueue( + BackgroundJobsTool::DEFAULT_QUEUE, + BackgroundJobsTool::CMD_SERVER, + [ + 'push_taxii', + $user['id'], + $id, + $jobId + ], + true, + $jobId + ); + } + + return $this->push($id, $user); + } + + public function push($id, $user, $jobId = null) + { + $this->Event = ClassRegistry::init('Event'); + $this->Job = ClassRegistry::init('Job'); + $taxii_server = $this->find('first', [ + 'recursive' => -1, + 'conditions' => ['TaxiiServer.id' => $id] + ]); + $filters = $this->__setPushFilters($taxii_server); + $elementCounter = 0; + $eventid = $this->Event->filterEventIds($user, $filters, $elementCounter); + $eventCount = count($eventid); + + $attribute_coefficient = Configure::check('MISP.default_attribute_memory_coefficient') ? Configure::read('MISP.default_attribute_memory_coefficient') : 80; + + $exportTool = ['memory_scaling_factor' => $attribute_coefficient]; + $eventids_chunked = $this->Event->clusterEventIds($exportTool, $eventid); + $i = 1; + $this->Allowedlist = ClassRegistry::init('Allowedlist'); + foreach ($eventids_chunked as $eventids) { + $this->__pushEvents($user, $taxii_server, $filters, $eventids, $i, $jobId, $eventCount); + } + unset($eventid); + return true; + } + + private function __setPushFilters($taxii_server) + { + $filters = empty($taxii_server['TaxiiServer']['filters']) ? [] : json_decode($taxii_server['TaxiiServer']['filters'], true); + $filters['include_attribute_count'] = 1; + return $filters; + } + + private function __pushEvents($user, $taxii_server, $filters, $eventids, &$i, $jobId = null, $eventCount) + { + $filters['eventid'] = $eventids; + if (!empty($filters['tags']['NOT'])) { + $filters['blockedAttributeTags'] = $filters['tags']['NOT']; + unset($filters['tags']['NOT']); + } + $result = $this->Event->fetchEvent($user, $filters, true); + + $result = $this->Allowedlist->removeAllowedlistedFromArray($result, false); + $temporaryFolder = $this->temporaryFolder(); + $temporaryFolderPath = $temporaryFolder['dir']->path; + $this->Job->id = $jobId; + foreach ($result as $event) { + $temporaryFile = $this->temporaryFile($temporaryFolderPath); + $temporaryFile->write( + JsonTool::encode( + JSONConverterTool::convert($event, false, true) + ) + ); + $temporaryFile->close(); + if ($jobId && $i % 10 == 0) { + $this->Job->saveField('progress', intval((100 * $i) / $eventCount)); + $this->Job->saveField('message', 'Pushing Event ' . $i . '/' . $eventCount . '.'); + } + $i++; + } + // execute python script here!!! + $scriptFile = APP . 'files/scripts/taxii/taxii_push.py'; + $command = [ + ProcessTool::pythonBin(), + $scriptFile, + '--dir', $temporaryFolder['dir']->path, + '--baseurl', $taxii_server['TaxiiServer']['baseurl'], + '--api_root', $taxii_server['TaxiiServer']['api_root'], + '--key', $taxii_server['TaxiiServer']['api_key'], + '--collection', $taxii_server['TaxiiServer']['collection'] + ]; + $result = ProcessTool::execute($command, null, true); + $temporaryFolder['dir']->delete(); + if ($jobId) { + $this->Job->saveField('progress', 100); + $this->Job->saveField('message', 'Done, pushed ' . $i . ' events to TAXII server.'); + } + } + + private function temporaryFolder() + { + $tmpDir = Configure::check('MISP.tmpdir') ? Configure::read('MISP.tmpdir') : '/tmp'; + $random = (new RandomTool())->random_str(true, 12); + $dir = new Folder($tmpDir . '/Taxii/' . $random, true); + return [ + 'random' => $random, + 'dir' => $dir + ]; + } + + private function temporaryFile($temporaryFolder) + { + $random = (new RandomTool())->random_str(true, 12); + return new File($temporaryFolder . '/' . $random . '.json', true, 0644); + } + + public function queryInstance($options) + { + $url = $options['TaxiiServer']['baseurl'] . $options['TaxiiServer']['uri']; + App::uses('HttpSocket', 'Network/Http'); + $HttpSocket = new HttpSocket(); + $request = [ + 'header' => [ + 'Accept' => 'application/taxii+json;version=2.1', + 'Content-type' => 'application/taxii+json;version=2.1' + ] + ]; + if (!empty($options['TaxiiServer']['api_key'])) { + $request['header']['Authorization'] = 'basic ' . $options['TaxiiServer']['api_key']; + } + try { + if (!empty($options['type']) && $options['type'] === 'post') { + $response = $HttpSocket->post($url, json_encode($options['body']), $request); + } else { + if (empty($options['query'])) { + $options['query'] = null; + } + $response = $HttpSocket->get( + $url, + $options['query'], + $request + ); + } + if ($response->isOk()) { + return json_decode($response->body, true); + } + } catch (SocketException $e) { + throw new BadRequestException(__('Something went wrong. Error returned: %s', $e->getMessage())); + } + if ($response->code === 403 || $response->code === 401) { + throw new ForbiddenException(__('Authentication failed.')); + } + throw new BadRequestException(__('Something went wrong with the request or the remote side is having issues.')); + } + + public function getCollections($id) + { + $taxii_server = $this->find('first', [ + 'recursive' => -1, + 'conditions' => ['TaxiiServer.id' => $id] + ]); + $taxii_server['TaxiiServer']['uri'] = '/' . $taxii_server['TaxiiServer']['api_root'] . '/collections/'; + $response = $this->queryInstance([ + 'TaxiiServer' => $taxii_server['TaxiiServer'], + 'type' => 'get' + ]); + if (empty($response['collections'])) { + throw new BadRequestException(__('No collections found.')); + } + return $response['collections']; + } + + public function getObjects($id, $collection_id = null, $next = null) + { + $taxii_server = $this->find('first', [ + 'recursive' => -1, + 'conditions' => ['TaxiiServer.id' => $id] + ]); + if (empty($collection_id)) { + $collection_id = $taxii_server['TaxiiServer']['collection']; + } + $taxii_server['TaxiiServer']['uri'] = '/' . $taxii_server['TaxiiServer']['api_root'] . '/collections/' . $collection_id . '/objects/'; + $response = $this->queryInstance([ + 'TaxiiServer' => $taxii_server['TaxiiServer'], + 'type' => 'get', + 'query' => [ + 'limit' => 50, + 'next' => $next + ] + ]); + if (empty($response['objects'])) { + throw new BadRequestException(__('No objects found in collection with the given query parameters.')); + } + return $response; + } + + public function getObject($id, $server_id, $collection_id) + { + $taxii_server = $this->find('first', [ + 'recursive' => -1, + 'conditions' => ['TaxiiServer.id' => $server_id] + ]); + $taxii_server['TaxiiServer']['uri'] = '/' . $taxii_server['TaxiiServer']['api_root'] . '/collections/' . $collection_id . '/objects/' . $id . '/'; + $response = $this->queryInstance([ + 'TaxiiServer' => $taxii_server['TaxiiServer'], + 'type' => 'get' + ]); + if (empty($response['objects'])) { + throw new BadRequestException(__('Invalid object or object not found in the given collection.')); + } + return $response['objects'][0]; + } +} diff --git a/app/Model/Taxonomy.php b/app/Model/Taxonomy.php index f2360ac86..69da0349d 100644 --- a/app/Model/Taxonomy.php +++ b/app/Model/Taxonomy.php @@ -33,6 +33,8 @@ class Taxonomy extends AppModel ) ); + private $__taxonomyConflicts = []; + public function update() { $existing = $this->find('all', array( @@ -118,7 +120,7 @@ class Taxonomy extends AppModel $current = $this->find('first', array( 'conditions' => array('namespace' => $vocab['namespace']), 'recursive' => -1, - 'fields' => array('version', 'enabled', 'namespace') + 'fields' => array('version', 'enabled', 'namespace', 'highlighted') )); $current = empty($current) ? [] : $current['Taxonomy']; $result = $this->__updateVocab($vocab, $current); @@ -147,6 +149,7 @@ class Taxonomy extends AppModel 'version' => $vocab['version'], 'exclusive' => !empty($vocab['exclusive']), 'enabled' => $enabled, + 'highlighted' => !empty($vocab['highlighted']), ]]; $predicateLookup = array(); foreach ($vocab['predicates'] as $k => $predicate) { @@ -176,15 +179,10 @@ class Taxonomy extends AppModel /** * @param int|string $id Taxonomy ID or namespace - * @param string|null $options * @return array|false */ - private function __getTaxonomy($id, $options = array('full' => false, 'filter' => false)) + private function __getTaxonomy($id) { - $filter = false; - if (isset($options['filter'])) { - $filter = $options['filter']; - } if (!is_numeric($id)) { $conditions = ['Taxonomy.namespace' => trim(mb_strtolower($id))]; } else { @@ -236,17 +234,10 @@ class Taxonomy extends AppModel $entries[] = $temp; } } - $taxonomy = array('Taxonomy' => $taxonomy['Taxonomy']); - if ($filter) { - $filter = mb_strtolower($filter); - $namespaceLength = strlen($taxonomy['Taxonomy']['namespace']); - foreach ($entries as $k => $entry) { - if (strpos(substr(mb_strtolower($entry['tag']), $namespaceLength), $filter) === false) { - unset($entries[$k]); - } - } - } - $taxonomy['entries'] = $entries; + $taxonomy = [ + 'Taxonomy' => $taxonomy['Taxonomy'], + 'entries' => $entries, + ]; return $taxonomy; } @@ -264,8 +255,13 @@ class Taxonomy extends AppModel { $taxonomies = $this->find('all', [ 'fields' => ['namespace'], - 'contain' => ['TaxonomyPredicate' => ['TaxonomyEntry']], + 'recursive' => -1, + 'contain' => ['TaxonomyPredicate' => [ + 'fields' => ['value'], + 'TaxonomyEntry' => ['fields' => ['value']]], + ], ]); + $allTaxonomyTags = []; foreach ($taxonomies as $taxonomy) { $namespace = $taxonomy['Taxonomy']['namespace']; @@ -326,7 +322,7 @@ class Taxonomy extends AppModel public function getTaxonomyTags($id, $upperCase = false, $existingOnly = false) { - $taxonomy = $this->__getTaxonomy($id, array('full' => true, 'filter' => false)); + $taxonomy = $this->__getTaxonomy($id); if ($existingOnly) { $this->Tag = ClassRegistry::init('Tag'); $tags = $this->Tag->find('list', array('fields' => array('name'), 'order' => array('UPPER(Tag.name) ASC'))); @@ -352,45 +348,31 @@ class Taxonomy extends AppModel /** * @param int|string $id Taxonomy ID or namespace - * @param array|null $options + * @param bool $full Add tag information to entries * @return array|false */ - public function getTaxonomy($id, $options = array('full' => true)) + public function getTaxonomy($id, $full = true) { - $taxonomy = $this->__getTaxonomy($id, $options); + $taxonomy = $this->__getTaxonomy($id); if (empty($taxonomy)) { return false; } - $this->Tag = ClassRegistry::init('Tag'); - if (isset($options['full']) && $options['full']) { + if ($full) { + $this->Tag = ClassRegistry::init('Tag'); $tagNames = array_column($taxonomy['entries'], 'tag'); $tags = $this->Tag->getTagsByName($tagNames, false); - $filterActive = false; - if (isset($options['enabled'])) { - $filterActive = true; - $enabledTag = isset($options['enabled']) ? $options['enabled'] : null; - } - if (isset($taxonomy['entries'])) { - foreach ($taxonomy['entries'] as $key => $temp) { - if (isset($tags[strtoupper($temp['tag'])])) { - $existingTag = $tags[strtoupper($temp['tag'])]; - if ($filterActive && $options['enabled'] == $existingTag['Tag']['hide_tag']) { - unset($taxonomy['entries'][$key]); - continue; - } - $taxonomy['entries'][$key]['existing_tag'] = $existingTag; - // numerical_value is overridden at tag level. Propagate the override further up - if (isset($existingTag['Tag']['original_numerical_value'])) { - $taxonomy['entries'][$key]['original_numerical_value'] = $existingTag['Tag']['original_numerical_value']; - $taxonomy['entries'][$key]['numerical_value'] = $existingTag['Tag']['numerical_value']; - } - } else { - if ($filterActive) { - unset($taxonomy['entries'][$key]); - } else { - $taxonomy['entries'][$key]['existing_tag'] = false; - } + foreach ($taxonomy['entries'] as $key => $temp) { + $tagLower = mb_strtolower($temp['tag']); + if (isset($tags[$tagLower])) { + $existingTag = $tags[$tagLower]; + $taxonomy['entries'][$key]['existing_tag'] = $existingTag; + // numerical_value is overridden at tag level. Propagate the override further up + if (isset($existingTag['Tag']['original_numerical_value'])) { + $taxonomy['entries'][$key]['original_numerical_value'] = $existingTag['Tag']['original_numerical_value']; + $taxonomy['entries'][$key]['numerical_value'] = $existingTag['Tag']['numerical_value']; } + } else { + $taxonomy['entries'][$key]['existing_tag'] = false; } } } @@ -401,7 +383,7 @@ class Taxonomy extends AppModel { App::uses('ColourPaletteTool', 'Tools'); $paletteTool = new ColourPaletteTool(); - $taxonomy = $this->__getTaxonomy($id, array('full' => true)); + $taxonomy = $this->__getTaxonomy($id); $colours = $paletteTool->generatePaletteFromString($taxonomy['Taxonomy']['namespace'], count($taxonomy['entries'])); $this->Tag = ClassRegistry::init('Tag'); $tags = $this->Tag->getTagsForNamespace($taxonomy['Taxonomy']['namespace'], false); @@ -440,7 +422,7 @@ class Taxonomy extends AppModel $this->Tag = ClassRegistry::init('Tag'); App::uses('ColourPaletteTool', 'Tools'); $paletteTool = new ColourPaletteTool(); - $taxonomy = $this->__getTaxonomy($id, array('full' => true)); + $taxonomy = $this->__getTaxonomy($id); if (empty($taxonomy)) { return false; } @@ -486,7 +468,7 @@ class Taxonomy extends AppModel if ($tagList) { $tags = $tagList; } else { - $taxonomy = $this->__getTaxonomy($id, array('full' => true)); + $taxonomy = $this->__getTaxonomy($id); foreach ($taxonomy['entries'] as $entry) { $tags[] = $entry['tag']; } @@ -513,7 +495,7 @@ class Taxonomy extends AppModel $this->Tag = ClassRegistry::init('Tag'); App::uses('ColourPaletteTool', 'Tools'); $paletteTool = new ColourPaletteTool(); - $taxonomy = $this->__getTaxonomy($id, array('full' => true)); + $taxonomy = $this->__getTaxonomy($id); $tags = $this->Tag->getTagsForNamespace($taxonomy['Taxonomy']['namespace']); $colours = $paletteTool->generatePaletteFromString($taxonomy['Taxonomy']['namespace'], count($taxonomy['entries'])); foreach ($taxonomy['entries'] as $k => $entry) { @@ -546,7 +528,7 @@ class Taxonomy extends AppModel $this->Tag = ClassRegistry::init('Tag'); App::uses('ColourPaletteTool', 'Tools'); $paletteTool = new ColourPaletteTool(); - $taxonomy = $this->__getTaxonomy($id, array('full' => true)); + $taxonomy = $this->__getTaxonomy($id); $tags = $this->Tag->getTagsForNamespace($taxonomy['Taxonomy']['namespace']); $colours = $paletteTool->generatePaletteFromString($taxonomy['Taxonomy']['namespace'], count($taxonomy['entries'])); foreach ($taxonomy['entries'] as $k => $entry) { @@ -613,7 +595,6 @@ class Taxonomy extends AppModel if ($splits === null) { return false; // not a taxonomy tag } - $key = "misp:taxonomies_cache:tagName=$tagName&fullTaxonomy=$fullTaxonomy"; try { @@ -699,6 +680,12 @@ class Taxonomy extends AppModel // at this point, we have a duplicated namespace(-predicate) $taxonomy = $this->getTaxonomyForTag($newTagName); if (!empty($taxonomy['Taxonomy']['exclusive'])) { + if ( + ($newTagName === 'tlp:white' && in_array('tlp:clear', $tagNameList)) || + ($newTagName === 'tlp:clear' && in_array('tlp:white', $tagNameList)) + ) { + return true; + } return false; // only one tag of this taxonomy is allowed } elseif (!empty($taxonomy['TaxonomyPredicate'][0]['exclusive'])) { return false; // only one tag belonging to this predicate is allowed @@ -743,19 +730,33 @@ class Taxonomy extends AppModel $conflictingTaxonomy = array(); foreach ($tagNameList as $tagName) { $tagShortened = $this->stripLastTagComponent($tagName); + // No exclusivity in non taxonomy tags. + if ($tagShortened === '') { + continue; + } if (isset($potentiallyConflictingTaxonomy[$tagShortened])) { - $potentiallyConflictingTaxonomy[$tagShortened]['taxonomy'] = $this->getTaxonomyForTag($tagName); + if (!isset($this->__taxonomyConflicts[$tagShortened])) { + $this->__taxonomyConflicts[$tagShortened] = $this->getTaxonomyForTag($tagName); + } $potentiallyConflictingTaxonomy[$tagShortened]['count']++; } else { - $potentiallyConflictingTaxonomy[$tagShortened] = array( + $potentiallyConflictingTaxonomy[$tagShortened] = [ 'count' => 1 - ); + ]; } $potentiallyConflictingTaxonomy[$tagShortened]['tagNames'][] = $tagName; } - foreach ($potentiallyConflictingTaxonomy as $potTaxonomy) { + if ( + !empty($potentiallyConflictingTaxonomy['tlp']) && + count($potentiallyConflictingTaxonomy['tlp']['tagNames']) == 2 && + in_array('tlp:white', $potentiallyConflictingTaxonomy['tlp']['tagNames']) && + in_array('tlp:clear', $potentiallyConflictingTaxonomy['tlp']['tagNames']) + ) { + unset($potentiallyConflictingTaxonomy['tlp']); + } + foreach ($potentiallyConflictingTaxonomy as $taxonomyName => $potTaxonomy) { if ($potTaxonomy['count'] > 1) { - $taxonomy = $potTaxonomy['taxonomy']; + $taxonomy = $this->__taxonomyConflicts[$taxonomyName]; if (isset($taxonomy['Taxonomy']['exclusive']) && $taxonomy['Taxonomy']['exclusive']) { $conflictingTaxonomy[] = array( 'tags' => $potTaxonomy['tagNames'], @@ -869,4 +870,46 @@ class Taxonomy extends AppModel { return $this->Tag->mergeTag($source_id, $target_id); } + + /** + * @return array + */ + public function getHighlightedTaxonomies() + { + return $this->find('all', [ + 'conditions' => [ + 'highlighted' => 1, + ] + ]); + } + + /** + * + * @param array $highlightedTaxonomies + * @param array $tags + * @return array + */ + public function getHighlightedTags($highlightedTaxonomies, $tags) + { + $highlightedTags = []; + if (is_array($highlightedTaxonomies) && !empty($highlightedTaxonomies)) { + foreach ($highlightedTaxonomies as $k => $taxonomy) { + $highlightedTags[$k] = [ + 'taxonomy' => $taxonomy, + 'tags' => [] + ]; + + foreach ($tags as $tag) { + $splits = $this->splitTagToComponents($tag['Tag']['name']); + if (!empty($splits) && $splits['namespace'] === $taxonomy['Taxonomy']['namespace']) { + $highlightedTags[$k]['tags'][] = $tag; + } + } + } + + return $highlightedTags; + } + + return $highlightedTags; + } } diff --git a/app/Model/User.php b/app/Model/User.php index 74d85e4e8..dee18d411 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -849,7 +849,7 @@ class User extends AppModel return true; } - $this->loadLog(); + $log = $this->loadLog(); $replyToLog = $replyToUser ? ' from ' . $replyToUser['User']['email'] : ''; $gpg = $this->initializeGpg(); @@ -859,8 +859,8 @@ class User extends AppModel } catch (SendEmailException $e) { $this->logException("Exception during sending e-mail", $e); - $this->Log->create(); - $this->Log->save(array( + $log->create(); + $log->save(array( 'org' => 'SYSTEM', 'model' => 'User', 'model_id' => $user['User']['id'], @@ -876,8 +876,8 @@ class User extends AppModel // Intentional two spaces to pass test :) $logTitle .= $replyToLog . ' to ' . $user['User']['email'] . ' sent, titled "' . $result['subject'] . '".'; - $this->Log->create(); - $this->Log->save(array( + $log->create(); + $log->save(array( 'org' => 'SYSTEM', 'model' => 'User', 'model_id' => $user['User']['id'], @@ -1229,6 +1229,15 @@ class User extends AppModel public function extralog($user, $action = null, $description = null, $fieldsResult = null, $modifiedUser = null) { + if (!is_array($user) && $user === 'SYSTEM') { + $user = [ + 'id' => 0, + 'email' => 'SYSTEM', + 'Organisation' => [ + 'name' => 'SYSTEM' + ] + ]; + } // new data $model = 'User'; $modelId = $user['id']; @@ -1414,6 +1423,8 @@ class User extends AppModel /** * Updates `last_api_access` time in database. + * Always update when MISP.store_api_access_time is set. + * Only update every hour when it isn't set * * @param array $user * @return array|bool @@ -1424,8 +1435,11 @@ class User extends AppModel if (!isset($user['id'])) { throw new InvalidArgumentException("Invalid user object provided."); } - $user['last_api_access'] = time(); - return $this->save($user, true, array('id', 'last_api_access')); + $storeAPITime = Configure::read('MISP.store_api_access_time'); + if ((!empty($storeAPITime) && $storeAPITime) || $user['last_api_access'] < time() - 60*60) { + $user['last_api_access'] = time(); + return $this->save($user, true, array('id', 'last_api_access')); + } } /** @@ -1458,18 +1472,23 @@ class User extends AppModel */ public function checkIfUserIsValid(array $user) { - $auth = Configure::read('Security.auth'); - if (!$auth) { - return true; + static $oidc; + + if ($oidc === null) { + $auth = Configure::read('Security.auth'); + if (!$auth) { + return true; + } + if (!is_array($auth)) { + throw new Exception("`Security.auth` config value must be array."); + } + if (!in_array('OidcAuth.Oidc', $auth, true)) { + return true; // this method currently makes sense just for OIDC auth provider + } + App::uses('Oidc', 'OidcAuth.Lib'); + $oidc = new Oidc($this); } - if (!is_array($auth)) { - throw new Exception("`Security.auth` config value must be array."); - } - if (!in_array('OidcAuth.Oidc', $auth, true)) { - return true; // this method currently makes sense just for OIDC auth provider - } - App::uses('Oidc', 'OidcAuth.Lib'); - $oidc = new Oidc($this); + return $oidc->isUserValid($user); } @@ -1971,4 +1990,115 @@ class User extends AppModel } return $users; } + + public function checkForSessionDestruction($id) + { + if (empty(CakeSession::read('creation_timestamp'))) { + return false; + } + $redis = $this->setupRedis(); + if ($redis) { + $cutoff = $redis->get('misp:session_destroy:' . $id); + $allcutoff = $redis->get('misp:session_destroy:all'); + if ( + empty($cutoff) || + ( + !empty($cutoff) && + !empty($allcutoff) && + $allcutoff < $cutoff + ) + ) { + $cutoff = $allcutoff; + } + if ($cutoff && CakeSession::read('creation_timestamp') < $cutoff) { + return true; + } + } + return false; + } + + public function forgotRouter($email, $ip) + { + if (Configure::read('MISP.background_jobs')) { + /** @var Job $job */ + $job = ClassRegistry::init('Job'); + $dummyUser = [ + 'email' => 'SYSTEM', + 'org_id' => 0, + 'role_id' => 0 + ]; + $jobId = $job->createJob($dummyUser, Job::WORKER_EMAIL, 'forgot_password', $email, 'Sending...'); + + $args = [ + 'jobForgot', + $email, + $ip, + $jobId, + ]; + + $this->getBackgroundJobsTool()->enqueue( + BackgroundJobsTool::EMAIL_QUEUE, + BackgroundJobsTool::CMD_ADMIN, + $args, + true, + $jobId + ); + + return true; + } else { + return $this->forgot($email); + } + } + + public function forgot($email, $ip, $jobId = null) + { + $user = $this->find('first', [ + 'recursive' => -1, + 'conditions' => [ + 'User.email' => $email, + 'User.disabled' => 0 + ] + ]); + if (empty($user)) { + return false; + } + $redis = $this->setupRedis(); + $token = RandomTool::random_str(true, 40, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); + $redis->set('misp:forgot:' . $token, $user['User']['id'], ['nx', 'ex' => 600]); + $baseurl = Configure::check('MISP.external_baseurl') ? Configure::read('MISP.external_baseurl') : Configure::read('MISP.baseurl'); + $body = __( + "Dear MISP user,\n\nyou have requested a password reset on the MISP instance at %s. Click the link below to change your password.\n\n%s\n\nThe link above is only valid for 10 minutes, feel free to request a new one if it has expired.\n\nIf you haven't requested a password reset, reach out to your admin team and let them know that someone has attempted it in your stead.\n\nMake sure you keep the contents of this e-mail confidential, do NOT ever forward it as it contains a reset token that is equivalent of a password if acted upon. The IP used to trigger the request was: %s\n\nBest regards,\nYour MISP admin team", + $baseurl, + $baseurl . '/users/password_reset/' . $token, + $ip + ); + $bodyNoEnc = __( + "Dear MISP user,\n\nyou have requested a password reset on the MISP instance at %s, however, no valid encryption key was found for your user and thus we cannot deliver your reset token. Please get in touch with your org admin / with an instance site admin to ask for a reset.\n\nThe IP used to trigger the request was: %s\n\nBest regards,\nYour MISP admin team", + $baseurl, + $ip + ); + $this->sendEmail($user, $body, $bodyNoEnc, __('MISP password reset')); + return true; + } + + public function fetchForgottenPasswordUser($token) + { + if (!ctype_alnum($token)) { + return false; + } + $redis = $this->setupRedis(); + $userId = $redis->get('misp:forgot:' . $token); + if (empty($userId)) { + return false; + } + $user = $this->getAuthUser($userId, true); + return $user; + } + + public function purgeForgetToken($token) + { + $redis = $this->setupRedis(); + $userId = $redis->del('misp:forgot:' . $token); + return true; + } } diff --git a/app/Model/Workflow.php b/app/Model/Workflow.php index 8db42d98d..acb10df12 100644 --- a/app/Model/Workflow.php +++ b/app/Model/Workflow.php @@ -13,7 +13,7 @@ class Workflow extends AppModel public $recursive = -1; public $actsAs = [ - 'AuditLog', + // 'AuditLog', 'Containable', 'SysLogLogable.SysLogLogable' => [ 'roleModel' => 'Role', @@ -518,15 +518,14 @@ class Workflow extends AppModel { $this->Log = ClassRegistry::init('Log'); $message = __('Started executing workflow for trigger `%s` (%s)', $triggerModule->id, $workflow['Workflow']['id']); - $this->Log->createLogEntry('SYSTEM', 'execute_workflow', 'Workflow', $workflow['Workflow']['id'], $message); - $this->__logToFile($workflow, $message); + $this->logExecutionIfDebug($workflow, $message); $workflow = $this->__incrementWorkflowExecutionCount($workflow); $walkResult = []; $debugData = ['original' => $data]; $data = $this->__normalizeDataForTrigger($triggerModule, $data); $debugData['normalized'] = $data; $for_path = !empty($triggerModule->blocking) ? GraphWalker::PATH_TYPE_BLOCKING : GraphWalker::PATH_TYPE_NON_BLOCKING; - $this->sendRequestToDebugEndpoint($workflow, [], '/init?type=' . $for_path, $debugData); + $this->sendRequestToDebugEndpointIfDebug($workflow, [], '/init?type=' . $for_path, $debugData); $blockingPathExecutionSuccess = $this->walkGraph($workflow, $startNodeID, $for_path, $data, $blockingErrors, $walkResult); $executionStoppedByStopModule = in_array('stop-execution', Hash::extract($walkResult, 'blocking_nodes.{n}.data.id')); @@ -542,9 +541,8 @@ class Workflow extends AppModel } $message = __('Finished executing workflow for trigger `%s` (%s). Outcome: %s', $triggerModule->id, $workflow['Workflow']['id'], $outcomeText); - $this->Log->createLogEntry('SYSTEM', 'execute_workflow', 'Workflow', $workflow['Workflow']['id'], $message); - $this->__logToFile($workflow, $message); - $this->sendRequestToDebugEndpoint($workflow, [], '/end?outcome=' . $outcomeText, $walkResult); + $this->logExecutionIfDebug($workflow, $message); + $this->sendRequestToDebugEndpointIfDebug($workflow, [], '/end?outcome=' . $outcomeText, $walkResult); return [ 'outcomeText' => $outcomeText, 'walkResult' => $walkResult, @@ -621,16 +619,20 @@ class Workflow extends AppModel 'id' => Configure::read('MISP.host_org_id') ], ]); + $this->User = ClassRegistry::init('User'); if (!empty($hostOrg)) { + $perms = array_keys($this->User->Role->permFlags); + $allPermEnabled = array_map(function($perm) { + return true; + }, array_flip($perms)); $userForWorkflow = [ 'email' => 'SYSTEM', 'id' => 0, 'org_id' => $hostOrg['Organisation']['id'], - 'Role' => ['perm_site_admin' => 1], + 'Role' => $allPermEnabled, 'Organisation' => $hostOrg['Organisation'] ]; } else { - $this->User = ClassRegistry::init('User'); $userForWorkflow = $this->User->find('first', [ 'recursive' => -1, 'conditions' => [ @@ -657,7 +659,7 @@ class Workflow extends AppModel $message = __('Could not execute disabled module `%s`.', $node['data']['id']); $this->logExecutionError($roamingData->getWorkflow(), $message); $errors[] = $message; - $this->sendRequestToDebugEndpoint($roamingData->getWorkflow(), $node, sprintf('/exec/%s?result=%s', $moduleClass->id, 'disabled_module'), $roamingData->getData()); + $this->sendRequestToDebugEndpointIfDebug($roamingData->getWorkflow(), $node, sprintf('/exec/%s?result=%s', $moduleClass->id, 'disabled_module'), $roamingData->getData()); return false; } if (!is_null($moduleClass)) { @@ -667,17 +669,25 @@ class Workflow extends AppModel $message = __('Error while executing module %s. Error: %s', $node['data']['id'], $e->getMessage()); $this->logExecutionError($roamingData->getWorkflow(), $message); $errors[] = $message; - $this->sendRequestToDebugEndpoint($roamingData->getWorkflow(), $node, sprintf('/exec/%s?result=%s&message=%s', $moduleClass->id, 'error', $e->getMessage()), $roamingData->getData()); + $this->sendRequestToDebugEndpointIfDebug($roamingData->getWorkflow(), $node, sprintf('/exec/%s?result=%s&message=%s', $moduleClass->id, 'error', $e->getMessage()), $roamingData->getData()); return false; } } else { $message = sprintf(__('Could not load class for module: %s'), $node['data']['id']); $this->logExecutionError($roamingData->getWorkflow(), $message); $errors[] = $message; - $this->sendRequestToDebugEndpoint($roamingData->getWorkflow(), $node, sprintf('/exec/%s?result=%s', $node['data']['id'], 'loading_error'), $roamingData->getData()); + $this->sendRequestToDebugEndpointIfDebug($roamingData->getWorkflow(), $node, sprintf('/exec/%s?result=%s', $node['data']['id'], 'loading_error'), $roamingData->getData()); return false; } - $this->sendRequestToDebugEndpoint($roamingData->getWorkflow(), $node, sprintf('/exec/%s?result=%s', $moduleClass->id, 'success'), $roamingData->getData()); + $message = __('Executed node `%s`' . PHP_EOL . 'Node `%s` (%s) from Workflow `%s` (%s) executed successfully', + $node['data']['id'], + $node['data']['id'], + $node['id'], + $roamingData->getWorkflow()['Workflow']['name'], + $roamingData->getWorkflow()['Workflow']['id'] + ); + $this->logExecutionIfDebug($roamingData->getWorkflow(), $message); + $this->sendRequestToDebugEndpointIfDebug($roamingData->getWorkflow(), $node, sprintf('/exec/%s?result=%s', $moduleClass->id, 'success'), $roamingData->getData()); return $success; } @@ -989,6 +999,14 @@ class Workflow extends AppModel $this->__logToFile($workflow, $message); } + public function logExecutionIfDebug(array $workflow, $message): void + { + if ($workflow['Workflow']['debug_enabled']) { + $this->Log->createLogEntry('SYSTEM', 'execute_workflow', 'Workflow', $workflow['Workflow']['id'], $message); + $this->__logToFile($workflow, $message); + } + } + /** * __logToFile Log to file * @@ -1022,8 +1040,8 @@ class Workflow extends AppModel $className = explode('/', $filepath); $className = str_replace('.php', '', $className[count($className)-1]); try { - if (!@include_once($filepath)) { - $message = __('Could not load module for path %s. File does not exists.', $filepath); + if (!include_once($filepath)) { + $message = __('Could not load module for path %s. File does not exist.', $filepath); $this->log($message, LOG_ERR); return $message; } @@ -1156,9 +1174,16 @@ class Workflow extends AppModel if (isset($options['contain'])) { $params['contain'] = !empty($options['contain']) ? $options['contain'] : []; } - if (isset($options['order'])) { - $params['order'] = !empty($options['order']) ? $options['order'] : []; + + $params['order'] = []; + if (!empty($options['order'])) { + $options['order'] = $this->findOrder( + $options['order'], + 'Workflow', + ['id', 'name', 'timestmap', 'trigger_id', 'counter'] + ); } + $workflows = $this->find('all', $params); return $workflows; } @@ -1290,6 +1315,83 @@ class Workflow extends AppModel return $data; } + public function getLabelsForConnections($workflow, $trigger_id): array + { + $graphData = !empty($workflow['Workflow']) ? $workflow['Workflow']['data'] : $workflow['data']; + $startNodeID = $this->workflowGraphTool->getNodeIdForTrigger($graphData, $trigger_id); + if ($startNodeID == -1) { + return []; + } + + $connections = []; + + $filterNodes = $this->workflowGraphTool->extractFilterNodesFromWorkflow($graphData, true); + $filterNodeIDToLabel = Hash::combine($filterNodes, '{n}.id', '{n}.data.indexed_params.filtering-label'); + $resetFilterNodes = $this->workflowGraphTool->extractResetFilterFromWorkflow($graphData, true); + $resetFilterNodeIDToLabel = Hash::combine($resetFilterNodes, '{n}.id', '{n}.data.indexed_params.filtering-label'); + $roamingData = $this->workflowGraphTool->getRoamingData(); + $graphWalker = $this->workflowGraphTool->getWalkerIterator($graphData, $this, $startNodeID, GraphWalker::PATH_TYPE_INCLUDE_LOGIC, $roamingData); + foreach ($graphWalker as $graphNode) { + $node = $graphNode['node']; + $nodeID = $node['id']; + $parsedPathList = GraphWalker::parsePathList($graphNode['path_list']); + if (!empty($parsedPathList)) { + $lastNodeInPath = $parsedPathList[count($parsedPathList)-1]; + $previousNodeId = $lastNodeInPath['source_id']; + $connections[$nodeID][$previousNodeId] = []; + } + foreach ($parsedPathList as $pathEntry) { + if (!empty($filterNodeIDToLabel[$pathEntry['source_id']])) { + $connections[$nodeID][$previousNodeId][] = $filterNodeIDToLabel[$pathEntry['source_id']]; + } + if (!empty($resetFilterNodeIDToLabel[$pathEntry['source_id']])) { + if ($resetFilterNodeIDToLabel[$pathEntry['source_id']] == 'all') { + $connections[$nodeID][$previousNodeId] = []; + } else { + $connections[$nodeID][$previousNodeId] = array_values(array_diff($connections[$nodeID][$previousNodeId], [$resetFilterNodeIDToLabel[$pathEntry['source_id']]])); + } + } + } + } + $connections = array_filter($connections, function($connection) { + foreach ($connection as $labels) { + if (!empty($labels)) { + return true; + } + } + return false; + }); + return $connections; + } + + public function attachLabelToConnections($workflow, $trigger_id=null): array + { + $graphData = !empty($workflow['Workflow']) ? $workflow['Workflow']['data'] : $workflow['data']; + if (is_null($trigger_id)) { + $startNode = $this->workflowGraphTool->extractTriggerFromWorkflow($graphData, true); + $trigger_id = $startNode['data']['id']; + } + $labelsByNodes = $this->getLabelsForConnections($workflow, $trigger_id); + foreach ($graphData as $i => $node) { + if ($i == '_frames') { + continue; + } + if (!empty($labelsByNodes[$node['id']])) { + foreach ($node['inputs'] as $inputName => $inputs) { + foreach ($inputs['connections'] as $j => $connection) { + $workflow['Workflow']['data'][$i]['inputs'][$inputName]['connections'][$j]['labels'] = array_map(function($label) { + return [ + 'id' => Inflector::variable($label), + 'name' => $label, + 'variant' => 'info', + ]; + }, $labelsByNodes[$node['id']][$connection['node']]); + } + } + } + } + return $workflow; + } /** * moduleSattelesExecution Executes a module using the provided configuration and returns back the result * @@ -1336,6 +1438,7 @@ class Workflow extends AppModel 'indexed_params' => $indexed_params, 'saved_filters' => $module_config['saved_filters'], 'module_data' => $module_config, + 'expect_misp_core_format' => $module_config['expect_misp_core_format'], ], 'inputs' => [], 'outputs' => [], @@ -1392,12 +1495,16 @@ class Workflow extends AppModel return $saveSuccess; } + public function sendRequestToDebugEndpointIfDebug(array $workflow, array $node, $path='/', array $data=[]) + { + if ($workflow['Workflow']['debug_enabled']) { + $this->sendRequestToDebugEndpoint($workflow, $node, $path, $data); + } + } + public function sendRequestToDebugEndpoint(array $workflow, array $node, $path='/', array $data=[]) { $debug_url = Configure::read('Plugin.Workflow_debug_url'); - if (empty($workflow['Workflow']['debug_enabled'])) { - return; - } App::uses('HttpSocket', 'Network/Http'); $socket = new HttpSocket([ 'timeout' => 5 diff --git a/app/Model/WorkflowModules/Module_misp_module.php b/app/Model/WorkflowModules/Module_misp_module.php index 8d37b0c4c..00c2c02f3 100644 --- a/app/Model/WorkflowModules/Module_misp_module.php +++ b/app/Model/WorkflowModules/Module_misp_module.php @@ -46,7 +46,7 @@ class Module_misp_module extends WorkflowBaseActionModule if (!empty($misp_module_config['meta']['config']['support_filters'])) { $this->support_filters = !empty($misp_module_config['meta']['config']['support_filters']); } - if (!empty($misp_module_config['meta']['config'])) { + if (!empty($misp_module_config['meta']['config']['params'])) { foreach ($misp_module_config['meta']['config']['params'] as $paramName => $moduleParam) { $this->params[] = $this->translateParams($paramName, $moduleParam); } diff --git a/app/Model/WorkflowModules/WorkflowBaseModule.php b/app/Model/WorkflowModules/WorkflowBaseModule.php index c61edc64a..23da2a3ee 100644 --- a/app/Model/WorkflowModules/WorkflowBaseModule.php +++ b/app/Model/WorkflowModules/WorkflowBaseModule.php @@ -23,7 +23,7 @@ class WorkflowBaseModule ]; public $params = []; - private $Event; + private $Workflow; /** @var PubSubTool */ private static $loadedPubSubTool; @@ -32,11 +32,20 @@ class WorkflowBaseModule { } + public function debug(array $node, WorkflowRoamingData $roamingData, array $data=[]): void + { + if (!isset($this->Workflow)) { + $this->Workflow = ClassRegistry::init('Workflow'); + } + $workflow = $roamingData->getWorkflow(); + $path = sprintf('/debug/%s', $node['data']['id'] ?? ''); + $this->Workflow->sendRequestToDebugEndpoint($workflow, $node, $path, $data); + } + protected function mergeNodeConfigIntoParameters($node): array { $fullIndexedParams = []; foreach ($this->params as $param) { - $param['value'] = $nodeParamByID[$param['id']]['value'] ?? null; $param['value'] = $node['data']['indexed_params'][$param['id']] ?? null; $fullIndexedParams[$param['id']] = $param; } @@ -135,7 +144,7 @@ class WorkflowBaseModule return 'The Factory Must Grow'; } - protected function extractData($data, $path) + public function extractData($data, $path) { $extracted = $data; if (!empty($path)) { @@ -211,14 +220,16 @@ class WorkflowBaseModule return false; } - protected function getItemsMatchingCondition($items, $value, $operator, $path) + public function getItemsMatchingCondition($items, $value, $operator, $path) { foreach ($items as $i => $item) { - $subItem = $this->extractData($item, $path, $operator); + $subItem = $this->extractData($item, $path); if (in_array($operator, ['equals', 'not_equals'])) { $subItem = !empty($subItem) ? $subItem[0] : $subItem; } - if (!$this->evaluateCondition($subItem, $operator, $value)) { + if ($operator == 'any_value' && !empty($subItem)) { + continue; + } else if (!$this->evaluateCondition($subItem, $operator, $value)) { unset($items[$i]); } } @@ -297,4 +308,64 @@ class WorkflowBaseLogicModule extends WorkflowBaseModule class WorkflowBaseActionModule extends WorkflowBaseModule { + protected $fastLookupArrayMispFormat = []; + protected $fastLookupArrayFlattened = []; + + public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors = []): bool + { + $rData = $roamingData->getData(); + $this->_buildFastLookupForRoamingData($rData); + return true; + } + + protected function _buildFastLookupForRoamingData($rData): void + { + if (!empty($rData['Event']['Attribute'])) { + foreach ($rData['Event']['Attribute'] as $i => $attribute) { + $this->fastLookupArrayMispFormat[$attribute['id']] = $i; + } + } + if (!empty($rData['Event']['Object'])) { + foreach ($rData['Event']['Object'] as $j => $object) { + foreach ($object['Attribute'] as $i => $attribute) { + $this->fastLookupArrayMispFormat[$attribute['id']] = [$j, $i]; + } + } + } + foreach ($rData['Event']['_AttributeFlattened'] as $i => $attribute) { + $this->fastLookupArrayFlattened[$attribute['id']] = $i; + } + } + + protected function _overrideAttribute(array $oldAttribute, array $newAttribute, array $rData): array + { + $attributeID = $oldAttribute['id']; + $rData['Event']['_AttributeFlattened'][$this->fastLookupArrayFlattened[$attributeID]] = $newAttribute; + if (is_array($this->fastLookupArrayMispFormat[$attributeID])) { + $objectID = $this->fastLookupArrayMispFormat[$attributeID][0]; + $attributeID = $this->fastLookupArrayMispFormat[$attributeID][1]; + $rData['Event']['Object'][$objectID]['Attribute'][$attributeID] = $newAttribute; + } else { + $attributeID = $this->fastLookupArrayMispFormat[$attributeID]; + $rData['Event']['Attribute'][$attributeID] = $newAttribute; + } + return $rData; + } } + +class WorkflowFilteringLogicModule extends WorkflowBaseLogicModule +{ + public $blocking = false; + public $inputs = 1; + public $outputs = 2; + + protected function _genFilteringLabels(): array + { + $names = ['A', 'B', 'C', 'D', 'E', 'F']; + $labels = []; + foreach ($names as $name) { + $labels[$name] = __('Label %s', $name); + } + return $labels; + } +} \ No newline at end of file diff --git a/app/Model/WorkflowModules/action/Module_assign_country_from_enrichment.php b/app/Model/WorkflowModules/action/Module_assign_country_from_enrichment.php new file mode 100644 index 000000000..5711f70d6 --- /dev/null +++ b/app/Model/WorkflowModules/action/Module_assign_country_from_enrichment.php @@ -0,0 +1,152 @@ +params = [ + [ + 'id' => 'scope', + 'label' => __('Scope'), + 'type' => 'select', + 'options' => [ + 'event' => __('Event'), + 'attribute' => __('Attributes'), + ], + 'default' => 'event', + ], + [ + 'id' => 'hash_path', + 'label' => 'Country Hash path', + 'type' => 'hashpath', + 'placeholder' => 'enrichment.{n}.{n}.values.0', + 'default' => 'enrichment.{n}.{n}.values.0' + ], + [ + 'id' => 'locality', + 'label' => __('Tag Locality'), + 'type' => 'select', + 'options' => [ + 'local' => __('Local'), + 'global' => __('Global'), + ], + 'default' => 'local', + ], + [ + 'id' => 'galaxy_name', + 'label' => __('Galaxy Name'), + 'type' => 'select', + 'options' => [ + 'country' => 'country', + ], + 'placeholder' => __('Pick a galaxy name'), + ], + [ + 'id' => 'relationship_type', + 'label' => __('Relationship Type'), + 'type' => 'input', + 'display_on' => [ + 'action' => 'add', + ], + ], + ]; + + $this->Galaxy = ClassRegistry::init('Galaxy'); + $this->countryClusters = $this->_fetchCountryGalaxyClusters(); + } + + public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors = []): bool + { + $params = $this->getParamsWithValues($node); + + $rData = $roamingData->getData(); + $user = $roamingData->getUser(); + + $countryExtractionPath = $params['hash_path']['value']; + if ($this->filtersEnabled($node)) { + $filters = $this->getFilters($node); + $extracted = $this->extractData($rData, $filters['selector']); + if ($extracted === false) { + return false; + } + $matchingItems = $this->getItemsMatchingCondition($extracted, $filters['value'], $filters['operator'], $filters['path']); + } else { + $matchingItems = $rData; + } + $matchingAttributes = Hash::extract($matchingItems, 'Event._AttributeFlattened.{n}'); + if ($params['scope']['value'] == 'event') { + if (substr($countryExtractionPath, 0, 4) !== '{n}.') { + $countryExtractionPath = '{n}.' . $countryExtractionPath; + } + } + + $result = false; + if ($params['scope']['value'] == 'event') { + $extractedCountries = array_unique(Hash::extract($matchingAttributes, $countryExtractionPath)); + $guessedCountryTags = $this->guessTagFromPath($extractedCountries); + $options = [ + 'tags' => $guessedCountryTags, + 'local' => ($params['locality']['value'] == 'local' ? true : false), + 'relationship_type' => $params['relationship_type']['value'], + ]; + $result = $this->__addTagsToEvent($matchingItems, $options, $user); + } else { + foreach ($matchingAttributes as $attribute) { + $extractedCountries = Hash::extract($attribute, $countryExtractionPath); + $guessedCountryTags = $this->guessTagFromPath($extractedCountries); + $options = [ + 'tags' => $guessedCountryTags, + 'local' => ($params['locality']['value'] == 'local' ? true : false), + 'relationship_type' => $params['relationship_type']['value'], + ]; + $result = $this->__addTagsToAttributes([$attribute], $options, $user); + } + } + return $result; + } + + protected function _fetchCountryGalaxyClusters(): array + { + $clusters = $this->Galaxy->find('first', [ + 'recursive' => -1, + 'conditions' => [ + 'name' => 'Country', + ], + 'contain' => [ + 'GalaxyCluster' => ['fields' => ['id', 'uuid', 'value', 'tag_name']], + ], + ]); + return $clusters['GalaxyCluster']; + } + + protected function guessTagFromPath($countries) + { + $matchingTags = []; + foreach ($countries as $country) { + foreach ($this->countryClusters as $countryCluster) { + if (strtolower($countryCluster['value']) == strtolower($country)) { + $matchingTags[] = $countryCluster['tag_name']; + } + } + } + return $matchingTags; + } +} diff --git a/app/Model/WorkflowModules/action/Module_attach_enrichment.php b/app/Model/WorkflowModules/action/Module_attach_enrichment.php index b7c6e55b7..cdf05e78d 100644 --- a/app/Model/WorkflowModules/action/Module_attach_enrichment.php +++ b/app/Model/WorkflowModules/action/Module_attach_enrichment.php @@ -5,6 +5,7 @@ class Module_attach_enrichment extends WorkflowBaseActionModule { public $id = 'attach-enrichment'; public $name = 'Attach enrichment'; + public $version = '0.3'; public $description = 'Attach selected enrichment result to Attributes.'; public $icon = 'asterisk'; public $inputs = 1; @@ -14,8 +15,7 @@ class Module_attach_enrichment extends WorkflowBaseActionModule public $params = []; private $Module; - private $fastLookupArrayMispFormat = []; - private $fastLookupArrayFlattened = []; + private $allModulesByName = []; public function __construct() @@ -23,19 +23,30 @@ class Module_attach_enrichment extends WorkflowBaseActionModule parent::__construct(); $this->Module = ClassRegistry::init('Module'); $modules = $this->Module->getModules('Enrichment'); - $moduleOptions = []; if (is_array($modules)) { + $this->allModulesByName = Hash::combine($modules, '{n}.name', '{n}'); + } + $moduleOptions = []; + $pickerOptions = []; + $enrichmentAvailable = false; + if (!empty($modules) && is_array($modules)) { + $enrichmentAvailable = true; $moduleOptions = array_merge([''], Hash::combine($modules, '{n}.name', '{n}.name')); } else { - $moduleOptions[] = $modules; + $pickerOptions = [ + 'placeholder_text_multiple' => __('No enrichment module available'), + ]; } sort($moduleOptions); $this->params = [ [ 'id' => 'modules', 'label' => 'Modules', - 'type' => 'select', + 'type' => 'picker', + 'multiple' => true, + 'disabled' => !$enrichmentAvailable, 'options' => $moduleOptions, + 'picker_options' => $pickerOptions, ], ]; } @@ -47,14 +58,17 @@ class Module_attach_enrichment extends WorkflowBaseActionModule if (empty($params['modules']['value'])) { $errors[] = __('No enrichmnent module selected'); return false; + } else if (is_string($params['modules']['value'])) { + $params['modules']['value'] = [$params['modules']['value']]; } + $selectedModules = array_filter($params['modules']['value'], function($module) { + return $module !== ''; + }); $rData = $roamingData->getData(); $event_id = $rData['Event']['id']; $options = [ 'user' => $roamingData->getUser(), 'event_id' => $event_id, - 'module' => $params['modules']['value'], - 'config' => ['_' => '_'], // avoid casting empty associative array in to empty list ]; $matchingItems = $this->getMatchingItemsForAttributes($node, $rData); @@ -64,10 +78,22 @@ class Module_attach_enrichment extends WorkflowBaseActionModule $this->_buildFastLookupForRoamingData($rData); foreach ($matchingItems as $attribute) { - $moduleData = $options; - $moduleData['attribute'] = $attribute; - $queryResult = $this->_queryModules($moduleData, $attribute, $rData); - $rData = $this->_attachEnrichmentData($attribute, $queryResult, $rData); + foreach ($selectedModules as $selectedModule) { + $moduleConfig = $this->allModulesByName[$selectedModule]; + $moduleData = $options; + $moduleData['config'] = $this->getModuleOptions($moduleConfig); + $moduleData['module'] = $selectedModule; + $moduleData['attribute'] = $attribute; + if (!$this->_checkIfInputSupported($attribute, $moduleConfig)) { // Queried module doesn't support the Attribute's type + continue; + } + if (empty($moduleConfig['mispattributes']['format'])) { // Adapt payload if modules doesn't support the misp-format + $moduleData = $this->_convertPayloadToModuleFormat($moduleData, $moduleConfig); + } + $queryResult = $this->_queryModules($moduleData, $attribute, $rData); + $queryResult = $this->_handleModuleResult($queryResult, $moduleConfig); + $rData = $this->_attachEnrichmentData($attribute, $queryResult, $rData); + } } $roamingData->setData($rData); return true; @@ -82,19 +108,31 @@ class Module_attach_enrichment extends WorkflowBaseActionModule return $result; } - protected function _buildFastLookupForRoamingData($rData): void + protected function _convertPayloadToModuleFormat(array $options, array $moduleConfig): array { - foreach ($rData['Event']['Attribute'] as $i => $attribute) { - $this->fastLookupArrayMispFormat[$attribute['id']] = $i; - } - foreach ($rData['Event']['Object'] as $j => $object) { - foreach ($object['Attribute'] as $i => $attribute) { - $this->fastLookupArrayMispFormat[$attribute['id']] = [$j, $i]; + $attribute = $options['attribute']; + unset($options['attribute']); + foreach ($moduleConfig['mispattributes']['input'] as $supportedAttributeType) { + if ($supportedAttributeType == $attribute['type']) { + $options[$supportedAttributeType] = $attribute['value']; } } - foreach ($rData['Event']['_AttributeFlattened'] as $i => $attribute) { - $this->fastLookupArrayFlattened[$attribute['id']] = $i; + return $options; + } + + protected function _checkIfInputSupported(array $attribute, array $moduleConfig): bool + { + foreach ($moduleConfig['mispattributes']['input'] as $supportedAttributeType) { + if ($supportedAttributeType == $attribute['type']) { + return true; + } } + return false; + } + + protected function _handleModuleResult(array $queryResult, array $moduleConfig): array + { + return $queryResult; } protected function _attachEnrichmentData(array $attribute, array $queryResult, array $rData): array @@ -111,4 +149,16 @@ class Module_attach_enrichment extends WorkflowBaseActionModule } return $rData; } + + protected function getModuleOptions(array $moduleConfig): array + { + $type = 'Enrichment'; + $options = []; + if (isset($moduleConfig['meta']['config'])) { + foreach ($moduleConfig['meta']['config'] as $conf) { + $options[$conf] = Configure::read('Plugin.' . $type . '_' . $moduleConfig['name'] . '_' . $conf); + } + } + return !empty($options) ? $options : ['_' => '_']; // avoid casting empty associative array in to empty list + } } diff --git a/app/Model/WorkflowModules/action/Module_attach_warninglist.php b/app/Model/WorkflowModules/action/Module_attach_warninglist.php new file mode 100644 index 000000000..11e335007 --- /dev/null +++ b/app/Model/WorkflowModules/action/Module_attach_warninglist.php @@ -0,0 +1,84 @@ +Warninglist = ClassRegistry::init('Warninglist'); + $warninglists = $this->Warninglist->getEnabled(); + $this->warninglists = $warninglists; + $moduleOptions = array_merge(['ALL' => __('ALL')], Hash::combine($warninglists, '{n}.Warninglist.name', '{n}.Warninglist.name')); + sort($moduleOptions); + $this->params = [ + [ + 'id' => 'warninglists', + 'label' => __('Warninglists'), + 'type' => 'select', + 'options' => $moduleOptions, + 'default' => 'ALL', + ], + ]; + } + + public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors = []): bool + { + parent::exec($node, $roamingData, $errors); + $params = $this->getParamsWithValues($node); + if (empty($params['warninglists']['value'])) { + $errors[] = __('No warninglist module selected'); + return false; + } + $rData = $roamingData->getData(); + + $matchingItems = $this->getMatchingItemsForAttributes($node, $rData); + if ($matchingItems === false) { + return true; + } + + $warninglists = []; + if ($params['warninglists']['value'] == 'ALL') { + $warninglists = $this->warninglists; + } else { + $warninglists = array_filter($this->warninglists, function($wl) use ($params) { + return $wl['Warninglist']['name'] == $params['warninglists']['value']; + }); + } + + $eventWarnings = []; + foreach ($matchingItems as $attribute) { + $attributeWithWarning = $this->Warninglist->checkForWarning($attribute, $warninglists); + if (!empty($attributeWithWarning['warnings'])) { + foreach ($attributeWithWarning['warnings'] as $warning) { + $eventWarnings[$warning['warninglist_id']] = [ + 'id' => $warning['warninglist_id'], + 'name' => $warning['warninglist_name'], + 'category' => $warning['warninglist_category'], + ]; + } + } + $rData = $this->_overrideAttribute($attribute, $attributeWithWarning, $rData); + } + $eventWarnings = array_values($eventWarnings); + $rData['Event']['warnings'] = $eventWarnings; + $roamingData->setData($rData); + return true; + } +} diff --git a/app/Model/WorkflowModules/action/Module_attribute_comment_operation.php b/app/Model/WorkflowModules/action/Module_attribute_comment_operation.php new file mode 100644 index 000000000..2ec0f7ee9 --- /dev/null +++ b/app/Model/WorkflowModules/action/Module_attribute_comment_operation.php @@ -0,0 +1,61 @@ +params = [ + [ + 'id' => 'comment', + 'label' => __('Comment'), + 'type' => 'textarea', + 'placeholder' => 'Comment to be set', + ], + ]; + } + + public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors = []): bool + { + parent::exec($node, $roamingData, $errors); + $params = $this->getParamsWithValues($node); + + $rData = $roamingData->getData(); + $user = $roamingData->getUser(); + + $matchingItems = $this->getMatchingItemsForAttributes($node, $rData); + if ($matchingItems === false) { + return true; + } + $result = $this->__saveAttributes($matchingItems, $rData, $params, $user); + $success = $result['success']; + $updatedRData = $result['updated_rData']; + $roamingData->setData($updatedRData); + return $success; + } + + protected function _editAttribute(array $attribute, array $rData, array $params): array + { + $currentRData = $rData; + $currentRData['__currentAttribute'] = $attribute; + $renderedComment = $this->render_jinja_template($params['comment']['value'], $currentRData); + if ($attribute['comment'] !== $params['comment']['value']) { + $attribute['comment'] = $renderedComment; + } + return $attribute; + } +} diff --git a/app/Model/WorkflowModules/action/Module_attribute_edition_operation.php b/app/Model/WorkflowModules/action/Module_attribute_edition_operation.php index c06ded738..7389e0ac5 100644 --- a/app/Model/WorkflowModules/action/Module_attribute_edition_operation.php +++ b/app/Model/WorkflowModules/action/Module_attribute_edition_operation.php @@ -36,15 +36,21 @@ class Module_attribute_edition_operation extends WorkflowBaseActionModule return $attribute; } - protected function __saveAttribute(array $attributes, array $rData, array $params, array $user): bool + protected function __saveAttributes(array $attributes, array $rData, array $params, array $user): array { $success = false; foreach ($attributes as $attribute) { - $attribute = $this->_editAttribute($attribute, $rData, $params); - unset($attribute['timestamp']); - $saveSuccess = $this->Attribute->editAttribute($attribute, $rData, $user, $attribute['object_id']); + $newAttribute = $this->_editAttribute($attribute, $rData, $params); + unset($newAttribute['timestamp']); + $saveSuccess = $this->Attribute->editAttribute($newAttribute, $rData, $user, $newAttribute['object_id']); + if ($saveSuccess) { + $rData = $this->_overrideAttribute($attribute, $newAttribute, $rData); + } $success = $success || !empty($saveSuccess); } - return $success; + return [ + 'success' => $success, + 'updated_rData' => $rData, + ]; } } diff --git a/app/Model/WorkflowModules/action/Module_attribute_ids_flag_operation.php b/app/Model/WorkflowModules/action/Module_attribute_ids_flag_operation.php index e25f31051..640f927fa 100644 --- a/app/Model/WorkflowModules/action/Module_attribute_ids_flag_operation.php +++ b/app/Model/WorkflowModules/action/Module_attribute_ids_flag_operation.php @@ -1,5 +1,5 @@ __saveAttribute($matchingItems, $rData, $params, $user); - return $result; + $result = $this->__saveAttributes($matchingItems, $rData, $params, $user); + $success = $result['success']; + $updatedRData = $result['updated_rData']; + $roamingData->setData($updatedRData); + return $success; } protected function _editAttribute(array $attribute, array $rData, array $params): array diff --git a/app/Model/WorkflowModules/action/Module_enrich_event.php b/app/Model/WorkflowModules/action/Module_enrich_event.php index 55c2034f3..e5b277462 100644 --- a/app/Model/WorkflowModules/action/Module_enrich_event.php +++ b/app/Model/WorkflowModules/action/Module_enrich_event.php @@ -5,6 +5,7 @@ class Module_enrich_event extends WorkflowBaseActionModule { public $id = 'enrich-event'; public $name = 'Enrich Event'; + public $version = '0.2'; public $description = 'Enrich all Attributes contained in the Event with the provided module.'; public $icon = 'asterisk'; public $inputs = 1; @@ -59,7 +60,9 @@ class Module_enrich_event extends WorkflowBaseActionModule return false; } $matchingItems = $this->getItemsMatchingCondition($extracted, $filters['value'], $filters['operator'], $filters['path']); - if (!empty($matchingItems)) { + if ($this->filtersEnabled($node) && empty($matchingItems)) { + return true; // Filters are enabled and no matching items was found + } else if (!empty($matchingItems)) { $extractedUUIDs = $this->extractData($matchingItems, '{n}.uuid'); if ($extractedUUIDs === false) { return false; diff --git a/app/Model/WorkflowModules/action/Module_ms_teams_webhook.php b/app/Model/WorkflowModules/action/Module_ms_teams_webhook.php index d49f08824..4960f094a 100644 --- a/app/Model/WorkflowModules/action/Module_ms_teams_webhook.php +++ b/app/Model/WorkflowModules/action/Module_ms_teams_webhook.php @@ -5,6 +5,7 @@ class Module_ms_teams_webhook extends Module_webhook { public $id = 'ms-teams-webhook'; public $name = 'MS Teams Webhook'; + public $version = '0.4'; public $description = 'Perform callbacks to the MS Teams webhook provided by the "Incoming Webhook" connector'; public $icon_path = 'MS_Teams.png'; @@ -30,16 +31,16 @@ class Module_ms_teams_webhook extends Module_webhook [ 'id' => 'data_extraction_path', 'label' => 'Data extraction path', - 'type' => 'input', + 'type' => 'hashpath', 'default' => '', 'placeholder' => 'Attribute.{n}.AttributeTag.{n}.Tag.name', ], ]; } - protected function doRequest($url, $contentType, $data) + protected function doRequest($url, $contentType, $data, $headers = [], $serverConfig = null) { - $data = '{"text":"' . implode($data) . '"}'; + $data = ['text' => JsonTool::encode($data)]; return parent::doRequest($url, $contentType, $data); } } diff --git a/app/Model/WorkflowModules/action/Module_push_zmq.php b/app/Model/WorkflowModules/action/Module_push_zmq.php index e6ea39ba6..ddab32965 100644 --- a/app/Model/WorkflowModules/action/Module_push_zmq.php +++ b/app/Model/WorkflowModules/action/Module_push_zmq.php @@ -6,6 +6,7 @@ class Module_push_zmq extends WorkflowBaseActionModule public $blocking = false; public $id = 'push-zmq'; public $name = 'Push to ZMQ'; + public $version = '0.2'; public $description = 'Push to the ZMQ channel'; public $icon_path = 'zeromq.png'; public $inputs = 1; @@ -19,7 +20,7 @@ class Module_push_zmq extends WorkflowBaseActionModule [ 'id' => 'data_extraction_path', 'label' => 'Data extraction path', - 'type' => 'input', + 'type' => 'hashpath', 'default' => '', 'placeholder' => 'Attribute.{n}.AttributeTag.{n}.Tag.name', ], @@ -30,14 +31,14 @@ class Module_push_zmq extends WorkflowBaseActionModule { parent::exec($node, $roamingData, $errors); $params = $this->getParamsWithValues($node); - $path = $params['match_condition']['value']; + $path = $params['data_extraction_path']['value']; $data = $roamingData->getData(); $extracted = $this->extractData($data, $path); if ($extracted === false) { $errors[] = __('Error while trying to extract data with path `%s`', $path); return false; } - $this->push_zmq(JsonTool::encode($extracted)); + $this->push_zmq($extracted); return true; } } diff --git a/app/Model/WorkflowModules/action/Module_send_log_mail.php b/app/Model/WorkflowModules/action/Module_send_log_mail.php new file mode 100644 index 000000000..0172decb7 --- /dev/null +++ b/app/Model/WorkflowModules/action/Module_send_log_mail.php @@ -0,0 +1,48 @@ +all_users, 'User'), 'email')); + return $rcpts; + } + + protected function conditionsFromRData(&$conditions, &$params, $rData) + { + // transform 'Org admins' to a search condition + if (in_array('Org admins', $params['recipients']['value'])) { + $params['recipients']['value'] = array_diff($params['recipients']['value'], ['Org admins']); + $org_id = $this->User->find('first', [ + 'conditions' => ['User.id' => $rData['Log']['user_id']], + 'recursive' => -1, + 'fields' => [ 'org_id'] + ]); + if (empty($org_id)) return; // when user_id=0 or other cases we need to have a fail open to not break all email + $admin_roles = $this->User->Role->find('all', [ + 'conditions' => ['Role.perm_admin' => '1'], + 'fields' => 'Role.id']); + $conditions['OR'][]['AND'] = [ + 'User.role_id' => Hash::extract($admin_roles, '{n}.Role.id'), + 'User.org_id' => Hash::extract($org_id, 'User.org_id') + ]; + } + // no need to return as variables are passed as reference + } + + +} diff --git a/app/Model/WorkflowModules/action/Module_send_mail.php b/app/Model/WorkflowModules/action/Module_send_mail.php index 4fc8c8fee..53a03df7d 100644 --- a/app/Model/WorkflowModules/action/Module_send_mail.php +++ b/app/Model/WorkflowModules/action/Module_send_mail.php @@ -5,15 +5,15 @@ class Module_send_mail extends WorkflowBaseActionModule { public $id = 'send-mail'; public $name = 'Send Mail'; - public $description = 'Allow to send a Mail to a list or recipients'; + public $description = 'Allow to send a Mail to a list or recipients. Requires functional misp-modules to be functional.'; public $icon = 'envelope'; public $inputs = 1; public $outputs = 1; public $support_filters = false; public $params = []; - private $User; - private $all_users; + protected $User; + protected $all_users; public function __construct() { @@ -24,7 +24,7 @@ class Module_send_mail extends WorkflowBaseActionModule 'conditions' => [], 'recursive' => -1, ]); - $users = array_merge(['All accounts'], array_column(array_column($this->all_users, 'User'), 'email')); + $users = $this->getRecipientsList(); $this->params = [ [ 'id' => 'recipients', @@ -32,7 +32,7 @@ class Module_send_mail extends WorkflowBaseActionModule 'type' => 'picker', 'multiple' => true, 'options' => $users, - 'default' => ['All accounts'], + 'default' => ['All admins'], ], [ 'id' => 'mail_template_subject', @@ -49,6 +49,14 @@ class Module_send_mail extends WorkflowBaseActionModule ]; } + protected function getRecipientsList() : array + { + return array_merge( + ['All accounts'], + ['All admins'], + array_column(array_column($this->all_users, 'User'), 'email')); + } + public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors = []): bool { parent::exec($node, $roamingData, $errors); @@ -70,11 +78,26 @@ class Module_send_mail extends WorkflowBaseActionModule if (in_array('All accounts', $params['recipients']['value'])) { $users = $this->all_users; } else { + $conditions = []; + // transform 'All admins' to a search condition + if (in_array('All admins', $params['recipients']['value'])) { + $params['recipients']['value'] = array_diff($params['recipients']['value'], ['All admins']); + $admin_roles = $this->User->Role->find('all', [ + 'conditions' => ['Role.perm_site_admin' => '1'], + 'fields' => 'Role.id']); + $conditions['OR']['User.role_id'] = Hash::extract($admin_roles, '{n}.Role.id'); + } + // call any subclass function using the data + $this->conditionsFromRData($conditions, $params, $rData); // variables are passed as reference + // last but not least, add the remaining items from the list + if (!empty($params['recipients']['value'])) + $conditions['OR']['User.email'] = $params['recipients']['value']; + if (empty($conditions)) { + return false; + } $users = $this->User->find('all', [ - 'conditions' => [ - 'User.email' => $params['recipients']['value'] - ], - 'recursive' => -1, + 'conditions' => $conditions, + 'recursive' => 0, ]); } @@ -84,6 +107,10 @@ class Module_send_mail extends WorkflowBaseActionModule return true; } + protected function conditionsFromRData(&$conditions, &$params, $rData) + { + } + protected function sendMail(array $user, string $content, string $subject): void { $res = $this->User->sendEmail($user, $content, false, $subject); diff --git a/app/Model/WorkflowModules/action/Module_splunk_hec_export.php b/app/Model/WorkflowModules/action/Module_splunk_hec_export.php new file mode 100644 index 000000000..6fd3e65eb --- /dev/null +++ b/app/Model/WorkflowModules/action/Module_splunk_hec_export.php @@ -0,0 +1,167 @@ +params = [ + [ + 'id' => 'url', + 'label' => __('HEC URL'), + 'type' => 'input', + 'placeholder' => 'https://splunk:8088/services/collector/event', + ], + [ + 'id' => 'verify_tls', + 'label' => __('Verify HTTPS Certificate'), + 'type' => 'select', + 'options' => [ + '1' => __('True'), + '0' => __('False'), + ], + 'default' => 1, + ], + [ + 'id' => 'hec_token', + 'label' => __('HEC Token'), + 'type' => 'select', + 'type' => 'input', + 'placeholder' => '00000000-0000-0000-000000000000' + ], + [ + 'id' => 'source_type', + 'label' => __('Source Type'), + 'type' => 'select', + 'type' => 'input', + 'default' => '', + 'placeholder' => 'misp:event' + ], + [ + 'id' => 'event_per_attribute', + 'label' => __('Create one Splunk Event per Attribute'), + 'type' => 'select', + 'options' => [ + '1' => __('True'), + '0' => __('False'), + ], + 'default' => 0, + ], + [ + 'id' => 'data_extraction_model', + 'label' => __('Data extraction model (JSON)'), + 'type' => 'textarea', + 'default' => '', + 'placeholder' => '{ "EventInfo": "Event.info", "AttributeValue": "Event.Attribute.{n}.value"}', + ], + ]; + } + + public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors = []): bool + { + if (empty(Configure::read('Security.rest_client_enable_arbitrary_urls'))) { + $errors[] = __('`Security.rest_client_enable_arbitrary_urls` is turned off'); + return false; + } + $params = $this->getParamsWithValues($node); + if (empty($params['url']['value'])) { + $errors[] = __('URL not provided.'); + return false; + } + if (empty($params['hec_token']['value'])) { + $errors[] = __('Authorization token not provided.'); + return false; + } + + $rData = $roamingData->getData(); + $event_without_attributes = $rData['Event']; + unset($event_without_attributes['Attribute']); + unset($event_without_attributes['_AttributeFlattened']); + + $splunk_events = []; + if (!empty($params['event_per_attribute']['value'])) { + foreach ($rData['Event']['Attribute'] as $attribute) { + $splunk_events[] = [ + 'Attribute' => $attribute, + 'Event' => $event_without_attributes + ]; + } + } else { + $splunk_events[] = $rData; + } + + if (!empty($params['data_extraction_model']['value'])) { + $data_extraction_model = JsonTool::decode($params['data_extraction_model']['value']); + $extracted_events = []; + foreach ($splunk_events as $splunk_event) { + $event = []; + foreach ($data_extraction_model as $field => $path) { + $field_data = $this->extractData($splunk_event, $path); + $event[$field] = count($field_data) == 1 ? $field_data[0] : $field_data; // unpack if only one element + } + $extracted_events[] = $event; + } + $splunk_events = $extracted_events; + } + + return $this->sendToSplunk($splunk_events, $params['hec_token']['value'], $params['url']['value'], $params['source_type']['value']); + } + + protected function sendToSplunk(array $splunk_events, $token, $url, $source_type): bool + { + foreach ($splunk_events as $splunk_event) { + try { + $headers = [ + 'Authorization' => "Splunk {$token}", + ]; + $serverConfig = [ + 'Server' => ['self_signed' => empty($params['verify_tls']['value'])] + ]; + + $hec_event = [ + 'event' => $splunk_event + ]; + if (!empty($source_type)) { + $hec_event['sourcetype'] = $source_type; + } + + $response = $this->doRequest( + $url, + 'json', + $hec_event, + $headers, + $serverConfig + ); + if (!$response->isOk()) { + if ($response->code === 403 || $response->code === 401) { + $errors[] = __('Authentication failed.'); + return false; + } + $errors[] = __('Something went wrong with the request or the remote side is having issues. Body returned: %s', $response->body); + return false; + } + } catch (SocketException $e) { + $errors[] = __('Something went wrong while sending the request. Error returned: %s', $e->getMessage()); + return false; + } catch (Exception $e) { + $errors[] = __('Something went wrong. Error returned: %s', $e->getMessage()); + return false; + } + } + return true; + } +} diff --git a/app/Model/WorkflowModules/action/Module_tag_operation.php b/app/Model/WorkflowModules/action/Module_tag_operation.php index 76add7559..76830ee2d 100644 --- a/app/Model/WorkflowModules/action/Module_tag_operation.php +++ b/app/Model/WorkflowModules/action/Module_tag_operation.php @@ -130,7 +130,7 @@ class Module_tag_operation extends WorkflowBaseActionModule return $result; } - private function __addTagsToAttributes(array $attributes, array $options, array $user): bool + protected function __addTagsToAttributes(array $attributes, array $options, array $user): bool { $success = false; foreach ($attributes as $attribute) { @@ -140,7 +140,7 @@ class Module_tag_operation extends WorkflowBaseActionModule return $success; } - private function __removeTagsFromAttributes(array $attributes, array $options): bool + protected function __removeTagsFromAttributes(array $attributes, array $options): bool { $success = false; foreach ($attributes as $attribute) { @@ -150,12 +150,12 @@ class Module_tag_operation extends WorkflowBaseActionModule return $success; } - private function __addTagsToEvent(array $event, array $options, array $user): bool + protected function __addTagsToEvent(array $event, array $options, array $user): bool { return !empty($this->Event->attachTagsToEventAndTouch($event['Event']['id'], $options, $user)); } - private function __removeTagsFromEvent(array $event, array $options): bool + protected function __removeTagsFromEvent(array $event, array $options): bool { return !empty($this->Event->detachTagsFromEventAndTouch($event['Event']['id'], $options)); } diff --git a/app/Model/WorkflowModules/action/Module_tag_replacement_generic.php b/app/Model/WorkflowModules/action/Module_tag_replacement_generic.php new file mode 100644 index 000000000..b4508097a --- /dev/null +++ b/app/Model/WorkflowModules/action/Module_tag_replacement_generic.php @@ -0,0 +1,184 @@ +params = [ + [ + 'id' => 'scope', + 'label' => __('Scope'), + 'type' => 'select', + 'options' => [ + 'event' => __('Event'), + 'attribute' => __('Attributes'), + 'all' => __('All'), + ], + 'default' => 'event', + ], + [ + 'id' => 'remove_substituted', + 'label' => 'Removed substituted tag', + 'type' => 'select', + 'default' => '1', + 'options' => [ + 'no' => __('No'), + 'yes' => __('Yes'), + ], + ], + [ + 'id' => 'locality', + 'label' => __('Tag Locality'), + 'type' => 'select', + 'options' => [ + 'local' => __('Local'), + 'global' => __('Global'), + ], + 'default' => 'local', + ], + [ + 'id' => 'relationship_type', + 'label' => __('Relationship Type'), + 'type' => 'input', + 'display_on' => [ + 'action' => 'add', + ], + ], + ]; + } + + public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors = []): bool + { + $params = $this->getParamsWithValues($node); + + $rData = $roamingData->getData(); + $user = $roamingData->getUser(); + + if ($this->filtersEnabled($node)) { + $filters = $this->getFilters($node); + $extracted = $this->extractData($rData, $filters['selector']); + if ($extracted === false) { + return false; + } + $matchingItems = $this->getItemsMatchingCondition($extracted, $filters['value'], $filters['operator'], $filters['path']); + } else { + $matchingItems = $rData; + } + + $matchingEvent = $matchingItems; + $matchingAttributes = []; + if ($params['scope']['value'] == 'attribute' || $params['scope']['value'] == 'all') { + $matchingAttributes = Hash::extract($matchingItems, 'Event._AttributeFlattened.{n}'); + } + + if (empty($matchingItems)) { + return true; + } + + $result = false; + $optionsRemove = [ + 'local' => [0, 1], + ]; + $optionsAdd = [ + 'local' => $params['locality']['value'] == 'local' ? true : false, + 'relationship_type' => $params['relationship_type']['value'], + ]; + if ($params['scope']['value'] == 'event' || $params['scope']['value'] == 'all') { + $result = $this->replaceOnEvent($matchingEvent, $params, $user, $optionsRemove, $optionsAdd); + } + if ($params['scope']['value'] == 'attribute' || $params['scope']['value'] == 'all') { + $result = $this->replaceOnAttribute($matchingAttributes, $params, $user, $optionsRemove, $optionsAdd); + } + return $result; + } + + + protected function replaceOnEvent(array $matchingItems, array $params, array $user, array $optionsRemove, array $optionsAdd): bool + { + $result = true; + $extractedTags = Hash::extract($matchingItems['Event']['Tag'], '{n}.name'); + $options = $this->getReplacementOptions($extractedTags); + $optionsRemove['tags'] = $options['remove']; + $optionsAdd['tags'] = $options['add']; + if ($params['remove_substituted']['value'] == 'yes' && !empty($optionsRemove['tags'])) { + $result = $this->__removeTagsFromEvent($matchingItems, $optionsRemove); + } + if (!empty($optionsAdd['tags'])) { + $result = $this->__addTagsToEvent($matchingItems, $optionsAdd, $user); + } + return $result; + } + + protected function replaceOnAttribute(array $matchingItems, array $params, array $user, array $optionsRemove, array $optionsAdd): bool + { + $result = true; + foreach ($matchingItems as $attribute) { + $extractedTags = Hash::extract($attribute['Tag'], '{n}.name'); + $options = $this->getReplacementOptions($extractedTags); + $optionsRemove['tags'] = $options['remove']; + $optionsAdd['tags'] = $options['add']; + if ($params['remove_substituted']['value'] == 'yes' && !empty($optionsRemove['tags'])) { + $result = $this->__removeTagsFromAttributes([$attribute], $optionsRemove); + } + if (!empty($optionsAdd['tags'])) { + $result = $this->__addTagsToAttributes([$attribute], $optionsAdd, $user); + } + } + return $result; + } + + protected function isAMatch($matches): bool + { + return !empty($matches); + } + + protected function searchAndReplaceTag(array $tags): array + { + $toReturn = []; + foreach ($tags as $tag) { + $matches = []; + preg_match($this->searchRegex, $tag, $matches); + if ($this->isAMatch($matches)) { + $toReturn[] = [ + 'matched' => $tag, + 'substitution' => $this->formatSubstitution($matches), + ]; + } + } + return $toReturn; + } + + protected function getReplacementOptions(array $extractedTags) + { + $substitutionResult = $this->searchAndReplaceTag($extractedTags); + $tagsToRemove = Hash::extract($substitutionResult, '{n}.matched'); + $tagsToAdd = Hash::extract($substitutionResult, '{n}.substitution'); + return [ + 'remove' => $tagsToRemove, + 'add' => $tagsToAdd, + ]; + } + + protected function formatSubstitution($matches) + { + return CakeText::insert($this->substitutionTemplate, $matches, ['before' => '{{', 'after' => '}}']); + } +} diff --git a/app/Model/WorkflowModules/action/Module_tag_replacement_pap.php b/app/Model/WorkflowModules/action/Module_tag_replacement_pap.php new file mode 100644 index 000000000..f67b00b5e --- /dev/null +++ b/app/Model/WorkflowModules/action/Module_tag_replacement_pap.php @@ -0,0 +1,36 @@ +white|clear|green|amber|red)"?/i'; + + + protected function isAMatch($matches): bool + { + $namespace = 'pap'; + $predicates = ['white', 'clear', 'green', 'amber', 'red']; + if (empty($matches)) { + return false; + } + return strtolower($matches[1]) == $namespace && in_array(strtolower($matches['predicate']), $predicates); + } + + protected function formatSubstitution($matches) + { + return sprintf('PAP:%s', strtoupper($matches['predicate'])); + } + +} diff --git a/app/Model/WorkflowModules/action/Module_tag_replacement_tlp.php b/app/Model/WorkflowModules/action/Module_tag_replacement_tlp.php new file mode 100644 index 000000000..ad3d19183 --- /dev/null +++ b/app/Model/WorkflowModules/action/Module_tag_replacement_tlp.php @@ -0,0 +1,36 @@ +white|clear|green|amber|amber\+strict|red)"?/i'; + + + protected function isAMatch($matches): bool + { + $namespace = 'tlp'; + $predicates = ['white', 'clear', 'green', 'amber', 'amber+strict', 'red']; + if (empty($matches)) { + return false; + } + return strtolower($matches[1]) == $namespace && in_array(strtolower($matches['predicate']), $predicates); + } + + protected function formatSubstitution($matches) + { + return sprintf('tlp:%s', strtolower($matches['predicate'])); + } + +} diff --git a/app/Model/WorkflowModules/action/Module_telegram_send_alert.php b/app/Model/WorkflowModules/action/Module_telegram_send_alert.php new file mode 100644 index 000000000..241934c73 --- /dev/null +++ b/app/Model/WorkflowModules/action/Module_telegram_send_alert.php @@ -0,0 +1,74 @@ +params = [ + [ + 'id' => 'bot_token', + 'label' => 'Telegram Bot Token', + 'type' => 'input', + 'placeholder' => 'bot123:ABC', + ], + [ + 'id' => 'chat_id', + 'label' => 'Telegram Chat id', + 'type' => 'input', + 'placeholder' => '123', + ], + [ + 'id' => 'message_body_template', + 'label' => 'Message Body Template', + 'type' => 'textarea', + 'placeholder' => __('Template redendered using Jinja2'), + ], + ]; + } + + public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors = []): bool + { + $params = $this->getParamsWithValues($node); + $rData = $roamingData->getData(); + + $bot_token = $params['bot_token']['value']; + $chat_id = $params['chat_id']['value']; + $message_body = $this->render_jinja_template($params['message_body_template']['value'], $rData); + + $data = [ + 'chat_id' => $chat_id, + 'text' => $message_body, + 'parse_mode' => "HTML", + ]; + + $url = $this->telegram_url . $bot_token . "/sendMessage"; + + $response = $this->doRequest( + $url, + 'json', + $data + ); + + if (!$response->isOk()) { + if ($response->code === 401) { + $errors[] = __('Authentication failed'); + return false; + } + $errors[] = __('Something went wrong with the request: %s', $response->body); + return false; + } + + return true; + } + +} diff --git a/app/Model/WorkflowModules/action/Module_webhook.php b/app/Model/WorkflowModules/action/Module_webhook.php index 51626983b..9baf9500e 100644 --- a/app/Model/WorkflowModules/action/Module_webhook.php +++ b/app/Model/WorkflowModules/action/Module_webhook.php @@ -8,7 +8,7 @@ class Module_webhook extends WorkflowBaseActionModule { public $id = 'webhook'; public $name = 'Webhook'; - public $version = '0.2'; + public $version = '0.4'; public $description = 'Allow to perform custom callbacks to the provided URL'; public $icon_path = 'webhook.png'; public $inputs = 1; @@ -42,7 +42,7 @@ class Module_webhook extends WorkflowBaseActionModule [ 'id' => 'data_extraction_path', 'label' => __('Data extraction path'), - 'type' => 'input', + 'type' => 'hashpath', 'default' => '', 'placeholder' => 'Attribute.{n}.AttributeTag.{n}.Tag.name', ], @@ -106,21 +106,22 @@ class Module_webhook extends WorkflowBaseActionModule return false; } - protected function doRequest($url, $contentType, $data) + protected function doRequest($url, $contentType, $data, $headers = [], $serverConfig = null) { $this->Event = ClassRegistry::init('Event'); // We just need a model to use AppModel functions $version = implode('.', $this->Event->checkMISPVersion()); $commit = $this->Event->checkMIPSCommit(); $request = [ - 'header' => [ + 'header' => array_merge([ 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'User-Agent' => 'MISP ' . $version . (empty($commit) ? '' : ' - #' . $commit), - ] + ], $headers) ]; $syncTool = new SyncTool(); - $HttpSocket = $syncTool->setupHttpSocket(null, $this->timeout); + $serverConfig = !empty($serverConfig['Server']) ? $serverConfig : ['Server' => $serverConfig]; + $HttpSocket = $syncTool->setupHttpSocket($serverConfig, $this->timeout); if ($contentType == 'form') { $request['header']['Content-Type'] = 'application/x-www-form-urlencoded'; $response = $HttpSocket->post($url, $data, $request); diff --git a/app/Model/WorkflowModules/logic/Module_generic_filter_data.php b/app/Model/WorkflowModules/logic/Module_generic_filter_data.php new file mode 100644 index 000000000..581a029b9 --- /dev/null +++ b/app/Model/WorkflowModules/logic/Module_generic_filter_data.php @@ -0,0 +1,103 @@ + 'In', + 'not_in' => 'Not in', + 'equals' => 'Equals', + 'not_equals' => 'Not equals', + 'any_value' => 'Any value', + 'in_or' => 'Any value from', + ]; + + public function __construct() + { + parent::__construct(); + $this->params = [ + [ + 'id' => 'filtering-label', + 'label' => __('Filtering Label'), + 'type' => 'select', + 'options' => $this->_genFilteringLabels(), + 'default' => array_keys($this->_genFilteringLabels())[0], + ], + [ + 'id' => 'selector', + 'label' => __('Data selector'), + 'type' => 'input', + 'placeholder' => 'Event._AttributeFlattened.{n}', + ], + [ + 'id' => 'value', + 'label' => __('Value'), + 'type' => 'input', + 'placeholder' => 'tlp:red', + 'display_on' => [ + 'operator' => ['in', 'not_in', 'equals', 'not_equals',], + ], + ], + [ + 'id' => 'value_list', + 'label' => __('Value list'), + 'type' => 'picker', + 'picker_create_new' => true, + 'placeholder' => '[\'ip-src\', \'ip-dst\']', + 'display_on' => [ + 'operator' => 'in_or', + ], + ], + [ + 'id' => 'operator', + 'label' => __('Operator'), + 'type' => 'select', + 'default' => 'in', + 'options' => $this->operators, + ], + [ + 'id' => 'hash_path', + 'label' => __('Hash path'), + 'type' => 'hashpath', + 'placeholder' => 'Tag.name', + ], + ]; + } + + public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors=[]): bool + { + parent::exec($node, $roamingData, $errors); + $params = $this->getParamsWithValues($node); + $selector = $params['selector']['value']; + $path = $params['hash_path']['value']; + $operator = $params['operator']['value']; + $value = $params['value']['value']; + $value_list = $params['value_list']['value']; + $valueToEvaluate = $operator == 'in_or' ? $value_list : $value; + $filteringLabel = $params['filtering-label']['value']; + $rData = $roamingData->getData(); + + $newRData = $rData; + if (empty($newRData['_unfilteredData'])) { + $newRData['_unfilteredData'] = $rData; + } + $newRData['enabledFilters'][$filteringLabel] = [ + 'selector' => $selector, + 'path' => $path, + 'operator' => $operator, + 'value' => $valueToEvaluate, + ]; + + $roamingData->setData($newRData); + return true; + } +} diff --git a/app/Model/WorkflowModules/logic/Module_generic_filter_reset.php b/app/Model/WorkflowModules/logic/Module_generic_filter_reset.php new file mode 100644 index 000000000..decf2fd16 --- /dev/null +++ b/app/Model/WorkflowModules/logic/Module_generic_filter_reset.php @@ -0,0 +1,44 @@ +params = [ + [ + 'id' => 'filtering-label', + 'label' => __('Filtering Label to remove'), + 'type' => 'select', + 'default' => 'all', + 'options' => ['all' => __('All filters')] + $this->_genFilteringLabels(), + ], + ]; + } + + public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors=[]): bool + { + parent::exec($node, $roamingData, $errors); + $params = $this->getParamsWithValues($node); + $filteringLabel = $params['filtering-label']['value']; + $rData = $roamingData->getData(); + + $newRData = $rData['_unfilteredData']; + if (in_array($filteringLabel, array_keys($this->_genFilteringLabels()))) { + unset($newRData['enabledFilters'][$filteringLabel]); + } else if ($filteringLabel === 'all') { + $newRData['enabledFilters'] = []; + } + $roamingData->setData($newRData); + return true; + } +} diff --git a/app/Model/WorkflowModules/logic/Module_generic_if.php b/app/Model/WorkflowModules/logic/Module_generic_if.php index 244efef53..4aafb778c 100644 --- a/app/Model/WorkflowModules/logic/Module_generic_if.php +++ b/app/Model/WorkflowModules/logic/Module_generic_if.php @@ -5,6 +5,7 @@ class Module_generic_if extends WorkflowBaseLogicModule { public $id = 'generic-if'; public $name = 'IF :: Generic'; + public $version = '0.2'; public $description = 'Generic IF / ELSE condition block. The `then` output will be used if the encoded conditions is satisfied, otherwise the `else` output will be used.'; public $icon = 'code-branch'; public $inputs = 1; @@ -17,6 +18,8 @@ class Module_generic_if extends WorkflowBaseLogicModule 'not_in' => 'Not in', 'equals' => 'Equals', 'not_equals' => 'Not equals', + 'any_value' => 'Any value', + 'in_or' => 'Any value from', ]; public function __construct() @@ -28,6 +31,19 @@ class Module_generic_if extends WorkflowBaseLogicModule 'label' => 'Value', 'type' => 'input', 'placeholder' => 'tlp:red', + 'display_on' => [ + 'operator' => ['in', 'not_in', 'equals', 'not_equals',], + ], + ], + [ + 'id' => 'value_list', + 'label' => __('Value list'), + 'type' => 'picker', + 'picker_create_new' => true, + 'placeholder' => '[\'ip-src\', \'ip-dst\']', + 'display_on' => [ + 'operator' => 'in_or', + ], ], [ 'id' => 'operator', @@ -39,7 +55,7 @@ class Module_generic_if extends WorkflowBaseLogicModule [ 'id' => 'hash_path', 'label' => 'Hash path', - 'type' => 'input', + 'type' => 'hashpath', 'placeholder' => 'Attribute.{n}.Tag', ], ]; @@ -52,6 +68,8 @@ class Module_generic_if extends WorkflowBaseLogicModule $path = $params['hash_path']['value']; $operator = $params['operator']['value']; $value = $params['value']['value']; + $value_list = $params['value_list']['value']; + $valueToEvaluate = $operator == 'in_or' ? $value_list : $value; $data = $roamingData->getData(); $extracted = []; if ($operator == 'equals' || $operator == 'not_equals') { @@ -59,7 +77,10 @@ class Module_generic_if extends WorkflowBaseLogicModule } else { $extracted = Hash::extract($data, $path); } - $eval = $this->evaluateCondition($extracted, $operator, $value); + if ($operator == 'any_value' && !empty($extracted)) { + return true; + } + $eval = $this->evaluateCondition($extracted, $operator, $valueToEvaluate); return !empty($eval); } } diff --git a/app/Model/WorkflowModules/logic/Module_tag_if.php b/app/Model/WorkflowModules/logic/Module_tag_if.php index f877419b4..b379d26de 100644 --- a/app/Model/WorkflowModules/logic/Module_tag_if.php +++ b/app/Model/WorkflowModules/logic/Module_tag_if.php @@ -5,6 +5,7 @@ class Module_tag_if extends WorkflowBaseLogicModule { public $id = 'tag-if'; public $name = 'IF :: Tag'; + public $version = '0.4'; public $description = 'Tag IF / ELSE condition block. The `then` output will be used if the encoded conditions is satisfied, otherwise the `else` output will be used.'; public $icon = 'code-branch'; public $inputs = 1; @@ -21,20 +22,34 @@ class Module_tag_if extends WorkflowBaseLogicModule 'not_in_and' => 'Is not tagged with all (AND)', ]; + private function getDisplayTag($fullTag) + { + return substr($fullTag, 12); + } + public function __construct() { parent::__construct(); $conditions = [ - 'Tag.is_galaxy' => 0, + 'Tag.is_galaxy' => [0, 1], ]; $this->Tag = ClassRegistry::init('Tag'); - $tags = $this->Tag->find('all', [ + $allTags = $this->Tag->find('all', [ 'conditions' => $conditions, 'recursive' => -1, 'order' => ['name asc'], - 'fields' => ['Tag.id', 'Tag.name'] + 'fields' => ['Tag.id', 'Tag.name', 'Tag.is_galaxy'] ]); - $tags = array_column(array_column($tags, 'Tag'), 'name'); + $tags = []; + $clusters = []; + foreach ($allTags as $tag) { + if ($tag['Tag']['is_galaxy']) { + $readableTagName = $this->getDisplayTag($tag['Tag']['name']); + $clusters[] = $readableTagName; + } else { + $tags[] = $tag['Tag']['name']; + } + } $this->params = [ [ 'id' => 'scope', @@ -62,6 +77,14 @@ class Module_tag_if extends WorkflowBaseLogicModule 'options' => $tags, 'placeholder' => __('Pick a tag'), ], + [ + 'id' => 'clusters', + 'label' => __('Galaxy Clusters'), + 'type' => 'picker', + 'multiple' => true, + 'options' => $clusters, + 'placeholder' => __('Pick a Galaxy Cluster'), + ], ]; } @@ -70,12 +93,17 @@ class Module_tag_if extends WorkflowBaseLogicModule parent::exec($node, $roamingData, $errors); $params = $this->getParamsWithValues($node); - $value = $params['tags']['value']; + $selectedTags = !empty($params['tags']['value']) ? $params['tags']['value'] : []; + $selectedClusters = !empty($params['clusters']['value']) ? $params['clusters']['value'] : []; + $selectedClusters = array_map(function($tagName) { + return "misp-galaxy:{$tagName}"; // restored stripped part for display purposes + }, $selectedClusters); + $allSelectedTags = array_merge($selectedTags, $selectedClusters); $operator = $params['condition']['value']; $scope = $params['scope']['value']; $data = $roamingData->getData(); $extracted = $this->__getTagFromScope($scope, $data); - $eval = $this->evaluateCondition($extracted, $operator, $value); + $eval = $this->evaluateCondition($extracted, $operator, $allSelectedTags); return !empty($eval); } diff --git a/app/Model/WorkflowModules/logic/Module_threat_level_if.php b/app/Model/WorkflowModules/logic/Module_threat_level_if.php new file mode 100644 index 000000000..6b281ea03 --- /dev/null +++ b/app/Model/WorkflowModules/logic/Module_threat_level_if.php @@ -0,0 +1,96 @@ + 'Is', + 'not_equals' => 'Is not', + 'greater_or_equal_than' => 'Greater or equal than', + 'less_or_equal_than' => 'Less or equal than', + ]; + private $threatlevels_mapping; + + public function __construct() + { + parent::__construct(); + $this->Event = ClassRegistry::init('Event'); + $this->threatlevels_mapping = $this->Event->ThreatLevel->listThreatLevels(); + + $this->params = [ + [ + 'id' => 'condition', + 'label' => 'Condition', + 'type' => 'select', + 'default' => 'equals', + 'options' => $this->operators, + ], + [ + 'id' => 'threatlevel', + 'label' => 'Threat Level', + 'type' => 'select', + 'default' => 'Low', + 'options' => $this->threatlevels_mapping, + 'placeholder' => __('Pick a threat level'), + ], + ]; + } + + public function exec(array $node, WorkflowRoamingData $roamingData, array &$errors=[]): bool + { + parent::exec($node, $roamingData, $errors); + $params = $this->getParamsWithValues($node); + + $operator = $params['condition']['value']; + $selected_threatlevel = $params['threatlevel']['value']; + + $data = $roamingData->getData(); + $threatlevel_id = $data['Event']['threat_level_id']; + + if ($operator == 'equals') { + if ($threatlevel_id == $selected_threatlevel) { + return true; + } else { + return false; + } + } + + if ($operator == 'not_equals') { + if ($threatlevel_id != $selected_threatlevel) { + return true; + } else { + return false; + } + } + + if ($operator == 'greater_or_equal_than') { + if($threatlevel_id <= $selected_threatlevel) { + return true; + } else { + return false; + } + } + + if ($operator == 'less_or_equal_than') { + if($threatlevel_id >= $selected_threatlevel) { + return true; + } else { + return false; + } + } + + return false; + } + +} diff --git a/app/Model/WorkflowModules/trigger/Module_log_after_save.php b/app/Model/WorkflowModules/trigger/Module_log_after_save.php new file mode 100644 index 000000000..e47bc410d --- /dev/null +++ b/app/Model/WorkflowModules/trigger/Module_log_after_save.php @@ -0,0 +1,21 @@ +trigger_overhead_message = __('This trigger is called each time after Log event has been saved. This means that when a large quantity of Logs are being saved, the workflow will be run for as many time as there are log entries.'); + } +} diff --git a/app/Plugin/AadAuth/Controller/Component/Auth/AadAuthenticateAuthenticate.php b/app/Plugin/AadAuth/Controller/Component/Auth/AadAuthenticateAuthenticate.php index a7a61cddd..11c43ff0f 100755 --- a/app/Plugin/AadAuth/Controller/Component/Auth/AadAuthenticateAuthenticate.php +++ b/app/Plugin/AadAuth/Controller/Component/Auth/AadAuthenticateAuthenticate.php @@ -214,7 +214,7 @@ class AadAuthenticateAuthenticate extends BaseAuthenticate ]; $url = self::$auth_provider . self::$ad_tenant . "/oauth2/v2.0/token"; - $response = (new HttpSocket())->post($url, $params, $options); + $response = ($this->_createHttpSocket())->post($url, $params, $options); if (!$response->isOk()) { $this->_log("warning", "Error received during Bearer token fetch (context)."); @@ -239,7 +239,7 @@ class AadAuthenticateAuthenticate extends BaseAuthenticate ]; $url = self::$auth_provider_user . "/v1.0/me"; - $response = (new HttpSocket())->get($url, null, $options); + $response = ($this->_createHttpSocket())->get($url, null, $options); if (!$response->isOk()) { $this->_log("warning", "Error received during user data fetch."); @@ -303,11 +303,11 @@ class AadAuthenticateAuthenticate extends BaseAuthenticate 'Authorization' => 'Bearer ' . $authdata["access_token"] ] ]; - + $has_next_page = true; $url = self::$auth_provider_user . "/v1.0/me/memberOf"; while ($has_next_page) { - $response = (new HttpSocket())->get($url, array(), $options); + $response = ($this->_createHttpSocket())->get($url, array(), $options); if (!$response->isOk()) { $this->_log("warning", "Error received during user group data fetch."); @@ -346,4 +346,20 @@ class AadAuthenticateAuthenticate extends BaseAuthenticate return false; } + + /** + * Create HttpSocket with proxy settings + * + * @return HttpSocket + */ + private function _createHttpSocket() + { + $httpSocket = new HttpSocket(); + $proxy = Configure::read('Proxy'); + if (isset($proxy['host']) && !empty($proxy['host'])) { + $httpSocket->configProxy($proxy['host'], $proxy['port'], $proxy['method'], $proxy['user'], $proxy['password']); + } + + return $httpSocket; + } } diff --git a/app/Plugin/Assets/models/behaviors/LogableBehavior.php b/app/Plugin/Assets/models/behaviors/LogableBehavior.php index 05e73d75d..a0ce0ba3f 100644 --- a/app/Plugin/Assets/models/behaviors/LogableBehavior.php +++ b/app/Plugin/Assets/models/behaviors/LogableBehavior.php @@ -161,10 +161,17 @@ class LogableBehavior extends ModelBehavior { 'limit' => 50); $params = array_merge($defaults, $params); $options = array( - 'order' => $params['order'], 'conditions' => $params['conditions'], 'fields' => $params['fields'], - 'limit' => $params['limit']); + 'limit' => $params['limit'] + ); + if (!empty($options['order'])) { + $options['order'] = $Model->findOrder( + $options['order'], + 'Attribute', + ['id', 'action', 'model_id', 'model', 'ip', 'org', 'email'] + ); + } if ($params[$this->settings[$Model->alias]['classField']] === NULL) { $params[$this->settings[$Model->alias]['classField']] = $Model->alias; } diff --git a/app/Plugin/OidcAuth/Lib/Oidc.php b/app/Plugin/OidcAuth/Lib/Oidc.php index f254296c8..edec8b65d 100644 --- a/app/Plugin/OidcAuth/Lib/Oidc.php +++ b/app/Plugin/OidcAuth/Lib/Oidc.php @@ -19,6 +19,8 @@ class Oidc { $oidc = $this->prepareClient(); + $this->log(null, 'Authenticate'); + if (!$oidc->authenticate()) { throw new Exception("OIDC authentication was not successful."); } @@ -133,13 +135,13 @@ class Oidc ]; if (!$this->User->save($userData)) { - throw new RuntimeException("Could not save user `$mispUsername` to database."); + throw new RuntimeException("Could not create user `$mispUsername` in database."); } $refreshToken = $this->getConfig('offline_access', false) ? $oidc->getRefreshToken() : null; $this->storeMetadata($this->User->id, $claims, $refreshToken); - $this->log($mispUsername, "User saved in database with ID {$this->User->id}"); + $this->log($mispUsername, "User created in database with ID {$this->User->id}"); $this->log($mispUsername, 'Logged in.'); $user = $this->_findUser($settings, ['User.id' => $this->User->id]); @@ -514,11 +516,21 @@ class Oidc } /** - * @param string $username + * @param string|null $username * @param string $message */ private function log($username, $message) { - CakeLog::info("OIDC: User `$username` – $message"); + $sessionId = substr(session_id(), 0, 6); + $ipHeader = Configure::read('MISP.log_client_ip_header') ?: 'REMOTE_ADDR'; + $ip = isset($_SERVER[$ipHeader]) ? trim($_SERVER[$ipHeader]) : $_SERVER['REMOTE_ADDR']; + + if ($username) { + $message = "OIDC user `$username` [$ip;$sessionId] – $message"; + } else { + $message = "OIDC [$ip;$sessionId] – $message"; + } + + CakeLog::info($message); } } diff --git a/app/Test/AttributeValidationToolTest.php b/app/Test/AttributeValidationToolTest.php index 16226d308..b1781220d 100644 --- a/app/Test/AttributeValidationToolTest.php +++ b/app/Test/AttributeValidationToolTest.php @@ -110,6 +110,20 @@ class AttributeValidationToolTest extends TestCase ]); } + public function testValidateAs(): void + { + $this->shouldBeValid('AS', [ + '0', + 0, + 1, + '1', + 4294967295, + ]); + $this->shouldBeInvalid('AS', [ + '1.2.3.4', + ]); + } + public function testCompressIpv6(): void { $this->assertEquals('1234:fd2:5621:1:89::4500', AttributeValidationTool::modifyBeforeValidation('ip-src', '1234:0fd2:5621:0001:0089:0000:0000:4500')); diff --git a/app/View/AccessLogs/admin_index.ctp b/app/View/AccessLogs/admin_index.ctp new file mode 100644 index 000000000..d8e0efa50 --- /dev/null +++ b/app/View/AccessLogs/admin_index.ctp @@ -0,0 +1,383 @@ +
+

+
+
+
+ + +
+
+ Html->script('moment.min'); + echo $this->Html->script('doT'); + echo $this->Html->script('extendext'); + echo $this->Html->css('query-builder.default'); + echo $this->Html->script('query-builder'); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LightPaginator->sort('created') ?>LightPaginator->sort('user_id', __('User')) ?>LightPaginator->sort('ip', __('IP')) ?>LightPaginator->sort('org_id', __('Org')) ?>LightPaginator->sort('request_method', __('Request')) ?>LightPaginator->sort('url', __('URL')) ?>LightPaginator->sort('response_code', __('Code')) ?>LightPaginator->sort('memory_usage', __('Memory')) ?>LightPaginator->sort('duration', __('Duration')) ?>LightPaginator->sort('query_count', __('Queries')) ?>
Time->time($item['AccessLog']['created']); ?>' . h($item['User']['email']) . ''; + } else { + echo __('Deleted user #%s', h($item['AccessLog']['user_id'])); + } + + if (!empty($item['AccessLog']['authkey_id'])) { + echo ' '; + } + ?> + OrgImg->getOrgLogo($item, 24); + } else if ($item['AccessLog']['org_id'] != 0) { + echo __('Deleted org #%s', h($item['AccessLog']['org_id'])); + } + ?> + + "> + ' : '' ?> + ms' : '') ?> +
+ +
+ +element('/genericElements/SideMenu/side_menu', ['menuList' => 'logs', 'menuItem' => 'listAccessLogs']); + diff --git a/app/View/AccessLogs/admin_query_log.ctp b/app/View/AccessLogs/admin_query_log.ctp new file mode 100644 index 000000000..f25bc0cb7 --- /dev/null +++ b/app/View/AccessLogs/admin_query_log.ctp @@ -0,0 +1,14 @@ + + + + + + + + + + + + + +
diff --git a/app/View/AccessLogs/admin_request.ctp b/app/View/AccessLogs/admin_request.ctp new file mode 100644 index 000000000..b2eb724d8 --- /dev/null +++ b/app/View/AccessLogs/admin_request.ctp @@ -0,0 +1 @@ +
diff --git a/app/View/Attributes/add.ctp b/app/View/Attributes/add.ctp index 288dd0387..db89756d6 100644 --- a/app/View/Attributes/add.ctp +++ b/app/View/Attributes/add.ctp @@ -52,16 +52,18 @@ 'div' => 'input clear', 'label' => __("Contextual Comment") ), + array( + 'field' => 'batch_import', + 'type' => 'checkbox', + 'requirements' => $action === 'add', + 'label' => __('Batch import') . ' ', + ), array( 'field' => 'to_ids', 'type' => 'checkbox', 'label' => __("For Intrusion Detection System"), //'stayInLine' => 1 ), - array( - 'field' => 'batch_import', - 'type' => 'checkbox' - ), array( 'field' => 'disable_correlation', 'type' => 'checkbox' @@ -89,7 +91,7 @@ ), 'metaFields' => array( '', - '
' + '
' ) ) )); diff --git a/app/View/Attributes/ajax/attributeEditCategoryForm.ctp b/app/View/Attributes/ajax/attributeEditCategoryForm.ctp index 4d45de114..a44a9b7f1 100644 --- a/app/View/Attributes/ajax/attributeEditCategoryForm.ctp +++ b/app/View/Attributes/ajax/attributeEditCategoryForm.ctp @@ -1,12 +1,12 @@ Form->create('Attribute', array('class' => 'inline-form inline-field-form', 'id' => 'Attribute_' . $object['id'] . '_category_form', 'url' => $baseurl . '/attributes/editField/' . $object['id'])); ?> -
+
Form->input('category', array( - 'options' => array(array_combine($typeCategory[$object['type']], $typeCategory[$object['type']])), + 'options' => array_combine($possibleCategories, $possibleCategories), 'label' => false, 'selected' => $object['category'], 'error' => array('escape' => false), diff --git a/app/View/Attributes/ajax/attributeEditTypeForm.ctp b/app/View/Attributes/ajax/attributeEditTypeForm.ctp index 64dc561fa..fb3092932 100644 --- a/app/View/Attributes/ajax/attributeEditTypeForm.ctp +++ b/app/View/Attributes/ajax/attributeEditTypeForm.ctp @@ -1,12 +1,12 @@ Form->create('Attribute', array('class' => 'inline-form inline-field-form', 'id' => 'Attribute_' . $object['id'] . '_type_form', 'url' => $baseurl . '/attributes/editField/' . $object['id'])); ?> -
+
Form->input('type', array( - 'options' => array(array_combine($categoryDefinitions[$object['category']]['types'], $categoryDefinitions[$object['category']]['types'])), + 'options' => $options, 'label' => false, 'selected' => $object['type'], 'error' => array('escape' => false), diff --git a/app/View/Attributes/ajax/attributeViewFieldForm.ctp b/app/View/Attributes/ajax/attributeViewFieldForm.ctp index 9c145fcc9..e89641444 100644 --- a/app/View/Attributes/ajax/attributeViewFieldForm.ctp +++ b/app/View/Attributes/ajax/attributeViewFieldForm.ctp @@ -3,12 +3,12 @@ if ($field === 'value') { echo $this->element('Events/View/value_field', ['object' => $object['Attribute']]); } elseif ($field === 'timestamp') { echo $this->Time->date($value); -} else { - if ($value === 'No') { - echo ''; - } else if ($value === 'Yes') { - echo ''; +} elseif ($field === 'distribution') { + if ($value == 0) { + echo '' . $shortDist[$value] . ''; } else { - echo nl2br(h($value), false); + echo $shortDist[$value]; } +} else { + echo nl2br(h($value), false); } diff --git a/app/View/Attributes/index.ctp b/app/View/Attributes/index.ctp index 788657df6..77dca0fb2 100755 --- a/app/View/Attributes/index.ctp +++ b/app/View/Attributes/index.ctp @@ -8,6 +8,7 @@ echo $this->element('/genericElements/IndexTable/index_table', [ 'title' => __('Attributes'), 'primary_id_path' => 'Attribute.id', 'data' => $attributes, + 'light_paginator' => true, 'fields' => [ [ 'name' => __('Date'), @@ -140,27 +141,24 @@ echo $this->element('/genericElements/IndexTable/index_table', [ 'Attribute.id' ], 'icon' => 'comment', - 'complex_requirement' => [ - 'function' => function ($object) use ($isSiteAdmin, $me) { - return $isSiteAdmin || ($object['Event']['orgc_id'] !== $me['org_id']); - } - ] + 'title' => __('Add proposal'), + 'complex_requirement' => function ($object) use ($isSiteAdmin, $me) { + return $isSiteAdmin || ($object['Event']['orgc_id'] !== $me['org_id']); + }, ], [ 'onclick' => "deleteObject('shadow_attributes', 'delete', '[onclick_params_data_path]');", 'onclick_params_data_path' => 'Attribute.id', 'icon' => 'trash', 'title' => __('Propose deletion'), - 'complex_requirement' => [ - 'function' => function ($object) use ($isSiteAdmin, $me) { - return $isSiteAdmin || ($object['Event']['orgc_id'] !== $me['org_id']); - } - ] + 'complex_requirement' => function ($object) use ($isSiteAdmin, $me) { + return $isSiteAdmin || ($object['Event']['orgc_id'] !== $me['org_id']); + } ], [ 'title' => __('Propose enrichment'), 'icon' => 'asterisk', - 'onclick' => 'simplePopup(\'' . $baseurl . '/events/queryEnrichment/[onclick_params_data_path]/ShadowAttribute\');', + 'onclick' => 'simplePopup(\'' . $baseurl . '/events/queryEnrichment/[onclick_params_data_path]/Enrichment/ShadowAttribute\');', 'onclick_params_data_path' => 'Attribute.id', 'complex_requirement' => [ 'function' => function ($object) use ($modules, $isSiteAdmin, $me) { @@ -180,7 +178,7 @@ echo $this->element('/genericElements/IndexTable/index_table', [ [ 'title' => __('Propose enrichment through Cortex'), 'icon' => 'eye', - 'onclick' => 'simplePopup(\'' . $baseurl . '/events/queryEnrichment/[onclick_params_data_path]/ShadowAttribute/Cortex\');', + 'onclick' => 'simplePopup(\'' . $baseurl . '/events/queryEnrichment/[onclick_params_data_path]/Enrichment/ShadowAttribute/Cortex\');', 'onclick_params_data_path' => 'Attribute.id', 'complex_requirement' => [ 'function' => function ($object) use ($cortex_modules, $isSiteAdmin, $me) { @@ -204,7 +202,7 @@ echo $this->element('/genericElements/IndexTable/index_table', [ [ 'title' => __('Add enrichment'), 'icon' => 'asterisk', - 'onclick' => 'simplePopup(\'' . $baseurl . '/events/queryEnrichment/[onclick_params_data_path]/Attribute\');', + 'onclick' => 'simplePopup(\'' . $baseurl . '/events/queryEnrichment/[onclick_params_data_path]/Enrichment/Attribute\');', 'onclick_params_data_path' => 'Attribute.id', 'complex_requirement' => function ($object) use ($modules) { return $this->Acl->canModifyEvent($object) && @@ -215,7 +213,7 @@ echo $this->element('/genericElements/IndexTable/index_table', [ [ 'title' => __('Add enrichment via Cortex'), 'icon' => 'eye', - 'onclick' => 'simplePopup(\'' . $baseurl . '/events/queryEnrichment/[onclick_params_data_path]/Attribute/Cortex\');', + 'onclick' => 'simplePopup(\'' . $baseurl . '/events/queryEnrichment/[onclick_params_data_path]/Enrichment/Attribute/Cortex\');', 'onclick_params_data_path' => 'Attribute.id', 'complex_requirement' => function ($object) use ($cortex_modules) { return $this->Acl->canModifyEvent($object) && @@ -252,7 +250,8 @@ echo $this->element('/genericElements/IndexTable/index_table', [ return $this->Acl->canModifyEvent($object) && empty($object['Event']['publish_timestamp']); }, ] - ] + ], + 'persistUrlParams' => ['results'] ] ]); @@ -272,7 +271,6 @@ $class = $isSearch ? 'searchAttributes' : 'listAttributes'; echo $this->element('/genericElements/SideMenu/side_menu', ['menuList' => 'event-collection', 'menuItem' => $class]); ?> - element('/genericElements/SideMenu/side_menu', array('menuList' => 'event-collection', 'menuItem' => 'searchAttributes')); diff --git a/app/View/AuditLogs/admin_index.ctp b/app/View/AuditLogs/admin_index.ctp index 81c556d48..5230b068b 100644 --- a/app/View/AuditLogs/admin_index.ctp +++ b/app/View/AuditLogs/admin_index.ctp @@ -14,7 +14,7 @@ echo $this->Html->css('query-builder.default'); echo $this->Html->script('query-builder'); ?> - element('/genericElements/SideMenu/side_menu', ['menuList' => 'logs', 'menuItem' => 'listAuditLogs']); diff --git a/app/View/AuditLogs/event_index.ctp b/app/View/AuditLogs/event_index.ctp index 76c88152a..b78afc18b 100644 --- a/app/View/AuditLogs/event_index.ctp +++ b/app/View/AuditLogs/event_index.ctp @@ -1,55 +1,59 @@ -
-

- - - - - - - - - - - - - - - - - - - - - - -
Paginator->sort('created') ?>Paginator->sort('user_id', __('User')) ?>Paginator->sort('org_id', __('Org')) ?>Paginator->sort('action') ?>
Time->time($item['AuditLog']['created']); ?>OrgImg->getOrgLogo($item, 24) : '' ?>element('AuditLog/change', ['item' => $item]) ?>
-

- Paginator->counter(array( - 'format' => __('Page {:page} of {:pages}, showing {:current} records out of {:count} total, starting on record {:start}, ending on {:end}') - )); - ?> -

- -
-element('/genericElements/SideMenu/side_menu', ['menuList' => 'event', 'menuItem' => 'eventLog']); - +', empty($ajax) ? ' class="index"' : ''); +echo $this->element('genericElements/IndexTable/index_table', [ + 'data' => [ + 'light_paginator' => 1, + 'data' => $data, + 'fields' => [ + [ + 'name' => __('Created'), + 'data_path' => 'AuditLog.created', + 'sort' => 'AuditLog.created', + 'class' => 'short', + 'element' => 'time' + ], + [ + 'name' => __('User'), + 'data_path' => 'User.email', + 'sort' => 'User.email', + 'class' => 'short', + 'empty' => 'SYSTEM' + ], + [ + 'name' => __('Organisation'), + 'data_path' => 'Organisation', + 'sort' => 'Organisation.name', + 'element' => 'org', + 'class' => 'short' + ], + [ + 'name' => __('Action'), + 'data_path' => 'AuditLog.action_human', + 'sort' => 'AuditLog.action_human', + 'class' => 'short' + ], + [ + 'name' => __('Model'), + 'data_path' => 'AuditLog', + 'element' => 'model', + 'class' => 'short' + ], + [ + 'name' => __('Title'), + 'data_path' => 'AuditLog.title', + 'class' => 'limitedWidth' + ], + [ + 'name' => __('Change'), + 'data_path' => 'AuditLog', + 'element' => 'custom_element', + 'element_path' => 'AuditLog/change' + ] + ], + 'title' => __('Audit logs for event #%s', intval($event['Event']['id'])) + ] +]); +echo '
'; +if (empty($ajax)) { + echo $this->element('/genericElements/SideMenu/side_menu', $menuData); +} diff --git a/app/View/AuthKeys/authkey_display.ctp b/app/View/AuthKeys/authkey_display.ctp index 5cd4161d3..f64321798 100644 --- a/app/View/AuthKeys/authkey_display.ctp +++ b/app/View/AuthKeys/authkey_display.ctp @@ -12,7 +12,7 @@

- + diff --git a/app/View/AuthKeys/index.ctp b/app/View/AuthKeys/index.ctp index bc61d58fa..cc84c1314 100644 --- a/app/View/AuthKeys/index.ctp +++ b/app/View/AuthKeys/index.ctp @@ -43,6 +43,7 @@ 'element' => empty($user_id) ? 'links' : 'generic_field', 'url' => $baseurl . '/users/view', 'url_params_data_paths' => ['User.id'], + 'requirement' => $me['Role']['perm_admin'] || $me['Role']['perm_site_admin'], ], [ 'name' => __('Auth Key'), @@ -72,6 +73,11 @@ 'name' => __('Allowed IPs'), 'data_path' => 'AuthKey.allowed_ips', ], + [ + 'name' => __('Seen IPs'), + 'data_path' => 'AuthKey.unique_ips', + 'element' => 'authkey_pin', + ] ], 'title' => empty($ajax) ? __('Authentication key Index') : false, 'description' => empty($ajax) ? __('A list of API keys bound to a user.') : false, @@ -93,6 +99,7 @@ ), 'icon' => 'edit', 'title' => 'Edit auth key', + 'requirement' => $canCreateAuthkey ], [ 'class' => 'modal-open', @@ -100,6 +107,7 @@ 'url_params_data_paths' => ['AuthKey.id'], 'icon' => 'trash', 'title' => __('Delete auth key'), + 'requirement' => $canCreateAuthkey ] ] ] diff --git a/app/View/AuthKeys/view.ctp b/app/View/AuthKeys/view.ctp index ff9c8262d..106063b27 100644 --- a/app/View/AuthKeys/view.ctp +++ b/app/View/AuthKeys/view.ctp @@ -48,7 +48,7 @@ echo $this->element('genericElements/SingleViews/single_view', [ 'type' => 'custom', 'function' => function (array $data) { if (is_array($data['AuthKey']['allowed_ips'])) { - return implode("
", array_map('h', $data['AuthKey']['allowed_ips'])); + return implode("
", array_map('h', $data['AuthKey']['allowed_ips'])); } return __('All'); } @@ -83,9 +83,9 @@ echo $this->element('genericElements/SingleViews/single_view', [ 'requirement' => isset($keyUsage), ], [ - 'key' => __('Unique IPs'), - 'raw' => $uniqueIps, - 'requirement' => isset($keyUsage), + 'key' => __('Seen IPs'), + 'path' => 'AuthKey.unique_ips', + 'type' => 'authkey_pin' ] ], ]); diff --git a/app/View/Communities/index.ctp b/app/View/Communities/index.ctp index 4b1498fcd..9280af2cf 100644 --- a/app/View/Communities/index.ctp +++ b/app/View/Communities/index.ctp @@ -31,7 +31,7 @@ ), 'fields' => array( array( - 'name' => __('Id'), + 'name' => __('ID'), 'sort' => 'id', 'class' => 'short', 'data_path' => 'id', @@ -76,14 +76,16 @@ 'url_params_data_paths' => array( 'uuid' ), - 'icon' => 'eye' + 'icon' => 'eye', + 'title' => __('View'), ), array( 'url' => $baseurl . '/communities/requestAccess', 'url_params_data_paths' => array( 'uuid' ), - 'icon' => 'comments' + 'icon' => 'comments', + 'title' => __('Request access'), ) ) ) @@ -91,12 +93,12 @@ echo '
'; echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'sync', 'menuItem' => 'list_communities')); ?> - \ No newline at end of file diff --git a/app/View/Elements/generic_picker.ctp b/app/View/Elements/generic_picker.ctp index a4ae2df31..365fc5e52 100644 --- a/app/View/Elements/generic_picker.ctp +++ b/app/View/Elements/generic_picker.ctp @@ -78,8 +78,7 @@ function setupChosen(id, redrawChosen) { fn = window[fn]; submitFunction(this, fn); } else { - select = this; - $select = $(select); + var $select = $(this); var endpoint; if (selected !== undefined) { endpoint = selected.selected; @@ -87,7 +86,7 @@ function setupChosen(id, redrawChosen) { endpoint = $(event.target).val(); } if (endpoint === '') { - $wrapper = $select.closest('div').find('div.generic-picker-wrapper'); + var $wrapper = $select.closest('div').find('div.generic-picker-wrapper'); $wrapper.hide(0); } else { $select.data('endpoint', endpoint); @@ -112,6 +111,13 @@ function setupChosen(id, redrawChosen) { } else { $elem.filter('[autofocus]').trigger('chosen:activate'); } + + // Hide popover when pressing ESC on closed chosen + $chosenContainer.on('keydown', function (e) { + if (e.keyCode === 27 && !$chosenContainer.hasClass('chosen-with-drop')) { + execAndClose($elem); + } + }); } var debounceTimer; diff --git a/app/View/Elements/global_menu.ctp b/app/View/Elements/global_menu.ctp index 8c4801f2c..0771b6529 100755 --- a/app/View/Elements/global_menu.ctp +++ b/app/View/Elements/global_menu.ctp @@ -1,7 +1,6 @@ 'root', @@ -253,15 +252,18 @@ 'url' => $baseurl . '/users/statistics' ), array( - 'type' => 'separator' + 'type' => 'separator', + 'requirement' => $this->Acl->canAccess('threads', 'index'), ), array( 'text' => __('List Discussions'), - 'url' => $baseurl . '/threads/index' + 'url' => $baseurl . '/threads/index', + 'requirement' => $this->Acl->canAccess('threads', 'index'), ), array( 'text' => __('Start Discussion'), - 'url' => $baseurl . '/posts/add' + 'url' => $baseurl . '/posts/add', + 'requirement' => $this->Acl->canAccess('posts', 'add'), ) ) ), @@ -281,12 +283,12 @@ 'requirement' => $this->Acl->canAccess('servers', 'import'), ), array( - 'text' => __('List Servers'), + 'text' => __('Remote Servers'), 'url' => $baseurl . '/servers/index', 'requirement' => $this->Acl->canAccess('servers', 'index'), ), array( - 'text' => __('List Feeds'), + 'text' => __('Feeds'), 'url' => $baseurl . '/feeds/index', 'requirement' => $this->Acl->canAccess('feeds', 'index'), ), @@ -296,17 +298,12 @@ 'requirement' => $this->Acl->canAccess('feeds', 'searchCaches'), ), array( - 'text' => __('List SightingDB Connections'), + 'text' => __('SightingDB'), 'url' => $baseurl . '/sightingdb/index', 'requirement' => $this->Acl->canAccess('sightingdb', 'index'), ), array( - 'text' => __('Add SightingDB Connection'), - 'url' => $baseurl . '/sightingdb/add', - 'requirement' => $this->Acl->canAccess('sightingdb', 'add'), - ), - array( - 'text' => __('List Communities'), + 'text' => __('Communities'), 'url' => $baseurl . '/communities/index', 'requirement' => $this->Acl->canAccess('communities', 'index'), ), @@ -315,6 +312,11 @@ 'url' => $baseurl . '/cerebrates/index', 'requirement' => $this->Acl->canAccess('cerebrates', 'index'), ), + array( + 'text' => __('List Taxii Servers'), + 'url' => $baseurl . '/TaxiiServers/index', + 'requirement' => $this->Acl->canAccess('taxiiServers', 'index'), + ), array( 'text' => __('Event ID translator'), 'url' => '/servers/idTranslator', @@ -393,65 +395,42 @@ ), array( 'type' => 'separator', - 'requirement' => Configure::read('MISP.background_jobs') && $isSiteAdmin + 'requirement' => $isSiteAdmin ), array( 'text' => __('Jobs'), 'url' => $baseurl . '/jobs/index', 'requirement' => Configure::read('MISP.background_jobs') && $isSiteAdmin ), - array( - 'type' => 'separator', - 'requirement' => Configure::read('MISP.background_jobs') && $isSiteAdmin - ), array( 'text' => __('Scheduled Tasks'), 'url' => $baseurl . '/tasks', 'requirement' => Configure::read('MISP.background_jobs') && $isSiteAdmin ), - array( - 'text' => __('Event Block Rules'), - 'url' => $baseurl . '/servers/eventBlockRule', - 'requirement' => $isSiteAdmin - ), - array( - 'type' => 'separator', - 'requirement' => Configure::read('MISP.enableEventBlocklisting') !== false && $isSiteAdmin - ), array( 'html' => sprintf( - '%s%s', - __('Workflows'), - __('new') + '%s', + __('Workflows') ), 'url' => $baseurl . '/workflows/triggers', 'requirement' => $isSiteAdmin ), array( 'type' => 'separator', - 'requirement' => Configure::read('MISP.enableEventBlocklisting') !== false && $isSiteAdmin + 'requirement' => $isSiteAdmin ), array( - 'text' => __('Blocklist Event'), - 'url' => $baseurl . '/eventBlocklists/add', - 'requirement' => Configure::read('MISP.enableEventBlocklisting') !== false && $isSiteAdmin + 'text' => __('Event Block Rules'), + 'url' => $baseurl . '/servers/eventBlockRule', + 'requirement' => $isSiteAdmin ), array( - 'text' => __('Manage Event Blocklists'), + 'text' => __('Event Blocklists'), 'url' => $baseurl . '/eventBlocklists', 'requirement' => Configure::read('MISP.enableEventBlocklisting') !== false && $isSiteAdmin ), array( - 'type' => 'separator', - 'requirement' => Configure::read('MISP.enableEventBlocklisting') !== false && $isSiteAdmin - ), - array( - 'text' => __('Blocklist Organisation'), - 'url' => $baseurl . '/orgBlocklists/add', - 'requirement' => Configure::read('MISP.enableOrgBlocklisting') !== false && $isSiteAdmin - ), - array( - 'text' => __('Manage Org Blocklists'), + 'text' => __('Org Blocklists'), 'url' => $baseurl . '/orgBlocklists', 'requirement' => Configure::read('MISP.enableOrgBlocklisting') !== false && $isSiteAdmin ), @@ -466,9 +445,8 @@ ], [ 'html' => sprintf( - '%s%s', - __('Over-correlating values'), - __('new') + '%s', + __('Over-correlating values') ), 'url' => $baseurl . '/correlations/overCorrelations', 'requirement' => $isSiteAdmin @@ -481,20 +459,26 @@ 'requirement' => $isAclAudit, 'children' => array( array( - 'text' => __('List Logs'), - 'url' => $baseurl . '/admin/logs/index' + 'text' => __('Application Logs'), + 'url' => $baseurl . '/logs/index' ), array( - 'text' => __('List Audit Logs'), + 'text' => __('Audit Logs'), 'url' => $baseurl . '/admin/audit_logs/index', - 'requirement' => Configure::read('MISP.log_new_audit'), + 'requirement' => Configure::read('MISP.log_new_audit') && $this->Acl->canAccess('auditLogs', 'admin_index'), + ), + array( + 'text' => __('Access Logs'), + 'url' => $baseurl . '/admin/access_logs/index', + 'requirement' => $isSiteAdmin ), array( 'text' => __('Search Logs'), - 'url' => $baseurl . '/admin/logs/search' + 'url' => $baseurl . '/admin/logs/search', + 'requirement' => $this->Acl->canAccess('logs', 'admin_search') ) ) - ), + ), array( 'type' => 'root', 'text' => __('API'), diff --git a/app/View/Elements/healthElements/correlations.ctp b/app/View/Elements/healthElements/correlations.ctp index b1e253034..3c7db1e38 100644 --- a/app/View/Elements/healthElements/correlations.ctp +++ b/app/View/Elements/healthElements/correlations.ctp @@ -2,7 +2,7 @@ echo '
'; echo sprintf( '

%s

%s

', - __('This is the correlation management interface. Its goal is to provide youwith information about the currently used correlation engine as well as the data stores of currently dormant engines.'), + __('This is the correlation management interface. Its goal is to provide you with information about the currently used correlation engine as well as the data stores of currently dormant engines.'), __('You will also find management tools for the various engines below, make sure that you keep an eye on the disk requirements as well as the exhaustion of IDs and recorrelate the instance when needed.') ); echo sprintf( diff --git a/app/View/Elements/healthElements/db_schema_diagnostic.ctp b/app/View/Elements/healthElements/db_schema_diagnostic.ctp index 351704f3f..e605e67f0 100644 --- a/app/View/Elements/healthElements/db_schema_diagnostic.ctp +++ b/app/View/Elements/healthElements/db_schema_diagnostic.ctp @@ -19,32 +19,28 @@ ), [...] ); - - */ - function highlightAndSanitize($dirty, $toHighlight, $colorType = 'success') - { - if (is_array($dirty)) { - $arraySane = array(); - foreach ($dirty as $i => $item) { - if (in_array($item, $toHighlight)) { - $arraySane[] = sprintf('', $colorType) . h($item) . ''; - } else { - $arraySane[] = h($item); - } +function highlightAndSanitize($dirty, $toHighlight, $colorType = 'success') +{ + if (is_array($dirty)) { + $arraySane = array(); + foreach ($dirty as $i => $item) { + if (in_array($item, $toHighlight)) { + $arraySane[] = sprintf('', $colorType) . h($item) . ''; + } else { + $arraySane[] = h($item); } - return $arraySane; - } else { - $sane = h($dirty); - $sane = str_replace($toHighlight, sprintf('', $colorType) . h($toHighlight) . '', $sane); - return $sane; } + return $arraySane; + } else { + $sane = h($dirty); + $sane = str_replace($toHighlight, sprintf('', $colorType) . h($toHighlight) . '', $sane); + return $sane; } -?> +} - $tableDiagnostic) { foreach ($tableDiagnostic as $i => $columnDiagnostic) { @@ -171,12 +167,13 @@ __('Updates are locked due to to many update fails') : sprintf(__('Updates unlocked in %s'), h($humanReadableTime))) : __('Updates are not locked'), $updateLocked ? 'times' : 'check' - ); + ); + $validDataSource = in_array($dataSource, ['Database/Mysql', 'Database/MysqlExtended'], true); echo sprintf('%s ', - $dataSource != 'Database/Mysql' ? 'important' : 'success', + $validDataSource ? 'success' : 'important', __('DataSource: ') . h($dataSource), __('DataSource: ') . h($dataSource), - $dataSource != 'Database/Mysql' ? 'times' : 'check' + $validDataSource ? 'check' : 'times' ); if ($expectedDbVersion == $actualDbVersion) { echo $this->element('/healthElements/db_indexes_diagnostic', array( @@ -198,7 +195,7 @@ function adjustRowSpan() { }) } -$(document).ready(function() { +$(function() { // hide non-critical issues if ($('#dbSchemaDiagnosticCheckbox').prop('checked')) { $('#dbSchemaDiagnosticTable').find('tr.noncritical').show(); diff --git a/app/View/Elements/healthElements/diagnostics.ctp b/app/View/Elements/healthElements/diagnostics.ctp index 90ab76710..0ced5e5c9 100644 --- a/app/View/Elements/healthElements/diagnostics.ctp +++ b/app/View/Elements/healthElements/diagnostics.ctp @@ -7,33 +7,48 @@ $humanReadableFilesize = function ($bytes, $dec = 2) { ?>
-
\'utf8\' line in ') . APP; ?>Config/database.php
+
+ \'utf8\' line in ') . APP; ?>Config/database.php +

-

+

" . __('The online version check is disabled, so you will not be warned about an outdated MISP version.') . " "; + } + echo __('Make sure that you update MISP regularly.'); + ?>

- + @@ -45,25 +60,49 @@ $humanReadableFilesize = function ($bytes, $dec = 2) {
- - + +
- +
- +
+ +
+ +

+

+ -

+ + You are using a MISP installation method that does not support or recommend using the MISP self-update, such as a Docker container. Please update using the appropriate update mechanism. + +

+

@@ -238,6 +277,40 @@ $humanReadableFilesize = function ($bytes, $dec = 2) { +

+

+ + + + + + + + + + + $info): ?> + + + + + + + + +
' : '' ?> (' . h($version) .')'; + } else { + echo ''; + if ($outdated) { + echo '
' . __("Version %s installed, but required at least %s", h($version), h($info['required_version'])); + } + } + ?>
+
element('/genericElements/IndexTable/index_table', array( 'data' => array( diff --git a/app/View/Elements/healthElements/tabs.ctp b/app/View/Elements/healthElements/tabs.ctp index daa0ee043..22ad4e038 100644 --- a/app/View/Elements/healthElements/tabs.ctp +++ b/app/View/Elements/healthElements/tabs.ctp @@ -50,12 +50,13 @@ ), 'active' => $active_tab === 'diagnostics' ); - - $data['children'][0]['children'][] = array( - 'url' => $baseurl . '/servers/serverSettings/files', - 'text' => __('Manage files'), - 'active' => $active_tab === 'files' - ); + if (empty(Configure::read('Security.disable_instance_file_uploads'))) { + $data['children'][0]['children'][] = array( + 'url' => $baseurl . '/servers/serverSettings/files', + 'text' => __('Manage files'), + 'active' => $active_tab === 'files' + ); + } $data['children'][0]['children'][] = array( 'url' => $baseurl . '/servers/serverSettings/workers', 'title' => __('Workers'), diff --git a/app/View/Elements/rich_tag.ctp b/app/View/Elements/rich_tag.ctp new file mode 100644 index 000000000..c7a70f97a --- /dev/null +++ b/app/View/Elements/rich_tag.ctp @@ -0,0 +1,99 @@ +TextColour->getTextColour($tag['Tag']['colour']); +$aClass = 'tag nowrap'; +$aText = trim($tag['Tag']['name']); +$aTextModified = null; +if (isset($tag_display_style)) { + if ($tag_display_style == 1) { + // default behaviour, do nothing for now + } else if ($tag_display_style == 2) { + $separator_pos = strpos($aText, ':'); + if ($separator_pos !== false) { + $aTextModified = substr($aText, $separator_pos + 1); + $value_pos = strpos($aTextModified, '='); + if ($value_pos !== false) { + $aTextModified = substr($aTextModified, $value_pos + 1); + $aTextModified = trim($aTextModified, '"'); + } + $aTextModified = h($aTextModified); + } + } else if ($tag_display_style === 0 || $tag_display_style === '0') { + $aTextModified = ' '; + } +} +$aText = h($aText); +$span_scope = !empty($hide_global_scope) ? '' : sprintf( + '', + 'black-white tag', + !empty($tag['local']) ? __('Local tag') : __('Global tag'), + !empty($tag['local']) ? __('Local tag') : __('Global tag'), + !empty($tag['local']) ? 'user' : 'globe-americas' +); +$span_relationship_type = empty($tag['relationship_type']) ? '' : sprintf( + '%s:', + h($tag['relationship_type']), + h($tag['relationship_type']), + h($tag['relationship_type']) +); +if (!empty($tag['Tag']['id'])) { + $span_tag = sprintf( + '%s', + $baseurl . $searchUrl . intval($tag['Tag']['id']), + $aStyle, + $aClass, + isset($aTextModified) ? ' title="' . $aText . '"' : '', + intval($tag['Tag']['id']), + isset($aTextModified) ? $aTextModified : $aText + ); +} else { + $span_tag = sprintf( + '%s', + $aStyle, + $aClass, + $aText + ); +} +$span_delete = ''; +$span_relationship = ''; +if ($canModifyAllTags || ($canModifyLocalTags && $tag['Tag']['local'])) { + $span_relationship = sprintf( + '', + 'black-white tag noPrint modal-open', + __('Modify Tag Relationship'), + __('Modify relationship for tag %s', h($tag['Tag']['name'])), + sprintf( + '%s/tags/modifyTagRelationship/%s/%s', + $baseurl, + h($scope), + h($tag['id']) + ) + ); + $span_delete = sprintf( + 'x', + 'black-white tag useCursorPointer noPrint', + __('Remove tag'), + "button", + "0", + __('Remove tag %s', h($tag['Tag']['name'])), + sprintf( + "removeObjectTagPopup(this, '%s', %s, %s)", + $scope, + $id, + intval($tag['Tag']['id']) + ) + ); +} + +echo '' . $span_scope . $span_relationship_type . $span_tag . $span_relationship . $span_delete . ' '; diff --git a/app/View/Elements/sparkline_new.ctp b/app/View/Elements/sparkline_new.ctp index 4dd7142ec..835787cde 100644 --- a/app/View/Elements/sparkline_new.ctp +++ b/app/View/Elements/sparkline_new.ctp @@ -1,6 +1,6 @@ -
+
diff --git a/app/View/Elements/templateElements/populateTemplateAttribute.ctp b/app/View/Elements/templateElements/populateTemplateAttribute.ctp index c6b77ab93..e7d853c92 100644 --- a/app/View/Elements/templateElements/populateTemplateAttribute.ctp +++ b/app/View/Elements/templateElements/populateTemplateAttribute.ctp @@ -65,7 +65,7 @@ ?>
> - +
diff --git a/app/View/Elements/view_event_distribution_graph.ctp b/app/View/Elements/view_event_distribution_graph.ctp index 74a323944..6326133e5 100644 --- a/app/View/Elements/view_event_distribution_graph.ctp +++ b/app/View/Elements/view_event_distribution_graph.ctp @@ -1,7 +1,7 @@
-
+
diff --git a/app/View/Elements/view_timeline.ctp b/app/View/Elements/view_timeline.ctp index ab8bb1bfb..370b09ddd 100644 --- a/app/View/Elements/view_timeline.ctp +++ b/app/View/Elements/view_timeline.ctp @@ -1,4 +1,4 @@ -
+
element('genericElements/assetLoader', [ - 'js' => [ - 'moment.min', - 'event-timeline', - ], - 'css' => [ - 'event-timeline', - ], - ]); + 'js' => [ + 'moment.min', + 'Chart.min', + 'chartjs-adapter-moment.min', + 'event-timeline', + ], + 'css' => [ + 'event-timeline', + ], +]); diff --git a/app/View/Emails/notification_common.ctp b/app/View/Emails/notification_common.ctp index e9a529d8f..3ed1836f4 100644 --- a/app/View/Emails/notification_common.ctp +++ b/app/View/Emails/notification_common.ctp @@ -52,6 +52,16 @@ $mitre_galaxy_tag_prefix = 'misp-galaxy:mitre-attack-pattern="'; $reportLink = sprintf('%s/users/viewPeriodicSummary/%s', $baseurl, $period); $eventLink = sprintf('%s/events/index/searchpublished:1/searchPublishTimestamp:%s/searchPublishTimestamp:%s', $baseurl, h($start_date->format('Y-m-d H:i:s')), h($now->format('Y-m-d H:i:s'))); +$newCorrelationExplanationText = implode(' ', [ + __('Correlations for the current set of Events are considered as `new` if their matching attribute has been modified during the chosen period.'), + '', + __('Example for a selected period of 7 days:'), + __(' Events from the past 7 days Any other Events'), + __('• Attribute( 3 days ago) → Attribute( 1 days ago) ✓'), + __('• Attribute( 3 days ago) → Attribute( 9 days ago) ✓'), + __('• Attribute(12 days ago) → Attribute( 3 days ago) ⨉'), + __('• Attribute( 9 days ago) → Attribute(11 days ago) ⨉'), +]); $processed_correlations = []; $new_correlations = []; foreach ($events as $event) { @@ -459,7 +469,11 @@ $top_mitre_attack_techniques = array_slice($mitre_attack_techniques, 0, 10); fetch('detailed-summary-correlations')) : ?> -

+

+ + + +

diff --git a/app/View/Events/ajax/enrichmentChoice.ctp b/app/View/Events/ajax/enrichmentChoice.ctp index 98c362a89..4e445c027 100644 --- a/app/View/Events/ajax/enrichmentChoice.ctp +++ b/app/View/Events/ajax/enrichmentChoice.ctp @@ -18,7 +18,7 @@ sprintf( "window.location='%s/events/queryEnrichment/%s';", $baseurl, - implode('/', array(h($attribute_id), h($module['name']), h($type))) + implode('/', array(h($id), h($module['name']), h($type), h($model))) ), h($module['description']), __('Enrich using the %s module', h($module['name'])), diff --git a/app/View/Events/ajax/importChoice.ctp b/app/View/Events/ajax/importChoice.ctp index 6996f364b..285ef97c9 100644 --- a/app/View/Events/ajax/importChoice.ctp +++ b/app/View/Events/ajax/importChoice.ctp @@ -2,9 +2,9 @@
- $import): ?> + - +
')">')">
@@ -12,7 +12,7 @@
\ No newline at end of file diff --git a/app/View/Events/view.ctp b/app/View/Events/view.ctp index 7008ca72a..d564266dc 100644 --- a/app/View/Events/view.ctp +++ b/app/View/Events/view.ctp @@ -147,8 +147,6 @@ ], [ 'key' => __('Warnings'), - 'key_class' => !empty($warnings) ? 'background-red bold' : '', - 'class' => !empty($warnings) ? 'background-red bold' : '', 'type' => 'warnings', 'warnings' => $warnings, 'requirement' => !empty($warnings) && $mayModify, @@ -181,7 +179,8 @@ ], [ 'key' => __('First recorded change'), - 'raw' => !$oldest_timestamp ? '' : $this->Time->time($oldest_timestamp) + 'raw' => !$oldest_timestamp ? '' : $this->Time->time($oldest_timestamp), + 'requirement' => $oldest_timestamp, ], [ 'key' => __('Last change'), diff --git a/app/View/Events/xml/index.ctp b/app/View/Events/xml/index.ctp index d0b01a3e6..305f47181 100644 --- a/app/View/Events/xml/index.ctp +++ b/app/View/Events/xml/index.ctp @@ -1,5 +1,4 @@ $event) { diff --git a/app/View/Feeds/add.ctp b/app/View/Feeds/add.ctp index 47ccdefbd..936b3b49f 100755 --- a/app/View/Feeds/add.ctp +++ b/app/View/Feeds/add.ctp @@ -73,7 +73,7 @@ echo $this->element('genericElements/Form/genericForm', [ 'field' => 'orgc_id', 'label' => __('Creator organisation'), 'options' => $dropdownData['orgs'], - 'value' => $this->request->params['action'] === 'add' ? $me['org_id'] : '', + 'value' => $this->request->params['action'] === 'add' ? $me['org_id'] : null, 'type' => 'dropdown', 'div' => ['id' => 'OrgcDiv', 'style' => 'display:none', 'class' => 'optionalField'], 'class' => 'form-control span6' @@ -91,7 +91,8 @@ echo $this->element('genericElements/Form/genericForm', [ 'label' => __('Target Event ID'), 'placeholder' => __('Leave blank unless you want to reuse an existing event.'), 'div' => ['id' => 'TargetEventDiv', 'style' => 'display:none', 'class' => 'optionalField'], - 'class' => 'form-control span6' + 'class' => 'form-control span6', + 'required' => 0 ], [ 'field' => 'Feed.settings.csv.value', diff --git a/app/View/Galaxies/import.ctp b/app/View/Galaxies/import.ctp index 633f8d5e0..30588481a 100644 --- a/app/View/Galaxies/import.ctp +++ b/app/View/Galaxies/import.ctp @@ -7,7 +7,7 @@ echo $this->element('genericElements/Form/genericForm', array( 'data' => array( 'model' => 'Galaxy', 'title' => __('Import galaxy clusters'), - 'description' => __('Paste a JSON of cluster to import or provide a JSON file below.'), + 'description' => __('Paste a JSON of cluster to import or provide a JSON file below.
Warning: Use galaxy files generated by a MISP instance. Do NOT try to import a galaxy from the MISP-galaxy file format/github repository, as this will fail. For more information on adding galaxies see the MISP-book '), 'fields' => array( array( 'field' => 'json', diff --git a/app/View/Galaxies/index.ctp b/app/View/Galaxies/index.ctp index 1f56f458f..6fd4f9b22 100644 --- a/app/View/Galaxies/index.ctp +++ b/app/View/Galaxies/index.ctp @@ -40,7 +40,7 @@ ), 'fields' => array( array( - 'name' => __('Galaxy Id'), + 'name' => __('ID'), 'sort' => 'Galaxy.id', 'element' => 'links', 'class' => 'short', @@ -60,13 +60,14 @@ 'data_path' => 'Galaxy.name', ), array( - 'name' => __('version'), + 'name' => __('Version'), 'class' => 'short', 'data_path' => 'Galaxy.version', ), array( 'name' => __('Namespace'), 'class' => 'short', + 'sort' => 'Galaxy.namespace', 'data_path' => 'Galaxy.namespace', ), array( @@ -92,7 +93,7 @@ 'actions' => array( array( 'url' => '/galaxies/view', - 'title' => __('View'), + 'title' => __('View'), 'url_params_data_paths' => array( 'Galaxy.id' ), @@ -106,16 +107,9 @@ 'url' => $baseurl . '/galaxies/enable', 'url_params_data_paths' => ['Galaxy.id'], 'postLinkConfirm' => __('Are you sure you want to enable this galaxy library?'), - 'complex_requirement' => array( - 'function' => function ($row, $options) use ($isSiteAdmin) { - return $isSiteAdmin && !$options['datapath']['enabled']; - }, - 'options' => array( - 'datapath' => array( - 'enabled' => 'Galaxy.enabled' - ) - ) - ), + 'complex_requirement' => function ($row) use ($isSiteAdmin) { + return $isSiteAdmin && !$row['Galaxy']['enabled']; + } ), array( 'title' => __('Disable'), @@ -124,20 +118,13 @@ 'url' => $baseurl . '/galaxies/disable', 'url_params_data_paths' => ['Galaxy.id'], 'postLinkConfirm' => __('Are you sure you want to disable this galaxy library?'), - 'complex_requirement' => array( - 'function' => function ($row, $options) use ($isSiteAdmin) { - return $isSiteAdmin && $options['datapath']['enabled']; - }, - 'options' => array( - 'datapath' => array( - 'enabled' => 'Galaxy.enabled' - ) - ) - ), + 'complex_requirement' => function ($row) use ($isSiteAdmin) { + return $isSiteAdmin && $row['Galaxy']['enabled']; + } ), array( 'url' => '/galaxies/delete', - 'title' => __('Delete'), + 'title' => __('Delete'), 'url_params_data_paths' => array( 'Galaxy.id' ), @@ -152,8 +139,7 @@ echo '
'; echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'galaxies', 'menuItem' => 'galaxy_index')); ?> - +element('/genericElements/SideMenu/side_menu', ['menuList' => 'news', 'menuItem' => 'index']); \ No newline at end of file diff --git a/app/View/Noticelists/index.ctp b/app/View/Noticelists/index.ctp index 0a9b9ac96..60cf13877 100644 --- a/app/View/Noticelists/index.ctp +++ b/app/View/Noticelists/index.ctp @@ -16,21 +16,21 @@ $fields = [ 'data_path' => 'Noticelist.expanded_name' ], [ - 'name' => __('ref'), + 'name' => __('Ref'), 'data_path' => 'Noticelist.ref', 'element' => 'links' ], [ - 'name' => __('geographical_area'), + 'name' => __('Geographical area'), 'data_path' => 'Noticelist.geographical_area', 'element' => 'list' ], [ - 'name' => __('version'), + 'name' => __('Version'), 'data_path' => 'Noticelist.version', ], [ - 'name' => __('enabled'), + 'name' => __('Enabled'), 'data_path' => 'Noticelist.enabled', 'element' => 'toggle', 'url' => '/noticelists/toggleEnable', diff --git a/app/View/Noticelists/view.ctp b/app/View/Noticelists/view.ctp index cadc68ecc..82dc3ea38 100644 --- a/app/View/Noticelists/view.ctp +++ b/app/View/Noticelists/view.ctp @@ -6,7 +6,7 @@ echo $this->element( 'data' => $data, 'fields' => [ [ - 'key' => __('Id'), + 'key' => __('ID'), 'path' => 'Noticelist.id' ], [ @@ -28,7 +28,10 @@ echo $this->element( ], [ 'key' => __('Geographical Area'), - 'path' => 'Noticelist.geographical_area', + 'type' => 'custom', + 'function' => function (array $data) { + return implode('
', array_map('h', $data['Noticelist']['geographical_area'])); + } ], [ 'key' => __('Enabled'), diff --git a/app/View/Objects/add.ctp b/app/View/Objects/add.ctp index 7b7d42d55..d10a9fe1f 100644 --- a/app/View/Objects/add.ctp +++ b/app/View/Objects/add.ctp @@ -15,7 +15,7 @@
+ if ($action === 'edit' && !$update_template_available && $newer_template_version !== false): ?> @@ -30,11 +30,11 @@ -
Requirements
+
Required: ' . h(implode(', ', $template['ObjectTemplate']['requirements']['required'])) . '
'; + echo 'Required: ' . h(implode(', ', $template['ObjectTemplate']['requirements']['required'])) . '
'; } if (!empty($template['ObjectTemplate']['requirements']['requiredOneOf'])) { echo 'Required one of: ' . h(implode(', ', $template['ObjectTemplate']['requirements']['requiredOneOf'])); @@ -83,15 +83,15 @@
Form->input('first_seen', array( - 'type' => 'text', - 'div' => 'input hidden', - 'required' => false, - )); + 'type' => 'text', + 'div' => 'input hidden', + 'required' => false, + )); echo $this->Form->input('last_seen', array( - 'type' => 'text', - 'div' => 'input hidden', - 'required' => false, - )); + 'type' => 'text', + 'div' => 'input hidden', + 'required' => false, + )); if ($update_template_available && $newer_template_version !== false) { echo $this->Form->input('template_version', array( 'type' => 'text', @@ -111,7 +111,7 @@
'; + echo h($warning) . '
'; } ?>
@@ -133,32 +133,33 @@ $element): - $row_list[] = $k; + $row_list[] = $k; echo $this->element( - 'Objects/object_add_attributes', - array( - 'element' => $element, - 'k' => $k, - 'action' => $action, - 'enabledRows' => $enabledRows - ) - ); + 'Objects/object_add_attributes', + array( + 'element' => $element, + 'k' => $k, + 'action' => $action, + 'enabledRows' => $enabledRows + ) + ); if ($element['multiple']): $lastOfType = true; $lookAheadArray = array_slice($template['ObjectTemplateElement'], $k, count($template['ObjectTemplateElement']), true); if (count($lookAheadArray) > 1) { foreach ($lookAheadArray as $k2 => $temp) { if ($k2 == $k) continue; - if ($temp['object_relation'] == $element['object_relation']) { + if ($temp['object_relation'] === $element['object_relation']) { $lastOfType = false; + break; } } } if ($lastOfType): ?> - - + + - + @@ -352,24 +353,30 @@ echo $this->element('/genericElements/SideMenu/side_menu', array('menuList' => 'event', 'menuItem' => 'addObject', 'event' => $event)); } ?> - + +element('/genericElements/SideMenu/side_menu', ['menuList' => 'event', 'menuItem' => 'freetextResults']); \ No newline at end of file diff --git a/app/View/Objects/get_row.ctp b/app/View/Objects/get_row.ctp index cc85899a4..7fa25174d 100644 --- a/app/View/Objects/get_row.ctp +++ b/app/View/Objects/get_row.ctp @@ -8,6 +8,6 @@ ) ); ?> - diff --git a/app/View/Objects/revise_object.ctp b/app/View/Objects/revise_object.ctp index c66979690..e08f12094 100644 --- a/app/View/Objects/revise_object.ctp +++ b/app/View/Objects/revise_object.ctp @@ -26,7 +26,7 @@ $tableData = [ echo $this->Form->create('Object', array('id', 'url' => $url)); $formSettings = array( 'type' => 'hidden', - 'value' => json_encode($data), + 'value' => JsonTool::encode($data), 'label' => false, 'div' => false ); @@ -49,7 +49,7 @@ $tableData = [ - + @@ -61,15 +61,11 @@ $tableData = [ $attribute): - $cur_flat = h($attribute['object_relation']) . '.' . h($attribute['type']) . '.' .h($attribute['value']); - $cur_flat_noval = h($attribute['object_relation']) . '.' . h($attribute['type']); - $simple_flattened_attribute[$cur_flat] = $id; - $simple_flattened_attribute_noval[$cur_flat_noval] = $id; + $cur_flat = $simple_flattened_attribute[$id] ?? ''; + $cur_flat_noval = $simple_flattened_attribute_noval[$id] ?? ''; echo sprintf('', h($cur_flat), h($cur_flat_noval)); echo ''; foreach ($attributeFields as $field) { @@ -103,16 +99,16 @@ $tableData = [ Form->button($action === 'add' ? __('Create new object') : __('Update object'), array('class' => 'btn btn-primary')); ?> - - ' . __('This event contains similar objects.') . ''; ?> - ' . __('Instead of creating a new object, would you like to merge your new object into one of the following?') . ''; ?> + +

+
element('Objects/object_similarities', array( 'object' => $object, + 'attributes' => $data['Attribute'], 'template' => $template, - 'similar_object_similarity_amount' => $similar_object_similarity_amount, 'simple_flattened_attribute_noval' => $simple_flattened_attribute_noval, 'simple_flattened_attribute' => $simple_flattened_attribute, 'merge_button_functionname' => 'setMergeObject' @@ -123,7 +119,7 @@ $tableData = [

- +
@@ -172,7 +168,7 @@ function highlight_rows($panel, state) { } var un_highlight_time; -$(document).ready(function() { +$(function() { $('.similarObjectPanel').hover( function() { var $panel = $(this); diff --git a/app/View/OrgBlocklists/index.ctp b/app/View/OrgBlocklists/index.ctp index b00767d59..62d2a9dbb 100644 --- a/app/View/OrgBlocklists/index.ctp +++ b/app/View/OrgBlocklists/index.ctp @@ -32,6 +32,17 @@ echo $this->element('genericElements/IndexTable/scaffold', [ 'data_path' => 'OrgBlocklist.comment', 'class' => 'bitwider' ], + [ + 'name' => 'Blocked amount', + 'sort' => 'OrgBlocklist.blocked_data.blocked_amount', + 'data_path' => 'OrgBlocklist.blocked_data.blocked_amount', + ], + [ + 'name' => 'Blocked last time ', + 'sort' => 'OrgBlocklist.blocked_data.blocked_last_time', + 'data_path' => 'OrgBlocklist.blocked_data.blocked_last_time', + 'element' => 'datetime' + ], ], 'title' => empty($ajax) ? __('Organisation Blocklists') : false, diff --git a/app/View/Organisations/index.ctp b/app/View/Organisations/index.ctp index 7e2f809ef..346d9cafa 100644 --- a/app/View/Organisations/index.ctp +++ b/app/View/Organisations/index.ctp @@ -91,16 +91,19 @@ echo $this->element('/genericElements/IndexTable/index_table', [ ], [ 'name' => __('Nationality'), + 'sort' => 'Organisation.nationality', 'data_path' => 'Organisation', 'class' => 'short', 'element' => 'country', ], [ 'name' => __('Sector'), + 'sort' => 'Organisation.sector', 'data_path' => 'Organisation.sector', ], [ 'name' => __('Type'), + 'sort' => 'Organisation.type', 'data_path' => 'Organisation.type', ], [ diff --git a/app/View/Pages/doc/administration.ctp b/app/View/Pages/doc/administration.ctp deleted file mode 100644 index 5458a0499..000000000 --- a/app/View/Pages/doc/administration.ctp +++ /dev/null @@ -1,250 +0,0 @@ -
- -
- -
-

-

-


-

:

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-


-

: -

    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-


-

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
-
-

-
-

-
-
-

-
-

- - The second use is blocking, if a regular expression is entered with a blank replacement, any event info or attribute value containing the expression will not be added. Please make sure the entered regexp expression follows the preg_replace pattern rules as described here.');?>
-

-
-


-
-

-
-

:

-
-

:

-
- Allowedlist
-
-

:

-
-

:

- :
- <?php echo __('Add user');?> -
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • : click here.');?>
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

:

- :
- <?php echo __('List users');?>
-
    -
  • Id:
  • -
  • Org:
  • -
  • :
  • -
  • :
  • -
  • Contactalert:
  • -
  • GnuPGkey:
  • -
  • Nids Sid:
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

:

- :
-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • : click here.');?>
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

:

- :
- <?php echo __('Contact');?>
-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
- -
-

-
-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • : .
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

:

-
-

:

-
- <?php echo __('List roles');?>
-
-

-
- :

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-
-

:

- :
- <?php echo __('List logs');?>

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • : :
    - variable (initial_value) => (new_value),...
    - :
    - org() => (ADMIN), date() => (20012-10-19),...
    -
- <?php echo __('Search log');?> -

:

- :

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-
-

-

-
-

- -

- CakeResque, so all of the CakeResque commands work. - To start all of the workers needed by MISP go to your /var/www/MISP/app/Console/worker (assuming a standard installation path) and execute start.sh. - To interact with the workers, here is a list of useful commands. Go to your /var/www/MISP/app/Console (assuming a standard installation path) and execute one of the following commands as a parameter to ./cake CakeResque.CakeResque (for example: ./cake CakeResque.CakeResque tail)');?>:

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
- CakeResque list of commands.');?>
-

- :

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

-

- :

-
    -
  • :
  • -
  • :
  • -
  • (h):
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

-

-

-
- :
- /var/www/MISP/app/Console/cake Password my.email@address.com
- -
diff --git a/app/View/Pages/doc/categories_and_types.ctp b/app/View/Pages/doc/categories_and_types.ctp index 296f20d95..11244e8f1 100644 --- a/app/View/Pages/doc/categories_and_types.ctp +++ b/app/View/Pages/doc/categories_and_types.ctp @@ -1,15 +1,10 @@ -
- -

' . h($attribute['object_relation']) . '
- $catDef): ?> + $catDef): ?> @@ -21,7 +16,7 @@ $catDef): ?> @@ -73,3 +68,4 @@
- +

+element('/genericElements/SideMenu/side_menu', array('menuList' => 'globalActions', 'menuItem' => 'categoriesAndTypes')); \ No newline at end of file diff --git a/app/View/Pages/doc/concepts.ctp b/app/View/Pages/doc/concepts.ctp deleted file mode 100644 index 505b9795d..000000000 --- a/app/View/Pages/doc/concepts.ctp +++ /dev/null @@ -1,46 +0,0 @@ -
- -
- -
-

-

- -

- -

- -

-
-

- -

- -

- -

- -

- -

- -

- -

- -

- -

- -

- -
diff --git a/app/View/Pages/doc/general.ctp b/app/View/Pages/doc/general.ctp deleted file mode 100644 index df8225a2c..000000000 --- a/app/View/Pages/doc/general.ctp +++ /dev/null @@ -1,97 +0,0 @@ -
- -
- -
-

-

-

-

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
- -

-
-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
- -
-
    -
  • :
  • -
  • :
  • -
- -
-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
- -
-
    -
  • :
  • -
- -
-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
- -
-
    -
  • :
  • -
  • :
  • -
- -
-
    -
  • :
  • -
  • :
  • -
-

-

-
diff --git a/app/View/Pages/doc/quickstart.ctp b/app/View/Pages/doc/quickstart.ctp deleted file mode 100644 index 6cd00f014..000000000 --- a/app/View/Pages/doc/quickstart.ctp +++ /dev/null @@ -1,26 +0,0 @@ -
- -
- -
-

-

-

-

-

-

-

-

- -
diff --git a/app/View/Pages/doc/user_management.ctp b/app/View/Pages/doc/user_management.ctp deleted file mode 100644 index 7cd94259b..000000000 --- a/app/View/Pages/doc/user_management.ctp +++ /dev/null @@ -1,72 +0,0 @@ -
- -
-
-

- -

:

-

-
    -
  • :

  • -
  • :

    -


  • -
  • :

  • -
  • :

    -


  • -
  • :

    -
  • : T

  • -
  • :
-
-

:

-
-
    -
  • -


    -
  • -
  • :

  • -
  • :

  • -
  • :

  • -
  • :

  • -
  • :

  • -
  • :

    -

-
-

:

-

-
    -
  • :

  • -
  • :

  • -
  • :

  • -
  • :

  • -
  • : -
      -
    • - : -
    • -
    • - : -
    • -
    -
  • -
-

:

-

-
    -
  • :

  • -
  • :

  • -
-
diff --git a/app/View/Pages/doc/using_the_system.ctp b/app/View/Pages/doc/using_the_system.ctp deleted file mode 100644 index 241c6c438..000000000 --- a/app/View/Pages/doc/using_the_system.ctp +++ /dev/null @@ -1,621 +0,0 @@ -
- -
-
-

:

-

:

-

- :

-

-
    -
  • :
  • -
  • : - :
  • -
  • -
      -
    • : .
      - -
    • -
    • : .
      - -
    • -
    • :
      - -
    • -
    • :
      - -
    • -
    -
  • -
  • : :
  • -
    • -
    • :
    • -
    • :
    • -
    • :
    • -
  • -
  • : :
  • -
    • -
    • :
    • -
    • :
    • -
    • :
    • -
  • -
  • :
  • -
-
-

:

-
-
-<?php echo __('Attribute tools');?>

-

-

-

:

-

<?php echo __('Add attribute');?>

-
    -
  • : Html->link(__('click here', true), array('controller' => 'pages', 'action' => 'display', 'doc', 'categories_and_types')));?>
  • -
  • : Html->link(__('click here', true), array('controller' => 'pages', 'action' => 'display', 'doc', 'categories_and_types')));?>
  • -
  • : click here.');?>
  • -
  • :
  • -
  • : Html->link(__('click here', true), array('controller' => 'pages', 'action' => 'display', 'doc', 'categories_and_types')));?>
  • -
  • :
  • -
  • : Html->link(__('administration', true), array('controller' => 'pages', 'action' => 'display', 'doc', 'administration', '#' => 'whitelist'))); ?>
  • -
  • :
  • -
-

-

-For users trying to populate an event, after clicking on the populate from template button, you\'ll be presented with a list of all currently accessible templates. Pick the one that best describes the event that you are creating.');?>

-<?php echo __('Template Choice');?>

-

-Templates are devided into sections, with each section having a title and a description in addition to a series of fields. Each field can be an attribute or a file attachment field. An attribute field has the following components');?>:

-<?php echo __('Template Field');?>

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

-<?php echo __('Freetext');?>

-

-<?php echo __('Freetext');?>

-

-

-

-<?php echo __('Attribute Replace Tool');?>

-

-

:

-:

-

<?php echo __('Add attachment');?>


-
    -
  • :
  • -
  • : click here.');?>
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-
-

- -

<?php echo __('Propose attribute');?>


- -
-

- -

<?php echo __('OpenIOC1');?>


-

<?php echo __('OpenIOC2');?>


-
-

-:
-
-

:

-

Publish


-

-
-
- -
-

:

-

-

:

-:

-<?php echo __('List events');?>

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • : :
  • -
  • -
      -
    • :
    • -
    • :
    • -
    • : /li> -
    • :
    • -
    -
  • -
  • : :
  • -
  • -
      -
    • :
    • -
    • :
    • -
    • :
    • -
    -
  • -
  • :
  • -
  • : here.');?>
  • -
  • : Html->link(__('click here', true), array('controller' => 'pages', 'action' => 'display', 'doc', 'administration', '#' => 'roles'))); ?> :
  • -
  • -
      -
    • : Html->link(__('section on connecting servers', true), array('controller' => 'pages', 'action' => 'display', 'doc', 'using_the_system', '#' => 'connect'))); ?>
    • -
    • : creating an event.');?>
    • -
    • :
    • -
    • : :

      -
    -
  • -
-

-

-

-<?php echo __('Event');?>

- -
    -
  • :
  • -
  • :
  • -
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-
-

-
-

-
-
-
-:
-
    -
  • -
  • -
-
-:
-
    -
  • :
  • -
  • : categories and types.', $baseurl);?>
  • -
  • : categories and types.', $baseurl);?>
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -

-:

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
-

-
-:

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-:

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
-
-

:

-

:

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-
-

:

- :

-

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

:

-

-<?php echo __('Search attribute');?>

-

-Html->link(__('click here', true), array('controller' => 'pages', 'action' => 'display', 'doc', 'categories_and_types')));?>

-
-

-
-

:

-browsing past events.');?>

-seen here, for an attribute the attribute screen as described here).');?>

-

-
-

:

-

:

-<?php echo __('Add tag');?>

-

:

-
    -
  • :
  • -
  • :
  • -
-
-

:

-



-
-

-<?php echo __('Create Template');?>

-

:

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

:

-

-<?php echo __('Template Attribute Element');?>

-

:

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

-<?php echo __('Template File Element');?>

-

:

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

-<?php echo __('Template Text Element');?>

-

:

-
    -
  • :
  • -
  • :
  • -
-
-

:

-

-

-


-
-
-

:

-

-automation.', $baseurl);?> -
-

:

-

-%s)', (Configure::read('MISP.background_jobs') == true ? __('On') : __('Off')));?>

-

-
-

-

-
-

-

-:

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-

-

-

-


-

-


-
-

:

-

-

:

-:

-

<?php echo __('Add server');?>

-
    -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-, trying to allow another instance to connect to your own, it is vital that two rules are followed when setting up a synchronisation account');?>:
-
    -
  • -
  • -
-

:

-

-


-
    -
  • : that is identical to the new instance view, is loaded, with all the current information of the instance pre-entered.');?>
  • -
  • :
  • -
  • :
  • -
  • :
  • -
-
-

:

-RESTfull, so this means that you can use structured format (XML or JSON) to access Events data.');?>

-

-

-

-:

-Authorization:
-Accept: application/xml
-Content-Type: application/xml

-:

-Authorization:
-Accept: application/json
-Content-Type: application/json

-:

- - ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
GET/eventsEventsController::index() (1)
GET/events/123EventsController::view(123) (2)
POST/eventsEventsController::add()
PUT/events/123EventsController::edit(123)
DELETE/events/123EventsController::delete(123)
POST/events/123EventsController::edit(123)
-index.');?>
-data tag.');?>
-

-

-

-


-:

-
GET /events/123
-

:

-
Accept: application/xml
-Authorization: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-

:

-
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<response>
-    <Event>
-        <id>57</id>
-        <org>NCIRC</org>
-        <date>2014-03-04</date>
-        <threat_level_id>1</threat_level_id>
-        <info></info>
-        <published>1</published>
-        <uuid>50aa54aa-f7a0-4d74-910d-10f0ff32448e</uuid>
-        <attribute_count>1</attribute_count>
-        <analysis>1</analysis>
-        <timestamp>1393327600</timestamp>
-        <distribution>1</distribution>
-        <proposal_email_lock>0</proposal_email_lock>
-        <orgc>Iglocska</orgc>
-        <locked>0</locked>
-        <publish_timestamp>1393327600</publish_timestamp>
-        <Attribute>
-            <id>9577</id>
-            <type>other</type>
-            <category></category>
-            <to_ids>1</to_ids>
-            <uuid>50aa54bd-adec-4544-b494-10f0ff32448e</uuid>
-            <event_id>57</event_id>
-            <distribution>1</distribution>
-            <timestamp>1393327600</timestamp>
-            <comment></comment>
-            <value></value>
-            <ShadowAttribute />
-        </Attribute>
-        <ShadowAttribute />
-        <RelatedEvent />
-    </Event>
-    <xml_version>2.2.0</xml_version>
-</response>
- -

-

-The request should be');?>:

-
POST /events
-Accept: application/xml
-Authorization: 
-

:

-
<Event>
-    <date>2014-03-04</date>
-    <threat_level_id>1</threat_level_id>
-    <info></info>
-    <published>1</published>
-    <analysis>1</analysis>
-    <distribution>1</distribution>
-    <Attribute>
-        <type>other</type>
-        <category>Artifacts dropped</category>
-        <to_ids>1</to_ids>
-        <distribution>1</distribution>
-        <comment></comment>
-        <value></value>
-    </Attribute>
-</Event>
-

:

-
-HTTP/1.1 100 Continue
-HTTP/1.1 200 Continue
-Date: Tue, 04-Mar-2014 15:00:00
-Server: Apache/2.2.22 (Ubuntu) PHP/5.4.9-4ubuntu2.3
-X-Powered-By: PHP/5.4.9-4ubuntu2.3
-Set-Cookie: CAKEPHP=deleted; expires=Wed, 05-Mar-2014 15:00:00 GMT; path=/
-Set-Cookie: CAKEPHP=a4ok3lr5p9n5drqj27025i4le3; expires Tue, 04-Mar-2014 15:00:00 GMT; path=/; HttpOnly
-Content-Length: 1 kB
-Content-Type: application/xml
-
-<?xml version="1.0" encoding="UTF-8">
-<response>
-    <Event>
-        <id>76</id>
-        <org>NCIRC</org>
-        <date>2014-03-04</date>
-        <threat_level_id>1</threat_level_id>
-        <info></info>
-        <published>1</published>
-        <uuid>50aa54aa-f7a0-4d74-920d-10f0ff32448e</uuid>
-        <attribute_count>1</attribute_count>
-        <analysis>1</analysis>
-        <timestamp>1393328991</timestamp>
-        <distribution>1</distribution>
-        <proposal_email_lock>0</proposal_email_lock>
-        <orgc>Iglocska</orgc>
-        <locked>0</locked>
-        <publish_timestamp>1393947960</publish_timestamp>
-        <Attribute>
-            <id>10462</id>
-            <type>other</type>
-            <category></category>
-            <to_ids>1</to_ids>
-            <uuid>50aa54bd-adec-4544-b412-10f0ff32448e</uuid>
-            <event_id>76</event_id>
-            <distribution>1</distribution>
-            <timestamp>1393328991</timestamp>
-            <comment/>
-            <value></value>
-            <ShadowAttribute/>
-        </Attribute>
-        <ShadowAttribute/>
-        <RelatedEvent>
-            <id>75</id>
-            <org>NCIRC</org>
-            <date>2012-11-19</date>
-            <info></info>
-            <uuid>50aa54aa-f7a0-4d74-910d-10f0ff32448e</uuid>
-            <published>1</published>
-            <analysis>1</analysis>
-            <attribute_count>1</attribute_count>
-            <orgc>Iglocska</orgc>
-            <timestamp>1393327600</timestamp>
-            <distribution>1</distribution>
-            <proposal_email_lock>0</proposal_email_lock>
-            <locked>0</locked>
-            <threat_level_id>1</threat_level_id>
-            <publish_timestamp>1393947655</publish_timestamp>
-        </RelatedEvent>
-    </Event>
-    <xml_version>2.2.0</xml_version>
-</response>
-
-

-
-<?xml version = "1.0" encoding = "UTF-8"?>
-<response>
-    <name>Not Found</name>
-    <url>/The_meaning_of_life</url>
-</response>
-
-
diff --git a/app/View/Regexp/index.ctp b/app/View/Regexp/index.ctp index 13d822f34..d4d124653 100755 --- a/app/View/Regexp/index.ctp +++ b/app/View/Regexp/index.ctp @@ -11,7 +11,7 @@
- + diff --git a/app/View/Servers/view_export_format_usage.ctp b/app/View/Servers/view_export_format_usage.ctp new file mode 100644 index 000000000..16a19bd09 --- /dev/null +++ b/app/View/Servers/view_export_format_usage.ctp @@ -0,0 +1,49 @@ + + + diff --git a/app/View/SharingGroups/view.ctp b/app/View/SharingGroups/view.ctp index fb08dbe2e..48dada082 100644 --- a/app/View/SharingGroups/view.ctp +++ b/app/View/SharingGroups/view.ctp @@ -50,7 +50,8 @@ echo $this->element( [ 'key' => __('Organisations'), 'type' => 'custom', - 'function' => function ($sharingGroup) { + 'requirement' => isset($sg['SharingGroupOrg']), + 'function' => function (array $sharingGroup) { echo sprintf( '
Paginator->sort('id');?>Paginator->sort('id', __('ID'));?> Paginator->sort('regexp', __('Regexp'));?> Paginator->sort('replacement', __('Replacement'));?> Paginator->sort('type');?>
@@ -77,7 +78,8 @@ echo $this->element( [ 'key' => __('Instances'), 'type' => 'custom', - 'function' => function ($sharingGroup) { + 'requirement' => isset($sg['SharingGroupServer']), + 'function' => function (array $sharingGroup) { echo sprintf( '
diff --git a/app/View/TagCollections/index.ctp b/app/View/TagCollections/index.ctp index 7d4d0195a..2a7cb31fd 100755 --- a/app/View/TagCollections/index.ctp +++ b/app/View/TagCollections/index.ctp @@ -4,7 +4,7 @@ 'alias' => __('Tag Collections'), 'controller' => 'tag_collections', 'headers' => array( - 'id' => array('sort' => 1), + 'id' => array('sort' => 1, 'alias' => __('ID')), 'uuid' => array('sort' => 1, 'alias' => __('UUID')), 'name' => array('sort' => 1), 'tags' => array('alias' => __('Tags')), diff --git a/app/View/TaxiiServers/add.ctp b/app/View/TaxiiServers/add.ctp new file mode 100644 index 000000000..b54cf799c --- /dev/null +++ b/app/View/TaxiiServers/add.ctp @@ -0,0 +1,79 @@ +request->params['action'] === 'edit' ? true : false; +$fields = [ + [ + 'field' => 'name', + 'class' => 'span6' + ], + [ + 'field' => 'owner', + 'class' => 'span6' + ], + [ + 'field' => 'baseurl', + 'class' => 'span6' + ], + [ + 'field' => 'api_key', + 'label' => 'API Key', + 'type' => 'text', + 'class' => 'input span6' + ], + [ + 'field' => 'api_root', + 'class' => 'span6', + 'type' => 'dropdown', + 'options' => [], + 'populateAction' => json_encode([ + 'uri' => '/taxii_servers/getRoot', + 'body' => [ + 'baseurl' => '{{#TaxiiServerBaseurl}}', + 'api_key' => '{{#TaxiiServerApiKey}}' + ], + 'type' => 'POST' + ]) + ], + [ + 'field' => 'collection', + 'class' => 'span6', + 'type' => 'dropdown', + 'options' => [], + 'populateAction' => json_encode([ + 'uri' => '/taxii_servers/getCollections', + 'body' => [ + 'baseurl' => '{{#TaxiiServerBaseurl}}', + 'api_key' => '{{#TaxiiServerApiKey}}', + 'api_root' => '{{#TaxiiServerApiRoot}}' + ], + 'type' => 'POST' + ]) + ], + [ + 'field' => 'description', + 'type' => 'textarea', + 'class' => 'input span6' + ], + [ + 'field' => 'filters', + 'label' => 'Filter Rules (restsearch JSON)', + 'type' => 'textarea', + 'class' => 'input span6' + ] +]; +echo $this->element('genericElements/Form/genericForm', [ + 'data' => [ + 'description' => false, + 'model' => 'TaxiiServer', + 'title' => $edit ? __('Edit TAXII Server connection') : __('Add TAXII Server connection'), + 'fields' => $fields, + 'submit' => [ + 'action' => $this->request->params['action'], + 'ajaxSubmit' => 'submitGenericFormInPlace();' + ] + ] +]); + +if (!$ajax) { + echo $this->element('/genericElements/SideMenu/side_menu', $menuData); +} diff --git a/app/View/TaxiiServers/collections_index.ctp b/app/View/TaxiiServers/collections_index.ctp new file mode 100644 index 000000000..2fad3a43a --- /dev/null +++ b/app/View/TaxiiServers/collections_index.ctp @@ -0,0 +1,59 @@ +element('genericElements/IndexTable/scaffold', [ + 'skip_pagination' => 1, + 'scaffold_data' => [ + 'data' => [ + + 'data' => $data, + 'top_bar' => [ + 'pull' => 'right', + 'children' => [ + ] + ], + 'fields' => [ + [ + 'name' => __('Id'), + 'sort' => 'id', + 'data_path' => 'id' + ], + [ + 'name' => __('Title'), + 'sort' => 'title', + 'data_path' => 'title' + ], + [ + 'name' => __('Description'), + 'data_path' => 'description' + ], + [ + 'name' => __('Writeable'), + 'sort' => 'can_write', + 'element' => 'boolean', + 'data_path' => 'can_write' + ], + [ + 'name' => __('Readable'), + 'sort' => 'can_read', + 'element' => 'boolean', + 'data_path' => 'can_read' + ], + [ + 'name' => __('Media types'), + 'element' => 'list', + 'data_path' => 'media_types' + ] + ], + 'title' => empty($ajax) ? __('The collections found on TaxiiServer #%s.', h($id)) : false, + 'description' => false, + 'actions' => [ + [ + 'url' => $baseurl . '/taxiiServers/objectsIndex/' . h($id) . '/%s/', + 'url_replace' => ['id'], + 'icon' => 'eye' + ] + ] + ] + ] + ]); + +?> \ No newline at end of file diff --git a/app/View/TaxiiServers/index.ctp b/app/View/TaxiiServers/index.ctp new file mode 100644 index 000000000..088b69db7 --- /dev/null +++ b/app/View/TaxiiServers/index.ctp @@ -0,0 +1,117 @@ +element('genericElements/IndexTable/scaffold', [ + 'scaffold_data' => [ + 'data' => [ + 'data' => $data, + 'top_bar' => [ + 'pull' => 'right', + 'children' => [ + [ + 'type' => 'simple', + 'children' => [ + 'data' => [ + 'type' => 'simple', + 'text' => __('Add TAXII Server'), + 'class' => 'btn btn-primary', + 'onClick' => 'openGenericModal', + 'onClickParams' => [ + sprintf( + '%s/taxiiServers/add', + $baseurl + ) + ] + ] + ] + ], + [ + 'type' => 'search', + 'button' => __('Filter'), + 'placeholder' => __('Enter value to search'), + 'data' => '', + 'searchKey' => 'quickFilter' + ] + ] + ], + 'fields' => [ + [ + 'name' => __('Id'), + 'sort' => 'TaxiiServer.id', + 'data_path' => 'TaxiiServer.id' + ], + [ + 'name' => __('Name'), + 'sort' => 'TaxiiServer.name', + 'data_path' => 'TaxiiServer.name' + ], + [ + 'name' => __('Baseurl'), + 'sort' => 'TaxiiServer.baseurl', + 'data_path' => 'TaxiiServer.baseurl' + ], + [ + 'name' => __('API root'), + 'sort' => 'TaxiiServer.api_root', + 'data_path' => 'TaxiiServer.api_root' + ], + [ + 'name' => __('Collection'), + 'sort' => 'TaxiiServer.collection', + 'data_path' => 'TaxiiServer.collection' + ], + [ + 'name' => __('Filters'), + 'sort' => 'TaxiiServer.filters', + 'data_path' => 'TaxiiServer.filters', + 'type' => 'json' + ], + [ + 'name' => __('api_key'), + 'sort' => 'TaxiiServer.api_key', + 'data_path' => 'TaxiiServer.api_key' + ], + [ + 'name' => __('Description'), + 'sort' => 'TaxiiServer.description', + 'data_path' => 'TaxiiServer.description' + ] + ], + 'title' => empty($ajax) ? __('Linked TAXII Servers') : false, + 'description' => empty($ajax) ? __('You can connect your MISP to one or several TAXII servers to push data to using a set of filters.') : false, + 'actions' => [ + [ + 'url' => $baseurl . '/taxiiServers/view', + 'url_params_data_paths' => ['TaxiiServer.id'], + 'icon' => 'eye' + ], + [ + 'onclick' => sprintf( + 'openGenericModal(\'%s/taxiiServers/push/[onclick_params_data_path]\');', + $baseurl + ), + 'onclick_params_data_path' => 'TaxiiServer.id', + 'title' => __('Pull all filtered data to TAXII server'), + 'icon' => 'upload' + ], + [ + 'onclick' => sprintf( + 'openGenericModal(\'%s/taxiiServers/edit/[onclick_params_data_path]\');', + $baseurl + ), + 'onclick_params_data_path' => 'TaxiiServer.id', + 'title' => __('Edit TAXII server configuration'), + 'icon' => 'edit' + ], + [ + 'onclick' => sprintf( + 'openGenericModal(\'%s/taxiiServers/delete/[onclick_params_data_path]\');', + $baseurl + ), + 'onclick_params_data_path' => 'TaxiiServer.id', + 'icon' => 'trash' + ] + ] + ] + ] + ]); + +?> diff --git a/app/View/TaxiiServers/object_view.ctp b/app/View/TaxiiServers/object_view.ctp new file mode 100644 index 000000000..3b5d29d69 --- /dev/null +++ b/app/View/TaxiiServers/object_view.ctp @@ -0,0 +1,14 @@ +%s', + $randomId, + json_encode($data) + );f +?> + + \ No newline at end of file diff --git a/app/View/TaxiiServers/objects_index.ctp b/app/View/TaxiiServers/objects_index.ctp new file mode 100644 index 000000000..b749ba7cd --- /dev/null +++ b/app/View/TaxiiServers/objects_index.ctp @@ -0,0 +1,69 @@ +element('genericElements/IndexTable/scaffold', [ + 'scaffold_data' => [ + 'data' => [ + 'skip_pagination' => 1, + 'data' => $data, + 'top_bar' => [ + 'pull' => 'right', + 'children' => [ + [ + 'type' => 'simple', + 'children' => [ + 'data' => [ + 'type' => 'simple', + 'text' => __('Next page'), + 'class' => 'btn btn-primary', + 'url' => $next_url, + 'requirement' => $more + ] + ] + ], + ] + ], + 'fields' => [ + [ + 'name' => __('Id'), + 'data_path' => 'id' + ], + [ + 'name' => __('Type'), + 'data_path' => 'type' + ], + [ + 'name' => __('Created'), + 'data_path' => 'created' + ], + [ + 'name' => __('Modified'), + 'data_path' => 'modified' + ], + [ + 'name' => __('Labels'), + 'element' => 'list', + 'data_path' => 'labels' + ], + [ + 'name' => __('STIX version'), + 'data_path' => 'spec_version' + ] + ], + 'title' => empty($ajax) ? __('Objects found in Collection %s on TaxiiServer #%s', h($collection_id), h($id)) : false, + 'description' => false, + 'actions' => [ + [ + 'onclick' => 'openGenericModal(\'' . h($baseurl) . '/taxiiServers/objectView/' . h($id) . '/' . h($collection_id) . '/%s\');', + 'onclick_replace' => ['id'], + 'title' => __('View raw STIX object'), + 'icon' => 'eye' + ] + ] + ] + ] + ]); + +?> diff --git a/app/View/TaxiiServers/view.ctp b/app/View/TaxiiServers/view.ctp new file mode 100644 index 000000000..666011694 --- /dev/null +++ b/app/View/TaxiiServers/view.ctp @@ -0,0 +1,72 @@ +element( + 'genericElements/SingleViews/single_view', + [ + 'title' => 'Taxii Server view', + 'data' => $data, + 'fields' => [ + [ + 'key' => __('Id'), + 'path' => 'TaxiiServer.id' + ], + [ + 'key' => __('Name'), + 'path' => 'TaxiiServer.name' + ], + [ + 'key' => __('Owner'), + 'path' => 'TaxiiServer.owner' + ], + [ + 'key' => __('Base URL'), + 'path' => 'TaxiiServer.baseurl' + ], + [ + 'key' => __('API Root'), + 'path' => 'TaxiiServer.api_root' + ], + [ + 'key' => __('Selected Collection'), + 'path' => 'TaxiiServer.collection' + ], + [ + 'key' => __('Description'), + 'path' => 'TaxiiServer.description' + ], + [ + 'key' => __('Filters'), + 'path' => 'TaxiiServer.filters', + 'type' => 'json' + ], + [ + 'key' => __('Owner Organisation'), + 'path' => 'TaxiiServer.Cerebrate.org_id', + 'pathName' => 'Organisation.name', + 'type' => 'model', + 'model' => 'organisations' + ], + [ + 'key' => __('API key'), + 'path' => 'TaxiiServer.api_key' + ], + [ + 'key' => __('Description'), + 'path' => 'TaxiiServer.Cerebrate.description' + ], + ], + 'children' => [ + [ + 'url' => '/taxii_servers/collectionsIndex/{{0}}/', + 'url_params' => ['TaxiiServer.id'], + 'title' => __('Collections'), + 'elementId' => 'taxii_collections' + ], + [ + 'url' => '/taxii_servers/objectsIndex/{{0}}/{{1}}/', + 'url_params' => ['TaxiiServer.id', 'TaxiiServer.collection'], + 'title' => __('Objects in selected Collection'), + 'elementId' => 'taxii_objects' + ], + ] + ] +); diff --git a/app/View/Taxonomies/ajax/taxonomy_tags.ctp b/app/View/Taxonomies/ajax/taxonomy_tags.ctp index 2b92d3fad..e7b422af3 100644 --- a/app/View/Taxonomies/ajax/taxonomy_tags.ctp +++ b/app/View/Taxonomies/ajax/taxonomy_tags.ctp @@ -71,7 +71,9 @@ $actions = [ 'icon' => 'share-alt', 'url' => $baseurl . '/tags/viewGraph', 'url_params_data_paths' => ['existing_tag.Tag.id'], - 'postLinkConfirm' => __('Are you sure you want to create this tag?'), + 'complex_requirement' => function ($row) { + return $row['existing_tag']; + }, 'requirement' => $isAclTagger && $taxonomy['enabled'], ], [ diff --git a/app/View/Taxonomies/ajax/toggle_highlighted.ctp b/app/View/Taxonomies/ajax/toggle_highlighted.ctp new file mode 100644 index 000000000..a17c30252 --- /dev/null +++ b/app/View/Taxonomies/ajax/toggle_highlighted.ctp @@ -0,0 +1,15 @@ +Form->create('Taxonomy', array( + 'id' => 'HighlightedCheckboxForm' . h($id), + 'label' => false, + 'style' => 'display:none;', + 'url' => $baseurl . '/taxonomies/toggleHighlighted/' . $id + )); + echo $this->Form->checkbox('highlighted', array( + 'checked' => $highlighted, + 'label' => false, + 'disabled' => !$isSiteAdmin, + 'class' => 'highlighted-toggle' + )); + echo $this->Form->end(); +?> diff --git a/app/View/Taxonomies/index.ctp b/app/View/Taxonomies/index.ctp index f2aefb7f0..b85fe5790 100644 --- a/app/View/Taxonomies/index.ctp +++ b/app/View/Taxonomies/index.ctp @@ -75,6 +75,18 @@ 'data_path' => 'Taxonomy.required', 'disabled' => !$isSiteAdmin, ), + array( + 'name' => __('Highlighted'), + 'element' => 'toggle', + 'url' => $baseurl . '/taxonomies/toggleHighlighted', + 'url_params_data_paths' => array( + 'Taxonomy.id' + ), + 'sort' => 'highlighted', + 'class' => 'short', + 'data_path' => 'Taxonomy.highlighted', + 'disabled' => !$isSiteAdmin, + ), array( 'name' => __('Active Tags'), 'element' => 'custom', diff --git a/app/View/Taxonomies/view.ctp b/app/View/Taxonomies/view.ctp index e569aa323..5a5cc786f 100644 --- a/app/View/Taxonomies/view.ctp +++ b/app/View/Taxonomies/view.ctp @@ -46,6 +46,11 @@ echo $this->element( 'path' => 'enabled', 'type' => 'boolean' ], + [ + 'key' => __('Highlighted'), + 'path' => 'highlighted', + 'type' => 'boolean' + ], [ 'key' => __('Action'), 'type' => 'custom', diff --git a/app/View/Templates/upload_file.ctp b/app/View/Templates/upload_file.ctp index 77d68182a..315b50be6 100644 --- a/app/View/Templates/upload_file.ctp +++ b/app/View/Templates/upload_file.ctp @@ -6,7 +6,7 @@ if ($batch == 'yes') { $multiple = false; if (isset($filenames)) { $buttonText = __('Replace File'); - } else { + } else { $buttonText = __('Upload File'); } } @@ -18,13 +18,13 @@ if ($batch == 'yes') { echo $this->Form->end(); ?> - + +
+ + + +
+

+
+ + + + + +
+
+ %s', + __('You have been successfully logged out.') + ); + ?> +
+
diff --git a/app/View/Users/otp.ctp b/app/View/Users/otp.ctp new file mode 100644 index 000000000..019b28373 --- /dev/null +++ b/app/View/Users/otp.ctp @@ -0,0 +1,29 @@ +Flash->render(); ?> + +
+
+

+
+
+ +element('/genericElements/Form/genericForm', array( + "form" => $this->Form, + "data" => array( + "title" => __("Validate your One Time Password"), + "fields" => array( + array( + "field" => "otp", + "label" => $label, + "type" => "text", + "placeholder" => __("Enter your OTP here"), + "autofocus" => 1 + ) + ), + "submit" => array ( + "action" => "otp", + ), +))); +?> diff --git a/app/View/Users/password_reset.ctp b/app/View/Users/password_reset.ctp new file mode 100644 index 000000000..90fc5647e --- /dev/null +++ b/app/View/Users/password_reset.ctp @@ -0,0 +1,25 @@ +
+ Form->create('User');?> +
+ + ' . __('Minimal length') . ': ' . h($length) . '
'; + $passwordPopover .= '' . __('Complexity') . ': ' . h($complexity); + echo $this->Form->input('password', array( + 'label' => __('New password') . ' ', 'autofocus' + )); + echo $this->Form->input('confirm_password', [ + 'type' => 'password', + 'label' => __('Confirm new password'), + 'div' => array('class' => 'input password required'), + ]); + ?> +
+
 
+
+
+Form->button(__('Submit'), array('class' => 'btn btn-primary')); +echo $this->Form->end(); +?> +
diff --git a/app/View/Users/totp_new.ctp b/app/View/Users/totp_new.ctp new file mode 100644 index 000000000..9293fba79 --- /dev/null +++ b/app/View/Users/totp_new.ctp @@ -0,0 +1,38 @@ +Flash->render(); ?> +" . $secret . ""; + +echo $this->element('/genericElements/Form/genericForm', array( + "form" => $this->Form, + "data" => array( + "title" => __("Validate your One Time Password"), + "fields" => array( + array( + "type" => 'html', + "field" => "html", + "html" => $detailsHtml + ), + array( + "type" => 'html', + "field" => 'qrcode', + "html" => $qrcode + ), + array( + "type" => 'html', + "field" => "secret", + "html" => $secretHtml + ), + array( + "field" => "otp", + "label" => __("One Time Password verification"), + "type" => "text", + "placeholder" => __("Enter your OTP code here"), + ) + ), + "submit" => array ( + "action" => "totp", + ), +))); +?> +
diff --git a/app/View/Users/view.ctp b/app/View/Users/view.ctp index 158e937a9..cf3b2f546 100755 --- a/app/View/Users/view.ctp +++ b/app/View/Users/view.ctp @@ -1,32 +1,70 @@ '; - foreach ($periodic_notifications as $periodic_notification) { - $active_html = $this->element('genericElements/IndexTable/Fields/boolean', [ - 'row' => ['active' => !empty($user['User'][$periodic_notification])], - 'field' => ['data_path' => 'active', 'colors' => true, ], - ]); - $periodic_notification_settings_html .= sprintf('%s%s', Inflector::humanize($periodic_notification), $active_html); - } - $periodic_notification_settings_html .= ''; - $table_data = array(); - $table_data[] = array('key' => __('ID'), 'value' => $user['User']['id']); - $table_data[] = array( - 'key' => __('Email'), - 'html' => h($user['User']['email']) . ($admin_view ? sprintf( - ' ', - $baseurl, - h($user['User']['id']), - __('Send email to user') - ) : ''), + +$notificationTypes = [ + 'autoalert' => __('Event published notification'), + 'notification_daily' => __('Daily notifications'), + 'notification_weekly' => __('Weekly notifications'), + 'notification_monthly' => __('Monthly notifications'), +]; + +$notificationsHtml = ''; +foreach ($notificationTypes as $notificationType => $description) { + $isEnabled = !empty($user['User'][$notificationType]); + $boolean = sprintf( + '%s', + $isEnabled ? 'label label-success label-padding' : 'label label-important label-padding', + $isEnabled ? __('Yes') : __('No')); + $notificationsHtml .= ''; +} +$notificationsHtml .= '
' . $description . '' . $boolean . '
'; + +$isTotp = isset($user['User']['totp']) ? true : false; +$boolean = sprintf( +'%s', + $isTotp ? 'label label-success label-padding' : 'label label-important label-padding', +$isTotp ? __('Yes') : __('No')); +$totpHtml = $boolean; +$totpHtml .= (!$isTotp && !$admin_view ? $this->Html->link(__('Generate'), array('action' => 'totp_new')) : ''); +$totpHtml .= ($isTotp && !$admin_view ? $this->Html->link(__('View paper tokens'), array('action' => 'hotp', $user['User']['id'])): ''); + +if ($admin_view && $isSiteAdmin && $isTotp) { + $totpHtml .= sprintf( + '%s', + h($baseurl), + h($user['User']['id']), + __('Delete') ); - $table_data[] = array( - 'key' => __('Organisation'), - 'html' => $this->OrgImg->getNameWithImg($user), - ); - $table_data[] = array('key' => __('Role'), 'html' => $this->Html->link($user['Role']['name'], array('controller' => 'roles', 'action' => 'view', $user['Role']['id']))); - $table_data[] = array('key' => __('Event alert enabled'), 'boolean' => $user['User']['autoalert']); - $table_data[] = ['key' => __('Periodic Notifications'), 'html' => $periodic_notification_settings_html]; - $table_data[] = array('key' => __('Contact alert enabled'), 'boolean' => $user['User']['contactalert']); +} + $table_data = [ + array('key' => __('ID'), 'value' => $user['User']['id']), + array( + 'key' => __('Email'), + 'html' => h($user['User']['email']) . ($admin_view ? sprintf( + ' ', + $baseurl, + h($user['User']['id']), + __('Send email to user') + ) : ''), + ), + array( + 'key' => __('Organisation'), + 'html' => $this->OrgImg->getNameWithImg($user), + ), + array( + 'key' => __('Role'), + 'html' => $this->Html->link($user['Role']['name'], array('controller' => 'roles', 'action' => 'view', $user['Role']['id'])), + ), + // array('key' => __('TOTP'), 'boolean' => isset($user['User']['totp']) ? true : false), + array( + 'key' => __('TOTP'), + 'html' => $totpHtml + ), + array( + 'key' => __('Email notifications'), + 'html' => $notificationsHtml, + ), + array('key' => __('Contact alert enabled'), 'boolean' => $user['User']['contactalert']) + ]; if (!$admin_view && !$user['Role']['perm_auth']) { $table_data[] = array( @@ -86,12 +124,12 @@ $table_data[] = array('key' => __('Terms accepted'), 'boolean' => $user['User']['termsaccepted']); $table_data[] = array('key' => __('Must change password'), 'boolean' => $user['User']['change_pw']); } - $table_data[] = array( - 'key' => __('PGP key'), - 'element' => 'genericElements/key', - 'element_params' => array('key' => $user['User']['gpgkey']), - ); if (!empty($user['User']['gpgkey'])) { + $table_data[] = array( + 'key' => __('PGP key'), + 'element' => 'genericElements/key', + 'element_params' => array('key' => $user['User']['gpgkey']), + ); $table_data[] = array( 'key' => __('PGP key fingerprint'), 'value_class' => 'quickSelect', @@ -102,6 +140,11 @@ 'value_class' => (empty($user['User']['pgp_status']) || $user['User']['pgp_status'] !== 'OK') ? 'red': '', 'value' => !empty($user['User']['pgp_status']) ? $user['User']['pgp_status'] : 'N/A' ); + } else { + $table_data[] = array( + 'key' => __('PGP key'), + 'boolean' => false, + ); } if (Configure::read('SMIME.enabled')) { $table_data[] = array( @@ -130,7 +173,7 @@ 'js' => array('vis', 'jquery-ui.min', 'network-distribution-graph') )); echo sprintf( - '
%s
%s
%s%s
', + '
%s
%s%s
%s%s
', sprintf( '

%s

%s', __('User %s', h($user['User']['email'])), @@ -145,6 +188,14 @@ ), __('Download user profile for data portability') ), + sprintf( + ' %s', + sprintf( + '%s/logs/index', + $baseurl + ), + __('Review user logs') + ), $me['Role']['perm_auth'] ? $this->element('/genericElements/accordion', array('title' => __('Auth keys'), 'url' => '/auth_keys/index/' . h($user['User']['id']))) : '', $this->element('/genericElements/accordion', array('title' => 'Events', 'url' => '/events/index/searchemail:' . urlencode(h($user['User']['email'])))) ); diff --git a/app/View/Warninglists/view.ctp b/app/View/Warninglists/view.ctp index abd701275..c51b159be 100644 --- a/app/View/Warninglists/view.ctp +++ b/app/View/Warninglists/view.ctp @@ -12,14 +12,14 @@ array( 'key' => __('Enabled'), 'boolean' => $data['enabled'], - 'html' => sprintf( + 'html' => $me['Role']['perm_warninglist'] ? sprintf( ' %s', $baseurl, h($warninglist['Warninglist']['id']), $data['enabled'] ? '' : '/1', $data['enabled'] ? __('Disable') : __('Enable'), $data['enabled'] ? __('Disable') : __('Enable') - ) + ): '', ), ); @@ -37,7 +37,7 @@ '
%s

%s

', sprintf( '

%s

%s', - h(mb_strtoupper($warninglist['Warninglist']['name'])), + h($warninglist['Warninglist']['name']), $this->element('genericElements/viewMetaTable', array('table_data' => $table_data)) ), __('Values') diff --git a/app/View/Workflows/editor.ctp b/app/View/Workflows/editor.ctp index d5aec5529..6d1ad3ac9 100644 --- a/app/View/Workflows/editor.ctp +++ b/app/View/Workflows/editor.ctp @@ -71,6 +71,12 @@ $debugEnabled = !empty($selectedWorkflow['Workflow']['debug_enabled']);