From 30fd4a974ae49d050a17e5fee3c7e7cdda3852a7 Mon Sep 17 00:00:00 2001 From: git workflows Date: Fri, 8 Nov 2024 16:11:38 +0000 Subject: [PATCH 01/21] not-ready for CI/CD --- .github/CHANGELOG.md | 475 +- .github/CODE_OF_CONDUCT.md | 22 +- .github/CONTRIBUTING.md | 2 +- .github/FUNDING.yml | 3 +- .github/ISSUE_TEMPLATE/issue_template.md | 8 +- .github/ISSUE_TEMPLATE/new_analyzer.md | 6 +- .github/ISSUE_TEMPLATE/new_connector.md | 6 +- .github/ISSUE_TEMPLATE/new_ingestor.md | 5 +- .github/ISSUE_TEMPLATE/new_playbook.md | 8 +- .github/ISSUE_TEMPLATE/new_visualizer.md | 6 +- .github/SECURITY.md | 9 +- .github/pull_request_template.md | 34 +- .github/release_template.md | 3 +- .github/workflows/mirror.yml | 16 - .github/workflows/pull_request_automation.yml | 10 +- .gitignore | 3 + README.md | 66 +- api_app/admin.py | 25 + api_app/analyzers_manager/constants.py | 12 +- .../file_analyzers/androguard.py | 35 + .../file_analyzers/artifacts.py | 13 +- .../file_analyzers/boxjs_scan.py | 35 +- .../file_analyzers/doc_info.py | 196 +- .../file_analyzers/droidlysis.py | 2 +- .../file_analyzers/lnk_info.py | 37 + .../file_analyzers/mwdb_scan.py | 2 +- .../file_analyzers/onenote.py | 10 + .../file_analyzers/pdf_info.py | 9 +- .../file_analyzers/strings_info.py | 49 + ...config_not_supported_filetypes_and_more.py | 180 + .../0121_analyzer_config_lnk_info.py | 120 + .../migrations/0122_alter_soft_time_limit.py | 34 + .../0123_basic_observable_analyzer.py | 87 + .../0124_analyzer_config_androguard.py | 129 + .../migrations/0125_update_yara_repo.py | 40 + .../0126_analyzer_config_nerd_analyzer.py | 163 + .../0127_analyzer_config_dshield.py | 124 + api_app/analyzers_manager/models.py | 1 + .../basic_observable_analyzer.py | 105 + .../download_file_from_uri.py | 6 +- .../observable_analyzers/dshield.py | 53 + .../observable_analyzers/intelx.py | 4 +- .../observable_analyzers/nerd.py | 68 + .../observable_analyzers/spyse.py | 2 +- .../observable_analyzers/urlscan.py | 5 +- .../observable_analyzers/vt/vt3_base.py | 449 -- .../observable_analyzers/vt/vt3_get.py | 7 +- .../vt/vt3_intelligence_search.py | 183 +- api_app/analyzers_manager/serializers.py | 43 + api_app/analyzers_manager/views.py | 19 +- api_app/classes.py | 9 +- .../connectors/abuse_submitter.py | 6 + .../connectors/email_sender.py | 2 +- .../connectors_manager/connectors/opencti.py | 12 +- api_app/documents.py | 12 +- api_app/exceptions.py | 8 +- api_app/ingestors_manager/classes.py | 5 + .../ingestors/malware_bazaar.py | 2 +- .../ingestors/virus_total.py | 316 ++ ...ngestor_config_virustotal_example_query.py | 272 ++ api_app/ingestors_manager/serializers.py | 21 +- api_app/management/commands/dumpplugin.py | 11 +- .../management/commands/elastic_templates.py | 39 + .../0063_singleton_and_elastic_report.py | 39 + api_app/mixins.py | 585 +++ api_app/models.py | 44 + api_app/permissions.py | 10 + ...0033_pivot_config_extractedonenotefiles.py | 149 + ...ubmitdownloadedfile_playbook_to_execute.py | 25 + api_app/pivots_manager/permissions.py | 10 + api_app/pivots_manager/pivots/any_compare.py | 17 +- api_app/pivots_manager/pivots/compare.py | 25 +- api_app/pivots_manager/pivots/load_file.py | 13 +- api_app/pivots_manager/serializers.py | 71 +- api_app/pivots_manager/signals.py | 17 + api_app/pivots_manager/views.py | 40 +- .../0051_add_lnk_info_analyzer_free_to_use.py | 34 + .../migrations/0052_playbook_config_uris.py | 118 + ...add_androguard_to_free_to_use_analyzers.py | 34 + api_app/playbooks_manager/serializers.py | 16 +- api_app/playbooks_manager/signals.py | 2 +- api_app/playbooks_manager/views.py | 5 +- api_app/queryset.py | 4 +- api_app/serializers/elastic.py | 67 + api_app/serializers/job.py | 3 +- api_app/serializers/plugin.py | 11 +- api_app/signals.py | 24 +- api_app/urls.py | 2 + api_app/views.py | 126 +- .../passive_dns/analyzer_extractor.py | 29 +- .../templates/authentication/emails/base.html | 6 +- .../emails/duplicate-email.html | 11 +- .../authentication/emails/reset-password.html | 11 +- .../authentication/emails/verify-email.html | 24 +- .../plugin_report.json | 45 + .../threat_matrix_bi.json | 4 +- configuration/ldap_config.py | 2 +- create_elastic_certs | 20 + docker/Dockerfile | 2 - docker/ci.override.yml | 17 +- docker/default.yml | 4 +- docker/elasticsearch.override.yml | 33 +- docker/entrypoints/celery_default.sh | 11 +- docker/entrypoints/celery_ingestor.sh | 10 +- docker/entrypoints/celery_local.sh | 8 +- docker/entrypoints/celery_long.sh | 9 +- docker/entrypoints/uwsgi.sh | 3 + docker/env_file_app_ci | 1 + docker/env_file_app_template | 1 + docker/env_file_elasticsearch_template | 1 + docker/flower.override.yml | 2 +- docker/postgres.override.yml | 5 +- docker/redis.override.yml | 2 +- docker/test.flower.override.yml | 2 +- docker/test.multi-queue.override.yml | 2 +- docker/test.override.yml | 2 +- docker/traefik_local.yml | 10 +- docker/traefik_prod.yml | 28 +- elasticsearch_instances.yml | 2 + frontend/.prettierignore | 2 + frontend/README.md | 2 +- frontend/package-lock.json | 4118 +++++++++-------- frontend/package.json | 42 +- frontend/src/components/GuideWrapper.jsx | 4 +- .../common/form/ScanConfigSelectInput.jsx | 89 + .../components/common/form/TLPSelectInput.jsx | 91 + .../utils => common/form}/TagSelectInput.jsx | 2 +- .../form/pluginsMultiSelectDropdownInput.jsx | 343 ++ .../common/form/runtimeConfigurationInput.jsx | 257 + .../investigations/flow/CustomJobNode.jsx | 7 +- .../components/investigations/flow/utils.js | 1 + .../table/investigationTableColumns.jsx | 21 +- .../src/components/jobs/notifications.jsx | 11 +- .../jobs/result/bar/JobActionBar.jsx | 34 +- .../jobs/result/bar/SaveAsPlaybooksForm.jsx | 20 +- .../components/jobs/result/bar/jobBarApi.jsx | 42 - .../src/components/jobs/result/jobApi.jsx | 27 + .../components/jobs/table/jobTableColumns.jsx | 18 +- .../src/components/organization/MyOrgPage.jsx | 8 +- .../src/components/organization/OrgConfig.jsx | 10 +- .../components/plugins/PluginsContainer.jsx | 131 +- .../plugins/forms/AnalyzerConfigForm.jsx | 615 +++ .../plugins/forms/PivotConfigForm.jsx | 442 ++ .../plugins/forms/PlaybookConfigForm.jsx | 443 ++ .../src/components/plugins/pluginsApi.jsx | 76 + .../src/components/plugins/types/Pivots.jsx | 2 +- .../plugins/types/PluginWrapper.jsx | 6 +- .../plugins/types/pluginActionsButtons.jsx | 214 +- .../plugins/types/pluginTableColumns.jsx | 38 +- frontend/src/components/scan/ScanForm.jsx | 420 +- frontend/src/components/scan/scanApi.jsx | 8 +- .../scan/utils/MultipleObservablesModal.jsx | 2 +- .../scan/utils/RuntimeConfigurationModal.jsx | 255 +- .../src/components/user/config/PluginData.jsx | 13 +- .../src/components/user/token/TokenAccess.jsx | 5 +- .../src/components/user/token/TokenPage.jsx | 4 +- frontend/src/constants/environment.js | 2 +- frontend/src/constants/miscConst.js | 8 + frontend/src/constants/pluginConst.js | 9 + frontend/src/layouts/AppHeader.jsx | 108 +- frontend/src/layouts/widgets/UserMenu.jsx | 6 +- frontend/src/stores/useOrganizationStore.jsx | 6 +- .../stores/usePluginConfigurationStore.jsx | 13 - frontend/src/utils/api.jsx | 10 +- frontend/src/utils/observables.js | 28 +- .../flow/InvestigationFlow.test.jsx | 56 +- .../jobs/result/utils/JobActionBar.test.jsx | 118 +- .../plugins/PluginsContainers.test.jsx | 18 + .../plugins/types/Ingestors.test.jsx | 2 +- .../types/forms/AnalyzerConfigForm.test.jsx | 386 ++ .../types/forms/PivotConfigForm.test.jsx | 296 ++ .../types/forms/PlaybookConfigForm.test.jsx | 418 ++ .../types/pluginActionsButtons.test.jsx | 101 +- .../scan/ScanForm/ScanForm.advanced.test.jsx | 16 + .../requests/ScanForm.observable.test.jsx | 2 +- frontend/tests/layouts/AppHeader.test.jsx | 209 + frontend/tests/mock.js | 23 +- frontend/tests/utils/observables.test.js | 53 +- integrations/cyberchef/compose-tests.yml | 2 +- integrations/cyberchef/compose.yml | 4 +- .../malware_tools_analyzers/Dockerfile | 8 +- integrations/malware_tools_analyzers/app.py | 15 +- .../malware_tools_analyzers/compose-tests.yml | 2 +- .../malware_tools_analyzers/compose.yml | 1 + integrations/phoneinfoga/compose.yml | 26 +- integrations/tor_analyzers/compose-tests.yml | 2 +- integrations/tor_analyzers/compose.yml | 2 +- requirements/project-requirements.txt | 19 +- requirements/test-requirements.txt | 4 +- start | 3 +- static/Certego.png | Bin 0 -> 53893 bytes static/greedybear.png | Bin 0 -> 296154 bytes static/gsoc_logo.png | Bin 0 -> 83945 bytes static/honeynet_logo.png | Bin 0 -> 20337 bytes static/threat_matrix_negative.png | Bin 0 -> 73684 bytes static/threat_matrix_positive.png | Bin 0 -> 74242 bytes static/threathunter_logo.png | Bin 0 -> 8695 bytes tests/__init__.py | 33 +- .../file_analyzers/__init__.py | 0 .../file_analyzers/test_boxjs.py | 37 + .../file_analyzers/test_doc_info.py | 112 + .../file_analyzers/test_iocextract.py | 33 + .../file_analyzers/test_lnk_info.py | 29 + .../file_analyzers/test_onenote_info.py | 33 + .../file_analyzers/test_pdf_info.py | 34 + .../file_analyzers/test_strings_info.py | 40 + .../api_app/analyzers_manager/test_classes.py | 15 +- tests/api_app/analyzers_manager/test_views.py | 155 +- .../pivots_manager/test_serializers.py | 37 + tests/api_app/pivots_manager/test_views.py | 149 +- tests/api_app/test_api.py | 211 + tests/api_app/test_mixins.py | 191 + tests/auth/test_auth.py | 4 +- tests/test_files.zip | Bin 17107334 -> 5943103 bytes tests/threat_matrix/__init__.py | 0 tests/threat_matrix/test_tasks.py | 397 ++ threat_matrix/celery.py | 9 + threat_matrix/secrets.py | 54 +- threat_matrix/settings/__init__.py | 1 + threat_matrix/settings/a_secrets.py | 7 + threat_matrix/settings/aws.py | 9 +- threat_matrix/settings/celery.py | 7 +- threat_matrix/settings/db.py | 3 +- threat_matrix/settings/elasticsearch.py | 46 +- threat_matrix/settings/mail.py | 2 +- threat_matrix/settings/security.py | 1 + threat_matrix/tasks.py | 100 +- 227 files changed, 12878 insertions(+), 4112 deletions(-) delete mode 100644 .github/workflows/mirror.yml create mode 100644 api_app/analyzers_manager/file_analyzers/androguard.py create mode 100644 api_app/analyzers_manager/file_analyzers/lnk_info.py create mode 100644 api_app/analyzers_manager/migrations/0120_alter_analyzerconfig_not_supported_filetypes_and_more.py create mode 100644 api_app/analyzers_manager/migrations/0121_analyzer_config_lnk_info.py create mode 100644 api_app/analyzers_manager/migrations/0122_alter_soft_time_limit.py create mode 100644 api_app/analyzers_manager/migrations/0123_basic_observable_analyzer.py create mode 100644 api_app/analyzers_manager/migrations/0124_analyzer_config_androguard.py create mode 100644 api_app/analyzers_manager/migrations/0125_update_yara_repo.py create mode 100644 api_app/analyzers_manager/migrations/0126_analyzer_config_nerd_analyzer.py create mode 100644 api_app/analyzers_manager/migrations/0127_analyzer_config_dshield.py create mode 100644 api_app/analyzers_manager/observable_analyzers/basic_observable_analyzer.py create mode 100644 api_app/analyzers_manager/observable_analyzers/dshield.py create mode 100644 api_app/analyzers_manager/observable_analyzers/nerd.py delete mode 100644 api_app/analyzers_manager/observable_analyzers/vt/vt3_base.py create mode 100644 api_app/ingestors_manager/ingestors/virus_total.py create mode 100644 api_app/ingestors_manager/migrations/0025_ingestor_config_virustotal_example_query.py create mode 100644 api_app/management/commands/elastic_templates.py create mode 100644 api_app/migrations/0063_singleton_and_elastic_report.py create mode 100644 api_app/pivots_manager/migrations/0033_pivot_config_extractedonenotefiles.py create mode 100644 api_app/pivots_manager/migrations/0034_changed_resubmitdownloadedfile_playbook_to_execute.py create mode 100644 api_app/playbooks_manager/migrations/0051_add_lnk_info_analyzer_free_to_use.py create mode 100644 api_app/playbooks_manager/migrations/0052_playbook_config_uris.py create mode 100644 api_app/playbooks_manager/migrations/0053_add_androguard_to_free_to_use_analyzers.py create mode 100644 api_app/serializers/elastic.py create mode 100644 configuration/elastic_search_mappings/plugin_report.json create mode 100755 create_elastic_certs create mode 100644 docker/env_file_elasticsearch_template create mode 100644 elasticsearch_instances.yml create mode 100644 frontend/.prettierignore create mode 100644 frontend/src/components/common/form/ScanConfigSelectInput.jsx create mode 100644 frontend/src/components/common/form/TLPSelectInput.jsx rename frontend/src/components/{scan/utils => common/form}/TagSelectInput.jsx (99%) create mode 100644 frontend/src/components/common/form/pluginsMultiSelectDropdownInput.jsx create mode 100644 frontend/src/components/common/form/runtimeConfigurationInput.jsx delete mode 100644 frontend/src/components/jobs/result/bar/jobBarApi.jsx create mode 100644 frontend/src/components/plugins/forms/AnalyzerConfigForm.jsx create mode 100644 frontend/src/components/plugins/forms/PivotConfigForm.jsx create mode 100644 frontend/src/components/plugins/forms/PlaybookConfigForm.jsx create mode 100644 frontend/src/components/plugins/pluginsApi.jsx create mode 100644 frontend/tests/components/plugins/types/forms/AnalyzerConfigForm.test.jsx create mode 100644 frontend/tests/components/plugins/types/forms/PivotConfigForm.test.jsx create mode 100644 frontend/tests/components/plugins/types/forms/PlaybookConfigForm.test.jsx create mode 100644 frontend/tests/layouts/AppHeader.test.jsx create mode 100644 static/Certego.png create mode 100644 static/greedybear.png create mode 100644 static/gsoc_logo.png create mode 100644 static/honeynet_logo.png create mode 100644 static/threat_matrix_negative.png create mode 100644 static/threat_matrix_positive.png create mode 100644 static/threathunter_logo.png create mode 100644 tests/api_app/analyzers_manager/file_analyzers/__init__.py create mode 100644 tests/api_app/analyzers_manager/file_analyzers/test_boxjs.py create mode 100644 tests/api_app/analyzers_manager/file_analyzers/test_doc_info.py create mode 100644 tests/api_app/analyzers_manager/file_analyzers/test_iocextract.py create mode 100644 tests/api_app/analyzers_manager/file_analyzers/test_lnk_info.py create mode 100644 tests/api_app/analyzers_manager/file_analyzers/test_onenote_info.py create mode 100644 tests/api_app/analyzers_manager/file_analyzers/test_pdf_info.py create mode 100644 tests/api_app/analyzers_manager/file_analyzers/test_strings_info.py create mode 100644 tests/api_app/test_mixins.py create mode 100644 tests/threat_matrix/__init__.py create mode 100644 tests/threat_matrix/test_tasks.py create mode 100644 threat_matrix/settings/a_secrets.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 9bc01527..61136f1f 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,45 +1,38 @@ # Changelog -[**Upgrade Guide**](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/installation/#update-to-the-most-recent-version) +[**Upgrade Guide**](https://khulnasoft.github.io/docs/ThreatMatrix/installation/#update-to-the-most-recent-version) ## [v6.1.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.1.0) - This release merges all the developments performed by our Google Summer of Code contributors for this year. The program has just ended. You can read the related blogs for more info about: - - [Nilay Gupta](https://x.com/guptanilay1): [New analyzers for ThreatMatrix](https://khulnasoft.github.io/blogs/gsoc24_new_analyzers_for_threatmatrix) - [Aryan Bhokare](https://www.linkedin.com/in/aryan-b-3803751a7/): [New Documentation Site for ThreatMatrix and friends](https://khulnasoft.github.io/blogs/gsoc24_New_documentation_site_summary) You'll get really tons of new analyzers this time to try out! -Plus we have a new official [documentation site](https://khulnasoft.github.io/ThreatMatrix-docs/)! Please refer to this one from now onwards. +Plus we have a new official [documentation site](https://khulnasoft.github.io/docs/)! Please refer to this one from now onwards. ## [v6.0.4](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.0.4) - Mostly adjusts and fixes with few new analyzers: Vulners and AILTypoSquatting Library. ## [v6.0.2](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.0.2) - Major fixes and adjustments. We improved the documentation to help the transition to the new major version. -We added **Pivot** buttons to enable manual Pivoting from an Observable/File analysis to another. See [Doc](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/usage/#pivots) for more info +We added **Pivot** buttons to enable manual Pivoting from an Observable/File analysis to another. See [Doc](https://khulnasoft.github.io/docs/ThreatMatrix/usage/#pivots) for more info As usual, we add new plugins. This release brings the following new ones: - -- a complete **TakedownRequest** playbook to automate TakeDown requests for malicious domains -- new File Analyzers for tools like [HFinger](https://github.com/CERT-Polska/hfinger), [Permhash](https://github.com/google/permhash) and [Blint](https://github.com/owasp-dep-scan/blint) -- new Observable Analyzers for [CyCat](https://cycat.org/) and [Hudson Rock](https://cavalier.hudsonrock.com/docs) -- improvement of the existing Maxmind analyzer: it now downloads the ASN database too. +* a complete **TakedownRequest** playbook to automate TakeDown requests for malicious domains +* new File Analyzers for tools like [HFinger](https://github.com/CERT-Polska/hfinger), [Permhash](https://github.com/google/permhash) and [Blint](https://github.com/owasp-dep-scan/blint) +* new Observable Analyzers for [CyCat](https://cycat.org/) and [Hudson Rock](https://cavalier.hudsonrock.com/docs) +* improvement of the existing Maxmind analyzer: it now downloads the ASN database too. ## [v6.0.1](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.0.1) - Little fixes for the major. ## [v6.0.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.0.0) +This major release is another important milestone for this project! We have been working hard to transform ThreatMatrix from a *Data Extraction Platform* to a complete *Investigation Platform*! -This major release is another important milestone for this project! We have been working hard to transform ThreatMatrix from a _Data Extraction Platform_ to a complete _Investigation Platform_! - -One of the most noticeable feature is the addition of the [**Investigation** framework](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/usage/#investigations-framework)! - +One of the most noticeable feature is the addition of the [**Investigation** framework](https://khulnasoft.github.io/docs/ThreatMatrix/usage/#investigations-framework)! + Thanks to the this new feature, analysts can leverage ThreatMatrix as the starting point of their "Investigations", register their findings, correlate the information found, and collaborate...all in a single place. Come and join us at the [Honeynet Workshop](https://denmark2024.honeynet.org/) in the Denmark this May to learn more about this new Major version and to meet the maintainers. :) @@ -50,23 +43,23 @@ You can also find us in [Fukuoka at the next FIRSTCON](https://www.first.org/con Many breaking changes have been introduced with this major release due to dependencies upgrades and architectural changes. -You can find more details in the [Upgrade Guide](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/installation/#updating-to-600-from-a-5xx-version). Please read it and follow it carefully before upgrading your ThreatMatrix instance to this Major version. +You can find more details in the [Upgrade Guide](https://khulnasoft.github.io/docs/ThreatMatrix/installation/#updating-to-600-from-a-5xx-version). Please read it and follow it carefully before upgrading your ThreatMatrix instance to this Major version. **New analyzers** As usual, we add new analyzers. This release brings a lot of new ones: +* [Zippy](https://github.com/thinkst/zippy) +* [Mmdb_server](https://github.com/adulau/mmdb-server) +* [BGP-Ranking](https://github.com/D4-project/BGP-Ranking) +* [Feodo Tracker](https://feodotracker.abuse.ch/) +* [IPQualityscore](https://www.ipqualityscore.com/) +* [IP2Location.io](https://www.ip2location.io/ip2location-documentation) +* [Validin](https://app.validin.com/) +* [PhoneInfoga](https://sundowndev.github.io/phoneinfoga/) +* [DNS0](https://docs.dns0.eu) +* [TweetFeed](https://tweetfeed.live/) +* [Tor Nodes DanMeUk](https://www.dan.me.uk/tornodes) -- [Zippy](https://github.com/thinkst/zippy) -- [Mmdb_server](https://github.com/adulau/mmdb-server) -- [BGP-Ranking](https://github.com/D4-project/BGP-Ranking) -- [Feodo Tracker](https://feodotracker.abuse.ch/) -- [IPQualityscore](https://www.ipqualityscore.com/) -- [IP2Location.io](https://www.ip2location.io/ip2location-documentation) -- [Validin](https://app.validin.com/) -- [PhoneInfoga](https://sundowndev.github.io/phoneinfoga/) -- [DNS0](https://docs.dns0.eu) -- [TweetFeed](https://tweetfeed.live/) -- [Tor Nodes DanMeUk](https://www.dan.me.uk/tornodes) ## [v5.2.3](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v5.2.3) @@ -78,45 +71,42 @@ The support for Docker Compose v1 has been dropped. Please upgrade to Docker Com The python `start.py` script is being replaced with a more light Bash script called `script` at the next Major version. Thanks to this change the installation requirements are a lot less than before and it should be easier to install and execute ThreatMatrix. Please start to use the new `start` script from now to avoid future issues. -For more information: [Installation docs](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/installation/) +For more information: [Installation docs](https://khulnasoft.github.io/docs/ThreatMatrix/installation/) ## [v5.2.2](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v5.2.2) This release has been done mainly to adjusts a broken database migration introduced in the previous release. **Main Improvements** - -- Added new analyzers for [DNS0](https://docs.dns0.eu/) PassiveDNS data -- Added the chance to collect metrics ([Business Intelligence](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/advanced_configuration/#business-intelligence) regarding Plugins Usage and send it to an ElasticSearch instance. -- Added new buttons to test ["Healthcheck" and "Pull" operations](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/usage/#special-plugins-operations) for each Plugin (A feature introduced in the previous version) +* Added new analyzers for [DNS0](https://docs.dns0.eu/) PassiveDNS data +* Added the chance to collect metrics ([Business Intelligence](https://khulnasoft.github.io/docs/ThreatMatrix/advanced_configuration/#business-intelligence) regarding Plugins Usage and send it to an ElasticSearch instance. +* Added new buttons to test ["Healthcheck" and "Pull" operations](https://khulnasoft.github.io/docs/ThreatMatrix/usage/#special-plugins-operations) for each Plugin (A feature introduced in the previous version) **Other improvements** - -- Various generic fixes and adjustments in the GUI -- dependencies upgrades -- adjusted contribution guides +* Various generic fixes and adjustments in the GUI +* dependencies upgrades +* adjusted contribution guides ## [v5.2.1](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v5.2.1) !!! This release has been found with a broken database migration !!! Please upgrade to v5.2.2 to fix the problem. **General improvements** +* Incremented wait time of containers' healthchecks to avoid to break clean installations +* Improvements to the "Scan page": + * Added the chance to customize the runtime configuration of a Playbook + * Moved TLP section from hidden in the "Advanced configuration" section to exposed by default +* Now every plugin can be configured with: + * a "healthcheck": this can be useful to verify the status of the service. + * a "pull": this can be useful to update a database that is used by the plugin, like a rules repository. -- Incremented wait time of containers' healthchecks to avoid to break clean installations -- Improvements to the "Scan page": - - Added the chance to customize the runtime configuration of a Playbook - - Moved TLP section from hidden in the "Advanced configuration" section to exposed by default -- Now every plugin can be configured with: - - a "healthcheck": this can be useful to verify the status of the service. - - a "pull": this can be useful to update a database that is used by the plugin, like a rules repository. **Fixes / adjusts / minor changes** - -- A lot of quality-of-life fixes in the frontend -- Removed footer in favor of social button at the top of the page -- minor adjustments in terms of performance and error handling -- better management of upload of big files -- dependencies upgrades +* A lot of quality-of-life fixes in the frontend +* Removed footer in favor of social button at the top of the page +* minor adjustments in terms of performance and error handling +* better management of upload of big files +* dependencies upgrades ## [v5.2.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v5.2.0) @@ -125,88 +115,79 @@ This is mostly a stability and maintainance release. We are happy to announce that we received support from Digital Ocean to host infrastructure for the community. :) If you are interested in helping us setting up a public instance of ThreatMatrix, **free** for the community, with all the privacy policy and related required stuff, please contact us :) -**Important usability changes** -- We added a new section in the "Scan" page called "Recent Scans" which allows the users to better interact with its own and other users' already made analysis, improving the efficiency of the users and their communication. -- By default jobs are executed with `TLP:AMBER` which means that they are shared with the other members of your organization **only**. (previously the default was `TLP:CLEAR`). This is to avoid possible users errors. -- From now on, VT file analyzers send files to VT only when TLP is `CLEAR` and not anymore based on a specific parameter. As a consequence, `VirusTotal_v3_Get_File_And_Scan` is not available anymore. Please use the new `VirusTotal_v3_Get_File` instead and set the analysis to the correct TLP. - - Same behavior has been extended to other analyzers: `Intezer_Scan`, `MWDB_Scan`, `Virushee_Upload_File` (renamed to `Virushee_Scan`), `YARAify_File_Scan`. +**Important usability changes** +* We added a new section in the "Scan" page called "Recent Scans" which allows the users to better interact with its own and other users' already made analysis, improving the efficiency of the users and their communication. +* By default jobs are executed with `TLP:AMBER` which means that they are shared with the other members of your organization **only**. (previously the default was `TLP:CLEAR`). This is to avoid possible users errors. +* From now on, VT file analyzers send files to VT only when TLP is `CLEAR` and not anymore based on a specific parameter. As a consequence, `VirusTotal_v3_Get_File_And_Scan` is not available anymore. Please use the new `VirusTotal_v3_Get_File` instead and set the analysis to the correct TLP. + * Same behavior has been extended to other analyzers: `Intezer_Scan`, `MWDB_Scan`, `Virushee_Upload_File` (renamed to `Virushee_Scan`), `YARAify_File_Scan`. **General improvements** - -- Added First Visit Guide -- Improved the documentation with the goal to help the users to understand better how all the available Plugins work. -- For OpenCTI users having problems in integrating ThreatMatrix, now you can use a workaround: [doc](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_configuration/#opencti) -- A new organization role is available to better manage the org: `admin`. [Doc](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#organizations-and-user-management) -- Improvements in the "Jobs History" table: now it shows executed Playbooks and file/observables types correctly. -- We added a new "Pivot" section in the "Plugin" GUI for the new Plugin type introduced in the [v5.1.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v5.1.0) release. We added a new dedicated visualizer which allows the user to see when a Pivot has been executed in the "Job Result" page. We are still working on it and planning to add more documentation and GUI usability soon. -- Improvements in the "Jobs Result" page: now playbooks are more relevant, warnings are shown next to errors, Raw JSON data has been moved next to the other raw data. -- Changed JSON viewer library because the old one was deprecated +* Added First Visit Guide +* Improved the documentation with the goal to help the users to understand better how all the available Plugins work. +* For OpenCTI users having problems in integrating ThreatMatrix, now you can use a workaround: [doc](https://khulnasoft.github.io/docs/advanced_configuration/#opencti) +* A new organization role is available to better manage the org: `admin`. [Doc](https://khulnasoft.github.io/docs/usage/#organizations-and-user-management) +* Improvements in the "Jobs History" table: now it shows executed Playbooks and file/observables types correctly. +* We added a new "Pivot" section in the "Plugin" GUI for the new Plugin type introduced in the [v5.1.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v5.1.0) release. We added a new dedicated visualizer which allows the user to see when a Pivot has been executed in the "Job Result" page. We are still working on it and planning to add more documentation and GUI usability soon. +* Improvements in the "Jobs Result" page: now playbooks are more relevant, warnings are shown next to errors, Raw JSON data has been moved next to the other raw data. +* Changed JSON viewer library because the old one was deprecated **New/Improved Plugins:** - -- deprecated `VirusTotal_v2_*` analyzers have been removed. -- added LOLDrivers Rules to ClamAV default signatures. -- added [Netlas.io](https://netlas.io/api) analyzer. -- removed CryptoScam analyzer because the service has been dismissed. -- added `timeout` to InQuest analyzers to avoid long time running jobs. -- fixed XLMMacroDeobfuscator always saying it decrypted the analyzed file even when the file was not encrypted. -- `Malpedia_Scan` has been deprecated and disabled because the service seems no more active. -- added more analyzers in the default `Sample_Static_Analysis` playbook. -- adjusted few analyzers: CAPESandbox, Dehashed, YARAify, GoogleWebRisk +* deprecated `VirusTotal_v2_*` analyzers have been removed. +* added LOLDrivers Rules to ClamAV default signatures. +* added [Netlas.io](https://netlas.io/api) analyzer. +* removed CryptoScam analyzer because the service has been dismissed. +* added `timeout` to InQuest analyzers to avoid long time running jobs. +* fixed XLMMacroDeobfuscator always saying it decrypted the analyzed file even when the file was not encrypted. +* `Malpedia_Scan` has been deprecated and disabled because the service seems no more active. +* added more analyzers in the default `Sample_Static_Analysis` playbook. +* adjusted few analyzers: CAPESandbox, Dehashed, YARAify, GoogleWebRisk **Fixes / adjusts / minor changes** +* Now "Restart" button in the Job Page does correctly work after having used a Playbook. +* basic support for IPv6 +* big refactors both in the backend and the frontend +* lot of fixes everywhere ;) +* improved documentation +* upgraded a lot of packages -- Now "Restart" button in the Job Page does correctly work after having used a Playbook. -- basic support for IPv6 -- big refactors both in the backend and the frontend -- lot of fixes everywhere ;) -- improved documentation -- upgraded a lot of packages ## [v5.1.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v5.1.0) - With this release we announce our new official site created by [Abheek Tripathy](https://twitter.com/abheekblahblah)! Feel free to check it out! Official [blog post here](https://khulnasoft.github.io/blogs/official_site_revamped)! **Important changes** - -- We added a new type of Plugin called [Ingestor](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#ingestors). **Ingestors** allow to automatically insert IOC streams from outside sources to ThreatMatrix itself. -- Visualizers are not connected anymore to Analyzers/Connectors. They are connected to a single Playbook instead. This allows the users to create and manage the Visualizers in an easier way. -- We added the new **Pivot** framework in the backend which allows to connect jobs to each other and to _pivot_ from one indicator to another. This is the first step to give the chance to the users to create more broader and complex investigation in ThreatMatrix. The next step will be to add the Frontend changes that allows the user to fully leverage the framework +* We added a new type of Plugin called [Ingestor](https://khulnasoft.github.io/docs/usage/#ingestors). **Ingestors** allow to automatically insert IOC streams from outside sources to ThreatMatrix itself. +* Visualizers are not connected anymore to Analyzers/Connectors. They are connected to a single Playbook instead. This allows the users to create and manage the Visualizers in an easier way. +* We added the new **Pivot** framework in the backend which allows to connect jobs to each other and to _pivot_ from one indicator to another. This is the first step to give the chance to the users to create more broader and complex investigation in ThreatMatrix. The next step will be to add the Frontend changes that allows the user to fully leverage the framework **New/Improved Plugins:** - -- Added new `DNS` playbook that collects the analyzers which performs DNS queries to various providers -- Added more option for `CapeSandbox` analyzer +* Added new `DNS` playbook that collects the analyzers which performs DNS queries to various providers +* Added more option for `CapeSandbox` analyzer **Fixes / adjusts / minor changes** - -- added chance to change the password of the account from the personal section in the application -- added a lot of Frontend tests for the "Scan" page to improve stability -- some frontend changes to improve overall experience (#1743, #1741, #1754, #1772, #1780, #1807, #1806) -- added new partial statuses for the Job which allow to better track the job progression [#1740)] -- Added new public Yara rules -- updated installation instructions -- upgraded a lot of packages +* added chance to change the password of the account from the personal section in the application +* added a lot of Frontend tests for the "Scan" page to improve stability +* some frontend changes to improve overall experience (#1743, #1741, #1754, #1772, #1780, #1807, #1806) +* added new partial statuses for the Job which allow to better track the job progression [#1740)] +* Added new public Yara rules +* updated installation instructions +* upgraded a lot of packages ## [v5.0.1](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v5.0.1) **Bug fixing for the v5.0.0 release** - -- The Scan Form button was not working. Now it works correctly. -- Added more frontend tests to reduce chances to introduce new bugs. +* The Scan Form button was not working. Now it works correctly. +* Added more frontend tests to reduce chances to introduce new bugs. **Important notice for users migrating to the new major release** A lot of database migrations needs to be applied during the upgrade. Just be patient few minutes once you install the new major release. If you get 500 status code errors in the GUI, just wait few minutes and then refresh the page. **Minor changes** - -- Upgrade Mandiant's Floss version +* Upgrade Mandiant's Floss version ## [v5.0.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v5.0.0) - This major release is another big step forward for ThreatMatrix!! 🚀 Official blog post: [v.5.0.0 Announcement](https://www.certego.net/blog/threatmatrix-v5-released) @@ -218,121 +199,108 @@ This framework is extremely powerful and allows every user to customize the GUI That would speed the analysis of the results a lot if done correctly! -To aid in this process we added a lot of [documentation and some very simple pre-built analyzers that you can use as example](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#visualizers): +To aid in this process we added a lot of [documentation and some very simple pre-built analyzers that you can use as example](https://khulnasoft.github.io/docs/usage/#visualizers): Moreover this release anticipates other important crucial steps for ThreatMatrix: - -- On June 10th [Matteo Lodi](https://twitter.com/matte_lodi) and [Simone Berni](https://twitter.com/0ssig3no) are presenting ThreatMatrix at one of the most important Cyber Security events in Italy: [HackinBo](https://www.hackinbo.it/programma.php) -- On May 28th the [Google Summer of Code 2023](https://developers.google.com/open-source/gsoc/timeline) is starting and ThreatMatrix is participating again with 2 new students! Welcome to [Shivam Purohit](https://twitter.com/stay_away_plss) and [Abheek Tripathy](https://twitter.com/abheekblahblah)! +* On June 10th [Matteo Lodi](https://twitter.com/matte_lodi) and [Simone Berni](https://twitter.com/0ssig3no) are presenting ThreatMatrix at one of the most important Cyber Security events in Italy: [HackinBo](https://www.hackinbo.it/programma.php) +* On May 28th the [Google Summer of Code 2023](https://developers.google.com/open-source/gsoc/timeline) is starting and ThreatMatrix is participating again with 2 new students! Welcome to [Shivam Purohit](https://twitter.com/stay_away_plss) and [Abheek Tripathy](https://twitter.com/abheekblahblah)! This release was possible thanks to the effort put in place by [Certego](https://www.certego.net) in supporting the maintainers. **Other important changes:** -We have done some big refactor changes that could make your application do not work as expected after this major upgrade. Please follow the the [migration guide](https://khulnasoft.github.io/ThreatMatrix-docs/installation/#updating-to-5-0-0-from-a-4-x-x-version) before upgrading ThreatMatrix to the new major release. +We have done some big refactor changes that could make your application do not work as expected after this major upgrade. Please follow the the [migration guide](https://khulnasoft.github.io/docs/installation/#updating-to-5-0-0-from-a-4-x-x-version) before upgrading ThreatMatrix to the new major release. -- We moved away from the old big `analyzer_config.json` which was storing all the base configuration of the Analyzers to a database model (we did the same for all the other plugins types too). This allows us to manage plugins creation/modification/deletion in a more reliable manner and via the Django Admin Interface. If you have created custom plugins and changed those `_config.json` file manually, you would need to re-create those custom plugins again from the Django Admin Interface. +* We moved away from the old big `analyzer_config.json` which was storing all the base configuration of the Analyzers to a database model (we did the same for all the other plugins types too). This allows us to manage plugins creation/modification/deletion in a more reliable manner and via the Django Admin Interface. If you have created custom plugins and changed those `_config.json` file manually, you would need to re-create those custom plugins again from the Django Admin Interface. -- We have REMOVED all the environment configuration that we deprecated with the v4.0.0 release and the script to migrate them. -- We have REMOVED/RENAMED all the analyzers that we deprecated during the v4 releases cycle plus some more (see [migration guide](https://khulnasoft.github.io/ThreatMatrix-docs/installation/#updating-to-5-0-0-from-a-4-x-x-version)). You might need to change the analyzer names in your integrations. -- We did a lot of code refactors here and there to remove some spaghetti code that was generated by the high amount of different contributors that we had during the recent years. This should be transparent for the user +* We have REMOVED all the environment configuration that we deprecated with the v4.0.0 release and the script to migrate them. +* We have REMOVED/RENAMED all the analyzers that we deprecated during the v4 releases cycle plus some more (see [migration guide](https://khulnasoft.github.io/docs/installation/#updating-to-5-0-0-from-a-4-x-x-version)). You might need to change the analyzer names in your integrations. +* We did a lot of code refactors here and there to remove some spaghetti code that was generated by the high amount of different contributors that we had during the recent years. This should be transparent for the user **Other added minor features** - -- We added the chance to add comments to "Job Result" pages to improve collaboration. -- We made few modifications to the "Scan" page to improve the user experience: - - By default, now the first available Playbook is executed and not all the available Analyzers anymore. - - By default, Analysis are run with TLP:RED and not with TLP:WHITE anymore. - - The Frontend automatically understand which type of observable you inserted. - - We moved the "Extra configuration" at the bottom of the "Scan" page and left only options that make actual sense. -- We added a Notification alert that, if the users has Notifications enabled in the browser, would notify the user once an analysis has finished. +* We added the chance to add comments to "Job Result" pages to improve collaboration. +* We made few modifications to the "Scan" page to improve the user experience: + * By default, now the first available Playbook is executed and not all the available Analyzers anymore. + * By default, Analysis are run with TLP:RED and not with TLP:WHITE anymore. + * The Frontend automatically understand which type of observable you inserted. + * We moved the "Extra configuration" at the bottom of the "Scan" page and left only options that make actual sense. +* We added a Notification alert that, if the users has Notifications enabled in the browser, would notify the user once an analysis has finished. **New/Improved Analyzers:** - -- Added more public Yara Rules (@dr4konia, @facebook) and we worked hard to optimize intensively Yara scanning. Now it should be super fast. -- Added [Sublime Security](https://docs.sublimesecurity.com/docs) analyzer (new framework to analyze emails). -- Updated and refactored `Dnstwist` analyzer to support more recent added options and work more reliably. -- Fixes to several analyzers like VirusTotal, OTX, APKiD, ClamAV +* Added more public Yara Rules (@dr4konia, @facebook) and we worked hard to optimize intensively Yara scanning. Now it should be super fast. +* Added [Sublime Security](https://docs.sublimesecurity.com/docs) analyzer (new framework to analyze emails). +* Updated and refactored `Dnstwist` analyzer to support more recent added options and work more reliably. +* Fixes to several analyzers like VirusTotal, OTX, APKiD, ClamAV **Fixes / adjust / minor changes** +* moved from TLP:WHITE to TLP:CLEAR +* several little fixes and adjustments here and there +* a lot of dependencies upgrades -- moved from TLP:WHITE to TLP:CLEAR -- several little fixes and adjustments here and there -- a lot of dependencies upgrades ## [v4.2.3](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v4.2.3) **New features** - -- Registration Page. Now you can configure your SMTP server (or AWS SES) to manage registration requests via email (user verification, password reset/change). This allows ThreatMatrix to be better suited for public deployments as a SaaS service. +* Registration Page. Now you can configure your SMTP server (or AWS SES) to manage registration requests via email (user verification, password reset/change). This allows ThreatMatrix to be better suited for public deployments as a SaaS service. **New/Improved Analyzers:** - -- Refactored `Yara` analyzer again to avoid memory leaks and improve performance intensively -- [Crowdsec](https://www.crowdsec.net/) analyzer no longer fails if the IP address is not found -- Added new [Hunter_How](https://hunter.how/search-api) analyzer -- We refactored the `malware_tools_analyzers` container that contains a lot of malware analysis tools. Thanks to that we have fixed `Qiling` and `Capa_Info` analyzer and we have updated all the other ones available (`Floss`, `APKid`, `Thug`, etc) +* Refactored `Yara` analyzer again to avoid memory leaks and improve performance intensively +* [Crowdsec](https://www.crowdsec.net/) analyzer no longer fails if the IP address is not found +* Added new [Hunter_How](https://hunter.how/search-api) analyzer +* We refactored the `malware_tools_analyzers` container that contains a lot of malware analysis tools. Thanks to that we have fixed `Qiling` and `Capa_Info` analyzer and we have updated all the other ones available (`Floss`, `APKid`, `Thug`, etc) **fixes / adjust / minor changes** - -- fixes to support for AWS Services (IAM authentication, AWS regions, AWS SQS) -- Added support for NFS storage -- minor fixes to a lot of different analyzers: `PDF_Info`, `Classic_DNS`, `Quad9`, `MWdb`, `OTX_Query`, etc -- fixes to `initialize.sh` -- now Observable name is copy pastable in the Job Result Page -- a lot of dependencies upgrade (like Django from v3.2 to v4.1) +* fixes to support for AWS Services (IAM authentication, AWS regions, AWS SQS) +* Added support for NFS storage +* minor fixes to a lot of different analyzers: `PDF_Info`, `Classic_DNS`, `Quad9`, `MWdb`, `OTX_Query`, etc +* fixes to `initialize.sh` +* now Observable name is copy pastable in the Job Result Page +* a lot of dependencies upgrade (like Django from v3.2 to v4.1) **CARE!!!** After having upgraded ThreatMatrix, in case the application does not start and you get an error like this: - ```commandline PermissionError: [Errno 13] Permission denied: '/var/log/threat_matrix/django/authentication.log ``` - just run this: - ```commandline sudo chown -R www-data:www-data /var/lib/docker/volumes/threat_matrix_generic_logs/_data/django ``` - and restart ThreatMatrix. It should solve the permissions problem. + ## [v4.2.2](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v4.2.2) **New/Improved Analyzers:** - -- added [Crowdsec](https://www.crowdsec.net/) analyzer. -- added [HuntressLab Yara rules](https://github.com/embee-research/Yara) to default Yara Rules List -- added [BinaryEdge](https://docs.binaryedge.io/api-v2/#v2queryiptarget) analyzer -- deprecated `Pulsedive_Active_IOC` analyzer. Please substitute it with the new `Pulsedive` analyzer. -- removed `Fortiguard` analyzer because endpoint does not work anymore. -- removed `Rendertron` analyzer not working as intended. +* added [Crowdsec](https://www.crowdsec.net/) analyzer. +* added [HuntressLab Yara rules](https://github.com/embee-research/Yara) to default Yara Rules List +* added [BinaryEdge](https://docs.binaryedge.io/api-v2/#v2queryiptarget) analyzer +* deprecated `Pulsedive_Active_IOC` analyzer. Please substitute it with the new `Pulsedive` analyzer. +* removed `Fortiguard` analyzer because endpoint does not work anymore. +* removed `Rendertron` analyzer not working as intended. **Deployment Changes** - -- added support for AWS RDS authentication with IAM roles -- added UwsgiTop for debugging -- Healthcheck is more permissive +* added support for AWS RDS authentication with IAM roles +* added UwsgiTop for debugging +* Healthcheck is more permissive **fixes / adjust** - -- fix ID and User lookups in Jobs History table (#1552) -- other minors +* fix ID and User lookups in Jobs History table (#1552) +* other minors ## [v4.2.1](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v4.2.1) -- Fixed Plugin bug which caused the inability to add new secrets. -- Fixed Yara Analyzer and added new open source rules -- Fixed Cape Sandbox analyzer not working -- Deprecated `ThreatMiner`, `SecurityTrails` and `Robtex` various analyzers and substituted with new versions. -- Refactoring and features in preparation to add support for cluster deployments. -- Added a new advanced Documentation section [Advanced Configuration](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_configuration) - - Added more support for Cloud Deployments (in particular AWS) -- Other minor adjustments and fixes +* Fixed Plugin bug which caused the inability to add new secrets. +* Fixed Yara Analyzer and added new open source rules +* Fixed Cape Sandbox analyzer not working +* Deprecated `ThreatMiner`, `SecurityTrails` and `Robtex` various analyzers and substituted with new versions. +* Refactoring and features in preparation to add support for cluster deployments. +* Added a new advanced Documentation section [Advanced Configuration](https://khulnasoft.github.io/docs/advanced_configuration) + * Added more support for Cloud Deployments (in particular AWS) +* Other minor adjustments and fixes ## [v4.2.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v4.2.0) With this release we welcome new official maintainers of ThreatMatrix: - - [Simone Berni](https://twitter.com/0ssig3no): Key Contributor and Backend Maintainer - [Daniele Rosetti](https://github.com/drosetti): Key Contributor and Frontend Maintainer @@ -342,37 +310,34 @@ Be ready for new awesome features! **Improved Document analysis** We added some improvements to handle recent Microsoft Office downloaders: - -- Now `Doc_Info` analyzer is able to extract URLs from samples that abuse [Follina](https://github.com/advisories/GHSA-4r9q-wqcj-x85j) vulnerability -- Now Microsoft Office analyzers does support OneNote documents -- We added [PyOneNote](https://github.com/DissectMalware/pyOneNote) analyzer to parse OneNote files. +* Now `Doc_Info` analyzer is able to extract URLs from samples that abuse [Follina](https://github.com/advisories/GHSA-4r9q-wqcj-x85j) vulnerability +* Now Microsoft Office analyzers does support OneNote documents +* We added [PyOneNote](https://github.com/DissectMalware/pyOneNote) analyzer to parse OneNote files. **Deployments:** -We are preparing to add more support for production deployments. We added some [documentation](https://khulnasoft.github.io/ThreatMatrix-docs/installation/) regarding: - -- Logrotate Configuration -- Crontab Configuration +We are preparing to add more support for production deployments. We added some [documentation](https://khulnasoft.github.io/docs/installation/) regarding: +* Logrotate Configuration +* Crontab Configuration **New/Improved Analyzers:** -- Now `ClamAV` analyzer makes use of all open source un-official community rules, not only the official ones -- `Yara` performance should be greatly improved. We also added other open source repositories plus the chance to configure a private repository of your own. -- Added [DNS0_EU](https://docs.dns0.eu/) analyzer (DNS resolver `DNS0_EU` + detection of malicious domains `DNS0_EU_Malicious_Detector`) -- Added [CheckPhish](https://checkphish.ai/checkphish-api/) analyzer -- Added [HaveIBeenPwned](https://haveibeenpwned.com/API/v3) analyzer -- Added [Koodous](https://docs.koodous.com/api/) analyzer -- Added [IPApi](https://ip-api.com) analyzer +* Now `ClamAV` analyzer makes use of all open source un-official community rules, not only the official ones +* `Yara` performance should be greatly improved. We also added other open source repositories plus the chance to configure a private repository of your own. +* Added [DNS0_EU](https://docs.dns0.eu/) analyzer (DNS resolver `DNS0_EU` + detection of malicious domains `DNS0_EU_Malicious_Detector`) +* Added [CheckPhish](https://checkphish.ai/checkphish-api/) analyzer +* Added [HaveIBeenPwned](https://haveibeenpwned.com/API/v3) analyzer +* Added [Koodous](https://docs.koodous.com/api/) analyzer +* Added [IPApi](https://ip-api.com) analyzer **DEPRECATION WARNING:** We have deprecated some analyzers and disabled them. We will remove them at the next major release. If you want to still use their functionalities, you need to explicitly enable them again. But you should move to the new ones: - -- Deprecated `Doc_Info_Experimental`. Its functionality (XLM Macro parsing) is moved to `Doc_Info` -- Deprecated `Strings_Info_Classic`. Please use `Strings_Info` -- Deprecated `Strings_Info_ML`. Please use `Strings_Info` and set the parameter `rank_strings` to `True` -- Deprecated all `Yara_Scan_` analyzers. They all went merged in the single `Yara` analyzer. +* Deprecated `Doc_Info_Experimental`. Its functionality (XLM Macro parsing) is moved to `Doc_Info` +* Deprecated `Strings_Info_Classic`. Please use `Strings_Info` +* Deprecated `Strings_Info_ML`. Please use `Strings_Info` and set the parameter `rank_strings` to `True` +* Deprecated all `Yara_Scan_` analyzers. They all went merged in the single `Yara` analyzer. **Others** @@ -381,7 +346,6 @@ If you want to still use their functionalities, you need to explicitly enable th - a lot of dependencies upgrades ## [v4.1.5](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v4.1.5) - With this release we announce that ThreatMatrix Project will apply as a new Organization in the next [Google Summer of Code](https://summerofcode.withgoogle.com/)! We have created a dedicated repository with all the info an aspiring contributor would need to participate to the program. @@ -391,43 +355,37 @@ All open source and cyber security fans! We are calling you! Be the next contrib (...and under the hood we did some fixes and updates here and there) ## [v4.1.4](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v4.1.4) - -With this release we welcome our first sponsor in [Open Collective](https://opencollective.com/khulnasoft): [ThreatHunter.ai](https://threathunter.ai/?utm_source=threatmatrix)! Thank you for your help! +With this release we welcome our first sponsor in [Open Collective](https://opencollective.com/threatmatrix-project): [ThreatHunter.ai](https://threathunter.ai/?utm_source=threatmatrix)! Thank you for your help! Moreover this release solves a bug regarding the creation of organization-level secrets which was not possible before. And this is the last release of this year for us! We will see each other back in 2023! ## [v4.1.3](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v4.1.3) - -With this version we officially announce that we have joined [Open Collective](https://opencollective.com/khulnasoft) with the ThreatMatrix Project! +With this version we officially announce that we have joined [Open Collective](https://opencollective.com/threatmatrix-project) with the ThreatMatrix Project! If you love this project and you would like to help us, we would love to get your support there! - - + + **New/Improved Analyzers:** - -- adjusted / fixed a lot of popular analyzers like Dehashed, MISP, VirusTotal, Alienvault OTX, PDF_Info and Unpacme -- fixed --malware_tools_analyzers broken +* adjusted / fixed a lot of popular analyzers like Dehashed, MISP, VirusTotal, Alienvault OTX, PDF_Info and Unpacme +* fixed --malware_tools_analyzers broken ## [v4.1.2](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v4.1.2) -This version mainly adds quality improvements to the recently released ["Playbook" feature](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#playbooks): - -- Now it is possible to create a new Playbook easily thanks to a proper button in the GUI. In this way you can save your own Playbooks and repeat them. -- Now Playbooks support the check of already existing similar analysis like normal analysis already do. This saves computational and analysts' time. +This version mainly adds quality improvements to the recently released ["Playbook" feature](https://khulnasoft.github.io/docs/usage/#playbooks): +* Now it is possible to create a new Playbook easily thanks to a proper button in the GUI. In this way you can save your own Playbooks and repeat them. +* Now Playbooks support the check of already existing similar analysis like normal analysis already do. This saves computational and analysts' time. Thanks to @0x0elliot for these new features. **New/Improved Analyzers:** - -- VT analyzer has been fixed and works correctly when performing a "rescan" of a sample. -- AbuseIPDB analyzer does not show all the reports by default (this could become quite large) +* VT analyzer has been fixed and works correctly when performing a "rescan" of a sample. +* AbuseIPDB analyzer does not show all the reports by default (this could become quite large) **Others** - - various fixes and stability contributions - a lot of dependencies upgrades @@ -440,7 +398,6 @@ The access is not open to prevent abuse. If you are interested in getting access Then, this release fixes some important bugs regarding the integration with OpenCTI and all the other optional DockerAnalyzers-based integrations which were not correctly working. **Others** - - Several documentation adjustments and updates - usual dependencies upgrades @@ -448,48 +405,43 @@ Then, this release fixes some important bugs regarding the integration with Open This release marks the end of the Google Summer of Code for this year (2022)! Each contributor wrote a blog post regarding his work for ThreatMatrix during this summer: - -- [Aditya Narayan Sinha](https://twitter.com/0x0elliot): [Creating Playbooks for ThreatMatrix](https://www.honeynet.org/2022/10/06/gsoc-2022-project-summary-creating-playbooks-for-threatmatrix/) -- [Aditya Pratap Singh](https://twitter.com/devmrfitz): [ThreatMatrix v4 improvements](https://www.honeynet.org/2022/09/26/gsoc-2022-project-summary-threatmatrix-v4-improvements/) -- [Hussain Khan](https://twitter.com/Hussain41099635): [ThreatMatrix Go Client](https://www.honeynet.org/2022/09/06/gsoc-2022-project-summary-threatmatrix-go-client-go-threatmatrix/) + - [Aditya Narayan Sinha](https://twitter.com/0x0elliot): [Creating Playbooks for ThreatMatrix](https://www.honeynet.org/2022/10/06/gsoc-2022-project-summary-creating-playbooks-for-threatmatrix/) + - [Aditya Pratap Singh](https://twitter.com/devmrfitz): [ThreatMatrix v4 improvements](https://www.honeynet.org/2022/09/26/gsoc-2022-project-summary-threatmatrix-v4-improvements/) + - [Hussain Khan](https://twitter.com/Hussain41099635): [ThreatMatrix Go Client](https://www.honeynet.org/2022/09/06/gsoc-2022-project-summary-threatmatrix-go-client-go-threatmatrix/) I would like to thank them and all the mentors (@sp35, @eshaan7, @0ssigeno, @drosetti) for the efforts put in the place during the last months! Looking forward for the Google Summer of Code 2023! **Time savers features** - -- New Plugin Type to allow to easily replicate the same type of analysis without having to select and/or configure groups of analyzers/connectors every time: **Playbooks** ([docs reference](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#playbooks)) -- Default Plugins Parameters can be customized from the GUI and are defined at user/org level instead of globally ([docs reference](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_usage/#customize-analyzer-execution)) -- Plugins Secrets can now be managed from the GUI and are defined at user/org level instead of globally ([docs reference](https://khulnasoft.github.io/ThreatMatrix-docs/installation/#deprecated-environment-configuration)) -- Organization admins can enable/disable analyzers for all the org ([docs reference](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#multi-tenancy)) -- Google Oauth authentication support ([docs reference](https://khulnasoft.github.io/ThreatMatrix-docs/Advanced-Configuration.html#google-oauth2)) -- Added support for `extends` key to simplify Analyzer configuration and customization ([docs reference](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#analyzers-customization)) +- New Plugin Type to allow to easily replicate the same type of analysis without having to select and/or configure groups of analyzers/connectors every time: **Playbooks** ([docs reference](https://khulnasoft.github.io/docs/usage/#playbooks)) +- Default Plugins Parameters can be customized from the GUI and are defined at user/org level instead of globally ([docs reference](https://khulnasoft.github.io/docs/advanced_usage/#customize-analyzer-execution)) +- Plugins Secrets can now be managed from the GUI and are defined at user/org level instead of globally ([docs reference](https://khulnasoft.github.io/docs/installation/#deprecated-environment-configuration)) +- Organization admins can enable/disable analyzers for all the org ([docs reference](https://khulnasoft.github.io/docs/usage/#multi-tenancy)) +- Google Oauth authentication support ([docs reference](https://khulnasoft.github.io/docs/Advanced-Configuration.html#google-oauth2)) +- Added support for `extends` key to simplify Analyzer configuration and customization ([docs reference](https://khulnasoft.github.io/docs/usage/#analyzers-customization)) **Others** - - Adjusted default time limits and configuration of some analyzers - various fixes and stability contributions - a lot of dependencies upgrades - other minor updates + ## [v4.0.1](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v4.0.1) **New/Improved Analyzers:** - - added pre-defined `Yara_Scan_Custom_Signatures` analyzer to give the chance to the users to add their own rules directly in ThreatMatrix. - added `ELF_Info` analyzer which parses ELF files. - added support for [TLSH](https://github.com/trendmicro/tlsh) hash in `File_Info` and telfhash in `ELF_Info` **Fixes/Adjustments:** - - renamed `Yara_Scan_YARAify_Rules` to `Yara_Scan_YARAify` - fixed `Yara_Scan_Community` update and extraction process - a lot of dependencies upgrades - fixed to the docs ## [v4.0.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v4.0.0) - **Notes:** After months of work, we are finally ready to move forward and anticipate the new major 4.0.0 release for ThreatMatrix! @@ -502,36 +454,34 @@ The overall user feeling should be drastically improved. We hope you'll enjoy th While developing the new GUI, our main goal was to at least provide the same features that were available before. Anyway, we had the chance to add some important features: -- A new way to manage users and their permissions: the "Organization" feature. Please refer to the [docs here](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#organizations-and-user-management). -- A notification mechanism was added. Please refer to the [docs here](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#notifications). +- A new way to manage users and their permissions: the "Organization" feature. Please refer to the [docs here](https://khulnasoft.github.io/docs/usage/#organizations-and-user-management). +- A notification mechanism was added. Please refer to the [docs here](https://khulnasoft.github.io/docs/usage/#notifications). - Now it is possible to do more advanced lookups through the Jobs History and have an overall better way to filter them. - A new "API Access/Sessions" section was added to facilitate the management of API tokens and User sessions. - Now it is possible to submit multiple observables / files at the same time. **RETROCOMPATIBILITY INFO AND HOW TO UPDATE** -Please refer to the [**Upgrade Guide**](https://khulnasoft.github.io/ThreatMatrix-docs/installation/#update-and-re-build) +Please refer to the [**Upgrade Guide**](https://khulnasoft.github.io/docs/installation/#update-and-re-build) **New/Improved Analyzers:** - -- Added an analyzer which supports the new service provided for free by [The Honeynet Project](https://www.honeynet.org/2021/12/27/new-project-available-greedybear/): [GreedyBear](https://github.com/honeynet/GreedyBear) +- Added an analyzer which supports the new service provided for free by [The Honeynet Project](https://www.honeynet.org/2021/12/27/new-project-available-greedybear/): [GreedyBear](https://github.com/honeynet/GreedyBear) - Added 3 new analyzers for the new service from Abuse.ch: [YARAify](https://yaraify.abuse.ch/) - Added support for PCAP files and a new analyzer for [Suricata](https://suricata.io/) which allows to analyze PCAPs with IDS rules very fast and at scale. **Other:** -- improved and updated the overall documentation (in particular the [Contribute](https://khulnasoft.github.io/ThreatMatrix-docs/contribute) section) to help the developers to start to work on the project +- improved and updated the overall documentation (in particular the [Contribute](https://khulnasoft.github.io/docs/contribute) section) to help the developers to start to work on the project - added DOCKER BUILDKIT, `--debug-build` and Watchman dependency to speed up development - now the Backend and the Frontend are respectively highly dependant from 2 new open source projects created by [Certego](https://www.certego.net/), [certego-saas](https://github.com/certego/certego-saas) and [certego-ui](https://github.com/certego/certego-ui). - a lot of dependencies upgrade, in particular in the new ReactJS Frontend. ## [v3.4.1](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v3.4.1) - **Notes:** We are proud to announce that we have selected 3 contributors for the upcoming [Google Summer of Code](https://summerofcode.withgoogle.com/)! -ThreatMatrixProject will run their projects under the umbrella of [The Honeynet Project](https://www.honeynet.org/), like the previous years. +KhulnaSoft will run their projects under the umbrella of [The Honeynet Project](https://www.honeynet.org/), like the previous years. The contributors are going to have 3 intense months of work: with the help of the ThreatMatrix maintainers, they'll bring new functionalities to the project! @@ -542,16 +492,14 @@ The contributors are going to have 3 intense months of work: with the help of th We are also moving forward to release the next major version (v4). We just need to work on some update scripts. **Fixes/Adjustments:** +* Add support for ".csv" file in all the Analyzers for documents +* Refactored `Triage` analyzers +* Fixes: #951, #1004, #1003 +* usual dependencies upgrades -- Add support for ".csv" file in all the Analyzers for documents -- Refactored `Triage` analyzers -- Fixes: #951, #1004, #1003 -- usual dependencies upgrades ## [v3.4.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v3.4.0) - **New/Improved Analyzers:** - - Improved MISP analyzer: more options and fixed a bug (#979, #1000) - Improved VT3 analyzers: now it is possible to extract relationships data + the analyzers are optimized to reduce the number of queries and save quota (#988) - New [VirusTotal_v3_Intelligence_Search](https://developers.virustotal.com/reference/search) for premium users (#981) @@ -561,73 +509,64 @@ We are also moving forward to release the next major version (v4). We just need - New [IntelX_Intelligent_Search](intelx.io) analyzer (it comes to complete the IntelX endpoints already available) (#974) **Other:** - - some fixes #952, #938 - adjusted PR automation - a lot of dependencies upgrades - renamed `Yara_Scan_McAfee` analyzer to `Yara_Scan_Trellix` and `Virushee_UploadFile` to `Virushee_Upload_File` ## [v3.3.2](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v3.3.2) - **Notes:** We are proud to announce two new sponsorships today! - -- [Milton Security](https://www.miltonsecurity.com?utm_source=threatmatrix) -- [LimaCharlie](https://limacharlie.io/blog/limacharlie-sponsors-intel-owl/?utm_source=threatmatrix&utm_medium=banner) + - [Milton Security](https://www.miltonsecurity.com?utm_source=threatmatrix) + - [LimaCharlie](https://limacharlie.io/blog/limacharlie-sponsors-intel-owl/?utm_source=threatmatrix&utm_medium=banner) If you are interested in helping the project through a donation, read [here](https://github.com/khulnasoft/ThreatMatrix/blob/master/.github/partnership_and_sponsors.md) how you can do it! **New/Improved Analyzers:** - -- New [CyberChef](https://gchq.githuba.io/CyberChef/) Analyzer! Run your own recipes in ThreatMatrix! Check the [docs](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_usage/#cyberchef)! +- New [CyberChef](https://gchq.githuba.io/CyberChef/) Analyzer! Run your own recipes in ThreatMatrix! Check the [docs](https://khulnasoft.github.io/docs/advanced_usage/#cyberchef)! **Other:** - - fixes: [#931](https://github.com/khulnasoft/ThreatMatrix/issues/931) - several dependencies upgrades + ## [v3.3.1](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v3.3.1) **Notes:** - - BREAKING CHANGE: - We merged some additional Docker Analyzers (`thug`, `static_analyzers`, `apk_analyzers`, `box-js` and `qiling`) into a single container called `malware_tools_analyzers`. In this way, the ThreatMatrix configuration with all those Malware Analyzers is a lot lighter than before. Just run `--malware_tools_analyzers` as a single option to leverage all those additional analyzers. - fixed `--all_analyzers` and `--tor_analyzers` options not working. **New/Improved Analyzers:** - - Added option to run shellcodes with Mandiant tools (Floss, SpeakEasy and Capa) - Minor fix to [Qiling](https://github.com/qilingframework/qiling) Analyzers - Added new Observable Analyzer for [Stalkphish](https://stalkphish.io) - Added new Yara Analyzer for [Malpedia](https://malpedia.caad.fkie.fraunhofer.de/) Rules **Other:** - - Added Issue Templates - Renewed PR automation to better detect possible bugs in deployments and to improve performance ## [v3.3.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v3.3.0) **Notes:** - -- Added helper script that checks and installs [initial requirements](https://khulnasoft.github.io/ThreatMatrix-docs/installation/#requirements). (`initialize.sh`) -- Added [RADIUS authentication support](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_configuration/#radius-authentication) +- Added helper script that checks and installs [initial requirements](https://khulnasoft.github.io/docs/installation/#requirements). (`initialize.sh`) +- Added [RADIUS authentication support](https://khulnasoft.github.io/docs/advanced_configuration/#radius-authentication) **New/Improved Analyzers:** - -- Added a new optional [Docker Analyzer](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_usage/#optional-analyzers) running [Onionscan](https://github.com/s-rah/onionscan) +- Added a new optional [Docker Analyzer](https://khulnasoft.github.io/docs/advanced_usage/#optional-analyzers) running [Onionscan](https://github.com/s-rah/onionscan) - Added [CAPE Sandbox](https://capesandbox.com/) file analyzer - `Doc_Info` analyzer now runs [msodde](https://github.com/decalage2/oletools/wiki/msodde) together with `olevba` and `XMLMacroDeobfuscator` - `PE_Info` analyzer now calculates [impfuzzy](https://github.com/JPCERTCC/impfuzzy) and [dashicon](https://github.com/fr0gger/SuperPeHasher) hashes too. **Other:** - -- Added option to run ElasticSearch/Kibana together with ThreatMatrix with option `--elastic`. Check the [doc here](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_configuration/#example-configuration) +- Added option to run ElasticSearch/Kibana together with ThreatMatrix with option `--elastic`. Check the [doc here](https://khulnasoft.github.io/docs/advanced_configuration/#example-configuration) - Security: Patched Django Critical Bug + Added Brute Force protection to the Admin page - Generic bug fixing and other maintenance work - Bump some python dependencies + ## [v3.2.4](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v3.2.4) **Notes:** @@ -667,7 +606,7 @@ If you are interested in helping the project through a donation, read [here](htt **For ThreatMatrix Contributors** -We updated the documentation on how to [Contribute](https://khulnasoft.github.io/ThreatMatrix-docs/contribute/#rules). Please read through them if interested in contributing in the project. +We updated the documentation on how to [Contribute](https://khulnasoft.github.io/docs/contribute/#rules). Please read through them if interested in contributing in the project. ## [v3.2.2](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v3.2.2) @@ -778,12 +717,12 @@ This is a minor patch release. **Features:** - Plugins (analyzers/connectors) that are not properly configured will not run even if requested. They will be marked as disabled from the dropdown on the analysis form and as a bonus you can also see if and why a plugin is not configured on the GUI tables. -- Added `kill`, `retry` and `healthcheck` features to analyzers and connectors. See [Managing Analyzers and Connectors](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#special-plugins-operations). -- Standardized threat-sharing using Traffic Light Protocol or `TLP`, thereby deprecating the use of booleans `force_privacy`, `disable_external_analyzers` and `private`. See [TLP Support](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#tlp-support). This makes the analysis form much more easier to use than before. +- Added `kill`, `retry` and `healthcheck` features to analyzers and connectors. See [Managing Analyzers and Connectors](https://khulnasoft.github.io/docs/usage/#special-plugins-operations). +- Standardized threat-sharing using Traffic Light Protocol or `TLP`, thereby deprecating the use of booleans `force_privacy`, `disable_external_analyzers` and `private`. See [TLP Support](https://khulnasoft.github.io/docs/usage/#tlp-support). This makes the analysis form much more easier to use than before. **New class of plugins called _Connectors_:** -- Connectors are designed to run after every successful analysis which makes them suitable for automated threat-sharing. Built to support integration with other SIEM/SOAR projects specifically aimed at Threat Sharing Platforms. See [Available Connectors](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#available-connectors). +- Connectors are designed to run after every successful analysis which makes them suitable for automated threat-sharing. Built to support integration with other SIEM/SOAR projects specifically aimed at Threat Sharing Platforms. See [Available Connectors](https://khulnasoft.github.io/docs/usage/#available-connectors). - Newly added connectors for threat-sharing: - `MISP`: automatically creates an event on your MISP instance. - `OpenCTI`: automatically creates an observable and a linked report on your OpenCTI instance. @@ -794,7 +733,7 @@ This is a minor patch release. - The `additional_config_params` attribute was split into the following 3 individual attributes. - `config`: Includes common parameters - `queue` and `soft_time_limit`. - - `params`: Includes default value, datatype and description for each [Analyzer](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#analyzers-customization) or [Connector](https://khulnasoft.github.io/ThreatMatrix-docs/usage/#connectors-customization) specific parameters that modify runtime behaviour. + - `params`: Includes default value, datatype and description for each [Analyzer](https://khulnasoft.github.io/docs/usage/#analyzers-customization) or [Connector](https://khulnasoft.github.io/docs/usage/#connectors-customization) specific parameters that modify runtime behaviour. - `secrets`: Includes analyzer or connector specific secrets (e.g. API Key) name along with the secret's description. All secrets are required. **New inbuilt analyzers/fixes to existing:** @@ -808,7 +747,7 @@ This is a minor patch release. - New `ClamAV` analyzer: scan files for viruses/malwares/trojans using [ClamAV antivirus engine](https://docs.clamav.net/). - Fixed `Tranco` Analyzer pointing to the wrong `python_module` - Removed `CirclePDNS` default value in `env_file_app_template` -- VirusTotal v3: New configuration options: `include_behaviour_summary` for behavioral analysis and `include_sigma_analyses` for sigma analysis report of the file. See [Customize Analyzers](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_usage/#customize-analyzer-execution). +- VirusTotal v3: New configuration options: `include_behaviour_summary` for behavioral analysis and `include_sigma_analyses` for sigma analysis report of the file. See [Customize Analyzers](https://khulnasoft.github.io/docs/advanced_usage/#customize-analyzer-execution). **REST API changes:** @@ -870,7 +809,7 @@ Then a lot of maintenance and overall project stability issues solved: - bumped new versions of a lot of dependencies - Improved "Installation" and "Contribute" documentation - added new badges to the README -- added `--django-server` [option](https://khulnasoft.github.io/ThreatMatrix-docs/contribute/#how-to-start) to speed up development +- added `--django-server` [option](https://khulnasoft.github.io/docs/contribute/#how-to-start) to speed up development - analyzed files are now correctly deleted with the periodic cronjob - other little refactors and fixes @@ -949,25 +888,25 @@ We changed `docker-compose` file names for optional analyzers. In the `v.2.0.0` - moved docker and docker-compose files under `docker/` folder. - users upgrading from previous versions need to manually move `env_file_app`, `env_file_postgres` and `env_file_integrations` files under `docker/`. -- users are to use the new [start.py](https://khulnasoft.github.io/ThreatMatrix-docs/installation/#run) method to build or start ThreatMatrix containers +- users are to use the new [start.py](https://khulnasoft.github.io/docs/installation/#run) method to build or start ThreatMatrix containers - moved the following analyzers together in a specific optional docker container named `static_analyzers`. - [`Capa`](https://github.com/fireeye/capa) - [`PeFrame`](https://github.com/guelfoweb/peframe) - `Strings_Info_Classic` (based on [flarestrings](https://github.com/fireeye/stringsifter)) - `Strings_Info_ML` (based on [stringsifter](https://github.com/fireeye/stringsifter)) -Please see [docs](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_usage/#optional-analyzers) to understand how to enable these optional analyzers +Please see [docs](https://khulnasoft.github.io/docs/advanced_usage/#optional-analyzers) to understand how to enable these optional analyzers **NEW INBUILT ANALYZERS:** -- added [Qiling](https://github.com/qilingframework/qiling) file analyzer. This is an optional analyzer (see [docs](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_usage.html#optional-analyzers) to understand how to activate it). +- added [Qiling](https://github.com/qilingframework/qiling) file analyzer. This is an optional analyzer (see [docs](https://khulnasoft.github.io/docs/advanced_usage.html#optional-analyzers) to understand how to activate it). - added [Stratosphere blacklists](https://www.stratosphereips.org/attacker-ip-prioritization-blacklist) analyzer - added [FireEye Red Team Tool Countermeasures](https://github.com/fireeye/red_team_tool_countermeasures) Yara rules analyzer - added [emailrep.io](https://emailrep.io/) analyzer - added [Triage](https://tria.ge) analyzer for observables (`search` API) - added [InQuest](https://labs.inquest.net) analyzer - added [WiGLE](api.wigle.net) analyzer -- new analyzers were added to the `static_analyzers` optional docker container (see [docs](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_usage/#optional-analyzers) to understand how to activate it). +- new analyzers were added to the `static_analyzers` optional docker container (see [docs](https://khulnasoft.github.io/docs/advanced_usage/#optional-analyzers) to understand how to activate it). - [`FireEye Floss`](https://github.com/fireeye/flare-floss) strings analysis. - [`Manalyze`](https://github.com/JusticeRage/Manalyze) file analyzer @@ -975,7 +914,7 @@ Please see [docs](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_usage/ - upgraded main Dockerfile to python 3.8 - added support for the `generic` observable type. In this way it is possible to build analyzers that can analyze everything and not only IPs, domains, URLs or hashes -- added [Multi-queue](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_configuration/#multi-queue) option to optimize usage of Celery queues. This is intended for advanced users. +- added [Multi-queue](https://khulnasoft.github.io/docs/advanced_configuration/#multi-queue) option to optimize usage of Celery queues. This is intended for advanced users. - updated GUI to new [ThreatMatrix-ng](https://github.com/khulnasoft/ThreatMatrix-ng/releases/tag/v1.7.0) version - upgraded [Speakeasy](https://github.com/fireeye/speakeasy), [Quark-Engine](https://github.com/quark-engine/quark-engine) and [Dnstwist](https://github.com/elceef/dnstwist) analyzers to last versions - moved from Travis CI to Github CI @@ -1106,7 +1045,7 @@ Patch after **v1.5.0**. **Breaking Changes:** -- Moved `ldap_config.py` under `configuration/` directory. If you were using LDAP before this release, please refer the [updated docs](https://khulnasoft.github.io/ThreatMatrix-docs/advanced_configuration/#ldap). +- Moved `ldap_config.py` under `configuration/` directory. If you were using LDAP before this release, please refer the [updated docs](https://khulnasoft.github.io/docs/advanced_configuration/#ldap). **Fixes:** diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 4ccf22a0..fe02c4da 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -- Focusing on what is best not just for us as individuals, but for the +* Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -- The use of sexualized language or imagery, and sexual attention or +* The use of sexualized language or imagery, and sexual attention or advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a +* Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 40635351..4c5c3812 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1 +1 @@ -Please refer to https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/contribute/ +Please refer to https://khulnasoft.github.io/docs/ThreatMatrix/contribute/ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a3f2b4d2..c7a8c847 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ -github: [khulnasoft-bot] +open_collective: threatmatrix-project +github: khulnasoft \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md index 3777e50e..2128877b 100644 --- a/.github/ISSUE_TEMPLATE/issue_template.md +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -1,19 +1,19 @@ --- name: Issue Template about: used to report bugs -title: "" +title: '' labels: bug -assignees: "" +assignees: '' + --- ## What happened ## Environment - 1. OS: 2. ThreatMatrix version: -## What did you expect to happen +## What did you expect to happen ## How to reproduce your issue diff --git a/.github/ISSUE_TEMPLATE/new_analyzer.md b/.github/ISSUE_TEMPLATE/new_analyzer.md index 1accf688..61b8c9cc 100644 --- a/.github/ISSUE_TEMPLATE/new_analyzer.md +++ b/.github/ISSUE_TEMPLATE/new_analyzer.md @@ -3,7 +3,8 @@ name: New Analyzer about: A new analyzer to integrate with ThreatMatrix title: "[Analyzer]" labels: new_analyzer -assignees: "" +assignees: '' + --- ## Name @@ -11,9 +12,10 @@ assignees: "" ## Link ## Type of analyzer - **this can be observable, file, and docker** + ## Why should we use it + ## Possible implementation diff --git a/.github/ISSUE_TEMPLATE/new_connector.md b/.github/ISSUE_TEMPLATE/new_connector.md index a793e09b..f9704998 100644 --- a/.github/ISSUE_TEMPLATE/new_connector.md +++ b/.github/ISSUE_TEMPLATE/new_connector.md @@ -3,7 +3,8 @@ name: New Connector about: A new connector to integrate with ThreatMatrix title: "[Connector]" labels: new_connector -assignees: "" +assignees: '' + --- ## Name @@ -11,9 +12,10 @@ assignees: "" ## Link ## Type of connector - ** what kind of data this connector would push to the integrated service ** + ## Why should we use it + ## Possible implementation diff --git a/.github/ISSUE_TEMPLATE/new_ingestor.md b/.github/ISSUE_TEMPLATE/new_ingestor.md index 3543790d..a430199b 100644 --- a/.github/ISSUE_TEMPLATE/new_ingestor.md +++ b/.github/ISSUE_TEMPLATE/new_ingestor.md @@ -3,13 +3,16 @@ name: New Ingestor about: A new ingestor to integrate with ThreatMatrix title: "[Ingestor]" labels: new_ingestor -assignees: "" +assignees: '' + --- ## Name ## Link + ## Why should we use it + ## Possible implementation diff --git a/.github/ISSUE_TEMPLATE/new_playbook.md b/.github/ISSUE_TEMPLATE/new_playbook.md index 0aee935a..fd7cee24 100644 --- a/.github/ISSUE_TEMPLATE/new_playbook.md +++ b/.github/ISSUE_TEMPLATE/new_playbook.md @@ -3,15 +3,21 @@ name: New Playbook about: A new playbook configured inside ThreatMatrix title: "[Playbook]" labels: new_playbook -assignees: "" +assignees: '' + --- ## Name + ## Analyzers + ## Connectors + ## Runtime configuration + ## Use case + diff --git a/.github/ISSUE_TEMPLATE/new_visualizer.md b/.github/ISSUE_TEMPLATE/new_visualizer.md index 9f187cd6..cda67bc9 100644 --- a/.github/ISSUE_TEMPLATE/new_visualizer.md +++ b/.github/ISSUE_TEMPLATE/new_visualizer.md @@ -3,13 +3,17 @@ name: New Visualizer about: A new visualizer to integrate with ThreatMatrix title: "[Visualizer]" labels: new_visualizer -assignees: "" +assignees: '' + --- ## Name + ## Playbooks + ## Why should we create it + ## Possible implementation diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 4c107eb0..3c32e78a 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -3,7 +3,7 @@ ## Supported Versions | Version | Supported | -| ------- | ------------------ | +|---------| ------------------ | | >4.x.x | :white_check_mark: | | <4.x.x | :x: | @@ -13,7 +13,6 @@ Please contact privately via Twitter one of the current maintainers. Current list of maintainers is available here: https://github.com/khulnasoft/ThreatMatrix#about-the-author-and-maintainers Then we would: - -- verify the vulnerability -- once verified, open a Security Advisory in Github -- update you with progress +* verify the vulnerability +* once verified, open a Security Advisory in Github +* update you with progress \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 137744be..b1365385 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,30 +14,28 @@ Please delete options that are not relevant. # Checklist -- [ ] I have read and understood the rules about [how to Contribute](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/contribute/) to this project +- [ ] I have read and understood the rules about [how to Contribute](https://khulnasoft.github.io/docs/ThreatMatrix/contribute/) to this project - [ ] The pull request is for the branch `develop` - [ ] A new plugin (analyzer, connector, visualizer, playbook, pivot or ingestor) was added or changed, in which case: - - [ ] I strictly followed the documentation ["How to create a Plugin"](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/contribute/#how-to-add-a-new-plugin) - - [ ] [Usage](https://github.com/khulnasoft/docs/blob/main/docs/ThreatMatrix/usage.md) file was updated. - - [ ] [Advanced-Usage](https://github.com/khulnasoft/docs/blob/main/docs/ThreatMatrix/advanced_usage.md) was updated (in case the plugin provides additional optional configuration). - - [ ] I have dumped the configuration from Django Admin using the `dumpplugin` command and added it in the project as a data migration. (["How to share a plugin with the community"](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/contribute/#how-to-share-your-plugin-with-the-community)) - - [ ] If a File analyzer was added and it supports a mimetype which is not already supported, you added a sample of that type inside the archive `test_files.zip` and you added the default tests for that mimetype in [test_classes.py](https://github.com/khulnasoft/ThreatMatrix/blob/master/tests/api_app/analyzers_manager/test_classes.py). - - [ ] If you created a new analyzer and it is free (does not require any API key), please add it in the `FREE_TO_USE_ANALYZERS` playbook by following [this guide](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/contribute/#how-to-modify-a-plugin). - - [ ] Check if it could make sense to add that analyzer/connector to other [freely available playbooks](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/usage/#list-of-pre-built-playbooks). - - [ ] I have provided the resulting raw JSON of a finished analysis and a screenshot of the results. - - [ ] If the plugin interacts with an external service, I have created an attribute called precisely `url` that contains this information. This is required for Health Checks. - - [ ] If the plugin requires mocked testing, `_monkeypatch()` was used in its class to apply the necessary decorators. - - [ ] I have added that raw JSON sample to the `MockUpResponse` of the `_monkeypatch()` method. This serves us to provide a valid sample for testing. + - [ ] I strictly followed the documentation ["How to create a Plugin"](https://khulnasoft.github.io/docs/ThreatMatrix/contribute/#how-to-add-a-new-plugin) + - [ ] [Usage](https://github.com/khulnasoft/docs/blob/main/docs/ThreatMatrix/usage.md) file was updated. + - [ ] [Advanced-Usage](https://github.com/khulnasoft/docs/blob/main/docs/ThreatMatrix/advanced_usage.md) was updated (in case the plugin provides additional optional configuration). + - [ ] I have dumped the configuration from Django Admin using the `dumpplugin` command and added it in the project as a data migration. (["How to share a plugin with the community"](https://khulnasoft.github.io/docs/ThreatMatrix/contribute/#how-to-share-your-plugin-with-the-community)) + - [ ] If a File analyzer was added and it supports a mimetype which is not already supported, you added a sample of that type inside the archive `test_files.zip` and you added the default tests for that mimetype in [test_classes.py](https://github.com/khulnasoft/ThreatMatrix/blob/master/tests/api_app/analyzers_manager/test_classes.py). + - [ ] If you created a new analyzer and it is free (does not require any API key), please add it in the `FREE_TO_USE_ANALYZERS` playbook by following [this guide](https://khulnasoft.github.io/docs/ThreatMatrix/contribute/#how-to-modify-a-plugin). + - [ ] Check if it could make sense to add that analyzer/connector to other [freely available playbooks](https://khulnasoft.github.io/docs/ThreatMatrix/usage/#list-of-pre-built-playbooks). + - [ ] I have provided the resulting raw JSON of a finished analysis and a screenshot of the results. + - [ ] If the plugin interacts with an external service, I have created an attribute called precisely `url` that contains this information. This is required for Health Checks. + - [ ] If the plugin requires mocked testing, `_monkeypatch()` was used in its class to apply the necessary decorators. + - [ ] I have added that raw JSON sample to the `MockUpResponse` of the `_monkeypatch()` method. This serves us to provide a valid sample for testing. - [ ] If external libraries/packages with restrictive licenses were used, they were added in the [Legal Notice](https://github.com/certego/ThreatMatrix/blob/master/.github/legal_notice.md) section. -- [ ] Linters (`Black`, `Flake`, `Isort`) gave 0 errors. If you have correctly installed [pre-commit](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/contribute/#how-to-start-setup-project-and-development-instance), it does these checks and adjustments on your behalf. +- [ ] Linters (`Black`, `Flake`, `Isort`) gave 0 errors. If you have correctly installed [pre-commit](https://khulnasoft.github.io/docs/ThreatMatrix/contribute/#how-to-start-setup-project-and-development-instance), it does these checks and adjustments on your behalf. - [ ] I have added tests for the feature/bug I solved (see `tests` folder). All the tests (new and old ones) gave 0 errors. -- [ ] If changes were made to an existing model/serializer/view, the docs were updated and regenerated (check [CONTRIBUTE.md](https://github.com/khulnasoft/ThreatMatrix/blob/master/docs/source/Contribute.md)). - [ ] If the GUI has been modified: - - [ ] I have a provided a screenshot of the result in the PR. - - [ ] I have created new frontend tests for the new component or updated existing ones. + - [ ] I have a provided a screenshot of the result in the PR. + - [ ] I have created new frontend tests for the new component or updated existing ones. - [ ] After you had submitted the PR, if `DeepSource`, `Django Doctors` or other third-party linters have triggered any alerts during the CI checks, I have solved those alerts. ### Important Rules - - If you miss to compile the Checklist properly, your PR won't be reviewed by the maintainers. -- Everytime you make changes to the PR and you think the work is done, you should explicitly ask for a review. After being reviewed and received a "change request", you should explicitly ask for a review again once you have made the requested changes. +- Everytime you make changes to the PR and you think the work is done, you should explicitly ask for a review. After being reviewed and received a "change request", you should explicitly ask for a review again once you have made the requested changes. \ No newline at end of file diff --git a/.github/release_template.md b/.github/release_template.md index 42f86c90..15765b79 100644 --- a/.github/release_template.md +++ b/.github/release_template.md @@ -17,7 +17,6 @@ WARNING: The release will be live within an hour! - [ ] Merge the PR to the `master` branch. **Note:** Only use "Merge and commit" as the merge strategy and not "Squash and merge". Using "Squash and merge" makes history between branches misaligned. - [ ] Remove the "wait" statement in the release description. - [ ] Publish new Post into official Twitter and LinkedIn accounts: - ```commandline published #ThreatMatrix vX.X.X! https://github.com/khulnasoft/ThreatMatrix/releases/tag/vX.X.X #ThreatIntelligence #CyberSecurity #OpenSource #OSINT #DFIR -``` +``` \ No newline at end of file diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml deleted file mode 100644 index 9c185854..00000000 --- a/.github/workflows/mirror.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Pushes the contents of the repo to the Codeberg mirror -name: 🪞 Mirror to Codeberg -on: - workflow_dispatch: - schedule: - - cron: '30 3 * * 0' # At 03:30 on Sunday -jobs: - codeberg: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: { fetch-depth: 0 } - - uses: pixta-dev/repository-mirroring-action@v1 - with: - target_repo_url: git@codeberg.org:khulnasoft-dev/ThreatMatrix.git - ssh_private_key: ${{ secrets.CODEBERG_SSH }} diff --git a/.github/workflows/pull_request_automation.yml b/.github/workflows/pull_request_automation.yml index ec04534f..a588a896 100644 --- a/.github/workflows/pull_request_automation.yml +++ b/.github/workflows/pull_request_automation.yml @@ -87,10 +87,12 @@ jobs: BUILDKIT_PROGRESS: "plain" STAGE: "ci" REPO_DOWNLOADER_ENABLED: false - - - name: Startup script launch (Fast) - if: "!contains(github.base_ref, 'master')" + + - name: Run Startup Script (Master Branch Only) + if: github.base_ref == 'master' run: | + echo "Starting CI build on 'master' branch" + set -e # Exit on error ./start ci up -- --build -d env: DOCKER_BUILDKIT: 1 @@ -123,7 +125,7 @@ jobs: - name: Set up NodeJS uses: actions/setup-node@v4 with: - node-version: 15 + node-version: 18 - name: Cache node modules uses: actions/cache@v4 with: diff --git a/.gitignore b/.gitignore index fe5ce5fe..a6d7a0f9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ test_files docker/env_file_app docker/env_file_postgres docker/env_file_integrations +docker/env_file_elasticsearch docker/custom.override.yml venv/ threat_matrix_test_env/ @@ -16,6 +17,8 @@ compose-elk.yml .python_history .viminfo +# certs +certs/ # docs docs_env/ diff --git a/README.md b/README.md index f248df96..dccdfa34 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Threat Matrix +Threat Matrix [![GitHub release (latest by date)](https://img.shields.io/github/v/release/khulnasoft/ThreatMatrix)](https://github.com/khulnasoft/ThreatMatrix/releases) [![GitHub Repo stars](https://img.shields.io/github/stars/khulnasoft/ThreatMatrix?style=social)](https://github.com/khulnasoft/ThreatMatrix/stargazers) @@ -17,7 +17,7 @@ [![DeepSource](https://app.deepsource.com/gh/khulnasoft/ThreatMatrix.svg/?label=resolved+issues&token=BSvKHrnk875Y0Bykb79GNo8w)](https://app.deepsource.com/gh/khulnasoft/ThreatMatrix/?ref=repository-badge) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/khulnasoft/ThreatMatrix/badge)](https://api.securityscorecards.dev/projects/github.com/khulnasoft/ThreatMatrix) [![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7120/badge)](https://bestpractices.coreinfrastructure.org/projects/7120) -[![Documentation Status](https://readthedocs.org/projects/threatmatrix/badge/?version=latest)](https://threatmatrix.readthedocs.io/en/latest/?badge=latest) +# Threat Matrix Do you want to get **threat intelligence data** about a malware, an IP address or a domain? Do you want to get this kind of data from multiple sources at the same time using **a single API request**? @@ -26,56 +26,54 @@ You are in the right place! ThreatMatrix is an Open Source solution for management of Threat Intelligence at scale. It integrates a number of analyzers available online and a lot of cutting-edge malware analysis tools. ### Features - This application is built to **scale out** and to **speed up the retrieval of threat info**. It provides: - - **Enrichment of Threat Intel** for files as well as observables (IP, Domain, URL, hash, etc). - A Fully-fledged REST APIs written in Django and Python. - An easy way to be integrated in your stack of security tools to automate common jobs usually performed, for instance, by SOC analysts manually. (Thanks to the official libraries [pythreatmatrix](https://github.com/khulnasoft/pythreatmatrix) and [go-threatmatrix](https://github.com/khulnasoft/go-threatmatrix)) - A **built-in GUI**: provides features such as dashboard, visualizations of analysis data, easy to use forms for requesting new analysis, etc. - A **framework** composed of modular components called **Plugins**: - - _analyzers_ that can be run to either retrieve data from external sources (like VirusTotal or AbuseIPDB) or to generate intel from internally available tools (like Yara or Oletools) - - _connectors_ that can be run to export data to external platforms (like MISP or OpenCTI) - - _pivots_ that are designed to trigger the execution of a chain of analysis and connect them to each other - - _visualizers_ that are designed to create custom visualizations of analyzers results - - _ingestors_ that allows to automatically ingest stream of observables or files to ThreatMatrix itself - - _playbooks_ that are meant to make analysis easily repeatable + - *analyzers* that can be run to either retrieve data from external sources (like VirusTotal or AbuseIPDB) or to generate intel from internally available tools (like Yara or Oletools) + - *connectors* that can be run to export data to external platforms (like MISP or OpenCTI) + - *pivots* that are designed to trigger the execution of a chain of analysis and connect them to each other + - *visualizers* that are designed to create custom visualizations of analyzers results + - *ingestors* that allows to automatically ingest stream of observables or files to ThreatMatrix itself + - *playbooks* that are meant to make analysis easily repeatable -### Documentation +### Documentation We try hard to keep our documentation well written, easy to understand and always updated. -All info about installation, usage, configuration and contribution can be found [here](https://threatmatrix.readthedocs.io/) +All info about installation, usage, configuration and contribution can be found [here](https://khulnasoft.github.io/docs/) ### Publications and Media -To know more about the project and its growth over time, you may be interested in reading [the official blog posts and/or videos about the project by clicking on this link](https://threatmatrix.readthedocs.io/en/latest/Introduction.html#publications-and-media) +To know more about the project and its growth over time, you may be interested in reading [the official blog posts and/or videos about the project by clicking on this link](https://khulnasoft.github.io/docs/ThreatMatrix/introduction/#publications-and-media) ### Available services or analyzers -You can see the full list of all available analyzers in the [documentation](https://threatmatrix.readthedocs.io/en/latest/Usage.html#available-analyzers). +You can see the full list of all available analyzers in the [documentation](https://khulnasoft.github.io/docs/ThreatMatrix/usage/#analyzers). -| Type | Analyzers Available | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Inbuilt modules | - Static Office Document, RTF, PDF, PE File Analysis and metadata extraction
- Strings Deobfuscation and analysis ([FLOSS](https://github.com/mandiant/flare-floss), [Stringsifter](https://github.com/mandiant/stringsifter), ...)
- PE Emulation with [Qiling](https://github.com/qilingframework/qiling) and [Speakeasy](https://github.com/mandiant/speakeasy)
- PE Signature verification
- PE Capabilities Extraction ([CAPA](https://github.com/mandiant/capa))
- Javascript Emulation ([Box-js](https://github.com/CapacitorSet/box-js))
- Android Malware Analysis ([Quark-Engine](https://github.com/quark-engine/quark-engine), ...)
- SPF and DMARC Validator
- Yara (a lot of public rules are available. You can also add your own rules)
- more... | -| External services | - Abuse.ch MalwareBazaar/URLhaus/Threatfox/YARAify
- GreyNoise v2
- Intezer
- VirusTotal v3
- Crowdsec
- URLscan
- Shodan
- AlienVault OTX
- Intelligence_X
- MISP
- many more.. | +| Type | Analyzers Available | +| -------------------------------------------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Inbuilt modules | - Static Office Document, RTF, PDF, PE File Analysis and metadata extraction
- Strings Deobfuscation and analysis ([FLOSS](https://github.com/mandiant/flare-floss), [Stringsifter](https://github.com/mandiant/stringsifter), ...)
- PE Emulation with [Qiling](https://github.com/qilingframework/qiling) and [Speakeasy](https://github.com/mandiant/speakeasy)
- PE Signature verification
- PE Capabilities Extraction ([CAPA](https://github.com/mandiant/capa))
- Javascript Emulation ([Box-js](https://github.com/CapacitorSet/box-js))
- Android Malware Analysis ([Quark-Engine](https://github.com/quark-engine/quark-engine), ...)
- SPF and DMARC Validator
- Yara (a lot of public rules are available. You can also add your own rules)
- more... | +| External services | - Abuse.ch MalwareBazaar/URLhaus/Threatfox/YARAify
- GreyNoise v2
- Intezer
- VirusTotal v3
- Crowdsec
- URLscan
- Shodan
- AlienVault OTX
- Intelligence_X
- MISP
- many more.. | ## Partnerships and sponsors As open source project maintainers, we strongly rely on external support to get the resources and time to work on keeping the project alive, with a constant release of new features, bug fixes and general improvements. -Because of this, we joined [Open Collective](https://opencollective.com/khulnasoft) to obtain non-profit equal level status which allows the organization to receive and manage donations transparently. Please support ThreatMatrix and all the community by choosing a plan (BRONZE, SILVER, etc). +Because of this, we joined [Open Collective](https://opencollective.com/threatmatrix-project) to obtain non-profit equal level status which allows the organization to receive and manage donations transparently. Please support ThreatMatrix and all the community by choosing a plan (BRONZE, SILVER, etc). - - + + ### 🥇 GOLD #### Certego - Certego Logo + Certego Logo [Certego](https://certego.net/?utm_source=threatmatrix) is a MDR (Managed Detection and Response) and Threat Intelligence Provider based in Italy. @@ -83,37 +81,45 @@ ThreatMatrix was born out of Certego's Threat intelligence R&D division and is c #### The Honeynet Project - Honeynet.org logo + Honeynet.org logo [The Honeynet Project](https://www.honeynet.org) is a non-profit organization working on creating open source cyber security tools and sharing knowledge about cyber threats. Thanks to Honeynet, we are hosting a public demo of the application [here](https://threatmatrix.honeynet.org). If you are interested, please contact a member of Honeynet to get access to the public service. #### Google Summer of Code - - GSoC logo + GSoC logo Since its birth this project has been participating in the [Google Summer of Code](https://summerofcode.withgoogle.com/) (GSoC)! If you are interested in participating in the next Google Summer of Code, check all the info available in the [dedicated repository](https://github.com/khulnasoft/gsoc)! + ### 🥈 SILVER #### ThreatHunter.ai - ThreatHunter.ai logo + ThreatHunter.ai logo [ThreatHunter.ai®](https://threathunter.ai?utm_source=threatmatrix), is a 100% Service-Disabled Veteran-Owned Small Business started in 2007 under the name Milton Security Group. ThreatHunter.ai is the global leader in Dynamic Threat Hunting. Operating a true 24x7x365 Security Operation Center with AI/ML-enhanced human Threat Hunters, ThreatHunter.ai has changed the industry in how threats are found, and mitigated in real time. For over 15 years, our teams of Threat Hunters have stopped hundreds of thousands of threats and assisted organizations in defending against threat actors around the clock. +### 🥉 BRONZE + #### Docker In 2021 ThreatMatrix joined the official [Docker Open Source Program](https://www.docker.com/blog/expanded-support-for-open-source-software-projects/). This allows ThreatMatrix developers to easily manage Docker images and focus on writing the code. You may find the official ThreatMatrix Docker images [here](https://hub.docker.com/search?q=khulnasoft). +#### DigitalOcean + +In 2022 ThreatMatrix joined the official [DigitalOcean Open Source Program](https://www.digitalocean.com/open-source?utm_medium=opensource&utm_source=ThreatMatrix). + + ## About the author and maintainers Feel free to contact the main developers at any time on Twitter: -- [KhulnaSoft DevSec](https://twitter.com/khulnasoft): Author and principal maintainer -- [Nx PKG](https://github.com/nxpkg): Backend Maintainer -- [KhulnaSoft Lab](https://github.com/khulnasoft-lab): Frontend Maintainer -- [KhulnaSoft BOT](https://github.com/khulnasoft-bot): Key Contributor +- [Matteo Lodi](https://twitter.com/matte_lodi): Author, Advisor and Administrator +- [Daniele Rosetti](https://github.com/drosetti): Administrator and Frontend Maintainer +- [Simone Berni](https://twitter.com/0ssig3no): Backend Maintainer +- [Federico Gibertoni](https://x.com/fgibertoni1): Maintainer and Community Assistant +- [Eshaan Bansal](https://twitter.com/eshaan7_): Key Contributor \ No newline at end of file diff --git a/api_app/admin.py b/api_app/admin.py index 36825f03..42f41907 100644 --- a/api_app/admin.py +++ b/api_app/admin.py @@ -5,6 +5,7 @@ from django.contrib import admin, messages from django.contrib.admin import widgets +from django.contrib.admin.models import LogEntry from django.db.models import JSONField, ManyToManyField from django.http import HttpRequest from prettyjson.widgets import PrettyJSONWidget @@ -254,3 +255,27 @@ class OrganizationPluginConfigurationAdminView(CustomAdminView): exclude = ["content_type", "object_id"] list_filter = ["organization", "content_type"] form = OrganizationPluginConfigurationForm + + +@admin.register(LogEntry) +class LogEntryAdmin(admin.ModelAdmin): + ordering = ["-action_time"] + list_display = [ + "pk", + "user", + "object_repr", + "action_flag", + "change_message", + "action_time", + ] + list_filter = ["user", "action_flag", "action_time", "content_type"] + search_fields = ["user__username", "object_repr", "change_message"] + + def has_delete_permission(self, request, obj=None): + return False + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False diff --git a/api_app/analyzers_manager/constants.py b/api_app/analyzers_manager/constants.py index d9dd6687..03070cf8 100644 --- a/api_app/analyzers_manager/constants.py +++ b/api_app/analyzers_manager/constants.py @@ -51,8 +51,8 @@ def calculate(cls, value: str) -> str: ): classification = cls.URL elif re.match( - r"^([\[\\]?\.[\]\\]?)?[a-z\d-]{1,63}" - r"(([\[\\]?\.[\]\\]?)[a-z\d-]{1,63})+$", + r"^([\[\\]?\.[\]\\]?)?[a-z\d\-_]{1,63}" + r"(([\[\\]?\.[\]\\]?)[a-z\d\-_]{1,63})+$", value, re.IGNORECASE, ): @@ -83,3 +83,11 @@ class AllTypes(models.TextChoices): HASH = "hash" GENERIC = "generic" FILE = "file" + + +class HTTPMethods(models.TextChoices): + GET = "get" + POST = "post" + PUT = "put" + PATCH = "patch" + DELETE = "delete" diff --git a/api_app/analyzers_manager/file_analyzers/androguard.py b/api_app/analyzers_manager/file_analyzers/androguard.py new file mode 100644 index 00000000..aeff03bb --- /dev/null +++ b/api_app/analyzers_manager/file_analyzers/androguard.py @@ -0,0 +1,35 @@ +import androguard +import androguard.core +import androguard.core.bytecodes +import androguard.core.bytecodes.apk + +from api_app.analyzers_manager.classes import FileAnalyzer + + +class AndroguardAnalyzer(FileAnalyzer): + + def update(self) -> bool: + pass + + def run(self): + + binary = self.read_file_bytes() + apk = androguard.core.bytecodes.apk.APK(binary, raw=True) + results = { + "app_name": apk.get_app_name(), + "permissions": apk.get_permissions(), + "activities": apk.get_activities(), + "requested_third_party_permissions": apk.get_requested_third_party_permissions(), + "providers": apk.get_providers(), + "features": apk.get_features(), + "receivers": apk.get_receivers(), + "services": apk.get_services(), + "is_valid_apk": apk.is_valid_APK(), + "min_sdk_version": apk.get_min_sdk_version(), + "max_sdk_version": apk.get_max_sdk_version(), + "target_sdk_version": apk.get_target_sdk_version(), + "android_version_code": apk.get_androidversion_code(), + "android_version_name": apk.get_androidversion_name(), + } + + return results diff --git a/api_app/analyzers_manager/file_analyzers/artifacts.py b/api_app/analyzers_manager/file_analyzers/artifacts.py index ce091293..ad2d4c3f 100644 --- a/api_app/analyzers_manager/file_analyzers/artifacts.py +++ b/api_app/analyzers_manager/file_analyzers/artifacts.py @@ -1,7 +1,6 @@ import logging from api_app.analyzers_manager.classes import DockerBasedAnalyzer, FileAnalyzer -from api_app.analyzers_manager.exceptions import AnalyzerRunException from tests.mock_utils import MockUpResponse logger = logging.getLogger(__name__) @@ -13,23 +12,15 @@ class Artifacts(FileAnalyzer, DockerBasedAnalyzer): # interval between http request polling poll_distance: int = 2 # http request polling max number of tries - max_tries: int = 10 - artifacts_report: bool = False - artifacts_analysis: bool = True + max_tries: int = 30 def update(self) -> bool: pass def run(self): - if self.artifacts_report and self.artifacts_analysis: - raise AnalyzerRunException( - "You can't run both report and analysis at the same time" - ) binary = self.read_file_bytes() fname = str(self.filename).replace("/", "_").replace(" ", "_") - args = [f"@{fname}"] - if self.artifacts_report: - args.append("--report") + args = [f"@{fname}", "-a", "-r"] req_data = {"args": args} req_files = {fname: binary} logger.info( diff --git a/api_app/analyzers_manager/file_analyzers/boxjs_scan.py b/api_app/analyzers_manager/file_analyzers/boxjs_scan.py index 4a99dea9..eef295f9 100644 --- a/api_app/analyzers_manager/file_analyzers/boxjs_scan.py +++ b/api_app/analyzers_manager/file_analyzers/boxjs_scan.py @@ -1,8 +1,12 @@ # This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix # See the file 'LICENSE' for copying permission. +import logging +from typing import List from api_app.analyzers_manager.classes import DockerBasedAnalyzer, FileAnalyzer +logger = logging.getLogger(__name__) + class BoxJS(FileAnalyzer, DockerBasedAnalyzer): name: str = "box-js" @@ -12,6 +16,10 @@ class BoxJS(FileAnalyzer, DockerBasedAnalyzer): # interval between http request polling (in secs) poll_distance: int = 12 + @classmethod + def update(cls) -> bool: + pass + def run(self): # construct a valid filename into which thug will save the result fname = str(self.filename).replace("/", "_").replace(" ", "_") @@ -36,5 +44,30 @@ def run(self): "callback_context": {"read_result_from": fname}, } req_files = {fname: binary} + report = self._docker_run(req_data, req_files) + + report["uris"] = [] + if "urls.json" in report and isinstance(report["urls.json"], List): + report["uris"].extend(report["urls.json"]) + if "active_urls.json" in report and isinstance( + report["active_urls.json"], List + ): + report["uris"].extend(report["active_urls.json"]) + if "IOC.json" in report and isinstance(report["IOC.json"], List): + for ioc in report["IOC.json"]: + try: + if "url" in ioc["type"].lower(): + report["uris"].append(ioc["value"]["url"]) + except KeyError: + error_message = ( + f"job_id {self.job_id} JSON structure changed in BoxJS report" + ) + logger.warning(error_message, stack_info=True) + self.report.errors.append(error_message) + report["uris"] = list(set(report["uris"])) # uniq + + return report - return self._docker_run(req_data, req_files) + # disable mockup connections for this class + @classmethod + def _monkeypatch(cls, patches: list = None) -> None: ... # noqa: E704 diff --git a/api_app/analyzers_manager/file_analyzers/doc_info.py b/api_app/analyzers_manager/file_analyzers/doc_info.py index 2b0cf261..67bf1c68 100644 --- a/api_app/analyzers_manager/file_analyzers/doc_info.py +++ b/api_app/analyzers_manager/file_analyzers/doc_info.py @@ -11,12 +11,15 @@ from re import sub from typing import Dict, List +import docxpy import olefile from defusedxml.ElementTree import fromstring -from oletools import mraptor +from defusedxml.minidom import parseString +from oletools import mraptor, oleid, oleobj from oletools.common.clsid import KNOWN_CLSIDS from oletools.msodde import process_maybe_encrypted as msodde_process_maybe_encrypted from oletools.olevba import VBA_Parser +from oletools.ooxml import XmlParser from api_app.analyzers_manager.classes import FileAnalyzer from api_app.analyzers_manager.models import MimeTypes @@ -29,6 +32,15 @@ except Exception as e: logger.exception(e) +XML_H_SCHEMA = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" +) +SCHEMA_DOMAINS = [ + "schemas.openxmlformats.org", + "schemas-microsoft-com", + "schemas.microsoft.com", +] + class CannotDecryptException(Exception): pass @@ -50,7 +62,7 @@ def update(self) -> bool: pass def run(self): - results = {} + results = {"uris": []} # olevba try: @@ -110,12 +122,12 @@ def run(self): ) # analyze macros - analyzer_results = self.vbaparser.analyze_macros() + analyzer_results = self.vbaparser.analyze_macros(True, True) # it gives None if it does not find anything if analyzer_results: analyze_macro_results = [] for kw_type, keyword, description in analyzer_results: - if kw_type != "Hex String": + if kw_type not in ("Hex String", "Base64 String"): analyze_macro_result = { "type": kw_type, "keyword": keyword, @@ -124,12 +136,17 @@ def run(self): analyze_macro_results.append(analyze_macro_result) self.olevba_results["analyze_macro"] = analyze_macro_results - results["extracted_CVEs"] = self.analyze_for_cve() + results["olevba"] = self.olevba_results + + if self.file_mimetype != MimeTypes.ONE_NOTE.value: + results["msodde"] = self.analyze_msodde() except CannotDecryptException as e: logger.info(e) except Exception as e: - error_message = f"job_id {self.job_id} vba parser failed. Error: {e}" + error_message = ( + f"job_id {self.job_id} doc info extraction failed. Error: {e}" + ) logger.warning(error_message, stack_info=True) self.report.errors.append(error_message) self.report.save() @@ -137,18 +154,46 @@ def run(self): if self.vbaparser: self.vbaparser.close() - results["olevba"] = self.olevba_results - if self.file_mimetype != MimeTypes.ONE_NOTE.value: - results["msodde"] = self.analyze_msodde() - if self.file_mimetype in [ - MimeTypes.WORD1.value, - MimeTypes.WORD2.value, - MimeTypes.ZIP1.value, - MimeTypes.ZIP2.value, - ]: - results["follina"] = self.analyze_for_follina_cve() + try: + if self.file_mimetype in [ + MimeTypes.WORD1.value, + MimeTypes.WORD2.value, + MimeTypes.ZIP1.value, + MimeTypes.ZIP2.value, + ]: + results["follina"] = self.analyze_for_follina_cve() + results["uris"].extend(self.get_docx_urls()) + + results["extracted_CVEs"] = self.analyze_for_cve() + results["uris"].extend(self.get_external_relationships()) + results["uris"].extend(self.extract_urls_from_IOCs()) + results["uris"] = list(set(results["uris"])) # make it uniq + except Exception as e: + error_message = ( + f"job_id {self.job_id} special extractions failed. Error: {e}" + ) + logger.warning(error_message, stack_info=True) + self.report.errors.append(error_message) + self.report.save() + return results + def extract_urls_from_IOCs(self): + urls = [] + # we have to re-parse the file entirely because the functions called before this one + # alter the internal state of the parser and as a result the IOC section is empty + vbaparser = VBA_Parser(self.filepath) + analyzer_results = vbaparser.analyze_macros(True, True) + + # it gives None if it does not find anything + if analyzer_results: + for kw_type, keyword, description in analyzer_results: + if kw_type == "IOC" and description == "URL": + urls.append(keyword) + if vbaparser: + vbaparser.close() + return urls + def analyze_for_follina_cve(self) -> List[str]: hits = [] try: @@ -172,25 +217,122 @@ def analyze_for_follina_cve(self) -> List[str]: target = xml_node.attrib.get("Target") if target: target = target.strip().lower() - hits += re.findall(r"mhtml:(https?://.*?)!", target) + # join the list as uniq string due to the other non-matched + # group in the OR mutual exclusive expressions + matches = re.findall( + r"((? List: pattern = r"CVE-\d{4}-\d{4,7}" results = [] - ole = olefile.OleFileIO(self.filepath) - for entry in sorted(ole.listdir(storages=True)): - clsid = ole.getclsid(entry) - if clsid_text := KNOWN_CLSIDS.get(clsid.upper(), None): - if "cve" in clsid_text.lower(): - results.append( + try: + olefile.isOleFile(self.filepath) + ole = olefile.OleFileIO(self.filepath) + except olefile.olefile.NotOleFileError: + logger.info("not an OLE2 structured storage file, do not proceed.") + else: + for entry in sorted(ole.listdir(storages=True)): + clsid = ole.getclsid(entry) + if clsid_text := KNOWN_CLSIDS.get(clsid.upper(), None): + if "cve" in clsid_text.lower(): + results.append( + { + "clsid": clsid, + "info": clsid_text, + "CVEs": list(re.findall(pattern, clsid_text)), + } + ) + return results + + def get_external_relationships(self) -> List: + external_relationships = [] + try: + olefile.isOleFile(self.filepath) + oid = oleid.OleID(self.filepath) + except olefile.olefile.NotOleFileError: + logger.info("not an OLE2 structured storage file, do not proceed.") + else: + if sum(i.value for i in oid.check() if i.id == "ext_rels") > 1: + xml_parser = XmlParser(self.filepath) + for relationship, target in oleobj.find_external_relationships( + xml_parser + ): + external_relationships.append( { - "clsid": clsid, - "info": clsid_text, - "CVEs": list(re.findall(pattern, clsid_text)), + "relationship": relationship, + "target": target, } ) - return results + return external_relationships + + def get_docx_urls(self) -> List: + urls = [] + pages_count = 0 + + try: + document = zipfile.ZipFile(self.filepath) + except zipfile.BadZipFile as e: # check if docx document + error_message = f"job_id {self.job_id} docx bad zip file: {e}" + logger.warning(error_message, stack_info=True) + self.report.errors.append(error_message) + else: + try: + dxml = document.read("docProps/app.xml") + pages_count = int( + parseString(dxml) + .getElementsByTagName("Pages")[0] + .childNodes[0] + .nodeValue + ) + except KeyError: + logger.info( + "number of pages not found, maybe the file is malformed, " + "proceed anyway in order to not lose the possibly contained IOCs" + ) + + if pages_count <= 1: + # extract urls from text + try: + doc = docxpy.DOCReader(self.filepath) + doc.process() + except Exception as e: + error_message = ( + f"job_id {self.job_id} docxpy url extraction failed. Error: {e}" + ) + logger.warning(error_message, stack_info=True) + self.report.errors.append(error_message) + else: + # decode bytes like links + links = [ + link.decode() if isinstance(link, bytes) else link + for link in doc.data["links"][0] + ] + # remove empty strings + links = [link for link in links if link != ""] + urls.extend(links) + + # also parse xml in case docxpy missed some links + try: + for relationship in list( + fromstring(document.read("word/_rels/document.xml.rels")) + ): + # exclude xml schema urls + if relationship.attrib["Type"] == XML_H_SCHEMA and any( + domain in relationship.attrib["Target"] + for domain in SCHEMA_DOMAINS + ): + urls.append(relationship.attrib["Target"]) + except KeyError as e: + error_message = ( + f"job_id {self.job_id} no xml rels found. Error: {e}" + ) + logger.warning(error_message, stack_info=True) + self.report.errors.append(error_message) + return urls def analyze_msodde(self): try: diff --git a/api_app/analyzers_manager/file_analyzers/droidlysis.py b/api_app/analyzers_manager/file_analyzers/droidlysis.py index 039a4350..bf2b8870 100644 --- a/api_app/analyzers_manager/file_analyzers/droidlysis.py +++ b/api_app/analyzers_manager/file_analyzers/droidlysis.py @@ -12,7 +12,7 @@ class DroidLysis(FileAnalyzer, DockerBasedAnalyzer): # interval between http request polling poll_distance: int = 2 # http request polling max number of tries - max_tries: int = 10 + max_tries: int = 30 def update(self) -> bool: pass diff --git a/api_app/analyzers_manager/file_analyzers/lnk_info.py b/api_app/analyzers_manager/file_analyzers/lnk_info.py new file mode 100644 index 00000000..b8dfeba7 --- /dev/null +++ b/api_app/analyzers_manager/file_analyzers/lnk_info.py @@ -0,0 +1,37 @@ +# This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix +# See the file 'LICENSE' for copying permission. + +import logging +import re + +import pylnk3 + +from api_app.analyzers_manager.classes import FileAnalyzer +from api_app.analyzers_manager.constants import ObservableTypes + +logger = logging.getLogger(__name__) + + +class LnkInfo(FileAnalyzer): + def update(self) -> bool: + pass + + def run(self): + result = {"uris": []} + try: + parsed = pylnk3.parse(self.filepath) + except Exception as e: + error_message = f"job_id {self.job_id} cannot parse lnk file. Error: {e}" + logger.warning(error_message, stack_info=False) + self.report.errors.append(error_message) + else: + if arguments := getattr(parsed, "arguments", None): + args = arguments.split() + for a in args: + if ObservableTypes.calculate(a) == ObservableTypes.URL: + # remove strings delimiters used in commands + a = re.sub(r"[\"\']", "", a) + result["uris"].append(a) + + result["uris"] = list(set(result["uris"])) + return result diff --git a/api_app/analyzers_manager/file_analyzers/mwdb_scan.py b/api_app/analyzers_manager/file_analyzers/mwdb_scan.py index 5496d641..f3edc04a 100644 --- a/api_app/analyzers_manager/file_analyzers/mwdb_scan.py +++ b/api_app/analyzers_manager/file_analyzers/mwdb_scan.py @@ -112,7 +112,7 @@ def run(self): else: try: file_info = self.mwdb.query_file(query) - except HTTPError: + except (HTTPError, mwdblib.exc.ObjectNotFoundError): result["not_found"] = True return result else: diff --git a/api_app/analyzers_manager/file_analyzers/onenote.py b/api_app/analyzers_manager/file_analyzers/onenote.py index fa92fd40..b5cb98eb 100644 --- a/api_app/analyzers_manager/file_analyzers/onenote.py +++ b/api_app/analyzers_manager/file_analyzers/onenote.py @@ -1,6 +1,7 @@ # This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix # See the file 'LICENSE' for copying permission. +import base64 import json import logging @@ -12,7 +13,16 @@ class OneNoteInfo(FileAnalyzer): + def update(self) -> bool: + pass + def run(self): with open(self.filepath, "rb") as file: results = json.loads(process_onenote_file(file, "", "", True)) + results["stored_base64"] = [] + for _, f in results["files"].items(): + if f["extension"] not in (".png", ".jpg"): + results["stored_base64"].append( + base64.b64encode(bytes.fromhex(f["content"])).decode("ascii") + ) return results diff --git a/api_app/analyzers_manager/file_analyzers/pdf_info.py b/api_app/analyzers_manager/file_analyzers/pdf_info.py index 2d490a5c..3335759c 100644 --- a/api_app/analyzers_manager/file_analyzers/pdf_info.py +++ b/api_app/analyzers_manager/file_analyzers/pdf_info.py @@ -23,7 +23,7 @@ def update(cls) -> bool: pass def run(self): - self.results = {"peepdf": {}, "pdfid": {}} + self.results = {"peepdf": {}, "pdfid": {}, "uris": []} # the analysis fails only when BOTH fails peepdf_success = self.__peepdf_analysis() pdfid_success = self.__pdfid_analysis() @@ -31,12 +31,13 @@ def run(self): raise AnalyzerRunException("both peepdf and pdfid failed") # pivot uris in the pdf only if we have one page - if "reports" in self.results["pdfid"] and isinstance( - self.results["pdfid"]["reports"], list + if ( + "reports" in self.results["pdfid"] + and isinstance(self.results["pdfid"]["reports"], list) + and peepdf_success ): for elem in self.results["pdfid"]["reports"]: if "/Page" in elem and elem["/Page"] == 1: - self.results["uris"] = [] for s in self.results["peepdf"]["stats"]: self.results["uris"].extend(s["uris"]) diff --git a/api_app/analyzers_manager/file_analyzers/strings_info.py b/api_app/analyzers_manager/file_analyzers/strings_info.py index 4e5e65a9..1ae1649a 100644 --- a/api_app/analyzers_manager/file_analyzers/strings_info.py +++ b/api_app/analyzers_manager/file_analyzers/strings_info.py @@ -4,6 +4,8 @@ from json import dumps as json_dumps from api_app.analyzers_manager.classes import DockerBasedAnalyzer, FileAnalyzer +from api_app.analyzers_manager.constants import ObservableTypes +from api_app.analyzers_manager.models import MimeTypes class StringsInfo(FileAnalyzer, DockerBasedAnalyzer): @@ -23,6 +25,9 @@ class StringsInfo(FileAnalyzer, DockerBasedAnalyzer): # CARE!! ranked_strings could be cpu/ram intensive and very slow rank_strings: int + def update(self) -> bool: + pass + def run(self): # get binary binary = self.read_file_bytes() @@ -51,5 +56,49 @@ def run(self): result = { "data": [row[: self.max_characters_for_string] for row in result], "exceeded_max_number_of_strings": exceed_max_strings, + "uris": [], } + + if self.file_mimetype in [ + MimeTypes.JAVASCRIPT1.value, + MimeTypes.JAVASCRIPT2.value, + MimeTypes.JAVASCRIPT3.value, + MimeTypes.VB_SCRIPT.value, + MimeTypes.ONE_NOTE.value, + MimeTypes.PDF.value, + MimeTypes.HTML.value, + MimeTypes.EXCEL1.value, + MimeTypes.EXCEL2.value, + MimeTypes.EXCEL_MACRO1.value, + MimeTypes.EXCEL_MACRO2.value, + MimeTypes.DOC.value, + MimeTypes.WORD1.value, + MimeTypes.WORD2.value, + MimeTypes.XML1.value, + MimeTypes.XML2.value, + MimeTypes.POWERPOINT.value, + MimeTypes.OFFICE.value, + MimeTypes.EML.value, + MimeTypes.JSON.value, + ]: + import re + + for d in result["data"]: + if ObservableTypes.calculate(d) == ObservableTypes.URL: + extracted_urls = re.findall( + r"[a-z]{1,5}://[a-z\d-]{1,200}" + r"(?:\.[a-zA-Z\d\u2044\u2215!#$&(-;=?-\[\]_~]{1,200})+" + r"(?::\d{2,6})?" + r"(?:/[a-zA-Z\d\u2044\u2215!#$&(-;=?-\[\]_~]{1,200})*" + r"(?:\.\w+)?", + d, + ) + for u in extracted_urls: + result["uris"].append(u) + result["uris"] = list(set(result["uris"])) + return result + + # disable mockup connections for this class + @classmethod + def _monkeypatch(cls, patches: list = None) -> None: ... # noqa: E704 diff --git a/api_app/analyzers_manager/migrations/0120_alter_analyzerconfig_not_supported_filetypes_and_more.py b/api_app/analyzers_manager/migrations/0120_alter_analyzerconfig_not_supported_filetypes_and_more.py new file mode 100644 index 00000000..2396c6a3 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0120_alter_analyzerconfig_not_supported_filetypes_and_more.py @@ -0,0 +1,180 @@ +# Generated by Django 4.2.14 on 2024-08-12 15:12 + +from django.db import migrations, models + +import api_app.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("analyzers_manager", "0119_analyzer_config_apk_artifacts"), + ] + + operations = [ + migrations.AlterField( + model_name="analyzerconfig", + name="not_supported_filetypes", + field=api_app.fields.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ("application/w-script-file", "Wscript"), + ("application/javascript", "Javascript1"), + ("application/x-javascript", "Javascript2"), + ("text/javascript", "Javascript3"), + ("application/x-vbscript", "Vb Script"), + ("text/x-ms-iqy", "Iqy"), + ("application/vnd.android.package-archive", "Apk"), + ("application/x-dex", "Dex"), + ("application/onenote", "One Note"), + ("application/zip", "Zip1"), + ("multipart/x-zip", "Zip2"), + ("application/java-archive", "Java"), + ("text/rtf", "Rtf1"), + ("application/rtf", "Rtf2"), + ("application/x-sharedlib", "Shared Lib"), + ("application/vnd.microsoft.portable-executable", "Exe"), + ("application/x-elf", "Elf"), + ("application/octet-stream", "Octet"), + ("application/vnd.tcpdump.pcap", "Pcap"), + ("application/pdf", "Pdf"), + ("text/html", "Html"), + ("application/x-mspublisher", "Pub"), + ("application/vnd.ms-excel.addin.macroEnabled", "Excel Macro1"), + ( + "application/vnd.ms-excel.sheet.macroEnabled.12", + "Excel Macro2", + ), + ("application/vnd.ms-excel", "Excel1"), + ("application/excel", "Excel2"), + ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Doc", + ), + ("application/xml", "Xml1"), + ("text/xml", "Xml2"), + ("application/encrypted", "Encrypted"), + ("text/plain", "Plain"), + ("text/csv", "Csv"), + ( + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "Pptx", + ), + ("application/msword", "Word1"), + ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "Word2", + ), + ("application/vnd.ms-powerpoint", "Powerpoint"), + ("application/vnd.ms-office", "Office"), + ("application/x-binary", "Binary"), + ("application/x-macbinary", "Mac1"), + ("application/mac-binary", "Mac2"), + ("application/x-mach-binary", "Mac3"), + ("application/x-zip-compressed", "Compress1"), + ("application/x-compressed", "Compress2"), + ("application/vnd.ms-outlook", "Outlook"), + ("message/rfc822", "Eml"), + ("application/pkcs7-signature", "Pkcs7"), + ("application/x-pkcs7-signature", "Xpkcs7"), + ("multipart/mixed", "Mixed"), + ("text/x-shellscript", "X Shellscript"), + ("application/x-chrome-extension", "Crx"), + ("application/json", "Json"), + ("application/x-executable", "Executable"), + ("application/x-ms-shortcut", "Lnk"), + ("text/x-java", "Java2"), + ("text/x-kotlin", "Kotlin"), + ("text/x-swift", "Swift"), + ("text/x-objective-c", "Objective C"), + ], + max_length=90, + ), + blank=True, + default=list, + size=None, + ), + ), + migrations.AlterField( + model_name="analyzerconfig", + name="supported_filetypes", + field=api_app.fields.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ("application/w-script-file", "Wscript"), + ("application/javascript", "Javascript1"), + ("application/x-javascript", "Javascript2"), + ("text/javascript", "Javascript3"), + ("application/x-vbscript", "Vb Script"), + ("text/x-ms-iqy", "Iqy"), + ("application/vnd.android.package-archive", "Apk"), + ("application/x-dex", "Dex"), + ("application/onenote", "One Note"), + ("application/zip", "Zip1"), + ("multipart/x-zip", "Zip2"), + ("application/java-archive", "Java"), + ("text/rtf", "Rtf1"), + ("application/rtf", "Rtf2"), + ("application/x-sharedlib", "Shared Lib"), + ("application/vnd.microsoft.portable-executable", "Exe"), + ("application/x-elf", "Elf"), + ("application/octet-stream", "Octet"), + ("application/vnd.tcpdump.pcap", "Pcap"), + ("application/pdf", "Pdf"), + ("text/html", "Html"), + ("application/x-mspublisher", "Pub"), + ("application/vnd.ms-excel.addin.macroEnabled", "Excel Macro1"), + ( + "application/vnd.ms-excel.sheet.macroEnabled.12", + "Excel Macro2", + ), + ("application/vnd.ms-excel", "Excel1"), + ("application/excel", "Excel2"), + ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Doc", + ), + ("application/xml", "Xml1"), + ("text/xml", "Xml2"), + ("application/encrypted", "Encrypted"), + ("text/plain", "Plain"), + ("text/csv", "Csv"), + ( + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "Pptx", + ), + ("application/msword", "Word1"), + ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "Word2", + ), + ("application/vnd.ms-powerpoint", "Powerpoint"), + ("application/vnd.ms-office", "Office"), + ("application/x-binary", "Binary"), + ("application/x-macbinary", "Mac1"), + ("application/mac-binary", "Mac2"), + ("application/x-mach-binary", "Mac3"), + ("application/x-zip-compressed", "Compress1"), + ("application/x-compressed", "Compress2"), + ("application/vnd.ms-outlook", "Outlook"), + ("message/rfc822", "Eml"), + ("application/pkcs7-signature", "Pkcs7"), + ("application/x-pkcs7-signature", "Xpkcs7"), + ("multipart/mixed", "Mixed"), + ("text/x-shellscript", "X Shellscript"), + ("application/x-chrome-extension", "Crx"), + ("application/json", "Json"), + ("application/x-executable", "Executable"), + ("application/x-ms-shortcut", "Lnk"), + ("text/x-java", "Java2"), + ("text/x-kotlin", "Kotlin"), + ("text/x-swift", "Swift"), + ("text/x-objective-c", "Objective C"), + ], + max_length=90, + ), + blank=True, + default=list, + size=None, + ), + ), + ] diff --git a/api_app/analyzers_manager/migrations/0121_analyzer_config_lnk_info.py b/api_app/analyzers_manager/migrations/0121_analyzer_config_lnk_info.py new file mode 100644 index 00000000..de1dfce5 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0121_analyzer_config_lnk_info.py @@ -0,0 +1,120 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "lnk_info.LnkInfo", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "Lnk_Info", + "description": "Extracting information from LNK files.", + "disabled": False, + "soft_time_limit": 30, + "routing_key": "local", + "health_check_status": True, + "type": "file", + "docker_based": False, + "maximum_tlp": "RED", + "observable_supported": [], + "supported_filetypes": ["application/x-ms-shortcut"], + "run_hash": False, + "run_hash_type": "", + "not_supported_filetypes": [], + "model": "analyzers_manager.AnalyzerConfig", +} + +params = [] + +values = [] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ForwardManyToOneDescriptor, ForwardOneToOneDescriptor] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0062_alter_parameter_python_module"), + ( + "analyzers_manager", + "0120_alter_analyzerconfig_not_supported_filetypes_and_more", + ), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/migrations/0122_alter_soft_time_limit.py b/api_app/analyzers_manager/migrations/0122_alter_soft_time_limit.py new file mode 100644 index 00000000..13f1391d --- /dev/null +++ b/api_app/analyzers_manager/migrations/0122_alter_soft_time_limit.py @@ -0,0 +1,34 @@ +from django.db import migrations + + +def migrate(apps, schema_editor): + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + plugin_name = "Droidlysis" + + try: + plugin = AnalyzerConfig.objects.get(name=plugin_name) + plugin.soft_time_limit = 60 + plugin.save() + except AnalyzerConfig.DoesNotExist: + pass + + +def reverse_migrate(apps, schema_editor): + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + plugin_name = "Droidlysis" + + try: + plugin = AnalyzerConfig.objects.get(name=plugin_name) + plugin.soft_time_limit = 20 + plugin.save() + except AnalyzerConfig.DoesNotExist: + pass + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("analyzers_manager", "0121_analyzer_config_lnk_info"), + ] + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/migrations/0123_basic_observable_analyzer.py b/api_app/analyzers_manager/migrations/0123_basic_observable_analyzer.py new file mode 100644 index 00000000..a5073d00 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0123_basic_observable_analyzer.py @@ -0,0 +1,87 @@ +from django.db import migrations + + +def migrate_python_module_pivot(apps, schema_editor): + PythonModule = apps.get_model("api_app", "PythonModule") + pm, _ = PythonModule.objects.update_or_create( + module="basic_observable_analyzer.BasicObservableAnalyzer", + base_path="api_app.analyzers_manager.observable_analyzers", + ) + Parameter = apps.get_model("api_app", "Parameter") + Parameter.objects.get_or_create( + name="url", + type="str", + python_module=pm, + is_secret=False, + required=True, + defaults={ + "description": "URL of the instance you want to connect to", + }, + ) + Parameter.objects.get_or_create( + name="api_key_name", + type="str", + python_module=pm, + is_secret=True, + required=False, + defaults={ + "description": "API key required for authentication", + }, + ) + Parameter.objects.get_or_create( + name="headers", + type="dict", + python_module=pm, + is_secret=False, + required=False, + defaults={ + "description": "Headers used for the request", + }, + ) + Parameter.objects.get_or_create( + name="http_method", + type="str", + python_module=pm, + is_secret=False, + required=True, + defaults={ + "description": "HTTP method used for the request", + }, + ) + Parameter.objects.get_or_create( + name="params", + type="dict", + python_module=pm, + is_secret=False, + required=False, + defaults={ + "description": "Params used for the query string or request payload", + }, + ) + Parameter.objects.get_or_create( + name="certificate", + type="str", + python_module=pm, + is_secret=True, + required=False, + defaults={ + "description": "Instance SSL certificate (multiline string).", + }, + ) + + +def reverse_migrate_module_pivot(apps, schema_editor): + PythonModule = apps.get_model("api_app", "PythonModule") + PythonModule.objects.get( + module="basic_observable_analyzer.BasicObservableAnalyzer", + base_path="api_app.analyzers_manager.observable_analyzers", + ).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("analyzers_manager", "0122_alter_soft_time_limit"), + ] + operations = [ + migrations.RunPython(migrate_python_module_pivot, reverse_migrate_module_pivot) + ] diff --git a/api_app/analyzers_manager/migrations/0124_analyzer_config_androguard.py b/api_app/analyzers_manager/migrations/0124_analyzer_config_androguard.py new file mode 100644 index 00000000..0f777d97 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0124_analyzer_config_androguard.py @@ -0,0 +1,129 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "androguard.AndroguardAnalyzer", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "Androguard", + "description": "[Androguard]\r\n(https://github.com/androguard/androguard) is a python tool to reverse engineer android applications.", + "disabled": False, + "soft_time_limit": 60, + "routing_key": "default", + "health_check_status": True, + "type": "file", + "docker_based": False, + "maximum_tlp": "RED", + "observable_supported": [], + "supported_filetypes": [ + "application/vnd.android.package-archive", + "application/x-dex", + "application/zip", + "application/java-archive", + ], + "run_hash": False, + "run_hash_type": "", + "not_supported_filetypes": [], + "model": "analyzers_manager.AnalyzerConfig", +} + +params = [] + +values = [] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ + ForwardManyToOneDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, + ForwardOneToOneDescriptor, + ] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0062_alter_parameter_python_module"), + ("analyzers_manager", "0123_basic_observable_analyzer"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/migrations/0125_update_yara_repo.py b/api_app/analyzers_manager/migrations/0125_update_yara_repo.py new file mode 100644 index 00000000..cbb919bc --- /dev/null +++ b/api_app/analyzers_manager/migrations/0125_update_yara_repo.py @@ -0,0 +1,40 @@ +from django.db import migrations + + +def migrate(apps, schema_editor): + PythonModule = apps.get_model("api_app", "PythonModule") + PluginConfig = apps.get_model("api_app", "PluginConfig") + + pm = PythonModule.objects.get( + module="yara_scan.YaraScan", + base_path="api_app.analyzers_manager.file_analyzers", + ) + param = pm.parameters.get(name="repositories") + pc = PluginConfig.objects.get(parameter=param) + pc.value.append("https://yaraify-api.abuse.ch/download/yaraify-rules.zip") + pc.value.remove("https://yaraify-api.abuse.ch/yarahub/yaraify-rules.zip") + pc.save() + + +def reverse_migrate(apps, schema_editor): + PythonModule = apps.get_model("api_app", "PythonModule") + PluginConfig = apps.get_model("api_app", "PluginConfig") + + pm = PythonModule.objects.get( + module="yara_scan.YaraScan", + base_path="api_app.analyzers_manager.file_analyzers", + ) + param = pm.parameters.get(name="repositories") + pc = PluginConfig.objects.get(parameter=param) + pc.value.remove("https://yaraify-api.abuse.ch/download/yaraify-rules.zip") + pc.value.append("https://yaraify-api.abuse.ch/yarahub/yaraify-rules.zip") + pc.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("analyzers_manager", "0124_analyzer_config_androguard"), + ] + operations = [ + migrations.RunPython(migrate, reverse_migrate), + ] diff --git a/api_app/analyzers_manager/migrations/0126_analyzer_config_nerd_analyzer.py b/api_app/analyzers_manager/migrations/0126_analyzer_config_nerd_analyzer.py new file mode 100644 index 00000000..03e001e3 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0126_analyzer_config_nerd_analyzer.py @@ -0,0 +1,163 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "nerd.NERD", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "NERD_analyzer", + "description": "scan an IP address against NERD database.\r\nBefore using you must set your api_key and nerd_analysis.\r\nYou can get your api_key on nerd.cesnet.cz.\r\nSet nerd_analysis to:\r\n- basic - returns basic information\r\n- full - returns all information in DB\r\n- fmp - returns only FMP score\r\n- rep - returns only the reputation score", + "disabled": False, + "soft_time_limit": 60, + "routing_key": "default", + "health_check_status": True, + "type": "observable", + "docker_based": False, + "maximum_tlp": "AMBER", + "observable_supported": ["ip"], + "supported_filetypes": [], + "run_hash": False, + "run_hash_type": "", + "not_supported_filetypes": [], + "model": "analyzers_manager.AnalyzerConfig", +} + +params = [ + { + "python_module": { + "module": "nerd.NERD", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "api_key_name", + "type": "str", + "description": "Set your api_key before running. You can get one on nerd.cesnet.cz", + "is_secret": True, + "required": True, + }, + { + "python_module": { + "module": "nerd.NERD", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "nerd_analysis", + "type": "str", + "description": "Set analysis type to basic, full, rep or fmp", + "is_secret": False, + "required": True, + }, +] + +values = [ + { + "parameter": { + "python_module": { + "module": "nerd.NERD", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "nerd_analysis", + "type": "str", + "description": "Set analysis type to basic, full, rep or fmp", + "is_secret": False, + "required": True, + }, + "analyzer_config": "NERD_analyzer", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": "basic", + "updated_at": "2024-10-11T14:00:47.545904Z", + "owner": None, + } +] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ForwardManyToOneDescriptor, ForwardOneToOneDescriptor] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0062_alter_parameter_python_module"), + ("analyzers_manager", "0125_update_yara_repo"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/migrations/0127_analyzer_config_dshield.py b/api_app/analyzers_manager/migrations/0127_analyzer_config_dshield.py new file mode 100644 index 00000000..7252ef5c --- /dev/null +++ b/api_app/analyzers_manager/migrations/0127_analyzer_config_dshield.py @@ -0,0 +1,124 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "dshield.DShield", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "DShield", + "description": "Service Provided by [DShield](https://www.dshield.org/) to get useful information about IP addresses", + "disabled": False, + "soft_time_limit": 30, + "routing_key": "default", + "health_check_status": True, + "type": "observable", + "docker_based": False, + "maximum_tlp": "AMBER", + "observable_supported": ["ip"], + "supported_filetypes": [], + "run_hash": False, + "run_hash_type": "", + "not_supported_filetypes": [], + "model": "analyzers_manager.AnalyzerConfig", +} + +params = [] + +values = [] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ + ForwardManyToOneDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, + ForwardOneToOneDescriptor, + ] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0062_alter_parameter_python_module"), + ("analyzers_manager", "0126_analyzer_config_nerd_analyzer"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/models.py b/api_app/analyzers_manager/models.py index b0977efd..f285b662 100644 --- a/api_app/analyzers_manager/models.py +++ b/api_app/analyzers_manager/models.py @@ -91,6 +91,7 @@ class MimeTypes(models.TextChoices): KOTLIN = "text/x-kotlin" SWIFT = "text/x-swift" OBJECTIVE_C_CODE = "text/x-objective-c" + LNK = "application/x-ms-shortcut" @classmethod def _calculate_from_filename(cls, file_name: str) -> Optional["MimeTypes"]: diff --git a/api_app/analyzers_manager/observable_analyzers/basic_observable_analyzer.py b/api_app/analyzers_manager/observable_analyzers/basic_observable_analyzer.py new file mode 100644 index 00000000..3b938790 --- /dev/null +++ b/api_app/analyzers_manager/observable_analyzers/basic_observable_analyzer.py @@ -0,0 +1,105 @@ +import base64 +import logging +from tempfile import NamedTemporaryFile + +import requests + +from api_app.analyzers_manager.classes import ObservableAnalyzer +from api_app.analyzers_manager.constants import HTTPMethods +from api_app.analyzers_manager.exceptions import ( + AnalyzerConfigurationException, + AnalyzerRunException, +) +from tests.mock_utils import MockUpResponse, if_mock_connections, patch + +logger = logging.getLogger(__name__) + + +class BasicObservableAnalyzer(ObservableAnalyzer): + url: str + headers: dict + params: dict + _certificate: str + _api_key_name: str + http_method: str = "get" + + @staticmethod + def _clean_certificate(cert): + return ( + cert.replace("-----BEGIN CERTIFICATE-----", "-----BEGIN_CERTIFICATE-----") + .replace("-----END CERTIFICATE-----", "-----END_CERTIFICATE-----") + .replace(" ", "\n") + .replace("-----BEGIN_CERTIFICATE-----", "-----BEGIN CERTIFICATE-----") + .replace("-----END_CERTIFICATE-----", "-----END CERTIFICATE-----") + ) + + def update(self) -> bool: + pass + + def run(self): + if not hasattr(self, "url"): + raise AnalyzerConfigurationException("Instance URL is required") + if self.http_method not in HTTPMethods.values: + raise AnalyzerConfigurationException("Http method is not valid") + + # replace placheholder + for key in self.params.keys(): + if self.params[key] == "": + self.params[key] = self.observable_name + + # optional authentication + if hasattr(self, "_api_key_name") and self._api_key_name: + api_key = self._api_key_name + if ( + "Authorization" in self.headers.keys() + and self.headers["Authorization"].split(" ")[0] == "Basic" + ): + # the API uses basic auth so we need to base64 encode the auth payload + api_key = base64.b64encode(self._api_key_name.encode()).decode() + # replace placeholder + for key in self.headers.keys(): + self.headers[key] = self.headers[key].replace("", api_key) + + # optional certificate + verify = True # defualt + if hasattr(self, "_certificate") and self._certificate: + self.__cert_file = NamedTemporaryFile(mode="w") + self.__cert_file.write(self._clean_certificate(self._certificate)) + self.__cert_file.flush() + verify = self.__cert_file.name + + try: + if self.http_method == HTTPMethods.GET: + url = self.url + if not self.params.keys(): + url = self.url + self.observable_name + response = requests.get( + url, + params=self.params, + headers=self.headers, + verify=verify, + ) + else: + request_method = getattr(requests, self.http_method) + response = request_method( + self.url, headers=self.headers, json=self.params, verify=verify + ) + response.raise_for_status() + except requests.RequestException as e: + raise AnalyzerRunException(e) + + response_json = response.json() + logger.debug(f"response received: {response_json}") + return response_json + + @classmethod + def _monkeypatch(cls): + patches = [ + if_mock_connections( + patch( + "requests.get", + return_value=MockUpResponse({}, 200), + ), + ) + ] + return super()._monkeypatch(patches=patches) diff --git a/api_app/analyzers_manager/observable_analyzers/download_file_from_uri.py b/api_app/analyzers_manager/observable_analyzers/download_file_from_uri.py index b4b304f5..ff366c66 100644 --- a/api_app/analyzers_manager/observable_analyzers/download_file_from_uri.py +++ b/api_app/analyzers_manager/observable_analyzers/download_file_from_uri.py @@ -24,7 +24,7 @@ def update(cls) -> bool: pass def run(self): - result = {"stored_base64": ""} + result = {"stored_base64": []} proxies = {"http": self._http_proxy} if self._http_proxy else {} headers = { @@ -46,8 +46,8 @@ def run(self): else: if r.content: if "text/html" not in r.headers["Content-Type"]: - result["stored_base64"] = base64.b64encode(r.content).decode( - "ascii" + result["stored_base64"].append( + base64.b64encode(r.content).decode("ascii") ) else: logger.info( diff --git a/api_app/analyzers_manager/observable_analyzers/dshield.py b/api_app/analyzers_manager/observable_analyzers/dshield.py new file mode 100644 index 00000000..099f2eb9 --- /dev/null +++ b/api_app/analyzers_manager/observable_analyzers/dshield.py @@ -0,0 +1,53 @@ +# This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix +# See the file 'LICENSE' for copying permission. + +import logging + +import requests + +from api_app.analyzers_manager.classes import ObservableAnalyzer +from tests.mock_utils import MockUpResponse, if_mock_connections, patch + +logger = logging.getLogger(__name__) + + +class DShield(ObservableAnalyzer): + url: str = "https://isc.sans.edu/api" + + def update(self) -> bool: + pass + + def run(self): + headers = {"User-Agent": "ThreatMatrix"} + + result = { + "ip_info": {"uri": f"/ip/{self.observable_name}?json"}, + "ip_details": {"uri": f"/ipdetails/{self.observable_name}?json"}, + } + + for query_type, values in result.items(): + try: + response = requests.get(self.url + values["uri"], headers=headers) + response.raise_for_status() + except requests.RequestException as e: + logger.warning(e, stack_info=True) + self.report.errors.append( + f"{query_type} check failed for {self.observable_name}. Err {e}" + ) + self.report.save() + else: + result[query_type] = response.json() + + return result + + @classmethod + def _monkeypatch(cls): + patches = [ + if_mock_connections( + patch( + "requests.get", + return_value=MockUpResponse({}, 200), + ), + ) + ] + return super()._monkeypatch(patches=patches) diff --git a/api_app/analyzers_manager/observable_analyzers/intelx.py b/api_app/analyzers_manager/observable_analyzers/intelx.py index a3bc468c..adb26f37 100644 --- a/api_app/analyzers_manager/observable_analyzers/intelx.py +++ b/api_app/analyzers_manager/observable_analyzers/intelx.py @@ -46,9 +46,7 @@ def config(self, runtime_configuration: Dict): @cached_property def _session(self): session = requests.Session() - session.headers.update( - {"x-key": self._api_key_name, "User-Agent": "ThreatMatrix"} - ) + session.headers.update({"x-key": self._api_key_name, "User-Agent": "ThreatMatrix"}) return session def _poll_for_results(self, search_id): diff --git a/api_app/analyzers_manager/observable_analyzers/nerd.py b/api_app/analyzers_manager/observable_analyzers/nerd.py new file mode 100644 index 00000000..305ddce2 --- /dev/null +++ b/api_app/analyzers_manager/observable_analyzers/nerd.py @@ -0,0 +1,68 @@ +# This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix +# See the file 'LICENSE' for copying permission. + +import requests +from requests.exceptions import HTTPError + +from api_app.analyzers_manager.classes import ObservableAnalyzer +from api_app.analyzers_manager.exceptions import ( + AnalyzerConfigurationException, + AnalyzerRunException, +) +from tests.mock_utils import MockUpResponse, if_mock_connections, patch + + +class NERD(ObservableAnalyzer): + url: str = "https://nerd.cesnet.cz/nerd/api/v1" + + _api_key_name: str + nerd_analysis: str + + def update(self) -> bool: + pass + + def run(self): + base_uri = f"/ip/{self.observable_name}" + headers = { + "Authorization": self._api_key_name, + "Accept": "application/json", + } + match self.nerd_analysis: + case "basic": + uri = base_uri + case "full" | "rep" | "fmp" as option: + uri = f"{base_uri}/{option}" + case _: + raise AnalyzerConfigurationException( + f"analysis type: '{self.nerd_analysis}' not supported." + "Supported are: 'basic', 'full', 'rep', 'fmp'." + ) + + try: + response = requests.get(self.url + uri, headers=headers) + response.raise_for_status() + result = response.json() + except requests.RequestException as e: + if ( + isinstance(e, HTTPError) + and e.response.status_code == 404 + and "NOT FOUND" in str(e) + ): + result = {"status": "NO DATA"} + else: + raise AnalyzerRunException(e) + + return result + + @classmethod + def _monkeypatch(cls): + + patches = [ + if_mock_connections( + patch( + "requests.get", + return_value=MockUpResponse({}, 200), + ), + ) + ] + return super()._monkeypatch(patches=patches) diff --git a/api_app/analyzers_manager/observable_analyzers/spyse.py b/api_app/analyzers_manager/observable_analyzers/spyse.py index d205d068..7792dcc1 100644 --- a/api_app/analyzers_manager/observable_analyzers/spyse.py +++ b/api_app/analyzers_manager/observable_analyzers/spyse.py @@ -7,8 +7,8 @@ from api_app.analyzers_manager import classes from api_app.analyzers_manager.exceptions import AnalyzerRunException -from tests.mock_utils import MockUpResponse, if_mock_connections, patch from threat_matrix.consts import REGEX_CVE, REGEX_EMAIL +from tests.mock_utils import MockUpResponse, if_mock_connections, patch class Spyse(classes.ObservableAnalyzer): diff --git a/api_app/analyzers_manager/observable_analyzers/urlscan.py b/api_app/analyzers_manager/observable_analyzers/urlscan.py index 123305df..1759e4c8 100644 --- a/api_app/analyzers_manager/observable_analyzers/urlscan.py +++ b/api_app/analyzers_manager/observable_analyzers/urlscan.py @@ -26,10 +26,7 @@ def update(cls) -> bool: pass def run(self): - headers = { - "Content-Type": "application/json", - "User-Agent": "ThreatMatrix/v1.x", - } + headers = {"Content-Type": "application/json", "User-Agent": "ThreatMatrix/v1.x"} if not hasattr(self, "_api_key_name") and self.urlscan_analysis == "search": logger.warning(f"{self.__repr__()} -> Continuing w/o API key..") else: diff --git a/api_app/analyzers_manager/observable_analyzers/vt/vt3_base.py b/api_app/analyzers_manager/observable_analyzers/vt/vt3_base.py deleted file mode 100644 index 965580c5..00000000 --- a/api_app/analyzers_manager/observable_analyzers/vt/vt3_base.py +++ /dev/null @@ -1,449 +0,0 @@ -# This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix -# See the file 'LICENSE' for copying permission. -import abc -import base64 -import logging -import time -from datetime import datetime, timedelta -from typing import Dict, Tuple - -import requests - -from api_app.analyzers_manager.classes import BaseAnalyzerMixin -from api_app.analyzers_manager.exceptions import AnalyzerRunException -from api_app.choices import ObservableClassification - -logger = logging.getLogger(__name__) - - -class VirusTotalv3AnalyzerMixin(BaseAnalyzerMixin, metaclass=abc.ABCMeta): - url = "https://www.virustotal.com/api/v3/" - - max_tries: int - poll_distance: int - rescan_max_tries: int - rescan_poll_distance: int - include_behaviour_summary: bool - include_sigma_analyses: bool - force_active_scan_if_old: bool - days_to_say_that_a_scan_is_old: int - relationships_to_request: list - relationships_elements: int - url_sub_path: str - _api_key_name: str - - @property - def headers(self) -> dict: - return {"x-apikey": self._api_key_name} - - def _get_relationship_limit(self, relationship): - # by default, just extract the first element - limit = self.relationships_elements - # resolutions data can be more valuable and it is not lot of data - if relationship == "resolutions": - limit = 40 - return limit - - def config(self, runtime_configuration: Dict): - super().config(runtime_configuration) - self.force_active_scan = self._job.tlp == self._job.TLP.CLEAR.value - - def _vt_get_relationships( - self, - observable_name: str, - relationships_requested: list, - uri: str, - result: dict, - ): - try: - # skip relationship request if something went wrong - if "error" not in result: - relationships_in_results = result.get("data", {}).get( - "relationships", {} - ) - for relationship in self.relationships_to_request: - if relationship not in relationships_requested: - result[relationship] = { - "error": "not supported, review configuration." - } - else: - found_data = relationships_in_results.get(relationship, {}).get( - "data", [] - ) - if found_data: - logger.info( - f"found data in relationship {relationship} " - f"for observable {observable_name}." - " Requesting additional information about" - ) - rel_uri = ( - uri + f"/{relationship}" - f"?limit={self._get_relationship_limit(relationship)}" - ) - logger.debug(f"requesting uri: {rel_uri}") - response = requests.get( - self.url + rel_uri, headers=self.headers - ) - result[relationship] = response.json() - except Exception as e: - logger.error( - "something went wrong when extracting relationships" - f" for observable {observable_name}: {e}" - ) - - def _vt_get_report( - self, - obs_clfn: str, - observable_name: str, - ) -> dict: - result = {} - already_done_active_scan_because_report_was_old = False - params, uri, relationships_requested = self._get_requests_params_and_uri( - obs_clfn, observable_name - ) - for chance in range(self.max_tries): - logger.info( - f"[POLLING] (Job: {self.job_id}, observable {observable_name}) -> " - f"GET VT/v3/_vt_get_report #{chance + 1}/{self.max_tries}" - ) - - result, response = self._perform_get_request( - uri, ignore_404=True, params=params - ) - - # if it is not a file, we don't need to perform any scan - if obs_clfn != self.ObservableTypes.HASH: - break - - # this is an option to force active scan... - # .. in the case the file is not in the VT DB - # you need the binary too for this case, .. - # .. otherwise it would fail if it's not available - if response.status_code == 404: - logger.info(f"hash {observable_name} not found on VT") - if self.force_active_scan: - logger.info(f"forcing VT active scan for hash {observable_name}") - result = self._vt_scan_file(observable_name) - result["performed_active_scan"] = True - break - else: - # we should consider the chance that the very sample was already... - # ...sent and VT is already analyzing it. - # In this case, just perform a little poll for the result - attributes = result.get("data", {}).get("attributes", {}) - last_analysis_results = attributes.get("last_analysis_results", {}) - if last_analysis_results: - # at this time, if the flag if set, - # we are going to force the analysis again for old samples - if ( - self.force_active_scan_if_old - and not already_done_active_scan_because_report_was_old - ): - scan_date = attributes.get("last_analysis_date", 0) - scan_date_time = datetime.fromtimestamp(scan_date) - some_days_ago = datetime.utcnow() - timedelta( - days=self.days_to_say_that_a_scan_is_old - ) - if some_days_ago > scan_date_time: - logger.info( - f"hash {observable_name} found on VT with AV reports" - " and scan is older than" - f" {self.days_to_say_that_a_scan_is_old} days.\n" - "We will force the analysis again" - ) - # the "rescan" option will burn quotas. - # We should reduce the polling at the minimum - extracted_result = self._vt_scan_file( - observable_name, rescan_instead=True - ) - # if we were able to do a successful rescan, - # overwrite old report - if extracted_result: - result = extracted_result - already_done_active_scan_because_report_was_old = True - else: - logger.info( - f"hash {observable_name} found on VT" - f" with AV reports and scan is recent" - ) - break - else: - logger.info( - f"hash {observable_name} found on VT with AV reports" - ) - break - else: - extra_polling_times = chance + 1 - base_log = f"hash {observable_name} found on VT withOUT AV reports," - if extra_polling_times == self.max_tries: - logger.warning( - f"{base_log} reached max tries ({self.max_tries})" - ) - result["reached_max_tries_and_no_av_report"] = True - else: - logger.info(f"{base_log} performing another request...") - result["extra_polling_times"] = extra_polling_times - time.sleep(self.poll_distance) - - if already_done_active_scan_because_report_was_old: - result["performed_rescan_because_report_was_old"] = True - - if obs_clfn == self.ObservableTypes.HASH: - # Include behavioral report, if flag enabled - if self.include_behaviour_summary: - sandbox_analysis = ( - result.get("data", {}) - .get("relationships", {}) - .get("behaviours", {}) - .get("data", []) - ) - if sandbox_analysis: - logger.info( - f"found {len(sandbox_analysis)} sandbox analysis" - f" for {observable_name}," - " requesting the additional details" - ) - result["behaviour_summary"] = self._fetch_behaviour_summary( - observable_name - ) - - # Include sigma analysis report, if flag enabled - if self.include_sigma_analyses: - sigma_analysis = ( - result.get("data", {}) - .get("relationships", {}) - .get("sigma_analysis", {}) - .get("data", []) - ) - if sigma_analysis: - logger.info( - f"found {len(sigma_analysis)} sigma analysis" - f" for {observable_name}," - " requesting the additional details" - ) - result["sigma_analyses"] = self._fetch_sigma_analyses( - observable_name - ) - - if self.relationships_to_request: - self._vt_get_relationships( - observable_name, relationships_requested, uri, result - ) - uri_prefix, uri_postfix = self._get_url_prefix_postfix(result) - result["link"] = f"https://www.virustotal.com/gui/{uri_prefix}/{uri_postfix}" - - return result - - def _get_url_prefix_postfix(self, result: Dict) -> Tuple[str, str]: - uri_postfix = self._job.observable_name - if self._job.observable_classification == ObservableClassification.DOMAIN.value: - uri_prefix = "domain" - elif self._job.observable_classification == ObservableClassification.IP.value: - uri_prefix = "ip-address" - elif self._job.observable_classification == ObservableClassification.URL.value: - uri_prefix = "url" - uri_postfix = result.get("data", {}).get("id", self._job.sha256) - else: # hash - uri_prefix = "search" - return uri_prefix, uri_postfix - - def _vt_scan_file(self, md5: str, rescan_instead: bool = False) -> dict: - if rescan_instead: - logger.info(f"(Job: {self.job_id}, {md5}) -> VT analyzer requested rescan") - files = {} - uri = f"files/{md5}/analyse" - poll_distance = self.rescan_poll_distance - max_tries = self.rescan_max_tries - else: - logger.info(f"(Job: {self.job_id}, {md5}) -> VT analyzer requested scan") - try: - binary = self._job.file.read() - except Exception: - raise AnalyzerRunException( - "ThreatMatrix error: couldn't retrieve the binary" - f" to perform a scan (Job: {self.job_id}, {md5})" - ) - files = {"file": binary} - uri = "files" - poll_distance = self.poll_distance - max_tries = self.max_tries - - result, _ = self._perform_post_request(uri, files=files) - - result_data = result.get("data", {}) - scan_id = result_data.get("id", "") - if not scan_id: - raise AnalyzerRunException( - "no scan_id given by VirusTotal to retrieve the results" - f" (Job: {self.job_id}, {md5})" - ) - # max 5 minutes waiting - got_result = False - uri = f"analyses/{scan_id}" - logger.info( - "Starting POLLING for Scan results. " - f"Poll Distance {poll_distance}, tries {max_tries}, ScanID {scan_id}" - f" (Job: {self.job_id}, {md5})" - ) - for chance in range(max_tries): - time.sleep(poll_distance) - result, _ = self._perform_get_request(uri, files=files) - analysis_status = ( - result.get("data", {}).get("attributes", {}).get("status", "") - ) - logger.info( - f"[POLLING] (Job: {self.job_id}, {md5}) -> " - f"GET VT/v3/_vt_scan_file #{chance + 1}/{self.max_tries} " - f"status:{analysis_status}" - ) - if analysis_status == "completed": - got_result = True - break - - result = {} - if got_result: - # retrieve the FULL report, not only scans results. - # If it's a new sample, it's free of charge. - result = self._vt_get_report(self.ObservableTypes.HASH, md5) - else: - message = ( - f"[POLLING] (Job: {self.job_id}, {md5}) -> " - f"max polls tried, no result" - ) - # if we tried a rescan, we can still use the old report - if rescan_instead: - logger.info(message) - else: - raise AnalyzerRunException(message) - - return result - - def _perform_get_request(self, uri: str, ignore_404=False, **kwargs): - return self._perform_request(uri, method="GET", ignore_404=ignore_404, **kwargs) - - def _perform_post_request(self, uri: str, ignore_404=False, **kwargs): - return self._perform_request( - uri, method="POST", ignore_404=ignore_404, **kwargs - ) - - def _perform_request(self, uri: str, method: str, ignore_404=False, **kwargs): - error = None - try: - url = self.url + uri - if method == "GET": - response = requests.get(url, headers=self.headers, **kwargs) - elif method == "POST": - response = requests.post(url, headers=self.headers, **kwargs) - else: - raise NotImplementedError() - logger.info(f"requests done to: {response.request.url} ") - logger.debug(f"text: {response.text}") - result = response.json() - # https://developers.virustotal.com/reference/errors - error = result.get("error", {}) - # this case is not a real error,... - # .. it happens when a requested object is not found and that's normal - if not ignore_404 or not response.status_code == 404: - response.raise_for_status() - except Exception as e: - error_message = f"Raised Error: {e}. Error data: {error}" - raise AnalyzerRunException(error_message) - return result, response - - def _fetch_behaviour_summary(self, observable_name: str) -> dict: - endpoint = f"files/{observable_name}/behaviour_summary" - result, _ = self._perform_get_request(endpoint, ignore_404=True) - return result - - def _fetch_sigma_analyses(self, observable_name: str) -> dict: - endpoint = f"sigma_analyses/{observable_name}" - result, _ = self._perform_get_request(endpoint, ignore_404=True) - return result - - @classmethod - def _get_relationship_for_classification(cls, obs_clfn: str): - # reference: https://developers.virustotal.com/reference/metadata - if obs_clfn == cls.ObservableTypes.DOMAIN: - relationships = [ - "communicating_files", - "historical_whois", - "referrer_files", - "resolutions", - "siblings", - "subdomains", - "collections", - "historical_ssl_certificates", - ] - elif obs_clfn == cls.ObservableTypes.IP: - relationships = [ - "communicating_files", - "historical_whois", - "referrer_files", - "resolutions", - "collections", - "historical_ssl_certificates", - ] - elif obs_clfn == cls.ObservableTypes.URL: - relationships = [ - "last_serving_ip_address", - "collections", - "network_location", - ] - elif obs_clfn == cls.ObservableTypes.HASH: - relationships = [ - # behaviors is necessary to check if there are sandbox analysis - "behaviours", - "bundled_files", - "comments", - "contacted_domains", - "contacted_ips", - "contacted_urls", - "execution_parents", - "pe_resource_parents", - "votes", - "distributors", - "pe_resource_children", - "dropped_files", - "collections", - ] - else: - raise AnalyzerRunException( - f"Not supported observable type {obs_clfn}. " - "Supported are: hash, ip, domain and url." - ) - return relationships - - def _get_requests_params_and_uri(self, obs_clfn: str, observable_name: str): - params = {} - # in this way, you just retrieved metadata about relationships - # if you like to get all the data about specific relationships,... - # ..you should perform another query - # check vt3 API docs for further info - relationships_requested = self._get_relationship_for_classification(obs_clfn) - if obs_clfn == self.ObservableTypes.DOMAIN: - uri = f"domains/{observable_name}" - elif obs_clfn == self.ObservableTypes.IP: - uri = f"ip_addresses/{observable_name}" - elif obs_clfn == self.ObservableTypes.URL: - url_id = ( - base64.urlsafe_b64encode(observable_name.encode()).decode().strip("=") - ) - uri = f"urls/{url_id}" - elif obs_clfn == self.ObservableTypes.HASH: - uri = f"files/{observable_name}" - else: - raise AnalyzerRunException( - f"Not supported observable type {obs_clfn}. " - "Supported are: hash, ip, domain and url." - ) - - if relationships_requested: - # this won't cost additional quota - # it just helps to understand if there is something to look for there - # so, if there is, we can make API requests without wasting quotas - params["relationships"] = ",".join(relationships_requested) - if self.url_sub_path: - if not self.url_sub_path.startswith("/"): - uri += "/" - uri += self.url_sub_path - return params, uri, relationships_requested diff --git a/api_app/analyzers_manager/observable_analyzers/vt/vt3_get.py b/api_app/analyzers_manager/observable_analyzers/vt/vt3_get.py index 16708205..c8400274 100644 --- a/api_app/analyzers_manager/observable_analyzers/vt/vt3_get.py +++ b/api_app/analyzers_manager/observable_analyzers/vt/vt3_get.py @@ -2,12 +2,15 @@ # See the file 'LICENSE' for copying permission. from api_app.analyzers_manager.classes import ObservableAnalyzer +from api_app.mixins import VirusTotalv3AnalyzerMixin from tests.mock_utils import MockUpResponse, if_mock_connections, patch -from .vt3_base import VirusTotalv3AnalyzerMixin - class VirusTotalv3(ObservableAnalyzer, VirusTotalv3AnalyzerMixin): + @classmethod + def update(cls) -> bool: + pass + def run(self): result = self._vt_get_report( self.observable_classification, diff --git a/api_app/analyzers_manager/observable_analyzers/vt/vt3_intelligence_search.py b/api_app/analyzers_manager/observable_analyzers/vt/vt3_intelligence_search.py index 39978af1..7d92128b 100644 --- a/api_app/analyzers_manager/observable_analyzers/vt/vt3_intelligence_search.py +++ b/api_app/analyzers_manager/observable_analyzers/vt/vt3_intelligence_search.py @@ -1,45 +1,19 @@ # This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix # See the file 'LICENSE' for copying permission. -from typing import Dict - -import requests from api_app.analyzers_manager.classes import ObservableAnalyzer +from api_app.mixins import VirusTotalv3AnalyzerMixin from tests.mock_utils import MockUpResponse, if_mock_connections, patch -from ...exceptions import AnalyzerRunException -from .vt3_base import VirusTotalv3AnalyzerMixin - class VirusTotalv3Intelligence(ObservableAnalyzer, VirusTotalv3AnalyzerMixin): - url = "https://www.virustotal.com/api/v3/intelligence" - limit: int order_by: str - def config(self, runtime_configuration: Dict): - super().config(runtime_configuration) - # this is a limit forced by VT service - if self.limit > 300: - self.limit = 300 - def run(self): - # ref: https://developers.virustotal.com/reference/intelligence-search - params = { - "query": self.observable_name, - "limit": self.limit, - } - if self.order_by: - params["order"] = self.order_by - try: - response = requests.get( - self.url + "/search", params=params, headers=self.headers - ) - response.raise_for_status() - except requests.RequestException as e: - raise AnalyzerRunException(e) - result = response.json() - return result + return self._vt_intelligence_search( + self.observable_name, self.limit, self.order_by + ) @classmethod def _monkeypatch(cls): @@ -47,7 +21,154 @@ def _monkeypatch(cls): if_mock_connections( patch( "requests.get", - return_value=MockUpResponse({}, 200), + return_value=MockUpResponse( + { + "data": [ + { + "id": "b665290bc6bba034a69c32f54862518a86a2dab93787a7e99daaa552c708b23a", + "type": "file", + "links": {"self": "redacted"}, + "attributes": { + "popular_threat_classification": { + "popular_threat_category": [ + { + "count": 18, + "value": "downloader", + }, + {"count": 10, "value": "trojan"}, + ], + "suggested_threat_label": "downloader.orcinius/x97m", + "popular_threat_name": [ + {"count": 9, "value": "orcinius"}, + {"count": 4, "value": "x97m"}, + {"count": 3, "value": "w97m"}, + ], + }, + "size": 94332, + "first_submission_date": 1726640386, + "crowdsourced_ids_stats": { + "high": 0, + "medium": 0, + "low": 0, + "info": 1, + }, + "trid": [], + "type_description": "Office Open XML Spreadsheet", + "magika": "XLSX", + "names": ["universityform.xlsm"], + "sigma_analysis_results": [], + "sha1": "14760fbb7615b561f86d0d48b01e5ee1b163a860", + "sandbox_verdicts": {}, + "type_tags": [ + "document", + "msoffice", + "spreadsheet", + "excel", + "xlsx", + ], + "threat_severity": { + "version": 5, + "threat_severity_level": "SEVERITY_HIGH", + "threat_severity_data": { + "popular_threat_category": "downloader", + "num_gav_detections": 5, + }, + "last_analysis_date": "1726640490", + "level_description": "Severity HIGH because it was considered " + "downloader. Other contributing factor was " + "that it could not be run in sandboxes.", + }, + "vhash": "1d6670848780bd2ccd6ec496a9ba15b4", + "downloadable": True, + "magic": "Microsoft Excel 2007+", + "last_analysis_date": 1726640386, + "unique_sources": 1, + "type_tag": "xlsx", + "available_tools": [], + "total_votes": { + "harmless": 0, + "malicious": 0, + }, + "sigma_analysis_stats": { + "critical": 0, + "high": 1, + "medium": 1, + "low": 1, + }, + "exiftool": {}, + "ssdeep": "1536:CguZCa6S5khUItn3RWa4znOSjhLzVubGa/M1NIpPkUlB7583fjnc" + "FYIISFI:CgugapkhltLaPjpzVw/Ms8ULavLc0", + "tlsh": "T17C93F06B96303918E0647837D03F5DA26638621D1F02FE8C2D46F1CC7" + "EEBB47764A898", + "tags": [ + "write-file", + "auto-open", + "create-ole", + "copy-file", + "enum-windows", + "exe-pattern", + "run-file", + "macros", + "registry", + "save-workbook", + "url-pattern", + "environ", + "create-file", + "xlsx", + "open-file", + "calls-wmi", + ], + "main_icon": {}, + "last_analysis_stats": { + "malicious": 44, + "suspicious": 0, + "undetected": 22, + "harmless": 0, + "timeout": 0, + "confirmed-timeout": 0, + "failure": 1, + "type-unsupported": 10, + }, + "reputation": 0, + "last_modification_date": 1726647690, + "md5": "368d2b0498d7464cc23acab82a806841", + "openxml_info": {}, + "last_analysis_results": {}, + "type_extension": "xlsx", + "meaningful_name": "universityform.xlsm", + "crowdsourced_ids_results": [], + "creation_date": 1421340901, + "sigma_analysis_summary": { + "Sigma Integrated Rule Set (GitHub)": { + "critical": 0, + "high": 1, + "medium": 1, + "low": 1, + } + }, + "last_submission_date": 1726640386, + "sha256": "b665290bc6bba034a69c32f54862518a86a2dab93787a7e99daaa552c708b23a", + "times_submitted": 1, + "crowdsourced_ai_results": [], + }, + }, + ], + "meta": { + "total_hits": 1, + "allowed_orders": [ + "first_submission_date", + "last_submission_date", + "positives", + "times_submitted", + "size", + "unique_sources", + ], + "days_back": 90, + }, + "links": {"self": "redacted"}, + }, + 200, + ), ), ) ] diff --git a/api_app/analyzers_manager/serializers.py b/api_app/analyzers_manager/serializers.py index d3d9211c..74b5535e 100644 --- a/api_app/analyzers_manager/serializers.py +++ b/api_app/analyzers_manager/serializers.py @@ -1,6 +1,10 @@ # This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix # See the file 'LICENSE' for copying permission. +from rest_framework import serializers as rfs + +from ..models import PluginConfig, PythonModule from ..serializers.plugin import ( + PluginConfigSerializer, PythonConfigSerializer, PythonConfigSerializerForMigration, ) @@ -23,11 +27,50 @@ class Meta: class AnalyzerConfigSerializer(PythonConfigSerializer): + plugin_config = rfs.ListField( + child=rfs.DictField(), write_only=True, required=False + ) + python_module = rfs.SlugRelatedField( + queryset=PythonModule.objects.all(), slug_field="module" + ) + class Meta: model = AnalyzerConfig exclude = PythonConfigSerializer.Meta.exclude list_serializer_class = PythonConfigSerializer.Meta.list_serializer_class + def create(self, validated_data): + plugin_config = validated_data.pop("plugin_config", {}) + pc = super().create(validated_data) + + # create plugin config + for config in plugin_config: + plugin_config_serializer = PluginConfigSerializer( + data=config, context={"request": self.context["request"]} + ) + plugin_config_serializer.is_valid(raise_exception=True) + plugin_config_serializer.save() + return pc + + def update(self, instance, validated_data): + plugin_config = validated_data.pop("plugin_config", []) + pc = super().update(instance, validated_data) + + # update plugin config + for config in plugin_config: + plugin_config_serializer = PluginConfigSerializer( + data=config, context={"request": self.context["request"]} + ) + plugin_config_serializer.is_valid(raise_exception=True) + PluginConfig.objects.filter( + owner=self.context["request"].user, + analyzer_config=plugin_config_serializer.validated_data[ + "analyzer_config" + ], + parameter=plugin_config_serializer.validated_data["parameter"], + ).update_or_create(plugin_config_serializer.validated_data) + return pc + class AnalyzerConfigSerializerForMigration(PythonConfigSerializerForMigration): class Meta: diff --git a/api_app/analyzers_manager/views.py b/api_app/analyzers_manager/views.py index e7b84372..ad04a228 100644 --- a/api_app/analyzers_manager/views.py +++ b/api_app/analyzers_manager/views.py @@ -2,9 +2,12 @@ # See the file 'LICENSE' for copying permission. import logging +from rest_framework import mixins + +from ..permissions import isPluginActionsPermission from ..views import PythonConfigViewSet, PythonReportActionViewSet from .filters import AnalyzerConfigFilter -from .models import AnalyzerReport +from .models import AnalyzerConfig, AnalyzerReport from .serializers import AnalyzerConfigSerializer logger = logging.getLogger(__name__) @@ -16,9 +19,21 @@ ] -class AnalyzerConfigViewSet(PythonConfigViewSet): +class AnalyzerConfigViewSet( + PythonConfigViewSet, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, +): serializer_class = AnalyzerConfigSerializer filterset_class = AnalyzerConfigFilter + queryset = AnalyzerConfig.objects.all() + + def get_permissions(self): + permissions = super().get_permissions() + if self.action in ["destroy", "update", "partial_update"]: + permissions.append(isPluginActionsPermission()) + return permissions class AnalyzerActionViewSet(PythonReportActionViewSet): diff --git a/api_app/classes.py b/api_app/classes.py index d84f1c85..1d88fb64 100644 --- a/api_app/classes.py +++ b/api_app/classes.py @@ -391,13 +391,20 @@ def health_check(self, user: User = None) -> bool: # momentarily set this to False to # avoid fails for https services response = requests.head(url, timeout=10, verify=False) + # This may happen when even the HEAD request is protected by authentication + # We cannot create a generic health check that consider auth too + # because every analyzer has its own way to authenticate + # So, in this case, we will consider it as check passed because we got an answer + # For ex 405 code is when HEADs are not allowed. But it is the same. The service answered. + if 400 <= response.status_code <= 408: + return True response.raise_for_status() except ( requests.exceptions.ConnectionError, requests.exceptions.Timeout, requests.exceptions.HTTPError, ) as e: - logger.info(f"healthcheck failed: url {url}" f" for {self}. Error: {e}") + logger.info(f"healthcheck failed: url {url} for {self}. Error: {e}") return False else: return True diff --git a/api_app/connectors_manager/connectors/abuse_submitter.py b/api_app/connectors_manager/connectors/abuse_submitter.py index 98846e68..c1c4cf69 100644 --- a/api_app/connectors_manager/connectors/abuse_submitter.py +++ b/api_app/connectors_manager/connectors/abuse_submitter.py @@ -1,3 +1,4 @@ +from api_app.analyzers_manager.exceptions import AnalyzerRunException from api_app.connectors_manager.connectors.email_sender import EmailSender @@ -11,6 +12,11 @@ def subject(self) -> str: @property def body(self) -> str: + if not self._job.parent_job: + raise AnalyzerRunException( + "Parent job does not exist. " + "This analyzer must be run only with the playbook Takedown_Request to work properly" + ) return ( f"Domain {self._job.parent_job.parent_job.observable_name} " "has been detected as malicious by our team. We kindly request you to take " diff --git a/api_app/connectors_manager/connectors/email_sender.py b/api_app/connectors_manager/connectors/email_sender.py index c0db82ba..c533a61c 100644 --- a/api_app/connectors_manager/connectors/email_sender.py +++ b/api_app/connectors_manager/connectors/email_sender.py @@ -3,8 +3,8 @@ from django.core.mail import EmailMessage from api_app.connectors_manager.classes import Connector -from tests.mock_utils import if_mock_connections, patch from threat_matrix.settings import DEFAULT_FROM_EMAIL +from tests.mock_utils import if_mock_connections, patch class EmailSender(Connector): diff --git a/api_app/connectors_manager/connectors/opencti.py b/api_app/connectors_manager/connectors/opencti.py index 18c69c98..8cdc542d 100644 --- a/api_app/connectors_manager/connectors/opencti.py +++ b/api_app/connectors_manager/connectors/opencti.py @@ -45,9 +45,7 @@ def get_observable_type(self) -> str: ]: # sha-512 not supported obs_type = THREATMATRIX_OPENCTI_TYPE_MAP["file"] else: - obs_type = THREATMATRIX_OPENCTI_TYPE_MAP[ - ObservableTypes.GENERIC - ] # text + obs_type = THREATMATRIX_OPENCTI_TYPE_MAP[ObservableTypes.GENERIC] # text elif self._job.observable_classification == ObservableTypes.IP: ip_version = helpers.get_ip_version(self._job.observable_name) if ip_version in [4, 6]: @@ -55,13 +53,9 @@ def get_observable_type(self) -> str: f"v{ip_version}" ] # v4/v6 else: - obs_type = THREATMATRIX_OPENCTI_TYPE_MAP[ - ObservableTypes.GENERIC - ] # text + obs_type = THREATMATRIX_OPENCTI_TYPE_MAP[ObservableTypes.GENERIC] # text else: - obs_type = THREATMATRIX_OPENCTI_TYPE_MAP[ - self._job.observable_classification - ] + obs_type = THREATMATRIX_OPENCTI_TYPE_MAP[self._job.observable_classification] return obs_type diff --git a/api_app/documents.py b/api_app/documents.py index 9755fdc7..85b4688c 100644 --- a/api_app/documents.py +++ b/api_app/documents.py @@ -1,13 +1,17 @@ # This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix # See the file 'LICENSE' for copying permission. +import logging + from django_elasticsearch_dsl import Document, fields from django_elasticsearch_dsl.registries import registry from .models import Job +logger = logging.getLogger(__name__) + -@registry.register_document +@registry.register_document # TODO: maybe we can replace this with the signal and remove django elasticsearch dsl class JobDocument(Document): # Object/List fields analyzers_to_execute = fields.NestedField( @@ -19,7 +23,11 @@ class JobDocument(Document): visualizers_to_execute = fields.NestedField( properties={"name": fields.KeywordField()} ) - playbook_to_execute = fields.KeywordField() + playbook_to_execute = fields.ObjectField( + properties={ + "name": fields.KeywordField(), + }, + ) # Normal fields errors = fields.TextField() diff --git a/api_app/exceptions.py b/api_app/exceptions.py index ef7579c0..27da0dfe 100644 --- a/api_app/exceptions.py +++ b/api_app/exceptions.py @@ -1,6 +1,6 @@ import logging -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import APIException, ValidationError from rest_framework.request import Request from certego_saas.ext.exceptions import custom_exception_handler @@ -18,3 +18,9 @@ def logging_exception_handler(exc, context): ) logger.info(context) return custom_exception_handler(exc, context) + + +class NotImplementedException(APIException): + status_code = 501 + default_detail = "Service not supported." + default_code = "service_not_implemented" diff --git a/api_app/ingestors_manager/classes.py b/api_app/ingestors_manager/classes.py index 8a0f17d8..6789f248 100644 --- a/api_app/ingestors_manager/classes.py +++ b/api_app/ingestors_manager/classes.py @@ -58,6 +58,11 @@ def _user(self): def before_run(self): self._config: IngestorConfig self._config.validate_playbooks(self._user) + logger.info(f"STARTED ingestor: {self.__repr__()}") + + def after_run(self): + super().after_run() + logger.info(f"FINISHED ingestor: {self.__repr__()}") def get_playbook_to_execute(self): self._config: IngestorConfig diff --git a/api_app/ingestors_manager/ingestors/malware_bazaar.py b/api_app/ingestors_manager/ingestors/malware_bazaar.py index 031df45a..ddd6364b 100644 --- a/api_app/ingestors_manager/ingestors/malware_bazaar.py +++ b/api_app/ingestors_manager/ingestors/malware_bazaar.py @@ -63,7 +63,7 @@ def get_recent_samples(self): "Last hour" if self.hours == 1 else f"Last {self.hours} hours" ) logger.info( - f"{last_hours_str} {signature} samples: " f"{len(hashes)}/{len(data)}" + f"{last_hours_str} {signature} samples: {len(hashes)}/{len(data)}" ) return hashes diff --git a/api_app/ingestors_manager/ingestors/virus_total.py b/api_app/ingestors_manager/ingestors/virus_total.py new file mode 100644 index 00000000..771383fe --- /dev/null +++ b/api_app/ingestors_manager/ingestors/virus_total.py @@ -0,0 +1,316 @@ +import logging +from typing import Any, Iterable +from unittest.mock import patch + +from django.utils import timezone + +from api_app.ingestors_manager.classes import Ingestor +from api_app.mixins import VirusTotalv3BaseMixin +from tests.mock_utils import MockUpResponse, if_mock_connections + +logger = logging.getLogger(__name__) + + +class VirusTotal(Ingestor, VirusTotalv3BaseMixin): + # Download samples/IOCs that are up to X hours old + hours: int + # The query to execute + query: str + # Extract IOCs? Otherwise, download the file + extract_IOCs: bool + # VT API key + _api_key_name: str + + @classmethod + def update(cls) -> bool: + pass + + def run(self) -> Iterable[Any]: + if "fs:" not in self.query: + delta_hours = timezone.datetime.now() - timezone.timedelta(hours=self.hours) + self.query = f"fs:{delta_hours.strftime('%Y-%m-%d%H:%M:%S')}+ " + self.query + data = self._vt_intelligence_search(self.query, 300, "").get("data", {}) + logger.info(f"Retrieved {len(data)} items from the query") + samples_hashes = [d["id"] for d in data] + for sample_hash in samples_hashes: + if self.extract_IOCs: + iocs = self._vt_get_iocs_from_file(sample_hash) + if iocs: + for category, ioc in iocs.items(): + logger.info( + f"Extracted {category} from VT sample {sample_hash}: {ioc}" + ) + yield ioc + else: + logger.info(f"Downloading VT sample: {sample_hash}") + if sample := self._vt_download_file(sample_hash): + yield sample + + @classmethod + def _monkeypatch(cls): + patches = [ + if_mock_connections( + # first search query + patch( + "requests.get", + side_effect=[ + # for intelligence search + MockUpResponse( + { + "data": [ + { + "id": "b665290bc6bba034a69c32f54862518a86a2dab93787a7e99daaa552c708b23a", + "type": "file", + "links": {"self": "redacted"}, + "attributes": { + "popular_threat_classification": { + "popular_threat_category": [ + { + "count": 18, + "value": "downloader", + }, + {"count": 10, "value": "trojan"}, + ], + "suggested_threat_label": "downloader.orcinius/x97m", + "popular_threat_name": [ + {"count": 9, "value": "orcinius"}, + {"count": 4, "value": "x97m"}, + {"count": 3, "value": "w97m"}, + ], + }, + "size": 94332, + "first_submission_date": 1726640386, + "crowdsourced_ids_stats": { + "high": 0, + "medium": 0, + "low": 0, + "info": 1, + }, + "trid": [], + "type_description": "Office Open XML Spreadsheet", + "magika": "XLSX", + "names": ["universityform.xlsm"], + "sigma_analysis_results": [], + "sha1": "14760fbb7615b561f86d0d48b01e5ee1b163a860", + "sandbox_verdicts": {}, + "type_tags": [ + "document", + "msoffice", + "spreadsheet", + "excel", + "xlsx", + ], + "threat_severity": { + "version": 5, + "threat_severity_level": "SEVERITY_HIGH", + "threat_severity_data": { + "popular_threat_category": "downloader", + "num_gav_detections": 5, + }, + "last_analysis_date": "1726640490", + "level_description": "Severity HIGH because it was considered " + "downloader. Other contributing factor was " + "that it could not be run in sandboxes.", + }, + "vhash": "1d6670848780bd2ccd6ec496a9ba15b4", + "downloadable": True, + "magic": "Microsoft Excel 2007+", + "last_analysis_date": 1726640386, + "unique_sources": 1, + "type_tag": "xlsx", + "available_tools": [], + "total_votes": { + "harmless": 0, + "malicious": 0, + }, + "sigma_analysis_stats": { + "critical": 0, + "high": 1, + "medium": 1, + "low": 1, + }, + "exiftool": {}, + "ssdeep": "1536:CguZCa6S5khUItn3RWa4znOSjhLzVubGa/M1NIpPkUlB7583fjnc" + "FYIISFI:CgugapkhltLaPjpzVw/Ms8ULavLc0", + "tlsh": "T17C93F06B96303918E0647837D03F5DA26638621D1F02FE8C2D46F1CC7" + "EEBB47764A898", + "tags": [ + "write-file", + "auto-open", + "create-ole", + "copy-file", + "enum-windows", + "exe-pattern", + "run-file", + "macros", + "registry", + "save-workbook", + "url-pattern", + "environ", + "create-file", + "xlsx", + "open-file", + "calls-wmi", + ], + "main_icon": {}, + "last_analysis_stats": { + "malicious": 44, + "suspicious": 0, + "undetected": 22, + "harmless": 0, + "timeout": 0, + "confirmed-timeout": 0, + "failure": 1, + "type-unsupported": 10, + }, + "reputation": 0, + "last_modification_date": 1726647690, + "md5": "368d2b0498d7464cc23acab82a806841", + "openxml_info": {}, + "last_analysis_results": {}, + "type_extension": "xlsx", + "meaningful_name": "universityform.xlsm", + "crowdsourced_ids_results": [], + "creation_date": 1421340901, + "sigma_analysis_summary": { + "Sigma Integrated Rule Set (GitHub)": { + "critical": 0, + "high": 1, + "medium": 1, + "low": 1, + } + }, + "last_submission_date": 1726640386, + "sha256": "b665290bc6bba034a69c32f54862518a86a2dab93787a7e99daaa552c708b23a", + "times_submitted": 1, + "crowdsourced_ai_results": [], + }, + }, + ], + "meta": { + "total_hits": 1, + "allowed_orders": [ + "first_submission_date", + "last_submission_date", + "positives", + "times_submitted", + "size", + "unique_sources", + ], + "days_back": 90, + }, + "links": {"self": "redacted"}, + }, + 200, + ), + # for relationships + MockUpResponse( + { + "data": { + "id": "b665290bc6bba034a69c32f54862518a86a2dab93787a7e99daaa552c708b23a", + "type": "file", + "links": {"self": "redacted"}, + "relationships": { + "contacted_urls": { + "data": [ + { + "type": "url", + "id": "548d0ca19336d289e61ff43b87330780234e8461151b88a4a6b34fc5ba721dfe", + "context_attributes": { + "url": "https://docs.google.com/uc?id=0BxsMXGfPIZfSVzUyaHFYVkQxeFk&export=download" + }, + }, + { + "type": "url", + "id": "e24125e866d9b72a68ae4b1c457eba59ee6a060efe3a1adb61ec328f42e85b7d", + "context_attributes": { + "url": "https://www.dropbox.com/s/zhp1b06imehwylq/Synaptics.rar?dl=1" + }, + }, + ], + "links": { + "self": "redacted", + "related": "redacted", + }, + }, + "contacted_domains": { + "data": [ + { + "type": "domain", + "id": "docs.google.com", + }, + {"type": "domain", "id": "dropbox.com"}, + {"type": "domain", "id": "google.com"}, + { + "type": "domain", + "id": "www-env.dropbox-dns.com", + }, + { + "type": "domain", + "id": "www.dropbox.com", + }, + ], + "links": { + "self": "redacted", + "related": "redacted", + }, + }, + "contacted_ips": { + "data": [ + { + "type": "ip_address", + "id": "108.177.119.113", + }, + { + "type": "ip_address", + "id": "108.177.96.113", + }, + { + "type": "ip_address", + "id": "162.125.1.18", + }, + { + "type": "ip_address", + "id": "162.125.65.18", + }, + { + "type": "ip_address", + "id": "172.253.117.100", + }, + { + "type": "ip_address", + "id": "172.253.117.101", + }, + { + "type": "ip_address", + "id": "172.253.117.102", + }, + { + "type": "ip_address", + "id": "172.253.117.113", + }, + { + "type": "ip_address", + "id": "172.253.117.138", + }, + { + "type": "ip_address", + "id": "172.253.117.139", + }, + ], + "links": { + "self": "redacted", + "related": "redacted", + }, + }, + }, + } + }, + status_code=200, + content=b"downloaded test file!", + ), + ], + ), + ) + ] + return super()._monkeypatch(patches=patches) diff --git a/api_app/ingestors_manager/migrations/0025_ingestor_config_virustotal_example_query.py b/api_app/ingestors_manager/migrations/0025_ingestor_config_virustotal_example_query.py new file mode 100644 index 00000000..54582d7a --- /dev/null +++ b/api_app/ingestors_manager/migrations/0025_ingestor_config_virustotal_example_query.py @@ -0,0 +1,272 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "virus_total.VirusTotal", + "base_path": "api_app.ingestors_manager.ingestors", + }, + "schedule": { + "minute": "0", + "hour": "*", + "day_of_week": "*", + "day_of_month": "*", + "month_of_year": "*", + }, + "periodic_task": { + "crontab": { + "minute": "0", + "hour": "*", + "day_of_week": "*", + "day_of_month": "*", + "month_of_year": "*", + }, + "name": "VirusTotal_Example_QueryIngestor", + "task": "threat_matrix.tasks.execute_ingestor", + "kwargs": '{"config_name": "VirusTotal_Example_Query"}', + "queue": "default", + "enabled": False, + }, + "user": { + "username": "VirusTotal_Example_QueryIngestor", + "profile": { + "user": { + "username": "VirusTotal_Example_QueryIngestor", + "email": "", + "first_name": "", + "last_name": "", + "password": "", + "is_active": True, + }, + "company_name": "", + "company_role": "", + "twitter_handle": "", + "discover_from": "other", + "task_priority": 7, + "is_robot": True, + }, + }, + "playbooks_choice": ["FREE_TO_USE_ANALYZERS"], + "name": "VirusTotal_Example_Query", + "description": "VirusTotal Ingestor example query taken from: https://blog.virustotal.com/2023/12/protecting-perimeter-with-vt.html. " + "It requires a valid Premium API key to work properly", + "disabled": True, + "soft_time_limit": 60, + "routing_key": "ingestor", + "health_check_status": True, + "maximum_jobs": 30, + "delay": "00:00:30", + "model": "ingestors_manager.IngestorConfig", +} + +params = [ + { + "python_module": { + "module": "virus_total.VirusTotal", + "base_path": "api_app.ingestors_manager.ingestors", + }, + "name": "hours", + "type": "int", + "description": "Download samples that are up to X hours old", + "is_secret": False, + "required": True, + }, + { + "python_module": { + "module": "virus_total.VirusTotal", + "base_path": "api_app.ingestors_manager.ingestors", + }, + "name": "query", + "type": "str", + "description": "The query to execute", + "is_secret": False, + "required": True, + }, + { + "python_module": { + "module": "virus_total.VirusTotal", + "base_path": "api_app.ingestors_manager.ingestors", + }, + "name": "extract_IOCs", + "type": "bool", + "description": "If enabled, this ingestor would extract IOCs instead of downloading retrieved files", + "is_secret": False, + "required": True, + }, + { + "python_module": { + "module": "virus_total.VirusTotal", + "base_path": "api_app.ingestors_manager.ingestors", + }, + "name": "api_key_name", + "type": "str", + "description": "VT API key", + "is_secret": True, + "required": True, + }, +] + +values = [ + { + "parameter": { + "python_module": { + "module": "virus_total.VirusTotal", + "base_path": "api_app.ingestors_manager.ingestors", + }, + "name": "hours", + "type": "int", + "description": "Download samples that are up to X hours old", + "is_secret": False, + "required": True, + }, + "analyzer_config": None, + "connector_config": None, + "visualizer_config": None, + "ingestor_config": "VirusTotal_Example_Query", + "pivot_config": None, + "for_organization": False, + "value": 1, + "updated_at": "2024-09-17T14:38:13.760609Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "virus_total.VirusTotal", + "base_path": "api_app.ingestors_manager.ingestors", + }, + "name": "query", + "type": "str", + "description": "The query to execute", + "is_secret": False, + "required": True, + }, + "analyzer_config": None, + "connector_config": None, + "visualizer_config": None, + "ingestor_config": "VirusTotal_Example_Query", + "pivot_config": None, + "for_organization": False, + "value": "(type:doc or type:docx or type:xls or type:xlsx) p:5+ (behaviour:powershell or (tag:macros and tag:run-file)) ", + "updated_at": "2024-09-17T14:21:38.931991Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "virus_total.VirusTotal", + "base_path": "api_app.ingestors_manager.ingestors", + }, + "name": "extract_IOCs", + "type": "bool", + "description": "If enabled, this ingestor would extract IOCs instead of downloading retrieved files", + "is_secret": False, + "required": True, + }, + "analyzer_config": None, + "connector_config": None, + "visualizer_config": None, + "ingestor_config": "VirusTotal_Example_Query", + "pivot_config": None, + "for_organization": False, + "value": False, + "updated_at": "2024-09-17T13:48:18.196119Z", + "owner": None, + }, +] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ + ForwardManyToOneDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, + ForwardOneToOneDescriptor, + ] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0062_alter_parameter_python_module"), + ("ingestors_manager", "0024_remove_ingestorconfig_playbook_to_execute"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/ingestors_manager/serializers.py b/api_app/ingestors_manager/serializers.py index 7c7660de..629a301a 100644 --- a/api_app/ingestors_manager/serializers.py +++ b/api_app/ingestors_manager/serializers.py @@ -39,7 +39,7 @@ class IngestorConfigSerializerForMigration(PythonConfigSerializerForMigration): class Meta: model = IngestorConfig - exclude = [] + exclude = PythonConfigSerializerForMigration.Meta.exclude def to_internal_value(self, data): raise NotImplementedError() @@ -67,7 +67,24 @@ class IngestorReportBISerializer(AbstractReportBISerializer): class Meta: model = IngestorReport - fields = AbstractReportBISerializer.Meta.fields + fields = ( + [ + "application", + "environment", + "timestamp", + ] + + [ + "username", + "class_instance", + "process_time", + "status", + "end_time", + ] + + [ + "name", + "parameters", + ] + ) list_serializer_class = AbstractReportBISerializer.Meta.list_serializer_class @classmethod diff --git a/api_app/management/commands/dumpplugin.py b/api_app/management/commands/dumpplugin.py index db6ac3a4..6876e935 100644 --- a/api_app/management/commands/dumpplugin.py +++ b/api_app/management/commands/dumpplugin.py @@ -81,6 +81,8 @@ def _imports() -> str: ForwardManyToOneDescriptor, ForwardOneToOneDescriptor, ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, ) """ @@ -108,8 +110,13 @@ def _get_obj(Model, other_model, value): if ( type(getattr(Model, field)) - in [ForwardManyToOneDescriptor, ForwardOneToOneDescriptor] - and value + in [ + ForwardManyToOneDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, + ForwardOneToOneDescriptor, + ] + and value ): other_model = getattr(Model, field).get_queryset().model value = _get_obj(Model, other_model, value) diff --git a/api_app/management/commands/elastic_templates.py b/api_app/management/commands/elastic_templates.py new file mode 100644 index 00000000..3fe0c09a --- /dev/null +++ b/api_app/management/commands/elastic_templates.py @@ -0,0 +1,39 @@ +import json +import logging + +from django.conf import settings +from django.core.management import BaseCommand +from elasticsearch import ApiError +from elasticsearch_dsl import connections + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + # NOTE: this command is runned by uwsgi startup script + + help = "Create or update the index templates in Elasticsearch" + + def handle(self, *args, **options): + if settings.ELASTICSEARCH_DSL_ENABLED and settings.ELASTICSEARCH_DSL_HOST: + self.stdout.write("Creating/updating the templates...") + # push template + with open( + settings.CONFIG_ROOT / "elastic_search_mappings" / "plugin_report.json" + ) as file_content: + try: + connections.get_connection().indices.put_template( + name="plugin-report", body=json.load(file_content) + ) + success_msg = ( + "created/updated Elasticsearch's template for plugin-report" + ) + self.stdout.write(self.style.SUCCESS(success_msg)) + logger.info(success_msg) + except ApiError as error: + self.stdout.write(self.style.ERROR(error)) + logger.critical(error) + else: + self.stdout.write( + self.style.WARNING("Elasticsearch not active, templates not updated") + ) diff --git a/api_app/migrations/0063_singleton_and_elastic_report.py b/api_app/migrations/0063_singleton_and_elastic_report.py new file mode 100644 index 00000000..3a46c575 --- /dev/null +++ b/api_app/migrations/0063_singleton_and_elastic_report.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.15 on 2024-10-29 10:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api_app", "0062_alter_parameter_python_module"), + ] + + operations = [ + migrations.CreateModel( + name="LastElasticReportUpdate", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("last_update_datetime", models.DateTimeField()), + ], + options={ + "abstract": False, + }, + ), + migrations.AddConstraint( + model_name="lastelasticreportupdate", + constraint=models.CheckConstraint( + check=models.Q(("pk", 1)), + name="singleton", + violation_error_message="This class is a singleton: only one object is allowed", + ), + ), + ] diff --git a/api_app/mixins.py b/api_app/mixins.py index 198e5b43..cfbe4720 100644 --- a/api_app/mixins.py +++ b/api_app/mixins.py @@ -1,8 +1,17 @@ +import abc +import base64 import logging +import time +from datetime import datetime, timedelta +from typing import Dict, List, Tuple +import requests from django.core.cache import cache from rest_framework.response import Response +from api_app.analyzers_manager.classes import BaseAnalyzerMixin +from api_app.analyzers_manager.exceptions import AnalyzerRunException +from api_app.choices import ObservableClassification from certego_saas.ext.pagination import CustomPageNumberPagination logger = logging.getLogger(__name__) @@ -66,3 +75,579 @@ def list(self, request, *args, **kwargs): cache.touch(cache_name, timeout=60 * 60 * 24 * 7) return Response(data) + + +class VirusTotalv3BaseMixin(BaseAnalyzerMixin, metaclass=abc.ABCMeta): + url = "https://www.virustotal.com/api/v3/" + + # If you want to query a specific subpath of the base endpoint, i.e: `analyses` + url_sub_path: str + _api_key_name: str + + @property + def headers(self) -> dict: + return {"x-apikey": self._api_key_name} + + def config(self, runtime_configuration: Dict): + super().config(runtime_configuration) + # An Ingestor does not have a corresponding job so we set the value to False, + # the aim of the ingestors usually is to download data not to upload. + self.force_active_scan = ( + self._job.tlp == self._job.TLP.CLEAR.value if self._job else False + ) + + def _perform_get_request( + self, uri: str, ignore_404: bool = False, **kwargs + ) -> Dict: + return self._perform_request(uri, method="GET", ignore_404=ignore_404, **kwargs) + + def _perform_post_request(self, uri: str, ignore_404: bool = False, **kwargs): + return self._perform_request( + uri, method="POST", ignore_404=ignore_404, **kwargs + ) + + def _perform_request( + self, uri: str, method: str, ignore_404: bool = False, **kwargs + ) -> Dict: + error = None + try: + url = self.url + uri + if method == "GET": + response = requests.get(url, headers=self.headers, **kwargs) + elif method == "POST": + response = requests.post(url, headers=self.headers, **kwargs) + else: + raise NotImplementedError() + logger.info(f"requests done to: {response.request.url} ") + logger.debug(f"text: {response.text}") + result = response.json() + # https://developers.virustotal.com/reference/errors + error = result.get("error", {}) + # this case is not a real error,... + # .. it happens when a requested object is not found and that's normal + if not ignore_404 or not response.status_code == 404: + response.raise_for_status() + except Exception as e: + error_message = f"Raised Error: {e}. Error data: {error}" + raise AnalyzerRunException(error_message) + return result, response + + # return available relationships from file mimetype + @classmethod + def _get_relationship_for_classification(cls, obs_clfn: str, iocs: bool) -> List: + # reference: https://developers.virustotal.com/reference/metadata + if obs_clfn == cls.ObservableTypes.DOMAIN: + relationships = [ + "communicating_files", + "historical_whois", + "referrer_files", + "resolutions", + "siblings", + "subdomains", + "collections", + "historical_ssl_certificates", + ] + elif obs_clfn == cls.ObservableTypes.IP: + relationships = [ + "communicating_files", + "historical_whois", + "referrer_files", + "resolutions", + "collections", + "historical_ssl_certificates", + ] + elif obs_clfn == cls.ObservableTypes.URL: + relationships = [ + "last_serving_ip_address", + "collections", + "network_location", + ] + elif obs_clfn == cls.ObservableTypes.HASH: + if iocs: + relationships = [ + "contacted_domains", + "contacted_ips", + "contacted_urls", + ] + else: + relationships = [ + # behaviors is necessary to check if there are sandbox analysis + "behaviours", + "bundled_files", + "comments", + "contacted_domains", + "contacted_ips", + "contacted_urls", + "execution_parents", + "pe_resource_parents", + "votes", + "distributors", + "pe_resource_children", + "dropped_files", + "collections", + ] + else: + raise AnalyzerRunException( + f"Not supported observable type {obs_clfn}. " + "Supported are: hash, ip, domain and url." + ) + return relationships + + # configure requests params from file mimetype to get relative relationships + def _get_requests_params_and_uri( + self, obs_clfn: str, observable_name: str, iocs: bool + ) -> Tuple[Dict, str, List]: + params = {} + # in this way, you just retrieved metadata about relationships + # if you like to get all the data about specific relationships,... + # ..you should perform another query + # check vt3 API docs for further info + relationships_requested = self._get_relationship_for_classification( + obs_clfn, iocs + ) + if obs_clfn == self.ObservableTypes.DOMAIN: + uri = f"domains/{observable_name}" + elif obs_clfn == self.ObservableTypes.IP: + uri = f"ip_addresses/{observable_name}" + elif obs_clfn == self.ObservableTypes.URL: + url_id = ( + base64.urlsafe_b64encode(observable_name.encode()).decode().strip("=") + ) + uri = f"urls/{url_id}" + elif obs_clfn == self.ObservableTypes.HASH: + uri = f"files/{observable_name}" + else: + raise AnalyzerRunException( + f"Not supported observable type {obs_clfn}. " + "Supported are: hash, ip, domain and url." + ) + + if relationships_requested: + # this won't cost additional quota + # it just helps to understand if there is something to look for there + # so, if there is, we can make API requests without wasting quotas + params["relationships"] = ",".join(relationships_requested) + if hasattr(self, "url_sub_path") and self.url_sub_path: + if not self.url_sub_path.startswith("/"): + uri += "/" + uri += self.url_sub_path + return params, uri, relationships_requested + + def _fetch_behaviour_summary(self, observable_name: str) -> Dict: + endpoint = f"files/{observable_name}/behaviour_summary" + logger.info(f"Requesting behaviour summary from {endpoint}") + result, _ = self._perform_get_request(endpoint, ignore_404=True) + return result + + def _fetch_sigma_analyses(self, observable_name: str) -> Dict: + endpoint = f"sigma_analyses/{observable_name}" + logger.info(f"Requesting sigma analyses from {endpoint}") + result, _ = self._perform_get_request(endpoint, ignore_404=True) + return result + + def _vt_download_file(self, file_hash: str) -> bytes: + try: + endpoint = self.url + f"files/{file_hash}/download" + logger.info(f"Requesting file from {endpoint}") + response = requests.get(endpoint, headers=self.headers) + if not isinstance(response.content, bytes): + raise ValueError("VT downloaded file is not instance of bytes") + except Exception as e: + error_message = f"Cannot download the file {file_hash}. Raised Error: {e}." + raise AnalyzerRunException(error_message) + return response.content + + # perform a query in VT and return the results + # ref: https://developers.virustotal.com/reference/intelligence-search + def _vt_intelligence_search( + self, + query: str, + limit: int, + order_by: str, + ) -> Dict: + logger.info(f"Running VirusTotal intelligence search query: {query}") + + limit = min(limit, 300) # this is a limit forced by VT service + params = { + "query": query, + "limit": limit, + } + if order_by: + params["order"] = order_by + + result, _ = self._perform_get_request("intelligence/search", params=params) + return result + + def _vt_get_iocs_from_file(self, sample_hash: str) -> Dict: + try: + params, uri, relationships_requested = self._get_requests_params_and_uri( + self.ObservableTypes.HASH, sample_hash, True + ) + logger.info(f"Requesting IOCs {relationships_requested} from {uri}") + result, response = self._perform_get_request( + uri, ignore_404=True, params=params + ) + if response.status_code != 404: + relationships = result.get("data", {}).get("relationships", {}) + contacted_ips = [ + i["id"] + for i in relationships.get("contacted_ips", {}).get("data", []) + ] + contacted_domains = [ + i["id"] + for i in relationships.get("contacted_domains", {}).get("data", []) + ] + contacted_urls = [ + i["context_attributes"]["url"] + for i in relationships.get("contacted_urls", {}).get("data", []) + ] + return { + "contacted_ips": contacted_ips, + "contacted_urls": contacted_urls, + "contacted_domains": contacted_domains, + } + except Exception as e: + logger.error( + "something went wrong when extracting iocs" + f" for sample {sample_hash}: {e}" + ) + + +class VirusTotalv3AnalyzerMixin(VirusTotalv3BaseMixin, metaclass=abc.ABCMeta): + # How many times we poll the VT API for scan results + max_tries: int + # ThreatMatrix would sleep for this time between each poll to VT APIs + poll_distance: int + # How many times we poll the VT API for RE-scan results (samples already available to VT) + rescan_max_tries: int + # ThreatMatrix would sleep for this time between each poll to VT APIs after having started a RE-scan + rescan_poll_distance: int + # Include a summary of behavioral analysis reports alongside default scan report. + # This will cost additional quota. + include_behaviour_summary: bool + # Include sigma analysis report alongside default scan report. + # This will cost additional quota. + include_sigma_analyses: bool + # If the sample is old, it would be rescanned. + # This will cost additional quota. + force_active_scan_if_old: bool + # How many days are required to consider a scan old to force rescan + days_to_say_that_a_scan_is_old: int + # Include a list of relationships to request if available. + # Full list here https://developers.virustotal.com/reference/metadata. + # This will cost additional quota. + relationships_to_request: list + # Number of elements to retrieve for each relationships + relationships_elements: int + + def _get_relationship_limit(self, relationship: str) -> int: + # by default, just extract the first element + limit = self.relationships_elements + # resolutions data can be more valuable and it is not lot of data + if relationship == "resolutions": + limit = 40 + return limit + + def _vt_get_relationships( + self, + observable_name: str, + relationships_requested: list, + uri: str, + result: dict, + ) -> None: + try: + # skip relationship request if something went wrong + if "error" not in result: + relationships_in_results = result.get("data", {}).get( + "relationships", {} + ) + for relationship in self.relationships_to_request: + if relationship not in relationships_requested: + result[relationship] = { + "error": "not supported, review configuration." + } + else: + found_data = relationships_in_results.get(relationship, {}).get( + "data", [] + ) + if found_data: + logger.info( + f"found data in relationship {relationship} " + f"for observable {observable_name}." + " Requesting additional information about" + ) + rel_uri = ( + uri + f"/{relationship}" + f"?limit={self._get_relationship_limit(relationship)}" + ) + logger.debug(f"requesting uri: {rel_uri}") + response = requests.get( + self.url + rel_uri, headers=self.headers + ) + result[relationship] = response.json() + except Exception as e: + logger.error( + "something went wrong when extracting relationships" + f" for observable {observable_name}: {e}" + ) + + def _get_url_prefix_postfix(self, result: Dict) -> Tuple[str, str]: + uri_postfix = self._job.observable_name + if self._job.observable_classification == ObservableClassification.DOMAIN.value: + uri_prefix = "domain" + elif self._job.observable_classification == ObservableClassification.IP.value: + uri_prefix = "ip-address" + elif self._job.observable_classification == ObservableClassification.URL.value: + uri_prefix = "url" + uri_postfix = result.get("data", {}).get("id", self._job.sha256) + else: # hash + uri_prefix = "search" + return uri_prefix, uri_postfix + + def _vt_scan_file(self, md5: str, rescan_instead: bool = False) -> Dict: + if rescan_instead: + logger.info(f"(Job: {self.job_id}, {md5}) -> VT analyzer requested rescan") + files = {} + uri = f"files/{md5}/analyse" + poll_distance = self.rescan_poll_distance + max_tries = self.rescan_max_tries + else: + logger.info(f"(Job: {self.job_id}, {md5}) -> VT analyzer requested scan") + try: + binary = self._job.file.read() + except Exception: + raise AnalyzerRunException( + "ThreatMatrix error: couldn't retrieve the binary" + f" to perform a scan (Job: {self.job_id}, {md5})" + ) + files = {"file": binary} + uri = "files" + poll_distance = self.poll_distance + max_tries = self.max_tries + + result, _ = self._perform_post_request(uri, files=files) + + result_data = result.get("data", {}) + scan_id = result_data.get("id", "") + if not scan_id: + raise AnalyzerRunException( + "no scan_id given by VirusTotal to retrieve the results" + f" (Job: {self.job_id}, {md5})" + ) + # max 5 minutes waiting + got_result = False + uri = f"analyses/{scan_id}" + logger.info( + "Starting POLLING for Scan results. " + f"Poll Distance {poll_distance}, tries {max_tries}, ScanID {scan_id}" + f" (Job: {self.job_id}, {md5})" + ) + for chance in range(max_tries): + time.sleep(poll_distance) + result, _ = self._perform_get_request(uri, files=files) + analysis_status = ( + result.get("data", {}).get("attributes", {}).get("status", "") + ) + logger.info( + f"[POLLING] (Job: {self.job_id}, {md5}) -> " + f"GET VT/v3/_vt_scan_file #{chance + 1}/{self.max_tries} " + f"status:{analysis_status}" + ) + if analysis_status == "completed": + got_result = True + break + + result = {} + if got_result: + # retrieve the FULL report, not only scans results. + # If it's a new sample, it's free of charge. + result = self._vt_get_report(self.ObservableTypes.HASH, md5) + else: + message = ( + f"[POLLING] (Job: {self.job_id}, {md5}) -> " + "max polls tried, no result" + ) + # if we tried a rescan, we can still use the old report + if rescan_instead: + logger.info(message) + else: + raise AnalyzerRunException(message) + + return result + + def _vt_poll_for_report( + self, + observable_name: str, + params: Dict, + uri: str, + obs_clfn: str, + ) -> Dict: + result = {} + already_done_active_scan_because_report_was_old = False + for chance in range(self.max_tries): + logger.info( + f"[POLLING] (Job: {self.job_id}, observable {observable_name}) -> " + f"GET VT/v3/_vt_get_report #{chance + 1}/{self.max_tries}" + ) + + result, response = self._perform_get_request( + uri, ignore_404=True, params=params + ) + + # if it is not a file, we don't need to perform any scan + if obs_clfn != self.ObservableTypes.HASH: + break + + # this is an option to force active scan... + # .. in the case the file is not in the VT DB + # you need the binary too for this case, .. + # .. otherwise it would fail if it's not available + if response.status_code == 404: + logger.info(f"hash {observable_name} not found on VT") + if self.force_active_scan: + logger.info(f"forcing VT active scan for hash {observable_name}") + result = self._vt_scan_file(observable_name) + result["performed_active_scan"] = True + break + else: + # we should consider the chance that the very sample was already... + # ...sent and VT is already analyzing it. + # In this case, just perform a little poll for the result + attributes = result.get("data", {}).get("attributes", {}) + last_analysis_results = attributes.get("last_analysis_results", {}) + if last_analysis_results: + # at this time, if the flag if set, + # we are going to force the analysis again for old samples + if ( + self.force_active_scan_if_old + and not already_done_active_scan_because_report_was_old + ): + scan_date = attributes.get("last_analysis_date", 0) + scan_date_time = datetime.fromtimestamp(scan_date) + some_days_ago = datetime.utcnow() - timedelta( + days=self.days_to_say_that_a_scan_is_old + ) + if some_days_ago > scan_date_time: + logger.info( + f"hash {observable_name} found on VT with AV reports" + " and scan is older than" + f" {self.days_to_say_that_a_scan_is_old} days.\n" + "We will force the analysis again" + ) + # the "rescan" option will burn quotas. + # We should reduce the polling at the minimum + extracted_result = self._vt_scan_file( + observable_name, rescan_instead=True + ) + # if we were able to do a successful rescan, + # overwrite old report + if extracted_result: + result = extracted_result + already_done_active_scan_because_report_was_old = True + else: + logger.info( + f"hash {observable_name} found on VT" + " with AV reports and scan is recent" + ) + break + else: + logger.info( + f"hash {observable_name} found on VT with AV reports" + ) + break + else: + extra_polling_times = chance + 1 + base_log = f"hash {observable_name} found on VT withOUT AV reports," + if extra_polling_times == self.max_tries: + logger.warning( + f"{base_log} reached max tries ({self.max_tries})" + ) + result["reached_max_tries_and_no_av_report"] = True + else: + logger.info(f"{base_log} performing another request...") + result["extra_polling_times"] = extra_polling_times + time.sleep(self.poll_distance) + + if already_done_active_scan_because_report_was_old: + result["performed_rescan_because_report_was_old"] = True + + return result + + def _vt_include_behaviour_summary( + self, + result: Dict, + observable_name: str, + ) -> Dict: + sandbox_analysis = ( + result.get("data", {}) + .get("relationships", {}) + .get("behaviours", {}) + .get("data", []) + ) + if sandbox_analysis: + logger.info( + f"found {len(sandbox_analysis)} sandbox analysis" + f" for {observable_name}," + " requesting the additional details" + ) + return self._fetch_behaviour_summary(observable_name) + + def _vt_include_sigma_analyses( + self, + result: Dict, + observable_name: str, + ) -> Dict: + sigma_analysis = ( + result.get("data", {}) + .get("relationships", {}) + .get("sigma_analysis", {}) + .get("data", []) + ) + if sigma_analysis: + logger.info( + f"found {len(sigma_analysis)} sigma analysis" + f" for {observable_name}," + " requesting the additional details" + ) + return self._fetch_sigma_analyses(observable_name) + + def _vt_get_report( + self, + obs_clfn: str, + observable_name: str, + ) -> Dict: + params, uri, relationships_requested = self._get_requests_params_and_uri( + obs_clfn, observable_name, False + ) + + result = self._vt_poll_for_report( + observable_name, + params, + uri, + obs_clfn, + ) + + if obs_clfn == self.ObservableTypes.HASH: + # Include behavioral report, if flag enabled + # Attention: this will cost additional quota! + if self.include_behaviour_summary: + result["behaviour_summary"] = self._vt_include_behaviour_summary( + result, observable_name + ) + + # Include sigma analysis report, if flag enabled + # Attention: this will cost additional quota! + if self.include_sigma_analyses: + result["sigma_analyses"] = self._vt_include_sigma_analyses( + result, observable_name + ) + + if self.relationships_to_request: + self._vt_get_relationships( + observable_name, relationships_requested, uri, result + ) + + uri_prefix, uri_postfix = self._get_url_prefix_postfix(result) + result["link"] = f"https://www.virustotal.com/gui/{uri_prefix}/{uri_postfix}" + + return result diff --git a/api_app/models.py b/api_app/models.py index 65b6c57f..075e5497 100644 --- a/api_app/models.py +++ b/api_app/models.py @@ -1419,6 +1419,24 @@ def process_time(self) -> float: secs = (self.end_time - self.start_time).total_seconds() return round(secs, 2) + def get_value(self, field: str) -> Any: + content = self.report + + for key in field.split("."): + try: + content = content[key] + except TypeError: + if isinstance(content, list) and len(content) > 0: + content = content[int(key)] + else: + raise RuntimeError(f"Not found {field}") + + if isinstance(content, (int, dict)): + raise ValueError(f"You can't use a {type(content)} as pivot") + if not content: + raise ValueError("Empty value") + return content + class PythonConfig(AbstractConfig): """ @@ -1787,3 +1805,29 @@ def generate_health_check_periodic_task(self): )[0] self.health_check_task = periodic_task self.save() + + +class SingletonModel(models.Model): + """Singleton base class. + Singleton is a desing pattern that allow only one istance of a class. + """ + + class Meta: + abstract = True + constraints = [ + models.CheckConstraint( + check=Q(pk=1), + name="singleton", + violation_error_message="This class is a singleton: only one object is allowed", + ), + ] + + def save(self, *args, **kwargs): + # check required to delete the singleton instance and create a new one + if type(self).objects.count() == 0: + self.pk = 1 + super().save(*args, **kwargs) + + +class LastElasticReportUpdate(SingletonModel): + last_update_datetime = models.DateTimeField() diff --git a/api_app/permissions.py b/api_app/permissions.py index ec00e996..0d8f1ecf 100644 --- a/api_app/permissions.py +++ b/api_app/permissions.py @@ -100,3 +100,13 @@ def has_object_permission(request, view, obj): and obj_owner.membership.organization == request.user.membership.organization ) + + +class isPluginActionsPermission(BasePermission): + @staticmethod + def has_object_permission(request, view, obj): + # only an admin or superuser can update or delete plugins + if request.user.has_membership(): + return request.user.membership.is_admin + else: + return request.user.is_superuser diff --git a/api_app/pivots_manager/migrations/0033_pivot_config_extractedonenotefiles.py b/api_app/pivots_manager/migrations/0033_pivot_config_extractedonenotefiles.py new file mode 100644 index 00000000..808187ac --- /dev/null +++ b/api_app/pivots_manager/migrations/0033_pivot_config_extractedonenotefiles.py @@ -0,0 +1,149 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "load_file.LoadFile", + "base_path": "api_app.pivots_manager.pivots", + }, + "related_analyzer_configs": ["OneNote_Info"], + "related_connector_configs": [], + "playbooks_choice": ["Sample_Static_Analysis"], + "name": "ExtractedOneNoteFiles", + "description": "Pivot for plugins OneNote_Info that " + "executes playbooks Sample_Static_Analysis", + "disabled": False, + "soft_time_limit": 60, + "routing_key": "stored_base64", + "health_check_status": True, + "delay": "00:00:00", + "model": "pivots_manager.PivotConfig", +} + +params = [ + { + "python_module": { + "module": "load_file.LoadFile", + "base_path": "api_app.pivots_manager.pivots", + }, + "name": "field_to_compare", + "type": "str", + "description": "Dotted path to the field", + "is_secret": False, + "required": True, + } +] + +values = [ + { + "parameter": { + "python_module": { + "module": "load_file.LoadFile", + "base_path": "api_app.pivots_manager.pivots", + }, + "name": "field_to_compare", + "type": "str", + "description": "Dotted path to the field", + "is_secret": False, + "required": True, + }, + "analyzer_config": None, + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": "ExtractedOneNoteFiles", + "for_organization": False, + "value": "stored_base64", + "updated_at": "2024-07-17T14:58:58.499626Z", + "owner": None, + } +] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ForwardManyToOneDescriptor, ForwardOneToOneDescriptor] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0062_alter_parameter_python_module"), + ("pivots_manager", "0032_remove_pivotconfig_playbook_to_execute"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/pivots_manager/migrations/0034_changed_resubmitdownloadedfile_playbook_to_execute.py b/api_app/pivots_manager/migrations/0034_changed_resubmitdownloadedfile_playbook_to_execute.py new file mode 100644 index 00000000..39f68e30 --- /dev/null +++ b/api_app/pivots_manager/migrations/0034_changed_resubmitdownloadedfile_playbook_to_execute.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def migrate(apps, schema_editor): + PivotConfig = apps.get_model("pivots_manager", "PivotConfig") + + pc = PivotConfig.objects.get( + name="ResubmitDownloadedFile", + ) + pc.playbook_to_execute = "Sample_Static_Analysis" + pc.save() + + +def reverse_migrate(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + dependencies = [ + ("api_app", "0062_alter_parameter_python_module"), + ("pivots_manager", "0033_pivot_config_extractedonenotefiles"), + ] + operations = [ + migrations.RunPython(migrate, reverse_migrate), + ] diff --git a/api_app/pivots_manager/permissions.py b/api_app/pivots_manager/permissions.py index 78c2d250..8c2a0ff0 100644 --- a/api_app/pivots_manager/permissions.py +++ b/api_app/pivots_manager/permissions.py @@ -8,3 +8,13 @@ def has_object_permission(request, view, obj): obj.starting_job.user.pk == request.user.pk and obj.ending_job.user.pk == request.user.pk ) + + +class PivotActionsPermission(BasePermission): + @staticmethod + def has_object_permission(request, view, obj): + # only an admin or superuser can update or delete pivots + if request.user.has_membership(): + return request.user.membership.is_admin + else: + return request.user.is_superuser diff --git a/api_app/pivots_manager/pivots/any_compare.py b/api_app/pivots_manager/pivots/any_compare.py index c0a23ff6..e9fd4ef4 100644 --- a/api_app/pivots_manager/pivots/any_compare.py +++ b/api_app/pivots_manager/pivots/any_compare.py @@ -8,16 +8,19 @@ class AnyCompare(Compare): def should_run(self) -> Tuple[bool, Optional[str]]: - if result := self.related_reports.filter( + for report in self.related_reports.filter( status=self.report_model.Status.SUCCESS.value - ).first(): + ): try: - self._value = self._get_value(self.field_to_compare) - except (RuntimeError, ValueError) as e: - return False, str(e) + self._value = report.get_value(self.field_to_compare) + except (RuntimeError, ValueError): + continue + else: + return True, "Key found with success" + return ( - bool(result), - f"All necessary reports{'' if result else ' do not'} have success status", + False, + f"Field {self.field_to_compare} not found in success reports", ) def update(self) -> bool: diff --git a/api_app/pivots_manager/pivots/compare.py b/api_app/pivots_manager/pivots/compare.py index dff23158..cc9e5f6e 100644 --- a/api_app/pivots_manager/pivots/compare.py +++ b/api_app/pivots_manager/pivots/compare.py @@ -10,29 +10,6 @@ class Compare(Pivot): def update(cls) -> bool: pass - def _get_value(self, field: str) -> Any: - report = self.related_reports.filter( - status=self.report_model.Status.SUCCESS.value - ).first() - if not report: - raise RuntimeError("No report found") - content = report.report - - for key in field.split("."): - try: - content = content[key] - except TypeError: - if isinstance(content, list) and len(content) > 0: - content = content[int(key)] - else: - raise RuntimeError(f"Not found {field}") - - if isinstance(content, (int, dict)): - raise ValueError(f"You can't use a {type(content)} as pivot") - if not content: - raise ValueError("Empty value") - return content - def should_run(self) -> Tuple[bool, Optional[str]]: if self.related_reports.count() != 1: return ( @@ -41,7 +18,7 @@ def should_run(self) -> Tuple[bool, Optional[str]]: "because attached to more than one configuration", ) try: - self._value = self._get_value(self.field_to_compare) + self._value = self.related_reports.first().get_value(self.field_to_compare) except (RuntimeError, ValueError) as e: return False, str(e) return super().should_run() diff --git a/api_app/pivots_manager/pivots/load_file.py b/api_app/pivots_manager/pivots/load_file.py index ad116aad..ec761589 100644 --- a/api_app/pivots_manager/pivots/load_file.py +++ b/api_app/pivots_manager/pivots/load_file.py @@ -1,5 +1,5 @@ import base64 -from typing import Any +from typing import Any, List from api_app.pivots_manager.pivots.compare import Compare @@ -12,4 +12,13 @@ def update(cls) -> bool: pass def get_value_to_pivot_to(self) -> Any: - return base64.b64decode(self._value) + if isinstance(self._value, List): + for v in self._value: + if isinstance(v, (bytes, bytearray, str)): + yield base64.b64decode(v) + else: + raise ValueError("Invalid data type to base64 decode") + elif isinstance(self._value, (bytes, bytearray, str)): + yield base64.b64decode(self._value) + else: + raise ValueError("Invalid data type to base64 decode") diff --git a/api_app/pivots_manager/serializers.py b/api_app/pivots_manager/serializers.py index 9b65a095..4e83ed28 100644 --- a/api_app/pivots_manager/serializers.py +++ b/api_app/pivots_manager/serializers.py @@ -1,10 +1,13 @@ from rest_framework import serializers as rfs from rest_framework.exceptions import ValidationError -from api_app.models import Job +from api_app.analyzers_manager.models import AnalyzerConfig +from api_app.connectors_manager.models import ConnectorConfig +from api_app.models import Job, PluginConfig, PythonModule from api_app.pivots_manager.models import PivotConfig, PivotMap, PivotReport from api_app.playbooks_manager.models import PlaybookConfig from api_app.serializers.plugin import ( + PluginConfigSerializer, PythonConfigSerializer, PythonConfigSerializerForMigration, ) @@ -57,15 +60,77 @@ class PivotConfigSerializer(PythonConfigSerializer): queryset=PlaybookConfig.objects.all(), slug_field="name", many=True ) - name = rfs.CharField(read_only=True) description = rfs.CharField(read_only=True) related_configs = rfs.SlugRelatedField(read_only=True, many=True, slug_field="name") + related_analyzer_configs = rfs.SlugRelatedField( + slug_field="name", + queryset=AnalyzerConfig.objects.all(), + many=True, + required=False, + ) + related_connector_configs = rfs.SlugRelatedField( + slug_field="name", + queryset=ConnectorConfig.objects.all(), + many=True, + required=False, + ) + python_module = rfs.SlugRelatedField( + queryset=PythonModule.objects.all(), slug_field="module" + ) + plugin_config = rfs.ListField( + child=rfs.DictField(), write_only=True, required=False + ) class Meta: model = PivotConfig - exclude = ["related_analyzer_configs", "related_connector_configs"] + fields = rfs.ALL_FIELDS list_serializer_class = PythonConfigSerializer.Meta.list_serializer_class + def validate(self, attrs): + related_analyzer_configs = attrs.get("related_analyzer_configs", []) + related_connector_configs = attrs.get("related_connector_configs", []) + if ( + not self.instance + and not related_analyzer_configs + and not related_connector_configs + ): + raise ValidationError( + {"detail": "No Analyzers and Connectors attached to pivot"} + ) + return attrs + + def create(self, validated_data): + plugin_config = validated_data.pop("plugin_config", {}) + pc = super().create(validated_data) + + # create plugin config + for config in plugin_config: + plugin_config_serializer = PluginConfigSerializer( + data=config, context={"request": self.context["request"]} + ) + plugin_config_serializer.is_valid(raise_exception=True) + plugin_config_serializer.save() + return pc + + def update(self, instance, validated_data): + plugin_config = validated_data.pop("plugin_config", []) + pc = super().update(instance, validated_data) + + # update plugin config + for config in plugin_config: + plugin_config_serializer = PluginConfigSerializer( + data=config, context={"request": self.context["request"]} + ) + plugin_config_serializer.is_valid(raise_exception=True) + PluginConfig.objects.filter( + owner=self.context["request"].user, + analyzer_config=plugin_config_serializer.validated_data[ + "analyzer_config" + ], + parameter=plugin_config_serializer.validated_data["parameter"], + ).update_or_create(plugin_config_serializer.validated_data) + return pc + class PivotConfigSerializerForMigration(PythonConfigSerializerForMigration): related_analyzer_configs = rfs.SlugRelatedField( diff --git a/api_app/pivots_manager/signals.py b/api_app/pivots_manager/signals.py index 073af96c..83ee8ad5 100644 --- a/api_app/pivots_manager/signals.py +++ b/api_app/pivots_manager/signals.py @@ -91,3 +91,20 @@ def m2m_changed_pivot_config_connector_config( if action.startswith("post"): instance.description = instance._generate_full_description() instance.save() + + +@receiver(m2m_changed, sender=PivotConfig.playbooks_choice.through) +def m2m_changed_pivot_config_playbooks_choice( + sender, + instance: PivotConfig, + action: str, + reverse, + model, + pk_set, + using, + *args, + **kwargs, +): + if action.startswith("post"): + instance.description = instance._generate_full_description() + instance.save() diff --git a/api_app/pivots_manager/views.py b/api_app/pivots_manager/views.py index 25fb0490..a26dec87 100644 --- a/api_app/pivots_manager/views.py +++ b/api_app/pivots_manager/views.py @@ -1,14 +1,46 @@ -from rest_framework import viewsets +from rest_framework import mixins, viewsets from rest_framework.permissions import IsAuthenticated -from api_app.pivots_manager.models import PivotMap, PivotReport -from api_app.pivots_manager.permissions import PivotOwnerPermission +from api_app.pivots_manager.models import PivotConfig, PivotMap, PivotReport +from api_app.pivots_manager.permissions import ( + PivotActionsPermission, + PivotOwnerPermission, +) from api_app.pivots_manager.serializers import PivotConfigSerializer, PivotMapSerializer from api_app.views import PythonConfigViewSet, PythonReportActionViewSet -class PivotConfigViewSet(PythonConfigViewSet): +class PivotConfigViewSet( + PythonConfigViewSet, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, +): serializer_class = PivotConfigSerializer + queryset = PivotConfig.objects.all() + + def get_queryset(self): + return ( + super() + .get_queryset() + .prefetch_related( + "related_analyzer_configs", + "related_connector_configs", + "playbooks_choice", + ) + ) + + def get_permissions(self): + permissions = super().get_permissions() + if self.action in ["destroy", "update", "partial_update"]: + permissions.append(PivotActionsPermission()) + return permissions + + def perform_destroy(self, instance: PivotConfig): + for pivot_map in PivotMap.objects.filter(pivot_config=instance): + pivot_map.pivot_config = None + pivot_map.save() + return super().perform_destroy(instance) class PivotActionViewSet(PythonReportActionViewSet): diff --git a/api_app/playbooks_manager/migrations/0051_add_lnk_info_analyzer_free_to_use.py b/api_app/playbooks_manager/migrations/0051_add_lnk_info_analyzer_free_to_use.py new file mode 100644 index 00000000..950e3b6f --- /dev/null +++ b/api_app/playbooks_manager/migrations/0051_add_lnk_info_analyzer_free_to_use.py @@ -0,0 +1,34 @@ +# This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix +# See the file 'LICENSE' for copying permission. + + +from django.db import migrations + + +def migrate(apps, schema_editor): + playbook_config = apps.get_model("playbooks_manager", "PlaybookConfig") + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + pc = playbook_config.objects.get(name="FREE_TO_USE_ANALYZERS") + pc.analyzers.add(AnalyzerConfig.objects.get(name="Lnk_Info").id) + pc.full_clean() + pc.save() + + +def reverse_migrate(apps, schema_editor): + playbook_config = apps.get_model("playbooks_manager", "PlaybookConfig") + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + pc = playbook_config.objects.get(name="FREE_TO_USE_ANALYZERS") + pc.analyzers.remove(AnalyzerConfig.objects.get(name="Lnk_Info").id) + pc.full_clean() + pc.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("playbooks_manager", "0050_add_goresym_to_sample_static_abalysis"), + ("analyzers_manager", "0121_analyzer_config_lnk_info"), + ] + + operations = [ + migrations.RunPython(migrate, reverse_migrate), + ] diff --git a/api_app/playbooks_manager/migrations/0052_playbook_config_uris.py b/api_app/playbooks_manager/migrations/0052_playbook_config_uris.py new file mode 100644 index 00000000..359afe28 --- /dev/null +++ b/api_app/playbooks_manager/migrations/0052_playbook_config_uris.py @@ -0,0 +1,118 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, +) + +plugin = { + "analyzers": ["BoxJS", "Doc_Info", "Lnk_Info", "PDF_Info", "Strings_Info"], + "connectors": [], + "pivots": ["DownloadFileFromUri"], + "for_organization": False, + "name": "Uris", + "description": 'A playbook containing only the analyzers that extract "uris".', + "disabled": False, + "type": ["file"], + "runtime_configuration": { + "pivots": {}, + "analyzers": {}, + "connectors": {}, + "visualizers": {}, + }, + "scan_mode": 2, + "scan_check_time": "1 00:00:00", + "tlp": "AMBER", + "starting": True, + "owner": None, + "tags": [], + "model": "playbooks_manager.PlaybookConfig", +} + +params = [] + +values = [] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ForwardManyToOneDescriptor, ForwardOneToOneDescriptor] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0062_alter_parameter_python_module"), + ("playbooks_manager", "0051_add_lnk_info_analyzer_free_to_use"), + ("pivots_manager", "0029_pivot_config_downloadfilefromuri"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/playbooks_manager/migrations/0053_add_androguard_to_free_to_use_analyzers.py b/api_app/playbooks_manager/migrations/0053_add_androguard_to_free_to_use_analyzers.py new file mode 100644 index 00000000..b4bf4dd6 --- /dev/null +++ b/api_app/playbooks_manager/migrations/0053_add_androguard_to_free_to_use_analyzers.py @@ -0,0 +1,34 @@ +# This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix +# See the file 'LICENSE' for copying permission. + + +from django.db import migrations + + +def migrate(apps, schema_editor): + playbook_config = apps.get_model("playbooks_manager", "PlaybookConfig") + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + pc = playbook_config.objects.get(name="FREE_TO_USE_ANALYZERS") + pc.analyzers.add(AnalyzerConfig.objects.get(name="Androguard").id) + pc.full_clean() + pc.save() + + +def reverse_migrate(apps, schema_editor): + playbook_config = apps.get_model("playbooks_manager", "PlaybookConfig") + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + pc = playbook_config.objects.get(name="FREE_TO_USE_ANALYZERS") + pc.analyzers.remove(AnalyzerConfig.objects.get(name="Androguard").id) + pc.full_clean() + pc.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("playbooks_manager", "0052_playbook_config_uris"), + ("analyzers_manager", "0124_analyzer_config_androguard"), + ] + + operations = [ + migrations.RunPython(migrate, reverse_migrate), + ] diff --git a/api_app/playbooks_manager/serializers.py b/api_app/playbooks_manager/serializers.py index 38f78554..260d6300 100644 --- a/api_app/playbooks_manager/serializers.py +++ b/api_app/playbooks_manager/serializers.py @@ -14,6 +14,7 @@ from api_app.serializers import ModelWithOwnershipSerializer from api_app.serializers.job import TagSerializer from api_app.serializers.plugin import AbstractConfigSerializerForMigration +from api_app.visualizers_manager.models import VisualizerConfig class PlaybookConfigSerializerForMigration(AbstractConfigSerializerForMigration): @@ -31,7 +32,7 @@ class Meta: model = PlaybookConfig fields = rfs.ALL_FIELDS - type = rfs.ListField(child=rfs.CharField(read_only=True), read_only=True) + type = rfs.ListField(child=rfs.CharField(), required=False) analyzers = rfs.SlugRelatedField( many=True, queryset=AnalyzerConfig.objects.all(), @@ -48,7 +49,12 @@ class Meta: pivots = rfs.SlugRelatedField( many=True, queryset=PivotConfig.objects.all(), required=True, slug_field="name" ) - visualizers = rfs.SlugRelatedField(read_only=True, many=True, slug_field="name") + visualizers = rfs.SlugRelatedField( + many=True, + queryset=VisualizerConfig.objects.all(), + required=False, + slug_field="name", + ) runtime_configuration = rfs.DictField(required=True) @@ -57,7 +63,7 @@ class Meta: tags = TagSerializer(required=False, allow_empty=True, many=True, read_only=True) tlp = rfs.CharField(read_only=True) weight = rfs.IntegerField(read_only=True, required=False, allow_null=True) - is_deletable = rfs.SerializerMethodField() + is_editable = rfs.SerializerMethodField() tags_labels = rfs.ListField( child=rfs.CharField(required=True), default=list, @@ -65,10 +71,10 @@ class Meta: write_only=True, ) - def get_is_deletable(self, instance: PlaybookConfig): + def get_is_editable(self, instance: PlaybookConfig): # if the playbook is not a default one if instance.owner: - # it is deletable by the owner of the playbook + # it is editable/deletable by the owner of the playbook # or by an admin of the same organization if instance.owner == self.context["request"].user or ( self.context["request"].user.membership.is_admin diff --git a/api_app/playbooks_manager/signals.py b/api_app/playbooks_manager/signals.py index fcc16cd6..c3701612 100644 --- a/api_app/playbooks_manager/signals.py +++ b/api_app/playbooks_manager/signals.py @@ -3,9 +3,9 @@ from typing import Type from django.conf import settings -from django.core.exceptions import ValidationError from django.db.models.signals import m2m_changed from django.dispatch import receiver +from rest_framework.exceptions import ValidationError from api_app.pivots_manager.models import PivotConfig from api_app.playbooks_manager.models import PlaybookConfig diff --git a/api_app/playbooks_manager/views.py b/api_app/playbooks_manager/views.py index 62976969..4dd21cfb 100644 --- a/api_app/playbooks_manager/views.py +++ b/api_app/playbooks_manager/views.py @@ -50,6 +50,7 @@ def get_queryset(self): ) @action(methods=["POST"], url_name="analyze_multiple_observables", detail=False) def analyze_multiple_observables(self, request): + logger.debug(f"{request.data=}") oas = ObservableAnalysisSerializer( data=request.data, many=True, context={"request": request} ) @@ -68,11 +69,13 @@ def analyze_multiple_observables(self, request): ) @action(methods=["POST"], url_name="analyze_multiple_files", detail=False) def analyze_multiple_files(self, request): + logger.debug(f"{request.data=}") oas = FileJobSerializer( data=request.data, many=True, context={"request": request} ) oas.is_valid(raise_exception=True) - jobs = oas.save(send_task=True) + parent_job = oas.validated_data[0].get("parent_job", None) + jobs = oas.save(send_task=True, parent=parent_job) return Response( JobResponseSerializer(jobs, many=True).data, status=status.HTTP_200_OK, diff --git a/api_app/queryset.py b/api_app/queryset.py index f0b7c790..f263cfa6 100644 --- a/api_app/queryset.py +++ b/api_app/queryset.py @@ -74,7 +74,7 @@ def _create_index_template(): ) as f: body = json.load(f) body["index_patterns"] = [f"{settings.ELASTICSEARCH_BI_INDEX}-*"] - settings.ELASTICSEARCH_CLIENT.indices.put_template( + settings.ELASTICSEARCH_BI_CLIENT.indices.put_template( name=settings.ELASTICSEARCH_BI_INDEX, body=body ) logger.info( @@ -105,7 +105,7 @@ def send_to_elastic_as_bi(self, max_timeout: int = 60) -> bool: serializer = self._get_bi_serializer_class()(instance=objects, many=True) objects_serialized = serializer.data _, errors = bulk( - settings.ELASTICSEARCH_CLIENT, + settings.ELASTICSEARCH_BI_CLIENT, objects_serialized, request_timeout=max_timeout, ) diff --git a/api_app/serializers/elastic.py b/api_app/serializers/elastic.py new file mode 100644 index 00000000..d9a87f10 --- /dev/null +++ b/api_app/serializers/elastic.py @@ -0,0 +1,67 @@ +import datetime +import logging +from dataclasses import dataclass + +from rest_framework import serializers + +from api_app.analyzers_manager.models import AnalyzerConfig +from api_app.choices import ReportStatus +from api_app.connectors_manager.models import ConnectorConfig +from api_app.pivots_manager.models import PivotConfig + +logger = logging.getLogger(__name__) + + +@dataclass +class ElasticRequest: + plugin_name: str = "" + name: str = "" + status: str = "" + errors: bool = None # different from False, we want both errors and no errors + start_start_time: datetime.datetime = None + end_start_time: datetime.datetime = None + start_end_time: datetime.datetime = None + end_end_time: datetime.datetime = None + report: str = "" + + +class ElasticRequestSerializer(serializers.Serializer): + plugin_name = serializers.ChoiceField( + choices=[ + AnalyzerConfig.plugin_name, + ConnectorConfig.plugin_name, + PivotConfig.plugin_name, + ], + required=False, + ) + name = serializers.CharField(required=False) + status = serializers.ChoiceField( + choices=ReportStatus.final_statuses(), required=False + ) + errors = serializers.BooleanField(required=False, allow_null=True) + start_start_time = serializers.DateTimeField(required=False) + end_start_time = serializers.DateTimeField(required=False) + start_end_time = serializers.DateTimeField(required=False) + end_end_time = serializers.DateTimeField(required=False) + report = serializers.CharField(required=False) + + def create(self, validated_data) -> ElasticRequest: + logger.debug(f"{validated_data=}") + return ElasticRequest(**validated_data) + + +class ElasticResponseSerializer(serializers.Serializer): + job_id = serializers.IntegerField() + plugin_name = serializers.ChoiceField( + choices=[ + AnalyzerConfig.plugin_name, + ConnectorConfig.plugin_name, + PivotConfig.plugin_name, + ], + ) + name = serializers.CharField() + status = serializers.ChoiceField(choices=ReportStatus.final_statuses()) + start_time = serializers.DateTimeField() + end_time = serializers.DateTimeField() + errors = serializers.BooleanField() + report = serializers.CharField() diff --git a/api_app/serializers/job.py b/api_app/serializers/job.py index 28f8e2dc..2f2c7975 100644 --- a/api_app/serializers/job.py +++ b/api_app/serializers/job.py @@ -478,6 +478,7 @@ class Meta: "playbook", "status", "received_request_time", + "is_sample", ] playbook = rfs.SlugRelatedField( @@ -772,7 +773,7 @@ def set_analyzers_to_execute( partially_filtered_analyzers_qs = AnalyzerConfig.objects.filter( pk__in=[config.pk for config in analyzers_to_execute] ) - if file_mimetype in [MimeTypes.ZIP1.value, MimeTypes.ZIP1.value]: + if file_mimetype in [MimeTypes.ZIP1.value, MimeTypes.ZIP2.value]: EXCEL_OFFICE_FILES = r"\.[xl]\w{0,3}$" DOC_OFFICE_FILES = r"\.[doc]\w{0,3}$" if re.search(DOC_OFFICE_FILES, file_name): diff --git a/api_app/serializers/plugin.py b/api_app/serializers/plugin.py index 3d4b0d57..507aa5a9 100644 --- a/api_app/serializers/plugin.py +++ b/api_app/serializers/plugin.py @@ -12,6 +12,7 @@ from api_app.connectors_manager.models import ConnectorConfig from api_app.ingestors_manager.models import IngestorConfig from api_app.models import Parameter, PluginConfig, PythonConfig, PythonModule +from api_app.pivots_manager.models import PivotConfig from api_app.serializers import ModelWithOwnershipSerializer from api_app.serializers.celery import CrontabScheduleSerializer from api_app.visualizers_manager.models import VisualizerConfig @@ -79,7 +80,7 @@ def to_representation(self, value): return json.dumps(result) return result - type = rfs.ChoiceField(choices=["1", "2", "3", "4"]) # retrocompatibility + type = rfs.ChoiceField(choices=["1", "2", "3", "4", "5"]) # retrocompatibility config_type = rfs.ChoiceField(choices=["1", "2"]) # retrocompatibility attribute = rfs.CharField() plugin_name = rfs.CharField() @@ -113,6 +114,8 @@ def validate(self, attrs): class_ = VisualizerConfig elif _type == "4": class_ = IngestorConfig + elif _type == "5": + class_ = PivotConfig else: raise RuntimeError("Not configured") # we set the pointers allowing retro-compatibility from the frontend @@ -265,11 +268,10 @@ class AbstractConfigSerializer(rfs.ModelSerializer): ... class PythonConfigSerializer(AbstractConfigSerializer): - parameters = ParameterSerializer(write_only=True, many=True) + parameters = ParameterSerializer(write_only=True, many=True, required=False) class Meta: exclude = [ - "python_module", "routing_key", "soft_time_limit", "health_check_status", @@ -277,9 +279,6 @@ class Meta: ] list_serializer_class = PythonConfigListSerializer - def to_internal_value(self, data): - raise NotImplementedError() - def to_representation(self, instance: PythonConfig): result = super().to_representation(instance) result["disabled"] = result["disabled"] | instance.health_check_status diff --git a/api_app/signals.py b/api_app/signals.py index bed637d4..456d7a86 100644 --- a/api_app/signals.py +++ b/api_app/signals.py @@ -5,6 +5,7 @@ from django import dispatch from django.conf import settings +from django.contrib.admin.models import LogEntry from django.db import models from django.dispatch import receiver @@ -260,6 +261,7 @@ def post_save_python_config_cache(sender, instance, *args, **kwargs): """ Signal receiver for the post_save signal. Deletes class cache keys for instances of ListCachable models. + Refreshes cache keys associated with the PythonConfig instance. Args: sender (Model): The model class sending the signal. @@ -269,13 +271,16 @@ def post_save_python_config_cache(sender, instance, *args, **kwargs): """ if issubclass(sender, ListCachable): instance.delete_class_cache_keys() + if issubclass(sender, PythonConfig): + instance.refresh_cache_keys() @receiver(models.signals.post_delete) -def post_delete_python_config_cache(sender, instance, using, origin, *args, **kwargs): +def post_delete_python_config_cache(sender, instance, *args, **kwargs): """ Signal receiver for the post_delete signal. Deletes class cache keys for instances of ListCachable models after deletion. + Refreshes cache keys associated with the PythonConfig instance after deletion. Args: sender (Model): The model class sending the signal. @@ -287,3 +292,20 @@ def post_delete_python_config_cache(sender, instance, using, origin, *args, **kw """ if issubclass(sender, ListCachable): instance.delete_class_cache_keys() + if issubclass(sender, PythonConfig): + instance.refresh_cache_keys() + + +@receiver(models.signals.post_save, sender=LogEntry) +def post_save_log_entry(sender, instance: LogEntry, *args, **kwargs): + """ + Signal receiver for the post_save signal. + Add a line of log + + Args: + sender (Model): The model class sending the signal. + instance: The instance of the model being saved. + *args: Additional positional arguments. + **kwargs: Additional keyword arguments. + """ + logger.info(str(instance)) diff --git a/api_app/urls.py b/api_app/urls.py index 5e6f285b..6944f772 100644 --- a/api_app/urls.py +++ b/api_app/urls.py @@ -15,6 +15,7 @@ analyze_observable, ask_analysis_availability, ask_multi_analysis_availability, + plugin_report_queries, plugin_state_viewer, ) @@ -40,6 +41,7 @@ analyze_multiple_observables, name="analyze_multiple_observables", ), + path("plugin_report_queries", plugin_report_queries), # router viewsets path("", include(router.urls)), # Plugins diff --git a/api_app/views.py b/api_app/views.py index f0a8afbb..640aec10 100644 --- a/api_app/views.py +++ b/api_app/views.py @@ -5,6 +5,7 @@ import uuid from abc import ABCMeta, abstractmethod +from django.conf import settings from django.db.models import Count, Q from django.db.models.functions import Trunc from django.http import FileResponse @@ -12,6 +13,8 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema as add_docs from drf_spectacular.utils import inline_serializer +from elasticsearch_dsl import Q as QElastic +from elasticsearch_dsl import Search from rest_framework import serializers as rfs from rest_framework import status, viewsets from rest_framework.decorators import action, api_view @@ -20,6 +23,8 @@ from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet +from api_app.choices import ScanMode +from api_app.exceptions import NotImplementedException from api_app.websocket import JobConsumer from certego_saas.apps.organization.permissions import ( IsObjectOwnerOrSameOrgPermission as IsObjectUserOrSameOrgPermission, @@ -50,6 +55,11 @@ ) from .permissions import IsObjectAdminPermission, IsObjectOwnerPermission from .pivots_manager.models import PivotConfig +from .serializers.elastic import ( + ElasticRequest, + ElasticRequestSerializer, + ElasticResponseSerializer, +) from .serializers.job import ( CommentSerializer, FileJobSerializer, @@ -452,7 +462,7 @@ def get_permissions(self): - List of applicable permissions. """ permissions = super().get_permissions() - if self.action in ["destroy", "kill"]: + if self.action in ["destroy", "kill", "rescan"]: permissions.append(IsObjectUserOrSameOrgPermission()) return permissions @@ -541,6 +551,36 @@ def retry(self, request, pk=None): job.retry() return Response(status=status.HTTP_204_NO_CONTENT) + @action(detail=True, methods=["post"]) + def rescan(self, request, pk=None): + logger.info(f"rescan request for job: {pk}") + existing_job: Job = self.get_object() + # create a new job + data = { + "tlp": existing_job.tlp, + "runtime_configuration": existing_job.runtime_configuration, + "scan_mode": ScanMode.FORCE_NEW_ANALYSIS, + } + if existing_job.playbook_requested: + data["playbook_requested"] = existing_job.playbook_requested + else: + data["analyzers_requested"] = existing_job.analyzers_requested.all() + data["connectors_requested"] = existing_job.connectors_requested.all() + if existing_job.is_sample: + data["file"] = existing_job.file + data["file_name"] = existing_job.file_name + job_serializer = FileJobSerializer(data=data, context={"request": request}) + else: + data["observable_classification"] = existing_job.observable_classification + data["observable_name"] = existing_job.observable_name + job_serializer = ObservableAnalysisSerializer( + data=data, context={"request": request} + ) + job_serializer.is_valid(raise_exception=True) + new_job = job_serializer.save(send_task=True) + logger.info(f"rescan request for job: {pk} generated job: {new_job.pk}") + return Response(data={"id": new_job.pk}, status=status.HTTP_202_ACCEPTED) + @add_docs( description="Kill running job by closing celery tasks and marking as killed", request=None, @@ -1525,3 +1565,87 @@ def pull(self, request, name=None): {"detail": "This Plugin has no Update implemented"} ) return Response(data={"status": update_status}, status=status.HTTP_200_OK) + + +# @add_docs( +# description="""This endpoint allows organization owners""", +# responses={ +# 200: inline_serializer( +# name="PluginStateViewerResponseSerializer", +# fields={ +# "data": rfs.JSONField(), +# }, +# ), +# }, +# ) +@api_view(["GET"]) +def plugin_report_queries(request): + """ + View enabled only with elastic. Allow to perform queries in the Plugin reports. + + Args: + request (HttpRequest): The request object containing the HTTP GET request. + + Returns: + Response: A JSON response with the state of each plugin configuration, + indicating whether it is disabled or not. + + Raises: + NotImplementedException: Elastic is not configured + PermissionDenied: If the requesting user does not belong to any organization. + """ + if not settings.ELASTICSEARCH_DSL_ENABLED: + raise NotImplementedException() + + # if not request.user.has_membership(): + # raise PermissionDenied() + + # 1 validate request + logger.debug(f"{request.query_params=}") + elastic_request_serializer = ElasticRequestSerializer(data=request.query_params) + elastic_request_serializer.is_valid(raise_exception=True) + elastic_request_params: ElasticRequest = elastic_request_serializer.save() + logger.debug(f"{elastic_request_params.__dict__=}") + + # 2 generate elasticsearch queries + filter_list = [] + if elastic_request_params.plugin_name: + filter_list.append( + QElastic("term", plugin_name=elastic_request_params.plugin_name) + ) + if elastic_request_params.name: + filter_list.append(QElastic("term", name=elastic_request_params.name)) + if elastic_request_params.status: + filter_list.append(QElastic("term", status=elastic_request_params.status)) + if elastic_request_params.errors: + filter_list.append(QElastic("exists", field="errors")) + if elastic_request_params.start_start_time: + filter_list.append( + QElastic( + "range", start_time={"gte": elastic_request_params.start_start_time} + ) + ) + if elastic_request_params.end_start_time: + filter_list.append( + QElastic("range", start_time={"lte": elastic_request_params.end_start_time}) + ) + if elastic_request_params.start_end_time: + filter_list.append( + QElastic("range", end_time={"gte": elastic_request_params.start_end_time}) + ) + if elastic_request_params.end_end_time: + filter_list.append( + QElastic("range", end_time={"lte": elastic_request_params.end_end_time}) + ) + if elastic_request_params.report: + filter_list.append(QElastic("term", report=elastic_request_params.report)) + + # 3 return data + logger.debug(f"{filter_list=}") + hits = Search(index="plugin-report-*").query(QElastic("bool", filter=filter_list)) + serialize_response = ElasticResponseSerializer(data=hits) + serialize_response.is_valid(raise_exception=True) + response_data = serialize_response.validated_data + + result = {"data": response_data} + return Response(result) diff --git a/api_app/visualizers_manager/visualizers/passive_dns/analyzer_extractor.py b/api_app/visualizers_manager/visualizers/passive_dns/analyzer_extractor.py index ecbb60cf..32319a02 100644 --- a/api_app/visualizers_manager/visualizers/passive_dns/analyzer_extractor.py +++ b/api_app/visualizers_manager/visualizers/passive_dns/analyzer_extractor.py @@ -178,20 +178,21 @@ def extract_robtex_reports(analyzer_reports: QuerySet, job: Job) -> List[PDNSRep robtex_reports = robtex_analyzer.report pdns_reports = [] for report in robtex_reports: - pdns_report = PDNSReport( - datetime.datetime.fromtimestamp(report.get("time_last")).strftime( - "%Y-%m-%d" - ), - datetime.datetime.fromtimestamp(report.get("time_first")).strftime( - "%Y-%m-%d" - ), - report.get("rrtype"), - report.get("rrdata"), - report.get("rrname"), - robtex_analyzer.config.name.replace("_", " "), - robtex_analyzer.config.description, - ) - pdns_reports.append(pdns_report) + if "rrdata" in report.keys(): + pdns_report = PDNSReport( + datetime.datetime.fromtimestamp(report.get("time_last")).strftime( + "%Y-%m-%d" + ), + datetime.datetime.fromtimestamp(report.get("time_first")).strftime( + "%Y-%m-%d" + ), + report.get("rrtype"), + report.get("rrdata"), + report.get("rrname"), + robtex_analyzer.config.name.replace("_", " "), + robtex_analyzer.config.description, + ) + pdns_reports.append(pdns_report) return pdns_reports return [] diff --git a/authentication/templates/authentication/emails/base.html b/authentication/templates/authentication/emails/base.html index caee0c28..9ec1ec2a 100644 --- a/authentication/templates/authentication/emails/base.html +++ b/authentication/templates/authentication/emails/base.html @@ -11,11 +11,9 @@
-
- {% block content %} {% endblock %} +
{% block content %} {% endblock %}

- Note: If you believe you received this email in error, please contact us - at + Note: If you believe you received this email in error, please contact us at {{ default_email }}.

diff --git a/authentication/templates/authentication/emails/duplicate-email.html b/authentication/templates/authentication/emails/duplicate-email.html index 92d5082f..161f610d 100644 --- a/authentication/templates/authentication/emails/duplicate-email.html +++ b/authentication/templates/authentication/emails/duplicate-email.html @@ -3,12 +3,11 @@
- Dear ThreatMatrix user, - + Dear ThreatMatrix user, +

- As part of our commitment to keep ThreatMatrix and its users secure, we - notify you that someone just attempted to register with this email - address. + As part of our commitment to keep ThreatMatrix and its users secure, we notify + you that someone just attempted to register with this email address.

@@ -19,4 +18,4 @@
ThreatMatrix © - + \ No newline at end of file diff --git a/authentication/templates/authentication/emails/reset-password.html b/authentication/templates/authentication/emails/reset-password.html index a5a7fd55..7716ec85 100644 --- a/authentication/templates/authentication/emails/reset-password.html +++ b/authentication/templates/authentication/emails/reset-password.html @@ -5,15 +5,15 @@
Dear ThreatMatrix user, -

Please click the link below to reset your password on ThreatMatrix.

+

+ Please click the link below to reset your password on ThreatMatrix. +

-

- or, you may also copy and paste directly into your browser's URL bar. -

+

or, you may also copy and paste directly into your browser's URL bar.

{{ reset_url }}
@@ -22,8 +22,7 @@

- If you did not request a password reset, you can safely ignore this - email. + If you did not request a password reset, you can safely ignore this email.

diff --git a/authentication/templates/authentication/emails/verify-email.html b/authentication/templates/authentication/emails/verify-email.html index 23162452..5d4565e7 100644 --- a/authentication/templates/authentication/emails/verify-email.html +++ b/authentication/templates/authentication/emails/verify-email.html @@ -4,24 +4,23 @@
Hello! - -

Please click the link below to verify your email address.

- - - +

- or, you may also copy and paste directly into your browser's URL bar. + Please click the link below to verify your email address.

- + + + +

or, you may also copy and paste directly into your browser's URL bar.

+
{{ verification_url }}
- +

Note: This URL is valid only for the next 24 hours.

+
@@ -32,3 +31,4 @@ ThreatMatrix © + diff --git a/configuration/elastic_search_mappings/plugin_report.json b/configuration/elastic_search_mappings/plugin_report.json new file mode 100644 index 00000000..0e874f0b --- /dev/null +++ b/configuration/elastic_search_mappings/plugin_report.json @@ -0,0 +1,45 @@ +{ + "index_patterns": [ + "plugin-report-*" + ], + "settings" : { + "number_of_shards" : 1, + "number_of_replicas": 0 + }, + "mappings": { + "properties": { + "config": { + "properties": { + "name": { + "type": "keyword" + }, + "plugin_name": { + "type": "keyword" + } + } + }, + "job": { + "properties": { + "id": { + "type": "long" + } + } + }, + "start_time": { + "type": "date" + }, + "end_time": { + "type": "date" + }, + "status": { + "type": "keyword" + }, + "errors": { + "type": "text" + }, + "report": { + "type": "flattened" + } + } + } +} \ No newline at end of file diff --git a/configuration/elastic_search_mappings/threat_matrix_bi.json b/configuration/elastic_search_mappings/threat_matrix_bi.json index 0342ef44..b1f431f3 100644 --- a/configuration/elastic_search_mappings/threat_matrix_bi.json +++ b/configuration/elastic_search_mappings/threat_matrix_bi.json @@ -39,8 +39,10 @@ }, "class_instance": { "type": "keyword" + }, + "job_id": { + "type": "long" } - } } } \ No newline at end of file diff --git a/configuration/ldap_config.py b/configuration/ldap_config.py index 343ad027..66703485 100644 --- a/configuration/ldap_config.py +++ b/configuration/ldap_config.py @@ -2,7 +2,7 @@ # See the file 'LICENSE' for copying permission. # Check the documentation for the details on how to configure LDAP -# https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/advanced_configuration/#ldap +# https://khulnasoft.github.io/docs/ThreatMatrix/advanced_configuration/#ldap import ldap from django_auth_ldap.config import GroupOfNamesType, LDAPSearch diff --git a/create_elastic_certs b/create_elastic_certs new file mode 100755 index 00000000..c9f846db --- /dev/null +++ b/create_elastic_certs @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +if [ ! -f ./certs/elastic_ca/ca.crt ] && [ ! -f ./certs/elastic_ca/ca.key ] && [ ! -f ./certs/elastic_instance/instance.crt ] && [ ! -f ./certs/elastic_instance/instance.key ]; then + # start container + docker pull docker.elastic.co/elasticsearch/elasticsearch:8.15.0 && + docker run -d --name elasticsearch_cert -v ./elasticsearch_instances.yml:/usr/share/elasticsearch/elasticsearch_instances.yml -it docker.elastic.co/elasticsearch/elasticsearch:8.15.0 && + # generate ca + docker exec -ti elasticsearch_cert ./bin/elasticsearch-certutil ca --pem --out ca.zip && + docker exec -ti elasticsearch_cert unzip ca.zip && + # generate cert signed with the ca previously generate + docker exec -ti elasticsearch_cert ./bin/elasticsearch-certutil cert --in /usr/share/elasticsearch/elasticsearch_instances.yml --pem --ca-cert ./ca/ca.crt --ca-key ./ca/ca.key --silent --out cert.zip && + docker exec -ti elasticsearch_cert unzip cert.zip && + # extract files from the container + docker cp elasticsearch_cert:/usr/share/elasticsearch/ca ./certs/elastic_ca && + docker cp elasticsearch_cert:/usr/share/elasticsearch/elasticsearch ./certs/elastic_instance && + # down container + docker kill elasticsearch_cert && + docker rm elasticsearch_cert +else + echo "files already exists" +fi \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 4d78de44..2e5c63bc 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -64,8 +64,6 @@ RUN touch ${LOG_PATH}/django/api_app.log ${LOG_PATH}/django/api_app_errors.log \ && touch ${LOG_PATH}/django/authentication.log ${LOG_PATH}/django/authentication_errors.log \ && touch ${LOG_PATH}/asgi/daphne.log \ && chown -R www-data:www-data ${LOG_PATH} /opt/deploy/ \ - # this is cause stringstifer creates this directory during the build and cause celery to crash - && rm -rf /root/.local \ && ${PYTHONPATH}/docker/scripts/watchman_install.sh \ # download github stuff && ${PYTHONPATH}/api_app/analyzers_manager/repo_downloader.sh diff --git a/docker/ci.override.yml b/docker/ci.override.yml index 189451bd..dac1e47e 100644 --- a/docker/ci.override.yml +++ b/docker/ci.override.yml @@ -5,9 +5,9 @@ services: deploy: resources: limits: - cpus: "1" + cpus: '1' memory: 2000M - + uwsgi: build: context: .. @@ -18,6 +18,7 @@ services: env_file: - env_file_app_ci + daphne: image: khulnasoft/threatmatrix:ci env_file: @@ -25,7 +26,7 @@ services: deploy: resources: limits: - cpus: "0.50" + cpus: '0.50' memory: 200M nginx: @@ -38,7 +39,7 @@ services: deploy: resources: limits: - cpus: "0.50" + cpus: '0.50' memory: 200M celery_beat: @@ -48,7 +49,7 @@ services: deploy: resources: limits: - cpus: "0.50" + cpus: '0.50' memory: 200M celery_worker_default: @@ -58,12 +59,12 @@ services: deploy: resources: limits: - cpus: "0.50" + cpus: '0.50' memory: 200M redis: deploy: resources: limits: - cpus: "0.50" - memory: 200M + cpus: '0.50' + memory: 200M \ No newline at end of file diff --git a/docker/default.yml b/docker/default.yml index cc30487e..07cd6bb4 100644 --- a/docker/default.yml +++ b/docker/default.yml @@ -20,7 +20,7 @@ services: - env_file_app - .env healthcheck: - test: ["CMD-SHELL", "nc -z localhost 8001 || exit 1"] + test: [ "CMD-SHELL", "nc -z localhost 8001 || exit 1" ] interval: 5s timeout: 2s start_period: 300s @@ -83,6 +83,7 @@ services: uwsgi: condition: service_healthy + celery_worker_default: image: khulnasoft/threatmatrix:${REACT_APP_THREATMATRIX_VERSION} container_name: threatmatrix_celery_worker_default @@ -106,6 +107,7 @@ services: condition: service_healthy <<: *no-healthcheck + volumes: postgres_data: nginx_logs: diff --git a/docker/elasticsearch.override.yml b/docker/elasticsearch.override.yml index 7bea7a65..0c67e016 100644 --- a/docker/elasticsearch.override.yml +++ b/docker/elasticsearch.override.yml @@ -2,18 +2,29 @@ services: uwsgi: depends_on: - elasticsearch + volumes: + - ../certs:/opt/deploy/threat_matrix/certs elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 + image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0 + container_name: threatmatrix_elasticsearch + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 9200 || exit 1"] + interval: 5s + timeout: 2s + start_period: 2s + retries: 6 + env_file: + - env_file_elasticsearch + volumes: + - elastic_data:/usr/share/elasticsearch/data + - ../certs:/usr/share/elasticsearch/config/certificates environment: - - "discovery.type=single-node" - - kibana: - image: docker.elastic.co/kibana/kibana:7.17.0 - environment: - ELASTICSEARCH_HOSTS: '["http://elasticsearch:9200"]' - ports: - - '5601:5601' - depends_on: - - elasticsearch + - discovery.type=single-node + - xpack.security.http.ssl.enabled=true + - xpack.security.http.ssl.key=/usr/share/elasticsearch/config/certificates/elastic_instance/elasticsearch.key + - xpack.security.http.ssl.certificate_authorities=/usr/share/elasticsearch/config/certificates/elastic_ca/ca.crt + - xpack.security.http.ssl.certificate=/usr/share/elasticsearch/config/certificates/elastic_instance/elasticsearch.crt +volumes: + elastic_data: \ No newline at end of file diff --git a/docker/entrypoints/celery_default.sh b/docker/entrypoints/celery_default.sh index 798d01c7..45741792 100755 --- a/docker/entrypoints/celery_default.sh +++ b/docker/entrypoints/celery_default.sh @@ -14,7 +14,16 @@ else worker_number=8 fi -ARGUMENTS="-A threat_matrix.celery worker -n worker_default --uid www-data --gid www-data --time-limit=10000 --pidfile= -c $worker_number -Ofair -Q default,broadcast,config -E --without-gossip" + +if [ "$AWS_SQS" = "True" ] +then + queues="default.fifo,config.fifo" +else + queues="default,broadcast,config" +fi + + +ARGUMENTS="-A threat_matrix.celery worker -n worker_default --uid www-data --gid www-data --time-limit=10000 --pidfile= -c $worker_number -Ofair -Q ${queues} -E --without-gossip" if [[ $DEBUG == "True" ]] && [[ $DJANGO_TEST_SERVER == "True" ]]; then echo "Running celery with autoreload" diff --git a/docker/entrypoints/celery_ingestor.sh b/docker/entrypoints/celery_ingestor.sh index 68bf11d3..cebfefb4 100755 --- a/docker/entrypoints/celery_ingestor.sh +++ b/docker/entrypoints/celery_ingestor.sh @@ -4,7 +4,15 @@ until cd /opt/deploy/threat_matrix do echo "Waiting for server volume..." done -ARGUMENTS="-A threat_matrix.celery worker -n worker_ingestor --uid www-data --gid www-data --time-limit=40000 --pidfile= -Ofair -Q ingestor,broadcast,config -E --autoscale=1,15 --without-gossip" + +if [ "$AWS_SQS" = "True" ] +then + queues="ingestor.fifo,config.fifo" +else + queues="ingestor,broadcast,config" +fi + +ARGUMENTS="-A threat_matrix.celery worker -n worker_ingestor --uid www-data --gid www-data --time-limit=40000 --pidfile= -Ofair -Q ${queues} -E --autoscale=1,15 --without-gossip" if [[ $DEBUG == "True" ]] && [[ $DJANGO_TEST_SERVER == "True" ]]; then echo "Running celery with autoreload" diff --git a/docker/entrypoints/celery_local.sh b/docker/entrypoints/celery_local.sh index 084aac31..2b6ee63b 100755 --- a/docker/entrypoints/celery_local.sh +++ b/docker/entrypoints/celery_local.sh @@ -4,8 +4,14 @@ until cd /opt/deploy/threat_matrix do echo "Waiting for server volume..." done +if [ "$AWS_SQS" = "True" ] +then + queues="local.fifo,config.fifo" +else + queues="local,broadcast,config" +fi -ARGUMENTS="-A threat_matrix.celery worker -n worker_local --uid www-data --time-limit=10000 --gid www-data --pidfile= -Ofair -Q local,broadcast,config -E --without-gossip" +ARGUMENTS="-A threat_matrix.celery worker -n worker_local --uid www-data --time-limit=10000 --gid www-data --pidfile= -Ofair -Q ${queues} -E --without-gossip" if [[ $DEBUG == "True" ]] && [[ $DJANGO_TEST_SERVER == "True" ]]; then echo "Running celery with autoreload" diff --git a/docker/entrypoints/celery_long.sh b/docker/entrypoints/celery_long.sh index 13c8333c..8a60779a 100755 --- a/docker/entrypoints/celery_long.sh +++ b/docker/entrypoints/celery_long.sh @@ -4,7 +4,14 @@ until cd /opt/deploy/threat_matrix do echo "Waiting for server volume..." done -ARGUMENTS="-A threat_matrix.celery worker -n worker_long --uid www-data --gid www-data --time-limit=40000 --pidfile= -Ofair -Q long,broadcast,config -E --without-gossip" +if [ "$AWS_SQS" = "True" ] +then + queues="long.fifo,config.fifo" +else + queues="long,broadcast,config" +fi + +ARGUMENTS="-A threat_matrix.celery worker -n worker_long --uid www-data --gid www-data --time-limit=40000 --pidfile= -Ofair -Q ${queues} -E --without-gossip" if [[ $DEBUG == "True" ]] && [[ $DJANGO_TEST_SERVER == "True" ]]; then echo "Running celery with autoreload" diff --git a/docker/entrypoints/uwsgi.sh b/docker/entrypoints/uwsgi.sh index 5f7420ae..e92dc6bf 100755 --- a/docker/entrypoints/uwsgi.sh +++ b/docker/entrypoints/uwsgi.sh @@ -28,6 +28,7 @@ echo "DEBUG: " $DEBUG echo "DJANGO_TEST_SERVER: " $DJANGO_TEST_SERVER echo "------------------------------" CHANGELOG_NOTIFICATION_COMMAND='python manage.py changelog_notification .github/CHANGELOG.md THREATMATRIX --number-of-releases 3' +ELASTIC_TEMPLATE_COMMAND='python manage.py elastic_templates' if [[ $DEBUG == "True" ]] && [[ $DJANGO_TEST_SERVER == "True" ]]; then @@ -43,8 +44,10 @@ then fi $CHANGELOG_NOTIFICATION_COMMAND --debug + $ELASTIC_TEMPLATE_COMMAND python manage.py runserver 0.0.0.0:8001 else $CHANGELOG_NOTIFICATION_COMMAND + $ELASTIC_TEMPLATE_COMMAND /usr/local/bin/uwsgi --ini /etc/uwsgi/sites/threat_matrix.ini --stats 127.0.0.1:1717 --stats-http fi diff --git a/docker/env_file_app_ci b/docker/env_file_app_ci index 16020a1d..8507cb2d 100644 --- a/docker/env_file_app_ci +++ b/docker/env_file_app_ci @@ -30,6 +30,7 @@ AWS_SECRET_ACCESS_KEY= # Elastic Search Configuration ELASTICSEARCH_DSL_ENABLED=False ELASTICSEARCH_DSL_HOST= +ELASTICSEARCH_DSL_PASSWORD=changeme ELASTICSEARCH_DSL_NO_OF_SHARDS=1 ELASTICSEARCH_DSL_NO_OF_REPLICAS=0 diff --git a/docker/env_file_app_template b/docker/env_file_app_template index 92b8053c..c96f91f1 100644 --- a/docker/env_file_app_template +++ b/docker/env_file_app_template @@ -55,6 +55,7 @@ DEFAULT_SLACK_CHANNEL= # Elastic Search Configuration ELASTICSEARCH_DSL_ENABLED=False ELASTICSEARCH_DSL_HOST= +ELASTICSEARCH_DSL_PASSWORD= # consult to: https://django-elasticsearch-dsl.readthedocs.io/en/latest/settings.html ELASTICSEARCH_DSL_NO_OF_SHARDS=1 ELASTICSEARCH_DSL_NO_OF_REPLICAS=0 diff --git a/docker/env_file_elasticsearch_template b/docker/env_file_elasticsearch_template new file mode 100644 index 00000000..6aad810f --- /dev/null +++ b/docker/env_file_elasticsearch_template @@ -0,0 +1 @@ +ELASTIC_PASSWORD= \ No newline at end of file diff --git a/docker/flower.override.yml b/docker/flower.override.yml index 05df92bc..8d74c5ca 100644 --- a/docker/flower.override.yml +++ b/docker/flower.override.yml @@ -43,4 +43,4 @@ services: - rabbitmq volumes: - shared_htpasswd: + shared_htpasswd: \ No newline at end of file diff --git a/docker/postgres.override.yml b/docker/postgres.override.yml index 20d85623..b5f3716b 100644 --- a/docker/postgres.override.yml +++ b/docker/postgres.override.yml @@ -1,4 +1,5 @@ services: + postgres: image: library/postgres:16-alpine container_name: threatmatrix_postgres @@ -7,17 +8,19 @@ services: env_file: - ./env_file_postgres healthcheck: - test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ] interval: 5s timeout: 2s retries: 6 start_period: 3s + uwsgi: depends_on: postgres: condition: service_healthy + celery_worker_default: depends_on: postgres: diff --git a/docker/redis.override.yml b/docker/redis.override.yml index de4bbd0f..91168699 100644 --- a/docker/redis.override.yml +++ b/docker/redis.override.yml @@ -38,4 +38,4 @@ services: celery_worker_default: environment: - BROKER_URL=redis://redis:6379/1 - - WEBSOCKETS_URL=redis://redis:6379/0 + - WEBSOCKETS_URL=redis://redis:6379/0 \ No newline at end of file diff --git a/docker/test.flower.override.yml b/docker/test.flower.override.yml index c7bf020a..0548e97d 100644 --- a/docker/test.flower.override.yml +++ b/docker/test.flower.override.yml @@ -2,4 +2,4 @@ services: flower: image: khulnasoft/threatmatrix:test volumes: - - ../:/opt/deploy/threat_matrix + - ../:/opt/deploy/threat_matrix \ No newline at end of file diff --git a/docker/test.multi-queue.override.yml b/docker/test.multi-queue.override.yml index 8f660fde..d52990eb 100644 --- a/docker/test.multi-queue.override.yml +++ b/docker/test.multi-queue.override.yml @@ -12,4 +12,4 @@ services: celery_worker_ingestor: image: khulnasoft/threatmatrix:test volumes: - - ../:/opt/deploy/threat_matrix + - ../:/opt/deploy/threat_matrix \ No newline at end of file diff --git a/docker/test.override.yml b/docker/test.override.yml index caf2e414..f558992b 100644 --- a/docker/test.override.yml +++ b/docker/test.override.yml @@ -40,4 +40,4 @@ services: volumes: - ../:/opt/deploy/threat_matrix environment: - - DEBUG=True + - DEBUG=True \ No newline at end of file diff --git a/docker/traefik_local.yml b/docker/traefik_local.yml index 2361ebee..41f4e3d6 100644 --- a/docker/traefik_local.yml +++ b/docker/traefik_local.yml @@ -21,8 +21,8 @@ services: - "/var/run/docker.sock:/var/run/docker.sock:ro" nginx: - depends_on: - - traefik - labels: - - "traefik.http.routers.nginx.rule=Host(`localhost`)" - - "traefik.http.routers.nginx.entrypoints=web" + depends_on: + - traefik + labels: + - "traefik.http.routers.nginx.rule=Host(`localhost`)" + - "traefik.http.routers.nginx.entrypoints=web" diff --git a/docker/traefik_prod.yml b/docker/traefik_prod.yml index 74105a64..79de4473 100644 --- a/docker/traefik_prod.yml +++ b/docker/traefik_prod.yml @@ -28,20 +28,20 @@ services: # PROD - use this if everything works fine - # CHANGE THIS #- "--certificatesresolvers.le.acme.caserver=https://acme-v02.api.letsencrypt.org/directory" - "--certificatesresolvers.le.acme.email=postmaster@example.com" # CHANGE THIS - - "--certificatesresolvers.le.acme.storage=/etc/letsencrypt/acme.json" + - "--certificatesresolvers.le.acme.storage=/etc/letsencrypt/acme.json" labels: # DASHBOARD - setup for secure dashboard access - "traefik.http.routers.dashboard.rule=Host(`traefik.threatmatrix.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" # CHANGE THIS (Only "Host"!) - "traefik.http.routers.dashboard.service=api@internal" - "traefik.http.routers.dashboard.entrypoints=websecure" - - "traefik.http.routers.dashboard.tls=true" + - "traefik.http.routers.dashboard.tls=true" - "traefik.http.routers.dashboard.tls.certresolver=le" # auth/ipallowlist middlewares allow to limit/secure access - may be omitted # Here you may define which IPs/CIDR ranges are allowed to access this resource - may be omitted # - "traefik.http.routers.dashboard.middlewares=dashboard-ipallowlist" # - "traefik.http.middlewares.dashboard-ipallowlist.ipallowlist.sourcerange=0.0.0.0" # CHANGE THIS - # You can create a new user and password for basic auth with this command: - # echo $(htpasswd -nbB user password) | sed -e s/\\$/\\$\\$/g + # You can create a new user and password for basic auth with this command: + # echo $(htpasswd -nbB user password) | sed -e s/\\$/\\$\\$/g # - "traefik.http.routers.dashboard.middlewares=auth" # - "traefik.http.middlewares.auth.basicauth.users=user:$$2y$$05$$v.ncVNXEJriELglCBEZJmu5I1VrhyhuaVCXATRQTUVuvOF1qgYwpa" # CHANGE THIS (default is user:password) - "traefik.http.services.dashboard.loadbalancer.server.port=8080" @@ -54,13 +54,13 @@ services: - "/var/log/traefik:/var/log/traefik" nginx: - depends_on: - - traefik - labels: - - "traefik.http.routers.nginx.rule=Host(`threatmatrix.example.com`)" # CHANGE THIS - - "traefik.http.routers.nginx.entrypoints=websecure" - - "traefik.http.routers.nginx.tls=true" - - "traefik.http.routers.nginx.tls.certresolver=le" - # Here you may define which IPs/CIDR ranges are allowed to access this resource - # - "traefik.http.routers.nginx.middlewares=nginx-ipallowlist" - # - "traefik.http.middlewares.nginx-ipallowlist.ipallowlist.sourcerange=0.0.0.0" # CHANGE THIS + depends_on: + - traefik + labels: + - "traefik.http.routers.nginx.rule=Host(`threatmatrix.example.com`)" # CHANGE THIS + - "traefik.http.routers.nginx.entrypoints=websecure" + - "traefik.http.routers.nginx.tls=true" + - "traefik.http.routers.nginx.tls.certresolver=le" + # Here you may define which IPs/CIDR ranges are allowed to access this resource + # - "traefik.http.routers.nginx.middlewares=nginx-ipallowlist" + # - "traefik.http.middlewares.nginx-ipallowlist.ipallowlist.sourcerange=0.0.0.0" # CHANGE THIS diff --git a/elasticsearch_instances.yml b/elasticsearch_instances.yml new file mode 100644 index 00000000..6df8f703 --- /dev/null +++ b/elasticsearch_instances.yml @@ -0,0 +1,2 @@ +instances: + - name: elasticsearch diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 00000000..84f38442 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,2 @@ +# Ignore artifacts: +.coverage diff --git a/frontend/README.md b/frontend/README.md index c7131ebc..20092288 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -52,7 +52,7 @@ src/ source code The frontend inside the docker containers does not hot-reload, so you need to use `CRA dev server` on your host machine to serve pages when doing development on the frontend, using docker nginx only as API source. -- Start ThreatMatrix containers (see [docs](https://khulnasoft.github.io/ThreatMatrix-docs/ThreatMatrix/installation/)). Original dockerized app is accessible on `http://localhost:80` +- Start ThreatMatrix containers (see [docs](https://khulnasoft.github.io/docs/ThreatMatrix/installation/)). Original dockerized app is accessible on `http://localhost:80` - If you have not `node-js` installed, you have to do that. Follow the guide [here](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-20-04). We tested this with NodeJS >=16.6 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c89ee56a..f225171e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,16 +1,16 @@ { "name": "threatmatrix", - "version": "6.0.0", + "version": "6.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "threatmatrix", - "version": "6.0.0", + "version": "6.1.0", "dependencies": { "@certego/certego-ui": "^0.1.13", - "@dagrejs/dagre": "^1.0.4", - "axios": "^1.7.4", + "@dagrejs/dagre": "^1.1.4", + "axios": "^1.7.7", "axios-hooks": "^3.1.5", "bootstrap": "^5.3.3", "classnames": "^2.5.1", @@ -21,40 +21,40 @@ "prop-types": "^15.8.1", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-error-boundary": "^4.0.13", + "react-error-boundary": "^4.1.0", "react-icons": "^4.12.0", - "react-joyride": "^2.8.1", + "react-joyride": "^2.9.2", "react-json-tree": "^0.19.0", "react-markdown": "^8.0.7", - "react-router-dom": "^6.22.0", + "react-router-dom": "^6.27.0", "react-scripts": "^5.0.1", - "react-select": "^5.8.0", + "react-select": "^5.8.1", "react-table": "^7.8.0", - "react-use": "^17.5.0", - "reactflow": "^11.10.4", - "reactstrap": "^9.2.1", - "recharts": "^2.12.6", + "react-use": "^17.5.1", + "reactflow": "^11.11.4", + "reactstrap": "^9.2.3", + "recharts": "^2.13.0", "zustand": "^4.5.4" }, "devDependencies": { - "@babel/preset-env": "^7.24.7", - "@babel/preset-react": "^7.24.7", - "@testing-library/jest-dom": "^6.4.2", + "@babel/preset-env": "^7.25.8", + "@babel/preset-react": "^7.25.7", + "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^14.5.2", "babel-eslint": "^10.1.0", "babel-jest": "^29.7.0", - "eslint": "^8.48.0", + "eslint": "^8.57.1", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsx-a11y": "^6.8.0", - "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^4.5.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.1", + "eslint-plugin-react-hooks": "^4.6.2", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "prettier": "^3.2.5", - "sass": "^1.77.0", + "prettier": "^3.3.3", + "sass": "^1.79.5", "stylelint": "^14.9.1", "stylelint-config-prettier": "^9.0.3", "stylelint-config-standard-scss": "^4.0.0" @@ -69,9 +69,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", - "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", "dev": true }, "node_modules/@alloc/quick-lru": { @@ -98,11 +98,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/highlight": "^7.25.7", "picocolors": "^1.0.0" }, "engines": { @@ -110,9 +110,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.8.tgz", - "integrity": "sha512-c4IM7OTg6k1Q+AJ153e2mc2QVTezTwnb4VzquwcyiEzGnW0Kedv4do/TrkU98qPeC5LNiMt/QXwIjzYXLBpyZg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", "engines": { "node": ">=6.9.0" } @@ -172,50 +172,50 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.8.tgz", - "integrity": "sha512-47DG+6F5SzOi0uEvK4wMShmn5yY0mVjVJoWTphdY2B4Rx9wHgjK7Yhtr0ru6nE+sn0v38mzrWOlah0p/YlHHOQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", "dependencies": { - "@babel/types": "^7.24.8", + "@babel/types": "^7.25.7", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", - "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", - "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz", + "integrity": "sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg==", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", - "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", "dependencies": { - "@babel/compat-data": "^7.24.8", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -224,18 +224,16 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.8.tgz", - "integrity": "sha512-4f6Oqnmyp2PP3olgUMmOwC3akxSm5aBYraQ6YDdKy7NcAMkDECHWG0DEnV6M2UAkERgIBhYt8S27rURPg7SxWA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", + "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/traverse": "^7.25.7", "semver": "^6.3.1" }, "engines": { @@ -246,12 +244,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", - "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz", + "integrity": "sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "regexpu-core": "^5.3.1", + "@babel/helper-annotate-as-pure": "^7.25.7", + "regexpu-core": "^6.1.1", "semver": "^6.3.1" }, "engines": { @@ -276,74 +274,39 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", - "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", + "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", "dependencies": { - "@babel/traverse": "^7.24.8", - "@babel/types": "^7.24.8" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.8.tgz", - "integrity": "sha512-m4vWKVqvkVAWLXfHCCfff2luJj86U+J0/x+0N3ArG/tP0Fq7zky2dYwMbtPmkc/oulkkbjdL3uWzuoBwQ8R00Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -353,32 +316,32 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", - "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", + "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", - "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz", + "integrity": "sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-wrap-function": "^7.24.7" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-wrap-function": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -388,13 +351,13 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", - "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", + "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7" + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -404,24 +367,24 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", - "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", + "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -439,38 +402,37 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", - "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz", + "integrity": "sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg==", "dependencies": { - "@babel/helper-function-name": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -490,11 +452,11 @@ } }, "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -504,9 +466,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", - "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "dependencies": { + "@babel/types": "^7.25.8" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -515,12 +480,26 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", - "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.7.tgz", + "integrity": "sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.7.tgz", + "integrity": "sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -530,11 +509,11 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", - "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz", + "integrity": "sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -544,13 +523,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", - "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz", + "integrity": "sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/plugin-transform-optional-chaining": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -560,12 +539,12 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", - "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.7.tgz", + "integrity": "sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw==", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -712,20 +691,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-decorators": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.22.5.tgz", @@ -740,28 +705,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-flow": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz", @@ -777,11 +720,11 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", - "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz", + "integrity": "sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -791,11 +734,11 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -827,11 +770,11 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -906,20 +849,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", @@ -964,11 +893,11 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", - "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz", + "integrity": "sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -978,14 +907,13 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", - "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.8.tgz", + "integrity": "sha512-9ypqkozyzpG+HxlH4o4gdctalFGIjjdufzo7I2XPda0iBnZ6a+FO0rIEQcdSPXp02CkvGsII1exJhmROPQd5oA==", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-remap-async-to-generator": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -995,13 +923,13 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", - "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.7.tgz", + "integrity": "sha512-ZUCjAavsh5CESCmi/xCpX1qcCaAglzs/7tmuvoFnJgA1dM7gQplsguljoTg+Ru8WENpX89cQyAtWoaE0I3X3Pg==", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7" + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-remap-async-to-generator": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1011,11 +939,11 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", - "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz", + "integrity": "sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1025,11 +953,11 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", - "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz", + "integrity": "sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1039,12 +967,12 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", - "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz", + "integrity": "sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1054,13 +982,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", - "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz", + "integrity": "sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1070,17 +997,15 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.8.tgz", - "integrity": "sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-replace-supers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz", + "integrity": "sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/traverse": "^7.25.7", "globals": "^11.1.0" }, "engines": { @@ -1091,12 +1016,12 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", - "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz", + "integrity": "sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/template": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/template": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1106,11 +1031,11 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", - "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz", + "integrity": "sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1120,12 +1045,12 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", - "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz", + "integrity": "sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1135,11 +1060,11 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", - "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz", + "integrity": "sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1148,13 +1073,27 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", - "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz", + "integrity": "sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1164,12 +1103,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", - "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz", + "integrity": "sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg==", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1179,12 +1118,11 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", - "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz", + "integrity": "sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1209,12 +1147,12 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", - "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz", + "integrity": "sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1224,13 +1162,13 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", - "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz", + "integrity": "sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ==", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1240,12 +1178,11 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", - "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz", + "integrity": "sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1255,11 +1192,11 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", - "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz", + "integrity": "sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1269,12 +1206,11 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", - "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz", + "integrity": "sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1284,11 +1220,11 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", - "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz", + "integrity": "sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1298,12 +1234,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", - "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz", + "integrity": "sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA==", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1313,13 +1249,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", - "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz", + "integrity": "sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg==", "dependencies": { - "@babel/helper-module-transforms": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-simple-access": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1329,14 +1265,14 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", - "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz", + "integrity": "sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g==", "dependencies": { - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1346,12 +1282,12 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", - "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz", + "integrity": "sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw==", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1361,12 +1297,12 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", - "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1376,11 +1312,11 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", - "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz", + "integrity": "sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1390,12 +1326,11 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", - "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz", + "integrity": "sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1405,12 +1340,11 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", - "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz", + "integrity": "sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1420,14 +1354,13 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", - "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz", + "integrity": "sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==", "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.7" + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-transform-parameters": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1437,12 +1370,12 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", - "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz", + "integrity": "sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1452,12 +1385,11 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", - "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz", + "integrity": "sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1467,13 +1399,12 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", - "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz", + "integrity": "sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1483,11 +1414,11 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", - "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz", + "integrity": "sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1497,12 +1428,12 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", - "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz", + "integrity": "sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1512,14 +1443,13 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", - "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz", + "integrity": "sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1529,11 +1459,11 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", - "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz", + "integrity": "sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1557,11 +1487,11 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", - "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.7.tgz", + "integrity": "sha512-r0QY7NVU8OnrwE+w2IWiRom0wwsTbjx4+xH2RTd7AVdof3uurXOF+/mXHQDRk+2jIvWgSaCHKMgggfvM4dyUGA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1571,15 +1501,15 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz", - "integrity": "sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.7.tgz", + "integrity": "sha512-vILAg5nwGlR9EXE8JIOX4NHXd49lrYbN8hnjffDtoULwpL9hUx/N55nqh2qd0q6FyNDfjl9V79ecKGvFbcSA0Q==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/plugin-syntax-jsx": "^7.24.7", - "@babel/types": "^7.25.2" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-syntax-jsx": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1589,11 +1519,11 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", - "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.7.tgz", + "integrity": "sha512-5yd3lH1PWxzW6IZj+p+Y4OLQzz0/LzlOG8vGqonHfVR3euf1vyzyMUJk9Ac+m97BH46mFc/98t9PmYLyvgL3qg==", "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.24.7" + "@babel/plugin-transform-react-jsx": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1603,12 +1533,12 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", - "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.7.tgz", + "integrity": "sha512-6YTHJ7yjjgYqGc8S+CbEXhLICODk0Tn92j+vNJo07HFk9t3bjFgAKxPLFhHwF2NjmQVSI1zBRfBWUeVBa2osfA==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1618,11 +1548,11 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", - "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz", + "integrity": "sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.25.7", "regenerator-transform": "^0.15.2" }, "engines": { @@ -1633,11 +1563,11 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", - "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz", + "integrity": "sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1666,11 +1596,11 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", - "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz", + "integrity": "sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1680,12 +1610,12 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", - "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz", + "integrity": "sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1695,11 +1625,11 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", - "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz", + "integrity": "sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1709,11 +1639,11 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", - "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz", + "integrity": "sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1723,11 +1653,11 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", - "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz", + "integrity": "sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1754,11 +1684,11 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", - "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz", + "integrity": "sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1768,12 +1698,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", - "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz", + "integrity": "sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1783,12 +1713,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", - "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz", + "integrity": "sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1798,12 +1728,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", - "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz", + "integrity": "sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw==", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1813,90 +1743,77 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.8.tgz", - "integrity": "sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ==", - "dependencies": { - "@babel/compat-data": "^7.24.8", - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-validator-option": "^7.24.8", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.8.tgz", + "integrity": "sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg==", + "dependencies": { + "@babel/compat-data": "^7.25.8", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.24.7", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-import-assertions": "^7.25.7", + "@babel/plugin-syntax-import-attributes": "^7.25.7", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.24.7", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoped-functions": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.24.7", - "@babel/plugin-transform-class-properties": "^7.24.7", - "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.24.8", - "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-dotall-regex": "^7.24.7", - "@babel/plugin-transform-duplicate-keys": "^7.24.7", - "@babel/plugin-transform-dynamic-import": "^7.24.7", - "@babel/plugin-transform-exponentiation-operator": "^7.24.7", - "@babel/plugin-transform-export-namespace-from": "^7.24.7", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-function-name": "^7.24.7", - "@babel/plugin-transform-json-strings": "^7.24.7", - "@babel/plugin-transform-literals": "^7.24.7", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-member-expression-literals": "^7.24.7", - "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-modules-systemjs": "^7.24.7", - "@babel/plugin-transform-modules-umd": "^7.24.7", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-new-target": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-numeric-separator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-object-super": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-property-literals": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-reserved-words": "^7.24.7", - "@babel/plugin-transform-shorthand-properties": "^7.24.7", - "@babel/plugin-transform-spread": "^7.24.7", - "@babel/plugin-transform-sticky-regex": "^7.24.7", - "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.8", - "@babel/plugin-transform-unicode-escapes": "^7.24.7", - "@babel/plugin-transform-unicode-property-regex": "^7.24.7", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/plugin-transform-arrow-functions": "^7.25.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.8", + "@babel/plugin-transform-async-to-generator": "^7.25.7", + "@babel/plugin-transform-block-scoped-functions": "^7.25.7", + "@babel/plugin-transform-block-scoping": "^7.25.7", + "@babel/plugin-transform-class-properties": "^7.25.7", + "@babel/plugin-transform-class-static-block": "^7.25.8", + "@babel/plugin-transform-classes": "^7.25.7", + "@babel/plugin-transform-computed-properties": "^7.25.7", + "@babel/plugin-transform-destructuring": "^7.25.7", + "@babel/plugin-transform-dotall-regex": "^7.25.7", + "@babel/plugin-transform-duplicate-keys": "^7.25.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.7", + "@babel/plugin-transform-dynamic-import": "^7.25.8", + "@babel/plugin-transform-exponentiation-operator": "^7.25.7", + "@babel/plugin-transform-export-namespace-from": "^7.25.8", + "@babel/plugin-transform-for-of": "^7.25.7", + "@babel/plugin-transform-function-name": "^7.25.7", + "@babel/plugin-transform-json-strings": "^7.25.8", + "@babel/plugin-transform-literals": "^7.25.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.8", + "@babel/plugin-transform-member-expression-literals": "^7.25.7", + "@babel/plugin-transform-modules-amd": "^7.25.7", + "@babel/plugin-transform-modules-commonjs": "^7.25.7", + "@babel/plugin-transform-modules-systemjs": "^7.25.7", + "@babel/plugin-transform-modules-umd": "^7.25.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.7", + "@babel/plugin-transform-new-target": "^7.25.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.8", + "@babel/plugin-transform-numeric-separator": "^7.25.8", + "@babel/plugin-transform-object-rest-spread": "^7.25.8", + "@babel/plugin-transform-object-super": "^7.25.7", + "@babel/plugin-transform-optional-catch-binding": "^7.25.8", + "@babel/plugin-transform-optional-chaining": "^7.25.8", + "@babel/plugin-transform-parameters": "^7.25.7", + "@babel/plugin-transform-private-methods": "^7.25.7", + "@babel/plugin-transform-private-property-in-object": "^7.25.8", + "@babel/plugin-transform-property-literals": "^7.25.7", + "@babel/plugin-transform-regenerator": "^7.25.7", + "@babel/plugin-transform-reserved-words": "^7.25.7", + "@babel/plugin-transform-shorthand-properties": "^7.25.7", + "@babel/plugin-transform-spread": "^7.25.7", + "@babel/plugin-transform-sticky-regex": "^7.25.7", + "@babel/plugin-transform-template-literals": "^7.25.7", + "@babel/plugin-transform-typeof-symbol": "^7.25.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.7", + "@babel/plugin-transform-unicode-property-regex": "^7.25.7", + "@babel/plugin-transform-unicode-regex": "^7.25.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.7", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-corejs3": "^0.10.6", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.37.1", + "core-js-compat": "^3.38.1", "semver": "^6.3.1" }, "engines": { @@ -1922,12 +1839,12 @@ } }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", - "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.1", - "core-js-compat": "^3.36.1" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -1958,16 +1875,16 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", - "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.25.7.tgz", + "integrity": "sha512-GjV0/mUEEXpi1U5ZgDprMRRgajGMRW3G5FjMr5KLKD8nT2fTG8+h/klV3+6Dm5739QE+K5+2e91qFKAYI3pmRg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.24.7", - "@babel/plugin-transform-react-jsx-development": "^7.24.7", - "@babel/plugin-transform-react-pure-annotations": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "@babel/plugin-transform-react-display-name": "^7.25.7", + "@babel/plugin-transform-react-jsx": "^7.25.7", + "@babel/plugin-transform-react-jsx-development": "^7.25.7", + "@babel/plugin-transform-react-pure-annotations": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -1994,11 +1911,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, "node_modules/@babel/runtime": { "version": "7.23.8", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", @@ -2016,31 +1928,28 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", - "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.8", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.8", - "@babel/types": "^7.24.8", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2049,12 +1958,12 @@ } }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2374,17 +2283,17 @@ } }, "node_modules/@dagrejs/dagre": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.0.4.tgz", - "integrity": "sha512-jrEore+HhW1yg1Rsd9H1PPMcoEOD4bVh0WCXc6GqzyzubnJj4GaWGg8ETOrskTd/3n/g5LOzumGM4CCgpNLJNw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz", + "integrity": "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==", "dependencies": { - "@dagrejs/graphlib": "2.1.13" + "@dagrejs/graphlib": "2.2.4" } }, "node_modules/@dagrejs/graphlib": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.1.13.tgz", - "integrity": "sha512-calbMa7Gcyo+/t23XBaqQqon8LlgE9regey4UVoikoenKBXvUnCUL3s9RP6USCxttfr0XWVICtYUuKMdehKqMw==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz", + "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==", "engines": { "node": ">17.0.0" } @@ -2545,9 +2454,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2572,9 +2481,9 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dependencies": { "type-fest": "^0.20.2" }, @@ -2608,9 +2517,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2634,12 +2543,13 @@ "integrity": "sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==" }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -2659,9 +2569,10 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead" }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -3518,6 +3429,279 @@ "node": ">= 8" } }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "devOptional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", @@ -3628,11 +3812,11 @@ } }, "node_modules/@reactflow/background": { - "version": "11.3.9", - "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.9.tgz", - "integrity": "sha512-byj/G9pEC8tN0wT/ptcl/LkEP/BBfa33/SvBkqE4XwyofckqF87lKp573qGlisfnsijwAbpDlf81PuFL41So4Q==", + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", "dependencies": { - "@reactflow/core": "11.10.4", + "@reactflow/core": "11.11.4", "classcat": "^5.0.3", "zustand": "^4.4.1" }, @@ -3642,11 +3826,11 @@ } }, "node_modules/@reactflow/controls": { - "version": "11.2.9", - "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.9.tgz", - "integrity": "sha512-e8nWplbYfOn83KN1BrxTXS17+enLyFnjZPbyDgHSRLtI5ZGPKF/8iRXV+VXb2LFVzlu4Wh3la/pkxtfP/0aguA==", + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", "dependencies": { - "@reactflow/core": "11.10.4", + "@reactflow/core": "11.11.4", "classcat": "^5.0.3", "zustand": "^4.4.1" }, @@ -3656,9 +3840,9 @@ } }, "node_modules/@reactflow/core": { - "version": "11.10.4", - "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.10.4.tgz", - "integrity": "sha512-j3i9b2fsTX/sBbOm+RmNzYEFWbNx4jGWGuGooh2r1jQaE2eV+TLJgiG/VNOp0q5mBl9f6g1IXs3Gm86S9JfcGw==", + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", "dependencies": { "@types/d3": "^7.4.0", "@types/d3-drag": "^3.0.1", @@ -3676,11 +3860,11 @@ } }, "node_modules/@reactflow/minimap": { - "version": "11.7.9", - "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.9.tgz", - "integrity": "sha512-le95jyTtt3TEtJ1qa7tZ5hyM4S7gaEQkW43cixcMOZLu33VAdc2aCpJg/fXcRrrf7moN2Mbl9WIMNXUKsp5ILA==", + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", "dependencies": { - "@reactflow/core": "11.10.4", + "@reactflow/core": "11.11.4", "@types/d3-selection": "^3.0.3", "@types/d3-zoom": "^3.0.1", "classcat": "^5.0.3", @@ -3694,11 +3878,11 @@ } }, "node_modules/@reactflow/node-resizer": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.9.tgz", - "integrity": "sha512-HfickMm0hPDIHt9qH997nLdgLt0kayQyslKE0RS/GZvZ4UMQJlx/NRRyj5y47Qyg0NnC66KYOQWDM9LLzRTnUg==", + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", "dependencies": { - "@reactflow/core": "11.10.4", + "@reactflow/core": "11.11.4", "classcat": "^5.0.4", "d3-drag": "^3.0.0", "d3-selection": "^3.0.0", @@ -3710,11 +3894,11 @@ } }, "node_modules/@reactflow/node-toolbar": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.9.tgz", - "integrity": "sha512-VmgxKmToax4sX1biZ9LXA7cj/TBJ+E5cklLGwquCCVVxh+lxpZGTBF3a5FJGVHiUNBBtFsC8ldcSZIK4cAlQww==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", "dependencies": { - "@reactflow/core": "11.10.4", + "@reactflow/core": "11.11.4", "classcat": "^5.0.3", "zustand": "^4.4.1" }, @@ -3724,9 +3908,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz", - "integrity": "sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", "engines": { "node": ">=14.0.0" } @@ -3813,6 +3997,11 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==" + }, "node_modules/@rushstack/eslint-patch": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz", @@ -4176,48 +4365,23 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz", - "integrity": "sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", "dev": true, "dependencies": { - "@adobe/css-tools": "^4.3.2", - "@babel/runtime": "^7.9.2", + "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.15", + "lodash": "^4.17.21", "redent": "^3.0.0" }, "engines": { "node": ">=14", "npm": ">=6", "yarn": ">=1" - }, - "peerDependencies": { - "@jest/globals": ">= 28", - "@types/bun": "latest", - "@types/jest": ">= 28", - "jest": ">= 28", - "vitest": ">= 0.32" - }, - "peerDependenciesMeta": { - "@jest/globals": { - "optional": true - }, - "@types/bun": { - "optional": true - }, - "@types/jest": { - "optional": true - }, - "jest": { - "optional": true - }, - "vitest": { - "optional": true - } } }, "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { @@ -4621,9 +4785,9 @@ } }, "node_modules/@types/d3-force": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.9.tgz", - "integrity": "sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==" + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==" }, "node_modules/@types/d3-format": { "version": "3.0.4", @@ -4639,9 +4803,9 @@ } }, "node_modules/@types/d3-hierarchy": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.6.tgz", - "integrity": "sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw==" + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" }, "node_modules/@types/d3-interpolate": { "version": "3.0.1", @@ -4685,9 +4849,9 @@ "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" }, "node_modules/@types/d3-selection": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", - "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==" + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==" }, "node_modules/@types/d3-shape": { "version": "3.1.1", @@ -4713,9 +4877,9 @@ "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==" }, "node_modules/@types/d3-transition": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", - "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "dependencies": { "@types/d3-selection": "*" } @@ -5405,6 +5569,11 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -5782,7 +5951,6 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, "dependencies": { "deep-equal": "^2.0.5" } @@ -5808,14 +5976,15 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" }, "node_modules/array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" }, "engines": { @@ -5853,15 +6022,16 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", - "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5922,27 +6092,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, "node_modules/array.prototype.tosorted": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", - "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { @@ -6059,17 +6221,17 @@ } }, "node_modules/axe-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", - "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz", + "integrity": "sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==", "engines": { "node": ">=4" } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -6118,11 +6280,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", - "dependencies": { - "dequal": "^2.0.3" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "engines": { + "node": ">= 0.4" } }, "node_modules/babel-eslint": { @@ -6629,9 +6791,9 @@ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" }, "node_modules/browserslist": { - "version": "4.23.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", - "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "funding": [ { "type": "opencollective", @@ -6647,9 +6809,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001640", - "electron-to-chromium": "^1.4.820", - "node-releases": "^2.0.14", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, "bin": { @@ -6771,9 +6933,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001642", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz", - "integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==", + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", "funding": [ { "type": "opencollective", @@ -6905,9 +7067,9 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, "node_modules/classcat": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz", - "integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==" + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==" }, "node_modules/classnames": { "version": "2.5.1", @@ -7200,11 +7362,11 @@ } }, "node_modules/core-js-compat": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", - "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", "dependencies": { - "browserslist": "^4.23.0" + "browserslist": "^4.23.3" }, "funding": { "type": "opencollective", @@ -8143,7 +8305,6 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", - "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", @@ -8265,6 +8426,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "devOptional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -8513,9 +8686,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.827", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.827.tgz", - "integrity": "sha512-VY+J0e4SFcNfQy19MEoMdaIcZLmDCprqvBtkii1WTCTQHpRvf5N8+3kTYCgL/PcntvwQvmMJWTuDPsq+IlhWKQ==" + "version": "1.5.39", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.39.tgz", + "integrity": "sha512-4xkpSR6CjuiaNyvwiWDI85N9AxsvbPawB8xc7yzLPonYTuP19BVgYweKyUMFtHEZgIcHWMt1ks5Cqx2m+6/Grg==" }, "node_modules/emittery": { "version": "0.13.1", @@ -8677,7 +8850,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -8812,17 +8984,19 @@ } }, "node_modules/eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -8962,9 +9136,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dependencies": { "debug": "^3.2.7" }, @@ -9003,33 +9177,35 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -9075,77 +9251,69 @@ } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", - "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", + "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", "dependencies": { - "@babel/runtime": "^7.23.2", - "aria-query": "^5.3.0", - "array-includes": "^3.1.7", + "aria-query": "~5.1.3", + "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", - "axe-core": "=4.7.0", - "axobject-query": "^3.2.1", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.15", - "hasown": "^2.0.0", + "es-iterator-helpers": "^1.0.19", + "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7" + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.0" }, "engines": { "node": ">=4.0" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dependencies": { - "dequal": "^2.0.3" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "node_modules/eslint-plugin-react": { - "version": "7.34.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz", - "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==", + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", + "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlast": "^1.2.4", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", - "array.prototype.tosorted": "^1.1.3", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.17", + "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7", - "object.hasown": "^1.1.3", - "object.values": "^1.1.7", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.10" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "engines": { "node": ">=10" }, @@ -9758,11 +9926,6 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, - "node_modules/fast-loops": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz", - "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==" - }, "node_modules/fast-shallow-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", @@ -11022,9 +11185,9 @@ } }, "node_modules/hyphenate-style-name": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", - "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==" }, "node_modules/iconv-lite": { "version": "0.6.3", @@ -11179,12 +11342,11 @@ "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" }, "node_modules/inline-style-prefixer": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.0.tgz", - "integrity": "sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", "dependencies": { - "css-in-js-utils": "^3.1.0", - "fast-loops": "^1.1.3" + "css-in-js-utils": "^3.1.0" } }, "node_modules/internal-slot": { @@ -11217,7 +11379,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -11317,11 +11478,14 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14441,14 +14605,14 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-parse-even-better-errors": { @@ -15637,15 +15801,15 @@ } }, "node_modules/nano-css": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.1.tgz", - "integrity": "sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz", + "integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "css-tree": "^1.1.2", "csstype": "^3.1.2", "fastest-stable-stringify": "^2.0.2", - "inline-style-prefixer": "^7.0.0", + "inline-style-prefixer": "^7.0.1", "rtl-css-js": "^1.16.1", "stacktrace-js": "^2.0.2", "stylis": "^4.3.0" @@ -15656,9 +15820,9 @@ } }, "node_modules/nano-css/node_modules/stylis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", - "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", + "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==" }, "node_modules/nanoid": { "version": "3.3.6", @@ -15709,6 +15873,12 @@ "tslib": "^2.0.3" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "devOptional": true + }, "node_modules/node-fetch": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", @@ -15761,9 +15931,9 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" }, "node_modules/normalize-package-data": { "version": "3.0.3", @@ -15895,7 +16065,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -15933,26 +16102,27 @@ } }, "node_modules/object.entries": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", - "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -15980,40 +16150,26 @@ } }, "node_modules/object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" - } - }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dependencies": { + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -17643,9 +17799,9 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -18156,12 +18312,16 @@ } }, "node_modules/react-error-boundary": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", - "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.0.tgz", + "integrity": "sha512-GFnM3kyswd+9Oy7oX1lxdr39ANHD3ty6cyAK4Kyku+w8Aq9fnK7+yRytKOaPLzOhgtGq18AfTXmDtwlojBPTRg==", "dependencies": { "@babel/runtime": "^7.12.5" }, + "engines": { + "node": ">=20", + "pnpm": "=9" + }, "peerDependencies": { "react": ">=16.13.1" } @@ -18253,9 +18413,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-joyride": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.8.1.tgz", - "integrity": "sha512-fVwCmoOvJsiFKKHn8mvPUYc4JUUkgAsQMvarpZDtFPTc4duj240b12+AB8+3NXlTYGZVnKNSTgFFzoSh9RxjmQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.9.2.tgz", + "integrity": "sha512-DQ3m3W/GeoASv4UE9ZaadFp3ACJusV0kjjBe7zTpPwWuHpvEoofc+2TCJkru0lbA+G9l39+vPVttcJA/p1XeSA==", "dependencies": { "@gilbarbara/deep-equal": "^0.3.1", "deep-diff": "^1.0.2", @@ -18267,7 +18427,7 @@ "scroll": "^3.0.1", "scrollparent": "^2.1.0", "tree-changes": "^0.11.2", - "type-fest": "^4.15.0" + "type-fest": "^4.26.1" }, "peerDependencies": { "react": "15 - 18", @@ -18283,9 +18443,9 @@ } }, "node_modules/react-joyride/node_modules/type-fest": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz", - "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", "engines": { "node": ">=16" }, @@ -18412,11 +18572,11 @@ } }, "node_modules/react-router": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz", - "integrity": "sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", "dependencies": { - "@remix-run/router": "1.15.0" + "@remix-run/router": "1.20.0" }, "engines": { "node": ">=14.0.0" @@ -18426,12 +18586,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.0.tgz", - "integrity": "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", "dependencies": { - "@remix-run/router": "1.15.0", - "react-router": "6.22.0" + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" }, "engines": { "node": ">=14.0.0" @@ -20099,9 +20259,9 @@ } }, "node_modules/react-select": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", - "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz", + "integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==", "dependencies": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", @@ -20212,9 +20372,9 @@ } }, "node_modules/react-use": { - "version": "17.5.0", - "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.0.tgz", - "integrity": "sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==", + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.1.tgz", + "integrity": "sha512-LG/uPEVRflLWMwi3j/sZqR00nF6JGqTTDblkXK2nzXsIvij06hXl1V/MZIlwj1OKIQUtlh1l9jK8gLsRyCQxMg==", "dependencies": { "@types/js-cookie": "^2.2.6", "@xobotyi/scrollbar-width": "^1.9.5", @@ -20222,7 +20382,7 @@ "fast-deep-equal": "^3.1.3", "fast-shallow-equal": "^1.0.0", "js-cookie": "^2.2.1", - "nano-css": "^5.6.1", + "nano-css": "^5.6.2", "react-universal-interface": "^0.6.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.1.0", @@ -20250,16 +20410,16 @@ } }, "node_modules/reactflow": { - "version": "11.10.4", - "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.10.4.tgz", - "integrity": "sha512-0CApYhtYicXEDg/x2kvUHiUk26Qur8lAtTtiSlptNKuyEuGti6P1y5cS32YGaUoDMoCqkm/m+jcKkfMOvSCVRA==", + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", "dependencies": { - "@reactflow/background": "11.3.9", - "@reactflow/controls": "11.2.9", - "@reactflow/core": "11.10.4", - "@reactflow/minimap": "11.7.9", - "@reactflow/node-resizer": "2.2.9", - "@reactflow/node-toolbar": "1.3.9" + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" }, "peerDependencies": { "react": ">=17", @@ -20267,9 +20427,9 @@ } }, "node_modules/reactstrap": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.2.2.tgz", - "integrity": "sha512-4KroiGOdqZLAnMGzHjpErW3G7bLB+QbKzzMLIDXydPIV0y74lpdL7WtXHkLWAGInd97WCPNx4+R0NQDPyzIfhw==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.2.3.tgz", + "integrity": "sha512-1nXy7FIBIoOgXr3AIHOpgzcZXdj6rZE5YvNSPd1hYgwv8X64m6TAJsU0ExlieJdlRXhaRfTYRSZoTWa127b0gw==", "dependencies": { "@babel/runtime": "^7.12.5", "@popperjs/core": "^2.6.0", @@ -20393,14 +20553,14 @@ } }, "node_modules/recharts": { - "version": "2.12.6", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.6.tgz", - "integrity": "sha512-D+7j9WI+D0NHauah3fKHuNNcRK8bOypPW7os1DERinogGBGaHI7i6tQKJ0aUF3JXyBZ63dyfKIW2WTOPJDxJ8w==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0.tgz", + "integrity": "sha512-sbfxjWQ+oLWSZEWmvbq/DFVdeRLqqA6d0CDjKx2PkxVVdoXo16jvENCE+u/x7HxOO+/fwx//nYRwb8p8X6s/lQ==", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", - "react-is": "^16.10.2", + "react-is": "^18.3.1", "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", @@ -20422,6 +20582,11 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -20471,9 +20636,9 @@ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dependencies": { "regenerate": "^1.4.2" }, @@ -20517,14 +20682,14 @@ } }, "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", "dependencies": { - "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.11.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -20532,25 +20697,22 @@ "node": ">=4" } }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==" + }, "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz", + "integrity": "sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==", "dependencies": { - "jsesc": "~0.5.0" + "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "bin": { - "jsesc": "bin/jsesc" - } - }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -20927,12 +21089,13 @@ "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" }, "node_modules/sass": { - "version": "1.77.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.0.tgz", - "integrity": "sha512-eGj4HNfXqBWtSnvItNkn7B6icqH14i3CiCGbzMKs3BAPTq62pp9NBYsBgyN4cA+qssqo9r26lW4JSvlaUUWbgw==", + "version": "1.79.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", + "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "devOptional": true, "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, @@ -20980,6 +21143,34 @@ } } }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "devOptional": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "devOptional": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -21591,7 +21782,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, "dependencies": { "internal-slot": "^1.0.4" }, @@ -21642,6 +21832,19 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", @@ -21667,6 +21870,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -22834,9 +23046,9 @@ } }, "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "engines": { "node": ">=4" } @@ -22854,9 +23066,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "engines": { "node": ">=4" } @@ -24293,9 +24505,9 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==" }, "@adobe/css-tools": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", - "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", "dev": true }, "@alloc/quick-lru": { @@ -24313,18 +24525,18 @@ } }, "@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", "requires": { - "@babel/highlight": "^7.24.7", + "@babel/highlight": "^7.25.7", "picocolors": "^1.0.0" } }, "@babel/compat-data": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.8.tgz", - "integrity": "sha512-c4IM7OTg6k1Q+AJ153e2mc2QVTezTwnb4VzquwcyiEzGnW0Kedv4do/TrkU98qPeC5LNiMt/QXwIjzYXLBpyZg==" + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==" }, "@babel/core": { "version": "7.22.9", @@ -24366,68 +24578,66 @@ } }, "@babel/generator": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.8.tgz", - "integrity": "sha512-47DG+6F5SzOi0uEvK4wMShmn5yY0mVjVJoWTphdY2B4Rx9wHgjK7Yhtr0ru6nE+sn0v38mzrWOlah0p/YlHHOQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", "requires": { - "@babel/types": "^7.24.8", + "@babel/types": "^7.25.7", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" } }, "@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", - "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", "requires": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.7" } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", - "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz", + "integrity": "sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg==", "requires": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" } }, "@babel/helper-compilation-targets": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", - "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", "requires": { - "@babel/compat-data": "^7.24.8", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "@babel/helper-create-class-features-plugin": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.8.tgz", - "integrity": "sha512-4f6Oqnmyp2PP3olgUMmOwC3akxSm5aBYraQ6YDdKy7NcAMkDECHWG0DEnV6M2UAkERgIBhYt8S27rURPg7SxWA==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.8", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", + "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/traverse": "^7.25.7", "semver": "^6.3.1" } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", - "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz", + "integrity": "sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ==", "requires": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "regexpu-core": "^5.3.1", + "@babel/helper-annotate-as-pure": "^7.25.7", + "regexpu-core": "^6.1.1", "semver": "^6.3.1" } }, @@ -24443,110 +24653,84 @@ "resolve": "^1.14.2" } }, - "@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "requires": { - "@babel/types": "^7.24.7" - } - }, - "@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "requires": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "requires": { - "@babel/types": "^7.24.7" - } - }, "@babel/helper-member-expression-to-functions": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", - "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", + "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", "requires": { - "@babel/traverse": "^7.24.8", - "@babel/types": "^7.24.8" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" } }, "@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", "requires": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" } }, "@babel/helper-module-transforms": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.8.tgz", - "integrity": "sha512-m4vWKVqvkVAWLXfHCCfff2luJj86U+J0/x+0N3ArG/tP0Fq7zky2dYwMbtPmkc/oulkkbjdL3uWzuoBwQ8R00Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", "requires": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" } }, "@babel/helper-optimise-call-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", - "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", + "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", "requires": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.7" } }, "@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==" + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==" }, "@babel/helper-remap-async-to-generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", - "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz", + "integrity": "sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw==", "requires": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-wrap-function": "^7.24.7" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-wrap-function": "^7.25.7", + "@babel/traverse": "^7.25.7" } }, "@babel/helper-replace-supers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", - "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", + "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", "requires": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7" + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/traverse": "^7.25.7" } }, "@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", "requires": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" } }, "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", - "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", + "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", "requires": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" } }, "@babel/helper-split-export-declaration": { @@ -24558,29 +24742,28 @@ } }, "@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==" + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==" }, "@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==" + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==" }, "@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==" + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==" }, "@babel/helper-wrap-function": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", - "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz", + "integrity": "sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg==", "requires": { - "@babel/helper-function-name": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" } }, "@babel/helpers": { @@ -24594,55 +24777,66 @@ } }, "@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", "requires": { - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "@babel/parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", - "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==" + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "requires": { + "@babel/types": "^7.25.8" + } }, "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", - "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.7.tgz", + "integrity": "sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ==", "requires": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" + } + }, + "@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.7.tgz", + "integrity": "sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", - "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz", + "integrity": "sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", - "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz", + "integrity": "sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/plugin-transform-optional-chaining": "^7.25.7" } }, "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", - "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.7.tgz", + "integrity": "sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw==", "requires": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" } }, "@babel/plugin-proposal-class-properties": { @@ -24733,14 +24927,6 @@ "@babel/helper-plugin-utils": "^7.12.13" } }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, "@babel/plugin-syntax-decorators": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.22.5.tgz", @@ -24749,22 +24935,6 @@ "@babel/helper-plugin-utils": "^7.22.5" } }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, "@babel/plugin-syntax-flow": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz", @@ -24774,19 +24944,19 @@ } }, "@babel/plugin-syntax-import-assertions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", - "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz", + "integrity": "sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-syntax-import-meta": { @@ -24806,11 +24976,11 @@ } }, "@babel/plugin-syntax-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-syntax-logical-assignment-operators": { @@ -24861,14 +25031,6 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, "@babel/plugin-syntax-top-level-await": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", @@ -24895,143 +25057,146 @@ } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", - "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz", + "integrity": "sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-async-generator-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", - "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.8.tgz", + "integrity": "sha512-9ypqkozyzpG+HxlH4o4gdctalFGIjjdufzo7I2XPda0iBnZ6a+FO0rIEQcdSPXp02CkvGsII1exJhmROPQd5oA==", "requires": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-remap-async-to-generator": "^7.25.7", + "@babel/traverse": "^7.25.7" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", - "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.7.tgz", + "integrity": "sha512-ZUCjAavsh5CESCmi/xCpX1qcCaAglzs/7tmuvoFnJgA1dM7gQplsguljoTg+Ru8WENpX89cQyAtWoaE0I3X3Pg==", "requires": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7" + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-remap-async-to-generator": "^7.25.7" } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", - "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz", + "integrity": "sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", - "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz", + "integrity": "sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-class-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", - "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz", + "integrity": "sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-class-static-block": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", - "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz", + "integrity": "sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-classes": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.8.tgz", - "integrity": "sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-replace-supers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz", + "integrity": "sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/traverse": "^7.25.7", "globals": "^11.1.0" } }, "@babel/plugin-transform-computed-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", - "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz", + "integrity": "sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/template": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/template": "^7.25.7" } }, "@babel/plugin-transform-destructuring": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", - "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz", + "integrity": "sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA==", "requires": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", - "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz", + "integrity": "sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", - "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz", + "integrity": "sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" + } + }, + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA==", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-dynamic-import": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", - "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz", + "integrity": "sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", - "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz", + "integrity": "sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg==", "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-export-namespace-from": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", - "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz", + "integrity": "sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-flow-strip-types": { @@ -25044,205 +25209,197 @@ } }, "@babel/plugin-transform-for-of": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", - "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz", + "integrity": "sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" } }, "@babel/plugin-transform-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", - "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz", + "integrity": "sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ==", "requires": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" } }, "@babel/plugin-transform-json-strings": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", - "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz", + "integrity": "sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", - "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz", + "integrity": "sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", - "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz", + "integrity": "sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", - "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz", + "integrity": "sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", - "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz", + "integrity": "sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA==", "requires": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", - "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz", + "integrity": "sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg==", "requires": { - "@babel/helper-module-transforms": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-simple-access": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", - "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz", + "integrity": "sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g==", "requires": { - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" } }, "@babel/plugin-transform-modules-umd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", - "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz", + "integrity": "sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw==", "requires": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", - "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-new-target": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", - "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz", + "integrity": "sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", - "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz", + "integrity": "sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-numeric-separator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", - "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz", + "integrity": "sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-object-rest-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", - "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz", + "integrity": "sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==", "requires": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.7" + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-transform-parameters": "^7.25.7" } }, "@babel/plugin-transform-object-super": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", - "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz", + "integrity": "sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7" } }, "@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", - "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz", + "integrity": "sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-optional-chaining": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", - "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz", + "integrity": "sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==", "requires": { - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" } }, "@babel/plugin-transform-parameters": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", - "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz", + "integrity": "sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-private-methods": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", - "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz", + "integrity": "sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-private-property-in-object": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", - "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz", + "integrity": "sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==", "requires": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-property-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", - "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz", + "integrity": "sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-react-constant-elements": { @@ -25254,57 +25411,57 @@ } }, "@babel/plugin-transform-react-display-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", - "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.7.tgz", + "integrity": "sha512-r0QY7NVU8OnrwE+w2IWiRom0wwsTbjx4+xH2RTd7AVdof3uurXOF+/mXHQDRk+2jIvWgSaCHKMgggfvM4dyUGA==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-react-jsx": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz", - "integrity": "sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.7.tgz", + "integrity": "sha512-vILAg5nwGlR9EXE8JIOX4NHXd49lrYbN8hnjffDtoULwpL9hUx/N55nqh2qd0q6FyNDfjl9V79ecKGvFbcSA0Q==", "requires": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/plugin-syntax-jsx": "^7.24.7", - "@babel/types": "^7.25.2" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-syntax-jsx": "^7.25.7", + "@babel/types": "^7.25.7" } }, "@babel/plugin-transform-react-jsx-development": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", - "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.7.tgz", + "integrity": "sha512-5yd3lH1PWxzW6IZj+p+Y4OLQzz0/LzlOG8vGqonHfVR3euf1vyzyMUJk9Ac+m97BH46mFc/98t9PmYLyvgL3qg==", "requires": { - "@babel/plugin-transform-react-jsx": "^7.24.7" + "@babel/plugin-transform-react-jsx": "^7.25.7" } }, "@babel/plugin-transform-react-pure-annotations": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", - "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.7.tgz", + "integrity": "sha512-6YTHJ7yjjgYqGc8S+CbEXhLICODk0Tn92j+vNJo07HFk9t3bjFgAKxPLFhHwF2NjmQVSI1zBRfBWUeVBa2osfA==", "requires": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-regenerator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", - "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz", + "integrity": "sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.25.7", "regenerator-transform": "^0.15.2" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", - "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz", + "integrity": "sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-runtime": { @@ -25321,44 +25478,44 @@ } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", - "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz", + "integrity": "sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", - "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz", + "integrity": "sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", - "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz", + "integrity": "sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-template-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", - "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz", + "integrity": "sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", - "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz", + "integrity": "sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA==", "requires": { - "@babel/helper-plugin-utils": "^7.24.8" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-typescript": { @@ -25373,125 +25530,112 @@ } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", - "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz", + "integrity": "sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", - "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz", + "integrity": "sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", - "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz", + "integrity": "sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", - "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz", + "integrity": "sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" } }, "@babel/preset-env": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.8.tgz", - "integrity": "sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ==", - "requires": { - "@babel/compat-data": "^7.24.8", - "@babel/helper-compilation-targets": "^7.24.8", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-validator-option": "^7.24.8", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.8.tgz", + "integrity": "sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg==", + "requires": { + "@babel/compat-data": "^7.25.8", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.24.7", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-import-assertions": "^7.25.7", + "@babel/plugin-syntax-import-attributes": "^7.25.7", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.24.7", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoped-functions": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.24.7", - "@babel/plugin-transform-class-properties": "^7.24.7", - "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.24.8", - "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-dotall-regex": "^7.24.7", - "@babel/plugin-transform-duplicate-keys": "^7.24.7", - "@babel/plugin-transform-dynamic-import": "^7.24.7", - "@babel/plugin-transform-exponentiation-operator": "^7.24.7", - "@babel/plugin-transform-export-namespace-from": "^7.24.7", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-function-name": "^7.24.7", - "@babel/plugin-transform-json-strings": "^7.24.7", - "@babel/plugin-transform-literals": "^7.24.7", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-member-expression-literals": "^7.24.7", - "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-modules-systemjs": "^7.24.7", - "@babel/plugin-transform-modules-umd": "^7.24.7", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-new-target": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-numeric-separator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-object-super": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-property-literals": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-reserved-words": "^7.24.7", - "@babel/plugin-transform-shorthand-properties": "^7.24.7", - "@babel/plugin-transform-spread": "^7.24.7", - "@babel/plugin-transform-sticky-regex": "^7.24.7", - "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.8", - "@babel/plugin-transform-unicode-escapes": "^7.24.7", - "@babel/plugin-transform-unicode-property-regex": "^7.24.7", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/plugin-transform-arrow-functions": "^7.25.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.8", + "@babel/plugin-transform-async-to-generator": "^7.25.7", + "@babel/plugin-transform-block-scoped-functions": "^7.25.7", + "@babel/plugin-transform-block-scoping": "^7.25.7", + "@babel/plugin-transform-class-properties": "^7.25.7", + "@babel/plugin-transform-class-static-block": "^7.25.8", + "@babel/plugin-transform-classes": "^7.25.7", + "@babel/plugin-transform-computed-properties": "^7.25.7", + "@babel/plugin-transform-destructuring": "^7.25.7", + "@babel/plugin-transform-dotall-regex": "^7.25.7", + "@babel/plugin-transform-duplicate-keys": "^7.25.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.7", + "@babel/plugin-transform-dynamic-import": "^7.25.8", + "@babel/plugin-transform-exponentiation-operator": "^7.25.7", + "@babel/plugin-transform-export-namespace-from": "^7.25.8", + "@babel/plugin-transform-for-of": "^7.25.7", + "@babel/plugin-transform-function-name": "^7.25.7", + "@babel/plugin-transform-json-strings": "^7.25.8", + "@babel/plugin-transform-literals": "^7.25.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.8", + "@babel/plugin-transform-member-expression-literals": "^7.25.7", + "@babel/plugin-transform-modules-amd": "^7.25.7", + "@babel/plugin-transform-modules-commonjs": "^7.25.7", + "@babel/plugin-transform-modules-systemjs": "^7.25.7", + "@babel/plugin-transform-modules-umd": "^7.25.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.7", + "@babel/plugin-transform-new-target": "^7.25.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.8", + "@babel/plugin-transform-numeric-separator": "^7.25.8", + "@babel/plugin-transform-object-rest-spread": "^7.25.8", + "@babel/plugin-transform-object-super": "^7.25.7", + "@babel/plugin-transform-optional-catch-binding": "^7.25.8", + "@babel/plugin-transform-optional-chaining": "^7.25.8", + "@babel/plugin-transform-parameters": "^7.25.7", + "@babel/plugin-transform-private-methods": "^7.25.7", + "@babel/plugin-transform-private-property-in-object": "^7.25.8", + "@babel/plugin-transform-property-literals": "^7.25.7", + "@babel/plugin-transform-regenerator": "^7.25.7", + "@babel/plugin-transform-reserved-words": "^7.25.7", + "@babel/plugin-transform-shorthand-properties": "^7.25.7", + "@babel/plugin-transform-spread": "^7.25.7", + "@babel/plugin-transform-sticky-regex": "^7.25.7", + "@babel/plugin-transform-template-literals": "^7.25.7", + "@babel/plugin-transform-typeof-symbol": "^7.25.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.7", + "@babel/plugin-transform-unicode-property-regex": "^7.25.7", + "@babel/plugin-transform-unicode-regex": "^7.25.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.7", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-corejs3": "^0.10.6", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.37.1", + "core-js-compat": "^3.38.1", "semver": "^6.3.1" }, "dependencies": { @@ -25508,12 +25652,12 @@ } }, "babel-plugin-polyfill-corejs3": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", - "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "requires": { - "@babel/helper-define-polyfill-provider": "^0.6.1", - "core-js-compat": "^3.36.1" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" } }, "babel-plugin-polyfill-regenerator": { @@ -25537,16 +25681,16 @@ } }, "@babel/preset-react": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", - "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.25.7.tgz", + "integrity": "sha512-GjV0/mUEEXpi1U5ZgDprMRRgajGMRW3G5FjMr5KLKD8nT2fTG8+h/klV3+6Dm5739QE+K5+2e91qFKAYI3pmRg==", "requires": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.24.7", - "@babel/plugin-transform-react-jsx-development": "^7.24.7", - "@babel/plugin-transform-react-pure-annotations": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "@babel/plugin-transform-react-display-name": "^7.25.7", + "@babel/plugin-transform-react-jsx": "^7.25.7", + "@babel/plugin-transform-react-jsx-development": "^7.25.7", + "@babel/plugin-transform-react-pure-annotations": "^7.25.7" } }, "@babel/preset-typescript": { @@ -25561,11 +25705,6 @@ "@babel/plugin-transform-typescript": "^7.22.5" } }, - "@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, "@babel/runtime": { "version": "7.23.8", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", @@ -25582,39 +25721,36 @@ } }, "@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", "requires": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" } }, "@babel/traverse": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", - "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", - "requires": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.8", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.8", - "@babel/types": "^7.24.8", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "requires": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "requires": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", "to-fast-properties": "^2.0.0" } }, @@ -25773,17 +25909,17 @@ "requires": {} }, "@dagrejs/dagre": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.0.4.tgz", - "integrity": "sha512-jrEore+HhW1yg1Rsd9H1PPMcoEOD4bVh0WCXc6GqzyzubnJj4GaWGg8ETOrskTd/3n/g5LOzumGM4CCgpNLJNw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz", + "integrity": "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==", "requires": { - "@dagrejs/graphlib": "2.1.13" + "@dagrejs/graphlib": "2.2.4" } }, "@dagrejs/graphlib": { - "version": "2.1.13", - "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.1.13.tgz", - "integrity": "sha512-calbMa7Gcyo+/t23XBaqQqon8LlgE9regey4UVoikoenKBXvUnCUL3s9RP6USCxttfr0XWVICtYUuKMdehKqMw==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz", + "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==" }, "@emotion/babel-plugin": { "version": "11.11.0", @@ -25911,9 +26047,9 @@ "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==" }, "@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -25932,9 +26068,9 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "requires": { "type-fest": "^0.20.2" } @@ -25955,9 +26091,9 @@ } }, "@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==" + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==" }, "@floating-ui/core": { "version": "1.3.1", @@ -25978,12 +26114,12 @@ "integrity": "sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==" }, "@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", "minimatch": "^3.0.5" } }, @@ -25993,9 +26129,9 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==" }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -26652,6 +26788,114 @@ "fastq": "^1.6.0" } }, + "@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "devOptional": true, + "requires": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1", + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + } + }, + "@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "dev": true, + "optional": true + }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", @@ -26706,29 +26950,29 @@ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@reactflow/background": { - "version": "11.3.9", - "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.9.tgz", - "integrity": "sha512-byj/G9pEC8tN0wT/ptcl/LkEP/BBfa33/SvBkqE4XwyofckqF87lKp573qGlisfnsijwAbpDlf81PuFL41So4Q==", + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", "requires": { - "@reactflow/core": "11.10.4", + "@reactflow/core": "11.11.4", "classcat": "^5.0.3", "zustand": "^4.4.1" } }, "@reactflow/controls": { - "version": "11.2.9", - "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.9.tgz", - "integrity": "sha512-e8nWplbYfOn83KN1BrxTXS17+enLyFnjZPbyDgHSRLtI5ZGPKF/8iRXV+VXb2LFVzlu4Wh3la/pkxtfP/0aguA==", + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", "requires": { - "@reactflow/core": "11.10.4", + "@reactflow/core": "11.11.4", "classcat": "^5.0.3", "zustand": "^4.4.1" } }, "@reactflow/core": { - "version": "11.10.4", - "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.10.4.tgz", - "integrity": "sha512-j3i9b2fsTX/sBbOm+RmNzYEFWbNx4jGWGuGooh2r1jQaE2eV+TLJgiG/VNOp0q5mBl9f6g1IXs3Gm86S9JfcGw==", + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", "requires": { "@types/d3": "^7.4.0", "@types/d3-drag": "^3.0.1", @@ -26742,11 +26986,11 @@ } }, "@reactflow/minimap": { - "version": "11.7.9", - "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.9.tgz", - "integrity": "sha512-le95jyTtt3TEtJ1qa7tZ5hyM4S7gaEQkW43cixcMOZLu33VAdc2aCpJg/fXcRrrf7moN2Mbl9WIMNXUKsp5ILA==", + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", "requires": { - "@reactflow/core": "11.10.4", + "@reactflow/core": "11.11.4", "@types/d3-selection": "^3.0.3", "@types/d3-zoom": "^3.0.1", "classcat": "^5.0.3", @@ -26756,11 +27000,11 @@ } }, "@reactflow/node-resizer": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.9.tgz", - "integrity": "sha512-HfickMm0hPDIHt9qH997nLdgLt0kayQyslKE0RS/GZvZ4UMQJlx/NRRyj5y47Qyg0NnC66KYOQWDM9LLzRTnUg==", + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", "requires": { - "@reactflow/core": "11.10.4", + "@reactflow/core": "11.11.4", "classcat": "^5.0.4", "d3-drag": "^3.0.0", "d3-selection": "^3.0.0", @@ -26768,19 +27012,19 @@ } }, "@reactflow/node-toolbar": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.9.tgz", - "integrity": "sha512-VmgxKmToax4sX1biZ9LXA7cj/TBJ+E5cklLGwquCCVVxh+lxpZGTBF3a5FJGVHiUNBBtFsC8ldcSZIK4cAlQww==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", "requires": { - "@reactflow/core": "11.10.4", + "@reactflow/core": "11.11.4", "classcat": "^5.0.3", "zustand": "^4.4.1" } }, "@remix-run/router": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz", - "integrity": "sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==" + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==" }, "@rollup/plugin-babel": { "version": "5.3.1", @@ -26837,6 +27081,11 @@ } } }, + "@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==" + }, "@rushstack/eslint-patch": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz", @@ -27075,18 +27324,17 @@ } }, "@testing-library/jest-dom": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz", - "integrity": "sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", "dev": true, "requires": { - "@adobe/css-tools": "^4.3.2", - "@babel/runtime": "^7.9.2", + "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.15", + "lodash": "^4.17.21", "redent": "^3.0.0" }, "dependencies": { @@ -27434,9 +27682,9 @@ } }, "@types/d3-force": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.9.tgz", - "integrity": "sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==" + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==" }, "@types/d3-format": { "version": "3.0.4", @@ -27452,9 +27700,9 @@ } }, "@types/d3-hierarchy": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.6.tgz", - "integrity": "sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw==" + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" }, "@types/d3-interpolate": { "version": "3.0.1", @@ -27498,9 +27746,9 @@ "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" }, "@types/d3-selection": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", - "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==" + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==" }, "@types/d3-shape": { "version": "3.1.1", @@ -27526,9 +27774,9 @@ "integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==" }, "@types/d3-transition": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", - "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "requires": { "@types/d3-selection": "*" } @@ -28090,6 +28338,11 @@ } } }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -28405,7 +28658,6 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, "requires": { "deep-equal": "^2.0.5" } @@ -28425,14 +28677,15 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" }, "array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, @@ -28455,15 +28708,16 @@ } }, "array.prototype.findlastindex": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", - "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" } }, "array.prototype.flat": { @@ -28500,26 +28754,15 @@ "is-string": "^1.0.7" } }, - "array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, "array.prototype.tosorted": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", - "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "requires": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, @@ -28597,14 +28840,14 @@ } }, "axe-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", - "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==" + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz", + "integrity": "sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==" }, "axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -28645,12 +28888,9 @@ } }, "axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", - "requires": { - "dequal": "^2.0.3" - } + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" }, "babel-eslint": { "version": "10.1.0", @@ -29039,13 +29279,13 @@ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" }, "browserslist": { - "version": "4.23.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", - "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "requires": { - "caniuse-lite": "^1.0.30001640", - "electron-to-chromium": "^1.4.820", - "node-releases": "^2.0.14", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" } }, @@ -29131,9 +29371,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001642", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz", - "integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==" + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -29211,9 +29451,9 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, "classcat": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz", - "integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==" + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==" }, "classnames": { "version": "2.5.1", @@ -29452,11 +29692,11 @@ "integrity": "sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ==" }, "core-js-compat": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", - "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", "requires": { - "browserslist": "^4.23.0" + "browserslist": "^4.23.3" } }, "core-js-pure": { @@ -30099,7 +30339,6 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", - "dev": true, "requires": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", @@ -30184,6 +30423,12 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "devOptional": true + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -30378,9 +30623,9 @@ } }, "electron-to-chromium": { - "version": "1.4.827", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.827.tgz", - "integrity": "sha512-VY+J0e4SFcNfQy19MEoMdaIcZLmDCprqvBtkii1WTCTQHpRvf5N8+3kTYCgL/PcntvwQvmMJWTuDPsq+IlhWKQ==" + "version": "1.5.39", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.39.tgz", + "integrity": "sha512-4xkpSR6CjuiaNyvwiWDI85N9AxsvbPawB8xc7yzLPonYTuP19BVgYweKyUMFtHEZgIcHWMt1ks5Cqx2m+6/Grg==" }, "emittery": { "version": "0.13.1", @@ -30509,7 +30754,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -30611,17 +30855,18 @@ } }, "eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -30832,9 +31077,9 @@ } }, "eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "requires": { "debug": "^3.2.7" }, @@ -30859,26 +31104,28 @@ } }, "eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "requires": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "dependencies": { @@ -30909,61 +31156,51 @@ } }, "eslint-plugin-jsx-a11y": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", - "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", + "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", "requires": { - "@babel/runtime": "^7.23.2", - "aria-query": "^5.3.0", - "array-includes": "^3.1.7", + "aria-query": "~5.1.3", + "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", - "axe-core": "=4.7.0", - "axobject-query": "^3.2.1", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.15", - "hasown": "^2.0.0", + "es-iterator-helpers": "^1.0.19", + "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7" - }, - "dependencies": { - "aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "requires": { - "dequal": "^2.0.3" - } - } + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.0" } }, "eslint-plugin-react": { - "version": "7.34.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz", - "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==", + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", + "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", "requires": { - "array-includes": "^3.1.7", - "array.prototype.findlast": "^1.2.4", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", - "array.prototype.tosorted": "^1.1.3", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.17", + "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7", - "object.hasown": "^1.1.3", - "object.values": "^1.1.7", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.10" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "dependencies": { "doctrine": { @@ -30987,9 +31224,9 @@ } }, "eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "requires": {} }, "eslint-plugin-testing-library": { @@ -31294,11 +31531,6 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, - "fast-loops": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz", - "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==" - }, "fast-shallow-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", @@ -32201,9 +32433,9 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" }, "hyphenate-style-name": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", - "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==" }, "iconv-lite": { "version": "0.6.3", @@ -32315,12 +32547,11 @@ "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" }, "inline-style-prefixer": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.0.tgz", - "integrity": "sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", "requires": { - "css-in-js-utils": "^3.1.0", - "fast-loops": "^1.1.3" + "css-in-js-utils": "^3.1.0" } }, "internal-slot": { @@ -32347,7 +32578,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -32411,11 +32641,11 @@ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" }, "is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "requires": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" } }, "is-data-view": { @@ -34725,9 +34955,9 @@ } }, "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==" }, "json-parse-even-better-errors": { "version": "2.3.1", @@ -35535,24 +35765,24 @@ } }, "nano-css": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.1.tgz", - "integrity": "sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz", + "integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==", "requires": { "@jridgewell/sourcemap-codec": "^1.4.15", "css-tree": "^1.1.2", "csstype": "^3.1.2", "fastest-stable-stringify": "^2.0.2", - "inline-style-prefixer": "^7.0.0", + "inline-style-prefixer": "^7.0.1", "rtl-css-js": "^1.16.1", "stacktrace-js": "^2.0.2", "stylis": "^4.3.0" }, "dependencies": { "stylis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", - "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", + "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==" } } }, @@ -35590,6 +35820,12 @@ "tslib": "^2.0.3" } }, + "node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "devOptional": true + }, "node-fetch": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", @@ -35630,9 +35866,9 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" }, "normalize-package-data": { "version": "3.0.3", @@ -35727,7 +35963,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -35750,23 +35985,24 @@ } }, "object.entries": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", - "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, "object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" } }, "object.getownpropertydescriptors": { @@ -35782,34 +36018,23 @@ } }, "object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" - } - }, - "object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "requires": { + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.2" } }, "object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, "obuf": { @@ -36778,9 +37003,9 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" }, "prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true }, "pretty-bytes": { @@ -37149,9 +37374,9 @@ } }, "react-error-boundary": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", - "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.0.tgz", + "integrity": "sha512-GFnM3kyswd+9Oy7oX1lxdr39ANHD3ty6cyAK4Kyku+w8Aq9fnK7+yRytKOaPLzOhgtGq18AfTXmDtwlojBPTRg==", "requires": { "@babel/runtime": "^7.12.5" } @@ -37230,9 +37455,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-joyride": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.8.1.tgz", - "integrity": "sha512-fVwCmoOvJsiFKKHn8mvPUYc4JUUkgAsQMvarpZDtFPTc4duj240b12+AB8+3NXlTYGZVnKNSTgFFzoSh9RxjmQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.9.2.tgz", + "integrity": "sha512-DQ3m3W/GeoASv4UE9ZaadFp3ACJusV0kjjBe7zTpPwWuHpvEoofc+2TCJkru0lbA+G9l39+vPVttcJA/p1XeSA==", "requires": { "@gilbarbara/deep-equal": "^0.3.1", "deep-diff": "^1.0.2", @@ -37244,7 +37469,7 @@ "scroll": "^3.0.1", "scrollparent": "^2.1.0", "tree-changes": "^0.11.2", - "type-fest": "^4.15.0" + "type-fest": "^4.26.1" }, "dependencies": { "deepmerge": { @@ -37253,9 +37478,9 @@ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, "type-fest": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz", - "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==" + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==" } } }, @@ -37356,20 +37581,20 @@ "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" }, "react-router": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz", - "integrity": "sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", "requires": { - "@remix-run/router": "1.15.0" + "@remix-run/router": "1.20.0" } }, "react-router-dom": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.0.tgz", - "integrity": "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", "requires": { - "@remix-run/router": "1.15.0", - "react-router": "6.22.0" + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" } }, "react-scripts": { @@ -38635,9 +38860,9 @@ } }, "react-select": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", - "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz", + "integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==", "requires": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", @@ -38709,9 +38934,9 @@ "requires": {} }, "react-use": { - "version": "17.5.0", - "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.0.tgz", - "integrity": "sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==", + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.1.tgz", + "integrity": "sha512-LG/uPEVRflLWMwi3j/sZqR00nF6JGqTTDblkXK2nzXsIvij06hXl1V/MZIlwj1OKIQUtlh1l9jK8gLsRyCQxMg==", "requires": { "@types/js-cookie": "^2.2.6", "@xobotyi/scrollbar-width": "^1.9.5", @@ -38719,7 +38944,7 @@ "fast-deep-equal": "^3.1.3", "fast-shallow-equal": "^1.0.0", "js-cookie": "^2.2.1", - "nano-css": "^5.6.1", + "nano-css": "^5.6.2", "react-universal-interface": "^0.6.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.1.0", @@ -38742,22 +38967,22 @@ } }, "reactflow": { - "version": "11.10.4", - "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.10.4.tgz", - "integrity": "sha512-0CApYhtYicXEDg/x2kvUHiUk26Qur8lAtTtiSlptNKuyEuGti6P1y5cS32YGaUoDMoCqkm/m+jcKkfMOvSCVRA==", + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", "requires": { - "@reactflow/background": "11.3.9", - "@reactflow/controls": "11.2.9", - "@reactflow/core": "11.10.4", - "@reactflow/minimap": "11.7.9", - "@reactflow/node-resizer": "2.2.9", - "@reactflow/node-toolbar": "1.3.9" + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" } }, "reactstrap": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.2.2.tgz", - "integrity": "sha512-4KroiGOdqZLAnMGzHjpErW3G7bLB+QbKzzMLIDXydPIV0y74lpdL7WtXHkLWAGInd97WCPNx4+R0NQDPyzIfhw==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.2.3.tgz", + "integrity": "sha512-1nXy7FIBIoOgXr3AIHOpgzcZXdj6rZE5YvNSPd1hYgwv8X64m6TAJsU0ExlieJdlRXhaRfTYRSZoTWa127b0gw==", "requires": { "@babel/runtime": "^7.12.5", "@popperjs/core": "^2.6.0", @@ -38857,18 +39082,25 @@ } }, "recharts": { - "version": "2.12.6", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.6.tgz", - "integrity": "sha512-D+7j9WI+D0NHauah3fKHuNNcRK8bOypPW7os1DERinogGBGaHI7i6tQKJ0aUF3JXyBZ63dyfKIW2WTOPJDxJ8w==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0.tgz", + "integrity": "sha512-sbfxjWQ+oLWSZEWmvbq/DFVdeRLqqA6d0CDjKx2PkxVVdoXo16jvENCE+u/x7HxOO+/fwx//nYRwb8p8X6s/lQ==", "requires": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", - "react-is": "^16.10.2", + "react-is": "^18.3.1", "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" + }, + "dependencies": { + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + } } }, "recharts-scale": { @@ -38916,9 +39148,9 @@ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" }, "regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "requires": { "regenerate": "^1.4.2" } @@ -38953,31 +39185,29 @@ } }, "regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", "requires": { - "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.11.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" } }, + "regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==" + }, "regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz", + "integrity": "sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==", "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" - } + "jsesc": "~3.0.2" } }, "relateurl": { @@ -39231,14 +39461,32 @@ "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" }, "sass": { - "version": "1.77.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.0.tgz", - "integrity": "sha512-eGj4HNfXqBWtSnvItNkn7B6icqH14i3CiCGbzMKs3BAPTq62pp9NBYsBgyN4cA+qssqo9r26lW4JSvlaUUWbgw==", + "version": "1.79.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", + "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", "devOptional": true, "requires": { - "chokidar": ">=3.0.0 <4.0.0", + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" + }, + "dependencies": { + "chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "devOptional": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "devOptional": true + } } }, "sass-loader": { @@ -39756,7 +40004,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, "requires": { "internal-slot": "^1.0.4" } @@ -39800,6 +40047,16 @@ } } }, + "string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + } + }, "string.prototype.matchall": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", @@ -39819,6 +40076,15 @@ "side-channel": "^1.0.6" } }, + "string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -40684,9 +40950,9 @@ } }, "unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==" }, "unicode-match-property-ecmascript": { "version": "2.0.0", @@ -40698,9 +40964,9 @@ } }, "unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==" }, "unicode-property-aliases-ecmascript": { "version": "2.1.0", diff --git a/frontend/package.json b/frontend/package.json index 99532d97..9b138d51 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,12 +1,12 @@ { "name": "threatmatrix", - "version": "6.0.0", + "version": "6.1.0", "private": true, "proxy": "http://localhost:80/", "dependencies": { "@certego/certego-ui": "^0.1.13", - "@dagrejs/dagre": "^1.0.4", - "axios": "^1.7.4", + "@dagrejs/dagre": "^1.1.4", + "axios": "^1.7.7", "axios-hooks": "^3.1.5", "bootstrap": "^5.3.3", "classnames": "^2.5.1", @@ -17,19 +17,19 @@ "prop-types": "^15.8.1", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-error-boundary": "^4.0.13", + "react-error-boundary": "^4.1.0", "react-icons": "^4.12.0", - "react-joyride": "^2.8.1", + "react-joyride": "^2.9.2", "react-json-tree": "^0.19.0", "react-markdown": "^8.0.7", - "react-router-dom": "^6.22.0", + "react-router-dom": "^6.27.0", "react-scripts": "^5.0.1", - "react-select": "^5.8.0", + "react-select": "^5.8.1", "react-table": "^7.8.0", - "react-use": "^17.5.0", - "reactflow": "^11.10.4", - "reactstrap": "^9.2.1", - "recharts": "^2.12.6", + "react-use": "^17.5.1", + "reactflow": "^11.11.4", + "reactstrap": "^9.2.3", + "recharts": "^2.13.0", "zustand": "^4.5.4" }, "scripts": { @@ -59,24 +59,24 @@ ] }, "devDependencies": { - "@babel/preset-env": "^7.24.7", - "@babel/preset-react": "^7.24.7", - "@testing-library/jest-dom": "^6.4.2", + "@babel/preset-env": "^7.25.8", + "@babel/preset-react": "^7.25.7", + "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^14.5.2", "babel-eslint": "^10.1.0", "babel-jest": "^29.7.0", - "eslint": "^8.48.0", + "eslint": "^8.57.1", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsx-a11y": "^6.8.0", - "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^4.5.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.1", + "eslint-plugin-react-hooks": "^4.6.2", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "prettier": "^3.2.5", - "sass": "^1.77.0", + "prettier": "^3.3.3", + "sass": "^1.79.5", "stylelint": "^14.9.1", "stylelint-config-prettier": "^9.0.3", "stylelint-config-standard-scss": "^4.0.0" diff --git a/frontend/src/components/GuideWrapper.jsx b/frontend/src/components/GuideWrapper.jsx index a4274408..7c7d90aa 100644 --- a/frontend/src/components/GuideWrapper.jsx +++ b/frontend/src/components/GuideWrapper.jsx @@ -17,8 +17,8 @@ export default function GuideWrapper() {

Welcome to ThreatMatrixs Guide for First Time Visitors! For further questions you could either check out our{" "} - docs or reach us - out on{" "} + docs or reach + us out on{" "} the official ThreatMatrix slack channel diff --git a/frontend/src/components/common/form/ScanConfigSelectInput.jsx b/frontend/src/components/common/form/ScanConfigSelectInput.jsx new file mode 100644 index 00000000..8d4735af --- /dev/null +++ b/frontend/src/components/common/form/ScanConfigSelectInput.jsx @@ -0,0 +1,89 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { FormGroup, Input, Label, UncontrolledTooltip } from "reactstrap"; +import { MdInfoOutline } from "react-icons/md"; + +import { ScanModesNumeric } from "../../../constants/advancedSettingsConst"; + +export function ScanConfigSelectInput(props) { + const { formik } = props; + console.debug("ScanConfigSelectInput - formik:"); + console.debug(formik); + + return ( +

+ +
+ + +
+
+ H: +
+ +
+
+ + + + Max age (in hours) for the similar analysis. +
+ The default value is 24 hours (1 day). +
+ Empty value takes all the previous analysis. +
+
+
+
+
+ + + + + +
+ ); +} + +ScanConfigSelectInput.propTypes = { + formik: PropTypes.object.isRequired, +}; diff --git a/frontend/src/components/common/form/TLPSelectInput.jsx b/frontend/src/components/common/form/TLPSelectInput.jsx new file mode 100644 index 00000000..bfd6a645 --- /dev/null +++ b/frontend/src/components/common/form/TLPSelectInput.jsx @@ -0,0 +1,91 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { + FormGroup, + Input, + Label, + UncontrolledTooltip, + FormText, +} from "reactstrap"; +import { Link } from "react-router-dom"; +import { MdInfoOutline } from "react-icons/md"; + +import { TLPDescriptions } from "../../../constants/miscConst"; +import { TlpChoices } from "../../../constants/advancedSettingsConst"; +import { TLPTag } from "../TLPTag"; +import { TLPColors } from "../../../constants/colorConst"; + +export function TLPSelectInputLabel(props) { + const { size } = props; + + return ( + + ); +} + +TLPSelectInputLabel.propTypes = { + size: PropTypes.number.isRequired, +}; + +export function TLPSelectInput(props) { + const { formik } = props; + console.debug("TLPSelectInput - formik:"); + console.debug(formik); + + return ( +
+
+ {TlpChoices.map((tlp) => ( + + + + + ))} +
+ + + {TLPDescriptions[formik.values.tlp].replace("TLP: ", "")} + + +
+ ); +} + +TLPSelectInput.propTypes = { + formik: PropTypes.object.isRequired, +}; diff --git a/frontend/src/components/scan/utils/TagSelectInput.jsx b/frontend/src/components/common/form/TagSelectInput.jsx similarity index 99% rename from frontend/src/components/scan/utils/TagSelectInput.jsx rename to frontend/src/components/common/form/TagSelectInput.jsx index 7ce415ee..f55c6d60 100644 --- a/frontend/src/components/scan/utils/TagSelectInput.jsx +++ b/frontend/src/components/common/form/TagSelectInput.jsx @@ -17,7 +17,7 @@ import { addToast, } from "@certego/certego-ui"; -import { JobTag } from "../../common/JobTag"; +import { JobTag } from "../JobTag"; import { useTagsStore } from "../../../stores/useTagsStore"; // constants diff --git a/frontend/src/components/common/form/pluginsMultiSelectDropdownInput.jsx b/frontend/src/components/common/form/pluginsMultiSelectDropdownInput.jsx new file mode 100644 index 00000000..a5694fcc --- /dev/null +++ b/frontend/src/components/common/form/pluginsMultiSelectDropdownInput.jsx @@ -0,0 +1,343 @@ +import React from "react"; +import PropTypes from "prop-types"; +import ReactSelect from "react-select"; + +import { + Loader, + MultiSelectDropdownInput, + selectStyles, +} from "@certego/certego-ui"; + +import { markdownToHtml } from "../markdownToHtml"; +import { useOrganizationStore } from "../../../stores/useOrganizationStore"; +import { usePluginConfigurationStore } from "../../../stores/usePluginConfigurationStore"; +import { JobTypes } from "../../../constants/jobConst"; +import { JobTag } from "../JobTag"; + +function dropdownOptions(plugins) { + return plugins + ?.map((plugin) => ({ + isDisabled: !plugin.verification.configured || plugin.disabled, + value: plugin.name, + label: ( +
+
+
{plugin.name} 
+
+ {markdownToHtml(plugin.description)} +
+
+ {!plugin.verification.configured && ( +
+ ⚠ {plugin.verification.details} +
+ )} +
+ ), + labelDisplay: plugin.name, + })) + .sort((currentPlugin, nextPlugin) => + // eslint-disable-next-line no-nested-ternary + currentPlugin.isDisabled === nextPlugin.isDisabled + ? 0 + : currentPlugin.isDisabled + ? 1 + : -1, + ); +} + +export function AnalyzersMultiSelectDropdownInput(props) { + const { formik } = props; + console.debug("AnalyzersMultiSelectDropdownInput - formik:"); + console.debug(formik); + + // API/ store + const [analyzersLoading, analyzersError, analyzers] = + usePluginConfigurationStore((state) => [ + state.analyzersLoading, + state.analyzersError, + state.analyzers, + ]); + + const analyzersGrouped = React.useMemo(() => { + const grouped = { + ip: [], + hash: [], + domain: [], + url: [], + generic: [], + file: [], + }; + analyzers.forEach((obj) => { + if (obj.type === JobTypes.FILE) { + grouped.file.push(obj); + } else { + obj.observable_supported.forEach((clsfn) => grouped[clsfn].push(obj)); + } + }); + return grouped; + }, [analyzers]); + + const analyzersOptions = React.useMemo(() => { + // case 1: scan page (classification in formik) + if (formik.values.classification) + return dropdownOptions(analyzersGrouped[formik.values.classification]); + // case 2: editing/creating playbook config (no classification in formik) + if (formik.values.type) { + const multipleSupportedTypes = [ + ...new Set( + formik.values.type.map((type) => analyzersGrouped[type]).flat(), + ), + ]; + return dropdownOptions(multipleSupportedTypes); + } + // case 3: creating pivot config (no classification or type in formik) + return dropdownOptions(analyzers); + }, [ + analyzersGrouped, + formik.values.classification, + formik.values.type, + analyzers, + ]); + + return ( + ( + formik.setFieldValue("analyzers", value, false)} + /> + )} + /> + ); +} + +AnalyzersMultiSelectDropdownInput.propTypes = { + formik: PropTypes.object.isRequired, +}; + +export function ConnectorsMultiSelectDropdownInput({ formik }) { + console.debug("ConnectorsMultiSelectDropdownInput - formik:"); + console.debug(formik); + + // API/ store + const [connectorsLoading, connectorsError, connectors] = + usePluginConfigurationStore((state) => [ + state.connectorsLoading, + state.connectorsError, + state.connectors, + ]); + + const connectorOptions = React.useMemo( + () => dropdownOptions(connectors), + [connectors], + ); + + return ( + ( + formik.setFieldValue("connectors", value, false)} + /> + )} + /> + ); +} + +ConnectorsMultiSelectDropdownInput.propTypes = { + formik: PropTypes.object.isRequired, +}; + +export function VisualizersMultiSelectDropdownInput({ formik }) { + console.debug("VisualizersMultiSelectDropdownInput - formik:"); + console.debug(formik); + + // API/ store + const [visualizersLoading, visualizersError, visualizers] = + usePluginConfigurationStore((state) => [ + state.visualizersLoading, + state.visualizersError, + state.visualizers, + ]); + + const visualizerOptions = React.useMemo( + () => dropdownOptions(visualizers), + [visualizers], + ); + + return ( + ( + + formik.setFieldValue("visualizers", value, false) + } + /> + )} + /> + ); +} + +VisualizersMultiSelectDropdownInput.propTypes = { + formik: PropTypes.object.isRequired, +}; + +export function PivotsMultiSelectDropdownInput({ formik }) { + console.debug("PivotsMultiSelectDropdownInput - formik:"); + console.debug(formik); + + // API/ store + const [pivotsLoading, pivotsError, pivots] = usePluginConfigurationStore( + (state) => [state.pivotsLoading, state.pivotsError, state.pivots], + ); + + const pivotOptions = React.useMemo(() => dropdownOptions(pivots), [pivots]); + + return ( + ( + formik.setFieldValue("pivots", value, false)} + /> + )} + /> + ); +} + +PivotsMultiSelectDropdownInput.propTypes = { + formik: PropTypes.object.isRequired, +}; + +const playbooksGrouped = (playbooks, organizationPluginsState) => { + const grouped = { + ip: [], + hash: [], + domain: [], + url: [], + generic: [], + file: [], + }; + playbooks.forEach((obj) => { + // filter on basis of type if the playbook is not disabled in org + if (organizationPluginsState[obj.name] === undefined) { + obj.type.forEach((clsfn) => grouped[clsfn].push(obj)); + } + }); + console.debug("Playbooks", grouped); + return grouped; +}; + +export const playbookOptions = ( + playbooks, + classification = null, + organizationPluginsState = {}, +) => { + const playbooksOptionsGrouped = classification + ? playbooksGrouped(playbooks, organizationPluginsState)[classification] + : playbooks; + + return playbooksOptionsGrouped + .map((playbook) => ({ + isDisabled: playbook.disabled, + starting: playbook.starting, + value: playbook.name, + analyzers: playbook.analyzers, + connectors: playbook.connectors, + visualizers: playbook.visualizers, + pivots: playbook.pivots, + label: ( +
+
+
{playbook.name} 
+
+ {markdownToHtml(playbook.description)} +
+
+
+ ), + labelDisplay: playbook.name, + tags: playbook.tags.map((tag) => ({ + value: tag, + label: , + })), + tlp: playbook.tlp, + scan_mode: `${playbook.scan_mode}`, + scan_check_time: playbook.scan_check_time, + runtime_configuration: playbook.runtime_configuration, + })) + .filter((item) => !item.isDisabled && item.starting); +}; + +export function PlaybookMultiSelectDropdownInput(props) { + const { formik, onChange } = props; + console.debug("PlaybookMultiSelectDropdownInput - formik:"); + console.debug(formik); + + // API/ store + const { pluginsState: organizationPluginsState } = useOrganizationStore( + React.useCallback( + (state) => ({ + pluginsState: state.pluginsState, + }), + [], + ), + ); + + const [playbooksLoading, playbooksError, playbooks] = + usePluginConfigurationStore((state) => [ + state.playbooksLoading, + state.playbooksError, + state.playbooks, + ]); + + const dropdownPlaybookOptions = React.useMemo(() => { + // case 1: scan page (classification in formik) + if (formik.values.classification) + return playbookOptions( + playbooks, + formik.values.classification, + organizationPluginsState, + ); + // case 2: creating pivot config (no classification in formik) + return playbookOptions(playbooks, null, organizationPluginsState); + }, [playbooks, formik.values.classification, organizationPluginsState]); + + return ( + ( + onChange(selectedPlaybook)} + /> + )} + /> + ); +} + +PlaybookMultiSelectDropdownInput.propTypes = { + formik: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, +}; diff --git a/frontend/src/components/common/form/runtimeConfigurationInput.jsx b/frontend/src/components/common/form/runtimeConfigurationInput.jsx new file mode 100644 index 00000000..2c4e5617 --- /dev/null +++ b/frontend/src/components/common/form/runtimeConfigurationInput.jsx @@ -0,0 +1,257 @@ +// @ts-nocheck +import React from "react"; +import PropTypes from "prop-types"; + +import { ContentSection, CustomJsonInput } from "@certego/certego-ui"; + +import { markdownToHtml } from "../markdownToHtml"; +import { ScanTypes } from "../../../constants/advancedSettingsConst"; + +export function runtimeConfigurationParam( + formik, + analyzersStored, + connectorsStored, + visualizersStored, + pivotsStored, +) { + function calculateStore(pluginType) { + switch (pluginType) { + case "analyzers": + return analyzersStored; + case "connectors": + return connectorsStored; + case "visualizers": + return visualizersStored; + case "pivots": + return pivotsStored; + default: + return []; + } + } + + console.debug("EditRuntimeConfiguration - formik:"); + console.debug(formik); + + const isScanPage = formik.values.analysisOptionValues || false; + + // IMPORTANT: We want to group the plugins in the categories (analyzers, connectors, etc...) + const selectedPluginsInFormik = { analyzers: {}, connectors: {} }; + const selectedPluginsParams = { analyzers: {}, connectors: {} }; + const editableConfig = { analyzers: {}, connectors: {} }; + + // case A: scan page + if (isScanPage) { + // case 1: analysis with analyzers/connectors + if ( + formik.values.analysisOptionValues === ScanTypes.analyzers_and_connectors + ) { + selectedPluginsInFormik.analyzers = formik.values.analyzers.map( + (analyzer) => analyzer.value, + ); + selectedPluginsInFormik.connectors = formik.values.connectors.map( + (connector) => connector.value, + ); + } + // case 2: analysis with playbooks + if (formik.values.analysisOptionValues === ScanTypes.playbooks) { + Object.keys(formik.values.runtime_configuration).forEach((pluginType) => { + selectedPluginsInFormik[pluginType] = + formik.values.playbook[pluginType] || []; + }); + } + } else { + // case B: create new playbook (no plugin selected) + if ( + formik.values.analyzers.length === 0 && + formik.values.connectors.length === 0 && + formik.values.pivots.length === 0 && + formik.values.visualizers.length === 0 + ) { + console.debug("Runtime config - create new playbook"); + selectedPluginsParams.pivots = {}; + selectedPluginsParams.visualizers = {}; + editableConfig.pivots = {}; + editableConfig.visualizers = {}; + return [selectedPluginsParams, editableConfig]; + } + // case C: edit playbook config + ["analyzers", "connectors", "visualizers", "pivots"].forEach( + (pluginType) => { + selectedPluginsInFormik[pluginType] = + formik.values[pluginType]?.map((plugin) => plugin.value) || []; + }, + ); + } + + console.debug("EditRuntimeConfiguration - selectedPluginsInFormik:"); + console.debug(selectedPluginsInFormik); + + // Extract plugin default params from the store. + // Description and type are used in the side section. + Object.keys(selectedPluginsInFormik).forEach((pluginType) => { + selectedPluginsParams[pluginType] = { + // for each selected plugin we extract the config and append it to the other configs + ...selectedPluginsInFormik[pluginType].reduce( + (configurationsToDisplay, pluginName) => ({ + // in this way we add to the new object the previous object + ...configurationsToDisplay, + // find the params in the store of the selected plugin and add it + [pluginName]: calculateStore(pluginType).find( + (plugin) => plugin.name === pluginName, + )?.params, + }), + {}, + ), + }; + }); + + console.debug("EditRuntimeConfiguration - selectedPluginsParams:"); + console.debug(selectedPluginsParams); + + /* this is the dict shown when the modal is open: load the default params and the previous saved config + (in case the user update the config, save and close and reopen the modal) + We want to show data in this format: + { + pluginType: { + pluginName: { + paramName: paramValue, + }, + }, + } + */ + Object.keys(selectedPluginsInFormik).forEach((pluginType) => { + editableConfig[pluginType] = {}; + // for each plugin extract name and default params + Object.entries(selectedPluginsParams[pluginType]).forEach( + ([pluginName, pluginParams]) => { + // add empty dict in editableConfig for plugin that have not params + editableConfig[pluginType][pluginName] = {}; + // for each param (dict) extract the value of the "value" key + Object.entries(pluginParams) + .filter(([_, { value: paramValue }]) => paramValue) + .forEach(([paramName, { value: paramValue }]) => { + editableConfig[pluginType][pluginName][paramName] = paramValue; + }); + }, + ); + // override config saved in formik + editableConfig[pluginType] = { + ...editableConfig[pluginType], + ...(formik.values.runtime_configuration[pluginType] || {}), + }; + }); + + console.debug("EditRuntimeConfiguration - editableConfig:"); + console.debug(editableConfig); + + return [selectedPluginsParams, editableConfig]; +} + +export function saveRuntimeConfiguration( + formik, + jsonInput, + selectedPluginsParams, + editableConfig, +) { + // we only want to save configuration against plugins whose params dict is not empty or was modified + if (jsonInput?.jsObject) { + const runtimeConfig = {}; + Object.keys(selectedPluginsParams).forEach((pluginType) => { + runtimeConfig[pluginType] = Object.entries( + jsonInput.jsObject[pluginType], + ).reduce( + (acc, [pluginName, pluginParams]) => + // we cannot exclude empty dict or it could erase "connectors: {}" and generate an error + JSON.stringify(editableConfig[pluginType][pluginName]) !== + JSON.stringify(pluginParams) + ? { ...acc, [pluginName]: pluginParams } + : acc, + {}, + ); + }); + console.debug("EditRuntimeConfiguration - saved runtimeConfig:"); + console.debug(runtimeConfig); + formik.setFieldValue("runtime_configuration", runtimeConfig, false); + } +} + +// components +export function EditRuntimeConfiguration(props) { + const { setJsonInput, selectedPluginsParams, editableConfig } = props; + + return ( +
+ + + Note: Edit this only if you know what you are doing! + + + + {/* lateral menu with the type and description of each param */} + + {Object.keys(selectedPluginsParams) + .sort() + .map((key) => ( +
+ {Object.keys(selectedPluginsParams[key]).length > 0 ? ( +
{key.toUpperCase()}:
+ ) : ( +
+ {key.toUpperCase()}:{" "} + null +
+ )} + {Object.entries(selectedPluginsParams[key]).map( + ([name, params]) => ( +
+
{name}
+ {Object.entries(params).length ? ( +
    + {Object.entries(params).map(([pName, pObj]) => ( +
  • + {pName} +   + ({pObj.type}) +
    + {markdownToHtml(pObj.description)} +
    +
  • + ))} +
+ ) : ( + null + )} +
+ ), + )} +
+ ))} +
+
+ ); +} + +EditRuntimeConfiguration.propTypes = { + setJsonInput: PropTypes.func.isRequired, + selectedPluginsParams: PropTypes.object.isRequired, + editableConfig: PropTypes.object.isRequired, +}; diff --git a/frontend/src/components/investigations/flow/CustomJobNode.jsx b/frontend/src/components/investigations/flow/CustomJobNode.jsx index 098d72e8..eba6ab80 100644 --- a/frontend/src/components/investigations/flow/CustomJobNode.jsx +++ b/frontend/src/components/investigations/flow/CustomJobNode.jsx @@ -56,7 +56,9 @@ function CustomJobNode({ data }) { id="investigation-pivotbtn" className="mx-1 p-2" size="sm" - href={`/scan?parent=${data.id}&observable=${data.name}`} + href={`/scan?parent=${data.id}&${ + data.is_sample ? "isSample=true" : `observable=${data.name}` + }`} target="_blank" rel="noreferrer" > @@ -67,7 +69,8 @@ function CustomJobNode({ data }) { placement="top" fade={false} > - Analyze the same observable again + Analyze the same observable again. CAUTION! Samples require to + select again the file. {data.isFirstLevel && } diff --git a/frontend/src/components/investigations/flow/utils.js b/frontend/src/components/investigations/flow/utils.js index 13c537b9..40939ff2 100644 --- a/frontend/src/components/investigations/flow/utils.js +++ b/frontend/src/components/investigations/flow/utils.js @@ -20,6 +20,7 @@ function addJobNode( investigation: investigationId, children: job.children || [], status: job.status, + is_sample: job.is_sample, refetchTree, refetchInvestigation, isFirstLevel: isFirstLevel || false, diff --git a/frontend/src/components/investigations/table/investigationTableColumns.jsx b/frontend/src/components/investigations/table/investigationTableColumns.jsx index 24e61f35..4989da92 100644 --- a/frontend/src/components/investigations/table/investigationTableColumns.jsx +++ b/frontend/src/components/investigations/table/investigationTableColumns.jsx @@ -1,10 +1,10 @@ /* eslint-disable react/prop-types */ import React from "react"; +import { UncontrolledTooltip } from "reactstrap"; import { DefaultColumnFilter, SelectOptionsFilter, - LinkOpenViewIcon, DateHoverable, CopyToClipboardButton, } from "@certego/certego-ui"; @@ -24,12 +24,21 @@ export const investigationTableColumns = [ disableSortBy: true, Cell: ({ value: id }) => (
-

#{id}

- + target="_blank" + rel="noreferrer" + > + #{id} + + + View Investigation Report +
), Filter: DefaultColumnFilter, diff --git a/frontend/src/components/jobs/notifications.jsx b/frontend/src/components/jobs/notifications.jsx index 3a6dbba0..94f56440 100644 --- a/frontend/src/components/jobs/notifications.jsx +++ b/frontend/src/components/jobs/notifications.jsx @@ -17,13 +17,10 @@ export function generateJobNotification(observableName, jobId) { // notification icon setNotificationFavicon(true); - const notification = new Notification( - "ThreatMatrix analysis terminated!", - { - body: `Observable: ${observableName} (job ${jobId}) reported.\n Click here to view the result!`, - icon: `${PUBLIC_URL}/logo-blue.png`, - }, - ); + const notification = new Notification("ThreatMatrix analysis terminated!", { + body: `Observable: ${observableName} (job ${jobId}) reported.\n Click here to view the result!`, + icon: `${PUBLIC_URL}/logo-blue.png`, + }); // close the notification after 5 seconds setTimeout(() => { diff --git a/frontend/src/components/jobs/result/bar/JobActionBar.jsx b/frontend/src/components/jobs/result/bar/JobActionBar.jsx index dd3bc3ce..767c8b11 100644 --- a/frontend/src/components/jobs/result/bar/JobActionBar.jsx +++ b/frontend/src/components/jobs/result/bar/JobActionBar.jsx @@ -8,9 +8,7 @@ import { ContentSection, IconButton, addToast } from "@certego/certego-ui"; import { SaveAsPlaybookButton } from "./SaveAsPlaybooksForm"; -import { downloadJobSample, deleteJob } from "../jobApi"; -import { createJob } from "../../../scan/scanApi"; -import { ScanModesNumeric } from "../../../../constants/advancedSettingsConst"; +import { downloadJobSample, deleteJob, rescanJob } from "../jobApi"; import { JobResultSections } from "../../../../constants/miscConst"; import { DeleteIcon, @@ -53,33 +51,11 @@ export function JobActionsBar({ job }) { }; const handleRetry = async () => { - if (job.is_sample) { - addToast( - "Rescan File!", - "It's not possible to repeat a sample analysis", - "warning", - false, - 2000, - ); - } else { - addToast("Retrying the same job...", null, "spinner", false, 2000); - const response = await createJob( - [job.observable_name], - job.observable_classification, - job.playbook_requested, - job.analyzers_requested, - job.connectors_requested, - job.runtime_configuration, - job.tags.map((optTag) => optTag.label), - job.tlp, - ScanModesNumeric.FORCE_NEW_ANALYSIS, - 0, - ); + addToast("Retrying the same job...", null, "spinner", false, 2000); + const newJobId = await rescanJob(job.id); + if (newJobId) { setTimeout( - () => - navigate( - `/jobs/${response.jobIds[0]}/${JobResultSections.VISUALIZER}/`, - ), + () => navigate(`/jobs/${newJobId}/${JobResultSections.VISUALIZER}/`), 1000, ); } diff --git a/frontend/src/components/jobs/result/bar/SaveAsPlaybooksForm.jsx b/frontend/src/components/jobs/result/bar/SaveAsPlaybooksForm.jsx index c52a0d11..3665a71d 100644 --- a/frontend/src/components/jobs/result/bar/SaveAsPlaybooksForm.jsx +++ b/frontend/src/components/jobs/result/bar/SaveAsPlaybooksForm.jsx @@ -6,7 +6,8 @@ import PropTypes from "prop-types"; import { addToast, PopupFormButton } from "@certego/certego-ui"; -import { saveJobAsPlaybook } from "./jobBarApi"; +import { PluginsTypes } from "../../../../constants/pluginConst"; +import { createPluginConfig } from "../../../plugins/pluginsApi"; // constants const initialValues = { @@ -35,12 +36,24 @@ const onValidate = (values) => { // Invitation Form export function SaveAsPlaybookForm({ onFormSubmit }) { - console.debug("InvitationForm rendered!"); + console.debug("SaveAsPlaybookForm rendered!"); const onSubmit = React.useCallback( async (values, formik) => { + const payloadData = { + name: values.name, + description: values.description, + analyzers: values.analyzers, + connectors: values.connectors, + pivots: values.pivots, + runtime_configuration: values.runtimeConfiguration, + tags_labels: values.tags_labels, + tlp: values.tlp, + scan_mode: values.scan_mode, + scan_check_time: values.scan_check_time, + }; try { - await saveJobAsPlaybook(values); + await createPluginConfig(PluginsTypes.PLAYBOOK, payloadData); onFormSubmit(); } catch (error) { addToast(Error!, error.parsedMsg, "warning"); @@ -48,6 +61,7 @@ export function SaveAsPlaybookForm({ onFormSubmit }) { formik.setSubmitting(false); } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [onFormSubmit], ); diff --git a/frontend/src/components/jobs/result/bar/jobBarApi.jsx b/frontend/src/components/jobs/result/bar/jobBarApi.jsx deleted file mode 100644 index fc812ab8..00000000 --- a/frontend/src/components/jobs/result/bar/jobBarApi.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from "react"; -import { addToast } from "@certego/certego-ui"; -import axios from "axios"; - -import { PLAYBOOKS_CONFIG_URI } from "../../../../constants/apiURLs"; - -export async function saveJobAsPlaybook(values) { - let success = false; - const data = { - name: values.name, - description: values.description, - analyzers: values.analyzers, - connectors: values.connectors, - pivots: values.pivots, - runtime_configuration: values.runtimeConfiguration, - tags_labels: values.tags_labels, - tlp: values.tlp, - scan_mode: values.scan_mode, - scan_check_time: values.scan_check_time, - }; - try { - const response = await axios.post(PLAYBOOKS_CONFIG_URI, data); - - success = response.status === 200; - if (success) { - addToast( - - Playbook with name {response.data.name} created with success - , - null, - "info", - ); - } - } catch (error) { - addToast( - Failed creation of playbook with name {values.name}, - error.parsedMsg, - "warning", - ); - } - return success; -} diff --git a/frontend/src/components/jobs/result/jobApi.jsx b/frontend/src/components/jobs/result/jobApi.jsx index f6d36666..50f5a537 100644 --- a/frontend/src/components/jobs/result/jobApi.jsx +++ b/frontend/src/components/jobs/result/jobApi.jsx @@ -61,6 +61,33 @@ export async function deleteJob(jobId) { return success; } +export async function rescanJob(jobId) { + try { + const response = await axios.post(`${JOB_BASE_URI}/${jobId}/rescan`); + const newJobId = response.data.id; + if (response.status === 202) { + addToast( + + Sent rescan request for job #{jobId}. Created job #{newJobId}. + , + null, + "success", + 2000, + ); + } + return newJobId; + } catch (error) { + addToast( + + Failed. Operation: rescan job #{jobId} + , + error.parsedMsg, + "warning", + ); + return null; + } +} + export async function killPlugin(jobId, plugin) { const sure = await areYouSureConfirmDialog( `kill ${plugin.type} '${plugin.name}'`, diff --git a/frontend/src/components/jobs/table/jobTableColumns.jsx b/frontend/src/components/jobs/table/jobTableColumns.jsx index c77b81ef..fa3dcfc0 100644 --- a/frontend/src/components/jobs/table/jobTableColumns.jsx +++ b/frontend/src/components/jobs/table/jobTableColumns.jsx @@ -1,12 +1,11 @@ /* eslint-disable react/prop-types */ import React from "react"; -import { Input } from "reactstrap"; +import { Input, UncontrolledTooltip } from "reactstrap"; import classnames from "classnames"; import { DefaultColumnFilter, SelectOptionsFilter, - LinkOpenViewIcon, DateHoverable, CopyToClipboardButton, } from "@certego/certego-ui"; @@ -35,12 +34,17 @@ export const jobTableColumns = [ disableSortBy: true, Cell: ({ value: id }) => (
-

#{id}

- + target="_blank" + rel="noreferrer" + > + #{id} + + + View Job Report +
), Filter: DefaultColumnFilter, diff --git a/frontend/src/components/organization/MyOrgPage.jsx b/frontend/src/components/organization/MyOrgPage.jsx index 856d6f69..da1420ed 100644 --- a/frontend/src/components/organization/MyOrgPage.jsx +++ b/frontend/src/components/organization/MyOrgPage.jsx @@ -20,7 +20,7 @@ export default function MyOrgPage() { error: respErr, organization, fetchAll, - noOrg, + isInOrganization, } = useOrganizationStore( React.useCallback( (state) => ({ @@ -28,7 +28,7 @@ export default function MyOrgPage() { error: state.error, organization: state.organization, fetchAll: state.fetchAll, - noOrg: state.noOrg, + isInOrganization: state.isInOrganization, }), [], ), @@ -36,10 +36,10 @@ export default function MyOrgPage() { // on component mount React.useEffect(() => { - if (Object.keys(organization).length === 0 && !noOrg) { + if (Object.keys(organization).length === 0 && isInOrganization) { fetchAll(); } - }, [organization, fetchAll, noOrg]); + }, [organization, fetchAll, isInOrganization]); // page title useTitle( diff --git a/frontend/src/components/organization/OrgConfig.jsx b/frontend/src/components/organization/OrgConfig.jsx index 24759a07..a392500c 100644 --- a/frontend/src/components/organization/OrgConfig.jsx +++ b/frontend/src/components/organization/OrgConfig.jsx @@ -21,7 +21,7 @@ export default function OrgConfig() { organization, fetchAll, isUserAdmin, - noOrg, + isInOrganization, } = useOrganizationStore( React.useCallback( (state) => ({ @@ -30,7 +30,7 @@ export default function OrgConfig() { organization: state.organization, fetchAll: state.fetchAll, isUserAdmin: state.isUserAdmin, - noOrg: state.noOrg, + isInOrganization: state.isInOrganization, }), [], ), @@ -38,10 +38,10 @@ export default function OrgConfig() { // on component mount React.useEffect(() => { - if (Object.keys(organization).length === 0 && !noOrg) { + if (Object.keys(organization).length === 0 && isInOrganization) { fetchAll(); } - }, [noOrg, organization, fetchAll]); + }, [isInOrganization, organization, fetchAll]); // page title useTitle( @@ -56,7 +56,7 @@ export default function OrgConfig() { loading={loading} error={respErr} render={() => { - if (noOrg) + if (!isInOrganization) return ( diff --git a/frontend/src/components/plugins/PluginsContainer.jsx b/frontend/src/components/plugins/PluginsContainer.jsx index 42f972bc..4825730d 100644 --- a/frontend/src/components/plugins/PluginsContainer.jsx +++ b/frontend/src/components/plugins/PluginsContainer.jsx @@ -1,20 +1,19 @@ import React, { Suspense } from "react"; import { AiOutlineApi } from "react-icons/ai"; -import { BsPeopleFill, BsSliders } from "react-icons/bs"; import { TiFlowChildren, TiBook } from "react-icons/ti"; import { IoIosEye } from "react-icons/io"; import { MdInput } from "react-icons/md"; import { PiGraphFill } from "react-icons/pi"; - -import { - RouterTabs, - FallBackLoading, - ContentSection, -} from "@certego/certego-ui"; -import { Link } from "react-router-dom"; +import { BsFillPlusCircleFill } from "react-icons/bs"; +import { useLocation } from "react-router-dom"; import { Button, Col } from "reactstrap"; -import { useOrganizationStore } from "../../stores/useOrganizationStore"; + +import { RouterTabs, FallBackLoading } from "@certego/certego-ui"; import { useGuideContext } from "../../contexts/GuideContext"; +import { PlaybookConfigForm } from "./forms/PlaybookConfigForm"; +import { PivotConfigForm } from "./forms/PivotConfigForm"; +import { AnalyzerConfigForm } from "./forms/AnalyzerConfigForm"; +import { PluginsTypes } from "../../constants/pluginConst"; const Analyzers = React.lazy(() => import("./types/Analyzers")); const Connectors = React.lazy(() => import("./types/Connectors")); @@ -118,20 +117,19 @@ const routes = [ export default function PluginsContainer() { console.debug("PluginsContainer rendered!"); - const { - isUserOwner, - organization, - fetchAll: fetchAllOrganizations, - } = useOrganizationStore( - React.useCallback( - (state) => ({ - isUserOwner: state.isUserOwner, - fetchAll: state.fetchAll, - organization: state.organization, - }), - [], - ), - ); + const location = useLocation(); + const pluginsPage = location?.pathname.split("/")[2]; + const enableCreateButton = [ + `${PluginsTypes.ANALYZER}s`, + `${PluginsTypes.PIVOT}s`, + `${PluginsTypes.PLAYBOOK}s`, + ].includes(pluginsPage); + + const [showModalCreatePlaybook, setShowModalCreatePlaybook] = + React.useState(false); + const [showModalCreatePivot, setShowModalCreatePivot] = React.useState(false); + const [showModalCreateAnalyzer, setShowModalCreateAnalyzer] = + React.useState(false); const { guideState, setGuideState } = useGuideContext(); @@ -144,49 +142,56 @@ export default function PluginsContainer() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // on component mount - React.useEffect(() => { - if (!isUserOwner) { - fetchAllOrganizations(); + const onClick = async () => { + // open modal for create playbook + if (pluginsPage === `${PluginsTypes.PLAYBOOK}s`) { + setShowModalCreatePlaybook(true); + } + // open modal for create pivot + if (pluginsPage === `${PluginsTypes.PIVOT}s`) { + setShowModalCreatePivot(true); + } + // open modal for create analyzer + if (pluginsPage === `${PluginsTypes.ANALYZER}s`) { + setShowModalCreateAnalyzer(true); } - }, [isUserOwner, fetchAllOrganizations]); - const configButtons = ( + return null; + }; + + const createButton = ( - - {organization?.name ? ( - - - - ) : null} - - - - + +  Create {pluginsPage} + + )} + {showModalCreatePlaybook && ( + + )} + {showModalCreatePivot && ( + + )} + {showModalCreateAnalyzer && ( + + )} ); - return ; + + return ; } diff --git a/frontend/src/components/plugins/forms/AnalyzerConfigForm.jsx b/frontend/src/components/plugins/forms/AnalyzerConfigForm.jsx new file mode 100644 index 00000000..01ac0a93 --- /dev/null +++ b/frontend/src/components/plugins/forms/AnalyzerConfigForm.jsx @@ -0,0 +1,615 @@ +import React from "react"; +import { + FormGroup, + Label, + Button, + Input, + Modal, + ModalHeader, + ModalBody, + Form, + Row, + Col, +} from "reactstrap"; +import { Link } from "react-router-dom"; +import { useFormik, FormikProvider } from "formik"; +import PropTypes from "prop-types"; +import { CustomJsonInput } from "@certego/certego-ui"; + +import { editPluginConfig, createPluginConfig } from "../pluginsApi"; +import { PluginsTypes } from "../../../constants/pluginConst"; +import { usePluginConfigurationStore } from "../../../stores/usePluginConfigurationStore"; +import { ObservableClassifications } from "../../../constants/jobConst"; +import { URL_REGEX } from "../../../constants/regexConst"; +import { + TLPSelectInput, + TLPSelectInputLabel, +} from "../../common/form/TLPSelectInput"; +import { HTTPMethods } from "../../../constants/miscConst"; + +export function AnalyzerConfigForm({ analyzerConfig, toggle, isOpen }) { + console.debug("AnalyzerConfigForm rendered!"); + + const isEditing = Object.keys(analyzerConfig).length > 0; + + // states + const [responseError, setResponseError] = React.useState(null); + const [headersJsonInput, setHeadersJsonInput] = React.useState({}); + const [paramsJsonInput, setParamsJsonInput] = React.useState({}); + + // store + const [retrieveAnalyzersConfiguration] = usePluginConfigurationStore( + (state) => [state.retrieveAnalyzersConfiguration], + ); + + const formik = useFormik({ + initialValues: { + name: analyzerConfig?.name || "", + description: analyzerConfig?.description || "", + observable_supported: analyzerConfig?.observable_supported || [], + tlp: analyzerConfig?.maximum_tlp || "RED", + url: analyzerConfig?.params?.url?.value || "", + http_method: analyzerConfig?.params?.http_method?.value || "get", + headers: analyzerConfig?.params?.headers?.value || { + Accept: "application/json", + }, + params: analyzerConfig?.params?.params?.value || { + param_name: "", + }, + api_key_name: analyzerConfig?.secrets?.api_key_name?.value || "", + certificate: analyzerConfig?.secrets?.certificate?.value || "", + }, + validate: (values) => { + console.debug("validate - values"); + console.debug(values); + + const minLength = 3; + const errors = {}; + + if (!values.name) { + errors.name = "This field is required."; + } else if (values.name.length < minLength) { + errors.name = `This field must be at least ${minLength} characters long`; + } + + if (!/^[a-zA-Z0-9_]+$/.test(values.name)) { + errors.name = + "This is not a valid name. It only supports alphanumeric characters and underscore"; + } + + if (!values.description) { + errors.description = "This field is required."; + } else if (values.description.length < minLength) { + errors.description = `This field must be at least ${minLength} characters long`; + } + + if (values.observable_supported.length === 0) { + errors.observable_supported = "This field is required."; + } + + if (!values.url) { + errors.url = "This field is required."; + } + if (!URL_REGEX.test(values.url)) { + errors.url = "This is not a valid url."; + } + + console.debug("formik validation errors"); + console.debug(errors); + return errors; + }, + onSubmit: async () => { + let response; + const payloadData = { + name: formik.values.name, + description: formik.values.description, + observable_supported: formik.values.observable_supported, + maximum_tlp: formik.values.tlp, + }; + if (!isEditing) { + payloadData.type = "observable"; + payloadData.python_module = + "basic_observable_analyzer.BasicObservableAnalyzer"; + } + // plugin config + payloadData.plugin_config = [ + { + type: 1, + plugin_name: formik.values.name, + attribute: "http_method", + value: formik.values.http_method, + config_type: 1, + }, + { + type: 1, + plugin_name: formik.values.name, + attribute: "url", + value: formik.values.url, + config_type: 1, + }, + { + type: 1, + plugin_name: formik.values.name, + attribute: "headers", + value: + headersJsonInput?.json || JSON.stringify(formik.values.headers), + config_type: 1, + }, + { + type: 1, + plugin_name: formik.values.name, + attribute: "api_key_name", + value: JSON.stringify(formik.values.api_key_name), + config_type: 2, + }, + { + type: 1, + plugin_name: formik.values.name, + attribute: "certificate", + value: JSON.stringify(formik.values.certificate), + config_type: 2, + }, + { + type: 1, + plugin_name: formik.values.name, + attribute: "params", + value: paramsJsonInput?.json || JSON.stringify({}), + config_type: 1, + }, + ]; + + if (isEditing) { + const analyzerToEdit = + formik.initialValues.name !== formik.values.name + ? formik.initialValues.name + : formik.values.name; + response = await editPluginConfig( + PluginsTypes.ANALYZER, + analyzerToEdit, + payloadData, + ); + } else { + response = await createPluginConfig(PluginsTypes.ANALYZER, payloadData); + } + + if (response?.success) { + formik.setSubmitting(false); + setResponseError(null); + formik.resetForm(); + toggle(false); + retrieveAnalyzersConfiguration(); + } else { + setResponseError(response?.error); + } + }, + }); + + const title = isEditing ? "Edit analyzer config" : "Create a new analyzer"; + + return ( + + toggle(false)}> + {title} + + + +
+ + + + + + + + {formik.touched.name && formik.errors.name && ( + {formik.errors.name} + )} + + + + + + + + + + + {formik.touched.description && formik.errors.description && ( + + {formik.errors.description} + + )} + + + + + + + + + + {Object.values(ObservableClassifications).map((type) => ( + + + + + ))} + {formik.touched.observable_supported && + formik.errors.observable_supported && ( + + {formik.errors.observable_supported} + + )} + + + + + + + +
+ Plugin Config +
+ {isEditing && ( + + Note: Your plugin configuration overrides your{" "} + + organization's configuration + {" "} + (if any). + + )} +
+ + + + + + + +
+ + URL of the instance you want to connect to + + {formik.touched.url && formik.errors.url && ( + {formik.errors.url} + )} +
+ +
+
+ + + + + + + {Object.values(HTTPMethods).map((method) => ( + + + + + ))} + + + {formik.values.http_method === HTTPMethods.GET ? ( + + + + Request formats +
    +
  • + Query string (default):  + + http://www.service.com?param_name=<observable> + + . The section below must be filled in correctly.  +
  • +
  • + REST:  + + http://www.service.com/<observable> + + . The params section below must be empty. In that case + the analyzed observable will be automatically added to + the URL during the analysis. +
  • +
+
+ +
+ ) : ( + + +
+ + The entire dictionary in the section below will be used + as the payload for the request. + +
+ +
+ )} +
+ + + + + + +
+ +
+
+ + You have to change <param_name> key to the correct + name. It is possible to add other parameters. +
+ Note: the <observable> placeholder will be + automatically replaced during the analysis. +
+
+ +
+
+ + + + + + +
+ +
+
+ + Headers used for the request.
+ If Authorization is required, you must + use the <api_key> placeholder insead of actual API + key. ex: Authorization: 'Token <api_key>' +
+
+ +
+
+ + + + + + + +
+ + API key required for authentication. It will replace the + <api_key> placeholder in the header. + + {formik.touched.api_key_name && + formik.errors.api_key_name && ( + + {formik.errors.api_key_name} + + )} +
+ +
+
+ + + + + + + +
+ + Self signed SSL certificate for internal services + + {formik.touched.certificate && + formik.errors.certificate && ( + + {formik.errors.certificate} + + )} +
+ +
+
+ + + {responseError && formik.submitCount && ( + {responseError} + )} + + +
+
+
+
+ ); +} + +AnalyzerConfigForm.propTypes = { + analyzerConfig: PropTypes.object, + toggle: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, +}; + +AnalyzerConfigForm.defaultProps = { + analyzerConfig: {}, +}; diff --git a/frontend/src/components/plugins/forms/PivotConfigForm.jsx b/frontend/src/components/plugins/forms/PivotConfigForm.jsx new file mode 100644 index 00000000..a44c1bd3 --- /dev/null +++ b/frontend/src/components/plugins/forms/PivotConfigForm.jsx @@ -0,0 +1,442 @@ +import React from "react"; +import { + FormGroup, + Label, + Button, + Spinner, + Input, + Modal, + ModalHeader, + ModalBody, + UncontrolledTooltip, +} from "reactstrap"; +import { Form, useFormik, FormikProvider } from "formik"; +import PropTypes from "prop-types"; +import ReactSelect from "react-select"; +import { MdInfoOutline } from "react-icons/md"; +import { selectStyles } from "@certego/certego-ui"; + +import { + PlaybookMultiSelectDropdownInput, + AnalyzersMultiSelectDropdownInput, + ConnectorsMultiSelectDropdownInput, +} from "../../common/form/pluginsMultiSelectDropdownInput"; +import { usePluginConfigurationStore } from "../../../stores/usePluginConfigurationStore"; +import { PluginsTypes } from "../../../constants/pluginConst"; +import { editPluginConfig, createPluginConfig } from "../pluginsApi"; + +export function PivotConfigForm({ pivotConfig, toggle, isOpen }) { + console.debug("PivotConfigForm rendered!"); + + // states + const [responseError, setResponseError] = React.useState(null); + + // store + const [retrievePivotsConfiguration] = usePluginConfigurationStore((state) => [ + state.retrievePivotsConfiguration, + ]); + + const pythonModuleOptions = [ + { + value: "any_compare.AnyCompare", + labelDisplay: "Compare field", + label: ( +
+
+
Compare field 
+
+ Create a custom Pivot from a specific value extracted from the + first successful analyzers or connectors. +
+
+
+ ), + }, + { + value: "self_analyzable.SelfAnalyzable", + labelDisplay: "Self Analyzable", + label: ( +
+
+
Self Analyzable 
+
+ Create a custom Pivot that would analyze again the same + observable/file. +
+
+
+ ), + }, + ]; + + const isPythonModuleSelectable = pythonModuleOptions.find( + (element) => element.value === pivotConfig?.python_module, + ); + + const isEditing = Object.keys(pivotConfig).length > 0; + + const formik = useFormik({ + initialValues: { + name: pivotConfig?.name || "", + description: pivotConfig?.description || "", + python_module: + { + value: pivotConfig?.python_module, + label: + pythonModuleOptions.find( + (element) => element.value === pivotConfig?.python_module, + )?.label || pivotConfig?.python_module, + } || {}, + playbook: + pivotConfig?.playbooks_choice?.map((playbook) => ({ + value: playbook, + label: playbook, + })) || [], + field_to_compare: pivotConfig?.params?.field_to_compare?.value || "", + analyzers: + pivotConfig?.related_analyzer_configs?.map((analyzer) => ({ + value: analyzer, + label: analyzer, + })) || [], + connectors: + pivotConfig?.related_connector_configs?.map((connector) => ({ + value: connector, + label: connector, + })) || [], + }, + validate: (values) => { + console.debug("validate - values"); + console.debug(values); + + const minLength = 3; + const errors = {}; + + if (!values.name) { + errors.name = "This field is required."; + } else if (values.name.length < minLength) { + errors.name = `This field must be at least ${minLength} characters long`; + } + + if ( + values.python_module.value === "any_compare.AnyCompare" && + !values.field_to_compare + ) { + errors.field_to_compare = "This field is required."; + } + + if (values.playbook.length === 0) { + errors.playbook = "This field is required."; + } + + if (values.analyzers.length === 0 && values.connectors.length === 0) { + errors.analyzers = "Analyzers or connectors required"; + errors.connectors = "Analyzers or connectors required"; + } + if (values.analyzers.length !== 0 && values.connectors.length !== 0) { + errors.analyzers = "You can't set both analyzers and connectors"; + errors.connectors = "You can't set both analyzers and connectors"; + } + + console.debug("formik validation errors"); + console.debug(errors); + return errors; + }, + onSubmit: async () => { + let response; + + const payloadData = { + name: formik.values.name, + python_module: formik.values.python_module.value, + playbooks_choice: [formik.values.playbook[0].value], + related_analyzer_configs: formik.values.analyzers.map( + (analyzer) => analyzer.value, + ), + related_connector_configs: formik.values.connectors.map( + (connector) => connector.value, + ), + }; + if (formik.values.field_to_compare) { + payloadData.plugin_config = { + type: 5, + plugin_name: formik.values.name, + attribute: "field_to_compare", + value: formik.values.field_to_compare, + config_type: 1, + }; + } + + if (isEditing) { + const pivotToEdit = + formik.initialValues.name !== formik.values.name + ? formik.initialValues.name + : formik.values.name; + response = await editPluginConfig( + PluginsTypes.PIVOT, + pivotToEdit, + payloadData, + ); + } else { + response = await createPluginConfig(PluginsTypes.PIVOT, payloadData); + } + + if (response?.success) { + formik.setSubmitting(false); + setResponseError(null); + formik.resetForm(); + toggle(false); + retrievePivotsConfiguration(); + } else { + setResponseError(response?.error); + } + }, + }); + + console.debug("Pivot Config - formik"); + console.debug(formik); + + /* With the setFieldValue the validation and rerender don't work properly: the last update seems to not trigger the validation + and leaves the UI with values not valid, for this reason the scan button is disabled, but if the user set focus on the UI the last + validation trigger and start scan is enabled. To avoid this we use this hook that force the validation when the form values change. + + This hook is the reason why we can disable the validation in the setFieldValue method (3rd params). + */ + React.useEffect(() => { + formik.validateForm(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formik.values]); + + // reset errors if the user change any field after a failed submission + React.useEffect(() => { + if (formik.submitCount && responseError) setResponseError(null); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formik.values]); + + const title = isEditing ? "Edit pivot config" : "Create a new pivot"; + + return ( + + toggle(false)}> + {title} + + + +
+ {formik.touched.name && formik.errors.name && ( + Name: {formik.errors.name} + )} + + + + + {formik.touched.description && formik.errors.description && ( + + Description: {formik.errors.description} + + )} + + + + +
+ Note: Pivots are designed to create a job from + another job after certain conditions are triggered.
+ This plugin can only run automatically within a playbook so it is + important to select the analyzers or connectors after which the + pivot will be executed.
+ Every playbook containing the following combination of + analyzers/connectors can have this Pivot attached to. +
+
+ {formik.values.analyzers.length !== 0 && + formik.values.connectors.length !== 0 && ( + <> +
+ + {formik.errors.analyzers} + + + )} + + + + + + + + +
+ + + {isEditing && !isPythonModuleSelectable ? ( + + ) : ( + + formik.setFieldValue("python_module", value, false) + } + /> + )} + + {formik.values.python_module.value === "any_compare.AnyCompare" && ( + + + + + )} + + + { + formik.setFieldValue("playbook", [playbook], false); + }} + /> + + + + {responseError && formik.submitCount && ( + {responseError} + )} + + +
+
+
+
+ ); +} + +PivotConfigForm.propTypes = { + pivotConfig: PropTypes.object, + toggle: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, +}; + +PivotConfigForm.defaultProps = { + pivotConfig: {}, +}; diff --git a/frontend/src/components/plugins/forms/PlaybookConfigForm.jsx b/frontend/src/components/plugins/forms/PlaybookConfigForm.jsx new file mode 100644 index 00000000..6ecab201 --- /dev/null +++ b/frontend/src/components/plugins/forms/PlaybookConfigForm.jsx @@ -0,0 +1,443 @@ +import React from "react"; +import { + FormGroup, + Label, + Button, + Spinner, + Input, + Modal, + ModalHeader, + ModalBody, +} from "reactstrap"; +import { Form, useFormik, FormikProvider } from "formik"; +import PropTypes from "prop-types"; + +import { + AnalyzersMultiSelectDropdownInput, + ConnectorsMultiSelectDropdownInput, + VisualizersMultiSelectDropdownInput, + PivotsMultiSelectDropdownInput, +} from "../../common/form/pluginsMultiSelectDropdownInput"; +import { usePluginConfigurationStore } from "../../../stores/usePluginConfigurationStore"; +import { + TLPSelectInput, + TLPSelectInputLabel, +} from "../../common/form/TLPSelectInput"; +import { + AllPluginSupportedTypes, + PluginsTypes, +} from "../../../constants/pluginConst"; +import { ScanConfigSelectInput } from "../../common/form/ScanConfigSelectInput"; +import { parseScanCheckTime } from "../../../utils/time"; +import { TagSelectInput } from "../../common/form/TagSelectInput"; +import { JobTag } from "../../common/JobTag"; +import { + TlpChoices, + TLPs, + ScanModesNumeric, +} from "../../../constants/advancedSettingsConst"; +import { + EditRuntimeConfiguration, + runtimeConfigurationParam, + saveRuntimeConfiguration, +} from "../../common/form/runtimeConfigurationInput"; +import { editPluginConfig, createPluginConfig } from "../pluginsApi"; + +// constants +const stateSelector = (state) => [ + state.analyzers, + state.connectors, + state.visualizers, + state.pivots, + state.retrievePlaybooksConfiguration, +]; + +export function PlaybookConfigForm({ + playbookConfig, + toggle, + isOpen, + pluginsLoading, +}) { + console.debug("PlaybookConfigForm rendered!"); + + // states + const [selectedPluginsParams, setSelectedPluginsParams] = React.useState({}); + const [editableConfig, setEditableConfig] = React.useState({}); + const [jsonInput, setJsonInput] = React.useState({}); + const [responseError, setResponseError] = React.useState(null); + + const isEditing = Object.keys(playbookConfig).length > 0; + + // store + const [ + analyzers, + connectors, + visualizers, + pivots, + retrievePlaybooksConfiguration, + ] = usePluginConfigurationStore(stateSelector); + + const formik = useFormik({ + initialValues: { + name: playbookConfig?.name || "", + description: playbookConfig?.description || "", + type: playbookConfig?.type || [], + analyzers: + playbookConfig?.analyzers?.map((analyzer) => ({ + value: analyzer, + label: analyzer, + })) || [], + connectors: + playbookConfig?.connectors?.map((connector) => ({ + value: connector, + label: connector, + })) || [], + visualizers: + playbookConfig?.visualizers?.map((visualizer) => ({ + value: visualizer, + label: visualizer, + })) || [], + pivots: + playbookConfig?.pivots?.map((pivot) => ({ + value: pivot, + label: pivot, + })) || [], + tags: + playbookConfig?.tags?.map((tag) => ({ + value: tag, + label: , + })) || [], + tlp: playbookConfig?.tlp || TLPs.AMBER, + scan_mode: playbookConfig?.scan_mode + ? `${playbookConfig?.scan_mode}` + : ScanModesNumeric.CHECK_PREVIOUS_ANALYSIS, + scan_check_time: parseScanCheckTime( + playbookConfig?.scan_check_time || "01:00:00:00", + ), + runtime_configuration: playbookConfig?.runtime_configuration || { + analyzers: {}, + connectors: {}, + pivots: {}, + visualizers: {}, + }, + }, + validate: (values) => { + console.debug("validate - values"); + console.debug(values); + + const minLength = 3; + const errors = {}; + + if (!values.name) { + errors.name = "This field is required."; + } else if (values.name.length < minLength) { + errors.name = `This field must be at least ${minLength} characters long`; + } + + if (!values.description) { + errors.description = "This field is required."; + } + if (values.type.length === 0) { + errors.type = "This field is required."; + } + + if (values.analyzers.length === 0 && values.connectors.length === 0) { + errors.analyzers = "analyzers or connectors required"; + errors.connectors = "analyzers or connectors required"; + } + if (!TlpChoices.includes(values.tlp)) { + errors.tlp = "Invalid choice"; + } + + console.debug("formik validation errors"); + console.debug(errors); + return errors; + }, + onSubmit: async () => { + let response; + const payloadData = { + name: formik.values.name, + description: formik.values.description, + type: formik.values.type, + analyzers: formik.values.analyzers.map((analyzer) => analyzer.value), + connectors: formik.values.connectors.map( + (connector) => connector.value, + ), + visualizers: formik.values.visualizers.map( + (visualizer) => visualizer.value, + ), + pivots: formik.values.pivots.map((pivot) => pivot.value), + runtime_configuration: formik.values.runtime_configuration, + tags_labels: formik.values.tags.map((tag) => tag.value.label), + tlp: formik.values.tlp, + scan_mode: parseInt(formik.values.scan_mode, 10), + scan_check_time: null, + }; + if ( + formik.values.scan_mode === ScanModesNumeric.CHECK_PREVIOUS_ANALYSIS + ) { + payloadData.scan_check_time = `${formik.values.scan_check_time}:00:00`; + } + + if (isEditing) { + const playbookToEdit = + formik.initialValues.name !== formik.values.name + ? formik.initialValues.name + : formik.values.name; + response = await editPluginConfig( + PluginsTypes.PLAYBOOK, + playbookToEdit, + payloadData, + ); + } else { + response = await createPluginConfig(PluginsTypes.PLAYBOOK, payloadData); + } + + if (response?.success) { + formik.setSubmitting(false); + setResponseError(null); + formik.resetForm(); + toggle(false); + retrievePlaybooksConfiguration(); + } else { + setResponseError(response?.error); + } + }, + }); + + console.debug("Playbook Config - formik"); + console.debug(formik); + + React.useEffect(() => { + if (!pluginsLoading) { + const [params, config] = runtimeConfigurationParam( + formik, + analyzers, + connectors, + visualizers, + pivots, + ); + setSelectedPluginsParams(params); + setEditableConfig(config); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + formik.values.analyzers, + formik.values.connectors, + formik.values.pivots, + formik.values.visualizers, + pluginsLoading, + ]); + + React.useEffect(() => { + saveRuntimeConfiguration( + formik, + jsonInput, + selectedPluginsParams, + editableConfig, + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jsonInput]); + + /* With the setFieldValue the validation and rerender don't work properly: the last update seems to not trigger the validation + and leaves the UI with values not valid, for this reason the scan button is disabled, but if the user set focus on the UI the last + validation trigger and start scan is enabled. To avoid this we use this hook that force the validation when the form values change. + + This hook is the reason why we can disable the validation in the setFieldValue method (3rd params). + */ + React.useEffect(() => { + formik.validateForm(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formik.values]); + + // reset errors if the user change any field after a failed submission + React.useEffect(() => { + if (formik.submitCount && responseError) setResponseError(null); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [formik.values]); + + const title = isEditing ? "Edit playbook config" : "Create a new playbook"; + + return ( + + toggle(false)}> + {title} + + + +
+ {formik.touched.name && formik.errors.name && ( + Name: {formik.errors.name} + )} + + + + + {formik.touched.description && formik.errors.description && ( + + Description: {formik.errors.description} + + )} + + + + + {formik.touched.type && formik.errors.type && ( + Type: {formik.errors.type} + )} + + + {Object.values(AllPluginSupportedTypes).map((type) => ( + + + + + ))} + + + + + + + + + + + + + + + + + + + + + + + + + formik.setFieldValue("tags", selectedTags, false) + } + /> + + + + + + + + + + + + {responseError && formik.submitCount && ( + {responseError} + )} + + +
+
+
+
+ ); +} + +PlaybookConfigForm.propTypes = { + playbookConfig: PropTypes.object, + toggle: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, + pluginsLoading: PropTypes.bool, +}; + +PlaybookConfigForm.defaultProps = { + playbookConfig: {}, + pluginsLoading: false, +}; diff --git a/frontend/src/components/plugins/pluginsApi.jsx b/frontend/src/components/plugins/pluginsApi.jsx new file mode 100644 index 00000000..9ee2eb51 --- /dev/null +++ b/frontend/src/components/plugins/pluginsApi.jsx @@ -0,0 +1,76 @@ +import axios from "axios"; + +import { addToast } from "@certego/certego-ui"; +import { API_BASE_URI } from "../../constants/apiURLs"; +import { prettifyErrors } from "../../utils/api"; + +export async function createPluginConfig(type, data) { + let success = false; + try { + const response = await axios.post(`${API_BASE_URI}/${type}`, data); + success = response.status === 201; + if (success) { + addToast( + `${type} with name ${response.data.name} created with success`, + null, + "success", + ); + } + } catch (error) { + addToast( + `Failed creation of ${type} with name ${data.name}`, + prettifyErrors(error), + "warning", + true, + 10000, + ); + return { success, error: prettifyErrors(error) }; + } + return { success }; +} + +export async function editPluginConfig(type, pluginName, data) { + let success = false; + try { + const response = await axios.patch( + `${API_BASE_URI}/${type}/${pluginName}`, + data, + ); + success = response.status === 200; + if (success) { + addToast(`${data.name} configuration saved`, null, "success"); + } + } catch (error) { + addToast( + `Failed to edited ${type} with name ${data.name}`, + prettifyErrors(error), + "warning", + true, + 10000, + ); + return { success, error: prettifyErrors(error) }; + } + return { success }; +} + +export async function deletePluginConfig(type, pluginName) { + try { + const response = await axios.delete( + `${API_BASE_URI}/${type}/${pluginName}`, + ); + addToast( + `${type} with name ${pluginName} deleted with success`, + null, + "success", + ); + return Promise.resolve(response); + } catch (error) { + addToast( + `Failed deletion of ${type} with name ${pluginName}`, + prettifyErrors(error), + "warning", + true, + ); + return null; + } +} diff --git a/frontend/src/components/plugins/types/Pivots.jsx b/frontend/src/components/plugins/types/Pivots.jsx index 23e106e7..76f14165 100644 --- a/frontend/src/components/plugins/types/Pivots.jsx +++ b/frontend/src/components/plugins/types/Pivots.jsx @@ -20,7 +20,7 @@ export default function Pivots() { return ( {dataList?.length} total - {description} Fore more info check the{" "} + {description} For more info check the{" "} - official doc + official doc. diff --git a/frontend/src/components/plugins/types/pluginActionsButtons.jsx b/frontend/src/components/plugins/types/pluginActionsButtons.jsx index 596b6109..af068003 100644 --- a/frontend/src/components/plugins/types/pluginActionsButtons.jsx +++ b/frontend/src/components/plugins/types/pluginActionsButtons.jsx @@ -2,7 +2,7 @@ import React from "react"; import PropTypes from "prop-types"; import { Button, Modal, ModalHeader, ModalBody } from "reactstrap"; import { RiHeartPulseLine } from "react-icons/ri"; -import { MdDelete, MdFileDownload } from "react-icons/md"; +import { MdDelete, MdFileDownload, MdEdit } from "react-icons/md"; import { BsPeopleFill } from "react-icons/bs"; import { IconButton } from "@certego/certego-ui"; @@ -11,6 +11,11 @@ import { useAuthStore } from "../../../stores/useAuthStore"; import { useOrganizationStore } from "../../../stores/useOrganizationStore"; import { usePluginConfigurationStore } from "../../../stores/usePluginConfigurationStore"; import { SpinnerIcon } from "../../common/icon/icons"; +import { PlaybookConfigForm } from "../forms/PlaybookConfigForm"; +import { PivotConfigForm } from "../forms/PivotConfigForm"; +import { deletePluginConfig } from "../pluginsApi"; +import { PluginsTypes } from "../../../constants/pluginConst"; +import { AnalyzerConfigForm } from "../forms/AnalyzerConfigForm"; export function PluginHealthCheckButton({ pluginName, pluginType_ }) { const { checkPluginHealth } = usePluginConfigurationStore( @@ -30,7 +35,7 @@ export function PluginHealthCheckButton({ pluginName, pluginType_ }) { }; return ( -
+
state.user, [])); const { - noOrg, + isInOrganization, fetchAll: fetchAllOrganizations, isUserAdmin, } = useOrganizationStore( React.useCallback( (state) => ({ fetchAll: state.fetchAll, - noOrg: state.noOrg, + isInOrganization: state.isInOrganization, isUserAdmin: state.isUserAdmin, }), [], @@ -98,8 +103,10 @@ export function OrganizationPluginStateToggle({ refetch(); }; return ( -
- {!noOrg && ( +
+ {isInOrganization && ( ({ - deletePlaybook: state.deletePlaybook, - retrievePlaybooksConfiguration: state.retrievePlaybooksConfiguration, - }), - [], - ), - ); + const user = useAuthStore(React.useCallback((state) => state.user, [])); + const { isInOrganization, isUserAdmin } = useOrganizationStore( + React.useCallback( + (state) => ({ + fetchAll: state.fetchAll, + isInOrganization: state.isInOrganization, + isUserAdmin: state.isUserAdmin, + }), + [], + ), + ); + + const { + retrievePlaybooksConfiguration, + retrievePivotsConfiguration, + retrieveAnalyzersConfiguration, + } = usePluginConfigurationStore( + React.useCallback( + (state) => ({ + retrievePlaybooksConfiguration: state.retrievePlaybooksConfiguration, + retrievePivotsConfiguration: state.retrievePivotsConfiguration, + retrieveAnalyzersConfiguration: state.retrieveAnalyzersConfiguration, + }), + [], + ), + ); const onClick = async () => { try { - await deletePlaybook(playbookName); + await deletePluginConfig(pluginType_, pluginName); + if (pluginType_ === PluginsTypes.PLAYBOOK) + retrievePlaybooksConfiguration(); + if (pluginType_ === PluginsTypes.PIVOT) retrievePivotsConfiguration(); + if (pluginType_ === PluginsTypes.ANALYZER) + retrieveAnalyzersConfiguration(); setShowModal(false); - await retrievePlaybooksConfiguration(); } catch { - // handle error in deletePlaybook + // handle error in deletePlugin } }; + // disabled icon for all plugins except playbooks if the user is not an admin of the org or a superuser + const disabled = + pluginType_ !== "playbook" && + ((isInOrganization && !isUserAdmin(user.username)) || + (!isInOrganization && !user.is_staff)); + return ( -
+
setShowModal(true)} + disabled={disabled} titlePlacement="top" /> setShowModal(false)}> - Delete playbook + Delete plugin
- Do you want to delete the playbook:{" "} - {playbookName}? + Do you want to delete the plugin:{" "} + {pluginName}?
- -
- - {/* lateral menu with the type and description of each param */} - - {Object.keys(selectedPluginsParams) - .sort() - .map((key) => ( -
- {Object.keys(selectedPluginsParams[key]).length > 0 ? ( -
{key.toUpperCase()}:
- ) : ( -
- {key.toUpperCase()}:{" "} - null -
- )} - {Object.entries(selectedPluginsParams[key]).map( - ([name, params]) => ( -
-
{name}
- {Object.entries(params).length ? ( -
    - {Object.entries(params).map(([pName, pObj]) => ( -
  • - {pName} -   - ({pObj.type}) -
    - {markdownToHtml(pObj.description)} -
    -
  • - ))} -
- ) : ( - null - )} -
- ), - )} -
- ))} -
+ +
+ + +
); diff --git a/frontend/src/components/user/config/PluginData.jsx b/frontend/src/components/user/config/PluginData.jsx index 6dadc3c1..c27b84c3 100644 --- a/frontend/src/components/user/config/PluginData.jsx +++ b/frontend/src/components/user/config/PluginData.jsx @@ -91,19 +91,23 @@ export function PluginData({ analyzers, connectors, pivots, + ingestors, visualizers, retrieveAnalyzersConfiguration, retrieveConnectorsConfiguration, retrievePivotsConfiguration, + retrieveIngestorsConfiguration, retrieveVisualizersConfiguration, ] = usePluginConfigurationStore((state) => [ filterEmptyData(state.analyzers, dataName), filterEmptyData(state.connectors, dataName), filterEmptyData(state.pivots, dataName), + filterEmptyData(state.ingestors, dataName), filterEmptyData(state.visualizers, dataName), state.retrieveAnalyzersConfiguration, state.retrieveConnectorsConfiguration, state.retrievePivotsConfiguration, + state.retrieveIngestorsConfiguration, state.retrieveVisualizersConfiguration, ]); @@ -124,6 +128,8 @@ export function PluginData({ plugins = connectors; } else if (res.type === PluginTypesNumeric.VISUALIZER) { plugins = visualizers; + } else if (res.type === PluginTypesNumeric.INGESTOR) { + plugins = ingestors; } else if (res.type === PluginTypesNumeric.PIVOT) { plugins = pivots; } else { @@ -139,12 +145,13 @@ export function PluginData({ }, ); - // download the configs and again the analyzers with the update values + // download the configs and again the plugins with the update values const refetchAll = () => { refetchPluginData(); retrieveAnalyzersConfiguration(); retrieveConnectorsConfiguration(); retrievePivotsConfiguration(); + retrieveIngestorsConfiguration(); retrieveVisualizersConfiguration(); }; @@ -179,6 +186,10 @@ export function PluginData({ PluginTypesNumeric.VISUALIZER ) { plugins = visualizers; + } else if ( + configuration.type === PluginTypesNumeric.INGESTOR + ) { + plugins = ingestors; } else if ( configuration.type === PluginTypesNumeric.PIVOT ) { diff --git a/frontend/src/components/user/token/TokenAccess.jsx b/frontend/src/components/user/token/TokenAccess.jsx index c18c5855..5cf75bdb 100644 --- a/frontend/src/components/user/token/TokenAccess.jsx +++ b/frontend/src/components/user/token/TokenAccess.jsx @@ -56,9 +56,8 @@ export default function TokenAccess() { Note: This is an irreversible operation.

- Once deleted, you cannot use this API key to access - ThreatMatrix's API. However, you will be able to generate a new - one. + Once deleted, you cannot use this API key to access ThreatMatrix's + API. However, you will be able to generate a new one.

Are you sure you wish to proceed ?
diff --git a/frontend/src/components/user/token/TokenPage.jsx b/frontend/src/components/user/token/TokenPage.jsx index 3ec7dc3f..68e6a569 100644 --- a/frontend/src/components/user/token/TokenPage.jsx +++ b/frontend/src/components/user/token/TokenPage.jsx @@ -21,8 +21,8 @@ export default function TokenPage() { - You can generate an API key to access ThreatMatrix's RESTful - API. Take a look to the available Python and Go clients: + You can generate an API key to access ThreatMatrix's RESTful API. + Take a look to the available Python and Go clients: - - - - - Dashboard - - - - - - - History - - - - - - Plugins - - - - - - Scan - - - -); +import { useOrganizationStore } from "../stores/useOrganizationStore"; const guestLinks = ( <> @@ -96,6 +67,62 @@ const guestLinks = ( ); +// eslint-disable-next-line react/prop-types +function AuthLinks({ isInOrganization }) { + return ( + <> + + + + + Dashboard + + + + + + + History + + + + Plugins + + + + Plugins List + +
+ + User Plugin Config + + {isInOrganization && ( + + Organization Plugin Config + + )} +
+ + + + Scan + + + + ); +} + // eslint-disable-next-line react/prop-types function RightLinks({ handleClickStart, isAuthenticated }) { const location = useLocation(); @@ -224,6 +251,11 @@ function AppHeader() { React.useCallback((state) => state.isAuthenticated(), []), ); + // organization store + const isInOrganization = useOrganizationStore( + React.useCallback((state) => state.isInOrganization, []), + ); + return (
{/* top loading bar */} @@ -243,17 +275,23 @@ function AppHeader() { setIsOpen(!isOpen)} /> {/* Navbar Left Side */} -
From 90eee483a229d8ca78ff193514cf9b08314f05b1 Mon Sep 17 00:00:00 2001 From: NxPKG Date: Fri, 29 Nov 2024 10:15:14 +0600 Subject: [PATCH 20/21] Update compose-tests.yml (#183) * Update compose-tests.yml Signed-off-by: NxPKG * Update compose.yml Signed-off-by: NxPKG * Update compose-tests.yml Signed-off-by: NxPKG * Update compose.yml Signed-off-by: NxPKG * Update compose-tests.yml Signed-off-by: NxPKG * Update compose.yml Signed-off-by: NxPKG * Update compose.yml Signed-off-by: NxPKG * Update compose-tests.yml Signed-off-by: NxPKG * Update compose.yml Signed-off-by: NxPKG --------- Signed-off-by: NxPKG --- integrations/cyberchef/compose-tests.yml | 2 -- integrations/cyberchef/compose.yml | 2 -- integrations/malware_tools_analyzers/compose-tests.yml | 6 ------ integrations/malware_tools_analyzers/compose.yml | 6 ------ integrations/pcap_analyzers/compose-tests.yml | 2 -- integrations/pcap_analyzers/compose.yml | 6 ------ integrations/phoneinfoga/compose.yml | 4 +--- integrations/tor_analyzers/compose-tests.yml | 6 ------ integrations/tor_analyzers/compose.yml | 6 ------ 9 files changed, 1 insertion(+), 39 deletions(-) diff --git a/integrations/cyberchef/compose-tests.yml b/integrations/cyberchef/compose-tests.yml index 68b07226..4163e9a7 100644 --- a/integrations/cyberchef/compose-tests.yml +++ b/integrations/cyberchef/compose-tests.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: cyberchef-server: build: diff --git a/integrations/cyberchef/compose.yml b/integrations/cyberchef/compose.yml index 19f496ac..5d4be9eb 100644 --- a/integrations/cyberchef/compose.yml +++ b/integrations/cyberchef/compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: cyberchef-server: image: khulnasoft/threatmatrix_cyberchef:${REACT_APP_THREATMATRIX_VERSION} diff --git a/integrations/malware_tools_analyzers/compose-tests.yml b/integrations/malware_tools_analyzers/compose-tests.yml index 5523176f..269ae9df 100644 --- a/integrations/malware_tools_analyzers/compose-tests.yml +++ b/integrations/malware_tools_analyzers/compose-tests.yml @@ -1,9 +1,3 @@ -# IMPORTANT: The version must match the version of docker-compose.yml ---- -version: '3.8' - -# All additional integrations should be added following this format only. - services: malware_tools_analyzers: build: diff --git a/integrations/malware_tools_analyzers/compose.yml b/integrations/malware_tools_analyzers/compose.yml index 53fcf2bb..144b049d 100644 --- a/integrations/malware_tools_analyzers/compose.yml +++ b/integrations/malware_tools_analyzers/compose.yml @@ -1,9 +1,3 @@ -# IMPORTANT: The version must match the version of docker-compose.yml ---- -version: "3.8" - -# All additional integrations should be added following this format only. - services: malware_tools_analyzers: image: khulnasoft/threatmatrix_malware_tools_analyzers:${REACT_APP_THREATMATRIX_VERSION} diff --git a/integrations/pcap_analyzers/compose-tests.yml b/integrations/pcap_analyzers/compose-tests.yml index 8bb3ea0a..b5b2bae6 100644 --- a/integrations/pcap_analyzers/compose-tests.yml +++ b/integrations/pcap_analyzers/compose-tests.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: pcap_analyzers: build: diff --git a/integrations/pcap_analyzers/compose.yml b/integrations/pcap_analyzers/compose.yml index 9a1e58ac..b4605530 100644 --- a/integrations/pcap_analyzers/compose.yml +++ b/integrations/pcap_analyzers/compose.yml @@ -1,9 +1,3 @@ -# IMPORTANT: The version must match the version of docker-compose.yml ---- -version: "3.8" - -# All additional integrations should be added following this format only. - services: pcap_analyzers: image: khulnasoft/threatmatrix_pcap_analyzers:${REACT_APP_THREATMATRIX_VERSION} diff --git a/integrations/phoneinfoga/compose.yml b/integrations/phoneinfoga/compose.yml index 7016b56f..c82e9481 100644 --- a/integrations/phoneinfoga/compose.yml +++ b/integrations/phoneinfoga/compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: phoneinfoga: container_name: threatmatrix_phoneinfoga @@ -13,4 +11,4 @@ services: env_file: - env_file_integrations depends_on: - - uwsgi \ No newline at end of file + - uwsgi diff --git a/integrations/tor_analyzers/compose-tests.yml b/integrations/tor_analyzers/compose-tests.yml index 9c94bfb6..3caa1c69 100644 --- a/integrations/tor_analyzers/compose-tests.yml +++ b/integrations/tor_analyzers/compose-tests.yml @@ -1,9 +1,3 @@ -# IMPORTANT: The version must match the version of docker-compose.yml ---- -version: '3.8' - -# All additional integrations should be added following this format only. - services: tor_analyzers: build: diff --git a/integrations/tor_analyzers/compose.yml b/integrations/tor_analyzers/compose.yml index b5ac75a0..9adaccaa 100644 --- a/integrations/tor_analyzers/compose.yml +++ b/integrations/tor_analyzers/compose.yml @@ -1,9 +1,3 @@ -# IMPORTANT: The version must match the version of docker-compose.yml ---- -version: '3.8' - -# All additional integrations should be added following this format only. - services: tor_analyzers: image: khulnasoft/threatmatrix_tor_analyzers:${REACT_APP_THREATMATRIX_VERSION} From 66983d21833591400b8c030e4accc0bc3a513218 Mon Sep 17 00:00:00 2001 From: gitworkflows <118260833+gitworkflows@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:12:36 +0600 Subject: [PATCH 21/21] fixed dashboard backend unittest (#187) * fixed dashboard backend unittest * black format * fix isort * prettier:write fix * Update test_auth.py Signed-off-by: gitworkflows <118260833+gitworkflows@users.noreply.github.com> --------- Signed-off-by: gitworkflows <118260833+gitworkflows@users.noreply.github.com> --- .github/CHANGELOG.md | 111 ++-- .github/CONTRIBUTING.md | 2 +- .github/FUNDING.yml | 3 +- .github/dependabot.yml | 38 +- .github/pull_request_template.md | 16 +- .github/release_template.md | 12 +- .github/workflows/pull_request_automation.yml | 8 +- README.md | 68 ++- api_app/analyzers_manager/classes.py | 64 ++- .../file_analyzers/elf_info.py | 2 +- .../file_analyzers/pe_info.py | 2 +- .../file_analyzers/phishing/__init__.py | 0 .../phishing/phishing_form_compiler.py | 247 +++++++++ .../file_analyzers/yara_scan.py | 30 + .../migrations/0125_update_yara_repo.py | 16 +- ..._analyzer_config_phishing_form_compiler.py | 396 ++++++++++++++ ...0129_analyzer_config_phishing_extractor.py | 224 ++++++++ ...0131_analyzer_config_vt_sample_download.py | 34 ++ .../0132_analyzer_config_urldna_new_scan.py | 401 ++++++++++++++ .../0133_analyzer_config_urldna_search.py | 247 +++++++++ .../0134_analyzerconfig_mapping_data_model.py | 20 + .../migrations/0135_data_mapping.py | 58 ++ ...lyzerconfig_mapping_data_model_and_more.py | 189 +++++++ ...report_data_model_content_type_and_more.py | 34 ++ ..._analyzerreport_data_model_content_type.py | 29 + ...alter_analyzerconfig_mapping_data_model.py | 22 + api_app/analyzers_manager/models.py | 148 ++++- .../observable_analyzers/abuseipdb.py | 12 + .../observable_analyzers/crowdsec.py | 80 +++ .../observable_analyzers/greynoiseintel.py | 43 ++ .../observable_analyzers/maxmind.py | 26 + .../observable_analyzers/nvd_cve.py | 3 +- .../observable_analyzers/phishing/__init__.py | 0 .../phishing/phishing_extractor.py | 54 ++ .../observable_analyzers/talos.py | 16 + .../observable_analyzers/tor.py | 3 + .../observable_analyzers/urldna.py | 122 +++++ .../observable_analyzers/urlhaus.py | 6 + .../vt/vt3_sample_download.py | 33 ++ api_app/analyzers_manager/queryset.py | 8 + api_app/classes.py | 8 +- api_app/data_model_manager/__init__.py | 0 api_app/data_model_manager/admin.py | 61 +++ api_app/data_model_manager/apps.py | 5 + api_app/data_model_manager/enums.py | 23 + api_app/data_model_manager/fields.py | 19 + .../migrations/0001_initial.py | 354 ++++++++++++ ...02_domaindatamodel_resolutions_and_more.py | 38 ++ ...remove_ipdatamodel_ietf_report_and_more.py | 24 + ...ter_domaindatamodel_evaluation_and_more.py | 63 +++ ...ndatamodel_external_references_and_more.py | 141 +++++ .../data_model_manager/migrations/__init__.py | 0 api_app/data_model_manager/models.py | 206 +++++++ api_app/data_model_manager/queryset.py | 8 + api_app/data_model_manager/serializers.py | 49 ++ api_app/data_model_manager/signals.py | 0 api_app/data_model_manager/urls.py | 22 + api_app/data_model_manager/views.py | 30 + .../ingestors/virus_total.py | 12 +- api_app/investigations_manager/models.py | 10 +- api_app/migrations/0064_vt_sample_download.py | 53 ++ api_app/mixins.py | 20 +- api_app/models.py | 71 +-- api_app/pivots_manager/classes.py | 2 +- ...ivot_config_phishingextractortoanalysis.py | 156 ++++++ ...bmitdownloadedfile_loadfilesameplaybook.py | 52 ++ api_app/pivots_manager/pivots/any_compare.py | 6 +- api_app/pivots_manager/pivots/compare.py | 5 +- .../pivots/load_file_same_playbook.py | 15 + .../0054_playbook_config_phishinganalysis.py | 125 +++++ .../0055_playbook_config_phishingextractor.py | 126 +++++ .../migrations/0056_download_sample_vt.py | 38 ++ api_app/queryset.py | 21 +- api_app/serializers/elastic.py | 39 +- api_app/serializers/job.py | 27 +- api_app/urls.py | 3 +- api_app/views.py | 116 ++-- api_app/visualizers_manager/classes.py | 45 ++ .../migrations/0039_sample_download.py | 38 ++ .../visualizers_manager/visualizers/dns.py | 6 +- .../visualizers/sample_download.py | 88 +++ .../visualizers_manager/visualizers/yara.py | 2 +- .../plugin_report.json | 24 + configuration/ldap_config.py | 2 +- create_elastic_certs | 4 + docker/elasticsearch.override.yml | 3 +- docker/test.override.yml | 4 +- frontend/README.md | 2 +- frontend/package-lock.json | 78 +++ frontend/package.json | 1 + frontend/src/components/GuideWrapper.jsx | 4 +- .../components/common/form/TLPSelectInput.jsx | 2 +- .../src/components/dashboard/Dashboard.jsx | 56 +- frontend/src/components/dashboard/charts.jsx | 217 ++++++++ .../src/components/dashboard/utils/charts.jsx | 201 ------- frontend/src/components/home/Home.jsx | 27 +- .../jobs/result/bar/JobActionBar.jsx | 11 +- .../jobs/result/visualizer/elements/const.js | 1 + .../result/visualizer/elements/download.jsx | 87 +++ .../jobs/result/visualizer/validators.js | 42 +- .../jobs/result/visualizer/visualizer.jsx | 21 +- .../plugins/types/PluginWrapper.jsx | 2 +- frontend/src/constants/apiURLs.js | 17 +- frontend/src/constants/environment.js | 3 +- frontend/src/utils/files.js | 20 + .../components/dashboard/Dashboard.test.jsx | 27 + .../components/dashboard/charts.test.jsx | 516 ++++++++++++++++++ .../visualizer/elements/download.test.jsx | 117 ++++ .../jobs/result/visualizer/validators.test.js | 256 +++++---- .../result/visualizer/visualizer.test.jsx | 14 + frontend/tests/layouts/AppHeader.test.jsx | 6 +- frontend/tests/utils/files.test.js | 13 + integrations/__init__.py | 0 .../malware_tools_analyzers/Dockerfile | 4 +- .../malware_tools_analyzers/compose-tests.yml | 2 + .../malware_tools_analyzers/compose.yml | 2 + integrations/pcap_analyzers/compose.yml | 2 + integrations/phishing_analyzers/Dockerfile | 43 ++ integrations/phishing_analyzers/__init__.py | 0 .../phishing_analyzers/analyzers/__init__.py | 0 .../analyzers/driver_wrapper.py | 135 +++++ .../analyzers/extract_phishing_site.py | 83 +++ .../seleniumwire_request_serializer.py | 104 ++++ integrations/phishing_analyzers/app.py | 39 ++ .../phishing_analyzers/compose-tests.yml | 8 + integrations/phishing_analyzers/compose.yml | 39 ++ integrations/phishing_analyzers/entrypoint.sh | 14 + .../phishing_analyzers/requirements.txt | 5 + integrations/phoneinfoga/compose.yml | 2 +- integrations/tor_analyzers/compose-tests.yml | 2 + integrations/tor_analyzers/compose.yml | 2 + requirements/project-requirements.txt | 2 +- start | 6 +- tests/__init__.py | 40 +- .../observable_analyzers/test_nvd_cve.py | 94 +++- .../api_app/analyzers_manager/test_classes.py | 28 +- .../api_app/analyzers_manager/test_models.py | 141 ++++- tests/api_app/analyzers_manager/test_views.py | 2 +- .../connectors_manager/test_classes.py | 6 +- .../api_app/connectors_manager/test_views.py | 2 +- tests/api_app/data_model_manager/__init__.py | 0 .../api_app/data_model_manager/test_models.py | 10 + .../data_model_manager/test_serializers.py | 44 ++ .../api_app/data_model_manager/test_views.py | 114 ++++ .../investigations_manager/test_models.py | 4 +- .../investigations_manager/test_views.py | 6 + tests/api_app/pivots_manager/test_views.py | 14 +- tests/api_app/test_classes.py | 6 +- tests/api_app/test_mixins.py | 5 +- tests/api_app/test_models.py | 2 +- tests/api_app/test_serializers.py | 7 +- tests/api_app/test_views.py | 399 ++++++++++++-- tests/api_app/test_websocket.py | 21 +- .../passive_dns/test_analyzer_extractor.py | 14 +- .../visualizers_manager/test_classes.py | 69 +++ tests/test_crons.py | 8 +- tests/threat_matrix/test_tasks.py | 100 +++- threat_matrix/settings/__init__.py | 1 + threat_matrix/tasks.py | 26 +- 159 files changed, 7592 insertions(+), 797 deletions(-) create mode 100644 api_app/analyzers_manager/file_analyzers/phishing/__init__.py create mode 100644 api_app/analyzers_manager/file_analyzers/phishing/phishing_form_compiler.py create mode 100644 api_app/analyzers_manager/migrations/0128_analyzer_config_phishing_form_compiler.py create mode 100644 api_app/analyzers_manager/migrations/0129_analyzer_config_phishing_extractor.py create mode 100644 api_app/analyzers_manager/migrations/0131_analyzer_config_vt_sample_download.py create mode 100644 api_app/analyzers_manager/migrations/0132_analyzer_config_urldna_new_scan.py create mode 100644 api_app/analyzers_manager/migrations/0133_analyzer_config_urldna_search.py create mode 100644 api_app/analyzers_manager/migrations/0134_analyzerconfig_mapping_data_model.py create mode 100644 api_app/analyzers_manager/migrations/0135_data_mapping.py create mode 100644 api_app/analyzers_manager/migrations/0136_alter_analyzerconfig_mapping_data_model_and_more.py create mode 100644 api_app/analyzers_manager/migrations/0137_analyzerreport_data_model_content_type_and_more.py create mode 100644 api_app/analyzers_manager/migrations/0138_alter_analyzerreport_data_model_content_type.py create mode 100644 api_app/analyzers_manager/migrations/0139_alter_analyzerconfig_mapping_data_model.py create mode 100644 api_app/analyzers_manager/observable_analyzers/phishing/__init__.py create mode 100644 api_app/analyzers_manager/observable_analyzers/phishing/phishing_extractor.py create mode 100644 api_app/analyzers_manager/observable_analyzers/urldna.py create mode 100644 api_app/analyzers_manager/observable_analyzers/vt/vt3_sample_download.py create mode 100644 api_app/data_model_manager/__init__.py create mode 100644 api_app/data_model_manager/admin.py create mode 100644 api_app/data_model_manager/apps.py create mode 100644 api_app/data_model_manager/enums.py create mode 100644 api_app/data_model_manager/fields.py create mode 100644 api_app/data_model_manager/migrations/0001_initial.py create mode 100644 api_app/data_model_manager/migrations/0002_domaindatamodel_resolutions_and_more.py create mode 100644 api_app/data_model_manager/migrations/0003_remove_ipdatamodel_ietf_report_and_more.py create mode 100644 api_app/data_model_manager/migrations/0004_alter_domaindatamodel_evaluation_and_more.py create mode 100644 api_app/data_model_manager/migrations/0005_alter_domaindatamodel_external_references_and_more.py create mode 100644 api_app/data_model_manager/migrations/__init__.py create mode 100644 api_app/data_model_manager/models.py create mode 100644 api_app/data_model_manager/queryset.py create mode 100644 api_app/data_model_manager/serializers.py create mode 100644 api_app/data_model_manager/signals.py create mode 100644 api_app/data_model_manager/urls.py create mode 100644 api_app/data_model_manager/views.py create mode 100644 api_app/migrations/0064_vt_sample_download.py create mode 100644 api_app/pivots_manager/migrations/0035_pivot_config_phishingextractortoanalysis.py create mode 100644 api_app/pivots_manager/migrations/0036_alter_extractedonenotefiles_resubmitdownloadedfile_loadfilesameplaybook.py create mode 100644 api_app/pivots_manager/pivots/load_file_same_playbook.py create mode 100644 api_app/playbooks_manager/migrations/0054_playbook_config_phishinganalysis.py create mode 100644 api_app/playbooks_manager/migrations/0055_playbook_config_phishingextractor.py create mode 100644 api_app/playbooks_manager/migrations/0056_download_sample_vt.py create mode 100644 api_app/visualizers_manager/migrations/0039_sample_download.py create mode 100644 api_app/visualizers_manager/visualizers/sample_download.py create mode 100644 frontend/src/components/dashboard/charts.jsx delete mode 100644 frontend/src/components/dashboard/utils/charts.jsx create mode 100644 frontend/src/components/jobs/result/visualizer/elements/download.jsx create mode 100644 frontend/src/utils/files.js create mode 100644 frontend/tests/components/dashboard/Dashboard.test.jsx create mode 100644 frontend/tests/components/dashboard/charts.test.jsx create mode 100644 frontend/tests/components/jobs/result/visualizer/elements/download.test.jsx create mode 100644 frontend/tests/utils/files.test.js create mode 100644 integrations/__init__.py create mode 100644 integrations/phishing_analyzers/Dockerfile create mode 100644 integrations/phishing_analyzers/__init__.py create mode 100644 integrations/phishing_analyzers/analyzers/__init__.py create mode 100644 integrations/phishing_analyzers/analyzers/driver_wrapper.py create mode 100644 integrations/phishing_analyzers/analyzers/extract_phishing_site.py create mode 100644 integrations/phishing_analyzers/analyzers/seleniumwire_request_serializer.py create mode 100644 integrations/phishing_analyzers/app.py create mode 100644 integrations/phishing_analyzers/compose-tests.yml create mode 100644 integrations/phishing_analyzers/compose.yml create mode 100644 integrations/phishing_analyzers/entrypoint.sh create mode 100644 integrations/phishing_analyzers/requirements.txt create mode 100644 tests/api_app/data_model_manager/__init__.py create mode 100644 tests/api_app/data_model_manager/test_models.py create mode 100644 tests/api_app/data_model_manager/test_serializers.py create mode 100644 tests/api_app/data_model_manager/test_views.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index b2bebbdd..73b6ec66 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,6 +1,19 @@ # Changelog -[**Upgrade Guide**](https://khulnasoft.github.io/docs/ThreatMatrix/installation/#update-to-the-most-recent-version) +[**Upgrade Guide**](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/installation/#update-to-the-most-recent-version) + +## [v6.2.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.2.0) +#### TODO: DRAFT CHANGELOG + +### New releases schedule +From this release onwards, we are adopting a new schedule for future releases containing new features: expect a new release on every April and October (like Ubuntu :P). + +In this way we aim to provide constant support for the users and expected deadlines to get the new features from our project into the official releases. + +Please remember that you can always use the most recent features available in the development branch at anytime! See [this section](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/installation#get-the-experimental-features-in-the-develop-branch) for additional details. + +Obviously, as always, important bugs and fixes will be handled differently with dedicated patch releases. + ## [v6.1.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.1.0) This release merges all the developments performed by our Google Summer of Code contributors for this year. The program has just ended. You can read the related blogs for more info about: @@ -9,15 +22,15 @@ This release merges all the developments performed by our Google Summer of Code You'll get really tons of new analyzers this time to try out! -Plus we have a new official [documentation site](https://khulnasoft.github.io/docs/)! Please refer to this one from now onwards. +Plus we have a new official [documentation site](https://khulnasoft.github.io/devsec-docs/)! Please refer to this one from now onwards. -## [v6.1.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.1.0) +## [v6.0.4](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.0.4) Mostly adjusts and fixes with few new analyzers: Vulners and AILTypoSquatting Library. ## [v6.0.2](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.0.2) Major fixes and adjustments. We improved the documentation to help the transition to the new major version. -We added **Pivot** buttons to enable manual Pivoting from an Observable/File analysis to another. See [Doc](https://khulnasoft.github.io/docs/ThreatMatrix/usage/#pivots) for more info +We added **Pivot** buttons to enable manual Pivoting from an Observable/File analysis to another. See [Doc](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/usage/#pivots) for more info As usual, we add new plugins. This release brings the following new ones: * a complete **TakedownRequest** playbook to automate TakeDown requests for malicious domains @@ -31,7 +44,7 @@ Little fixes for the major. ## [v6.0.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.0.0) This major release is another important milestone for this project! We have been working hard to transform ThreatMatrix from a *Data Extraction Platform* to a complete *Investigation Platform*! -One of the most noticeable feature is the addition of the [**Investigation** framework](https://khulnasoft.github.io/docs/ThreatMatrix/usage/#investigations-framework)! +One of the most noticeable feature is the addition of the [**Investigation** framework](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/usage/#investigations-framework)! Thanks to the this new feature, analysts can leverage ThreatMatrix as the starting point of their "Investigations", register their findings, correlate the information found, and collaborate...all in a single place. @@ -43,7 +56,7 @@ You can also find us in [Fukuoka at the next FIRSTCON](https://www.first.org/con Many breaking changes have been introduced with this major release due to dependencies upgrades and architectural changes. -You can find more details in the [Upgrade Guide](https://khulnasoft.github.io/docs/ThreatMatrix/installation/#updating-to-600-from-a-5xx-version). Please read it and follow it carefully before upgrading your ThreatMatrix instance to this Major version. +You can find more details in the [Upgrade Guide](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/installation/#updating-to-600-from-a-5xx-version). Please read it and follow it carefully before upgrading your ThreatMatrix instance to this Major version. **New analyzers** @@ -71,7 +84,7 @@ The support for Docker Compose v1 has been dropped. Please upgrade to Docker Com The python `start.py` script is being replaced with a more light Bash script called `script` at the next Major version. Thanks to this change the installation requirements are a lot less than before and it should be easier to install and execute ThreatMatrix. Please start to use the new `start` script from now to avoid future issues. -For more information: [Installation docs](https://khulnasoft.github.io/docs/ThreatMatrix/installation/) +For more information: [Installation docs](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/installation/) ## [v5.2.2](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v5.2.2) @@ -79,8 +92,8 @@ This release has been done mainly to adjusts a broken database migration introdu **Main Improvements** * Added new analyzers for [DNS0](https://docs.dns0.eu/) PassiveDNS data -* Added the chance to collect metrics ([Business Intelligence](https://khulnasoft.github.io/docs/ThreatMatrix/advanced_configuration/#business-intelligence) regarding Plugins Usage and send it to an ElasticSearch instance. -* Added new buttons to test ["Healthcheck" and "Pull" operations](https://khulnasoft.github.io/docs/ThreatMatrix/usage/#special-plugins-operations) for each Plugin (A feature introduced in the previous version) +* Added the chance to collect metrics ([Business Intelligence](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/advanced_configuration/#business-intelligence) regarding Plugins Usage and send it to an ElasticSearch instance. +* Added new buttons to test ["Healthcheck" and "Pull" operations](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/usage/#special-plugins-operations) for each Plugin (A feature introduced in the previous version) **Other improvements** * Various generic fixes and adjustments in the GUI @@ -125,8 +138,8 @@ If you are interested in helping us setting up a public instance of ThreatMatrix **General improvements** * Added First Visit Guide * Improved the documentation with the goal to help the users to understand better how all the available Plugins work. -* For OpenCTI users having problems in integrating ThreatMatrix, now you can use a workaround: [doc](https://khulnasoft.github.io/docs/advanced_configuration/#opencti) -* A new organization role is available to better manage the org: `admin`. [Doc](https://khulnasoft.github.io/docs/usage/#organizations-and-user-management) +* For OpenCTI users having problems in integrating ThreatMatrix, now you can use a workaround: [doc](https://khulnasoft.github.io/devsec-docs/advanced_configuration/#opencti) +* A new organization role is available to better manage the org: `admin`. [Doc](https://khulnasoft.github.io/devsec-docs/usage/#organizations-and-user-management) * Improvements in the "Jobs History" table: now it shows executed Playbooks and file/observables types correctly. * We added a new "Pivot" section in the "Plugin" GUI for the new Plugin type introduced in the [v5.1.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v5.1.0) release. We added a new dedicated visualizer which allows the user to see when a Pivot has been executed in the "Job Result" page. We are still working on it and planning to add more documentation and GUI usability soon. * Improvements in the "Jobs Result" page: now playbooks are more relevant, warnings are shown next to errors, Raw JSON data has been moved next to the other raw data. @@ -157,7 +170,7 @@ With this release we announce our new official site created by [Abheek Tripathy] Feel free to check it out! Official [blog post here](https://khulnasoft.github.io/blogs/official_site_revamped)! **Important changes** -* We added a new type of Plugin called [Ingestor](https://khulnasoft.github.io/docs/usage/#ingestors). **Ingestors** allow to automatically insert IOC streams from outside sources to ThreatMatrix itself. +* We added a new type of Plugin called [Ingestor](https://khulnasoft.github.io/devsec-docs/usage/#ingestors). **Ingestors** allow to automatically insert IOC streams from outside sources to ThreatMatrix itself. * Visualizers are not connected anymore to Analyzers/Connectors. They are connected to a single Playbook instead. This allows the users to create and manage the Visualizers in an easier way. * We added the new **Pivot** framework in the backend which allows to connect jobs to each other and to _pivot_ from one indicator to another. This is the first step to give the chance to the users to create more broader and complex investigation in ThreatMatrix. The next step will be to add the Frontend changes that allows the user to fully leverage the framework @@ -199,7 +212,7 @@ This framework is extremely powerful and allows every user to customize the GUI That would speed the analysis of the results a lot if done correctly! -To aid in this process we added a lot of [documentation and some very simple pre-built analyzers that you can use as example](https://khulnasoft.github.io/docs/usage/#visualizers): +To aid in this process we added a lot of [documentation and some very simple pre-built analyzers that you can use as example](https://khulnasoft.github.io/devsec-docs/usage/#visualizers): Moreover this release anticipates other important crucial steps for ThreatMatrix: * On June 10th [Matteo Lodi](https://twitter.com/matte_lodi) and [Simone Berni](https://twitter.com/0ssig3no) are presenting ThreatMatrix at one of the most important Cyber Security events in Italy: [HackinBo](https://www.hackinbo.it/programma.php) @@ -209,12 +222,12 @@ This release was possible thanks to the effort put in place by [Certego](https:/ **Other important changes:** -We have done some big refactor changes that could make your application do not work as expected after this major upgrade. Please follow the the [migration guide](https://khulnasoft.github.io/docs/installation/#updating-to-5-0-0-from-a-4-x-x-version) before upgrading ThreatMatrix to the new major release. +We have done some big refactor changes that could make your application do not work as expected after this major upgrade. Please follow the the [migration guide](https://khulnasoft.github.io/devsec-docs/installation/#updating-to-5-0-0-from-a-4-x-x-version) before upgrading ThreatMatrix to the new major release. * We moved away from the old big `analyzer_config.json` which was storing all the base configuration of the Analyzers to a database model (we did the same for all the other plugins types too). This allows us to manage plugins creation/modification/deletion in a more reliable manner and via the Django Admin Interface. If you have created custom plugins and changed those `_config.json` file manually, you would need to re-create those custom plugins again from the Django Admin Interface. * We have REMOVED all the environment configuration that we deprecated with the v4.0.0 release and the script to migrate them. -* We have REMOVED/RENAMED all the analyzers that we deprecated during the v4 releases cycle plus some more (see [migration guide](https://khulnasoft.github.io/docs/installation/#updating-to-5-0-0-from-a-4-x-x-version)). You might need to change the analyzer names in your integrations. +* We have REMOVED/RENAMED all the analyzers that we deprecated during the v4 releases cycle plus some more (see [migration guide](https://khulnasoft.github.io/devsec-docs/installation/#updating-to-5-0-0-from-a-4-x-x-version)). You might need to change the analyzer names in your integrations. * We did a lot of code refactors here and there to remove some spaghetti code that was generated by the high amount of different contributors that we had during the recent years. This should be transparent for the user **Other added minor features** @@ -294,7 +307,7 @@ and restart ThreatMatrix. It should solve the permissions problem. * Fixed Cape Sandbox analyzer not working * Deprecated `ThreatMiner`, `SecurityTrails` and `Robtex` various analyzers and substituted with new versions. * Refactoring and features in preparation to add support for cluster deployments. -* Added a new advanced Documentation section [Advanced Configuration](https://khulnasoft.github.io/docs/advanced_configuration) +* Added a new advanced Documentation section [Advanced Configuration](https://khulnasoft.github.io/devsec-docs/advanced_configuration) * Added more support for Cloud Deployments (in particular AWS) * Other minor adjustments and fixes @@ -316,7 +329,7 @@ We added some improvements to handle recent Microsoft Office downloaders: **Deployments:** -We are preparing to add more support for production deployments. We added some [documentation](https://khulnasoft.github.io/docs/installation/) regarding: +We are preparing to add more support for production deployments. We added some [documentation](https://khulnasoft.github.io/devsec-docs/installation/) regarding: * Logrotate Configuration * Crontab Configuration @@ -375,7 +388,7 @@ If you love this project and you would like to help us, we would love to get you ## [v4.1.2](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v4.1.2) -This version mainly adds quality improvements to the recently released ["Playbook" feature](https://khulnasoft.github.io/docs/usage/#playbooks): +This version mainly adds quality improvements to the recently released ["Playbook" feature](https://khulnasoft.github.io/devsec-docs/usage/#playbooks): * Now it is possible to create a new Playbook easily thanks to a proper button in the GUI. In this way you can save your own Playbooks and repeat them. * Now Playbooks support the check of already existing similar analysis like normal analysis already do. This saves computational and analysts' time. @@ -414,12 +427,12 @@ I would like to thank them and all the mentors (@sp35, @eshaan7, @0ssigeno, @dro Looking forward for the Google Summer of Code 2023! **Time savers features** -- New Plugin Type to allow to easily replicate the same type of analysis without having to select and/or configure groups of analyzers/connectors every time: **Playbooks** ([docs reference](https://khulnasoft.github.io/docs/usage/#playbooks)) -- Default Plugins Parameters can be customized from the GUI and are defined at user/org level instead of globally ([docs reference](https://khulnasoft.github.io/docs/advanced_usage/#customize-analyzer-execution)) -- Plugins Secrets can now be managed from the GUI and are defined at user/org level instead of globally ([docs reference](https://khulnasoft.github.io/docs/installation/#deprecated-environment-configuration)) -- Organization admins can enable/disable analyzers for all the org ([docs reference](https://khulnasoft.github.io/docs/usage/#multi-tenancy)) -- Google Oauth authentication support ([docs reference](https://khulnasoft.github.io/docs/Advanced-Configuration.html#google-oauth2)) -- Added support for `extends` key to simplify Analyzer configuration and customization ([docs reference](https://khulnasoft.github.io/docs/usage/#analyzers-customization)) +- New Plugin Type to allow to easily replicate the same type of analysis without having to select and/or configure groups of analyzers/connectors every time: **Playbooks** ([docs reference](https://khulnasoft.github.io/devsec-docs/usage/#playbooks)) +- Default Plugins Parameters can be customized from the GUI and are defined at user/org level instead of globally ([docs reference](https://khulnasoft.github.io/devsec-docs/advanced_usage/#customize-analyzer-execution)) +- Plugins Secrets can now be managed from the GUI and are defined at user/org level instead of globally ([docs reference](https://khulnasoft.github.io/devsec-docs/installation/#deprecated-environment-configuration)) +- Organization admins can enable/disable analyzers for all the org ([docs reference](https://khulnasoft.github.io/devsec-docs/usage/#multi-tenancy)) +- Google Oauth authentication support ([docs reference](https://khulnasoft.github.io/devsec-docs/Advanced-Configuration.html#google-oauth2)) +- Added support for `extends` key to simplify Analyzer configuration and customization ([docs reference](https://khulnasoft.github.io/devsec-docs/usage/#analyzers-customization)) **Others** - Adjusted default time limits and configuration of some analyzers @@ -454,15 +467,15 @@ The overall user feeling should be drastically improved. We hope you'll enjoy th While developing the new GUI, our main goal was to at least provide the same features that were available before. Anyway, we had the chance to add some important features: -- A new way to manage users and their permissions: the "Organization" feature. Please refer to the [docs here](https://khulnasoft.github.io/docs/usage/#organizations-and-user-management). -- A notification mechanism was added. Please refer to the [docs here](https://khulnasoft.github.io/docs/usage/#notifications). +- A new way to manage users and their permissions: the "Organization" feature. Please refer to the [docs here](https://khulnasoft.github.io/devsec-docs/usage/#organizations-and-user-management). +- A notification mechanism was added. Please refer to the [docs here](https://khulnasoft.github.io/devsec-docs/usage/#notifications). - Now it is possible to do more advanced lookups through the Jobs History and have an overall better way to filter them. - A new "API Access/Sessions" section was added to facilitate the management of API tokens and User sessions. - Now it is possible to submit multiple observables / files at the same time. **RETROCOMPATIBILITY INFO AND HOW TO UPDATE** -Please refer to the [**Upgrade Guide**](https://khulnasoft.github.io/docs/installation/#update-and-re-build) +Please refer to the [**Upgrade Guide**](https://khulnasoft.github.io/devsec-docs/installation/#update-and-re-build) **New/Improved Analyzers:** - Added an analyzer which supports the new service provided for free by [The Honeynet Project](https://www.honeynet.org/2021/12/27/new-project-available-greedybear/): [GreedyBear](https://github.com/honeynet/GreedyBear) @@ -471,7 +484,7 @@ Please refer to the [**Upgrade Guide**](https://khulnasoft.github.io/docs/instal **Other:** -- improved and updated the overall documentation (in particular the [Contribute](https://khulnasoft.github.io/docs/contribute) section) to help the developers to start to work on the project +- improved and updated the overall documentation (in particular the [Contribute](https://khulnasoft.github.io/devsec-docs/contribute) section) to help the developers to start to work on the project - added DOCKER BUILDKIT, `--debug-build` and Watchman dependency to speed up development - now the Backend and the Frontend are respectively highly dependant from 2 new open source projects created by [Certego](https://www.certego.net/), [certego-saas](https://github.com/certego/certego-saas) and [certego-ui](https://github.com/certego/certego-ui). - a lot of dependencies upgrade, in particular in the new ReactJS Frontend. @@ -519,12 +532,12 @@ We are also moving forward to release the next major version (v4). We just need We are proud to announce two new sponsorships today! - [Milton Security](https://www.miltonsecurity.com?utm_source=threatmatrix) - - [LimaCharlie](https://limacharlie.io/blog/limacharlie-sponsors-threat-matrix/?utm_source=threatmatrix&utm_medium=banner) + - [LimaCharlie](https://limacharlie.io/blog/limacharlie-sponsors-intel-owl/?utm_source=threatmatrix&utm_medium=banner) If you are interested in helping the project through a donation, read [here](https://github.com/khulnasoft/ThreatMatrix/blob/master/.github/partnership_and_sponsors.md) how you can do it! **New/Improved Analyzers:** -- New [CyberChef](https://gchq.githuba.io/CyberChef/) Analyzer! Run your own recipes in ThreatMatrix! Check the [docs](https://khulnasoft.github.io/docs/advanced_usage/#cyberchef)! +- New [CyberChef](https://gchq.githuba.io/CyberChef/) Analyzer! Run your own recipes in ThreatMatrix! Check the [docs](https://khulnasoft.github.io/devsec-docs/advanced_usage/#cyberchef)! **Other:** - fixes: [#931](https://github.com/khulnasoft/ThreatMatrix/issues/931) @@ -551,17 +564,17 @@ If you are interested in helping the project through a donation, read [here](htt ## [v3.3.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v3.3.0) **Notes:** -- Added helper script that checks and installs [initial requirements](https://khulnasoft.github.io/docs/installation/#requirements). (`initialize.sh`) -- Added [RADIUS authentication support](https://khulnasoft.github.io/docs/advanced_configuration/#radius-authentication) +- Added helper script that checks and installs [initial requirements](https://khulnasoft.github.io/devsec-docs/installation/#requirements). (`initialize.sh`) +- Added [RADIUS authentication support](https://khulnasoft.github.io/devsec-docs/advanced_configuration/#radius-authentication) **New/Improved Analyzers:** -- Added a new optional [Docker Analyzer](https://khulnasoft.github.io/docs/advanced_usage/#optional-analyzers) running [Onionscan](https://github.com/s-rah/onionscan) +- Added a new optional [Docker Analyzer](https://khulnasoft.github.io/devsec-docs/advanced_usage/#optional-analyzers) running [Onionscan](https://github.com/s-rah/onionscan) - Added [CAPE Sandbox](https://capesandbox.com/) file analyzer - `Doc_Info` analyzer now runs [msodde](https://github.com/decalage2/oletools/wiki/msodde) together with `olevba` and `XMLMacroDeobfuscator` - `PE_Info` analyzer now calculates [impfuzzy](https://github.com/JPCERTCC/impfuzzy) and [dashicon](https://github.com/fr0gger/SuperPeHasher) hashes too. **Other:** -- Added option to run ElasticSearch/Kibana together with ThreatMatrix with option `--elastic`. Check the [doc here](https://khulnasoft.github.io/docs/advanced_configuration/#example-configuration) +- Added option to run ElasticSearch/Kibana together with ThreatMatrix with option `--elastic`. Check the [doc here](https://khulnasoft.github.io/devsec-docs/advanced_configuration/#example-configuration) - Security: Patched Django Critical Bug + Added Brute Force protection to the Admin page - Generic bug fixing and other maintenance work - Bump some python dependencies @@ -606,7 +619,7 @@ If you are interested in helping the project through a donation, read [here](htt **For ThreatMatrix Contributors** -We updated the documentation on how to [Contribute](https://khulnasoft.github.io/docs/contribute/#rules). Please read through them if interested in contributing in the project. +We updated the documentation on how to [Contribute](https://khulnasoft.github.io/devsec-docs/contribute/#rules). Please read through them if interested in contributing in the project. ## [v3.2.2](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v3.2.2) @@ -669,7 +682,7 @@ We updated the documentation on how to [Contribute](https://khulnasoft.github.io ## [v3.1.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v3.1.0) -> 🎉 We are glad to welcome [Tines](https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=threatmatrix) as a new sponsor for ThreatMatrix. Read everything about this partnership [in the Tines' blog](https://www.tines.com/blog/announcing-our-sponsorship-of-threat-matrix). +> 🎉 We are glad to welcome [Tines](https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=threatmatrix) as a new sponsor for ThreatMatrix. Read everything about this partnership [in the Tines' blog](https://www.tines.com/blog/announcing-our-sponsorship-of-intel-owl). **Notes:** @@ -705,7 +718,7 @@ This is a minor patch release. > Note: This is a major release with MANY breaking changes. > -> ✒️ [Link](https://www.honeynet.org/2021/09/13/threat-matrix-release-v3-0-0/) to the blogpost announcing the release and summary of top new features. +> ✒️ [Link](https://www.honeynet.org/2021/09/13/intel-owl-release-v3-0-0/) to the blogpost announcing the release and summary of top new features. > > 💻 GUI changes can be seen in action on the [demo](https://threatmatrixclient.firebaseapp.com/pages/connectors). @@ -717,12 +730,12 @@ This is a minor patch release. **Features:** - Plugins (analyzers/connectors) that are not properly configured will not run even if requested. They will be marked as disabled from the dropdown on the analysis form and as a bonus you can also see if and why a plugin is not configured on the GUI tables. -- Added `kill`, `retry` and `healthcheck` features to analyzers and connectors. See [Managing Analyzers and Connectors](https://khulnasoft.github.io/docs/usage/#special-plugins-operations). -- Standardized threat-sharing using Traffic Light Protocol or `TLP`, thereby deprecating the use of booleans `force_privacy`, `disable_external_analyzers` and `private`. See [TLP Support](https://khulnasoft.github.io/docs/usage/#tlp-support). This makes the analysis form much more easier to use than before. +- Added `kill`, `retry` and `healthcheck` features to analyzers and connectors. See [Managing Analyzers and Connectors](https://khulnasoft.github.io/devsec-docs/usage/#special-plugins-operations). +- Standardized threat-sharing using Traffic Light Protocol or `TLP`, thereby deprecating the use of booleans `force_privacy`, `disable_external_analyzers` and `private`. See [TLP Support](https://khulnasoft.github.io/devsec-docs/usage/#tlp-support). This makes the analysis form much more easier to use than before. **New class of plugins called _Connectors_:** -- Connectors are designed to run after every successful analysis which makes them suitable for automated threat-sharing. Built to support integration with other SIEM/SOAR projects specifically aimed at Threat Sharing Platforms. See [Available Connectors](https://khulnasoft.github.io/docs/usage/#available-connectors). +- Connectors are designed to run after every successful analysis which makes them suitable for automated threat-sharing. Built to support integration with other SIEM/SOAR projects specifically aimed at Threat Sharing Platforms. See [Available Connectors](https://khulnasoft.github.io/devsec-docs/usage/#available-connectors). - Newly added connectors for threat-sharing: - `MISP`: automatically creates an event on your MISP instance. - `OpenCTI`: automatically creates an observable and a linked report on your OpenCTI instance. @@ -733,7 +746,7 @@ This is a minor patch release. - The `additional_config_params` attribute was split into the following 3 individual attributes. - `config`: Includes common parameters - `queue` and `soft_time_limit`. - - `params`: Includes default value, datatype and description for each [Analyzer](https://khulnasoft.github.io/docs/usage/#analyzers-customization) or [Connector](https://khulnasoft.github.io/docs/usage/#connectors-customization) specific parameters that modify runtime behaviour. + - `params`: Includes default value, datatype and description for each [Analyzer](https://khulnasoft.github.io/devsec-docs/usage/#analyzers-customization) or [Connector](https://khulnasoft.github.io/devsec-docs/usage/#connectors-customization) specific parameters that modify runtime behaviour. - `secrets`: Includes analyzer or connector specific secrets (e.g. API Key) name along with the secret's description. All secrets are required. **New inbuilt analyzers/fixes to existing:** @@ -747,7 +760,7 @@ This is a minor patch release. - New `ClamAV` analyzer: scan files for viruses/malwares/trojans using [ClamAV antivirus engine](https://docs.clamav.net/). - Fixed `Tranco` Analyzer pointing to the wrong `python_module` - Removed `CirclePDNS` default value in `env_file_app_template` -- VirusTotal v3: New configuration options: `include_behaviour_summary` for behavioral analysis and `include_sigma_analyses` for sigma analysis report of the file. See [Customize Analyzers](https://khulnasoft.github.io/docs/advanced_usage/#customize-analyzer-execution). +- VirusTotal v3: New configuration options: `include_behaviour_summary` for behavioral analysis and `include_sigma_analyses` for sigma analysis report of the file. See [Customize Analyzers](https://khulnasoft.github.io/devsec-docs/advanced_usage/#customize-analyzer-execution). **REST API changes:** @@ -809,7 +822,7 @@ Then a lot of maintenance and overall project stability issues solved: - bumped new versions of a lot of dependencies - Improved "Installation" and "Contribute" documentation - added new badges to the README -- added `--django-server` [option](https://khulnasoft.github.io/docs/contribute/#how-to-start) to speed up development +- added `--django-server` [option](https://khulnasoft.github.io/devsec-docs/contribute/#how-to-start) to speed up development - analyzed files are now correctly deleted with the periodic cronjob - other little refactors and fixes @@ -888,25 +901,25 @@ We changed `docker-compose` file names for optional analyzers. In the `v.2.0.0` - moved docker and docker-compose files under `docker/` folder. - users upgrading from previous versions need to manually move `env_file_app`, `env_file_postgres` and `env_file_integrations` files under `docker/`. -- users are to use the new [start.py](https://khulnasoft.github.io/docs/installation/#run) method to build or start ThreatMatrix containers +- users are to use the new [start.py](https://khulnasoft.github.io/devsec-docs/installation/#run) method to build or start ThreatMatrix containers - moved the following analyzers together in a specific optional docker container named `static_analyzers`. - [`Capa`](https://github.com/fireeye/capa) - [`PeFrame`](https://github.com/guelfoweb/peframe) - `Strings_Info_Classic` (based on [flarestrings](https://github.com/fireeye/stringsifter)) - `Strings_Info_ML` (based on [stringsifter](https://github.com/fireeye/stringsifter)) -Please see [docs](https://khulnasoft.github.io/docs/advanced_usage/#optional-analyzers) to understand how to enable these optional analyzers +Please see [docs](https://khulnasoft.github.io/devsec-docs/advanced_usage/#optional-analyzers) to understand how to enable these optional analyzers **NEW INBUILT ANALYZERS:** -- added [Qiling](https://github.com/qilingframework/qiling) file analyzer. This is an optional analyzer (see [docs](https://khulnasoft.github.io/docs/advanced_usage.html#optional-analyzers) to understand how to activate it). +- added [Qiling](https://github.com/qilingframework/qiling) file analyzer. This is an optional analyzer (see [docs](https://khulnasoft.github.io/devsec-docs/advanced_usage.html#optional-analyzers) to understand how to activate it). - added [Stratosphere blacklists](https://www.stratosphereips.org/attacker-ip-prioritization-blacklist) analyzer - added [FireEye Red Team Tool Countermeasures](https://github.com/fireeye/red_team_tool_countermeasures) Yara rules analyzer - added [emailrep.io](https://emailrep.io/) analyzer - added [Triage](https://tria.ge) analyzer for observables (`search` API) - added [InQuest](https://labs.inquest.net) analyzer - added [WiGLE](api.wigle.net) analyzer -- new analyzers were added to the `static_analyzers` optional docker container (see [docs](https://khulnasoft.github.io/docs/advanced_usage/#optional-analyzers) to understand how to activate it). +- new analyzers were added to the `static_analyzers` optional docker container (see [docs](https://khulnasoft.github.io/devsec-docs/advanced_usage/#optional-analyzers) to understand how to activate it). - [`FireEye Floss`](https://github.com/fireeye/flare-floss) strings analysis. - [`Manalyze`](https://github.com/JusticeRage/Manalyze) file analyzer @@ -914,7 +927,7 @@ Please see [docs](https://khulnasoft.github.io/docs/advanced_usage/#optional-ana - upgraded main Dockerfile to python 3.8 - added support for the `generic` observable type. In this way it is possible to build analyzers that can analyze everything and not only IPs, domains, URLs or hashes -- added [Multi-queue](https://khulnasoft.github.io/docs/advanced_configuration/#multi-queue) option to optimize usage of Celery queues. This is intended for advanced users. +- added [Multi-queue](https://khulnasoft.github.io/devsec-docs/advanced_configuration/#multi-queue) option to optimize usage of Celery queues. This is intended for advanced users. - updated GUI to new [ThreatMatrix-ng](https://github.com/khulnasoft/ThreatMatrix-ng/releases/tag/v1.7.0) version - upgraded [Speakeasy](https://github.com/fireeye/speakeasy), [Quark-Engine](https://github.com/quark-engine/quark-engine) and [Dnstwist](https://github.com/elceef/dnstwist) analyzers to last versions - moved from Travis CI to Github CI @@ -1045,7 +1058,7 @@ Patch after **v1.5.0**. **Breaking Changes:** -- Moved `ldap_config.py` under `configuration/` directory. If you were using LDAP before this release, please refer the [updated docs](https://khulnasoft.github.io/docs/advanced_configuration/#ldap). +- Moved `ldap_config.py` under `configuration/` directory. If you were using LDAP before this release, please refer the [updated docs](https://khulnasoft.github.io/devsec-docs/advanced_configuration/#ldap). **Fixes:** diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4c5c3812..6cb983af 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1 +1 @@ -Please refer to https://khulnasoft.github.io/docs/ThreatMatrix/contribute/ +Please refer to https://khulnasoft.github.io/devsec-docs/ThreatMatrix/contribute/ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index afe74a8c..c7a8c847 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ -patreon: khulnasoft \ No newline at end of file +open_collective: threatmatrix-project +github: khulnasoft \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e6b901c5..a0394741 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,7 +19,7 @@ updates: schedule: interval: "weekly" day: "tuesday" - target-branch: "dependabot-validation" + target-branch: "develop" ignore: # ignore all patch updates since we are using ~= # this does not work for security updates @@ -31,7 +31,7 @@ updates: schedule: interval: "weekly" day: "tuesday" - target-branch: "dependabot-validation" + target-branch: "develop" ignore: # ignore all patch updates since we are using ~= # this does not work for security updates @@ -43,7 +43,19 @@ updates: schedule: interval: "weekly" day: "tuesday" - target-branch: "dependabot-validation" + target-branch: "develop" + ignore: + # ignore all patch updates since we are using ~= + # this does not work for security updates + - dependency-name: "*" + update-types: [ "version-update:semver-patch" ] + + - package-ecosystem: "pip" + directory: "/integrations/phishing_analyzers" + schedule: + interval: "weekly" + day: "tuesday" + target-branch: "develop" ignore: # ignore all patch updates since we are using ~= # this does not work for security updates @@ -76,7 +88,7 @@ updates: schedule: interval: "weekly" day: "tuesday" - target-branch: "dependabot-validation" + target-branch: "develop" ignore: # ignore all patch updates since we are using ~= # this does not work for security updates @@ -88,7 +100,7 @@ updates: schedule: interval: "weekly" day: "tuesday" - target-branch: "dependabot-validation" + target-branch: "develop" ignore: # ignore all patch updates since we are using ~= # this does not work for security updates @@ -100,7 +112,7 @@ updates: schedule: interval: "weekly" day: "tuesday" - target-branch: "dependabot-validation" + target-branch: "develop" ignore: # ignore all patch updates since we are using ~= # this does not work for security updates @@ -112,7 +124,19 @@ updates: schedule: interval: "weekly" day: "tuesday" - target-branch: "dependabot-validation" + target-branch: "develop" + ignore: + # ignore all patch updates since we are using ~= + # this does not work for security updates + - dependency-name: "*" + update-types: ["version-update:semver-patch"] + + - package-ecosystem: "docker" + directory: "/integrations/phishing_analyzers" + schedule: + interval: "weekly" + day: "tuesday" + target-branch: "develop" ignore: # ignore all patch updates since we are using ~= # this does not work for security updates diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b1365385..d22f3c35 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,22 +14,22 @@ Please delete options that are not relevant. # Checklist -- [ ] I have read and understood the rules about [how to Contribute](https://khulnasoft.github.io/docs/ThreatMatrix/contribute/) to this project +- [ ] I have read and understood the rules about [how to Contribute](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/contribute/) to this project - [ ] The pull request is for the branch `develop` - [ ] A new plugin (analyzer, connector, visualizer, playbook, pivot or ingestor) was added or changed, in which case: - - [ ] I strictly followed the documentation ["How to create a Plugin"](https://khulnasoft.github.io/docs/ThreatMatrix/contribute/#how-to-add-a-new-plugin) - - [ ] [Usage](https://github.com/khulnasoft/docs/blob/main/docs/ThreatMatrix/usage.md) file was updated. - - [ ] [Advanced-Usage](https://github.com/khulnasoft/docs/blob/main/docs/ThreatMatrix/advanced_usage.md) was updated (in case the plugin provides additional optional configuration). - - [ ] I have dumped the configuration from Django Admin using the `dumpplugin` command and added it in the project as a data migration. (["How to share a plugin with the community"](https://khulnasoft.github.io/docs/ThreatMatrix/contribute/#how-to-share-your-plugin-with-the-community)) + - [ ] I strictly followed the documentation ["How to create a Plugin"](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/contribute/#how-to-add-a-new-plugin) + - [ ] [Usage](https://github.com/khulnasoft/docs/blob/main/docs/ThreatMatrix/usage.md) file was updated. A link to the PR to the [docs](https://github.com/khulnasoft/docs) repo has been added as a comment here. + - [ ] [Advanced-Usage](https://github.com/khulnasoft/docs/blob/main/docs/ThreatMatrix/advanced_usage.md) was updated (in case the plugin provides additional optional configuration). A link to the PR to the [docs](https://github.com/khulnasoft/docs) repo has been added as a comment here. + - [ ] I have dumped the configuration from Django Admin using the `dumpplugin` command and added it in the project as a data migration. (["How to share a plugin with the community"](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/contribute/#how-to-share-your-plugin-with-the-community)) - [ ] If a File analyzer was added and it supports a mimetype which is not already supported, you added a sample of that type inside the archive `test_files.zip` and you added the default tests for that mimetype in [test_classes.py](https://github.com/khulnasoft/ThreatMatrix/blob/master/tests/api_app/analyzers_manager/test_classes.py). - - [ ] If you created a new analyzer and it is free (does not require any API key), please add it in the `FREE_TO_USE_ANALYZERS` playbook by following [this guide](https://khulnasoft.github.io/docs/ThreatMatrix/contribute/#how-to-modify-a-plugin). - - [ ] Check if it could make sense to add that analyzer/connector to other [freely available playbooks](https://khulnasoft.github.io/docs/ThreatMatrix/usage/#list-of-pre-built-playbooks). + - [ ] If you created a new analyzer and it is free (does not require any API key), please add it in the `FREE_TO_USE_ANALYZERS` playbook by following [this guide](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/contribute/#how-to-modify-a-plugin). + - [ ] Check if it could make sense to add that analyzer/connector to other [freely available playbooks](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/usage/#list-of-pre-built-playbooks). - [ ] I have provided the resulting raw JSON of a finished analysis and a screenshot of the results. - [ ] If the plugin interacts with an external service, I have created an attribute called precisely `url` that contains this information. This is required for Health Checks. - [ ] If the plugin requires mocked testing, `_monkeypatch()` was used in its class to apply the necessary decorators. - [ ] I have added that raw JSON sample to the `MockUpResponse` of the `_monkeypatch()` method. This serves us to provide a valid sample for testing. - [ ] If external libraries/packages with restrictive licenses were used, they were added in the [Legal Notice](https://github.com/certego/ThreatMatrix/blob/master/.github/legal_notice.md) section. -- [ ] Linters (`Black`, `Flake`, `Isort`) gave 0 errors. If you have correctly installed [pre-commit](https://khulnasoft.github.io/docs/ThreatMatrix/contribute/#how-to-start-setup-project-and-development-instance), it does these checks and adjustments on your behalf. +- [ ] Linters (`Black`, `Flake`, `Isort`) gave 0 errors. If you have correctly installed [pre-commit](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/contribute/#how-to-start-setup-project-and-development-instance), it does these checks and adjustments on your behalf. - [ ] I have added tests for the feature/bug I solved (see `tests` folder). All the tests (new and old ones) gave 0 errors. - [ ] If the GUI has been modified: - [ ] I have a provided a screenshot of the result in the PR. diff --git a/.github/release_template.md b/.github/release_template.md index 15765b79..38cb38d3 100644 --- a/.github/release_template.md +++ b/.github/release_template.md @@ -1,10 +1,11 @@ # Checklist for creating a new release -- [ ] (optional) If we changed/added Docker Analyzers, we need to configure Docker Hub / Dependabot properly. -- [ ] Update `CHANGELOG.md` for the new version +- [ ] If we changed/added Docker Analyzers, we need to configure Docker Hub / Dependabot properly. +- [ ] I have already checked if all Dependabot issues have been solved before creating this PR. +- [ ] Update `CHANGELOG.md` for the new version. Tag another maintainer to review the Changelog and wait for their feedback. - [ ] Change version number `docker/.env` - [ ] Verify CI Tests -- [ ] Create release for the branch `develop`. +- [ ] Create release for the branch `develop`. Remember to prepend a `v` to the version number. Write the following statement there (change the version number): ```commandline @@ -16,7 +17,8 @@ WARNING: The release will be live within an hour! - [ ] Wait for [dockerHub](https://hub.docker.com/repository/docker/khulnasoft/threatmatrix) to finish the builds - [ ] Merge the PR to the `master` branch. **Note:** Only use "Merge and commit" as the merge strategy and not "Squash and merge". Using "Squash and merge" makes history between branches misaligned. - [ ] Remove the "wait" statement in the release description. -- [ ] Publish new Post into official Twitter and LinkedIn accounts: +- [ ] Publish new Post into official Twitter and LinkedIn accounts (change the version number): ```commandline published #ThreatMatrix vX.X.X! https://github.com/khulnasoft/ThreatMatrix/releases/tag/vX.X.X #ThreatIntelligence #CyberSecurity #OpenSource #OSINT #DFIR -``` \ No newline at end of file +``` +- [ ] If that was a major release or an important release, communicate the news to the marketing staff \ No newline at end of file diff --git a/.github/workflows/pull_request_automation.yml b/.github/workflows/pull_request_automation.yml index a588a896..cfa61307 100644 --- a/.github/workflows/pull_request_automation.yml +++ b/.github/workflows/pull_request_automation.yml @@ -87,12 +87,10 @@ jobs: BUILDKIT_PROGRESS: "plain" STAGE: "ci" REPO_DOWNLOADER_ENABLED: false - - - name: Run Startup Script (Master Branch Only) - if: github.base_ref == 'master' + + - name: Startup script launch (Fast) + if: "!contains(github.base_ref, 'master')" run: | - echo "Starting CI build on 'master' branch" - set -e # Exit on error ./start ci up -- --build -d env: DOCKER_BUILDKIT: 1 diff --git a/README.md b/README.md index f248df96..4abe4ca8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Threat Matrix +Threat Matrix [![GitHub release (latest by date)](https://img.shields.io/github/v/release/khulnasoft/ThreatMatrix)](https://github.com/khulnasoft/ThreatMatrix/releases) [![GitHub Repo stars](https://img.shields.io/github/stars/khulnasoft/ThreatMatrix?style=social)](https://github.com/khulnasoft/ThreatMatrix/stargazers) @@ -17,7 +17,7 @@ [![DeepSource](https://app.deepsource.com/gh/khulnasoft/ThreatMatrix.svg/?label=resolved+issues&token=BSvKHrnk875Y0Bykb79GNo8w)](https://app.deepsource.com/gh/khulnasoft/ThreatMatrix/?ref=repository-badge) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/khulnasoft/ThreatMatrix/badge)](https://api.securityscorecards.dev/projects/github.com/khulnasoft/ThreatMatrix) [![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7120/badge)](https://bestpractices.coreinfrastructure.org/projects/7120) -[![Documentation Status](https://readthedocs.org/projects/threatmatrix/badge/?version=latest)](https://threatmatrix.readthedocs.io/en/latest/?badge=latest) +# Threat Matrix Do you want to get **threat intelligence data** about a malware, an IP address or a domain? Do you want to get this kind of data from multiple sources at the same time using **a single API request**? @@ -26,56 +26,56 @@ You are in the right place! ThreatMatrix is an Open Source solution for management of Threat Intelligence at scale. It integrates a number of analyzers available online and a lot of cutting-edge malware analysis tools. ### Features - This application is built to **scale out** and to **speed up the retrieval of threat info**. It provides: - - **Enrichment of Threat Intel** for files as well as observables (IP, Domain, URL, hash, etc). - A Fully-fledged REST APIs written in Django and Python. - An easy way to be integrated in your stack of security tools to automate common jobs usually performed, for instance, by SOC analysts manually. (Thanks to the official libraries [pythreatmatrix](https://github.com/khulnasoft/pythreatmatrix) and [go-threatmatrix](https://github.com/khulnasoft/go-threatmatrix)) - A **built-in GUI**: provides features such as dashboard, visualizations of analysis data, easy to use forms for requesting new analysis, etc. - A **framework** composed of modular components called **Plugins**: - - _analyzers_ that can be run to either retrieve data from external sources (like VirusTotal or AbuseIPDB) or to generate intel from internally available tools (like Yara or Oletools) - - _connectors_ that can be run to export data to external platforms (like MISP or OpenCTI) - - _pivots_ that are designed to trigger the execution of a chain of analysis and connect them to each other - - _visualizers_ that are designed to create custom visualizations of analyzers results - - _ingestors_ that allows to automatically ingest stream of observables or files to ThreatMatrix itself - - _playbooks_ that are meant to make analysis easily repeatable + - *analyzers* that can be run to either retrieve data from external sources (like VirusTotal or AbuseIPDB) or to generate intel from internally available tools (like Yara or Oletools) + - *connectors* that can be run to export data to external platforms (like MISP or OpenCTI) + - *pivots* that are designed to trigger the execution of a chain of analysis and connect them to each other + - *visualizers* that are designed to create custom visualizations of analyzers results in the GUI + - *ingestors* that allow to automatically ingest stream of observables or files to ThreatMatrix itself + - *playbooks* that are meant to make analysis easily repeatable + - *data models* to map the different data extracted from analyzers to a single common schema +- A starting point for analysts' **Investigations**: users can register their findings, correlate the information found, and collaborate...all in a single place -### Documentation +### Documentation We try hard to keep our documentation well written, easy to understand and always updated. -All info about installation, usage, configuration and contribution can be found [here](https://threatmatrix.readthedocs.io/) +All info about installation, usage, configuration and contribution can be found [here](https://khulnasoft.github.io/devsec-docs/) ### Publications and Media -To know more about the project and its growth over time, you may be interested in reading [the official blog posts and/or videos about the project by clicking on this link](https://threatmatrix.readthedocs.io/en/latest/Introduction.html#publications-and-media) +To know more about the project and its growth over time, you may be interested in reading [the official blog posts and/or videos about the project by clicking on this link](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/introduction/#publications-and-media) ### Available services or analyzers -You can see the full list of all available analyzers in the [documentation](https://threatmatrix.readthedocs.io/en/latest/Usage.html#available-analyzers). +You can see the full list of all available analyzers in the [documentation](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/usage/#analyzers). -| Type | Analyzers Available | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Inbuilt modules | - Static Office Document, RTF, PDF, PE File Analysis and metadata extraction
- Strings Deobfuscation and analysis ([FLOSS](https://github.com/mandiant/flare-floss), [Stringsifter](https://github.com/mandiant/stringsifter), ...)
- PE Emulation with [Qiling](https://github.com/qilingframework/qiling) and [Speakeasy](https://github.com/mandiant/speakeasy)
- PE Signature verification
- PE Capabilities Extraction ([CAPA](https://github.com/mandiant/capa))
- Javascript Emulation ([Box-js](https://github.com/CapacitorSet/box-js))
- Android Malware Analysis ([Quark-Engine](https://github.com/quark-engine/quark-engine), ...)
- SPF and DMARC Validator
- Yara (a lot of public rules are available. You can also add your own rules)
- more... | -| External services | - Abuse.ch MalwareBazaar/URLhaus/Threatfox/YARAify
- GreyNoise v2
- Intezer
- VirusTotal v3
- Crowdsec
- URLscan
- Shodan
- AlienVault OTX
- Intelligence_X
- MISP
- many more.. | +| Type | Analyzers Available | +| -------------------------------------------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Inbuilt modules | - Static Office Document, RTF, PDF, PE File Analysis and metadata extraction
- Strings Deobfuscation and analysis ([FLOSS](https://github.com/mandiant/flare-floss), [Stringsifter](https://github.com/mandiant/stringsifter), ...)
- PE Emulation with [Qiling](https://github.com/qilingframework/qiling) and [Speakeasy](https://github.com/mandiant/speakeasy)
- PE Signature verification
- PE Capabilities Extraction ([CAPA](https://github.com/mandiant/capa))
- Javascript Emulation ([Box-js](https://github.com/CapacitorSet/box-js))
- Android Malware Analysis ([Quark-Engine](https://github.com/quark-engine/quark-engine), ...)
- SPF and DMARC Validator
- Yara (a lot of public rules are available. You can also add your own rules)
- more... | +| External services | - Abuse.ch MalwareBazaar/URLhaus/Threatfox/YARAify
- GreyNoise v2
- Intezer
- VirusTotal v3
- Crowdsec
- URLscan
- Shodan
- AlienVault OTX
- Intelligence_X
- MISP
- many more.. | ## Partnerships and sponsors As open source project maintainers, we strongly rely on external support to get the resources and time to work on keeping the project alive, with a constant release of new features, bug fixes and general improvements. -Because of this, we joined [Open Collective](https://opencollective.com/khulnasoft) to obtain non-profit equal level status which allows the organization to receive and manage donations transparently. Please support ThreatMatrix and all the community by choosing a plan (BRONZE, SILVER, etc). +Because of this, we joined [Open Collective](https://opencollective.com/threatmatrix-project) to obtain non-profit equal level status which allows the organization to receive and manage donations transparently. Please support ThreatMatrix and all the community by choosing a plan (BRONZE, SILVER, etc). - - + + ### 🥇 GOLD #### Certego - Certego Logo + Certego Logo [Certego](https://certego.net/?utm_source=threatmatrix) is a MDR (Managed Detection and Response) and Threat Intelligence Provider based in Italy. @@ -83,37 +83,45 @@ ThreatMatrix was born out of Certego's Threat intelligence R&D division and is c #### The Honeynet Project - Honeynet.org logo + Honeynet.org logo [The Honeynet Project](https://www.honeynet.org) is a non-profit organization working on creating open source cyber security tools and sharing knowledge about cyber threats. Thanks to Honeynet, we are hosting a public demo of the application [here](https://threatmatrix.honeynet.org). If you are interested, please contact a member of Honeynet to get access to the public service. #### Google Summer of Code - - GSoC logo + GSoC logo Since its birth this project has been participating in the [Google Summer of Code](https://summerofcode.withgoogle.com/) (GSoC)! If you are interested in participating in the next Google Summer of Code, check all the info available in the [dedicated repository](https://github.com/khulnasoft/gsoc)! + ### 🥈 SILVER #### ThreatHunter.ai - ThreatHunter.ai logo + ThreatHunter.ai logo [ThreatHunter.ai®](https://threathunter.ai?utm_source=threatmatrix), is a 100% Service-Disabled Veteran-Owned Small Business started in 2007 under the name Milton Security Group. ThreatHunter.ai is the global leader in Dynamic Threat Hunting. Operating a true 24x7x365 Security Operation Center with AI/ML-enhanced human Threat Hunters, ThreatHunter.ai has changed the industry in how threats are found, and mitigated in real time. For over 15 years, our teams of Threat Hunters have stopped hundreds of thousands of threats and assisted organizations in defending against threat actors around the clock. +### 🥉 BRONZE + #### Docker In 2021 ThreatMatrix joined the official [Docker Open Source Program](https://www.docker.com/blog/expanded-support-for-open-source-software-projects/). This allows ThreatMatrix developers to easily manage Docker images and focus on writing the code. You may find the official ThreatMatrix Docker images [here](https://hub.docker.com/search?q=khulnasoft). +#### DigitalOcean + +In 2022 ThreatMatrix joined the official [DigitalOcean Open Source Program](https://www.digitalocean.com/open-source?utm_medium=opensource&utm_source=ThreatMatrix). + + ## About the author and maintainers Feel free to contact the main developers at any time on Twitter: -- [KhulnaSoft DevSec](https://twitter.com/khulnasoft): Author and principal maintainer -- [Nx PKG](https://github.com/nxpkg): Backend Maintainer -- [KhulnaSoft Lab](https://github.com/khulnasoft-lab): Frontend Maintainer -- [KhulnaSoft BOT](https://github.com/khulnasoft-bot): Key Contributor +- [Matteo Lodi](https://twitter.com/matte_lodi): Author, Advisor and Administrator +- [Daniele Rosetti](https://github.com/drosetti): Administrator and Frontend Maintainer +- [Simone Berni](https://twitter.com/0ssig3no): Backend Maintainer +- [Federico Gibertoni](https://x.com/fgibertoni1): Maintainer and Community Assistant +- [Eshaan Bansal](https://twitter.com/eshaan7_): Key Contributor \ No newline at end of file diff --git a/api_app/analyzers_manager/classes.py b/api_app/analyzers_manager/classes.py index 836dacab..1c618601 100644 --- a/api_app/analyzers_manager/classes.py +++ b/api_app/analyzers_manager/classes.py @@ -35,6 +35,56 @@ class BaseAnalyzerMixin(Plugin, metaclass=ABCMeta): ObservableTypes = ObservableTypes TypeChoices = TypeChoices + MALICIOUS_EVALUATION = 75 + SUSPICIOUS_EVALUATION = 35 + FALSE_POSITIVE = -50 + + def threat_to_evaluation(self, threat_level): + # MAGIC NUMBERS HERE!!! + # I know, it should be 25-50-75-100. We raised it a bit because too many false positives were generated + self.report: AnalyzerReport + if threat_level >= self.MALICIOUS_EVALUATION: + evaluation = self.report.data_model_class.EVALUATIONS.MALICIOUS.value + elif threat_level >= self.SUSPICIOUS_EVALUATION: + evaluation = self.report.data_model_class.EVALUATIONS.SUSPICIOUS.value + elif threat_level <= self.FALSE_POSITIVE: + evaluation = self.report.data_model_class.EVALUATIONS.TRUSTED.value + else: + evaluation = self.report.data_model_class.EVALUATIONS.CLEAN.value + return evaluation + + def _do_create_data_model(self) -> bool: + if self.report.job.observable_classification == ObservableTypes.GENERIC: + return False + if ( + not self._config.mapping_data_model + and self.__class__._create_data_model_mtm + == BaseAnalyzerMixin._create_data_model_mtm + and self.__class__._update_data_model + == BaseAnalyzerMixin._update_data_model + ): + return False + return True + + def _create_data_model_mtm(self): + return {} + + def _update_data_model(self, data_model) -> None: + mtm = self._create_data_model_mtm() + for field_name, value in mtm.items(): + field = getattr(data_model, field_name) + field.add(*value) + + def create_data_model(self): + self.report: AnalyzerReport + if self._do_create_data_model(): + data_model = self.report.create_data_model() + if data_model: + self._update_data_model(data_model) + data_model.save() + return data_model + return None + @classmethod @property def config_exception(cls): @@ -108,7 +158,17 @@ def after_run_success(self, content): Args: content (any): The content to process after a successful run. """ - super().after_run_success(self._validate_result(content, max_recursion=15)) + result = super().after_run_success( + self._validate_result(content, max_recursion=15) + ) + try: + self.create_data_model() + except Exception as e: + logger.exception(e) + self._job.errors.append( + f"Data model creation failed for {self._config.name}" + ) + return result class ObservableAnalyzer(BaseAnalyzerMixin, metaclass=ABCMeta): @@ -326,7 +386,7 @@ def __polling(self, req_key: str, chance: int, re_poll_try: int = 0): return self.__polling(req_key, chance, re_poll_try=re_poll_try + 1) else: status = json_data.get("status", None) - if status and status == self._job.Status.RUNNING.value: + if status and status == self._job.STATUSES.RUNNING.value: logger.info( f"Poll number #{chance + 1}, " f"status: 'running' <-- {self.__repr__()}" diff --git a/api_app/analyzers_manager/file_analyzers/elf_info.py b/api_app/analyzers_manager/file_analyzers/elf_info.py index 98671ae7..049be70e 100644 --- a/api_app/analyzers_manager/file_analyzers/elf_info.py +++ b/api_app/analyzers_manager/file_analyzers/elf_info.py @@ -46,7 +46,7 @@ def run(self): ) logger.warning(warning_message) self.report.errors.append(warning_message) - self.report.status = self.report.Status.FAILED + self.report.status = self.report.STATUSES.FAILED self.report.save() return results diff --git a/api_app/analyzers_manager/file_analyzers/pe_info.py b/api_app/analyzers_manager/file_analyzers/pe_info.py index 8f5235e6..8b316f08 100644 --- a/api_app/analyzers_manager/file_analyzers/pe_info.py +++ b/api_app/analyzers_manager/file_analyzers/pe_info.py @@ -165,7 +165,7 @@ def run(self): ) logger.warning(warning_message) self.report.errors.append(warning_message) - self.report.status = self.report.Status.FAILED + self.report.status = self.report.STATUSES.FAILED self.report.save() return results diff --git a/api_app/analyzers_manager/file_analyzers/phishing/__init__.py b/api_app/analyzers_manager/file_analyzers/phishing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api_app/analyzers_manager/file_analyzers/phishing/phishing_form_compiler.py b/api_app/analyzers_manager/file_analyzers/phishing/phishing_form_compiler.py new file mode 100644 index 00000000..d2cedd0e --- /dev/null +++ b/api_app/analyzers_manager/file_analyzers/phishing/phishing_form_compiler.py @@ -0,0 +1,247 @@ +import logging +from datetime import date, timedelta +from typing import Dict + +import requests +from faker import Faker +from lxml.etree import HTMLParser +from lxml.html import document_fromstring +from requests import HTTPError, Response + +from api_app.analyzers_manager.classes import FileAnalyzer +from api_app.models import PythonConfig + +logger = logging.getLogger(__name__) + + +def xpath_query_on_page(page, xpath_selector: str) -> []: + return page.xpath(xpath_selector) + + +class PhishingFormCompiler(FileAnalyzer): + # good short guide for writing XPath expressions + # https://upg-dh.newtfire.org/explainXPath.html + # we're supporting XPath up to v3.1 with elementpath package + xpath_form_selector: str = "" + xpath_js_selector: str = "" + proxy_address: str = "" + + name_matching: list = [] + cc_matching: list = [] + pin_matching: list = [] + cvv_matching: list = [] + expiration_date_matching: list = [] + + def __init__( + self, + config: PythonConfig, + **kwargs, + ): + super().__init__(config, **kwargs) + self.target_site: str = "" + self.html_source_code: str = "" + self.parsed_page = None + self.args: [] = [] + self._name_text_input_mapping: {} = None + self.FAKE_EMAIL_INPUT = None + self.FAKE_PASSWORD_INPUT = None + self.FAKE_TEL_INPUT = None + + def config(self, runtime_configuration: Dict): + super().config(runtime_configuration) + if hasattr(self._job, "pivot_parent"): + # extract target site from parent job + self.target_site = self._job.pivot_parent.starting_job.observable_name + else: + logger.warning( + f"Job #{self.job_id}: Analyzer {self.analyzer_name} should be ran from PhishingAnalysis playbook." + ) + if self.target_site: + logger.info( + f"Job #{self.job_id}: Extracted {self.target_site} from parent job." + ) + else: + logger.info( + f"Job #{self.job_id}: Target site from parent job not found! Proceeding with only source code." + ) + + # generate fake values for each mapping + fake = Faker() + # mapping between name attribute of text + # and their corresponding fake values + self._name_text_input_mapping: {tuple: str} = { + tuple(self.name_matching): fake.user_name(), + tuple(self.cc_matching): fake.credit_card_number(), + tuple(self.pin_matching): str(fake.random.randint(10000, 100000)), + tuple(self.cvv_matching): fake.credit_card_security_code(), + tuple(self.expiration_date_matching): fake.credit_card_expire( + start=date.today(), + end=date.today() + timedelta(days=fake.random.randint(1, 1000)), + date_format="%m/%y", + ), + } + logger.info( + f"Generated name text input mapping {self._name_text_input_mapping}" + ) + self.FAKE_EMAIL_INPUT: str = fake.email() + logger.info(f"Generated fake email input {self.FAKE_EMAIL_INPUT}") + self.FAKE_PASSWORD_INPUT: str = fake.password( + length=16, + special_chars=True, + digits=True, + upper_case=True, + lower_case=True, + ) + logger.info(f"Generated fake password input {self.FAKE_PASSWORD_INPUT}") + self.FAKE_TEL_INPUT: str = fake.phone_number() + logger.info(f"Generated fake tel input {self.FAKE_TEL_INPUT}") + + # extract and decode source code from file + self.html_source_code = self.read_file_bytes() + if self.html_source_code: + logger.debug(f"Job #{self.job_id}: {self.html_source_code=}") + try: + self.html_source_code = self.html_source_code.decode("utf-8") + except UnicodeDecodeError as e: + logger.warning( + f"Job #{self.job_id}: Error during HTML source page decoding: {e}\nTrying to fix the error..." + ) + self.html_source_code = self.html_source_code.decode( + "utf-8", errors="replace" + ) + else: + logger.info( + f"Job #{self.job_id}: Extracted html source code from pivot." + ) + else: + raise ValueError("Failed to extract source code from pivot!") + + # recover=True tries to read not well-formed HTML + html_parser = HTMLParser(recover=True, no_network=True) + self.parsed_page = document_fromstring( + self.html_source_code, parser=html_parser + ) + + def search_phishing_forms_xpath(self) -> []: + # extract using a custom XPath selector if set + return ( + xpath_query_on_page(self.parsed_page, self.xpath_form_selector) + if self.xpath_form_selector + else [] + ) + + def identify_text_input(self, input_name: str) -> str: + for names, fake_value in self._name_text_input_mapping.items(): + if input_name in names: + return fake_value + + def compile_form_field(self, form) -> (dict, str): + result: {} = {} + # setting default to page itself if action is not specified + if not (form_action := form.get("action", None)): + form_action = self.target_site + for element in form.findall(".//input"): + input_type: str = element.get("type", None) + input_name: str = element.get("name", None) + input_value: str = element.get("value", None) + value_to_set: str = "" + match input_type.lower(): + case "hidden": + logger.info( + f"Job #{self.job_id}: Found hidden input tag with {input_name=} and {input_value=}" + ) + value_to_set = input_value + + case "text": + value_to_set = self.identify_text_input(input_name) + case "password": + value_to_set = self.FAKE_PASSWORD_INPUT + case "tel": + value_to_set = self.FAKE_TEL_INPUT + case "email": + value_to_set = self.FAKE_EMAIL_INPUT + case _: + logger.info( + f"Job #{self.job_id}: {input_type.lower()} is not supported yet!" + ) + + logger.info( + f"Job #{self.job_id}: Sending value {value_to_set} for {input_name=}" + ) + result.setdefault(input_name, value_to_set) + return result, form_action + + def perform_request_to_form(self, form) -> Response: + params, dest_url = self.compile_form_field(form) + logger.info(f"Job #{self.job_id}: Sending {params=} to submit url {dest_url}") + return requests.post( + url=dest_url, + data=params, + proxies=( + {"http": self.proxy_address, "https": self.proxy_address} + if self.proxy_address + else None + ), + ) + + @staticmethod + def handle_3xx_response(response: Response) -> [str]: + # extract all redirection history + return [history.request.url for history in response.history] + + @staticmethod + def handle_2xx_response(response: Response) -> str: + return response.request.url + + def is_js_used_in_page(self) -> bool: + js_tag: [] = xpath_query_on_page(self.parsed_page, self.xpath_js_selector) + if js_tag: + logger.info(f"Job #{self.job_id}: Found script tag: {js_tag}") + return bool(js_tag) + + def analyze_responses(self, responses: [Response]) -> {}: + result: [] = [] + for response in responses: + try: + # handle 4xx and 5xx + response.raise_for_status() + except HTTPError as e: + message = f"Error during request to {response.request.url}: {e}" + logger.error(f"Job #{self.job_id}:" + message) + self.report.errors.append(message) + else: + if response.history: + result.extend(self.handle_3xx_response(response)) + + result.append(self.handle_2xx_response(response)) + self.report.save() + + return result + + def run(self) -> dict: + result: {} = {} + if not ( + forms := xpath_query_on_page(self.parsed_page, self.xpath_form_selector) + ): + message = ( + f"Form not found in {self.target_site=} with " + f"{self.xpath_form_selector=}! This could mean that the XPath" + f" selector requires some tuning." + ) + logger.warning(f"Job #{self.job_id}: " + message) + self.report.errors.append(message) + self.report.save() + logger.info( + f"Job #{self.job_id}: Found {len(forms)} forms in page {self.target_site}" + ) + + responses: [Response] = [] + for form in forms: + responses.append(self.perform_request_to_form(form)) + + result.setdefault("extracted_urls", self.analyze_responses(responses)) + result.setdefault("has_javascript", self.is_js_used_in_page()) + return result + + def update(self) -> bool: + pass diff --git a/api_app/analyzers_manager/file_analyzers/yara_scan.py b/api_app/analyzers_manager/file_analyzers/yara_scan.py index 3159f52c..a04bc4ff 100644 --- a/api_app/analyzers_manager/file_analyzers/yara_scan.py +++ b/api_app/analyzers_manager/file_analyzers/yara_scan.py @@ -438,3 +438,33 @@ def update(cls): logger.info("Finished updating yara rules") set_permissions(settings.YARA_RULES_PATH) return True + + def _create_data_model_mtm(self): + from api_app.data_model_manager.models import Signature + + signatures = [] + for yara_signatures in self.report.report.values(): + for yara_signature in yara_signatures: + url = yara_signature.pop("rule_url", None) + sign = Signature.objects.create( + provider=Signature.PROVIDERS.YARA.value, + signature=yara_signature, + url=url, + score=1, + ) + signatures.append(sign) + + return {"signatures": signatures} + + def _update_data_model(self, data_model): + from api_app.data_model_manager.models import FileDataModel + + super()._update_data_model(data_model) + data_model: FileDataModel + signatures = data_model.signatures.count() + + if signatures: + self.MALICIOUS_EVALUATION = 20 + self.SUSPICIOUS_EVALUATION = 10 + + data_model.evaluation = self.threat_to_evaluation(signatures) diff --git a/api_app/analyzers_manager/migrations/0125_update_yara_repo.py b/api_app/analyzers_manager/migrations/0125_update_yara_repo.py index cbb919bc..2d87c297 100644 --- a/api_app/analyzers_manager/migrations/0125_update_yara_repo.py +++ b/api_app/analyzers_manager/migrations/0125_update_yara_repo.py @@ -10,10 +10,10 @@ def migrate(apps, schema_editor): base_path="api_app.analyzers_manager.file_analyzers", ) param = pm.parameters.get(name="repositories") - pc = PluginConfig.objects.get(parameter=param) - pc.value.append("https://yaraify-api.abuse.ch/download/yaraify-rules.zip") - pc.value.remove("https://yaraify-api.abuse.ch/yarahub/yaraify-rules.zip") - pc.save() + for pc in PluginConfig.objects.filter(parameter=param): + pc.value.append("https://yaraify-api.abuse.ch/download/yaraify-rules.zip") + pc.value.remove("https://yaraify-api.abuse.ch/yarahub/yaraify-rules.zip") + pc.save() def reverse_migrate(apps, schema_editor): @@ -25,10 +25,10 @@ def reverse_migrate(apps, schema_editor): base_path="api_app.analyzers_manager.file_analyzers", ) param = pm.parameters.get(name="repositories") - pc = PluginConfig.objects.get(parameter=param) - pc.value.remove("https://yaraify-api.abuse.ch/download/yaraify-rules.zip") - pc.value.append("https://yaraify-api.abuse.ch/yarahub/yaraify-rules.zip") - pc.save() + for pc in PluginConfig.objects.filter(parameter=param): + pc.value.remove("https://yaraify-api.abuse.ch/download/yaraify-rules.zip") + pc.value.append("https://yaraify-api.abuse.ch/yarahub/yaraify-rules.zip") + pc.save() class Migration(migrations.Migration): diff --git a/api_app/analyzers_manager/migrations/0128_analyzer_config_phishing_form_compiler.py b/api_app/analyzers_manager/migrations/0128_analyzer_config_phishing_form_compiler.py new file mode 100644 index 00000000..1b2b29e5 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0128_analyzer_config_phishing_form_compiler.py @@ -0,0 +1,396 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "Phishing_Form_Compiler", + "description": "Analyzer that retrieves all forms in a web page and tries to compile and submit them.", + "disabled": False, + "soft_time_limit": 60, + "routing_key": "default", + "health_check_status": True, + "type": "file", + "docker_based": False, + "maximum_tlp": "CLEAR", + "observable_supported": [], + "supported_filetypes": [ + "application/javascript", + "application/octet-stream", + "application/x-javascript", + "text/javascript", + "text/html", + ], + "run_hash": False, + "run_hash_type": "", + "not_supported_filetypes": [], + "model": "analyzers_manager.AnalyzerConfig", +} + +params = [ + { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "proxy_address", + "type": "str", + "description": "Address for proxy to use for requests.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "xpath_form_selector", + "type": "str", + "description": "XPath expression to match a form on phishing page.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "xpath_js_selector", + "type": "str", + "description": "XPath expression to match all js tag on phishing page.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "name_matching", + "type": "list", + "description": 'List of values that should match the "name" attribute of "input" tag of type="text".\r\nMatching data will be replaced by a fake username.', + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "cc_matching", + "type": "list", + "description": 'List of values that should match the "name" attribute of "input" tag of type="text".\r\nMatching data will be replaced by a fake credit card number.', + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "pin_matching", + "type": "list", + "description": 'List of values that should match the "name" attribute of "input" tag of type="text".\r\nMatching data will be replaced by a fake credit card pin.', + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "cvv_matching", + "type": "list", + "description": 'List of values that should match the "name" attribute of "input" tag of type="text".\r\nMatching data will be replaced by a fake credit card cvv/cvc.', + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "expiration_date_matching", + "type": "list", + "description": 'List of values that should match the "name" attribute of "input" tag of type="text".\r\nMatching data will be replaced by a fake credit card expiration date.', + "is_secret": False, + "required": False, + }, +] + +values = [ + { + "parameter": { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "proxy_address", + "type": "str", + "description": "Address for proxy to use for requests.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "Phishing_Form_Compiler", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": "", + "updated_at": "2024-10-23T10:48:55.311636Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "xpath_form_selector", + "type": "str", + "description": "XPath expression to match a form on phishing page.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "Phishing_Form_Compiler", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": "//*[self::form or self::iframe or self::fieldset][.//input[not(@type) or @type='' or @type='text']][.//input[@type='password']][.//input[@type='submit' or contains(@class, 'submit')] or .//button[not(@type) or @type='' or @type='submit' or contains(@class, 'submit')]]", + "updated_at": "2024-10-23T10:48:55.289420Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "xpath_js_selector", + "type": "str", + "description": "XPath expression to match all js tag on phishing page.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "Phishing_Form_Compiler", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": "//script[@type='text/javascript' or @type='' or (@src and not(contains(@src, 'jquery')))]", + "updated_at": "2024-10-23T10:48:55.289420Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "name_matching", + "type": "list", + "description": 'List of values that should match the "name" attribute of "input" tag of type="text".\r\nMatching data will be replaced by a fake username.', + "is_secret": False, + "required": False, + }, + "analyzer_config": "Phishing_Form_Compiler", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": ["username", "user", "name", "first-name", "last-name"], + "updated_at": "2024-10-23T13:07:03.010102Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "cc_matching", + "type": "list", + "description": 'List of values that should match the "name" attribute of "input" tag of type="text".\r\nMatching data will be replaced by a fake credit card number.', + "is_secret": False, + "required": False, + }, + "analyzer_config": "Phishing_Form_Compiler", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": ["card", "card_number", "card-number", "cc", "cc-number"], + "updated_at": "2024-10-23T13:07:45.231863Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "pin_matching", + "type": "list", + "description": 'List of values that should match the "name" attribute of "input" tag of type="text".\r\nMatching data will be replaced by a fake credit card pin.', + "is_secret": False, + "required": False, + }, + "analyzer_config": "Phishing_Form_Compiler", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": ["pin"], + "updated_at": "2024-10-23T13:07:57.878006Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "cvv_matching", + "type": "list", + "description": 'List of values that should match the "name" attribute of "input" tag of type="text".\r\nMatching data will be replaced by a fake credit card cvv/cvc.', + "is_secret": False, + "required": False, + }, + "analyzer_config": "Phishing_Form_Compiler", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": ["cvv", "cvc"], + "updated_at": "2024-10-23T13:08:29.552992Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "phishing.phishing_form_compiler.PhishingFormCompiler", + "base_path": "api_app.analyzers_manager.file_analyzers", + }, + "name": "expiration_date_matching", + "type": "list", + "description": 'List of values that should match the "name" attribute of "input" tag of type="text".\r\nMatching data will be replaced by a fake credit card expiration date.', + "is_secret": False, + "required": False, + }, + "analyzer_config": "Phishing_Form_Compiler", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": ["exp", "date", "expiration-date", "exp-date"], + "updated_at": "2024-10-23T13:08:29.568943Z", + "owner": None, + }, +] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ + ForwardManyToOneDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, + ForwardOneToOneDescriptor, + ] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0063_singleton_and_elastic_report"), + ("analyzers_manager", "0127_analyzer_config_dshield"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/migrations/0129_analyzer_config_phishing_extractor.py b/api_app/analyzers_manager/migrations/0129_analyzer_config_phishing_extractor.py new file mode 100644 index 00000000..bb36d241 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0129_analyzer_config_phishing_extractor.py @@ -0,0 +1,224 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "phishing.phishing_extractor.PhishingExtractor", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "Phishing_Extractor", + "description": "This analyzer is the first phase of the phishing analysis playbook. Its main purpose is to open the web page and dump its source code and screenshot plus some other details.", + "disabled": False, + "soft_time_limit": 60, + "routing_key": "default", + "health_check_status": True, + "type": "observable", + "docker_based": True, + "maximum_tlp": "CLEAR", + "observable_supported": ["url", "domain"], + "supported_filetypes": [], + "run_hash": False, + "run_hash_type": "", + "not_supported_filetypes": [], + "model": "analyzers_manager.AnalyzerConfig", +} + +params = [ + { + "python_module": { + "module": "phishing.phishing_extractor.PhishingExtractor", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "proxy_address", + "type": "str", + "description": "Address for proxy to use for requests.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "phishing.phishing_extractor.PhishingExtractor", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "window_width", + "type": "int", + "description": "Width of Selenium browser. Default is 1920.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "phishing.phishing_extractor.PhishingExtractor", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "window_height", + "type": "int", + "description": "Height of Selenium browser. Default is 1080.", + "is_secret": False, + "required": False, + }, +] + +values = [ + { + "parameter": { + "python_module": { + "module": "phishing.phishing_extractor.PhishingExtractor", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "proxy_address", + "type": "str", + "description": "Address for proxy to use for requests.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "Phishing_Extractor", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": "", + "updated_at": "2024-10-18T09:25:01.624934Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "phishing.phishing_extractor.PhishingExtractor", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "window_width", + "type": "int", + "description": "Width of Selenium browser. Default is 1920.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "Phishing_Extractor", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": 1920, + "updated_at": "2024-10-22T06:18:01.101202Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "phishing.phishing_extractor.PhishingExtractor", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "window_height", + "type": "int", + "description": "Height of Selenium browser. Default is 1080.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "Phishing_Extractor", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": 1080, + "updated_at": "2024-10-22T06:18:01.119554Z", + "owner": None, + }, +] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ + ForwardManyToOneDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, + ForwardOneToOneDescriptor, + ] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("analyzers_manager", "0128_analyzer_config_phishing_form_compiler"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/migrations/0131_analyzer_config_vt_sample_download.py b/api_app/analyzers_manager/migrations/0131_analyzer_config_vt_sample_download.py new file mode 100644 index 00000000..751f6328 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0131_analyzer_config_vt_sample_download.py @@ -0,0 +1,34 @@ +from django.db import migrations + +from api_app.analyzers_manager.constants import ObservableTypes, TypeChoices +from api_app.choices import TLP + + +def migrate(apps, schema_editor): + PythonModule = apps.get_model("api_app", "PythonModule") + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + AnalyzerConfig.objects.create( + name="VirusTotalv3SampleDownload", + description="Download sample from VT.", + type=TypeChoices.OBSERVABLE.value, + maximum_tlp=TLP.AMBER.value, + python_module=PythonModule.objects.get( + module="vt.vt3_sample_download.VirusTotalv3SampleDownload" + ), + observable_supported=[ObservableTypes.HASH.value], + ) + + +def reverse_migrate(apps, schema_editor): + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + AnalyzerConfig.objects.get(name="VirusTotalv3SampleDownload").delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("analyzers_manager", "0130_analyzer_config_nvd_cve"), + ("api_app", "0064_vt_sample_download"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/migrations/0132_analyzer_config_urldna_new_scan.py b/api_app/analyzers_manager/migrations/0132_analyzer_config_urldna_new_scan.py new file mode 100644 index 00000000..88c19afb --- /dev/null +++ b/api_app/analyzers_manager/migrations/0132_analyzer_config_urldna_new_scan.py @@ -0,0 +1,401 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "UrlDNA_New_Scan", + "description": "Submit the URL to [urlDNA.io](https://urldna.io) to retrieve a comprehensive URL analysis. The results will include details such as certificate information, WHOIS data, IP data, outgoing links, and DOM structure.", + "disabled": False, + "soft_time_limit": 100, + "routing_key": "default", + "health_check_status": True, + "type": "observable", + "docker_based": False, + "maximum_tlp": "GREEN", + "observable_supported": ["url"], + "supported_filetypes": [], + "run_hash": False, + "run_hash_type": "", + "not_supported_filetypes": [], + "model": "analyzers_manager.AnalyzerConfig", +} + +params = [ + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "api_key_name", + "type": "str", + "description": "urlDNA.io API KEY.", + "is_secret": True, + "required": True, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "scanned_from", + "type": "str", + "description": "The country from which the new scan is performed.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "viewport_width", + "type": "int", + "description": "The screen width of the viewport used for the scan.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "viewport_height", + "type": "int", + "description": "The screen height of the viewport used for the scan.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "private_scan", + "type": "bool", + "description": "The visibility setting for the new scan: False will make it publicly visible and searchable.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "user_agent", + "type": "str", + "description": "The browser User Agent used for the scan.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "waiting_time", + "type": "int", + "description": "The waiting time for the page to load during the scan.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "device", + "type": "str", + "description": "The device type used for scraping, either DESKTOP or MOBILE.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "urldna_analysis", + "type": "str", + "description": "The analysis type can either involve conducting a NEW_SCAN or performing a SEARCH within the urlDNA.io database.", + "is_secret": False, + "required": True, + }, +] + +values = [ + { + "parameter": { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "scanned_from", + "type": "str", + "description": "The country from which the new scan is performed.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "UrlDNA_New_Scan", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": "DEFAULT", + "updated_at": "2024-11-22T13:56:01.732166Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "viewport_width", + "type": "int", + "description": "The screen width of the viewport used for the scan.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "UrlDNA_New_Scan", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": 1920, + "updated_at": "2024-11-22T13:56:01.854667Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "viewport_height", + "type": "int", + "description": "The screen height of the viewport used for the scan.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "UrlDNA_New_Scan", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": 1080, + "updated_at": "2024-11-22T13:56:02.002611Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "private_scan", + "type": "bool", + "description": "The visibility setting for the new scan: False will make it publicly visible and searchable.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "UrlDNA_New_Scan", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": False, + "updated_at": "2024-11-22T13:56:02.215352Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "user_agent", + "type": "str", + "description": "The browser User Agent used for the scan.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "UrlDNA_New_Scan", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": "Mozilla/5.0 (Windows NT 10.0;Win64;x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36", + "updated_at": "2024-11-22T13:56:02.116761Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "waiting_time", + "type": "int", + "description": "The waiting time for the page to load during the scan.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "UrlDNA_New_Scan", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": 5, + "updated_at": "2024-11-22T13:56:02.331523Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "device", + "type": "str", + "description": "The device type used for scraping, either DESKTOP or MOBILE.", + "is_secret": False, + "required": False, + }, + "analyzer_config": "UrlDNA_New_Scan", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": "DESKTOP", + "updated_at": "2024-11-22T13:56:02.452591Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "urldna_analysis", + "type": "str", + "description": "The analysis type can either involve conducting a NEW_SCAN or performing a SEARCH within the urlDNA.io database.", + "is_secret": False, + "required": True, + }, + "analyzer_config": "UrlDNA_New_Scan", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": "NEW_SCAN", + "updated_at": "2024-11-22T13:56:01.613430Z", + "owner": None, + }, +] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ + ForwardManyToOneDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, + ForwardOneToOneDescriptor, + ] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0063_singleton_and_elastic_report"), + ("analyzers_manager", "0131_analyzer_config_vt_sample_download"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/migrations/0133_analyzer_config_urldna_search.py b/api_app/analyzers_manager/migrations/0133_analyzer_config_urldna_search.py new file mode 100644 index 00000000..43b2d175 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0133_analyzer_config_urldna_search.py @@ -0,0 +1,247 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "UrlDNA_Search", + "description": "Search the [urlDNA.io](https://urldna.io) database to retrieve information about a domain, URL, or IP address.", + "disabled": False, + "soft_time_limit": 100, + "routing_key": "default", + "health_check_status": True, + "type": "observable", + "docker_based": False, + "maximum_tlp": "GREEN", + "observable_supported": ["ip", "url", "domain"], + "supported_filetypes": [], + "run_hash": False, + "run_hash_type": "", + "not_supported_filetypes": [], + "model": "analyzers_manager.AnalyzerConfig", +} + +params = [ + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "api_key_name", + "type": "str", + "description": "urlDNA.io API KEY.", + "is_secret": True, + "required": True, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "scanned_from", + "type": "str", + "description": "The country from which the new scan is performed.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "viewport_width", + "type": "int", + "description": "The screen width of the viewport used for the scan.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "viewport_height", + "type": "int", + "description": "The screen height of the viewport used for the scan.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "private_scan", + "type": "bool", + "description": "The visibility setting for the new scan: False will make it publicly visible and searchable.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "user_agent", + "type": "str", + "description": "The browser User Agent used for the scan.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "waiting_time", + "type": "int", + "description": "The waiting time for the page to load during the scan.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "device", + "type": "str", + "description": "The device type used for scraping, either DESKTOP or MOBILE.", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "urldna_analysis", + "type": "str", + "description": "The analysis type can either involve conducting a NEW_SCAN or performing a SEARCH within the urlDNA.io database.", + "is_secret": False, + "required": True, + }, +] + +values = [ + { + "parameter": { + "python_module": { + "module": "urldna.UrlDNA", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "urldna_analysis", + "type": "str", + "description": "The analysis type can either involve conducting a NEW_SCAN or performing a SEARCH within the urlDNA.io database.", + "is_secret": False, + "required": True, + }, + "analyzer_config": "UrlDNA_Search", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": "SEARCH", + "updated_at": "2024-11-22T14:01:18.449928Z", + "owner": None, + } +] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ + ForwardManyToOneDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, + ForwardOneToOneDescriptor, + ] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0063_singleton_and_elastic_report"), + ("analyzers_manager", "0132_analyzer_config_urldna_new_scan"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/migrations/0134_analyzerconfig_mapping_data_model.py b/api_app/analyzers_manager/migrations/0134_analyzerconfig_mapping_data_model.py new file mode 100644 index 00000000..9986b161 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0134_analyzerconfig_mapping_data_model.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.15 on 2024-10-14 07:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("analyzers_manager", "0133_analyzer_config_urldna_search"), + ] + + operations = [ + migrations.AddField( + model_name="analyzerconfig", + name="mapping_data_model", + field=models.JSONField( + default=dict, help_text="Mapping data_model_key: analyzer_report_key. " + ), + ), + ] diff --git a/api_app/analyzers_manager/migrations/0135_data_mapping.py b/api_app/analyzers_manager/migrations/0135_data_mapping.py new file mode 100644 index 00000000..b0d4215d --- /dev/null +++ b/api_app/analyzers_manager/migrations/0135_data_mapping.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.15 on 2024-10-14 07:24 + +from django.db import migrations + + +def migrate_urlhaus(apps, schema_editor): + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + ac = AnalyzerConfig.objects.filter(name="URLhaus").first() + if not ac: + return + ac.mapping_data_model = { + "urlhaus_reference": "external_references", + "$malicious": "evaluation", + "urls.url": "related_threats", + } + ac.save() + + +def migrate_maxmind(apps, schema_editor): + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + ac = AnalyzerConfig.objects.filter(name="MaxMindGeoIP").first() + if not ac: + return + ac.mapping_data_model = { + "country.iso_code": "country_code", + "registered_country_code.iso_code": "registered_country_code", + "autonomous_system_number": "asn", + "autonomous_system_organization": "isp", + } + ac.save() + + +def migrate_abuse_ipdb(apps, schema_editor): + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + ac = AnalyzerConfig.objects.filter(name="AbuseIPDB").first() + if not ac: + return + ac.mapping_data_model = { + "data.countryCode": "country_code", + "permalink": "external_references", + "data.hostnames": "resolutions", + "data.isp": "isp", + "categories_found": "tags", + } + ac.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("analyzers_manager", "0134_analyzerconfig_mapping_data_model"), + ] + + operations = [ + migrations.RunPython(migrate_maxmind, migrations.RunPython.noop), + migrations.RunPython(migrate_abuse_ipdb, migrations.RunPython.noop), + migrations.RunPython(migrate_urlhaus, migrations.RunPython.noop), + ] diff --git a/api_app/analyzers_manager/migrations/0136_alter_analyzerconfig_mapping_data_model_and_more.py b/api_app/analyzers_manager/migrations/0136_alter_analyzerconfig_mapping_data_model_and_more.py new file mode 100644 index 00000000..17e41cc1 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0136_alter_analyzerconfig_mapping_data_model_and_more.py @@ -0,0 +1,189 @@ +# Generated by Django 4.2.16 on 2024-11-08 09:21 + +from django.db import migrations, models + +import api_app.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("analyzers_manager", "0135_data_mapping"), + ] + + operations = [ + migrations.AlterField( + model_name="analyzerconfig", + name="mapping_data_model", + field=models.JSONField( + default=dict, + help_text="Mapping analyzer_report_key: data_model_key. Keys preceded by the symbol $ will be considered as constants.", + ), + ), + migrations.AlterField( + model_name="analyzerconfig", + name="not_supported_filetypes", + field=api_app.fields.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ("application/w-script-file", "Wscript"), + ("application/javascript", "Javascript1"), + ("application/x-javascript", "Javascript2"), + ("text/javascript", "Javascript3"), + ("application/x-vbscript", "Vb Script"), + ("text/x-ms-iqy", "Iqy"), + ("application/vnd.android.package-archive", "Apk"), + ("application/x-dex", "Dex"), + ("application/onenote", "One Note"), + ("application/zip", "Zip1"), + ("multipart/x-zip", "Zip2"), + ("application/java-archive", "Java"), + ("text/rtf", "Rtf1"), + ("application/rtf", "Rtf2"), + ("application/x-sharedlib", "Shared Lib"), + ("application/vnd.microsoft.portable-executable", "Exe"), + ("application/x-elf", "Elf"), + ("application/octet-stream", "Octet"), + ("application/vnd.tcpdump.pcap", "Pcap"), + ("application/pdf", "Pdf"), + ("text/html", "Html"), + ("application/x-mspublisher", "Pub"), + ("application/vnd.ms-excel.addin.macroEnabled", "Excel Macro1"), + ( + "application/vnd.ms-excel.sheet.macroEnabled.12", + "Excel Macro2", + ), + ("application/vnd.ms-excel", "Excel1"), + ("application/excel", "Excel2"), + ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Doc", + ), + ("application/xml", "Xml1"), + ("text/xml", "Xml2"), + ("application/encrypted", "Encrypted"), + ("text/plain", "Plain"), + ("text/csv", "Csv"), + ( + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "Pptx", + ), + ("application/msword", "Word1"), + ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "Word2", + ), + ("application/vnd.ms-powerpoint", "Powerpoint"), + ("application/vnd.ms-office", "Office"), + ("application/x-binary", "Binary"), + ("application/x-macbinary", "Mac1"), + ("application/mac-binary", "Mac2"), + ("application/x-mach-binary", "Mac3"), + ("application/x-zip-compressed", "Compress1"), + ("application/x-compressed", "Compress2"), + ("application/vnd.ms-outlook", "Outlook"), + ("message/rfc822", "Eml"), + ("application/pkcs7-signature", "Pkcs7"), + ("application/x-pkcs7-signature", "Xpkcs7"), + ("multipart/mixed", "Mixed"), + ("text/x-shellscript", "X Shellscript"), + ("application/x-chrome-extension", "Crx"), + ("application/json", "Json"), + ("application/x-executable", "Executable"), + ("text/x-java", "Java2"), + ("text/x-kotlin", "Kotlin"), + ("text/x-swift", "Swift"), + ("text/x-objective-c", "Objective C Code"), + ("application/x-ms-shortcut", "Lnk"), + ], + max_length=90, + ), + blank=True, + default=list, + size=None, + ), + ), + migrations.AlterField( + model_name="analyzerconfig", + name="supported_filetypes", + field=api_app.fields.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ("application/w-script-file", "Wscript"), + ("application/javascript", "Javascript1"), + ("application/x-javascript", "Javascript2"), + ("text/javascript", "Javascript3"), + ("application/x-vbscript", "Vb Script"), + ("text/x-ms-iqy", "Iqy"), + ("application/vnd.android.package-archive", "Apk"), + ("application/x-dex", "Dex"), + ("application/onenote", "One Note"), + ("application/zip", "Zip1"), + ("multipart/x-zip", "Zip2"), + ("application/java-archive", "Java"), + ("text/rtf", "Rtf1"), + ("application/rtf", "Rtf2"), + ("application/x-sharedlib", "Shared Lib"), + ("application/vnd.microsoft.portable-executable", "Exe"), + ("application/x-elf", "Elf"), + ("application/octet-stream", "Octet"), + ("application/vnd.tcpdump.pcap", "Pcap"), + ("application/pdf", "Pdf"), + ("text/html", "Html"), + ("application/x-mspublisher", "Pub"), + ("application/vnd.ms-excel.addin.macroEnabled", "Excel Macro1"), + ( + "application/vnd.ms-excel.sheet.macroEnabled.12", + "Excel Macro2", + ), + ("application/vnd.ms-excel", "Excel1"), + ("application/excel", "Excel2"), + ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Doc", + ), + ("application/xml", "Xml1"), + ("text/xml", "Xml2"), + ("application/encrypted", "Encrypted"), + ("text/plain", "Plain"), + ("text/csv", "Csv"), + ( + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "Pptx", + ), + ("application/msword", "Word1"), + ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "Word2", + ), + ("application/vnd.ms-powerpoint", "Powerpoint"), + ("application/vnd.ms-office", "Office"), + ("application/x-binary", "Binary"), + ("application/x-macbinary", "Mac1"), + ("application/mac-binary", "Mac2"), + ("application/x-mach-binary", "Mac3"), + ("application/x-zip-compressed", "Compress1"), + ("application/x-compressed", "Compress2"), + ("application/vnd.ms-outlook", "Outlook"), + ("message/rfc822", "Eml"), + ("application/pkcs7-signature", "Pkcs7"), + ("application/x-pkcs7-signature", "Xpkcs7"), + ("multipart/mixed", "Mixed"), + ("text/x-shellscript", "X Shellscript"), + ("application/x-chrome-extension", "Crx"), + ("application/json", "Json"), + ("application/x-executable", "Executable"), + ("text/x-java", "Java2"), + ("text/x-kotlin", "Kotlin"), + ("text/x-swift", "Swift"), + ("text/x-objective-c", "Objective C Code"), + ("application/x-ms-shortcut", "Lnk"), + ], + max_length=90, + ), + blank=True, + default=list, + size=None, + ), + ), + ] diff --git a/api_app/analyzers_manager/migrations/0137_analyzerreport_data_model_content_type_and_more.py b/api_app/analyzers_manager/migrations/0137_analyzerreport_data_model_content_type_and_more.py new file mode 100644 index 00000000..5adb67e5 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0137_analyzerreport_data_model_content_type_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.16 on 2024-11-08 09:38 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ( + "analyzers_manager", + "0136_alter_analyzerconfig_mapping_data_model_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="analyzerreport", + name="data_model_content_type", + field=models.ForeignKey( + editable=False, + limit_choices_to={"app_label": "data_model"}, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.contenttype", + ), + ), + migrations.AddField( + model_name="analyzerreport", + name="data_model_object_id", + field=models.IntegerField(editable=False, null=True), + ), + ] diff --git a/api_app/analyzers_manager/migrations/0138_alter_analyzerreport_data_model_content_type.py b/api_app/analyzers_manager/migrations/0138_alter_analyzerreport_data_model_content_type.py new file mode 100644 index 00000000..d15554bb --- /dev/null +++ b/api_app/analyzers_manager/migrations/0138_alter_analyzerreport_data_model_content_type.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.16 on 2024-11-08 11:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ( + "analyzers_manager", + "0137_analyzerreport_data_model_content_type_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="analyzerreport", + name="data_model_content_type", + field=models.ForeignKey( + editable=False, + limit_choices_to={"app_label": "data_model_manager"}, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.contenttype", + ), + ), + ] diff --git a/api_app/analyzers_manager/migrations/0139_alter_analyzerconfig_mapping_data_model.py b/api_app/analyzers_manager/migrations/0139_alter_analyzerconfig_mapping_data_model.py new file mode 100644 index 00000000..3af46871 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0139_alter_analyzerconfig_mapping_data_model.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.16 on 2024-12-06 09:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("analyzers_manager", "0138_alter_analyzerreport_data_model_content_type"), + ] + + operations = [ + migrations.AlterField( + model_name="analyzerconfig", + name="mapping_data_model", + field=models.JSONField( + blank=True, + default=dict, + help_text="Mapping analyzer_report_key: data_model_key. Keys preceded by the symbol $ will be considered as constants.", + ), + ), + ] diff --git a/api_app/analyzers_manager/models.py b/api_app/analyzers_manager/models.py index f285b662..5d78b905 100644 --- a/api_app/analyzers_manager/models.py +++ b/api_app/analyzers_manager/models.py @@ -1,12 +1,15 @@ # This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix # See the file 'LICENSE' for copying permission. - +import json from logging import getLogger -from typing import Optional +from typing import Dict, Optional, Type, Union -from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation +from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.db import models +from django.db.models import ForeignKey from api_app.analyzers_manager.constants import ( HashChoices, @@ -16,6 +19,12 @@ from api_app.analyzers_manager.exceptions import AnalyzerConfigurationException from api_app.analyzers_manager.queryset import AnalyzerReportQuerySet from api_app.choices import TLP, PythonModuleBasePaths +from api_app.data_model_manager.models import ( + BaseDataModel, + DomainDataModel, + FileDataModel, + IPDataModel, +) from api_app.fields import ChoiceArrayField from api_app.models import AbstractReport, PythonConfig, PythonModule @@ -27,11 +36,132 @@ class AnalyzerReport(AbstractReport): config = models.ForeignKey( "AnalyzerConfig", related_name="reports", null=False, on_delete=models.CASCADE ) + data_model_content_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + limit_choices_to={ + "app_label": "data_model_manager", + }, + null=True, + editable=False, + ) + data_model_object_id = models.IntegerField(null=True, editable=False) + data_model = GenericForeignKey("data_model_content_type", "data_model_object_id") class Meta: unique_together = [("config", "job")] indexes = AbstractReport.Meta.indexes + def clean(self): + if self.data_model_content_type: + if ( + ContentType.objects.get_for_model(model=self.data_model_class) + != self.data_model_content_type + ): + raise ValidationError("Wrong data model for this report") + + @classmethod + def get_data_model_class(cls, job) -> Type[BaseDataModel]: + if job.is_sample or job.observable_classification == ObservableTypes.HASH.value: + return FileDataModel + if job.observable_classification == ObservableTypes.IP.value: + return IPDataModel + if ( + job.observable_classification == ObservableTypes.DOMAIN.value + or job.observable_classification == ObservableTypes.URL.value + ): + return DomainDataModel + raise NotImplementedError( + f"Unable to find data model for {job.observable_classification}" + ) + + @property + def data_model_class(self) -> Type[BaseDataModel]: + return self.get_data_model_class(self.job) + + def _validation_before_data_model(self) -> bool: + if not self.status == self.STATUSES.SUCCESS.value: + logger.info( + f"Skipping data model of {self.config.name} for job {self.config_id} because status is " + f"{self.status}" + ) + return False + data_model_keys = self.data_model_class.get_fields().keys() + for data_model_key in self.config.mapping_data_model.values(): + if data_model_key not in data_model_keys: + self.errors.append( + f"Field {data_model_key} not available in {self.data_model_class.__name__}" + ) + return True + + def _create_data_model_dictionary(self) -> Dict: + """ + Returns a dictionary that will be used to create an initial data model for the report. + + It uses the mapping_data_model field of the AnalyzerConfig to map the fields of the report with the fields of the data model. + + For example, if we have + + analyzer_report = { + "family": "MalwareFamily" + } + + mapping_data_model = {"family": "malware_family"} + + the method returns + result = {"malware_family": "MalwareFamily"}. + """ + result = {} + data_model_fields = self.data_model_class.get_fields() + logger.debug(f"Mapping is {json.dumps(self.config.mapping_data_model)}") + for report_key, data_model_key in self.config.mapping_data_model.items(): + # this is a constant + if report_key.startswith("$"): + value = report_key + # this is a field of the report + else: + try: + value = self.get_value(self.report, report_key.split(".")) + logger.debug(f"Retrieved {value} from key {report_key}") + except Exception: + # validation + self.errors.append(f"Field {report_key} not available in report") + continue + + # create the related object if necessary + if isinstance(data_model_fields[data_model_key], ForeignKey): + # to create an object we need at least a dictionary + if not isinstance(value, dict): + self.errors.append( + f"Field {report_key} has type {type(report_key)} while a dictionary is expected" + ) + continue + value, _ = data_model_fields[ + data_model_key + ].related_model.objects.get_or_create(**value) + result[data_model_key] = value + elif isinstance(data_model_fields[data_model_key], ArrayField): + if data_model_key not in result: + result[data_model_key] = [] + if isinstance(value, list): + result[data_model_key].extend(value) + elif isinstance(value, dict): + result[data_model_key].extend(list(value.keys())) + else: + result[data_model_key].append(value) + else: + result[data_model_key] = value + return result + + def create_data_model(self) -> Optional[BaseDataModel]: + if not self._validation_before_data_model(): + return None + dictionary = self._create_data_model_dictionary() + data_model = self.data_model_class.objects.create(**dictionary) + self.data_model = data_model + self.save() + return data_model + class MimeTypes(models.TextChoices): # IMPORTANT! in case you update this Enum remember to update also the frontend @@ -121,7 +251,7 @@ def _calculate_from_filename(cls, file_name: str) -> Optional["MimeTypes"]: return mimetype @classmethod - def calculate(cls, file_pointer, file_name) -> str: + def calculate(cls, buffer: Union[bytes, str], file_name: str) -> str: from magic import from_buffer as magic_from_buffer mimetype = None @@ -129,8 +259,9 @@ def calculate(cls, file_pointer, file_name) -> str: mimetype = cls._calculate_from_filename(file_name) if mimetype is None: - buffer = file_pointer.read() - mimetype = magic_from_buffer(buffer, mime=True) + mimetype = magic_from_buffer( + buffer.encode() if isinstance(buffer, str) else buffer, mime=True + ) logger.debug(f"mimetype is {mimetype}") try: mimetype = cls(mimetype) @@ -188,6 +319,11 @@ class AnalyzerConfig(PythonConfig): orgs_configuration = GenericRelation( "api_app.OrganizationPluginConfiguration", related_name="%(class)s" ) + mapping_data_model = models.JSONField( + default=dict, + help_text="Mapping analyzer_report_key: data_model_key. Keys preceded by the symbol $ will be considered as constants.", + blank=True, + ) @classmethod @property diff --git a/api_app/analyzers_manager/observable_analyzers/abuseipdb.py b/api_app/analyzers_manager/observable_analyzers/abuseipdb.py index 2f5b975f..81bd5e5f 100644 --- a/api_app/analyzers_manager/observable_analyzers/abuseipdb.py +++ b/api_app/analyzers_manager/observable_analyzers/abuseipdb.py @@ -4,6 +4,7 @@ import requests from api_app.analyzers_manager.classes import ObservableAnalyzer +from api_app.analyzers_manager.models import AnalyzerReport from tests.mock_utils import MockUpResponse, if_mock_connections, patch @@ -93,3 +94,14 @@ def _monkeypatch(cls): ) ] return super()._monkeypatch(patches=patches) + + def _update_data_model(self, data_model) -> None: + super()._update_data_model(data_model) + report_data = self.report.report.get("data", {}) + if report_data.get("totalReports", 0): + self.report: AnalyzerReport + if report_data["isWhitelisted"]: + evaluation = self.report.data_model_class.EVALUATIONS.TRUSTED.value + else: + evaluation = self.report.data_model_class.EVALUATIONS.MALICIOUS.value + data_model.evaluation = evaluation diff --git a/api_app/analyzers_manager/observable_analyzers/crowdsec.py b/api_app/analyzers_manager/observable_analyzers/crowdsec.py index 5efd4a5c..b07bd54f 100644 --- a/api_app/analyzers_manager/observable_analyzers/crowdsec.py +++ b/api_app/analyzers_manager/observable_analyzers/crowdsec.py @@ -1,12 +1,16 @@ # This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix # See the file 'LICENSE' for copying permission. +import logging import requests from django.conf import settings from api_app.analyzers_manager.classes import ObservableAnalyzer +from api_app.data_model_manager.enums import DataModelTags from tests.mock_utils import MockUpResponse, if_mock_connections, patch +logger = logging.getLogger(__name__) + class Crowdsec(ObservableAnalyzer): _api_key_name: str @@ -31,6 +35,82 @@ def run(self): result["link"] = f"https://app.crowdsec.net/cti/{self.observable_name}" return result + def _do_create_data_model(self): + return super()._do_create_data_model() and not self.report.report.get( + "not_found", False + ) + + def _update_data_model(self, data_model): + from api_app.analyzers_manager.models import AnalyzerReport + + self.report: AnalyzerReport + super()._update_data_model(data_model) + + classifications = self.report.report.get("classifications", {}).get( + "classifications", [] + ) + for classification in classifications: + label = classification.get("label", "") + if label in ["Legit scanner", "Known Security Company", "Known CERT"]: + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.TRUSTED.value + ) + elif label in ["Likely Botnet", "CrowdSec Community Blocklist"]: + data_model.additional_info = {"classifications": classifications} + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.CLEAN.value + ) + elif "Proxy" in label or "VPN" in label: + data_model.tags = [DataModelTags.ANONYMIZER] + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.CLEAN.value + ) + elif label in ["TOR exit node"]: + data_model.tags = [ + DataModelTags.ANONYMIZER, + DataModelTags.TOR_EXIT_NODE, + ] + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.CLEAN.value + ) + + highest_total_score = max( + [ + values["total"] + for key, values in self.report.report.get("scores", {}).items() + ], + default=0, + ) + if ( + data_model.evaluation + != self.report.data_model_class.EVALUATIONS.TRUSTED.value + ): + if highest_total_score <= 1: + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.CLEAN.value + ) + elif 1 < highest_total_score <= 3: + highest_trust_score = max( + [ + values["trust"] + for key, values in self.report.report.get("scores", {}).items() + ] + ) + if highest_trust_score >= 4: + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.MALICIOUS.value + ) + else: + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.SUSPICIOUS.value + ) + elif 3 < highest_total_score <= 5: + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.MALICIOUS.value + ) + else: + logger.error(f"unexpected score: {highest_total_score}") + @classmethod def _monkeypatch(cls): patches = [ diff --git a/api_app/analyzers_manager/observable_analyzers/greynoiseintel.py b/api_app/analyzers_manager/observable_analyzers/greynoiseintel.py index c810eabb..86d1f8fd 100644 --- a/api_app/analyzers_manager/observable_analyzers/greynoiseintel.py +++ b/api_app/analyzers_manager/observable_analyzers/greynoiseintel.py @@ -69,6 +69,49 @@ def run(self): return response + def _do_create_data_model(self): + return super()._do_create_data_model() and ( + self.report.report.get("riot", False) + or self.report.report.get("noise", False) + ) + + def _update_data_model(self, data_model): + from api_app.analyzers_manager.models import AnalyzerReport + + super()._update_data_model(data_model) + classification = self.report.report.get("classification", None) + riot = self.report.report.get("riot", None) + noise = self.report.report.get("noise", None) + if classification: + classification = classification.lower() + self.report: AnalyzerReport + if ( + classification + == self.report.data_model_class.EVALUATIONS.MALICIOUS.value + ): + if not noise: + logger.error("malicious IP is not a noise!?! How is this possible") + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.MALICIOUS.value + ) + elif classification == "unknown": + if riot: + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.CLEAN.value + ) + elif noise: + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.MALICIOUS.value + ) + elif classification == "benign": + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.TRUSTED.value + ) + else: + logger.error( + f"there should not be other types of classification. Classification found: {classification}" + ) + @classmethod def _monkeypatch(cls): patches = [ diff --git a/api_app/analyzers_manager/observable_analyzers/maxmind.py b/api_app/analyzers_manager/observable_analyzers/maxmind.py index 087af357..7f0d795e 100644 --- a/api_app/analyzers_manager/observable_analyzers/maxmind.py +++ b/api_app/analyzers_manager/observable_analyzers/maxmind.py @@ -228,3 +228,29 @@ def _monkeypatch(cls): # completely skip because does not work without connection. patches = [if_mock_connections(patch.object(cls, "run", return_value={}))] return super()._monkeypatch(patches=patches) + + def _update_data_model(self, data_model) -> None: + from api_app.analyzers_manager.models import AnalyzerReport + + super()._update_data_model(data_model) + org = self.report.report.get("autonomous_system_organization", None) + if org: + org = org.lower() + self.report: AnalyzerReport + if org in ["fastly", "cloudflare", "akamai"]: + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.CLEAN.value + ) + elif org in [ + "zscaler", + "palo alto networks", + "microdata service srl", + "forcepoint", + ]: + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.TRUSTED.value + ) + elif org in ["stark industries"]: + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.SUSPICIOUS.value + ) diff --git a/api_app/analyzers_manager/observable_analyzers/nvd_cve.py b/api_app/analyzers_manager/observable_analyzers/nvd_cve.py index 52516d53..32b10cbe 100644 --- a/api_app/analyzers_manager/observable_analyzers/nvd_cve.py +++ b/api_app/analyzers_manager/observable_analyzers/nvd_cve.py @@ -1,7 +1,6 @@ import re import requests -from django.conf import settings from api_app.analyzers_manager.classes import AnalyzerRunException, ObservableAnalyzer from tests.mock_utils import MockUpResponse, if_mock_connections, patch @@ -23,7 +22,7 @@ def run(self): try: # Validate if CVE format is correct E.g CVE-2014-1234 or cve-2022-1234567 - if not settings.STAGE_CI and not re.match( + if not re.match( self.cve_pattern, self.observable_name, flags=re.IGNORECASE ): raise ValueError(f"Invalid CVE format: {self.observable_name}") diff --git a/api_app/analyzers_manager/observable_analyzers/phishing/__init__.py b/api_app/analyzers_manager/observable_analyzers/phishing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api_app/analyzers_manager/observable_analyzers/phishing/phishing_extractor.py b/api_app/analyzers_manager/observable_analyzers/phishing/phishing_extractor.py new file mode 100644 index 00000000..ae6a67d3 --- /dev/null +++ b/api_app/analyzers_manager/observable_analyzers/phishing/phishing_extractor.py @@ -0,0 +1,54 @@ +from logging import getLogger +from typing import Dict + +from api_app.analyzers_manager.classes import DockerBasedAnalyzer, ObservableAnalyzer +from api_app.analyzers_manager.constants import ObservableTypes +from api_app.models import PythonConfig + +logger = getLogger(__name__) + + +class PhishingExtractor(ObservableAnalyzer, DockerBasedAnalyzer): + name: str = "Phishing_Extractor" + url: str = "http://phishing_analyzers:4005/phishing_extractor" + max_tries: int = 20 + poll_distance: int = 3 + + proxy_address: str = "" + window_width: int + window_height: int + + def __init__( + self, + config: PythonConfig, + **kwargs, + ): + super().__init__(config, **kwargs) + self.args: [] = [] + + def config(self, runtime_configuration: Dict): + super().config(runtime_configuration) + target = self.observable_name + # handle domain names by appending default + # protocol. selenium opens only URL types + if self.observable_classification == ObservableTypes.DOMAIN: + target = "http://" + target + self.args.append(f"--target={target}") + if self.proxy_address: + self.args.append(f"--proxy_address={self.proxy_address}") + if self.window_width: + self.args.append(f"--window_width={self.window_width}") + if self.window_height: + self.args.append(f"--window_height={self.window_height}") + + def run(self): + req_data: {} = { + "args": [ + *self.args, + ], + } + logger.info(f"sending {req_data=} to {self.url}") + return self._docker_run(req_data) + + def update(self) -> bool: + pass diff --git a/api_app/analyzers_manager/observable_analyzers/talos.py b/api_app/analyzers_manager/observable_analyzers/talos.py index 74e79df8..d840ebf9 100644 --- a/api_app/analyzers_manager/observable_analyzers/talos.py +++ b/api_app/analyzers_manager/observable_analyzers/talos.py @@ -58,6 +58,22 @@ def update(cls) -> bool: return False + def _do_create_data_model(self): + return super()._do_create_data_model() + + def _update_data_model(self, data_model): + super()._update_data_model(data_model) + found = self.report.report.get("found", False) + if found: + data_model.external_references.append( + f"https://www.talosintelligence.com/reputation_center/lookup?search={self.report.job.observable_name}" + ) + data_model.evaluation = ( + self.report.data_model_class.EVALUATIONS.MALICIOUS.value + ) + else: + data_model.evaluation = self.report.data_model_class.EVALUATIONS.CLEAN.value + @classmethod def _monkeypatch(cls): patches = [ diff --git a/api_app/analyzers_manager/observable_analyzers/tor.py b/api_app/analyzers_manager/observable_analyzers/tor.py index 61562654..6bda339c 100644 --- a/api_app/analyzers_manager/observable_analyzers/tor.py +++ b/api_app/analyzers_manager/observable_analyzers/tor.py @@ -19,6 +19,9 @@ class Tor(classes.ObservableAnalyzer): + def _do_create_data_model(self) -> bool: + return super()._do_create_data_model() and self.report.report["found"] + def run(self): result = {"found": False} if not os.path.isfile(database_location) and not self.update(): diff --git a/api_app/analyzers_manager/observable_analyzers/urldna.py b/api_app/analyzers_manager/observable_analyzers/urldna.py new file mode 100644 index 00000000..e0cc56fb --- /dev/null +++ b/api_app/analyzers_manager/observable_analyzers/urldna.py @@ -0,0 +1,122 @@ +# This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix +# See the file 'LICENSE' for copying permission. + +import logging +import time + +import requests + +from api_app.analyzers_manager.classes import ObservableAnalyzer +from api_app.analyzers_manager.exceptions import AnalyzerRunException +from tests.mock_utils import MockUpResponse, if_mock_connections, patch + +logger = logging.getLogger(__name__) + + +class UrlDNA(ObservableAnalyzer): + url: str = "https://api.urldna.io" + + urldna_analysis: str + _api_key_name: str + + # Scan options + device = "DESKTOP" + user_agent = ( + "Mozilla/5.0 (Windows NT 10.0;Win64;x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36" + ) + viewport_width = 1920 + viewport_height = 1080 + waiting_time = 5 + private_scan = False + scanned_from = "DEFAULT" + + @classmethod + def update(cls) -> bool: + pass + + def run(self): + headers = { + "Content-Type": "application/json", + "User-Agent": "ThreatMatrix", + "Authorization": self._api_key_name, + } + + self.session = requests.Session() + self.session.headers = headers + if self.urldna_analysis == "SEARCH": + result = self.__urldna_search() + elif self.urldna_analysis == "NEW_SCAN": + scan_id = self.__urldna_new_scan() + result = self.__poll_for_result(scan_id) + else: + raise AnalyzerRunException( + f"Not supported analysis_type {self.urldna_analysis}. " + "Supported are 'SEARCH' and 'NEW_SCAN'." + ) + return result + + def __urldna_new_scan(self) -> str: + submitted_url = self.observable_name + data = { + "submitted_url": submitted_url, + "device": self.device, + "user_agent": self.user_agent, + "width": self.viewport_width, + "height": self.viewport_height, + "scanned_from": self.scanned_from, + "waiting_time": self.waiting_time, + "private_scan": self.private_scan, + } + uri = "/scan" + response = self.session.post(self.url + uri, json=data) + if response.status_code == 500: + error_description = response.content + raise requests.HTTPError(error_description) + response.raise_for_status() + return response.json().get("id", "") + + def __poll_for_result(self, scan_id): + uri = f"/scan/{scan_id}" + max_tries = 10 + poll_distance = 2 + result = {} + time.sleep(10) + for chance in range(max_tries): + if chance: + time.sleep(poll_distance) + resp = self.session.get(self.url + uri) + if resp.json().get("scan", {}).get("status") in ["RUNNING", "PENDING"]: + continue + result = resp.json() + break + return result + + def __urldna_search(self): + uri = "/search" + data = {"query": f"{self.observable_name}"} + if self.observable_classification == self.ObservableTypes.URL: + data["query"] = f"submitted_url = {self.observable_name}" + elif self.observable_classification == self.ObservableTypes.DOMAIN: + data["query"] = f"domain = {self.observable_name}" + elif self.observable_classification == self.ObservableTypes.IP: + data["query"] = f"ip = {self.observable_name}" + else: + data["query"] = f"{self.observable_name}" + resp = self.session.post(self.url + uri, json=data) + resp.raise_for_status() + result = resp.json() + return result + + @classmethod + def _monkeypatch(cls): + patches = [ + if_mock_connections( + patch( + "requests.Session.post", + return_value=MockUpResponse({"api": "test"}, 200), + ), + patch("requests.Session.get", return_value=MockUpResponse({}, 200)), + ) + ] + return super()._monkeypatch(patches=patches) diff --git a/api_app/analyzers_manager/observable_analyzers/urlhaus.py b/api_app/analyzers_manager/observable_analyzers/urlhaus.py index edd2c18f..6d734aeb 100644 --- a/api_app/analyzers_manager/observable_analyzers/urlhaus.py +++ b/api_app/analyzers_manager/observable_analyzers/urlhaus.py @@ -39,6 +39,12 @@ def run(self): return response.json() + def _do_create_data_model(self) -> bool: + return ( + super()._do_create_data_model() + and self.report.report.get("query_status", "no_results") != "no_results" + ) + @classmethod def _monkeypatch(cls): patches = [ diff --git a/api_app/analyzers_manager/observable_analyzers/vt/vt3_sample_download.py b/api_app/analyzers_manager/observable_analyzers/vt/vt3_sample_download.py new file mode 100644 index 00000000..9d490f2f --- /dev/null +++ b/api_app/analyzers_manager/observable_analyzers/vt/vt3_sample_download.py @@ -0,0 +1,33 @@ +# This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix +# See the file 'LICENSE' for copying permission. + +from api_app.analyzers_manager.classes import ObservableAnalyzer +from api_app.mixins import VirusTotalv3AnalyzerMixin +from tests.mock_utils import MockUpResponse, if_mock_connections, patch + + +class VirusTotalv3SampleDownload(ObservableAnalyzer, VirusTotalv3AnalyzerMixin): + @classmethod + def update(cls) -> bool: + pass + + def run(self): + return {"data": self._vt_download_file(self.observable_name).decode()} + + @classmethod + def _monkeypatch(cls): + patches = [ + if_mock_connections( + patch( + "requests.get", + side_effect=[ + MockUpResponse( + {}, + 200, + text="hello world", + ), + ], + ), + ) + ] + return super()._monkeypatch(patches=patches) diff --git a/api_app/analyzers_manager/queryset.py b/api_app/analyzers_manager/queryset.py index ce6da8fd..e1012c5a 100644 --- a/api_app/analyzers_manager/queryset.py +++ b/api_app/analyzers_manager/queryset.py @@ -1,5 +1,7 @@ from typing import TYPE_CHECKING, Type +from django.db.models import QuerySet + from api_app.queryset import AbstractReportQuerySet if TYPE_CHECKING: @@ -12,3 +14,9 @@ def _get_bi_serializer_class(cls) -> Type["AnalyzerReportBISerializer"]: from api_app.analyzers_manager.serializers import AnalyzerReportBISerializer return AnalyzerReportBISerializer + + def get_data_models(self, job) -> QuerySet: + DataModel = self.model.get_data_model_class(job) # noqa + return DataModel.objects.filter( + pk__in=self.values_list("data_model_object_id", flat=True) + ) diff --git a/api_app/classes.py b/api_app/classes.py index 1d88fb64..7a7d4a69 100644 --- a/api_app/classes.py +++ b/api_app/classes.py @@ -202,7 +202,7 @@ def after_run_success(self, content: typing.Any): report_content.append(n) self.report.report = report_content - self.report.status = self.report.Status.SUCCESS.value + self.report.status = self.report.STATUSES.SUCCESS.value self.report.save(update_fields=["status", "report"]) def log_error(self, e): @@ -230,7 +230,7 @@ def after_run_failed(self, e: Exception): e (Exception): The exception that caused the failure. """ self.report.errors.append(str(e)) - self.report.status = self.report.Status.FAILED + self.report.status = self.report.STATUSES.FAILED self.report.save(update_fields=["status", "errors"]) if isinstance(e, HTTPError) and ( hasattr(e, "response") @@ -289,7 +289,7 @@ def start( """ self.job_id = job_id self.report: AbstractReport = self._config.generate_empty_report( - self._job, task_id, AbstractReport.Status.RUNNING.value + self._job, task_id, AbstractReport.STATUSES.RUNNING.value ) try: self.config(runtime_configuration) @@ -309,7 +309,7 @@ def _handle_exception(self, exc, is_base_err: bool = False) -> None: error_message = self.get_error_message(exc, is_base_err=is_base_err) logger.error(error_message) self.report.errors.append(str(exc)) - self.report.status = self.report.Status.FAILED + self.report.status = self.report.STATUSES.FAILED @classmethod def _monkeypatch(cls, patches: list = None) -> None: diff --git a/api_app/data_model_manager/__init__.py b/api_app/data_model_manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api_app/data_model_manager/admin.py b/api_app/data_model_manager/admin.py new file mode 100644 index 00000000..2b9819d1 --- /dev/null +++ b/api_app/data_model_manager/admin.py @@ -0,0 +1,61 @@ +from django.contrib import admin + +from api_app.admin import CustomAdminView +from api_app.data_model_manager.models import ( + DomainDataModel, + FileDataModel, + IPDataModel, +) + + +class BaseDataModelAdminView(CustomAdminView): + list_display = ( + "pk", + "evaluation", + "external_references", + "related_threats", + "tags", + "malware_family", + "additional_info", + ) + + +@admin.register(DomainDataModel) +class DomainDataModelAdminView(BaseDataModelAdminView): + list_display = BaseDataModelAdminView.list_display + ("rank", "get_ietf_report") + + @admin.display(description="IETF Reports") + def get_ietf_report(self, instance: DomainDataModel): + return list(map(str, instance.ietf_report.all())) + + +@admin.register(IPDataModel) +class IPDataModelAdminView(BaseDataModelAdminView): + list_display = BaseDataModelAdminView.list_display + ( + "get_ietf_report", + "asn", + "asn_rank", + "certificates", + "org_name", + "country_code", + "registered_country_code", + "isp", + ) + + @admin.display(description="IETF Reports") + def get_ietf_report(self, instance: IPDataModel): + return list(map(str, instance.ietf_report.all())) + + +@admin.register(FileDataModel) +class FileDataModelAdminView(BaseDataModelAdminView): + list_display = BaseDataModelAdminView.list_display + ( + "get_signatures", + "comments", + "file_information", + "stats", + ) + + @admin.display(description="Signatures") + def get_signatures(self, instance: FileDataModel): + return list(map(str, instance.signatures.all())) diff --git a/api_app/data_model_manager/apps.py b/api_app/data_model_manager/apps.py new file mode 100644 index 00000000..b3aef5ee --- /dev/null +++ b/api_app/data_model_manager/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class DataModelConfig(AppConfig): + name = "api_app.data_model_manager" diff --git a/api_app/data_model_manager/enums.py b/api_app/data_model_manager/enums.py new file mode 100644 index 00000000..45507607 --- /dev/null +++ b/api_app/data_model_manager/enums.py @@ -0,0 +1,23 @@ +from django.db.models import Choices + + +class SignatureProviderChoices(Choices): + CLAMAV = "clam_av" + SIGMA = "sigma" + YARA = "yara" + SURICATA = "suricata" + + +class DataModelTags(Choices): + PHISHING = "phishing" + MALWARE = "malware" + SOCIAL_ENGINEERING = "social_engineering" + ANONYMIZER = "anonymizer" + TOR_EXIT_NODE = "tor_exit_node" + + +class DataModelEvaluations(Choices): + TRUSTED = "trusted" + CLEAN = "clean" + SUSPICIOUS = "suspicious" + MALICIOUS = "malicious" diff --git a/api_app/data_model_manager/fields.py b/api_app/data_model_manager/fields.py new file mode 100644 index 00000000..e5f93df6 --- /dev/null +++ b/api_app/data_model_manager/fields.py @@ -0,0 +1,19 @@ +from typing import Any + +from django.contrib.postgres.fields import ArrayField +from django.db import models + + +class SetField(ArrayField): + def to_python(self, value): + result = super().to_python(value) + return list(set(result)) + + +class LowercaseCharField(models.CharField): + + def to_python(self, value: Any): + result = super().to_python(value) + if result and isinstance(result, str): + return result.lower() + return result diff --git a/api_app/data_model_manager/migrations/0001_initial.py b/api_app/data_model_manager/migrations/0001_initial.py new file mode 100644 index 00000000..2daec139 --- /dev/null +++ b/api_app/data_model_manager/migrations/0001_initial.py @@ -0,0 +1,354 @@ +# Generated by Django 4.2.16 on 2024-11-08 09:38 + +import django.contrib.postgres.fields +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + +import api_app.data_model_manager.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="IETFReport", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "rrname", + api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + ), + ( + "rrtype", + api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + ), + ( + "rdata", + django.contrib.postgres.fields.ArrayField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + size=None, + ), + ), + ("time_first", models.DateTimeField()), + ("time_last", models.DateTimeField()), + ], + options={ + "unique_together": {("rrname", "rrtype", "rdata")}, + }, + ), + migrations.CreateModel( + name="Signature", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "provider", + api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + ), + ("url", models.URLField(blank=True, default=None, null=True)), + ("score", models.PositiveIntegerField(default=0)), + ("signature", models.JSONField()), + ], + ), + migrations.CreateModel( + name="IPDataModel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "evaluation", + api_app.data_model_manager.fields.LowercaseCharField( + blank=True, default=None, max_length=100, null=True + ), + ), + ( + "external_references", + django.contrib.postgres.fields.ArrayField( + base_field=models.URLField(), + blank=True, + default=list, + size=None, + ), + ), + ( + "related_threats", + django.contrib.postgres.fields.ArrayField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=list, + size=None, + ), + ), + ( + "tags", + django.contrib.postgres.fields.ArrayField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=None, + null=True, + size=None, + ), + ), + ( + "malware_family", + api_app.data_model_manager.fields.LowercaseCharField( + blank=True, default=None, max_length=100, null=True + ), + ), + ("additional_info", models.JSONField(default=dict)), + ("date", models.DateTimeField(default=django.utils.timezone.now)), + ("asn", models.IntegerField(blank=True, default=None, null=True)), + ( + "asn_rank", + models.DecimalField( + blank=True, + decimal_places=2, + default=None, + max_digits=3, + null=True, + ), + ), + ("certificates", models.JSONField(blank=True, default=None, null=True)), + ( + "org_name", + api_app.data_model_manager.fields.LowercaseCharField( + blank=True, default=None, max_length=100, null=True + ), + ), + ( + "country_code", + api_app.data_model_manager.fields.LowercaseCharField( + blank=True, default=None, max_length=100, null=True + ), + ), + ( + "registered_country_code", + api_app.data_model_manager.fields.LowercaseCharField( + blank=True, default=None, max_length=100, null=True + ), + ), + ( + "isp", + api_app.data_model_manager.fields.LowercaseCharField( + blank=True, default=None, max_length=100, null=True + ), + ), + ( + "resolutions", + django.contrib.postgres.fields.ArrayField( + base_field=models.URLField(), default=list, size=None + ), + ), + ( + "ietf_report", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="ips", + to="data_model_manager.ietfreport", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="FileDataModel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "evaluation", + api_app.data_model_manager.fields.LowercaseCharField( + blank=True, default=None, max_length=100, null=True + ), + ), + ( + "external_references", + django.contrib.postgres.fields.ArrayField( + base_field=models.URLField(), + blank=True, + default=list, + size=None, + ), + ), + ( + "related_threats", + django.contrib.postgres.fields.ArrayField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=list, + size=None, + ), + ), + ( + "tags", + django.contrib.postgres.fields.ArrayField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=None, + null=True, + size=None, + ), + ), + ( + "malware_family", + api_app.data_model_manager.fields.LowercaseCharField( + blank=True, default=None, max_length=100, null=True + ), + ), + ("additional_info", models.JSONField(default=dict)), + ("date", models.DateTimeField(default=django.utils.timezone.now)), + ( + "comments", + django.contrib.postgres.fields.ArrayField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=list, + size=None, + ), + ), + ("file_information", models.JSONField(blank=True, default=dict)), + ("stats", models.JSONField(blank=True, default=dict)), + ( + "signatures", + models.ManyToManyField( + related_name="files", to="data_model_manager.signature" + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="DomainDataModel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "evaluation", + api_app.data_model_manager.fields.LowercaseCharField( + blank=True, default=None, max_length=100, null=True + ), + ), + ( + "external_references", + django.contrib.postgres.fields.ArrayField( + base_field=models.URLField(), + blank=True, + default=list, + size=None, + ), + ), + ( + "related_threats", + django.contrib.postgres.fields.ArrayField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=list, + size=None, + ), + ), + ( + "tags", + django.contrib.postgres.fields.ArrayField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=None, + null=True, + size=None, + ), + ), + ( + "malware_family", + api_app.data_model_manager.fields.LowercaseCharField( + blank=True, default=None, max_length=100, null=True + ), + ), + ("additional_info", models.JSONField(default=dict)), + ("date", models.DateTimeField(default=django.utils.timezone.now)), + ("rank", models.IntegerField(blank=True, default=None, null=True)), + ( + "ietf_report", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="domains", + to="data_model_manager.ietfreport", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/api_app/data_model_manager/migrations/0002_domaindatamodel_resolutions_and_more.py b/api_app/data_model_manager/migrations/0002_domaindatamodel_resolutions_and_more.py new file mode 100644 index 00000000..c2bdf6b5 --- /dev/null +++ b/api_app/data_model_manager/migrations/0002_domaindatamodel_resolutions_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.16 on 2024-11-08 11:42 + +import django.contrib.postgres.fields +from django.db import migrations, models + +import api_app.data_model_manager.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("data_model_manager", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="domaindatamodel", + name="resolutions", + field=django.contrib.postgres.fields.ArrayField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + default=list, + size=None, + ), + ), + migrations.RemoveField( + model_name="domaindatamodel", + name="ietf_report", + ), + migrations.AddField( + model_name="domaindatamodel", + name="ietf_report", + field=models.ManyToManyField( + related_name="domains", to="data_model_manager.ietfreport" + ), + ), + ] diff --git a/api_app/data_model_manager/migrations/0003_remove_ipdatamodel_ietf_report_and_more.py b/api_app/data_model_manager/migrations/0003_remove_ipdatamodel_ietf_report_and_more.py new file mode 100644 index 00000000..8e056097 --- /dev/null +++ b/api_app/data_model_manager/migrations/0003_remove_ipdatamodel_ietf_report_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.16 on 2024-11-08 17:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("data_model_manager", "0002_domaindatamodel_resolutions_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="ipdatamodel", + name="ietf_report", + ), + migrations.AddField( + model_name="ipdatamodel", + name="ietf_report", + field=models.ManyToManyField( + related_name="ips", to="data_model_manager.ietfreport" + ), + ), + ] diff --git a/api_app/data_model_manager/migrations/0004_alter_domaindatamodel_evaluation_and_more.py b/api_app/data_model_manager/migrations/0004_alter_domaindatamodel_evaluation_and_more.py new file mode 100644 index 00000000..8dff28f4 --- /dev/null +++ b/api_app/data_model_manager/migrations/0004_alter_domaindatamodel_evaluation_and_more.py @@ -0,0 +1,63 @@ +# Generated by Django 4.2.16 on 2024-11-29 09:06 + +from django.db import migrations + +import api_app.data_model_manager.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("data_model_manager", "0003_remove_ipdatamodel_ietf_report_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="domaindatamodel", + name="evaluation", + field=api_app.data_model_manager.fields.LowercaseCharField( + blank=True, + choices=[ + ("trusted", "Trusted"), + ("clean", "Clean"), + ("suspicious", "Suspicious"), + ("malicious", "Malicious"), + ], + default=None, + max_length=100, + null=True, + ), + ), + migrations.AlterField( + model_name="filedatamodel", + name="evaluation", + field=api_app.data_model_manager.fields.LowercaseCharField( + blank=True, + choices=[ + ("trusted", "Trusted"), + ("clean", "Clean"), + ("suspicious", "Suspicious"), + ("malicious", "Malicious"), + ], + default=None, + max_length=100, + null=True, + ), + ), + migrations.AlterField( + model_name="ipdatamodel", + name="evaluation", + field=api_app.data_model_manager.fields.LowercaseCharField( + blank=True, + choices=[ + ("trusted", "Trusted"), + ("clean", "Clean"), + ("suspicious", "Suspicious"), + ("malicious", "Malicious"), + ], + default=None, + max_length=100, + null=True, + ), + ), + ] diff --git a/api_app/data_model_manager/migrations/0005_alter_domaindatamodel_external_references_and_more.py b/api_app/data_model_manager/migrations/0005_alter_domaindatamodel_external_references_and_more.py new file mode 100644 index 00000000..f6e06d7a --- /dev/null +++ b/api_app/data_model_manager/migrations/0005_alter_domaindatamodel_external_references_and_more.py @@ -0,0 +1,141 @@ +# Generated by Django 4.2.16 on 2024-12-06 09:28 + +from django.db import migrations, models + +import api_app.data_model_manager.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("data_model_manager", "0004_alter_domaindatamodel_evaluation_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="domaindatamodel", + name="external_references", + field=api_app.data_model_manager.fields.SetField( + base_field=models.URLField(), blank=True, default=list, size=None + ), + ), + migrations.AlterField( + model_name="domaindatamodel", + name="related_threats", + field=api_app.data_model_manager.fields.SetField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=list, + size=None, + ), + ), + migrations.AlterField( + model_name="domaindatamodel", + name="resolutions", + field=api_app.data_model_manager.fields.SetField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + default=list, + size=None, + ), + ), + migrations.AlterField( + model_name="domaindatamodel", + name="tags", + field=api_app.data_model_manager.fields.SetField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=None, + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="filedatamodel", + name="comments", + field=api_app.data_model_manager.fields.SetField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=list, + size=None, + ), + ), + migrations.AlterField( + model_name="filedatamodel", + name="external_references", + field=api_app.data_model_manager.fields.SetField( + base_field=models.URLField(), blank=True, default=list, size=None + ), + ), + migrations.AlterField( + model_name="filedatamodel", + name="related_threats", + field=api_app.data_model_manager.fields.SetField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=list, + size=None, + ), + ), + migrations.AlterField( + model_name="filedatamodel", + name="tags", + field=api_app.data_model_manager.fields.SetField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=None, + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="ipdatamodel", + name="external_references", + field=api_app.data_model_manager.fields.SetField( + base_field=models.URLField(), blank=True, default=list, size=None + ), + ), + migrations.AlterField( + model_name="ipdatamodel", + name="related_threats", + field=api_app.data_model_manager.fields.SetField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=list, + size=None, + ), + ), + migrations.AlterField( + model_name="ipdatamodel", + name="resolutions", + field=api_app.data_model_manager.fields.SetField( + base_field=models.URLField(), default=list, size=None + ), + ), + migrations.AlterField( + model_name="ipdatamodel", + name="tags", + field=api_app.data_model_manager.fields.SetField( + base_field=api_app.data_model_manager.fields.LowercaseCharField( + max_length=100 + ), + blank=True, + default=None, + null=True, + size=None, + ), + ), + ] diff --git a/api_app/data_model_manager/migrations/__init__.py b/api_app/data_model_manager/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api_app/data_model_manager/models.py b/api_app/data_model_manager/models.py new file mode 100644 index 00000000..67bfd51f --- /dev/null +++ b/api_app/data_model_manager/models.py @@ -0,0 +1,206 @@ +import json +from typing import Dict, Type + +from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres import fields as pg_fields +from django.db import models +from django.utils.timezone import now +from rest_framework.serializers import ModelSerializer + +from api_app.data_model_manager.enums import ( + DataModelEvaluations, + DataModelTags, + SignatureProviderChoices, +) +from api_app.data_model_manager.fields import LowercaseCharField, SetField +from api_app.data_model_manager.queryset import BaseDataModelQuerySet +from certego_saas.apps.user.models import User + + +class IETFReport(models.Model): + rrname = LowercaseCharField(max_length=100) + rrtype = LowercaseCharField(max_length=100) + rdata = pg_fields.ArrayField(LowercaseCharField(max_length=100)) + time_first = models.DateTimeField() + time_last = models.DateTimeField() + + class Meta: + unique_together = ("rrname", "rrtype", "rdata") + + def __str__(self): + return json.dumps( + { + "rrname": self.rrname, + "rrtype": self.rrtype, + "rdata": self.rdata, + "time_first": self.time_first.strftime("%Y-%m-%d %H:%M:%S"), + "time_last": self.time_last.strftime("%Y-%m-%d %H:%M:%S"), + } + ) + + +class Signature(models.Model): + provider = LowercaseCharField(max_length=100) + url = models.URLField(default=None, null=True, blank=True) + score = models.PositiveIntegerField(default=0) + signature = models.JSONField() + + PROVIDERS = SignatureProviderChoices + + def __str__(self): + return f"{self.provider}: {json.dumps(self.signature)}" + + +class BaseDataModel(models.Model): + objects = BaseDataModelQuerySet.as_manager() + evaluation = LowercaseCharField( + max_length=100, + null=True, + blank=True, + default=None, + choices=DataModelEvaluations.choices, + ) # classification/verdict/found/score/malscore + # HybridAnalysisObservable (verdict), BasicMaliciousDetector (malicious), + # GoogleSafeBrowsing (malicious), Crowdsec (classifications), + # GreyNoise (classification), Cymru (found), Cuckoo (malscore), + # Intezer (verdict/sub_verdict), Triage (analysis.score), + # HybridAnalysisFileAnalyzer (classification_tags) + external_references = SetField( + models.URLField(), + blank=True, + default=list, + ) # link/external_references/permalink/domains + # Crowdsec (link), UrlHaus (external_references), BoxJs, + # Cuckoo (result_url/permalink), Intezer (link/analysis_url), + # MalwareBazaarFileAnalyzer (permalink/file_information.value), MwDB (permalink), + # StringsInfo (data), Triage (permalink), UnpacMe (permalink), XlmMacroDeobfuscator, + # Yara (report.list_el.url/rule_url), Yaraify (link), + # HybridAnalysisFileAnalyzer (domains), + # VirusTotalV3FileAnalyzer (data.relationships.contacted_urls/contacted_domains) + related_threats = SetField( + LowercaseCharField(max_length=100), default=list, blank=True + ) # threats/related_threats, used as a pointer to other IOCs + tags = SetField( + LowercaseCharField(max_length=100), null=True, blank=True, default=None + ) # used for generic tags like phishing, malware, social_engineering + # HybridAnalysisFileAnalyzer, MalwareBazaarFileAnalyzer, MwDB, + # VirusTotalV3FileAnalyzer (report.data.attributes.tags) + # GoogleSafeBrowsing, QuarkEngineAPK (crimes.crime) + malware_family = LowercaseCharField( + max_length=100, null=True, blank=True, default=None + ) # family/family_name/malware_family + # HybridAnalysisObservable, Intezer (family_name), Cuckoo, MwDB, + # Triage (analysis.family), UnpacMe (results.malware_id.malware_family), + # VirusTotalV3FileAnalyzer + # (attributes.last_analysis_results.list_el.results/attributes.names) + additional_info = models.JSONField( + default=dict + ) # field for additional information related to a specific analyzer + date = models.DateTimeField(default=now) + analyzers_report = GenericRelation( + to="analyzers_manager.AnalyzerReport", + object_id_field="data_model_object_id", + content_type_field="data_model_content_type", + ) + + TAGS = DataModelTags + + EVALUATIONS = DataModelEvaluations + + class Meta: + abstract = True + + @classmethod + def get_content_type(cls) -> ContentType: + return ContentType.objects.get_for_model(model=cls) + + @classmethod + def get_fields(cls) -> Dict: + return { + field.name: field for field in cls._meta.fields + cls._meta.many_to_many + } + + @property + def owner(self) -> User: + return self.analyzers_report.first().user + + @classmethod + def get_serializer(cls) -> Type[ModelSerializer]: + raise NotImplementedError() + + +class DomainDataModel(BaseDataModel): + ietf_report = models.ManyToManyField(IETFReport, related_name="domains") # pdns + rank = models.IntegerField(null=True, blank=True, default=None) # Tranco + resolutions = SetField(LowercaseCharField(max_length=100), default=list) + + @classmethod + def get_serializer(cls) -> Type[ModelSerializer]: + from api_app.data_model_manager.serializers import DomainDataModelSerializer + + return DomainDataModelSerializer + + +class IPDataModel(BaseDataModel): + ietf_report = models.ManyToManyField(IETFReport, related_name="ips") # pdns + asn = models.IntegerField( + null=True, blank=True, default=None + ) # BGPRanking, MaxMind + asn_rank = models.DecimalField( + null=True, blank=True, default=None, decimal_places=2, max_digits=3 + ) # BGPRanking + certificates = models.JSONField(null=True, blank=True, default=None) # CIRCL_PSSL + org_name = LowercaseCharField( + max_length=100, null=True, blank=True, default=None + ) # GreyNoise + country_code = LowercaseCharField( + max_length=100, null=True, blank=True, default=None + ) # MaxMind, AbuseIPDB + registered_country_code = LowercaseCharField( + max_length=100, null=True, blank=True, default=None + ) # MaxMind, AbuseIPDB + isp = LowercaseCharField(max_length=100, null=True, blank=True, default=None) + resolutions = SetField(models.URLField(), default=list) + # AbuseIPDB + # additional_info + # behavior = LowercaseCharField(max_length=100, null=True) # Crowdsec + # noise = models.BooleanField(null=True) # GreyNoise + # riot = models.BooleanField(null=True) # GreyNoise + + @classmethod + def get_serializer(cls) -> Type[ModelSerializer]: + from api_app.data_model_manager.serializers import IPDataModelSerializer + + return IPDataModelSerializer + + +class FileDataModel(BaseDataModel): + signatures = models.ManyToManyField( + Signature, related_name="files" + ) # ClamAvFileAnalyzer, + # MalwareBazaarFileAnalyzer (signatures/yara_rules), Yara (report.list_el.match) + # Yaraify (report.data.tasks.static_result) + comments = SetField( + LowercaseCharField(max_length=100), default=list, blank=True + ) # MalwareBazaarFileAnalyzer, + # VirusTotalV3FileAnalyzer (data.relationships.comments) + file_information = models.JSONField( + default=dict, blank=True + ) # MalwareBazaarFileAnalyzer, OneNoteInfo (files), + # QuarkEngineAPK (crimes.confidence, threat_level, total_score) + # RtfInfo (exploit_equation_editor, exploit_ole2link_vuln) + stats = models.JSONField(default=dict, blank=True) # PdfInfo (peepdf_stats) + # additional_info + # compromised_hosts = pg_fields.ArrayField( + # LowercaseCharField(max_length=100), null=True + # ) # HybridAnalysisFileAnalyzer + # pdfid_reports = models.JSONField(null=True) # PdfInfo + # imphash = LowercaseCharField(max_length=100, null=True) # PeInfo + # type = LowercaseCharField(max_length=100, null=True) # PeInfo + + @classmethod + def get_serializer(cls) -> Type[ModelSerializer]: + from api_app.data_model_manager.serializers import FileDataModelSerializer + + return FileDataModelSerializer diff --git a/api_app/data_model_manager/queryset.py b/api_app/data_model_manager/queryset.py new file mode 100644 index 00000000..1cdfa1af --- /dev/null +++ b/api_app/data_model_manager/queryset.py @@ -0,0 +1,8 @@ +from typing import Dict, List + +from django.db.models import QuerySet + + +class BaseDataModelQuerySet(QuerySet): + def serialize(self) -> List[Dict]: + return self.model.get_serializer()(self, many=True, read_only=True).data diff --git a/api_app/data_model_manager/serializers.py b/api_app/data_model_manager/serializers.py new file mode 100644 index 00000000..a2c323a5 --- /dev/null +++ b/api_app/data_model_manager/serializers.py @@ -0,0 +1,49 @@ +from rest_flex_fields import FlexFieldsModelSerializer +from rest_framework.relations import SlugRelatedField + +from api_app.data_model_manager.models import ( + DomainDataModel, + FileDataModel, + IETFReport, + IPDataModel, + Signature, +) + + +class IETFReportSerializer(FlexFieldsModelSerializer): + class Meta: + model = IETFReport + fields = "__all__" + + +class SignatureSerializer(FlexFieldsModelSerializer): + class Meta: + model = Signature + fields = "__all__" + + +class DomainDataModelSerializer(FlexFieldsModelSerializer): + ietf_report = IETFReportSerializer(many=True) + analyzers_report = SlugRelatedField(slug_field="pk", read_only=True, many=True) + + class Meta: + model = DomainDataModel + fields = "__all__" + + +class IPDataModelSerializer(FlexFieldsModelSerializer): + ietf_report = IETFReportSerializer(many=True) + analyzers_report = SlugRelatedField(slug_field="pk", read_only=True, many=True) + + class Meta: + model = IPDataModel + fields = "__all__" + + +class FileDataModelSerializer(FlexFieldsModelSerializer): + signatures = SignatureSerializer(many=True) + analyzers_report = SlugRelatedField(slug_field="pk", read_only=True, many=True) + + class Meta: + model = FileDataModel + fields = "__all__" diff --git a/api_app/data_model_manager/signals.py b/api_app/data_model_manager/signals.py new file mode 100644 index 00000000..e69de29b diff --git a/api_app/data_model_manager/urls.py b/api_app/data_model_manager/urls.py new file mode 100644 index 00000000..5aef9c4f --- /dev/null +++ b/api_app/data_model_manager/urls.py @@ -0,0 +1,22 @@ +# This file is a part of ThreatMatrix https://github.com/khulnasoft/ThreatMatrix +# See the file 'LICENSE' for copying permission. + +from django.urls import include, path +from rest_framework import routers + +# Routers provide an easy way of automatically determining the URL conf. +from api_app.data_model_manager.views import ( + DomainDataModelView, + FileDataModelView, + IPDataModelView, +) + +router = routers.DefaultRouter(trailing_slash=False) +router.register(r"domain", DomainDataModelView, basename="domain") +router.register(r"ip", IPDataModelView, basename="ip") +router.register(r"file", FileDataModelView, basename="file") + +urlpatterns = [ + # Viewsets + path(r"", include(router.urls)), +] diff --git a/api_app/data_model_manager/views.py b/api_app/data_model_manager/views.py new file mode 100644 index 00000000..b905a80c --- /dev/null +++ b/api_app/data_model_manager/views.py @@ -0,0 +1,30 @@ +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated + +from api_app.data_model_manager.serializers import ( + DomainDataModelSerializer, + FileDataModelSerializer, + IPDataModelSerializer, +) +from api_app.mixins import PaginationMixin +from api_app.permissions import IsObjectOwnerOrSameOrgPermission + + +class BaseDataModelView(PaginationMixin, viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated, IsObjectOwnerOrSameOrgPermission] + ordering = ["date"] + + def get_queryset(self): + return self.serializer_class.Meta.model.objects.all() + + +class DomainDataModelView(BaseDataModelView): + serializer_class = DomainDataModelSerializer + + +class IPDataModelView(BaseDataModelView): + serializer_class = IPDataModelSerializer + + +class FileDataModelView(BaseDataModelView): + serializer_class = FileDataModelSerializer diff --git a/api_app/ingestors_manager/ingestors/virus_total.py b/api_app/ingestors_manager/ingestors/virus_total.py index 771383fe..8786d11c 100644 --- a/api_app/ingestors_manager/ingestors/virus_total.py +++ b/api_app/ingestors_manager/ingestors/virus_total.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Iterable +from typing import Any, Dict, Iterable from unittest.mock import patch from django.utils import timezone @@ -21,6 +21,12 @@ class VirusTotal(Ingestor, VirusTotalv3BaseMixin): # VT API key _api_key_name: str + def config(self, runtime_configuration: Dict): + super().config(runtime_configuration) + # An Ingestor does not have a corresponding job so we set the value to False, + # the aim of the ingestors usually is to download data not to upload. + self.force_active_scan = False + @classmethod def update(cls) -> bool: pass @@ -30,7 +36,9 @@ def run(self) -> Iterable[Any]: delta_hours = timezone.datetime.now() - timezone.timedelta(hours=self.hours) self.query = f"fs:{delta_hours.strftime('%Y-%m-%d%H:%M:%S')}+ " + self.query data = self._vt_intelligence_search(self.query, 300, "").get("data", {}) - logger.info(f"Retrieved {len(data)} items from the query") + logger.info( + f"VT ingestor: Retrieved {len(data)} items from the query {self.query}" + ) samples_hashes = [d["id"] for d in data] for sample_hash in samples_hashes: if self.extract_IOCs: diff --git a/api_app/investigations_manager/models.py b/api_app/investigations_manager/models.py index 05eec38d..9b7778f8 100644 --- a/api_app/investigations_manager/models.py +++ b/api_app/investigations_manager/models.py @@ -28,7 +28,7 @@ class Investigation(OwnershipAbstractModel, ListCachable): max_length=20, default=InvestigationStatusChoices.CREATED.value, ) - Status = InvestigationStatusChoices + STATUSES = InvestigationStatusChoices objects = InvestigationQuerySet.as_manager() @@ -66,20 +66,20 @@ def set_correct_status(self, save: bool = True): for job in self.jobs.all(): job: Job jobs = job.get_tree(job) - if jobs.exclude(status__in=Job.Status.final_statuses()).count() > 0: - self.status = self.Status.RUNNING.value + if jobs.exclude(status__in=Job.STATUSES.final_statuses()).count() > 0: + self.status = self.STATUSES.RUNNING.value self.end_time = None break # and they are all completed else: - self.status = self.Status.CONCLUDED.value + self.status = self.STATUSES.CONCLUDED.value self.end_time = ( self.jobs.order_by("-finished_analysis_time") .first() .finished_analysis_time ) else: - self.status = self.Status.CREATED.value + self.status = self.STATUSES.CREATED.value self.end_time = None if save: self.save(update_fields=["status", "end_time"]) diff --git a/api_app/migrations/0064_vt_sample_download.py b/api_app/migrations/0064_vt_sample_download.py new file mode 100644 index 00000000..7a4414ca --- /dev/null +++ b/api_app/migrations/0064_vt_sample_download.py @@ -0,0 +1,53 @@ +from django.db import migrations + +from api_app.choices import PythonModuleBasePaths + + +def migrate(apps, schema_editor): + PythonModule = apps.get_model("api_app", "PythonModule") + Parameter = apps.get_model("api_app", "Parameter") + + # analyzer python module + vt_sample_analyzer_python_module, _ = PythonModule.objects.get_or_create( + module="vt.vt3_sample_download.VirusTotalv3SampleDownload", + base_path=PythonModuleBasePaths.ObservableAnalyzer.value, + ) + + # visualizer python module + PythonModule.objects.get_or_create( + module="sample_download.SampleDownload", + base_path=PythonModuleBasePaths.Visualizer.value, + ) + + # analyzer parameter + try: + Parameter.objects.get( + name="api_key_name", python_module=vt_sample_analyzer_python_module + ) + except Parameter.DoesNotExist: + p = Parameter( + name="api_key_name", + type="str", + description="VT API key", + is_secret=True, + required=True, + python_module=vt_sample_analyzer_python_module, + ) + p.full_clean() + p.save() + + +def reverse_migrate(apps, schema_editor): + # cannot undo: + # depending on migration order, some field could miss and the reverse fail + # for this reason the reversion didn't delete nothing + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("api_app", "0063_singleton_and_elastic_report"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/mixins.py b/api_app/mixins.py index cfbe4720..449c0716 100644 --- a/api_app/mixins.py +++ b/api_app/mixins.py @@ -10,6 +10,7 @@ from rest_framework.response import Response from api_app.analyzers_manager.classes import BaseAnalyzerMixin +from api_app.analyzers_manager.constants import ObservableTypes from api_app.analyzers_manager.exceptions import AnalyzerRunException from api_app.choices import ObservableClassification from certego_saas.ext.pagination import CustomPageNumberPagination @@ -77,25 +78,18 @@ def list(self, request, *args, **kwargs): return Response(data) -class VirusTotalv3BaseMixin(BaseAnalyzerMixin, metaclass=abc.ABCMeta): +class VirusTotalv3BaseMixin(metaclass=abc.ABCMeta): url = "https://www.virustotal.com/api/v3/" # If you want to query a specific subpath of the base endpoint, i.e: `analyses` url_sub_path: str _api_key_name: str + ObservableTypes = ObservableTypes @property def headers(self) -> dict: return {"x-apikey": self._api_key_name} - def config(self, runtime_configuration: Dict): - super().config(runtime_configuration) - # An Ingestor does not have a corresponding job so we set the value to False, - # the aim of the ingestors usually is to download data not to upload. - self.force_active_scan = ( - self._job.tlp == self._job.TLP.CLEAR.value if self._job else False - ) - def _perform_get_request( self, uri: str, ignore_404: bool = False, **kwargs ) -> Dict: @@ -313,7 +307,9 @@ def _vt_get_iocs_from_file(self, sample_hash: str) -> Dict: ) -class VirusTotalv3AnalyzerMixin(VirusTotalv3BaseMixin, metaclass=abc.ABCMeta): +class VirusTotalv3AnalyzerMixin( + VirusTotalv3BaseMixin, BaseAnalyzerMixin, metaclass=abc.ABCMeta +): # How many times we poll the VT API for scan results max_tries: int # ThreatMatrix would sleep for this time between each poll to VT APIs @@ -340,6 +336,10 @@ class VirusTotalv3AnalyzerMixin(VirusTotalv3BaseMixin, metaclass=abc.ABCMeta): # Number of elements to retrieve for each relationships relationships_elements: int + def config(self, runtime_configuration: Dict): + super().config(runtime_configuration) + self.force_active_scan = self._job.tlp == self._job.TLP.CLEAR.value + def _get_relationship_limit(self, relationship: str) -> int: # by default, just extract the first element limit = self.relationships_elements diff --git a/api_app/models.py b/api_app/models.py index 075e5497..be81e4ab 100644 --- a/api_app/models.py +++ b/api_app/models.py @@ -342,7 +342,7 @@ class Meta: # constants TLP = TLP - Status = Status + STATUSES = Status investigation = models.ForeignKey( "investigations_manager.Investigation", on_delete=models.PROTECT, @@ -365,7 +365,7 @@ class Meta: file_name = models.CharField(max_length=512, blank=True) file_mimetype = models.CharField(max_length=80, blank=True) status = models.CharField( - max_length=32, blank=False, choices=Status.choices, default="pending" + max_length=32, blank=False, choices=STATUSES.choices, default="pending" ) analyzers_requested = models.ManyToManyField( @@ -513,7 +513,7 @@ def retry(self): """ Retry the job by setting its status to running and re-executing the pipeline. """ - self.status = self.Status.RUNNING + self.status = self.STATUSES.RUNNING self.save(update_fields=["status"]) runner = self._get_pipeline( @@ -532,7 +532,7 @@ def retry(self): def set_final_status(self) -> None: logger.info(f"[STARTING] set_final_status for <-- {self}.") - if self.status == self.Status.FAILED: + if self.status == self.STATUSES.FAILED: logger.error( f"[REPORT] {self}, status: failed. " "Do not process the report" ) @@ -541,13 +541,13 @@ def set_final_status(self) -> None: logger.info(f"[REPORT] {self}, status:{self.status}, reports:{stats}") if stats["success"] == stats["all"]: - self.status = self.Status.REPORTED_WITHOUT_FAILS + self.status = self.STATUSES.REPORTED_WITHOUT_FAILS elif stats["failed"] == stats["all"]: - self.status = self.Status.FAILED + self.status = self.STATUSES.FAILED elif stats["killed"] == stats["all"]: - self.status = self.Status.KILLED + self.status = self.STATUSES.KILLED elif stats["failed"] >= 1 or stats["killed"] >= 1: - self.status = self.Status.REPORTED_WITH_FAILS + self.status = self.STATUSES.REPORTED_WITH_FAILS self.finished_analysis_time = get_now() @@ -584,7 +584,7 @@ def __get_single_config_reports_stats( reports = self.__get_config_reports(config) aggregators = { s.lower(): models.Count("status", filter=models.Q(status=s)) - for s in AbstractReport.Status.values + for s in AbstractReport.STATUSES.values } return reports.aggregate( all=models.Count("status"), @@ -616,8 +616,8 @@ def kill_if_ongoing(self): for config in [AnalyzerConfig, ConnectorConfig, VisualizerConfig]: reports = self.__get_config_reports(config).filter( status__in=[ - AbstractReport.Status.PENDING, - AbstractReport.Status.RUNNING, + AbstractReport.STATUSES.PENDING, + AbstractReport.STATUSES.RUNNING, ] ) @@ -626,9 +626,9 @@ def kill_if_ongoing(self): # kill celery tasks using task ids celery_app.control.revoke(ids, terminate=True) - reports.update(status=self.Status.KILLED) + reports.update(status=self.STATUSES.KILLED) - self.status = self.Status.KILLED + self.status = self.STATUSES.KILLED self.save(update_fields=["status"]) JobConsumer.serialize_and_send_job(self) @@ -700,7 +700,7 @@ def _get_pipeline( return runner def execute(self): - self.status = self.Status.RUNNING + self.status = self.STATUSES.RUNNING self.save(update_fields=["status"]) runner = self._get_pipeline( self.analyzers_to_execute.all(), @@ -739,7 +739,7 @@ def user_month_submissions(cls, user: User) -> int: day=1, hour=0, minute=0, second=0, microsecond=0 ) ) - .exclude(status=cls.Status.FAILED) + .exclude(status=cls.STATUSES.FAILED) .count() ) @@ -1346,10 +1346,10 @@ class AbstractReport(models.Model): objects = AbstractReportQuerySet.as_manager() # constants - Status = ReportStatus + STATUSES = ReportStatus # fields - status = models.CharField(max_length=50, choices=Status.choices) + status = models.CharField(max_length=50, choices=STATUSES.choices) report = models.JSONField(default=dict) errors = pg_fields.ArrayField( models.CharField(max_length=512), default=list, blank=True @@ -1399,12 +1399,12 @@ def runtime_configuration(self): # properties @property - def user(self) -> models.Model: + def user(self) -> User: """ Returns the user associated with the job that generated the report. Returns: - models.Model: The user associated with the job. + User: The user associated with the job. """ return self.job.user @@ -1419,23 +1419,24 @@ def process_time(self) -> float: secs = (self.end_time - self.start_time).total_seconds() return round(secs, 2) - def get_value(self, field: str) -> Any: - content = self.report - - for key in field.split("."): + def get_value( + self, search_from: typing.Any, fields: typing.List[str] + ) -> typing.Any: + if not fields: + return search_from + search_keyword = fields.pop(0) + if isinstance(search_from, list): try: - content = content[key] - except TypeError: - if isinstance(content, list) and len(content) > 0: - content = content[int(key)] - else: - raise RuntimeError(f"Not found {field}") - - if isinstance(content, (int, dict)): - raise ValueError(f"You can't use a {type(content)} as pivot") - if not content: - raise ValueError("Empty value") - return content + index = int(search_keyword) + except ValueError: + result = [] + for obj in search_from: + result.append(self.get_value(obj, [search_keyword] + fields)) + return result + else: + # a.b.0 + return self.get_value(search_from[index], fields) + return self.get_value(search_from[search_keyword], fields) class PythonConfig(AbstractConfig): diff --git a/api_app/pivots_manager/classes.py b/api_app/pivots_manager/classes.py index a448bff9..8bfce310 100644 --- a/api_app/pivots_manager/classes.py +++ b/api_app/pivots_manager/classes.py @@ -48,7 +48,7 @@ def config_model(cls) -> Type[PivotConfig]: def should_run(self) -> Tuple[bool, Optional[str]]: # by default, the pivot run IF every report attached to it was success result = not self.related_reports.exclude( - status=self.report_model.Status.SUCCESS.value + status=self.report_model.STATUSES.SUCCESS.value ).exists() return ( result, diff --git a/api_app/pivots_manager/migrations/0035_pivot_config_phishingextractortoanalysis.py b/api_app/pivots_manager/migrations/0035_pivot_config_phishingextractortoanalysis.py new file mode 100644 index 00000000..c7b64d6d --- /dev/null +++ b/api_app/pivots_manager/migrations/0035_pivot_config_phishingextractortoanalysis.py @@ -0,0 +1,156 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "load_file.LoadFile", + "base_path": "api_app.pivots_manager.pivots", + }, + "related_analyzer_configs": ["Phishing_Extractor"], + "related_connector_configs": [], + "playbooks_choice": ["PhishingAnalysis"], + "name": "PhishingExtractorToAnalysis", + "description": "Pivot for plugins Phishing_Extractor that executes playbooks PhishingAnalysis", + "disabled": False, + "soft_time_limit": 60, + "routing_key": "default", + "health_check_status": True, + "delay": "00:00:00", + "model": "pivots_manager.PivotConfig", +} + +params = [ + { + "python_module": { + "module": "load_file.LoadFile", + "base_path": "api_app.pivots_manager.pivots", + }, + "name": "field_to_compare", + "type": "str", + "description": "Dotted path to the field", + "is_secret": False, + "required": True, + } +] + +values = [ + { + "parameter": { + "python_module": { + "module": "load_file.LoadFile", + "base_path": "api_app.pivots_manager.pivots", + }, + "name": "field_to_compare", + "type": "str", + "description": "Dotted path to the field", + "is_secret": False, + "required": True, + }, + "analyzer_config": None, + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": "PhishingExtractorToAnalysis", + "for_organization": False, + "value": "page_source", + "updated_at": "2024-09-25T13:45:58.643835Z", + "owner": None, + } +] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ + ForwardManyToOneDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, + ForwardOneToOneDescriptor, + ] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0063_singleton_and_elastic_report"), + ("pivots_manager", "0034_changed_resubmitdownloadedfile_playbook_to_execute"), + ("playbooks_manager", "0054_playbook_config_phishinganalysis"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/pivots_manager/migrations/0036_alter_extractedonenotefiles_resubmitdownloadedfile_loadfilesameplaybook.py b/api_app/pivots_manager/migrations/0036_alter_extractedonenotefiles_resubmitdownloadedfile_loadfilesameplaybook.py new file mode 100644 index 00000000..a977a3c5 --- /dev/null +++ b/api_app/pivots_manager/migrations/0036_alter_extractedonenotefiles_resubmitdownloadedfile_loadfilesameplaybook.py @@ -0,0 +1,52 @@ +from django.db import migrations + + +def migrate(apps, schema_editor): + PythonModule = apps.get_model("api_app", "PythonModule") + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + PivotConfig = apps.get_model("pivots_manager", "PivotConfig") + pivots_to_update = PivotConfig.objects.filter( + name__in=["ExtractedOneNoteFiles", "ResubmitDownloadedFile"] + ) + pm = PythonModule.objects.create( + health_check_schedule=None, + update_schedule=None, + module="load_file_same_playbook.LoadFileSamePlaybook", + base_path="api_app.pivots_manager.pivots", + ) + param1 = Parameter.objects.create( + name="field_to_compare", + type="str", + description="Dotted path to the field", + is_secret=False, + required=True, + python_module=pm, + ) + for pivot_to_update in pivots_to_update: + + PluginConfig.objects.filter(pivot_config=pivot_to_update).delete() + pivot_to_update.python_module = pm + PluginConfig.objects.create( + parameter=param1, + value="stored_base64", + for_organization=False, + updated_at="2024-11-07T10:35:46.217160Z", + analyzer_config=None, + connector_config=None, + visualizer_config=None, + ingestor_config=None, + pivot_config=pivot_to_update, + ) + pivot_to_update.full_clean() + pivot_to_update.save() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0063_singleton_and_elastic_report"), + ("pivots_manager", "0035_pivot_config_phishingextractortoanalysis"), + ] + + operations = [migrations.RunPython(migrate, migrations.RunPython.noop)] diff --git a/api_app/pivots_manager/pivots/any_compare.py b/api_app/pivots_manager/pivots/any_compare.py index e9fd4ef4..31a1a65e 100644 --- a/api_app/pivots_manager/pivots/any_compare.py +++ b/api_app/pivots_manager/pivots/any_compare.py @@ -9,10 +9,12 @@ class AnyCompare(Compare): def should_run(self) -> Tuple[bool, Optional[str]]: for report in self.related_reports.filter( - status=self.report_model.Status.SUCCESS.value + status=self.report_model.STATUSES.SUCCESS.value ): try: - self._value = report.get_value(self.field_to_compare) + self._value = report.get_value( + report.report, self.field_to_compare.split(".") + ) except (RuntimeError, ValueError): continue else: diff --git a/api_app/pivots_manager/pivots/compare.py b/api_app/pivots_manager/pivots/compare.py index cc9e5f6e..ece0fcef 100644 --- a/api_app/pivots_manager/pivots/compare.py +++ b/api_app/pivots_manager/pivots/compare.py @@ -17,8 +17,11 @@ def should_run(self) -> Tuple[bool, Optional[str]]: f"Unable to run pivot {self._config.name} " "because attached to more than one configuration", ) + report = self.related_reports.first() try: - self._value = self.related_reports.first().get_value(self.field_to_compare) + self._value = report.get_value( + report.report, self.field_to_compare.split(".") + ) except (RuntimeError, ValueError) as e: return False, str(e) return super().should_run() diff --git a/api_app/pivots_manager/pivots/load_file_same_playbook.py b/api_app/pivots_manager/pivots/load_file_same_playbook.py new file mode 100644 index 00000000..f32f3a96 --- /dev/null +++ b/api_app/pivots_manager/pivots/load_file_same_playbook.py @@ -0,0 +1,15 @@ +from api_app.pivots_manager.models import PivotConfig +from api_app.pivots_manager.pivots.load_file import LoadFile + + +class LoadFileSamePlaybook(LoadFile): + field_to_compare: str + + @classmethod + def update(cls) -> bool: + pass + + def get_playbook_to_execute(self): + self._config: PivotConfig + # use the same playbook of the parent when resubmit a file + return self._job.get_root().playbook_to_execute diff --git a/api_app/playbooks_manager/migrations/0054_playbook_config_phishinganalysis.py b/api_app/playbooks_manager/migrations/0054_playbook_config_phishinganalysis.py new file mode 100644 index 00000000..c18acbea --- /dev/null +++ b/api_app/playbooks_manager/migrations/0054_playbook_config_phishinganalysis.py @@ -0,0 +1,125 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, +) + +plugin = { + "analyzers": ["Phishing_Form_Compiler"], + "connectors": [], + "pivots": [], + "for_organization": False, + "name": "PhishingAnalysis", + "description": "This playbook is used to perform a complete phishing analysis of " + "a given URL. It wraps all the analyzers for the purpose.", + "disabled": False, + "type": ["file"], + "runtime_configuration": { + "pivots": {}, + "analyzers": {}, + "connectors": {}, + "visualizers": {}, + }, + "scan_mode": 1, + "scan_check_time": None, + "tlp": "CLEAR", + "starting": False, + "owner": None, + "tags": [], + "model": "playbooks_manager.PlaybookConfig", +} + +params = [] + +values = [] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ + ForwardManyToOneDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, + ForwardOneToOneDescriptor, + ] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("playbooks_manager", "0053_add_androguard_to_free_to_use_analyzers"), + ("analyzers_manager", "0128_analyzer_config_phishing_form_compiler"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/playbooks_manager/migrations/0055_playbook_config_phishingextractor.py b/api_app/playbooks_manager/migrations/0055_playbook_config_phishingextractor.py new file mode 100644 index 00000000..a49bc6e3 --- /dev/null +++ b/api_app/playbooks_manager/migrations/0055_playbook_config_phishingextractor.py @@ -0,0 +1,126 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, +) + +plugin = { + "analyzers": ["Phishing_Extractor"], + "connectors": [], + "pivots": ["PhishingExtractorToAnalysis"], + "for_organization": False, + "name": "PhishingExtractor", + "description": "This playbook is the first phase of the phishing analysis framework. " + "Its main purpose is to open the web page and dump its source code" + " and screenshot plus some other details.", + "disabled": False, + "type": ["url"], + "runtime_configuration": { + "pivots": {}, + "analyzers": {}, + "connectors": {}, + "visualizers": {}, + }, + "scan_mode": 2, + "scan_check_time": "1 00:00:00", + "tlp": "CLEAR", + "starting": True, + "owner": None, + "tags": [], + "model": "playbooks_manager.PlaybookConfig", +} + +params = [] + +values = [] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ + ForwardManyToOneDescriptor, + ReverseManyToOneDescriptor, + ReverseOneToOneDescriptor, + ForwardOneToOneDescriptor, + ] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("playbooks_manager", "0054_playbook_config_phishinganalysis"), + ("analyzers_manager", "0129_analyzer_config_phishing_extractor"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/playbooks_manager/migrations/0056_download_sample_vt.py b/api_app/playbooks_manager/migrations/0056_download_sample_vt.py new file mode 100644 index 00000000..ffde7af1 --- /dev/null +++ b/api_app/playbooks_manager/migrations/0056_download_sample_vt.py @@ -0,0 +1,38 @@ +import datetime + +from django.db import migrations + +from api_app.analyzers_manager.constants import AllTypes +from api_app.choices import TLP + + +def migrate(apps, schema_editor): + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + PlaybookConfig = apps.get_model("playbooks_manager", "PlaybookConfig") + playbook_download_sample_vt, _ = PlaybookConfig.objects.get_or_create( + name="Download_File_VT", + description="Download a sample from VT", + type=[AllTypes.HASH.value], + tlp=TLP.AMBER.value, + scan_check_time=datetime.timedelta(days=14), + ) + vt_download_file_analyzer = AnalyzerConfig.objects.get( + name="VirusTotalv3SampleDownload" + ) + playbook_download_sample_vt.analyzers.set([vt_download_file_analyzer]) + playbook_download_sample_vt.save() + + +def reverse_migrate(apps, schema_editor): + PlaybookConfig = apps.get_model("playbooks_manager", "PlaybookConfig") + PlaybookConfig.objects.get(name="Download_File_VT").delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("playbooks_manager", "0055_playbook_config_phishingextractor"), + ("analyzers_manager", "0131_analyzer_config_vt_sample_download"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/queryset.py b/api_app/queryset.py index 99bc9413..070869cc 100644 --- a/api_app/queryset.py +++ b/api_app/queryset.py @@ -15,7 +15,7 @@ from treebeard.mp_tree import MP_NodeQuerySet if TYPE_CHECKING: - from api_app.models import PythonConfig + from api_app.models import PythonConfig, AbstractConfig from api_app.serializers import AbstractBIInterface import logging @@ -312,7 +312,7 @@ def filter_completed(self): Returns: The filtered queryset. """ - return self.filter(status__in=self.model.Status.final_statuses()) + return self.filter(status__in=self.model.STATUSES.final_statuses()) def visible_for_user(self, user: User) -> "JobQuerySet": """ @@ -409,10 +409,10 @@ def running( The filtered queryset. """ qs = self.exclude( - status__in=[status.value for status in self.model.Status.final_statuses()] + status__in=[status.value for status in self.model.STATUSES.final_statuses()] ) if not check_pending: - qs = qs.exclude(status=self.model.Status.PENDING.value) + qs = qs.exclude(status=self.model.STATUSES.PENDING.value) difference = now() - datetime.timedelta(minutes=minutes_ago) return qs.filter(received_request_time__lte=difference) @@ -574,7 +574,9 @@ def _alias_for_test(self): test_value=Case( When( name__icontains="url", - then=Value("https://threatmatrix.com", output_field=JSONField()), + then=Value( + "https://threatmatrix.khulnasoft.com", output_field=JSONField() + ), ), When( name="pdns_credentials", @@ -673,7 +675,7 @@ def filter_completed(self): Returns: AbstractReportQuerySet: The filtered queryset. """ - return self.filter(status__in=self.model.Status.final_statuses()) + return self.filter(status__in=self.model.STATUSES.final_statuses()) def filter_retryable(self): """ @@ -683,7 +685,10 @@ def filter_retryable(self): AbstractReportQuerySet: The filtered queryset. """ return self.filter( - status__in=[self.model.Status.FAILED.value, self.model.Status.PENDING.value] + status__in=[ + self.model.STATUSES.FAILED.value, + self.model.STATUSES.PENDING.value, + ] ) def get_configurations(self) -> AbstractConfigQuerySet: @@ -936,7 +941,7 @@ def get_signatures(self, job) -> Generator[Signature, None, None]: task_id = str(uuid.uuid4()) config.generate_empty_report( - job, task_id, AbstractReport.Status.PENDING.value + job, task_id, AbstractReport.STATUSES.PENDING.value ) args = [ job.pk, diff --git a/api_app/serializers/elastic.py b/api_app/serializers/elastic.py index d9a87f10..4670a069 100644 --- a/api_app/serializers/elastic.py +++ b/api_app/serializers/elastic.py @@ -12,7 +12,14 @@ logger = logging.getLogger(__name__) -@dataclass +supported_plugin_name_list = [ + AnalyzerConfig.plugin_name.lower(), + ConnectorConfig.plugin_name.lower(), + PivotConfig.plugin_name.lower(), +] + + +@dataclass(frozen=True) class ElasticRequest: plugin_name: str = "" name: str = "" @@ -27,11 +34,7 @@ class ElasticRequest: class ElasticRequestSerializer(serializers.Serializer): plugin_name = serializers.ChoiceField( - choices=[ - AnalyzerConfig.plugin_name, - ConnectorConfig.plugin_name, - PivotConfig.plugin_name, - ], + choices=supported_plugin_name_list, required=False, ) name = serializers.CharField(required=False) @@ -50,18 +53,20 @@ def create(self, validated_data) -> ElasticRequest: return ElasticRequest(**validated_data) -class ElasticResponseSerializer(serializers.Serializer): - job_id = serializers.IntegerField() - plugin_name = serializers.ChoiceField( - choices=[ - AnalyzerConfig.plugin_name, - ConnectorConfig.plugin_name, - PivotConfig.plugin_name, - ], - ) +class ElasticJobSerializer(serializers.Serializer): + id = serializers.IntegerField() + + +class ElasticConfigSerializer(serializers.Serializer): name = serializers.CharField() + plugin_name = serializers.ChoiceField(choices=supported_plugin_name_list) + + +class ElasticResponseSerializer(serializers.Serializer): + job = ElasticJobSerializer() + config = ElasticConfigSerializer() status = serializers.ChoiceField(choices=ReportStatus.final_statuses()) start_time = serializers.DateTimeField() end_time = serializers.DateTimeField() - errors = serializers.BooleanField() - report = serializers.CharField() + errors = serializers.ListField(child=serializers.CharField()) + report = serializers.JSONField() diff --git a/api_app/serializers/job.py b/api_app/serializers/job.py index 2f2c7975..afd91467 100644 --- a/api_app/serializers/job.py +++ b/api_app/serializers/job.py @@ -322,9 +322,9 @@ def check_previous_jobs(self, validated_data: Dict) -> Job: logger.info("Checking previous jobs") if not validated_data["scan_check_time"]: raise ValidationError({"detail": "Scan check time can't be null"}) - status_to_exclude = [Job.Status.KILLED, Job.Status.FAILED] + status_to_exclude = [Job.STATUSES.KILLED, Job.STATUSES.FAILED] if not validated_data.get("playbook_to_execute", None): - status_to_exclude.append(Job.Status.REPORTED_WITH_FAILS) + status_to_exclude.append(Job.STATUSES.REPORTED_WITH_FAILS) qs = ( self.Meta.model.objects.visible_for_user(self.context["request"].user) .filter( @@ -539,6 +539,8 @@ class Meta: investigation = rfs.SerializerMethodField(read_only=True, default=None) permissions = rfs.SerializerMethodField() + analyzers_data_model = rfs.SerializerMethodField(read_only=True) + def get_pivots_to_execute(self, obj: Job): # skipcq: PYL-R0201 # this cast is required or serializer doesn't work with websocket return list(obj.pivots_to_execute.all().values_list("name", flat=True)) @@ -566,6 +568,9 @@ def get_fields(self): ) return super().get_fields() + def get_analyzers_data_model(self, instance: Job): + return instance.analyzerreports.get_data_models(instance).serialize() + class RestJobSerializer(JobSerializer): def get_permissions(self, obj: Job) -> Dict[str, bool]: @@ -604,7 +609,7 @@ def save(self, parent: Job = None, **kwargs): # so we don't need to do anything because everything is already connected root = parent.get_root() if root.investigation: - root.investigation.status = root.investigation.Status.RUNNING.value + root.investigation.status = root.investigation.STATUSES.RUNNING.value root.investigation.save() return jobs # if we have a parent, it means we are pivoting from one job to another @@ -630,7 +635,7 @@ def save(self, parent: Job = None, **kwargs): # set investigation into running status if len(jobs) >= 1 and jobs[0].investigation: investigation = jobs[0].investigation - investigation.status = investigation.Status.RUNNING.value + investigation.status = investigation.STATUSES.RUNNING.value investigation.save() return jobs # if we do not have a parent or an investigation, and we have multiple jobs, @@ -648,7 +653,7 @@ def save(self, parent: Job = None, **kwargs): else: return jobs investigation: Investigation - investigation.status = investigation.Status.RUNNING.value + investigation.status = investigation.STATUSES.RUNNING.value investigation.for_organization = True investigation.save() return jobs @@ -749,7 +754,9 @@ def validate(self, attrs: dict) -> dict: # calculate ``file_mimetype`` if "file_name" not in attrs: attrs["file_name"] = attrs["file"].name - attrs["file_mimetype"] = MimeTypes.calculate(attrs["file"], attrs["file_name"]) + attrs["file_mimetype"] = MimeTypes.calculate( + attrs["file"].read(), attrs["file_name"] + ) # calculate ``md5`` file_obj = attrs["file"].file file_obj.seek(0) @@ -1006,7 +1013,7 @@ def to_representation(self, instance: Job): result = super().to_representation(instance) result["status"] = self.STATUS_ACCEPTED result["already_exists"] = bool( - instance.status in instance.Status.final_statuses() + instance.status in instance.STATUSES.final_statuses() ) return result @@ -1054,15 +1061,15 @@ def validate(self, attrs): return attrs def create(self, validated_data): - statuses_to_check = [Job.Status.RUNNING] + statuses_to_check = [Job.STATUSES.RUNNING] if not validated_data["running_only"]: - statuses_to_check.append(Job.Status.REPORTED_WITHOUT_FAILS) + statuses_to_check.append(Job.STATUSES.REPORTED_WITHOUT_FAILS) # since with playbook # it is expected behavior # for analyzers to often fail if validated_data.get("playbooks", []): - statuses_to_check.append(Job.Status.REPORTED_WITH_FAILS) + statuses_to_check.append(Job.STATUSES.REPORTED_WITH_FAILS) # this means that the user is trying to # check availability of the case where all # analyzers were run but no playbooks were diff --git a/api_app/urls.py b/api_app/urls.py index 6944f772..1ba238ce 100644 --- a/api_app/urls.py +++ b/api_app/urls.py @@ -41,7 +41,7 @@ analyze_multiple_observables, name="analyze_multiple_observables", ), - path("plugin_report_queries", plugin_report_queries), + path("plugin_report_queries", plugin_report_queries, name="plugin_report_queries"), # router viewsets path("", include(router.urls)), # Plugins @@ -52,6 +52,7 @@ path("", include("api_app.pivots_manager.urls")), path("", include("api_app.playbooks_manager.urls")), path("", include("api_app.investigations_manager.urls")), + path("data_model/", include("api_app.data_model_manager.urls")), # auth path("auth/", include("authentication.urls")), # certego_saas: diff --git a/api_app/views.py b/api_app/views.py index 640aec10..4a1ebfca 100644 --- a/api_app/views.py +++ b/api_app/views.py @@ -425,8 +425,9 @@ class JobViewSet(ReadAndDeleteOnlyViewSet, SerializerActionMixin): - **aggregate_type**: Aggregate jobs by type (file or observable) over a specified time range. - **aggregate_observable_classification**: Aggregate jobs by observable classification over a specified time range. - **aggregate_file_mimetype**: Aggregate jobs by file MIME type over a specified time range. - - **aggregate_observable_name**: Aggregate jobs by observable name over a specified time range. - - **aggregate_md5**: Aggregate jobs by MD5 hash over a specified time range. + - **aggregate_top_playbook**: Aggregate jobs by playbook over a specified time range and show the most used. + - **aggregate_top_user**: Aggregate jobs by user over a specified time range and show the most used. + - **aggregate_top_tlp**: Aggregate jobs by TLP over a specified time range and show the most used. Permissions: - **IsAuthenticated**: Requires authentication for all actions. @@ -546,7 +547,7 @@ def retry(self, request, pk=None): - No content (204) if the job is successfully retried. """ job = self.get_object() - if job.status not in Job.Status.final_statuses(): + if job.status not in Job.STATUSES.final_statuses(): raise ValidationError({"detail": "Job is running"}) job.retry() return Response(status=status.HTTP_204_NO_CONTENT) @@ -602,7 +603,7 @@ def kill(self, request, pk=None): job = self.get_object() # check if job running - if job.status in Job.Status.final_statuses(): + if job.status in Job.STATUSES.final_statuses(): raise ValidationError({"detail": "Job is not running"}) # close celery tasks and mark reports as killed job.kill_if_ongoing() @@ -696,7 +697,14 @@ def aggregate_status(self, request): """ annotations = { key.lower(): Count("status", filter=Q(status=key)) - for key in Job.Status.values + for key in Job.STATUSES.values + if key + in [ + Job.STATUSES.PENDING, + Job.STATUSES.FAILED, + Job.STATUSES.REPORTED_WITH_FAILS, + Job.STATUSES.REPORTED_WITHOUT_FAILS, + ] } return self.__aggregation_response_static( annotations, users=self.get_org_members(request) @@ -764,38 +772,54 @@ def aggregate_file_mimetype(self, request): ) @action( - url_path="aggregate/observable_name", + url_path="aggregate/top_playbook", + detail=False, + methods=["GET"], + ) + @cache_action_response(timeout=60 * 5) + def aggregate_top_playbook(self, request): + """ + Aggregate playbooks by usage. + + Returns: + - Aggregated count of playbooks for each one. + """ + return self.__aggregation_response_dynamic( + "playbook_to_execute__name", users=self.get_org_members(request) + ) + + @action( + url_path="aggregate/top_user", detail=False, methods=["GET"], ) @cache_action_response(timeout=60 * 5) - def aggregate_observable_name(self, request): + def aggregate_top_user(self, request): """ - Aggregate jobs by observable name. + Aggregate Users by usage. Returns: - - Aggregated count of jobs for each observable name. + - Aggregated count of users for each one. """ return self.__aggregation_response_dynamic( - "observable_name", False, users=self.get_org_members(request) + "user__username", users=self.get_org_members(request) ) @action( - url_path="aggregate/md5", + url_path="aggregate/top_tlp", detail=False, methods=["GET"], ) @cache_action_response(timeout=60 * 5) - def aggregate_md5(self, request): + def aggregate_top_tlp(self, request): """ - Aggregate jobs by MD5 hash. + Aggregate TLPs by usage. Returns: - - Aggregated count of jobs for each MD5 hash. + - Aggregated count of TLPs for each one. """ - # this is for file return self.__aggregation_response_dynamic( - "md5", False, users=self.get_org_members(request) + "tlp", users=self.get_org_members(request) ) @staticmethod @@ -876,6 +900,7 @@ def __aggregation_response_dynamic( and the aggregated data. """ delta, basis = self.__parse_range(self.request) + logger.debug(f"{delta=}, {basis=}, {users=}") filter_kwargs = {"received_request_time__gte": delta} if users: filter_kwargs["user__in"] = users @@ -1214,7 +1239,7 @@ def perform_kill(report: AbstractReport): # kill celery task celery_app.control.revoke(report.task_id, terminate=True) # update report - report.status = AbstractReport.Status.KILLED + report.status = AbstractReport.STATUSES.KILLED report.save(update_fields=["status"]) # clean up job @@ -1291,8 +1316,8 @@ def kill(self, request, job_id, report_id): # get report object or raise 404 report = self.get_object(job_id, report_id) if report.status not in [ - AbstractReport.Status.RUNNING, - AbstractReport.Status.PENDING, + AbstractReport.STATUSES.RUNNING, + AbstractReport.STATUSES.PENDING, ]: raise ValidationError({"detail": "Plugin is not running or pending"}) @@ -1327,8 +1352,8 @@ def retry(self, request, job_id, report_id): # get report object or raise 404 report = self.get_object(job_id, report_id) if report.status not in [ - AbstractReport.Status.FAILED, - AbstractReport.Status.KILLED, + AbstractReport.STATUSES.FAILED, + AbstractReport.STATUSES.KILLED, ]: raise ValidationError( {"detail": "Plugin status should be failed or killed"} @@ -1567,17 +1592,17 @@ def pull(self, request, name=None): return Response(data={"status": update_status}, status=status.HTTP_200_OK) -# @add_docs( -# description="""This endpoint allows organization owners""", -# responses={ -# 200: inline_serializer( -# name="PluginStateViewerResponseSerializer", -# fields={ -# "data": rfs.JSONField(), -# }, -# ), -# }, -# ) +@add_docs( + description="""This endpoint allows users to search analyzer, connector and pivot reports. ELASTIC REQUIRED""", + responses={ + 200: inline_serializer( + name="ElasticResponseSerializer", + fields={ + "data": rfs.JSONField(), + }, + ), + }, +) @api_view(["GET"]) def plugin_report_queries(request): """ @@ -1597,9 +1622,6 @@ def plugin_report_queries(request): if not settings.ELASTICSEARCH_DSL_ENABLED: raise NotImplementedException() - # if not request.user.has_membership(): - # raise PermissionDenied() - # 1 validate request logger.debug(f"{request.query_params=}") elastic_request_serializer = ElasticRequestSerializer(data=request.query_params) @@ -1607,8 +1629,15 @@ def plugin_report_queries(request): elastic_request_params: ElasticRequest = elastic_request_serializer.save() logger.debug(f"{elastic_request_params.__dict__=}") - # 2 generate elasticsearch queries - filter_list = [] + # 2 generate elasticsearch queries, default filter: object owner or in org + permission_filter = QElastic("term", user__username=request.user.username) + if request.user.has_membership(): + permission_filter |= QElastic( + "term", membership__organization__name=request.user.username + ) + filter_list = [permission_filter] + + # additional filters based on request params if elastic_request_params.plugin_name: filter_list.append( QElastic("term", plugin_name=elastic_request_params.plugin_name) @@ -1641,11 +1670,16 @@ def plugin_report_queries(request): filter_list.append(QElastic("term", report=elastic_request_params.report)) # 3 return data - logger.debug(f"{filter_list=}") - hits = Search(index="plugin-report-*").query(QElastic("bool", filter=filter_list)) - serialize_response = ElasticResponseSerializer(data=hits) + hits = ( + Search(index="plugin-report-*") + .query(QElastic("bool", filter=filter_list)) + .execute() + ) + logger.debug(f"filters: {filter_list}, hits: {len(hits)}") + serialize_response = ElasticResponseSerializer( + data=[h.to_dict() for h in hits], many=True + ) serialize_response.is_valid(raise_exception=True) response_data = serialize_response.validated_data - result = {"data": response_data} return Response(result) diff --git a/api_app/visualizers_manager/classes.py b/api_app/visualizers_manager/classes.py index ca7d85bc..e38f1ec4 100644 --- a/api_app/visualizers_manager/classes.py +++ b/api_app/visualizers_manager/classes.py @@ -7,6 +7,7 @@ from django.db.models import QuerySet +from api_app.analyzers_manager.models import MimeTypes from api_app.choices import PythonModuleBasePaths from api_app.classes import Plugin from api_app.models import AbstractReport @@ -141,6 +142,50 @@ def type(self) -> str: return "title" +class VisualizableDownload(VisualizableObject): + + def __init__( + self, + value: str, + payload: str, + alignment: VisualizableAlignment = VisualizableAlignment.CENTER, + size: VisualizableSize = VisualizableSize.S_AUTO, + disable: bool = False, + copy_text: str = "", + description: str = "", + add_metadata_in_description: bool = True, + link: str = "", + ): + # assignments + super().__init__(size, alignment, disable) + self.value = value + self.payload = payload + self.copy_text = copy_text + self.description = description + self.add_metadata_in_description = add_metadata_in_description + self.link = link + # logic + self.mimetype = MimeTypes.calculate( + self.payload, self.value + ) # needed as field from the frontend + + @property + def type(self) -> str: + return "download" + + @property + def attributes(self) -> List[str]: + return super().attributes + [ + "value", + "mimetype", + "payload", + "copy_text", + "description", + "add_metadata_in_description", + "link", + ] + + class VisualizableBool(VisualizableBase): def __init__( self, diff --git a/api_app/visualizers_manager/migrations/0039_sample_download.py b/api_app/visualizers_manager/migrations/0039_sample_download.py new file mode 100644 index 00000000..190886c5 --- /dev/null +++ b/api_app/visualizers_manager/migrations/0039_sample_download.py @@ -0,0 +1,38 @@ +from django.db import migrations + + +def migrate(apps, schema_editor): + PythonModule = apps.get_model("api_app", "PythonModule") + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + PlaybookConfig = apps.get_model("playbooks_manager", "PlaybookConfig") + VisualizerConfig = apps.get_model("visualizers_manager", "VisualizerConfig") + + visualizer_download_sample, _ = VisualizerConfig.objects.get_or_create( + name="Download_File", + description="Download a sample", + python_module=PythonModule.objects.get(module="sample_download.SampleDownload"), + ) + visualizer_download_sample.playbooks.add( + *PlaybookConfig.objects.filter( + analyzers=AnalyzerConfig.objects.get(name="DownloadFileFromUri") + ), + *PlaybookConfig.objects.filter( + analyzers=AnalyzerConfig.objects.get(name="VirusTotalv3SampleDownload") + ) + ) + visualizer_download_sample.save() + + +def reverse_migrate(apps, schema_editor): + VisualizerConfig = apps.get_model("visualizers_manager", "VisualizerConfig") + VisualizerConfig.objects.get(name="Download_File").delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("visualizers_manager", "0038_visualizer_config_passive_dns"), + ("playbooks_manager", "0056_download_sample_vt"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/visualizers_manager/visualizers/dns.py b/api_app/visualizers_manager/visualizers/dns.py index c42a0f3f..a79d7c4b 100644 --- a/api_app/visualizers_manager/visualizers/dns.py +++ b/api_app/visualizers_manager/visualizers/dns.py @@ -134,13 +134,13 @@ def _monkeypatch(cls): AnalyzerReport.objects.get( config=AnalyzerConfig.objects.get(python_module=python_module), job=Job.objects.first(), - status=AnalyzerReport.Status.SUCCESS, + status=AnalyzerReport.STATUSES.SUCCESS, ) except AnalyzerReport.DoesNotExist: report = AnalyzerReport( config=AnalyzerConfig.objects.get(python_module=python_module), job=Job.objects.first(), - status=AnalyzerReport.Status.SUCCESS, + status=AnalyzerReport.STATUSES.SUCCESS, report={ "observable": "dns.google.com", "resolutions": [ @@ -175,7 +175,7 @@ def _monkeypatch(cls): report = AnalyzerReport( config=AnalyzerConfig.objects.get(python_module=python_module), job=Job.objects.first(), - status=AnalyzerReport.Status.SUCCESS, + status=AnalyzerReport.STATUSES.SUCCESS, report={"observable": "dns.google.com", "malicious": False}, task_id=uuid(), parameters={}, diff --git a/api_app/visualizers_manager/visualizers/sample_download.py b/api_app/visualizers_manager/visualizers/sample_download.py new file mode 100644 index 00000000..f6be4b19 --- /dev/null +++ b/api_app/visualizers_manager/visualizers/sample_download.py @@ -0,0 +1,88 @@ +from logging import getLogger +from typing import Dict, List + +# ignore flake line too long in imports +from api_app.analyzers_manager.models import AnalyzerReport +from api_app.analyzers_manager.observable_analyzers.download_file_from_uri import ( + DownloadFileFromUri, +) +from api_app.analyzers_manager.observable_analyzers.vt.vt3_sample_download import ( + VirusTotalv3SampleDownload, +) +from api_app.visualizers_manager.classes import ( + VisualizableBase, + VisualizableDownload, + VisualizableVerticalList, + Visualizer, +) +from api_app.visualizers_manager.decorators import ( + visualizable_error_handler_with_params, +) + +logger = getLogger(__name__) + + +class SampleDownload(Visualizer): + + @visualizable_error_handler_with_params("Download") + def _download_button(self): + # first attempt is download with VT + try: + vt_report = self.analyzer_reports().get( + config__python_module=VirusTotalv3SampleDownload.python_module + ) + except AnalyzerReport.DoesNotExist: + pass + else: + payload = vt_report.report["data"] + disable = not payload + return VisualizableDownload( + value="VirusTotal Download", + payload=payload, + disable=disable, + ) + + # second attempt is download with VT + try: + uri_report = self.analyzer_reports().get( + config__python_module=DownloadFileFromUri.python_module + ) + except AnalyzerReport.DoesNotExist: + raise Exception("no VirusTotal nor uri analyzer used") + else: + base64_file_list = uri_report.report["stored_base64"] + disable_element = not base64_file_list + return VisualizableVerticalList( + name=VisualizableBase(value="URI's Downloads", disable=disable_element), + value=[ + VisualizableDownload( + value=f"Sample-{index + 1}", + payload=base64_file, + ) + for index, base64_file in enumerate(base64_file_list) + ], + disable=disable_element, + start_open=True, + ) + + def run(self) -> List[Dict]: + page = self.Page(name="Download") + page.add_level( + self.Level( + position=1, + size=self.LevelSize.S_3, + horizontal_list=self.HList( + value=[ + self._download_button(), + ] + ), + ) + ) + logger.debug(f"levels: {page.to_dict()}") + return [page.to_dict()] + + @classmethod + def _monkeypatch(cls): + # TODO + patches = [] + return super()._monkeypatch(patches=patches) diff --git a/api_app/visualizers_manager/visualizers/yara.py b/api_app/visualizers_manager/visualizers/yara.py index 9659bb5e..c806c139 100644 --- a/api_app/visualizers_manager/visualizers/yara.py +++ b/api_app/visualizers_manager/visualizers/yara.py @@ -86,7 +86,7 @@ def _monkeypatch(cls): report = AnalyzerReport( config=AnalyzerConfig.objects.get(name="Yara"), job=Job.objects.first(), - status=AnalyzerReport.Status.SUCCESS, + status=AnalyzerReport.STATUSES.SUCCESS, report={ "inquest_yara-rules": [ { diff --git a/configuration/elastic_search_mappings/plugin_report.json b/configuration/elastic_search_mappings/plugin_report.json index 0e874f0b..eb677743 100644 --- a/configuration/elastic_search_mappings/plugin_report.json +++ b/configuration/elastic_search_mappings/plugin_report.json @@ -8,6 +8,30 @@ }, "mappings": { "properties": { + "user": { + "properties": { + "username": { + "type": "keyword" + } + } + }, + "membership": { + "properties": { + "is_owner": { + "type": "boolean" + }, + "is_admin": { + "type": "boolean" + }, + "organization": { + "properties": { + "name": { + "type": "keyword" + } + } + } + } + }, "config": { "properties": { "name": { diff --git a/configuration/ldap_config.py b/configuration/ldap_config.py index 66703485..3ed20771 100644 --- a/configuration/ldap_config.py +++ b/configuration/ldap_config.py @@ -2,7 +2,7 @@ # See the file 'LICENSE' for copying permission. # Check the documentation for the details on how to configure LDAP -# https://khulnasoft.github.io/docs/ThreatMatrix/advanced_configuration/#ldap +# https://khulnasoft.github.io/devsec-docs/ThreatMatrix/advanced_configuration/#ldap import ldap from django_auth_ldap.config import GroupOfNamesType, LDAPSearch diff --git a/create_elastic_certs b/create_elastic_certs index c9f846db..7ef744ab 100755 --- a/create_elastic_certs +++ b/create_elastic_certs @@ -1,4 +1,8 @@ #!/usr/bin/env bash + +# create dir only in case they missing +mkdir -p ./certs + if [ ! -f ./certs/elastic_ca/ca.crt ] && [ ! -f ./certs/elastic_ca/ca.key ] && [ ! -f ./certs/elastic_instance/instance.crt ] && [ ! -f ./certs/elastic_instance/instance.key ]; then # start container docker pull docker.elastic.co/elasticsearch/elasticsearch:8.15.0 && diff --git a/docker/elasticsearch.override.yml b/docker/elasticsearch.override.yml index 0c67e016..2cf0643b 100644 --- a/docker/elasticsearch.override.yml +++ b/docker/elasticsearch.override.yml @@ -1,7 +1,8 @@ services: uwsgi: depends_on: - - elasticsearch + elasticsearch: + condition: service_healthy volumes: - ../certs:/opt/deploy/threat_matrix/certs diff --git a/docker/test.override.yml b/docker/test.override.yml index f558992b..50f26938 100644 --- a/docker/test.override.yml +++ b/docker/test.override.yml @@ -5,7 +5,7 @@ services: dockerfile: docker/Dockerfile args: REPO_DOWNLOADER_ENABLED: ${REPO_DOWNLOADER_ENABLED} - WATCHMAN: true + WATCHMAN: "true" PYCTI_VERSION: ${PYCTI_VERSION:-5.10.0} image: khulnasoft/threatmatrix:test volumes: @@ -40,4 +40,4 @@ services: volumes: - ../:/opt/deploy/threat_matrix environment: - - DEBUG=True \ No newline at end of file + - DEBUG=True diff --git a/frontend/README.md b/frontend/README.md index 20092288..597a5fc6 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -52,7 +52,7 @@ src/ source code The frontend inside the docker containers does not hot-reload, so you need to use `CRA dev server` on your host machine to serve pages when doing development on the frontend, using docker nginx only as API source. -- Start ThreatMatrix containers (see [docs](https://khulnasoft.github.io/docs/ThreatMatrix/installation/)). Original dockerized app is accessible on `http://localhost:80` +- Start ThreatMatrix containers (see [docs](https://khulnasoft.github.io/devsec-docs/ThreatMatrix/installation/)). Original dockerized app is accessible on `http://localhost:80` - If you have not `node-js` installed, you have to do that. Follow the guide [here](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-20-04). We tested this with NodeJS >=16.6 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f225171e..2e43ca08 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -42,6 +42,7 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.14", "babel-eslint": "^10.1.0", "babel-jest": "^29.7.0", "eslint": "^8.57.1", @@ -5015,6 +5016,48 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/@types/js-cookie": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", @@ -27912,6 +27955,41 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + } + } + }, "@types/js-cookie": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9b138d51..d9ee1a52 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -64,6 +64,7 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.14", "babel-eslint": "^10.1.0", "babel-jest": "^29.7.0", "eslint": "^8.57.1", diff --git a/frontend/src/components/GuideWrapper.jsx b/frontend/src/components/GuideWrapper.jsx index aa53ebb3..24070c9f 100644 --- a/frontend/src/components/GuideWrapper.jsx +++ b/frontend/src/components/GuideWrapper.jsx @@ -17,8 +17,8 @@ export default function GuideWrapper() {

Welcome to ThreatMatrixs Guide for First Time Visitors! For further questions you could either check out our{" "} - docs or reach us - out on{" "} + docs or + reach us out on{" "} the official ThreatMatrix slack channel diff --git a/frontend/src/components/common/form/TLPSelectInput.jsx b/frontend/src/components/common/form/TLPSelectInput.jsx index cba43659..a8ffd9e0 100644 --- a/frontend/src/components/common/form/TLPSelectInput.jsx +++ b/frontend/src/components/common/form/TLPSelectInput.jsx @@ -36,7 +36,7 @@ export function TLPSelectInputLabel(props) {
For more info check the{" "} official doc. diff --git a/frontend/src/components/dashboard/Dashboard.jsx b/frontend/src/components/dashboard/Dashboard.jsx index fdb98e6f..951582b5 100644 --- a/frontend/src/components/dashboard/Dashboard.jsx +++ b/frontend/src/components/dashboard/Dashboard.jsx @@ -12,23 +12,15 @@ import { JobTypeBarChart, JobObsClassificationBarChart, JobFileMimetypeBarChart, - JobObsNamePieChart, - JobFileHashPieChart, -} from "./utils/charts"; + JobTopPlaybookBarChart, + JobTopUserBarChart, + JobTopTLPBarChart, +} from "./charts"; import { useGuideContext } from "../../contexts/GuideContext"; import { useOrganizationStore } from "../../stores/useOrganizationStore"; -const charts1 = [ - ["JobStatusBarChart", "Job: Status", JobStatusBarChart], - [ - "JobObsNamePieChart", - "Job: Frequent IPs, Hash & Domains", - JobObsNamePieChart, - ], - ["JobFileHashPieChart", "Job: Frequent Files", JobFileHashPieChart], -]; -const charts2 = [ +const typeRow = [ ["JobTypeBarChart", "Job: Type", JobTypeBarChart], [ "JobObsClassificationBarChart", @@ -37,9 +29,13 @@ const charts2 = [ ], ["JobFileMimetypeBarChart", "Job: File Mimetype", JobFileMimetypeBarChart], ]; +const usageRow = [ + ["JobTopPlaybookBarChart", "Job: Top 5 Playbooks", JobTopPlaybookBarChart], + ["JobTopUserBarChart", "Job: Top 5 Users", JobTopUserBarChart], + ["JobTopTLPBarChart", "Job: Top 5 TLP", JobTopTLPBarChart], +]; export default function Dashboard() { - // const isSelectedUI = JobResultSections.VISUALIZER; const { guideState, setGuideState } = useGuideContext(); const [orgState, setOrgState] = useState(() => false); @@ -116,18 +112,28 @@ export default function Dashboard() {

- {charts1.map(([id, header, Component], index) => ( - + + + +
+ } + style={{ minHeight: 360 }} + /> + + + + {typeRow.map(([id, header, Component]) => ( + - +
} style={{ minHeight: 360 }} @@ -136,18 +142,14 @@ export default function Dashboard() { ))} - {charts2.map(([id, header, Component]) => ( + {usageRow.map(([id, header, Component]) => ( - +
} style={{ minHeight: 360 }} diff --git a/frontend/src/components/dashboard/charts.jsx b/frontend/src/components/dashboard/charts.jsx new file mode 100644 index 00000000..f16f0345 --- /dev/null +++ b/frontend/src/components/dashboard/charts.jsx @@ -0,0 +1,217 @@ +import React from "react"; +import { Bar } from "recharts"; + +import { getRandomColorsArray, AnyChartWidget } from "@certego/certego-ui"; + +import { + JobTypeColors, + ObservableClassificationColors, + TLPColors, +} from "../../constants/colorConst"; + +import { JobStatuses } from "../../constants/jobConst"; + +import { + JOB_AGG_STATUS_URI, + JOB_AGG_TYPE_URI, + JOB_AGG_OBS_CLASSIFICATION_URI, + JOB_AGG_FILE_MIMETYPE_URI, + JOB_AGG_TOP_PLAYBOOK_URI, + JOB_AGG_TOP_USER_URI, + JOB_AGG_TOP_TLP_URI, +} from "../../constants/apiURLs"; + +// constants +const colors = getRandomColorsArray(10, true); + +// bar charts +export const JobStatusBarChart = React.memo((props) => { + console.debug("JobStatusBarChart rendered!"); + const ORG_JOB_AGG_STATUS_URI = `${JOB_AGG_STATUS_URI}?org=${props.orgName}`; + + const mappingStatusColor = Object.freeze({ + [JobStatuses.PENDING]: "#ffffff", + [JobStatuses.REPORTED_WITH_FAILS]: "#ffa31a", + [JobStatuses.REPORTED_WITHOUT_FAILS]: "#009933", + [JobStatuses.FAILED]: "#cc0000", + }); + + const chartProps = React.useMemo( + () => ({ + url: ORG_JOB_AGG_STATUS_URI, + accessorFnAggregation: (jobStatusesPerDay) => jobStatusesPerDay, + componentsFn: () => + Object.entries(mappingStatusColor).map(([jobStatus, jobColor]) => ( + + )), + }), + [ORG_JOB_AGG_STATUS_URI, mappingStatusColor], + ); + + return ; +}); + +export const JobTypeBarChart = React.memo((props) => { + console.debug("JobTypeBarChart rendered!"); + const ORG_JOB_AGG_TYPE_URI = `${JOB_AGG_TYPE_URI}?org=${props.orgName}`; + + const chartProps = React.useMemo( + () => ({ + url: ORG_JOB_AGG_TYPE_URI, + accessorFnAggregation: (jobTypesPerDay) => jobTypesPerDay, + componentsFn: () => + Object.entries(JobTypeColors).map(([jobType, jobColor]) => ( + + )), + }), + [ORG_JOB_AGG_TYPE_URI], + ); + + return ; +}); + +export const JobObsClassificationBarChart = React.memo((props) => { + console.debug("JobObsClassificationBarChart rendered!"); + const ORG_JOB_AGG_OBS_CLASSIFICATION_URI = `${JOB_AGG_OBS_CLASSIFICATION_URI}?org=${props.orgName}`; + + const chartProps = React.useMemo( + () => ({ + url: ORG_JOB_AGG_OBS_CLASSIFICATION_URI, + accessorFnAggregation: (jobObservableSubTypesPerDay) => + jobObservableSubTypesPerDay, + componentsFn: () => + Object.entries(ObservableClassificationColors).map( + ([observableClassification, observableColor]) => ( + + ), + ), + }), + [ORG_JOB_AGG_OBS_CLASSIFICATION_URI], + ); + + return ; +}); + +export const JobFileMimetypeBarChart = React.memo((props) => { + console.debug("JobFileMimetypeBarChart rendered!"); + const ORG_JOB_AGG_FILE_MIMETYPE_URI = `${JOB_AGG_FILE_MIMETYPE_URI}?org=${props.orgName}`; + + const chartProps = React.useMemo( + () => ({ + url: ORG_JOB_AGG_FILE_MIMETYPE_URI, + accessorFnAggregation: (jobFileSubTypesPerDay) => + jobFileSubTypesPerDay?.aggregation, + componentsFn: (respData) => { + const { values: mimetypeList } = respData; + if (!mimetypeList || !mimetypeList?.length) return null; + return mimetypeList.map((mimetype, index) => ( + + )); + }, + }), + [ORG_JOB_AGG_FILE_MIMETYPE_URI], + ); + + return ; +}); + +export const JobTopPlaybookBarChart = React.memo((props) => { + console.debug("JobTopPlaybookBarChart rendered!"); + const ORG_JOB_AGG_TOP_PLAYBOOK_URI = `${JOB_AGG_TOP_PLAYBOOK_URI}?org=${props.orgName}`; + + const chartProps = React.useMemo( + () => ({ + url: ORG_JOB_AGG_TOP_PLAYBOOK_URI, + accessorFnAggregation: (jobPlaybooks) => jobPlaybooks?.aggregation, + componentsFn: (playbookUsageAggregatedByPlaybookName) => { + const { values } = playbookUsageAggregatedByPlaybookName; + if (!values || !values?.length) return null; + return values.map((playbookName, index) => ( + + )); + }, + }), + [ORG_JOB_AGG_TOP_PLAYBOOK_URI], + ); + + return ; +}); + +export const JobTopUserBarChart = React.memo((props) => { + console.debug("JobTopUserBarChart rendered!"); + const ORG_JOB_AGG_TOP_USER_URI = `${JOB_AGG_TOP_USER_URI}?org=${props.orgName}`; + + const chartProps = React.useMemo( + () => ({ + url: ORG_JOB_AGG_TOP_USER_URI, + accessorFnAggregation: (jobUsers) => jobUsers?.aggregation, + componentsFn: (JobUsageAggregatedByUsername) => { + const { values } = JobUsageAggregatedByUsername; + if (!values || !values?.length) return null; + return values.map((username, index) => ( + + )); + }, + }), + [ORG_JOB_AGG_TOP_USER_URI], + ); + + return ; +}); + +export const JobTopTLPBarChart = React.memo((props) => { + console.debug("JobTopTLPBarChart rendered!"); + const ORG_JOB_AGG_TOP_TLP_URI = `${JOB_AGG_TOP_TLP_URI}?org=${props.orgName}`; + + const chartProps = React.useMemo( + () => ({ + url: ORG_JOB_AGG_TOP_TLP_URI, + accessorFnAggregation: (jobTLPs) => jobTLPs?.aggregation, + componentsFn: (JobUsageAggregatedByTLP) => { + const { values } = JobUsageAggregatedByTLP; + if (!values || !values?.length) return null; + return values.map((tlp) => ( + + )); + }, + }), + [ORG_JOB_AGG_TOP_TLP_URI], + ); + + return ; +}); diff --git a/frontend/src/components/dashboard/utils/charts.jsx b/frontend/src/components/dashboard/utils/charts.jsx deleted file mode 100644 index 3ca14cc6..00000000 --- a/frontend/src/components/dashboard/utils/charts.jsx +++ /dev/null @@ -1,201 +0,0 @@ -import React from "react"; -import { Bar } from "recharts"; - -import { - getRandomColorsArray, - AnyChartWidget, - PieChartWidget, -} from "@certego/certego-ui"; - -import { - JobStatusColors, - JobTypeColors, - ObservableClassificationColors, -} from "../../../constants/colorConst"; - -import { - JOB_AGG_STATUS_URI, - JOB_AGG_TYPE_URI, - JOB_AGG_OBS_CLASSIFICATION_URI, - JOB_AGG_FILE_MIMETYPE_URI, - JOB_AGG_OBS_NAME_URI, - JOB_AGG_FILE_MD5_URI, -} from "../../../constants/apiURLs"; - -// constants -const colors = getRandomColorsArray(10, true); - -// bar charts - -export const JobStatusBarChart = React.memo((props) => { - console.debug("JobStatusBarChart rendered!"); - /* eslint-disable */ - var ORG_JOB_AGG_STATUS_URI = JOB_AGG_STATUS_URI; - const parameter = props.sendOrgState; - const getValue = parameter.key; - ORG_JOB_AGG_STATUS_URI = `${JOB_AGG_STATUS_URI}?org=${getValue}`; - - const chartProps = React.useMemo( - () => ({ - url: ORG_JOB_AGG_STATUS_URI, - accessorFnAggregation: (jobStatusesPerDay) => jobStatusesPerDay, - componentsFn: () => - Object.entries(JobStatusColors).map(([jobStatus, jobColor]) => ( - - )), - }), - [ORG_JOB_AGG_STATUS_URI], - ); - - return ; -}); - -export const JobTypeBarChart = React.memo((props) => { - console.debug("JobTypeBarChart rendered!"); - /* eslint-disable */ - var ORG_JOB_AGG_TYPE_URI = JOB_AGG_TYPE_URI; - const parameter = props.sendOrgState; - const getValue = parameter.key; - ORG_JOB_AGG_TYPE_URI = `${JOB_AGG_TYPE_URI}?org=${getValue}`; - - const chartProps = React.useMemo( - () => ({ - url: ORG_JOB_AGG_TYPE_URI, - accessorFnAggregation: (jobTypesPerDay) => jobTypesPerDay, - componentsFn: () => - Object.entries(JobTypeColors).map(([jobType, jobColor]) => ( - - )), - }), - [ORG_JOB_AGG_TYPE_URI], - ); - - return ; -}); - -export const JobObsClassificationBarChart = React.memo((props) => { - console.debug("JobObsClassificationBarChart rendered!"); - /* eslint-disable */ - var ORG_JOB_AGG_OBS_CLASSIFICATION_URI = JOB_AGG_OBS_CLASSIFICATION_URI; - const parameter = props.sendOrgState; - const getValue = parameter.key; - ORG_JOB_AGG_OBS_CLASSIFICATION_URI = `${JOB_AGG_OBS_CLASSIFICATION_URI}?org=${getValue}`; - - const chartProps = React.useMemo( - () => ({ - url: ORG_JOB_AGG_OBS_CLASSIFICATION_URI, - accessorFnAggregation: (jobObservableSubTypesPerDay) => - jobObservableSubTypesPerDay, - componentsFn: () => - Object.entries(ObservableClassificationColors).map( - ([observableClassification, observableColor]) => ( - - ), - ), - }), - [ORG_JOB_AGG_OBS_CLASSIFICATION_URI], - ); - - return ; -}); - -export const JobFileMimetypeBarChart = React.memo((props) => { - console.debug("JobFileMimetypeBarChart rendered!"); - /* eslint-disable */ - var ORG_JOB_AGG_FILE_MIMETYPE_URI = JOB_AGG_FILE_MIMETYPE_URI; - const parameter = props.sendOrgState; - const getValue = parameter.key; - ORG_JOB_AGG_FILE_MIMETYPE_URI = `${JOB_AGG_FILE_MIMETYPE_URI}?org=${getValue}`; - - const chartProps = React.useMemo( - () => ({ - url: ORG_JOB_AGG_FILE_MIMETYPE_URI, - accessorFnAggregation: (jobFileSubTypesPerDay) => - jobFileSubTypesPerDay?.aggregation, - componentsFn: (respData) => { - const { values: mimetypeList } = respData; - if (!mimetypeList || !mimetypeList?.length) return null; - return mimetypeList.map((mimetype, index) => ( - - )); - }, - }), - [ORG_JOB_AGG_FILE_MIMETYPE_URI], - ); - - return ; -}); - -// pie charts - -export const JobObsNamePieChart = React.memo((props) => { - console.debug("JobObsNamePieChart rendered!"); - /* eslint-disable */ - var ORG_JOB_AGG_OBS_NAME_URI = JOB_AGG_OBS_NAME_URI; - const parameter = props.sendOrgState; - const getValue = parameter.key; - ORG_JOB_AGG_OBS_NAME_URI = `${JOB_AGG_OBS_NAME_URI}?org=${getValue}`; - - const chartProps = React.useMemo( - () => ({ - url: ORG_JOB_AGG_OBS_NAME_URI, - modifierFn: (respData) => - Object.entries(respData?.aggregation).map( - ([observableName, analyzedTimes], index) => ({ - name: observableName.toLowerCase(), - value: analyzedTimes, - fill: colors[index], - }), - ), - }), - [ORG_JOB_AGG_OBS_NAME_URI], - ); - - return ; -}); - -export const JobFileHashPieChart = React.memo((props) => { - console.debug("JobFileHashPieChart rendered!"); - /* eslint-disable */ - var ORG_JOB_AGG_FILE_MD5_URI = JOB_AGG_FILE_MD5_URI; - const parameter = props.sendOrgState; - const getValue = parameter.key; - ORG_JOB_AGG_FILE_MD5_URI = `${JOB_AGG_FILE_MD5_URI}?org=${getValue}`; - - const chartProps = React.useMemo( - () => ({ - url: ORG_JOB_AGG_FILE_MD5_URI, - modifierFn: (respData) => - Object.entries(respData?.aggregation).map( - ([fileMd5, analyzedTimes], index) => ({ - name: fileMd5.toLowerCase(), - value: analyzedTimes, - fill: colors[index], - }), - ), - }), - [ORG_JOB_AGG_FILE_MD5_URI], - ); - - return ; -}); diff --git a/frontend/src/components/home/Home.jsx b/frontend/src/components/home/Home.jsx index 5ec7a257..29238f08 100644 --- a/frontend/src/components/home/Home.jsx +++ b/frontend/src/components/home/Home.jsx @@ -12,29 +12,28 @@ const versionText = VERSION; const logoBgImg = `url('${PUBLIC_URL}/logo-negative.png')`; const blogPosts = [ { - title: "ThreatMatrix: Release v4.0.0", - subText: "Certego Blog: v4.0.0 Announcement", - date: "1st July 2022", - link: "https://www.certego.net/en/news/threat-matrix-release-v4-0-0/", + title: "ThreatMatrix: Open-source threat intelligence management", + subText: "HelpNetSecurity: Interview with Matteo Lodi", + date: "14th August 2024", + link: "https://www.helpnetsecurity.com/2024/08/14/threatmatrix-open-source-threat-intelligence-management/", }, { - title: "ThreatMatrix: Release v3.0.0", - subText: "Honeynet Blog: v3.0.0 Announcement", - date: "13th September 2021", - link: "https://www.honeynet.org/2021/09/13/threat-matrix-release-v3-0-0/", + title: "ThreatMatrix: Making the life of cyber security analysts easier", + subText: "FIRSTCON24 Fukuoka Talk with Matteo Lodi and Simone Berni", + date: "10th June 2024", + link: "https://www.youtube.com/watch?v=1L5rzvlRjdU", }, { - title: - "Threat Matrix – OSINT tool automates the intel-gathering process using a single API", - subText: "Daily Swig: Interview with Matteo Lodi and Eshaan Bansal", - date: "18th August 2020", - link: "https://portswigger.net/daily-swig/threat-matrix-osint-tool-automates-the-intel-gathering-process-using-a-single-api", + title: "From Zero to ThreatMatrix!", + subText: "The Honeynet Workshop: Denmark 2024", + date: "29th May 2024", + link: "https://github.com/khulnasoft/thp_workshop_2024", }, { title: "New year, new tool: Threat Matrix", subText: "Certego Blog: First announcement", date: "2nd January 2020", - link: "https://www.certego.net/en/news/new-year-new-tool-threat-matrix/", + link: "https://www.certego.net/en/news/new-year-new-tool-intel-owl/", }, ]; diff --git a/frontend/src/components/jobs/result/bar/JobActionBar.jsx b/frontend/src/components/jobs/result/bar/JobActionBar.jsx index 767c8b11..e9d6c3d1 100644 --- a/frontend/src/components/jobs/result/bar/JobActionBar.jsx +++ b/frontend/src/components/jobs/result/bar/JobActionBar.jsx @@ -16,6 +16,7 @@ import { retryJobIcon, downloadReportIcon, } from "../../../common/icon/icons"; +import { fileDownload } from "../../../../utils/files"; export function JobActionsBar({ job }) { // routers @@ -29,16 +30,6 @@ export function JobActionsBar({ job }) { setTimeout(() => navigate(-1), 250); }; - const fileDownload = (blob, filename) => { - // create URL blob and a hidden tag to serve file for download - const fileLink = document.createElement("a"); - fileLink.href = window.URL.createObjectURL(blob); - fileLink.rel = "noopener,noreferrer"; - fileLink.download = `${filename}`; - // triggers the click event - fileLink.click(); - }; - const onDownloadSampleBtnClick = async () => { const blob = await downloadJobSample(job.id); if (!blob) return; diff --git a/frontend/src/components/jobs/result/visualizer/elements/const.js b/frontend/src/components/jobs/result/visualizer/elements/const.js index 7e0d1a21..71bb52d5 100644 --- a/frontend/src/components/jobs/result/visualizer/elements/const.js +++ b/frontend/src/components/jobs/result/visualizer/elements/const.js @@ -5,4 +5,5 @@ export const VisualizerComponentType = Object.freeze({ HLIST: "horizontal_list", TITLE: "title", TABLE: "table", + DOWNLOAD: "download", }); diff --git a/frontend/src/components/jobs/result/visualizer/elements/download.jsx b/frontend/src/components/jobs/result/visualizer/elements/download.jsx new file mode 100644 index 00000000..4409954f --- /dev/null +++ b/frontend/src/components/jobs/result/visualizer/elements/download.jsx @@ -0,0 +1,87 @@ +import React from "react"; +import { Button } from "reactstrap"; +import PropTypes from "prop-types"; +import { FaFileDownload } from "react-icons/fa"; +import { fileDownload, humanReadbleSize } from "../../../../../utils/files"; +import { VisualizerTooltip } from "../VisualizerTooltip"; + +export function DownloadVisualizer({ + size, + alignment, + disable, + id, + value, + mimetype, + payload, + isChild, + copyText, + description, + addMetadataInDescription, + link, +}) { + const blobFile = new Blob([payload], { type: mimetype }); + let finalDescription = description; + if (addMetadataInDescription) { + finalDescription += `\n\n**Mimetype**: ${mimetype}. **Size**: ${humanReadbleSize( + blobFile.size, + )}`; + } + + return ( + <> +
+ +
+ + + ); +} + +DownloadVisualizer.propTypes = { + size: PropTypes.string.isRequired, + alignment: PropTypes.string, + disable: PropTypes.bool, + id: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + mimetype: PropTypes.string.isRequired, + payload: PropTypes.string.isRequired, + isChild: PropTypes.bool, + copyText: PropTypes.string, + description: PropTypes.string, + addMetadataInDescription: PropTypes.bool, + link: PropTypes.string, +}; + +DownloadVisualizer.defaultProps = { + alignment: "center", + disable: false, + isChild: false, + copyText: "", + description: "", + addMetadataInDescription: true, + link: "", +}; diff --git a/frontend/src/components/jobs/result/visualizer/validators.js b/frontend/src/components/jobs/result/visualizer/validators.js index fa4be52a..221b81ed 100644 --- a/frontend/src/components/jobs/result/visualizer/validators.js +++ b/frontend/src/components/jobs/result/visualizer/validators.js @@ -1,3 +1,4 @@ +import { FileMimeTypes } from "../../../../constants/jobConst"; import { VisualizerComponentType } from "./elements/const"; function parseLevelSize(value) { @@ -49,16 +50,7 @@ function parseElementWidth(value) { } function parseComponentType(value) { - if ( - [ - VisualizerComponentType.BASE, - VisualizerComponentType.TITLE, - VisualizerComponentType.BOOL, - VisualizerComponentType.VLIST, - VisualizerComponentType.HLIST, - VisualizerComponentType.TABLE, - ].includes(value) - ) { + if (Object.values(VisualizerComponentType).includes(value)) { return value; } // default type @@ -101,8 +93,18 @@ function parseString(value) { return String(stringValue); } +function parseMimetype(value) { + if (Object.values(FileMimeTypes).includes(value)) { + return value; + } + return FileMimeTypes.OCTET; +} + // parse list of Elements function parseElementList(rawElementList) { + if (!Array.isArray(rawElementList)) { + return []; + } return rawElementList?.map((additionalElementrawData) => parseElementFields(additionalElementrawData), ); @@ -142,6 +144,20 @@ function parseElementFields(rawElement) { // validation for the elements switch (validatedFields.type) { + case VisualizerComponentType.DOWNLOAD: { + validatedFields.value = parseString(rawElement.value); + validatedFields.mimetype = parseMimetype(rawElement.mimetype); + validatedFields.payload = parseString(rawElement.payload); + validatedFields.copyText = parseString( + rawElement.copy_text || rawElement.value, + ); + validatedFields.description = parseString(rawElement.description); + validatedFields.addMetadataInDescription = parseBool( + rawElement.add_metadata_in_description, + ); + validatedFields.link = parseString(rawElement.link); + break; + } case VisualizerComponentType.BOOL: { validatedFields.value = parseString(rawElement.value); validatedFields.icon = parseString(rawElement.icon); @@ -173,9 +189,9 @@ function parseElementFields(rawElement) { } case VisualizerComponentType.TABLE: { validatedFields.data = parseElementListOfDict(rawElement.data || []); - validatedFields.columns = rawElement.columns.map((column) => - parseColumnElementList(column), - ); + validatedFields.columns = Array.isArray(rawElement.columns) + ? rawElement.columns.map((column) => parseColumnElementList(column)) + : []; validatedFields.pageSize = rawElement.page_size; validatedFields.sortById = parseString(rawElement.sort_by_id); validatedFields.sortByDesc = parseBool(rawElement.sort_by_desc); diff --git a/frontend/src/components/jobs/result/visualizer/visualizer.jsx b/frontend/src/components/jobs/result/visualizer/visualizer.jsx index acae849a..4cc13c1d 100644 --- a/frontend/src/components/jobs/result/visualizer/visualizer.jsx +++ b/frontend/src/components/jobs/result/visualizer/visualizer.jsx @@ -14,18 +14,37 @@ import { getIcon } from "./icons"; import { HorizontalListVisualizer } from "./elements/horizontalList"; import { TableVisualizer } from "./elements/table"; +import { DownloadVisualizer } from "./elements/download"; /** * Convert the validated data into a VisualizerElement. * This is a recursive function: It's called by the component to convert the inner components. * * @param {object} element data used to generate the component - * @param {bool} isChild flag used in Title and VList to create a smaller children components. + * @param {boolean} isChild flag used in Title and VList to create a smaller children components. * @returns {Object} component to visualize */ function convertToElement(element, idElement, isChild = false) { let visualizerElement; switch (element.type) { + case VisualizerComponentType.DOWNLOAD: { + visualizerElement = ( + + ); + break; + } case VisualizerComponentType.BOOL: { visualizerElement = ( {description} For more info check the{" "} official doc. diff --git a/frontend/src/constants/apiURLs.js b/frontend/src/constants/apiURLs.js index a49f64da..f13dc228 100644 --- a/frontend/src/constants/apiURLs.js +++ b/frontend/src/constants/apiURLs.js @@ -19,12 +19,14 @@ export const PLAYBOOKS_CONFIG_URI = `${API_BASE_URI}/playbook`; export const PLAYBOOKS_ANALYZE_MULTIPLE_FILES_URI = `${PLAYBOOKS_CONFIG_URI}/analyze_multiple_files`; export const PLAYBOOKS_ANALYZE_MULTIPLE_OBSERVABLE_URI = `${PLAYBOOKS_CONFIG_URI}/analyze_multiple_observables`; -export const JOB_AGG_STATUS_URI = `${JOB_BASE_URI}/aggregate/status`; -export const JOB_AGG_TYPE_URI = `${JOB_BASE_URI}/aggregate/type`; -export const JOB_AGG_OBS_CLASSIFICATION_URI = `${JOB_BASE_URI}/aggregate/observable_classification`; -export const JOB_AGG_FILE_MIMETYPE_URI = `${JOB_BASE_URI}/aggregate/file_mimetype`; -export const JOB_AGG_OBS_NAME_URI = `${JOB_BASE_URI}/aggregate/observable_name`; -export const JOB_AGG_FILE_MD5_URI = `${JOB_BASE_URI}/aggregate/md5`; +const AGGREGATE_PATH = "/aggregate"; +export const JOB_AGG_STATUS_URI = `${JOB_BASE_URI}${AGGREGATE_PATH}/status`; +export const JOB_AGG_TYPE_URI = `${JOB_BASE_URI}${AGGREGATE_PATH}/type`; +export const JOB_AGG_OBS_CLASSIFICATION_URI = `${JOB_BASE_URI}${AGGREGATE_PATH}/observable_classification`; +export const JOB_AGG_FILE_MIMETYPE_URI = `${JOB_BASE_URI}${AGGREGATE_PATH}/file_mimetype`; +export const JOB_AGG_TOP_PLAYBOOK_URI = `${JOB_BASE_URI}${AGGREGATE_PATH}/top_playbook`; +export const JOB_AGG_TOP_USER_URI = `${JOB_BASE_URI}${AGGREGATE_PATH}/top_user`; +export const JOB_AGG_TOP_TLP_URI = `${JOB_BASE_URI}${AGGREGATE_PATH}/top_tlp`; export const JOB_RECENT_SCANS = `${JOB_BASE_URI}/recent_scans`; export const JOB_RECENT_SCANS_USER = `${JOB_BASE_URI}/recent_scans_user`; @@ -47,6 +49,5 @@ export const AUTH_BASE_URI = `${API_BASE_URI}/auth`; export const APIACCESS_BASE_URI = `${AUTH_BASE_URI}/apiaccess`; // WEBSOCKETS -export const WEBSOCKET_BASE_URI = "ws"; - +const WEBSOCKET_BASE_URI = "ws"; export const WEBSOCKET_JOBS_URI = `${WEBSOCKET_BASE_URI}/jobs`; diff --git a/frontend/src/constants/environment.js b/frontend/src/constants/environment.js index 91a5ceef..e44e4f49 100644 --- a/frontend/src/constants/environment.js +++ b/frontend/src/constants/environment.js @@ -1,5 +1,6 @@ /* eslint-disable prefer-destructuring */ -export const THREATMATRIX_DOCS_URL = "https://khulnasoft.github.io/docs/"; +export const THREATMATRIX_DOCS_URL = + "https://khulnasoft.github.io/devsec-docs/"; export const PYTHREATMATRIX_GH_URL = "https://github.com/khulnasoft/pythreatmatrix"; export const THREATMATRIX_TWITTER_ACCOUNT = "threat_matrix"; diff --git a/frontend/src/utils/files.js b/frontend/src/utils/files.js new file mode 100644 index 00000000..500b85c6 --- /dev/null +++ b/frontend/src/utils/files.js @@ -0,0 +1,20 @@ +export const fileDownload = (blob, filename) => { + // create URL blob and a hidden
tag to serve file for download + const fileLink = document.createElement("a"); + fileLink.href = window.URL.createObjectURL(blob); + fileLink.rel = "noopener,noreferrer"; + fileLink.download = `${filename}`; + // triggers the click event + fileLink.click(); + console.debug("clicked"); +}; + +export const humanReadbleSize = (byteNumber) => { + if (byteNumber === 0) { + return "0.00 B"; + } + const number = Math.floor(Math.log(byteNumber) / Math.log(1024)); + return `${(byteNumber / 1024 ** number).toFixed(2)} ${" KMGTP".charAt( + number, + )}B`; +}; diff --git a/frontend/tests/components/dashboard/Dashboard.test.jsx b/frontend/tests/components/dashboard/Dashboard.test.jsx new file mode 100644 index 00000000..e0403e9b --- /dev/null +++ b/frontend/tests/components/dashboard/Dashboard.test.jsx @@ -0,0 +1,27 @@ +import React from "react"; +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import Dashboard from "../../../src/components/dashboard/Dashboard"; + +describe("test Dashboard component", () => { + test("Dashboard page", async () => { + render(); + + // header + expect(screen.getByText("Dashboard")).toBeInTheDocument(); + // time picker + expect(screen.getByText("6h")).toBeInTheDocument(); + expect(screen.getByText("24h")).toBeInTheDocument(); + expect(screen.getByText("7d")).toBeInTheDocument(); + // charts + expect(screen.getByText("Job: Status")).toBeInTheDocument(); + expect(screen.getByText("Job: Type")).toBeInTheDocument(); + expect( + screen.getByText("Job: Observable Classification"), + ).toBeInTheDocument(); + expect(screen.getByText("Job: File Mimetype")).toBeInTheDocument(); + expect(screen.getByText("Job: Top 5 Playbooks")).toBeInTheDocument(); + expect(screen.getByText("Job: Top 5 Users")).toBeInTheDocument(); + expect(screen.getByText("Job: Top 5 TLP")).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/components/dashboard/charts.test.jsx b/frontend/tests/components/dashboard/charts.test.jsx new file mode 100644 index 00000000..2fb230da --- /dev/null +++ b/frontend/tests/components/dashboard/charts.test.jsx @@ -0,0 +1,516 @@ +import React from "react"; +import useAxios from "axios-hooks"; +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import { + JobFileMimetypeBarChart, + JobObsClassificationBarChart, + JobStatusBarChart, + JobTopPlaybookBarChart, + JobTopTLPBarChart, + JobTopUserBarChart, + JobTypeBarChart, +} from "../../../src/components/dashboard/charts"; + +jest.mock("axios-hooks"); +jest.mock("recharts", () => { + const OriginalModule = jest.requireActual("recharts"); + return { + ...OriginalModule, + ResponsiveContainer: ({ children }) => ( + + {children} + + ), + }; +}); + +describe("test dashboard's charts", () => { + global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), + })); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("test JobStatusBarChart", async () => { + useAxios.mockReturnValue([ + { + data: [ + { + date: "2024-11-28T22:00:00Z", + pending: 0, + reported_without_fails: 8, + reported_with_fails: 0, + failed: 0, + }, + { + date: "2024-11-29T22:00:00Z", + pending: 0, + reported_without_fails: 0, + reported_with_fails: 1, + failed: 1, + }, + { + date: "2024-11-29T23:00:00Z", + pending: 0, + reported_without_fails: 1, + reported_with_fails: 2, + failed: 0, + }, + ], + loading: false, + error: null, + }, + ]); + + render(); + + // needed to support different timezones (ex: ci and local could be different) + expect( + screen.getByText( + `28/11, ${new Date("2024-11-28T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T23:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect(screen.getByText("pending")).toBeInTheDocument(); + expect(screen.getByText("reported_without_fails")).toBeInTheDocument(); + expect(screen.getByText("reported_with_fails")).toBeInTheDocument(); + expect(screen.getByText("failed")).toBeInTheDocument(); + }); + + test("test JobStatusBarChart no data", async () => { + useAxios.mockReturnValue([ + { + data: [], + loading: false, + error: null, + }, + ]); + + render(); + expect( + screen.getByText("No data in the selected range."), + ).toBeInTheDocument(); + }); + + test("test JobTypeBarChart", async () => { + useAxios.mockReturnValue([ + { + data: [ + { + date: "2024-11-28T22:00:00Z", + file: 0, + observable: 8, + }, + { + date: "2024-11-29T22:00:00Z", + file: 2, + observable: 0, + }, + { + date: "2024-11-29T23:00:00Z", + file: 0, + observable: 3, + }, + ], + loading: false, + error: null, + }, + ]); + + render(); + + // needed to support different timezones (ex: ci and local could be different) + expect( + screen.getByText( + `28/11, ${new Date("2024-11-28T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T23:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect(screen.getByText("file")).toBeInTheDocument(); + expect(screen.getByText("observable")).toBeInTheDocument(); + }); + + test("test JobTypeBarChart no data", async () => { + useAxios.mockReturnValue([ + { + data: [], + loading: false, + error: null, + }, + ]); + + render(); + expect( + screen.getByText("No data in the selected range."), + ).toBeInTheDocument(); + }); + + test("test JobObsClassificationBarChart", async () => { + useAxios.mockReturnValue([ + { + data: [ + { + date: "2024-11-28T22:00:00Z", + ip: 0, + url: 0, + domain: 1, + hash: 0, + generic: 0, + }, + { + date: "2024-11-29T22:00:00Z", + ip: 0, + url: 0, + domain: 0, + hash: 0, + generic: 0, + }, + { + date: "2024-11-29T23:00:00Z", + ip: 0, + url: 0, + domain: 3, + hash: 0, + generic: 0, + }, + ], + loading: false, + error: null, + }, + ]); + + render(); + + // needed to support different timezones (ex: ci and local could be different) + expect( + screen.getByText( + `28/11, ${new Date("2024-11-28T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T23:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect(screen.getByText("ip")).toBeInTheDocument(); + expect(screen.getByText("url")).toBeInTheDocument(); + expect(screen.getByText("domain")).toBeInTheDocument(); + expect(screen.getByText("hash")).toBeInTheDocument(); + expect(screen.getByText("generic")).toBeInTheDocument(); + }); + + test("test JobObsClassificationBarChart no data", async () => { + useAxios.mockReturnValue([ + { + data: [], + loading: false, + error: null, + }, + ]); + + render(); + expect( + screen.getByText("No data in the selected range."), + ).toBeInTheDocument(); + }); + + test("test JobFileMimetypeBarChart", async () => { + useAxios.mockReturnValue([ + { + data: { + values: ["application/json", "text/plain"], + aggregation: [ + { + date: "2024-11-28T22:00:00Z", + "application/json": 0, + "text/plain": 0, + }, + { + date: "2024-11-29T22:00:00Z", + "application/json": 1, + "text/plain": 1, + }, + { + date: "2024-11-29T23:00:00Z", + "application/json": 0, + "text/plain": 0, + }, + ], + }, + loading: false, + error: null, + }, + ]); + + render(); + + // needed to support different timezones (ex: ci and local could be different) + expect( + screen.getByText( + `28/11, ${new Date("2024-11-28T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T23:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect(screen.getByText("application/json")).toBeInTheDocument(); + expect(screen.getByText("text/plain")).toBeInTheDocument(); + }); + + test("test JobFileMimetypeBarChart no data", async () => { + useAxios.mockReturnValue([ + { + data: [], + loading: false, + error: null, + }, + ]); + + render(); + expect( + screen.getByText("No data in the selected range."), + ).toBeInTheDocument(); + }); + + test("test JobTopPlaybookBarChart", async () => { + useAxios.mockReturnValue([ + { + data: { + values: ["Dns", "FREE_TO_USE_ANALYZERS", "Passive_DNS"], + aggregation: [ + { + date: "2024-11-28T22:00:00Z", + Dns: 5, + FREE_TO_USE_ANALYZERS: 1, + Passive_DNS: 3, + }, + { + date: "2024-11-29T22:00:00Z", + Dns: 1, + FREE_TO_USE_ANALYZERS: 0, + Passive_DNS: 0, + }, + { + date: "2024-11-29T23:00:00Z", + Dns: 1, + FREE_TO_USE_ANALYZERS: 0, + Passive_DNS: 0, + }, + ], + }, + loading: false, + error: null, + }, + ]); + + render(); + + // needed to support different timezones (ex: ci and local could be different) + expect( + screen.getByText( + `28/11, ${new Date("2024-11-28T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T23:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect(screen.getByText("Dns")).toBeInTheDocument(); + expect(screen.getByText("FREE_TO_USE_ANALYZERS")).toBeInTheDocument(); + expect(screen.getByText("Passive_DNS")).toBeInTheDocument(); + }); + + test("test JobTopPlaybookBarChart no data", async () => { + useAxios.mockReturnValue([ + { + data: [], + loading: false, + error: null, + }, + ]); + + render(); + expect( + screen.getByText("No data in the selected range."), + ).toBeInTheDocument(); + }); + + test("test JobTopUserBarChart", async () => { + useAxios.mockReturnValue([ + { + data: { + values: ["user_a", "user_b", "user_c"], + aggregation: [ + { + date: "2024-11-28T22:00:00Z", + user_a: 5, + user_b: 1, + user_c: 3, + }, + { + date: "2024-11-29T22:00:00Z", + user_a: 1, + user_b: 0, + user_c: 0, + }, + { + date: "2024-11-29T23:00:00Z", + user_a: 1, + user_b: 0, + user_c: 0, + }, + ], + }, + loading: false, + error: null, + }, + ]); + + render(); + + // needed to support different timezones (ex: ci and local could be different) + expect( + screen.getByText( + `28/11, ${new Date("2024-11-28T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T23:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect(screen.getByText("user_a")).toBeInTheDocument(); + expect(screen.getByText("user_b")).toBeInTheDocument(); + expect(screen.getByText("user_c")).toBeInTheDocument(); + }); + + test("test JobTopUserBarChart no data", async () => { + useAxios.mockReturnValue([ + { + data: [], + loading: false, + error: null, + }, + ]); + + render(); + expect( + screen.getByText("No data in the selected range."), + ).toBeInTheDocument(); + }); + + test("test JobTopTLPBarChart", async () => { + useAxios.mockReturnValue([ + { + data: { + values: ["AMBER", "CLEAR", "RED"], + aggregation: [ + { + date: "2024-11-28T22:00:00Z", + AMBER: 5, + CLEAR: 1, + RED: 3, + }, + { + date: "2024-11-29T22:00:00Z", + AMBER: 1, + CLEAR: 0, + RED: 0, + }, + { + date: "2024-11-29T23:00:00Z", + AMBER: 1, + CLEAR: 0, + RED: 0, + }, + ], + }, + loading: false, + error: null, + }, + ]); + + render(); + + // needed to support different timezones (ex: ci and local could be different) + expect( + screen.getByText( + `28/11, ${new Date("2024-11-28T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T22:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + `29/11, ${new Date("2024-11-29T23:00:00Z").getHours()}:00`, + ), + ).toBeInTheDocument(); + expect(screen.getByText("AMBER")).toBeInTheDocument(); + expect(screen.getByText("CLEAR")).toBeInTheDocument(); + expect(screen.getByText("RED")).toBeInTheDocument(); + }); + + test("test JobTopTLPBarChart no data", async () => { + useAxios.mockReturnValue([ + { + data: [], + loading: false, + error: null, + }, + ]); + + render(); + expect( + screen.getByText("No data in the selected range."), + ).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/components/jobs/result/visualizer/elements/download.test.jsx b/frontend/tests/components/jobs/result/visualizer/elements/download.test.jsx new file mode 100644 index 00000000..afd80ebc --- /dev/null +++ b/frontend/tests/components/jobs/result/visualizer/elements/download.test.jsx @@ -0,0 +1,117 @@ +import React from "react"; +import "@testing-library/jest-dom"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { DownloadVisualizer } from "../../../../../../src/components/jobs/result/visualizer/elements/download"; + +// mock useLocation +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useLocation: () => ({ + pathname: "localhost/jobs/123/visualizer", + }), +})); + +describe("DownalodVisualizer component", () => { + test("required-only params", async () => { + const { container } = render( + , + ); + + // check id + const idElement = container.querySelector("#test-id"); + expect(idElement).toBeInTheDocument(); + // chec text (inner span) + const innerPartComponent = screen.getByText("test-required.txt"); + expect(innerPartComponent).toBeInTheDocument(); + // check no link + expect(innerPartComponent.closest("div").style).not.toHaveProperty( + "text-decoration", + "underline dotted", + ); + // check size and alignment + const outerPartComponent = innerPartComponent.closest("div"); + expect(outerPartComponent.className).toBe( + "col-1 p-0 m-1 d-flex align-items-center text-center justify-content-center ", + ); + // check tooltip + const user = userEvent.setup(); + await user.hover(innerPartComponent); + await waitFor(() => { + const tooltipElement = screen.getByRole("tooltip"); + expect(tooltipElement).toBeInTheDocument(); + }); + }); + + test("all params", async () => { + const { container } = render( + , + ); + + // check id + const idElement = container.querySelector("#test-id"); + expect(idElement).toBeInTheDocument(); + // chec text (inner span) + const innerPartComponent = screen.getByText("test-all.txt"); + expect(innerPartComponent).toBeInTheDocument(); + // check optional elements + expect(idElement.className).toBe( + "col-2 small p-0 m-1 d-flex align-items-center text-end justify-content-end ", + ); + // check tooltip + const user = userEvent.setup(); + await user.hover(innerPartComponent); + await waitFor(() => { + const tooltipElement = screen.getByRole("tooltip"); + expect(tooltipElement).toBeInTheDocument(); + }); + }); + + test("test disable", async () => { + // it's a special case because change the style, but also the interactions + + const { container } = render( + , + ); + + // check id + const idElement = container.querySelector("#test-id"); + expect(idElement).toBeInTheDocument(); + // chec text (inner span) + const innerPartComponent = screen.getByText("test-disabled.txt"); + expect(innerPartComponent).toBeInTheDocument(); + // check optional elements + expect(idElement.className).toBe( + "col-2 small p-0 m-1 d-flex align-items-center text-end justify-content-end opacity-25", + ); + }); +}); diff --git a/frontend/tests/components/jobs/result/visualizer/validators.test.js b/frontend/tests/components/jobs/result/visualizer/validators.test.js index 207d037e..53d55111 100644 --- a/frontend/tests/components/jobs/result/visualizer/validators.test.js +++ b/frontend/tests/components/jobs/result/visualizer/validators.test.js @@ -12,6 +12,12 @@ describe("visualizer data validation", () => { values: [ {}, { type: "base" }, + { + type: "download", + value: "test.txt", + payload: "hello, world", + mimetype: "text/plain", + }, { type: "bool", disable: true }, { type: "title", title: { type: "base" }, value: { type: "base" } }, { type: "horizontal_list", value: [] }, @@ -57,6 +63,19 @@ describe("visualizer data validation", () => { type: "base", value: "", }, + { + addMetadataInDescription: false, + alignment: "around", + copyText: "test.txt", + description: "", + disable: false, + link: "", + size: "col-auto", + type: "download", + value: "test.txt", + payload: "hello, world", + mimetype: "text/plain", + }, { activeColor: "danger", alignment: "around", @@ -170,6 +189,19 @@ describe("visualizer data validation", () => { copy_text: "placeholder", description: "description", }, + { + add_metadata_in_description: true, + type: "download", + value: "test.txt", + payload: "hello, world", + mimetype: "text/plain", + link: "https://test.com", + disable: false, + size: "1", + alignment: "start", + copy_text: "test.txt", + description: "this is a test file", + }, { type: "bool", value: "phishing", @@ -357,6 +389,19 @@ describe("visualizer data validation", () => { type: "base", value: "placeholder", }, + { + type: "download", + value: "test.txt", + payload: "hello, world", + mimetype: "text/plain", + link: "https://test.com", + disable: false, + size: "col-1", + alignment: "start", + copyText: "test.txt", + addMetadataInDescription: true, + description: "this is a test file", + }, { activeColor: "danger", alignment: "end", @@ -556,76 +601,73 @@ describe("visualizer data validation", () => { type: "horizontal_list", values: [ { - type: "not existing type", + type: "base", value: null, icon: "invalid icon", - color: "#ff0000", - link: "https://google.com", - bold: "yes it's bold!", - italic: "yes it's italic!", - disable: "yes it's disabled", + color: "invalid color", + link: {}, + bold: {}, + italic: {}, + disable: {}, size: "120", - alignment: "start", + alignment: {}, }, { - type: "not existing type", + type: "bool", value: undefined, icon: "invalid icon", - color: "#ff0000", + color: "invalid", link: "https://google.com", - bold: "yes it's bold!", italic: "yes it's italic!", disable: "yes it's disabled", size: "120", - alignment: "start", + alignment: {}, }, { - type: "not existing type", - value: 0, - icon: "invalid icon", - color: "#ff0000", - link: "https://google.com", - bold: "yes it's bold!", - italic: "yes it's italic!", + type: "vertical_list", + name: 0, + values: "invalid", + start_open: "invalid", disable: "yes it's disabled", size: "120", - alignment: "start", + alignment: {}, }, { - type: "not existing type", - value: 10, - icon: "invalid icon", - color: "#ff0000", - link: "https://google.com", - bold: "yes it's bold!", - italic: "yes it's italic!", + type: "horizontal_list", + value: "invalid", disable: "yes it's disabled", size: "120", - alignment: "start", + alignment: {}, }, { - type: "not existing type", - value: [1, 2, 3], - icon: "invalid icon", - color: "#ff0000", - link: "https://google.com", - bold: "yes it's bold!", - italic: "yes it's italic!", + type: "title", + title: 0, + value: 0, disable: "yes it's disabled", size: "120", - alignment: "start", + alignment: {}, }, { - type: "not existing type", - value: { value: "invalid" }, - icon: "invalid icon", - color: "#ff0000", - link: "https://google.com", - bold: "yes it's bold!", - italic: "yes it's italic!", + type: "table", + page_size: "invalid", + sort_by_id: "invalid", + sort_by_desc: "invalid", disable: "yes it's disabled", size: "120", - alignment: "start", + alignment: {}, + }, + { + type: "download", + disable: "yes it's disabled", + size: "120", + alignment: {}, + value: {}, + mimetype: "nanana", + payload: {}, + copy_text: {}, + description: {}, + add_metadata_in_description: {}, + link: {}, }, ], }, @@ -638,23 +680,22 @@ describe("visualizer data validation", () => { type: "horizontal_list", values: [ { - alignment: "start", - bold: true, + alignment: "around", + bold: false, color: "bg-undefined", description: "", copyText: "", - disable: true, + disable: false, icon: "invalid icon", - italic: true, - link: "https://google.com", + italic: false, + link: "{}", size: "col-auto", type: "base", value: "", }, { - alignment: "start", - bold: true, - color: "bg-undefined", + alignment: "around", + activeColor: "danger", description: "", copyText: "", disable: true, @@ -662,64 +703,95 @@ describe("visualizer data validation", () => { italic: true, link: "https://google.com", size: "col-auto", - type: "base", + type: "bool", value: "", }, { - alignment: "start", - bold: true, - color: "bg-undefined", - description: "", - copyText: "0", + alignment: "around", disable: true, - icon: "invalid icon", - italic: true, - link: "https://google.com", size: "col-auto", - type: "base", - value: "0", + type: "vertical_list", + name: { + alignment: "around", + bold: false, + color: "bg-undefined", + description: "", + copyText: "", + disable: false, + icon: "", + italic: false, + link: "", + size: "col-auto", + type: "base", + value: "", + }, + values: [], + startOpen: true, }, { - alignment: "start", - bold: true, - color: "bg-undefined", - description: "", - copyText: "10", + alignment: "around", disable: true, - icon: "invalid icon", - italic: true, - link: "https://google.com", size: "col-auto", - type: "base", - value: "10", + type: "horizontal_list", + values: [], }, { - alignment: "start", - bold: true, - color: "bg-undefined", - description: "", - copyText: "1,2,3", + alignment: "around", disable: true, - icon: "invalid icon", - italic: true, - link: "https://google.com", size: "col-auto", - type: "base", - value: "1,2,3", + type: "title", + title: { + alignment: "around", + bold: false, + color: "bg-undefined", + description: "", + copyText: "", + disable: false, + icon: "", + italic: false, + link: "", + size: "col-auto", + type: "base", + value: "", + }, + value: { + alignment: "around", + bold: false, + color: "bg-undefined", + description: "", + copyText: "", + disable: false, + icon: "", + italic: false, + link: "", + size: "col-auto", + type: "base", + value: "", + }, }, { - alignment: "start", - bold: true, - color: "bg-undefined", - description: "", - copyText: '{"value":"invalid"}', + alignment: "around", disable: true, - icon: "invalid icon", - italic: true, - link: "https://google.com", size: "col-auto", - type: "base", - value: '{"value":"invalid"}', + type: "table", + pageSize: "invalid", + sortById: "invalid", + sortByDesc: true, + columns: [], + data: [], + }, + { + alignment: "around", + disable: true, + size: "col-auto", + type: "download", + value: "{}", + mimetype: "application/octet-stream", + payload: "{}", + copyText: "{}", + description: "{}", + addMetadataInDescription: false, + link: "{}", }, ], }, diff --git a/frontend/tests/components/jobs/result/visualizer/visualizer.test.jsx b/frontend/tests/components/jobs/result/visualizer/visualizer.test.jsx index 18db76e1..2d885109 100644 --- a/frontend/tests/components/jobs/result/visualizer/visualizer.test.jsx +++ b/frontend/tests/components/jobs/result/visualizer/visualizer.test.jsx @@ -195,6 +195,19 @@ describe("test VisualizerReport (conversion from backend data to frontend compon sort_by_id: "", sort_by_desc: false, }, + { + type: "download", + size: "auto", + alignment: "around", + disable: true, + value: "test.txt", + payload: "hello, world", + copy_text: "base component", + description: "test file", + mimetype: "plain/text", + add_metadata_in_description: true, + link: "", + }, ], alignment: "around", }, @@ -237,5 +250,6 @@ describe("test VisualizerReport (conversion from backend data to frontend compon // check base and bool are still present in the document expect(screen.getByText("base component")).toBeInTheDocument(); expect(screen.getByText("bool component")).toBeInTheDocument(); + expect(screen.getByText("test.txt")).toBeInTheDocument(); }); }); diff --git a/frontend/tests/layouts/AppHeader.test.jsx b/frontend/tests/layouts/AppHeader.test.jsx index a3413a93..cf4548c3 100644 --- a/frontend/tests/layouts/AppHeader.test.jsx +++ b/frontend/tests/layouts/AppHeader.test.jsx @@ -50,7 +50,7 @@ describe("test AppHeader component", () => { const docsButton = screen.getByText("Docs"); expect(docsButton).toBeInTheDocument(); expect(docsButton.closest("a").href).toBe( - "https://khulnasoft.github.io/docs/", + "https://khulnasoft.github.io/devsec-docs/", ); const socialButton = screen.getByText("Social"); @@ -119,7 +119,7 @@ describe("test AppHeader component", () => { const docsButton = screen.getByText("Docs"); expect(docsButton).toBeInTheDocument(); expect(docsButton.closest("a").href).toBe( - "https://khulnasoft.github.io/docs/", + "https://khulnasoft.github.io/devsec-docs/", ); const socialButton = screen.getByText("Social"); @@ -190,7 +190,7 @@ describe("test AppHeader component", () => { const docsButton = screen.getByText("Docs"); expect(docsButton).toBeInTheDocument(); expect(docsButton.closest("a").href).toBe( - "https://khulnasoft.github.io/docs/", + "https://khulnasoft.github.io/devsec-docs/", ); const socialButton = screen.getByText("Social"); diff --git a/frontend/tests/utils/files.test.js b/frontend/tests/utils/files.test.js new file mode 100644 index 00000000..86628950 --- /dev/null +++ b/frontend/tests/utils/files.test.js @@ -0,0 +1,13 @@ +const { humanReadbleSize } = require("../../src/utils/files"); + +describe("test utilities functions for files", () => { + test("test humanReadbleSize", () => { + expect(humanReadbleSize(0)).toBe("0.00 B"); + expect(humanReadbleSize(1)).toBe("1.00 B"); + expect(humanReadbleSize(1024 ** 1)).toBe("1.00 KB"); + expect(humanReadbleSize(1024 ** 2)).toBe("1.00 MB"); + expect(humanReadbleSize(1024 ** 3)).toBe("1.00 GB"); + expect(humanReadbleSize(1024 ** 4)).toBe("1.00 TB"); + expect(humanReadbleSize(1024 ** 5)).toBe("1.00 PB"); + }); +}); diff --git a/integrations/__init__.py b/integrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/integrations/malware_tools_analyzers/Dockerfile b/integrations/malware_tools_analyzers/Dockerfile index f0d19f4a..69cb3f2d 100644 --- a/integrations/malware_tools_analyzers/Dockerfile +++ b/integrations/malware_tools_analyzers/Dockerfile @@ -30,8 +30,8 @@ RUN npm install box-js@1.9.17 --global --production \ # Install CAPA WORKDIR ${PROJECT_PATH}/capa -RUN wget -q https://github.com/mandiant/capa/releases/download/v7.3.0/capa-v7.3.0-linux.zip \ - && unzip capa-v7.3.0-linux.zip \ +RUN wget -q https://github.com/mandiant/capa/releases/download/v8.0.0/capa-v8.0.0-linux.zip \ + && unzip capa-v8.0.0-linux.zip \ && ln -s ${PROJECT_PATH}/capa/capa /usr/local/bin/capa # Install Floss diff --git a/integrations/malware_tools_analyzers/compose-tests.yml b/integrations/malware_tools_analyzers/compose-tests.yml index 269ae9df..f84a1a97 100644 --- a/integrations/malware_tools_analyzers/compose-tests.yml +++ b/integrations/malware_tools_analyzers/compose-tests.yml @@ -1,3 +1,5 @@ +# All additional integrations should be added following this format only. + services: malware_tools_analyzers: build: diff --git a/integrations/malware_tools_analyzers/compose.yml b/integrations/malware_tools_analyzers/compose.yml index 144b049d..bc6b527d 100644 --- a/integrations/malware_tools_analyzers/compose.yml +++ b/integrations/malware_tools_analyzers/compose.yml @@ -1,3 +1,5 @@ +# All additional integrations should be added following this format only. + services: malware_tools_analyzers: image: khulnasoft/threatmatrix_malware_tools_analyzers:${REACT_APP_THREATMATRIX_VERSION} diff --git a/integrations/pcap_analyzers/compose.yml b/integrations/pcap_analyzers/compose.yml index b4605530..4fe10dbc 100644 --- a/integrations/pcap_analyzers/compose.yml +++ b/integrations/pcap_analyzers/compose.yml @@ -1,3 +1,5 @@ +# All additional integrations should be added following this format only. + services: pcap_analyzers: image: khulnasoft/threatmatrix_pcap_analyzers:${REACT_APP_THREATMATRIX_VERSION} diff --git a/integrations/phishing_analyzers/Dockerfile b/integrations/phishing_analyzers/Dockerfile new file mode 100644 index 00000000..b5bea574 --- /dev/null +++ b/integrations/phishing_analyzers/Dockerfile @@ -0,0 +1,43 @@ +FROM python:3.12.3 + +ENV PROJECT_PATH=/opt/deploy +ENV LOG_PATH=/var/log/threat_matrix/phishing_analyzers +ENV USER=phishing-user +ENV HOME=${PYTHONPATH} + +# Add a new low-privileged user +RUN useradd -ms /bin/bash ${USER} + +# Install Google Chrome and Chromium +RUN DEBIAN_FRONTEND=noninteractive apt-get update -qq \ + && apt-get install -y --no-install-recommends \ + libvulkan1 libu2f-udev fonts-liberation chromium sudo \ + && wget --progress=dot:giga https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \ + && dpkg -i google-chrome-stable_current_amd64.deb \ + && pip3 install --no-cache-dir --upgrade pip \ + # Cleanup + && apt-get remove --purge -y gcc \ + && apt-get clean \ + && apt-get autoclean \ + && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* /tmp/* /usr/share/doc/* /usr/share/man/* > /dev/null 2>&1 + +# Create application environment and files +WORKDIR ${PROJECT_PATH}/phishing_analyzers +COPY --chown=${USER}:${USER} app.py requirements.txt entrypoint.sh ./ +COPY --chown=${USER}:${USER} analyzers/* ./analyzers/ +RUN chmod u+x entrypoint.sh \ + && pip3 install -r requirements.txt --no-cache-dir + +# unprivileged user must run some commands as root to avoid conflicts with other volumes. +# adding them to sudoers file to avoid running entrypoint as root usesr. +RUN echo "${USER} ALL=(root) NOPASSWD: /usr/bin/mkdir -p ${LOG_PATH}" > /etc/sudoers.d/create_log_directory \ + && echo "${USER} ALL=(root) NOPASSWD: /usr/bin/touch ${LOG_PATH}/gunicorn_access.log ${LOG_PATH}/gunicorn_errors.log" > /etc/sudoers.d/create_log_files \ + && echo "${USER} ALL=(root) NOPASSWD: /usr/bin/chown -R ${USER}\:${USER} ${PROJECT_PATH}/phishing_analyzers ${LOG_PATH}" > /etc/sudoers.d/chown_log_and_workdir_unprivileged + +# use a unprivileged user to run flask +USER ${USER} + +# Serve Flask application using gunicorn as low-privileged user +EXPOSE 4005 +ENTRYPOINT ["./entrypoint.sh"] diff --git a/integrations/phishing_analyzers/__init__.py b/integrations/phishing_analyzers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/integrations/phishing_analyzers/analyzers/__init__.py b/integrations/phishing_analyzers/analyzers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/integrations/phishing_analyzers/analyzers/driver_wrapper.py b/integrations/phishing_analyzers/analyzers/driver_wrapper.py new file mode 100644 index 00000000..388929d0 --- /dev/null +++ b/integrations/phishing_analyzers/analyzers/driver_wrapper.py @@ -0,0 +1,135 @@ +import functools +import logging +import os +from typing import Iterator + +from selenium.common import WebDriverException +from seleniumwire.request import Request +from seleniumwire.webdriver import ChromeOptions, Remote + +LOG_NAME = "driver_wrapper" + +# get flask-shell2http logger instance +logger = logging.getLogger(LOG_NAME) +# logger config +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +log_level = os.getenv("LOG_LEVEL", logging.INFO) +log_path = os.getenv("LOG_PATH", f"/var/log/threat_matrix/{LOG_NAME}") +# create new file handlers, files are created if doesn't already exists +fh = logging.FileHandler(f"{log_path}/{LOG_NAME}.log") +fh.setFormatter(formatter) +fh.setLevel(log_level) +fh_err = logging.FileHandler(f"{log_path}/{LOG_NAME}_errors.log") +fh_err.setFormatter(formatter) +fh_err.setLevel(logging.ERROR) +# add the handlers to the logger +logger.addHandler(fh) +logger.addHandler(fh_err) +logger.setLevel(log_level) + + +def driver_exception_handler(func): + @functools.wraps(func) + def handle_exception(self, *args, **kwargs): + # if url is set the action should be "navigate" + url = kwargs.get("url", "") + try: + return func(self, *args, **kwargs) + except WebDriverException as e: + logger.error( + f"Error while performing {func.__name__}" + f"{' for url=' + url if func.__name__ == 'navigate' else ''}: {e}" + ) + self.restart(motivation=func.__name__) + func(self, *args, **kwargs) + + return handle_exception + + +class DriverWrapper: + def __init__( + self, + proxy_address: str = "", + window_width: int = 1920, + window_height: int = 1080, + ): + self.proxy: str = proxy_address + self.window_width: int = window_width + self.window_height: int = window_height + self.last_url: str = "" + self._driver: Remote = self._init_driver(self.window_width, self.window_height) + + def _init_driver(self, window_width: int, window_height: int) -> Remote: + logger.info(f"Adding proxy with option: {self.proxy}") + logger.info("Creating Chrome driver...") + sw_options: {} = { + "auto_config": False, # Ensure this is set to False + "enable_har": True, + # https://github.com/wkeeling/selenium-wire/issues/220#issuecomment-794308386 + # config to have local seleniumwire proxy compatible with another proxy + "addr": "0.0.0.0", # where selenium-wire proxy will run + "port": 7007, + } + if self.proxy: + sw_options["proxy"] = {"http": self.proxy, "https": self.proxy} + + options = ChromeOptions() + # no_sandbox=True is a bad practice but it's almost the only way + # to run chromium-based browsers in docker. browser is running + # as unprivileged user and it's in a container: trade-off + options.add_argument("--no-sandbox") + options.add_argument("--headless=new") + options.add_argument("--ignore-certificate-errors") + options.add_argument(f"--window-size={window_width},{window_height}") + # traffic must go back to host running selenium-wire + options.add_argument("--proxy-server=http://phishing_analyzers:7007") + driver = Remote( + command_executor="http://selenium-hub:4444/wd/hub", + options=options, + seleniumwire_options=sw_options, + ) + return driver + + def restart(self, motivation: str = ""): + logger.info(f"Restarting driver: {motivation=}") + self._driver.quit() + self._driver = self._init_driver( + window_width=self.window_width, window_height=self.window_height + ) + if self.last_url: + logger.info(f"Navigating to {self.last_url} after driver has restarted") + self.navigate(self.last_url) + + @driver_exception_handler + def navigate(self, url: str = ""): + if not url: + logger.error("Empty URL! Something's wrong!") + return + + self.last_url = url + logger.info(f"Navigating to {url=}") + self._driver.get(url) + + @driver_exception_handler + def get_page_source(self) -> str: + logger.info(f"Extracting page source for url {self.last_url}") + return self._driver.page_source + + @driver_exception_handler + def get_current_url(self) -> str: + logger.info("Extracting current URL of page") + return self._driver.current_url + + @driver_exception_handler + def get_base64_screenshot(self) -> str: + logger.info(f"Extracting screenshot of page as base64 for url {self.last_url}") + return self._driver.get_screenshot_as_base64() + + def iter_requests(self) -> Iterator[Request]: + return self._driver.iter_requests() + + def get_har(self) -> str: + return self._driver.har + + def quit(self): + self._driver.quit() diff --git a/integrations/phishing_analyzers/analyzers/extract_phishing_site.py b/integrations/phishing_analyzers/analyzers/extract_phishing_site.py new file mode 100644 index 00000000..5696c44d --- /dev/null +++ b/integrations/phishing_analyzers/analyzers/extract_phishing_site.py @@ -0,0 +1,83 @@ +import base64 +import json +import logging +import os +from argparse import ArgumentParser + +from driver_wrapper import DriverWrapper +from seleniumwire_request_serializer import dump_seleniumwire_requests + +LOG_NAME = "extract_phishing_site" + +# get flask-shell2http logger instance +logger = logging.getLogger(LOG_NAME) +# logger config +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +log_level = os.getenv("LOG_LEVEL", logging.INFO) +log_path = os.getenv("LOG_PATH", f"/var/log/threat_matrix/{LOG_NAME}") +# create new file handlers, files are created if doesn't already exists +fh = logging.FileHandler(f"{log_path}/{LOG_NAME}.log") +fh.setFormatter(formatter) +fh.setLevel(log_level) +fh_err = logging.FileHandler(f"{log_path}/{LOG_NAME}_errors.log") +fh_err.setFormatter(formatter) +fh_err.setLevel(logging.ERROR) +# add the handlers to the logger +logger.addHandler(fh) +logger.addHandler(fh_err) +logger.setLevel(log_level) + + +def extract_driver_result(driver_wrapper: DriverWrapper) -> dict: + logger.info("Extracting driver result...") + driver_result: {} = { + "page_source": base64.b64encode( + driver_wrapper.get_page_source().encode("utf-8") + ).decode("utf-8"), + "page_screenshot_base64": driver_wrapper.get_base64_screenshot(), + "page_http_traffic": [ + dump_seleniumwire_requests(request) + for request in driver_wrapper.iter_requests() + ], + "page_http_har": driver_wrapper.get_har(), + } + logger.info("Finished extracting driver result") + logger.debug(f"{driver_result=}") + return driver_result + + +def analyze_target( + target_url: str, + proxy_address: str, + window_width: int = 1920, + window_height: int = 1080, +): + driver_wrapper = DriverWrapper( + proxy_address=proxy_address, + window_width=window_width, + window_height=window_height, + ) + driver_wrapper.navigate(url=target_url) + + result: str = json.dumps(extract_driver_result(driver_wrapper), default=str) + logger.debug(f"JSON dump of driver {result=}") + print(result) + + driver_wrapper.quit() + + +if __name__ == "__main__": + parser = ArgumentParser() + parser.add_argument("--target", type=str, required=True) + parser.add_argument("--proxy_address", type=str, required=False) + parser.add_argument("--window_width", type=int, required=False) + parser.add_argument("--window_height", type=int, required=False) + arguments = parser.parse_args() + logger.info(f"Extracted arguments for {LOG_NAME}: {vars(arguments)}") + + analyze_target( + target_url=arguments.target, + proxy_address=arguments.proxy_address, + window_width=arguments.window_width, + window_height=arguments.window_height, + ) diff --git a/integrations/phishing_analyzers/analyzers/seleniumwire_request_serializer.py b/integrations/phishing_analyzers/analyzers/seleniumwire_request_serializer.py new file mode 100644 index 00000000..97825006 --- /dev/null +++ b/integrations/phishing_analyzers/analyzers/seleniumwire_request_serializer.py @@ -0,0 +1,104 @@ +import base64 +from datetime import datetime +from logging import getLogger + +from seleniumwire.request import Request, Response, WebSocketMessage + +logger = getLogger(__name__) + + +def dump_seleniumwire_requests(request: Request) -> dict: + """ + Serializer for seleniumwire.request.Request + """ + logger.info(f"Starting to serialize seleniumwire request for url {request.url}") + response: Response = request.response + serialized: {} = { + "id": request.id if request.id else "", + "method": request.method, + "url": request.url, + "headers": request.headers.items(), + "body": base64.b64encode(request.body).decode("utf-8"), + "date": request.date.strftime("%Y-%m-%d, %H:%M:%S.%f"), + "ws_message": ( + [ + { + "from_client": message.from_client, + "content": base64.b64encode(message.content).decode("utf-8"), + "date": message.date.strftime("%Y-%m-%d, %H:%M:%S.%f"), + } + for message in request.ws_messages + ] + if request.ws_messages + else [] + ), + "cert": request.cert, + "response": ( + { + "status_code": response.status_code, + "reason": response.reason, + "headers": response.headers.items(), + "body": base64.b64encode(response.body).decode("utf-8"), + "date": response.date.strftime("%Y-%m-%d, %H:%M:%S.%f"), + # cert is not always available in response + "cert": response.cert if hasattr(response, "cert") else {}, + } + if response + else None + ), + } + logger.info(f"Finished serializing seleniumwire request for url {request.url}") + return serialized + + +# at the moment this method is not used. it can be used +# to decode data encoded with the previous function +def load_seleniumwire_requests(to_load: dict) -> Request: + logger.info( + f"Starting to deserialize seleniumwire request for url {to_load['url']}" + ) + response_to_load = to_load["response"] + response = ( + Response( + status_code=response_to_load["status_code"], + reason=response_to_load["reason"], + headers=response_to_load["headers"], + # body gets re-encoded into utf-8 by its setter method + body=base64.b64decode(response_to_load["body"]), + ) + if response_to_load + else None + ) + + request = Request( + method=to_load["method"], + url=to_load["url"], + headers=to_load["headers"], + body=base64.b64decode(to_load["body"]), + ) + request.id = to_load["id"] + request.date = datetime.strptime(to_load["date"], "%Y-%m-%d, %H:%M:%S.%f") + request.ws_messages = ( + [ + WebSocketMessage( + from_client=message["from_client"], + content=base64.b64decode(message["content"]), + date=datetime.strptime(message["date"], "%Y-%m-%d, %H:%M:%S.%f"), + ) + for message in to_load["ws_messages"] + ] + if "ws_messages" in to_load.keys() + else [] + ) + request.cert = to_load["cert"] + + if response: + response.date = datetime.strptime( + response_to_load["date"], "%Y-%m-%d, %H:%M:%S.%f" + ) + if response_to_load["cert"]: + response.cert = response_to_load["cert"] + request.response = response + + logger.info(f"Finished deserializing seleniumwire request for url {to_load['url']}") + return request diff --git a/integrations/phishing_analyzers/app.py b/integrations/phishing_analyzers/app.py new file mode 100644 index 00000000..f6f1d465 --- /dev/null +++ b/integrations/phishing_analyzers/app.py @@ -0,0 +1,39 @@ +import logging +import os +import secrets + +# web imports +from flask import Flask +from flask_executor import Executor +from flask_shell2http import Shell2HTTP + +LOG_NAME = "phishing_analyzers" + +# get flask-shell2http logger instance +logger = logging.getLogger("flask_shell2http") +# logger config +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +log_level = os.getenv("LOG_LEVEL", logging.INFO) +log_path = os.getenv("LOG_PATH", f"/var/log/threat_matrix/{LOG_NAME}") +# create new file handlers, files are created if doesn't already exists +fh = logging.FileHandler(f"{log_path}/{LOG_NAME}.log") +fh.setFormatter(formatter) +fh.setLevel(log_level) +fh_err = logging.FileHandler(f"{log_path}/{LOG_NAME}_errors.log") +fh_err.setFormatter(formatter) +fh_err.setLevel(logging.ERROR) +# add the handlers to the logger +logger.addHandler(fh) +logger.addHandler(fh_err) +logger.setLevel(log_level) + +app = Flask(__name__) +app.config["SECRET_KEY"] = secrets.token_hex(16) +executor = Executor(app) +shell2http = Shell2HTTP(app, executor) + +shell2http.register_command( + endpoint="phishing_extractor", + command_name="/usr/local/bin/python3 " + "/opt/deploy/phishing_analyzers/analyzers/extract_phishing_site.py", +) diff --git a/integrations/phishing_analyzers/compose-tests.yml b/integrations/phishing_analyzers/compose-tests.yml new file mode 100644 index 00000000..396ee3b5 --- /dev/null +++ b/integrations/phishing_analyzers/compose-tests.yml @@ -0,0 +1,8 @@ +# All additional integrations should be added following this format only. + +services: + phishing_analyzers: + build: + context: ../integrations/phishing_analyzers + dockerfile: Dockerfile + image: khulnasoft/threatmatrix_phishing_analyzers:test diff --git a/integrations/phishing_analyzers/compose.yml b/integrations/phishing_analyzers/compose.yml new file mode 100644 index 00000000..52894ec0 --- /dev/null +++ b/integrations/phishing_analyzers/compose.yml @@ -0,0 +1,39 @@ +# All additional integrations should be added following this format only. + +services: + phishing_analyzers: + image: khulnasoft/threatmatrix_phishing_analyzers:${REACT_APP_THREATMATRIX_VERSION} + container_name: threatmatrix_phishing_analyzers + restart: unless-stopped + expose: + - "4005" + - "7007" # selenium-wire proxy + env_file: + - env_file_integrations + volumes: + - generic_logs:/var/log/threat_matrix + depends_on: + - uwsgi + + chrome-webdriver: + # tagging convention for chrome webdriver + # https://github.com/SeleniumHQ/docker-selenium/wiki/Tagging-Convention + image: selenium/node-chrome:130.0.6723.91-chromedriver-130.0.6723.91-grid-4.26.0-20241101 + shm_size: 2gb # https://github.com/SeleniumHQ/docker-selenium?tab=readme-ov-file#--shm-size2g + depends_on: + - selenium-hub + environment: + - SE_ENABLE_TRACING=false + - SE_EVENT_BUS_HOST=selenium-hub + - SE_EVENT_BUS_PUBLISH_PORT=4442 + - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 + + selenium-hub: + image: selenium/hub:4.26.0 + container_name: selenium-hub + environment: + - SE_ENABLE_TRACING=false + ports: + - "4442:4442" + - "4443:4443" + - "4444:4444" \ No newline at end of file diff --git a/integrations/phishing_analyzers/entrypoint.sh b/integrations/phishing_analyzers/entrypoint.sh new file mode 100644 index 00000000..1473b9ea --- /dev/null +++ b/integrations/phishing_analyzers/entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/sh +/usr/bin/sudo /usr/bin/mkdir -p /var/log/threat_matrix/phishing_analyzers +/usr/bin/sudo /usr/bin/touch /var/log/threat_matrix/phishing_analyzers/gunicorn_access.log \ + /var/log/threat_matrix/phishing_analyzers/gunicorn_errors.log +/usr/bin/sudo /usr/bin/chown -R phishing-user:phishing-user \ + /opt/deploy/phishing_analyzers /var/log/threat_matrix/phishing_analyzers + +/usr/local/bin/gunicorn 'app:app' \ + --bind '0.0.0.0:4005' \ + --log-level ${LOG_LEVEL} \ + --user phishing-user \ + --group phishing-user \ + --access-logfile /var/log/threat_matrix/phishing_analyzers/gunicorn_access.log \ + --error-logfile /var/log/threat_matrix/phishing_analyzers/gunicorn_errors.log \ No newline at end of file diff --git a/integrations/phishing_analyzers/requirements.txt b/integrations/phishing_analyzers/requirements.txt new file mode 100644 index 00000000..6e7b9bfe --- /dev/null +++ b/integrations/phishing_analyzers/requirements.txt @@ -0,0 +1,5 @@ +Flask-Shell2HTTP-fork==1.9.2 +gunicorn==23.0.0 +selenium==4.25.0 +selenium-wire==5.1.0 +blinker==1.7.0 # selenium-wire depends on this library version <1.8 \ No newline at end of file diff --git a/integrations/phoneinfoga/compose.yml b/integrations/phoneinfoga/compose.yml index c82e9481..15e9eda6 100644 --- a/integrations/phoneinfoga/compose.yml +++ b/integrations/phoneinfoga/compose.yml @@ -11,4 +11,4 @@ services: env_file: - env_file_integrations depends_on: - - uwsgi + - uwsgi \ No newline at end of file diff --git a/integrations/tor_analyzers/compose-tests.yml b/integrations/tor_analyzers/compose-tests.yml index 3caa1c69..32684775 100644 --- a/integrations/tor_analyzers/compose-tests.yml +++ b/integrations/tor_analyzers/compose-tests.yml @@ -1,3 +1,5 @@ +# All additional integrations should be added following this format only. + services: tor_analyzers: build: diff --git a/integrations/tor_analyzers/compose.yml b/integrations/tor_analyzers/compose.yml index 9adaccaa..f55a14bc 100644 --- a/integrations/tor_analyzers/compose.yml +++ b/integrations/tor_analyzers/compose.yml @@ -1,3 +1,5 @@ +# All additional integrations should be added following this format only. + services: tor_analyzers: image: khulnasoft/threatmatrix_tor_analyzers:${REACT_APP_THREATMATRIX_VERSION} diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index f6a13d56..1c8f722c 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -100,4 +100,4 @@ pyzipper==0.3.6 dateparser==1.2.0 # phishing form compiler module lxml==5.3.0 -Faker==30.8.0 +Faker==30.8.0 \ No newline at end of file diff --git a/start b/start index c185feb7..87454e06 100755 --- a/start +++ b/start @@ -99,7 +99,7 @@ check_parameters "$@" && shift 2 load_env "docker/.env" current_version=${REACT_APP_THREATMATRIX_VERSION/"v"/""} -docker_analyzers=("pcap_analyzers" "tor_analyzers" "malware_tools_analyzers" "cyberchef" "phoneinfoga") +docker_analyzers=("pcap_analyzers" "tor_analyzers" "malware_tools_analyzers" "cyberchef" "phoneinfoga" "phishing_analyzers") for value in "${docker_analyzers[@]}"; do @@ -163,6 +163,10 @@ while [[ $# -gt 0 ]]; do analyzers["phoneinfoga"]=true shift 1 ;; + --phishing_analyzers) + analyzers["phishing_analyzers"]=true + shift 1 + ;; --multi_queue) params["multi_queue"]=true shift 1 diff --git a/tests/__init__.py b/tests/__init__.py index a77da108..77c23d15 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,11 +6,12 @@ from django.contrib.auth import get_user_model from django.core.files import File from django.db import connections +from django.db.models import Model from django.test import TestCase from rest_framework.test import APIClient from api_app.analyzers_manager.models import AnalyzerConfig -from api_app.models import AbstractConfig, AbstractReport, Job +from api_app.models import AbstractReport, Job User = get_user_model() @@ -64,21 +65,21 @@ def setUpTestData(cls): cls.guest = User.objects.get(is_superuser=False, username="guest") except User.DoesNotExist: cls.guest = User.objects.create( - username="guest", email="guest@threatmatrix.com", password="test" + username="guest", email="guest@khulnasoft.com", password="test" ) try: cls.user = User.objects.get(is_superuser=False, username="user") except User.DoesNotExist: cls.user = User.objects.create( - username="user", email="user@threatmatrix.com", password="test" + username="user", email="user@khulnasoft.com", password="test" ) try: cls.admin = User.objects.get(is_superuser=False, username="admin") except User.DoesNotExist: cls.admin = User.objects.create( - username="admin", email="admin@threatmatrix.com", password="test" + username="admin", email="admin@khulnasoft.com", password="test" ) try: @@ -88,7 +89,7 @@ def setUpTestData(cls): except User.DoesNotExist: cls.superuser = User.objects.create_superuser( username="superuser@threatmatrix.org", - email="test@threatmatrix.com", + email="test@khulnasoft.com", password="test", ) @@ -118,18 +119,22 @@ def init_report(self, status, user): raise NotImplementedError() def test_kill_204(self): - _report = self.init_report(status=AbstractReport.Status.PENDING, user=self.user) + _report = self.init_report( + status=AbstractReport.STATUSES.PENDING, user=self.user + ) response = self.client.patch( f"/api/jobs/{_report.job_id}/{self.plugin_type}/{_report.pk}/kill" ) _report.refresh_from_db() self.assertEqual(response.status_code, 204) - self.assertEqual(_report.status, AbstractReport.Status.KILLED) + self.assertEqual(_report.status, AbstractReport.STATUSES.KILLED) def test_kill_400(self): # create a new report whose status is not "running"/"pending" - _report = self.init_report(status=AbstractReport.Status.SUCCESS, user=self.user) + _report = self.init_report( + status=AbstractReport.STATUSES.SUCCESS, user=self.user + ) response = self.client.patch( f"/api/jobs/{_report.job_id}/{self.plugin_type}/{_report.pk}/kill" ) @@ -145,7 +150,7 @@ def test_kill_400(self): def test_kill_403(self): # create a new report which does not belong to user - _report = self.init_report(status=AbstractReport.Status.PENDING, user=None) + _report = self.init_report(status=AbstractReport.STATUSES.PENDING, user=None) response = self.client.patch( f"/api/jobs/{_report.job_id}/{self.plugin_type}/{_report.pk}/kill" ) @@ -162,7 +167,7 @@ def test_retry_204(self): # create new report with status "FAILED" _report = self.init_report( - status=AbstractReport.Status.FAILED, user=self.superuser + status=AbstractReport.STATUSES.FAILED, user=self.superuser ) self.client.force_authenticate(self.superuser) pcs = [] @@ -191,7 +196,9 @@ def test_retry_204(self): def test_retry_400(self): # create a new report whose status is not "FAILED"/"KILLED" - _report = self.init_report(status=AbstractReport.Status.SUCCESS, user=self.user) + _report = self.init_report( + status=AbstractReport.STATUSES.SUCCESS, user=self.user + ) response = self.client.patch( f"/api/jobs/{_report.job_id}/{self.plugin_type}/{_report.pk}/retry" ) @@ -207,7 +214,7 @@ def test_retry_400(self): def test_retry_403(self): # create a new report which does not belong to user - _report = self.init_report(status=AbstractReport.Status.FAILED, user=None) + _report = self.init_report(status=AbstractReport.STATUSES.FAILED, user=None) response = self.client.patch( f"/api/jobs/{_report.job_id}/{self.plugin_type}/{_report.pk}/retry" ) @@ -224,7 +231,7 @@ class ViewSetTestCaseMixin: @classmethod @property @abstractmethod - def model_class(cls) -> Type[AbstractConfig]: + def model_class(cls) -> Type[Model]: raise NotImplementedError() def test_list(self): @@ -243,15 +250,20 @@ def test_list(self): response = self.client.get(self.URL) self.assertEqual(response.status_code, 200, response.json()) - def test_get(self): + def test_get_user(self): plugin = self.get_object() response = self.client.get(f"{self.URL}/{plugin}") self.assertEqual(response.status_code, 200, response.json()) + def test_get_guest(self): + self.client.force_authenticate(self.user) + plugin = self.get_object() self.client.force_authenticate(None) response = self.client.get(f"{self.URL}/{plugin}") self.assertEqual(response.status_code, 401, response.json()) + def test_get_superuser(self): + plugin = self.get_object() self.client.force_authenticate(self.superuser) response = self.client.get(f"{self.URL}/{plugin}") self.assertEqual(response.status_code, 200, response.json()) diff --git a/tests/api_app/analyzers_manager/observable_analyzers/test_nvd_cve.py b/tests/api_app/analyzers_manager/observable_analyzers/test_nvd_cve.py index c159610f..061d017d 100644 --- a/tests/api_app/analyzers_manager/observable_analyzers/test_nvd_cve.py +++ b/tests/api_app/analyzers_manager/observable_analyzers/test_nvd_cve.py @@ -1,14 +1,104 @@ +from unittest.mock import patch + from django.test import TestCase from api_app.analyzers_manager.classes import AnalyzerRunException from api_app.analyzers_manager.models import AnalyzerConfig from api_app.analyzers_manager.observable_analyzers.nvd_cve import NVDDetails +from tests.mock_utils import MockUpResponse, if_mock_connections +@if_mock_connections( + patch( + "requests.get", + return_value=MockUpResponse( + { + "resultsPerPage": 1, + "startIndex": 0, + "totalResults": 1, + "format": "NVD_CVE", + "version": "2.0", + "timestamp": "2024-11-01T05:25:09.787", + "vulnerabilities": [ + { + "cve": { + "id": "CVE-2024-51181", + "sourceIdentifier": "cve@mitre.org", + "published": "2024-10-29T13:15:07.297", + "lastModified": "2024-10-29T20:35:37.490", + "vulnStatus": "Undergoing Analysis", + "cveTags": [], + "descriptions": [ + { + "lang": "en", + "value": "A Reflected Cross Site Scripting (XSS) vulnerability was found" + "in /ifscfinder/admin/profile.php in PHPGurukul IFSC Code Finder" + "Project v1.0, which allows remote attackers to execute arbitrary" + 'code via " searchifsccode" parameter.', + }, + { + "lang": "es", + "value": " Se encontró una vulnerabilidad de Cross Site Scripting reflejado" + "(XSS) en /ifscfinder/admin/profile.php en PHPGurukul IFSC Code Finder" + "Project v1.0, que permite a atacantes remotos ejecutar código arbitrario" + 'a través del parámetro "searchifsccode".', + }, + ], + "metrics": { + "cvssMetricV31": [ + { + "source": "134c704f-9b21-4f2e-91b3-4a467353bcc0", + "type": "Secondary", + "cvssData": { + "version": "3.1", + "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:L", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "REQUIRED", + "scope": "CHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "LOW", + "availabilityImpact": "LOW", + "baseScore": 8.8, + "baseSeverity": "HIGH", + }, + "exploitabilityScore": 2.8, + "impactScore": 5.3, + } + ] + }, + "weaknesses": [ + { + "source": "134c704f-9b21-4f2e-91b3-4a467353bcc0", + "type": "Secondary", + "description": [ + { + "lang": "en", + "value": "CWE-79", + } + ], + } + ], + "references": [ + { + "url": "https://github.com/Santoshcyber1/CVE-wirteup/blob/main/" + "Phpgurukul/IFSC%20Code%20Finder/IFSC%20Code%20Finder%20Admin.pdf", + "source": "cve@mitre.org", + } + ], + } + } + ], + }, + 200, + ), + ) +) class NVDCVETestCase(TestCase): config = AnalyzerConfig.objects.get(python_module=NVDDetails.python_module) - def test_valid_cve_format(self): + def test_valid_cve_format(self, *args, **kwargs): """Test that a valid CVE format passes without raising an exception""" analyzer = NVDDetails(self.config) @@ -19,7 +109,7 @@ def test_valid_cve_format(self): except AnalyzerRunException: self.fail("AnalyzerRunException raised with valid CVE format") - def test_invalid_cve_format(self): + def test_invalid_cve_format(self, *args, **kwargs): """Test that an invalid CVE format raises an AnalyzerRunException""" analyzer = NVDDetails(self.config) analyzer.observable_name = "2024-51181" # Invalid format diff --git a/tests/api_app/analyzers_manager/test_classes.py b/tests/api_app/analyzers_manager/test_classes.py index 3fa99142..155afbf0 100644 --- a/tests/api_app/analyzers_manager/test_classes.py +++ b/tests/api_app/analyzers_manager/test_classes.py @@ -6,6 +6,7 @@ from kombu import uuid from api_app.analyzers_manager.classes import FileAnalyzer, ObservableAnalyzer +from api_app.analyzers_manager.constants import ObservableTypes from api_app.analyzers_manager.models import AnalyzerConfig, MimeTypes from api_app.models import Job, PluginConfig from tests import CustomTestCase @@ -215,7 +216,7 @@ def _create_jobs(self): ) Job.objects.create( user=self.superuser, - observable_name="https://www.honeynet.org/projects/active/threat-matrix/", + observable_name="https://www.honeynet.org/projects/active/intel-owl/", observable_classification="url", status="reported_without_fails", ) @@ -228,7 +229,13 @@ def _create_jobs(self): ) Job.objects.create( user=self.superuser, - observable_name="test@threatmatrix.com", + observable_name="test@khulnasoft.com", + observable_classification="generic", + status="reported_without_fails", + ), + Job.objects.create( + user=self.superuser, + observable_name="CVE-2024-51181", observable_classification="generic", status="reported_without_fails", ) @@ -255,9 +262,20 @@ def handler(signum, frame): f"Testing datatype {observable_supported}" f" for {timeout_seconds} seconds" ) - job = Job.objects.get( - observable_classification=observable_supported - ) + if observable_supported == ObservableTypes.GENERIC.value: + # generic should handle different use cases + job = Job.objects.get( + observable_classification=ObservableTypes.GENERIC.value, + observable_name=( + "CVE-2024-51181" + if config.name == "NVD_CVE" + else "test@khulnasoft.com" + ), + ) + else: + job = Job.objects.get( + observable_classification=observable_supported + ) job.analyzers_to_execute.set([config]) sub = subclass( config, diff --git a/tests/api_app/analyzers_manager/test_models.py b/tests/api_app/analyzers_manager/test_models.py index 1f55ed1a..b03f8b2c 100644 --- a/tests/api_app/analyzers_manager/test_models.py +++ b/tests/api_app/analyzers_manager/test_models.py @@ -3,14 +3,151 @@ from django.core.exceptions import ValidationError +from kombu import uuid -from api_app.analyzers_manager.models import AnalyzerConfig +from api_app.analyzers_manager.models import AnalyzerConfig, AnalyzerReport from api_app.choices import PythonModuleBasePaths -from api_app.models import PythonModule +from api_app.data_model_manager.models import DomainDataModel, IPDataModel +from api_app.models import Job, PythonModule from tests import CustomTestCase +class AnalyzerReportTestCase(CustomTestCase): + + def test_get_data_models(self): + job = Job.objects.create( + observable_name="test.com", + observable_classification="domain", + status=Job.STATUSES.ANALYZERS_RUNNING.value, + ) + config = AnalyzerConfig.objects.first() + domain_data_model = DomainDataModel.objects.create() + ar: AnalyzerReport = AnalyzerReport.objects.create( + report={ + "evaluation": "MALICIOUS", + "urls": [ + {"url": "www.threatmatrix.khulnasoft.com"}, + {"url": "www.threatmatrix.khulnasoft.com"}, + ], + }, + job=job, + config=config, + status=AnalyzerReport.STATUSES.SUCCESS.value, + task_id=str(uuid()), + parameters={}, + data_model=domain_data_model, + ) + dm = AnalyzerReport.objects.filter(pk=ar.pk).get_data_models(job) + self.assertEqual(dm.model, DomainDataModel) + + def test_clean(self): + job = Job.objects.create( + observable_name="test.com", + observable_classification="domain", + status=Job.STATUSES.ANALYZERS_RUNNING.value, + ) + config = AnalyzerConfig.objects.first() + ar: AnalyzerReport = AnalyzerReport.objects.create( + report={ + "evaluation": "MALICIOUS", + "urls": [ + {"url": "www.threatmatrix.khulnasoft.com"}, + {"url": "www.threatmatrix.khulnasoft.com"}, + ], + }, + job=job, + config=config, + status=AnalyzerReport.STATUSES.SUCCESS.value, + task_id=str(uuid()), + parameters={}, + ) + ip_data_model = IPDataModel.objects.create() + ar.data_model = ip_data_model + with self.assertRaises(ValidationError): + ar.full_clean() + ip_data_model.delete() + domain_data_model = DomainDataModel.objects.create() + ar.data_model = domain_data_model + ar.full_clean() + ar.delete() + job.delete() + domain_data_model.delete() + + def test_create_data_model(self): + job = Job.objects.create( + observable_name="test.com", + observable_classification="domain", + status=Job.STATUSES.ANALYZERS_RUNNING.value, + ) + config = AnalyzerConfig.objects.first() + ar: AnalyzerReport = AnalyzerReport.objects.create( + report={ + "evaluation": "MALICIOUS", + "urls": [ + {"url": "www.threatmatrix.khulnasoft.com"}, + {"url": "www.threatmatrix.khulnasoft.com"}, + ], + }, + job=job, + config=config, + status=AnalyzerReport.STATUSES.SUCCESS.value, + task_id=str(uuid()), + parameters={}, + ) + config: AnalyzerConfig + config.mapping_data_model = { + "evaluation": "evaluation", + "urls.url": "external_references", + } + config.save() + job.analyzers_to_execute.set([config]) + data_model = ar.create_data_model() + data_model.refresh_from_db() + self.assertIsNotNone(data_model) + self.assertEqual(data_model.evaluation, "malicious") + self.assertCountEqual( + data_model.external_references, + ["www.threatmatrix.khulnasoft.com", "www.threatmatrix.khulnasoft.com"], + ) + self.assertCountEqual([], ar.errors) + data_model.delete() + ar.delete() + job.delete() + + def test_get_value(self): + job = Job.objects.create( + observable_name="test.com", + observable_classification="domain", + status=Job.STATUSES.ANALYZERS_RUNNING.value, + ) + config = AnalyzerConfig.objects.first() + ar = AnalyzerReport.objects.create( + report={ + "evaluation": "MALICIOUS", + "urls": [ + {"url": "www.threatmatrix.khulnasoft.com"}, + {"url": "www.threatmatrix.khulnasoft.com"}, + ], + }, + job=job, + config=config, + status=AnalyzerReport.STATUSES.SUCCESS.value, + task_id=str(uuid()), + parameters={}, + ) + self.assertEqual(ar.get_value(ar.report, ["evaluation"]), "MALICIOUS") + self.assertEqual( + ar.get_value(ar.report, "urls.0.url".split(".")), + "www.threatmatrix.khulnasoft.com", + ) + self.assertCountEqual( + ar.get_value(ar.report, "urls.url".split(".")), + ["www.threatmatrix.khulnasoft.com", "www.threatmatrix.khulnasoft.com"], + ) + + class AnalyzerConfigTestCase(CustomTestCase): + def test_clean_run_hash_type(self): ac = AnalyzerConfig( name="test", diff --git a/tests/api_app/analyzers_manager/test_views.py b/tests/api_app/analyzers_manager/test_views.py index c4717060..b2340897 100644 --- a/tests/api_app/analyzers_manager/test_views.py +++ b/tests/api_app/analyzers_manager/test_views.py @@ -238,7 +238,7 @@ def init_report(self, status: str, user) -> AnalyzerReport: config = AnalyzerConfig.objects.get(name="HaveIBeenPwned") _job = Job.objects.create( user=user, - status=Job.Status.RUNNING, + status=Job.STATUSES.RUNNING, observable_name="8.8.8.8", observable_classification=ObservableTypes.IP, ) diff --git a/tests/api_app/connectors_manager/test_classes.py b/tests/api_app/connectors_manager/test_classes.py index 0d9676fc..85ecf6df 100644 --- a/tests/api_app/connectors_manager/test_classes.py +++ b/tests/api_app/connectors_manager/test_classes.py @@ -38,7 +38,7 @@ def run(self) -> dict: with self.assertRaises(NotImplementedError): MockUpConnector(cc).health_check(self.user) pc = PluginConfig.objects.create( - value="https://threatmatrix.com", + value="https://threatmatrix.khulnasoft.com", owner=self.user, parameter=Parameter.objects.get(name="url_key_name", python_module=pm), connector_config=cc, @@ -61,13 +61,13 @@ def run(self) -> dict: job = Job.objects.create( observable_name="test.com", observable_classification="domain", - status=Job.Status.CONNECTORS_RUNNING.value, + status=Job.STATUSES.CONNECTORS_RUNNING.value, ) AnalyzerReport.objects.create( report={}, job=job, config=AnalyzerConfig.objects.first(), - status=AnalyzerReport.Status.FAILED.value, + status=AnalyzerReport.STATUSES.FAILED.value, task_id=str(uuid()), parameters={}, ) diff --git a/tests/api_app/connectors_manager/test_views.py b/tests/api_app/connectors_manager/test_views.py index 256b6eab..cd37493b 100644 --- a/tests/api_app/connectors_manager/test_views.py +++ b/tests/api_app/connectors_manager/test_views.py @@ -67,7 +67,7 @@ def tearDown(self) -> None: def init_report(self, status: str, user) -> ConnectorReport: _job = Job.objects.create( user=user, - status=Job.Status.REPORTED_WITHOUT_FAILS, + status=Job.STATUSES.REPORTED_WITHOUT_FAILS, observable_name="8.8.8.8", observable_classification=ObservableTypes.IP, ) diff --git a/tests/api_app/data_model_manager/__init__.py b/tests/api_app/data_model_manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/api_app/data_model_manager/test_models.py b/tests/api_app/data_model_manager/test_models.py new file mode 100644 index 00000000..41a4252d --- /dev/null +++ b/tests/api_app/data_model_manager/test_models.py @@ -0,0 +1,10 @@ +from api_app.data_model_manager.models import IPDataModel +from tests import CustomTestCase + + +class BaseDataModelTestCase(CustomTestCase): + + def test_serialize(self): + ip = IPDataModel.objects.create() + results = IPDataModel.objects.filter(pk=ip.pk).serialize() + self.assertEqual(1, len(results)) diff --git a/tests/api_app/data_model_manager/test_serializers.py b/tests/api_app/data_model_manager/test_serializers.py new file mode 100644 index 00000000..832756ae --- /dev/null +++ b/tests/api_app/data_model_manager/test_serializers.py @@ -0,0 +1,44 @@ +from kombu import uuid + +from api_app.analyzers_manager.models import AnalyzerConfig, AnalyzerReport +from api_app.data_model_manager.models import DomainDataModel +from api_app.data_model_manager.serializers import DomainDataModelSerializer +from api_app.models import Job +from tests import CustomTestCase + + +class TestDomainDataModelSerializer(CustomTestCase): + + def test_to_representation(self): + job = Job.objects.create( + observable_name="test.com", + observable_classification="domain", + status=Job.STATUSES.ANALYZERS_RUNNING.value, + ) + config = AnalyzerConfig.objects.first() + dm = DomainDataModel.objects.create(evaluation="malicious") + ar: AnalyzerReport = AnalyzerReport.objects.create( + report={ + "evaluation": "MALICIOUS", + "urls": [ + {"url": "www.threatmatrix.khulnasoft.com"}, + {"url": "www.threatmatrix.khulnasoft.com"}, + ], + }, + job=job, + config=config, + status=AnalyzerReport.STATUSES.SUCCESS.value, + task_id=str(uuid()), + parameters={}, + ) + + ar.data_model = dm + ar.save() + dm.refresh_from_db() + + ser = DomainDataModelSerializer(dm) + result = ser.data + print(result) + + dm.delete() + ar.delete() diff --git a/tests/api_app/data_model_manager/test_views.py b/tests/api_app/data_model_manager/test_views.py new file mode 100644 index 00000000..7c3a2eeb --- /dev/null +++ b/tests/api_app/data_model_manager/test_views.py @@ -0,0 +1,114 @@ +from typing import Type + +from django.db.models import Model +from kombu import uuid + +from api_app.analyzers_manager.models import AnalyzerConfig, AnalyzerReport +from api_app.data_model_manager.models import ( + DomainDataModel, + FileDataModel, + IPDataModel, +) +from api_app.models import Job +from tests import CustomViewSetTestCase, ViewSetTestCaseMixin + + +def create_report(user): + job = Job.objects.create( + observable_name="test.com", + observable_classification="domain", + status=Job.STATUSES.CONNECTORS_RUNNING.value, + user=user, + ) + return AnalyzerReport.objects.create( + report={}, + job=job, + config=AnalyzerConfig.objects.first(), + status=AnalyzerReport.STATUSES.FAILED.value, + task_id=str(uuid()), + parameters={}, + ) + + +class DomainDataModelViewSetTestCase(ViewSetTestCaseMixin, CustomViewSetTestCase): + URL = "/api/data_model/domain" + + def test_url(self): + response = self.client.get(self.URL) + self.assertEqual(response.status_code, 200, response.content) + try: + response.json() + except Exception as e: + self.fail(e) + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + report = create_report(cls.user) + report.data_model = cls.model_class.objects.create() + report.save() + + @classmethod + @property + def model_class(cls) -> Type[Model]: + return DomainDataModel + + def get_object(self): + return self.model_class.objects.order_by("?").first().pk + + def test_get_superuser(self): + plugin = self.get_object() + self.assertIsNotNone(plugin) + self.client.force_authenticate(self.superuser) + response = self.client.get(f"{self.URL}/{plugin}") + self.assertEqual(response.status_code, 403, response.json()) + + +class IPDataModelViewSetTestCase(ViewSetTestCaseMixin, CustomViewSetTestCase): + URL = "/api/data_model/ip" + + @classmethod + @property + def model_class(cls) -> Type[Model]: + return IPDataModel + + def get_object(self): + return self.model_class.objects.order_by("?").first().pk + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + report = create_report(cls.user) + report.data_model = cls.model_class.objects.create() + report.save() + + def test_get_superuser(self): + plugin = self.get_object() + self.client.force_authenticate(self.superuser) + response = self.client.get(f"{self.URL}/{plugin}") + self.assertEqual(response.status_code, 403, response.json()) + + +class FileDataModelViewSetTestCase(ViewSetTestCaseMixin, CustomViewSetTestCase): + URL = "/api/data_model/file" + + @classmethod + @property + def model_class(cls) -> Type[Model]: + return FileDataModel + + def get_object(self): + return self.model_class.objects.order_by("?").first().pk + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + report = create_report(cls.user) + report.data_model = cls.model_class.objects.create() + report.save() + + def test_get_superuser(self): + plugin = self.get_object() + self.client.force_authenticate(self.superuser) + response = self.client.get(f"{self.URL}/{plugin}") + self.assertEqual(response.status_code, 403, response.json()) diff --git a/tests/api_app/investigations_manager/test_models.py b/tests/api_app/investigations_manager/test_models.py index 2e142c7f..5df3d54d 100644 --- a/tests/api_app/investigations_manager/test_models.py +++ b/tests/api_app/investigations_manager/test_models.py @@ -17,7 +17,7 @@ def test_set_correct_status_running(self): observable_name="test.com", observable_classification="domain", user=self.user, - status=Job.Status.REPORTED_WITH_FAILS, + status=Job.STATUSES.REPORTED_WITH_FAILS, ) an: Investigation = Investigation.objects.create(name="Test", owner=self.user) an.jobs.add(job) @@ -28,7 +28,7 @@ def test_set_correct_status_running(self): observable_name="test.com", observable_classification="domain", user=self.user, - status=Job.Status.PENDING, + status=Job.STATUSES.PENDING, ) an.refresh_from_db() an.set_correct_status() diff --git a/tests/api_app/investigations_manager/test_views.py b/tests/api_app/investigations_manager/test_views.py index 4dede491..c5eac742 100644 --- a/tests/api_app/investigations_manager/test_views.py +++ b/tests/api_app/investigations_manager/test_views.py @@ -152,3 +152,9 @@ def test_remove_job(self): job.delete() job2.delete() + + def test_get_superuser(self): + plugin = self.get_object() + self.client.force_authenticate(self.superuser) + response = self.client.get(f"{self.URL}/{plugin}") + self.assertEqual(response.status_code, 403, response.json()) diff --git a/tests/api_app/pivots_manager/test_views.py b/tests/api_app/pivots_manager/test_views.py index 853e5f6d..b783c050 100644 --- a/tests/api_app/pivots_manager/test_views.py +++ b/tests/api_app/pivots_manager/test_views.py @@ -22,6 +22,8 @@ def get_object(self): return self.model_class.objects.order_by("?").first().pk def test_get(self): + self.client.force_authenticate(self.superuser) + plugin = self.model_class.objects.order_by("?").first().pk response = self.client.get(f"{self.URL}/{plugin}") self.assertEqual(response.status_code, 403, response.json()) @@ -30,20 +32,20 @@ def test_get(self): response = self.client.get(f"{self.URL}/{plugin}") self.assertEqual(response.status_code, 401, response.json()) - self.client.force_authenticate(self.superuser) + self.client.force_authenticate(self.user) response = self.client.get(f"{self.URL}/{plugin}") self.assertEqual(response.status_code, 200, response.json()) def setUp(self): super().setUp() self.j1 = Job.objects.create( - user=self.superuser, + user=self.user, observable_name="test.com", observable_classification="domain", status="reported_without_fails", ) self.j2 = Job.objects.create( - user=self.superuser, + user=self.user, observable_name="test2.com", observable_classification="domain", status="reported_without_fails", @@ -66,6 +68,12 @@ def tearDown(self) -> None: self.pc.delete() PivotMap.objects.all().delete() + def test_get_superuser(self): + plugin = self.get_object() + self.client.force_authenticate(self.superuser) + response = self.client.get(f"{self.URL}/{plugin}") + self.assertEqual(response.status_code, 403, response.json()) + class PivotConfigViewSetTestCase( AbstractConfigViewSetTestCaseMixin, CustomViewSetTestCase diff --git a/tests/api_app/test_classes.py b/tests/api_app/test_classes.py index 9448299f..13c149cd 100644 --- a/tests/api_app/test_classes.py +++ b/tests/api_app/test_classes.py @@ -17,7 +17,7 @@ class PluginTestCase(CustomTestCase): def setUp(self) -> None: super().setUp() self.job, _ = Job.objects.get_or_create( - user=self.user, status=Job.Status.REPORTED_WITHOUT_FAILS + user=self.user, status=Job.STATUSES.REPORTED_WITHOUT_FAILS ) self.cc, _ = ConnectorConfig.objects.get_or_create( name="test", @@ -50,7 +50,7 @@ def test_start_no_errors(self): except Exception as e: self.fail(e) else: - self.assertEqual(plugin.report.status, plugin.report.Status.SUCCESS) + self.assertEqual(plugin.report.status, plugin.report.STATUSES.SUCCESS) def test_start_errors(self): def raise_error(self): @@ -62,7 +62,7 @@ def raise_error(self): plugin = Connector(self.cc) with self.assertRaises(TypeError): plugin.start(self.job.pk, {}, uuid()) - self.assertEqual(plugin.report.status, plugin.report.Status.FAILED) + self.assertEqual(plugin.report.status, plugin.report.STATUSES.FAILED) self.assertEqual(1, len(plugin.report.errors)) self.assertEqual("Test", plugin.report.errors[0]) diff --git a/tests/api_app/test_mixins.py b/tests/api_app/test_mixins.py index 814ef6d7..6a969690 100644 --- a/tests/api_app/test_mixins.py +++ b/tests/api_app/test_mixins.py @@ -5,7 +5,6 @@ from api_app.analyzers_manager.constants import ObservableTypes from api_app.analyzers_manager.models import AnalyzerConfig -from api_app.ingestors_manager.models import IngestorConfig from api_app.mixins import VirusTotalv3AnalyzerMixin, VirusTotalv3BaseMixin from tests import CustomTestCase from tests.mock_utils import MockUpResponse @@ -92,9 +91,7 @@ def run(self) -> dict: class VirusTotalMixinTestCase(CustomTestCase): def setUp(self) -> None: - self.base = VirusTotalv3Base( - IngestorConfig.objects.get(name="VirusTotal_Example_Query") - ) + self.base = VirusTotalv3Base() self.analyzer_file = VirusTotalv3Analyzer( AnalyzerConfig.objects.get(name="VirusTotal_v3_Get_File") ) diff --git a/tests/api_app/test_models.py b/tests/api_app/test_models.py index 386bf086..4db14483 100644 --- a/tests/api_app/test_models.py +++ b/tests/api_app/test_models.py @@ -467,7 +467,7 @@ def test_pivots_to_execute(self): observable_classification="domain", user=self.user, md5="72cf478e87b031233091d8c00a38ce00", - status=Job.Status.REPORTED_WITHOUT_FAILS, + status=Job.STATUSES.REPORTED_WITHOUT_FAILS, ) pc = PivotConfig.objects.create( name="test", diff --git a/tests/api_app/test_serializers.py b/tests/api_app/test_serializers.py index a6612b97..bd1ead5a 100644 --- a/tests/api_app/test_serializers.py +++ b/tests/api_app/test_serializers.py @@ -63,7 +63,7 @@ def test_to_representation(self): type="str", ).first() pc = PluginConfig.objects.create( - value="https://threatmatrix.com", + value="https://threatmatrix.khulnasoft.com", owner=self.user, parameter=param, analyzer_config=AnalyzerConfig.objects.filter( @@ -77,7 +77,7 @@ def test_to_representation(self): self.assertEqual(org.name, data["organization"]) pc.delete() pc = PluginConfig.objects.create( - value="https://threatmatrix.com", + value="https://threatmatrix.khulnasoft.com", owner=self.user, parameter=param, analyzer_config=AnalyzerConfig.objects.filter( @@ -192,6 +192,7 @@ def test_validate(self): self.assertIn("analyzer_reports", js.data) self.assertIn("connector_reports", js.data) self.assertIn("visualizer_reports", js.data) + self.assertIn("analyzers_data_model", js.data) job.delete() @@ -212,7 +213,7 @@ def test_check_previous_job(self): observable_classification="domain", user=self.user, md5="72cf478e87b031233091d8c00a38ce00", - status=Job.Status.REPORTED_WITHOUT_FAILS, + status=Job.STATUSES.REPORTED_WITHOUT_FAILS, received_request_time=now() - datetime.timedelta(hours=3), ) j1.analyzers_requested.add(a1) diff --git a/tests/api_app/test_views.py b/tests/api_app/test_views.py index c6044673..79aca23f 100644 --- a/tests/api_app/test_views.py +++ b/tests/api_app/test_views.py @@ -2,15 +2,21 @@ # See the file 'LICENSE' for copying permission. import abc import datetime +from unittest.mock import MagicMock, patch +from zoneinfo import ZoneInfo from django.contrib.auth import get_user_model +from django.test import override_settings from django.utils.timezone import now +from elasticsearch_dsl.query import Bool, Range, Term from rest_framework.reverse import reverse from rest_framework.test import APIClient from api_app.analyzers_manager.constants import ObservableTypes from api_app.analyzers_manager.models import AnalyzerConfig +from api_app.choices import ReportStatus from api_app.models import Comment, Job, Parameter, PluginConfig, Tag +from api_app.playbooks_manager.models import PlaybookConfig from certego_saas.apps.organization.membership import Membership from certego_saas.apps.organization.organization import Organization @@ -101,7 +107,7 @@ def test_get(self): # they should not find anything self.standard_user = User.objects.create_user( username="standard_user", - email="standard_user@threatmatrix.com", + email="standard_user@khulnasoft.com", password="test", ) self.standard_user.save() @@ -178,7 +184,7 @@ def test_list(self): org1 = Organization.objects.create(name="testorg1") another_owner = User.objects.create_user( username="another_owner", - email="another_owner@threatmatrix.com", + email="another_owner@khulnasoft.com", password="test", ) another_owner.save() @@ -359,6 +365,15 @@ def test_get(self): self.assertEqual(response.status_code, 200) +@patch( + "api_app.views.parse_humanized_range", + MagicMock( + return_value=( + datetime.datetime(2024, 11, 27, 12, tzinfo=datetime.timezone.utc), + "day", + ) + ), +) class JobViewSetTests(CustomViewSetTestCase): jobs_list_uri = reverse("jobs-list") jobs_recent_scans_uri = reverse("jobs-recent-scans") @@ -369,28 +384,37 @@ class JobViewSetTests(CustomViewSetTestCase): "jobs-aggregate-observable-classification" ) agg_file_mimetype_uri = reverse("jobs-aggregate-file-mimetype") - agg_observable_name_uri = reverse("jobs-aggregate-observable-name") - agg_file_name_uri = reverse("jobs-aggregate-md5") + agg_top_playbook = reverse("jobs-aggregate-top-playbook") + agg_top_user = reverse("jobs-aggregate-top-user") + agg_top_tlp = reverse("jobs-aggregate-top-tlp") def setUp(self): super().setUp() - self.job, _ = Job.objects.get_or_create( - **{ - "user": self.superuser, - "is_sample": False, - "observable_name": "1.2.3.4", - "observable_classification": "ip", - } - ) - self.job2, _ = Job.objects.get_or_create( - **{ - "user": self.superuser, - "is_sample": True, - "md5": "test.file", - "file_name": "test.file", - "file_mimetype": "application/vnd.microsoft.portable-executable", - } - ) + with patch( + "django.utils.timezone.now", + return_value=datetime.datetime(2024, 11, 28, tzinfo=datetime.timezone.utc), + ): + self.job, _ = Job.objects.get_or_create( + **{ + "user": self.superuser, + "is_sample": False, + "observable_name": "1.2.3.4", + "observable_classification": "ip", + "playbook_to_execute": PlaybookConfig.objects.get(name="Dns"), + "tlp": Job.TLP.CLEAR.value, + } + ) + self.job2, _ = Job.objects.get_or_create( + **{ + "user": self.superuser, + "is_sample": True, + "md5": "test.file", + "file_name": "test.file", + "file_mimetype": "application/vnd.microsoft.portable-executable", + "playbook_to_execute": PlaybookConfig.objects.get(name="Dns"), + "tlp": Job.TLP.GREEN.value, + } + ) def test_recent_scan(self): j1 = Job.objects.create( @@ -430,7 +454,9 @@ def test_recent_scan_user(self): "is_sample": False, "observable_name": "gigatest.com", "observable_classification": "domain", - "finished_analysis_time": now() - datetime.timedelta(days=2), + "finished_analysis_time": datetime.datetime( + 2024, 11, 28, tzinfo=datetime.timezone.utc + ), } ) j2 = Job.objects.create( @@ -439,7 +465,9 @@ def test_recent_scan_user(self): "is_sample": False, "observable_name": "gigatest.com", "observable_classification": "domain", - "finished_analysis_time": now() - datetime.timedelta(hours=2), + "finished_analysis_time": datetime.datetime( + 2024, 11, 28, tzinfo=datetime.timezone.utc + ), } ) response = self.client.post( @@ -487,8 +515,12 @@ def test_delete(self): # @action endpoints def test_kill(self): - job = Job.objects.create(status=Job.Status.RUNNING, user=self.superuser) - self.assertEqual(job.status, Job.Status.RUNNING) + job = Job.objects.create( + status=Job.STATUSES.RUNNING, + user=self.superuser, + observable_classification="ip", + ) + self.assertEqual(job.status, Job.STATUSES.RUNNING) uri = reverse("jobs-kill", args=[job.pk]) response = self.client.patch(uri) @@ -498,12 +530,14 @@ def test_kill(self): self.assertEqual(response.status_code, 204) job.refresh_from_db() - self.assertEqual(job.status, Job.Status.KILLED) + self.assertEqual(job.status, Job.STATUSES.KILLED) def test_kill_400(self): # create a new job whose status is not "running" job = Job.objects.create( - status=Job.Status.REPORTED_WITHOUT_FAILS, user=self.superuser + status=Job.STATUSES.REPORTED_WITHOUT_FAILS, + user=self.superuser, + observable_classification="ip", ) uri = reverse("jobs-kill", args=[job.pk]) self.client.force_authenticate(user=self.job.user) @@ -516,19 +550,24 @@ def test_kill_400(self): ) # aggregation endpoints - def test_agg_status_200(self): resp = self.client.get(self.agg_status_uri) content = resp.json() msg = (resp, content) self.assertEqual(resp.status_code, 200, msg) - for field in ["date", *Job.Status.values]: - self.assertIn( - field, - content[0], - msg=msg, - ) + self.assertEqual( + content, + [ + { + "date": "2024-11-28T00:00:00Z", + "pending": 2, + "failed": 0, + "reported_with_fails": 0, + "reported_without_fails": 0, + } + ], + ) def test_agg_type_200(self): resp = self.client.get(self.agg_type_uri) @@ -570,31 +609,42 @@ def test_agg_file_mimetype_200(self): msg=msg, ) - def test_agg_observable_name_200(self): - resp = self.client.get(self.agg_observable_name_uri) - content = resp.json() - msg = (resp, content) - - self.assertEqual(resp.status_code, 200, msg) - for field in content["values"]: - self.assertIn( - field, - content["aggregation"], - msg=msg, - ) + def test_agg_top_playbook_200(self): + resp = self.client.get(self.agg_top_playbook) + self.assertEqual(resp.status_code, 200) + self.assertEqual( + resp.json(), + { + "values": ["Dns"], + "aggregation": [{"date": "2024-11-28T00:00:00Z", "Dns": 2}], + }, + ) - def test_agg_file_name_200(self): - resp = self.client.get(self.agg_file_name_uri) - content = resp.json() - msg = (resp, content) + def test_agg_top_user_200(self): + resp = self.client.get(self.agg_top_user) + self.assertEqual(resp.status_code, 200) + self.assertEqual( + resp.json(), + { + "values": ["superuser@threatmatrix.org"], + "aggregation": [ + {"date": "2024-11-28T00:00:00Z", "superuser@threatmatrix.org": 2} + ], + }, + ) - self.assertEqual(resp.status_code, 200, msg) - for field in content["values"]: - self.assertIn( - field, - content["aggregation"], - msg=msg, - ) + def test_agg_top_tlp_200(self): + resp = self.client.get(self.agg_top_tlp) + self.assertEqual(resp.status_code, 200) + self.assertEqual( + resp.json(), + { + "values": ["CLEAR", "GREEN"], + "aggregation": [ + {"date": "2024-11-28T00:00:00Z", "CLEAR": 1, "GREEN": 1} + ], + }, + ) class TagViewsetTests(CustomViewSetTestCase): @@ -824,3 +874,238 @@ def test_organization_enable(self): m.delete() org.delete() + + +class ElasticTestCase(CustomViewSetTestCase): + uri = reverse("plugin_report_queries") + + class ElasticObject: + + def __init__(self, response): + self.response = response + + def to_dict(self): + return self.response + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.org_user, _ = User.objects.get_or_create( + is_superuser=False, username="elastic_test_user" + ) + cls.org = Organization.objects.create(name="test_elastic_org") + cls.membership = Membership.objects.create( + user=cls.org_user, organization=cls.org, is_owner=True + ) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + cls.membership.delete() + cls.org.delete() + cls.org_user.delete() + + def test_not_authenticated(self): + self.client.logout() + response = self.client.get(self.uri) + self.assertEqual(response.status_code, 401) + + @override_settings(ELASTICSEARCH_DSL_ENABLED=True) + @patch( + "api_app.views.Search.execute", + MagicMock( + return_value=( + [ + ElasticObject( + { + "user": {"username": "elastic_test_user"}, + "membership": { + "is_owner": True, + "is_admin": False, + "organization": {"name": "test_elastic_org"}, + }, + "job": {"id": 1}, + "config": { + "name": "Quad9_DNS", + "plugin_name": "analyzer", + }, + "status": "SUCCESS", + "start_time": "2024-11-27T09:56:59.555203Z", + "end_time": "2024-11-27T09:57:03.805453Z", + "errors": [], + "report": { + "observable": "google.com", + "resolutions": [ + { + "TTL": 268, + "data": "216.58.205.46", + "name": "google.com.", + "type": 1, + "Expires": "Wed, 27 Nov 2024 10:01:31 UTC", + }, + ], + }, + } + ), + ElasticObject( + { + "user": {"username": "another_user"}, + "membership": { + "is_owner": False, + "is_admin": False, + "organization": {"name": "test_elastic_org"}, + }, + "job": {"id": 2}, + "config": { + "name": "Classic_DNS", + "plugin_name": "analyzer", + }, + "status": "SUCCESS", + "start_time": "2024-11-26T09:56:59.555203Z", + "end_time": "2024-11-26T09:57:03.805453Z", + "errors": [], + "report": { + "observable": "google.com", + "resolutions": [ + { + "TTL": 268, + "data": "216.58.205.46", + "name": "google.com.", + "type": 1, + "Expires": "Wed, 26 Nov 2024 10:01:31 UTC", + }, + ], + }, + } + ), + ] + ) + ), + ) + def test_client_request(self): + self.client.force_authenticate(self.org_user) + response = self.client.get( + self.uri, + data={ + "report": "216.58.205.46", + }, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "data": [ + { + "job": {"id": 1}, + "config": { + "name": "Quad9_DNS", + "plugin_name": "analyzer", + }, + "status": "SUCCESS", + "start_time": "2024-11-27T09:56:59.555203Z", + "end_time": "2024-11-27T09:57:03.805453Z", + "errors": [], + "report": { + "observable": "google.com", + "resolutions": [ + { + "TTL": 268, + "data": "216.58.205.46", + "name": "google.com.", + "type": 1, + "Expires": "Wed, 27 Nov 2024 10:01:31 UTC", + }, + ], + }, + }, + { + "job": {"id": 2}, + "config": { + "name": "Classic_DNS", + "plugin_name": "analyzer", + }, + "status": "SUCCESS", + "start_time": "2024-11-26T09:56:59.555203Z", + "end_time": "2024-11-26T09:57:03.805453Z", + "errors": [], + "report": { + "observable": "google.com", + "resolutions": [ + { + "TTL": 268, + "data": "216.58.205.46", + "name": "google.com.", + "type": 1, + "Expires": "Wed, 26 Nov 2024 10:01:31 UTC", + }, + ], + }, + }, + ] + }, + ) + + @override_settings(ELASTICSEARCH_DSL_ENABLED=True) + @patch("api_app.views.Search") + def test_elastic_request(self, mocked_search): + self.client.force_authenticate(self.org_user) + response = self.client.get( + self.uri, + data={ + "plugin_name": "analyzer", + "name": "classic_dns", + "status": "SUCCESS", + "errors": False, + "start_start_time": datetime.datetime(2024, 11, 27), + "end_start_time": datetime.datetime(2024, 11, 28), + "start_end_time": datetime.datetime(2024, 11, 27), + "end_end_time": datetime.datetime(2024, 11, 28), + "report": "8.8.8.8", + }, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + mocked_search.return_value.query.call_args_list[0][0][0], + Bool( + filter=[ + Bool( + should=[ + Term(user__username="elastic_test_user"), + Term(membership__organization__name="elastic_test_user"), + ] + ), + Term(plugin_name="analyzer"), + Term(name="classic_dns"), + Term(status=ReportStatus.SUCCESS), + Range( + start_time={ + "gte": datetime.datetime( + 2024, 11, 27, 0, 0, tzinfo=ZoneInfo(key="UTC") + ) + } + ), + Range( + start_time={ + "lte": datetime.datetime( + 2024, 11, 28, 0, 0, tzinfo=ZoneInfo(key="UTC") + ) + } + ), + Range( + end_time={ + "gte": datetime.datetime( + 2024, 11, 27, 0, 0, tzinfo=ZoneInfo(key="UTC") + ) + } + ), + Range( + end_time={ + "lte": datetime.datetime( + 2024, 11, 28, 0, 0, tzinfo=ZoneInfo(key="UTC") + ) + } + ), + Term(report="8.8.8.8"), + ] + ), + ) diff --git a/tests/api_app/test_websocket.py b/tests/api_app/test_websocket.py index cfa32986..442dc4e4 100644 --- a/tests/api_app/test_websocket.py +++ b/tests/api_app/test_websocket.py @@ -56,7 +56,7 @@ def setUp(self) -> None: self.job = Job.objects.create( id=1027, user=self.user, - status=Job.Status.REPORTED_WITHOUT_FAILS.value, + status=Job.STATUSES.REPORTED_WITHOUT_FAILS.value, observable_name="8.8.8.8", observable_classification=ObservableTypes.IP, ) @@ -89,7 +89,7 @@ async def test_job_terminated(self, *args, **kwargs): self.assertEqual(job_report["id"], 1027) self.assertEqual(job_report["observable_name"], "8.8.8.8") self.assertEqual( - job_report["status"], Job.Status.REPORTED_WITHOUT_FAILS.value + job_report["status"], Job.STATUSES.REPORTED_WITHOUT_FAILS.value ) async def test_job_running(self, *args, **kwargs): @@ -103,7 +103,7 @@ async def test_job_running(self, *args, **kwargs): job = await sync_to_async(Job.objects.create)( id=1029, user=self.user, - status=Job.Status.PENDING.value, + status=Job.STATUSES.PENDING.value, observable_name="test.com", observable_classification=ObservableTypes.DOMAIN, ) @@ -165,7 +165,7 @@ async def test_job_running(self, *args, **kwargs): job_report_running = await communicator.receive_json_from() self.assertEqual(job_report_running["id"], 1029) self.assertEqual(job_report_running["observable_name"], "test.com") - self.assertEqual(job_report_running["status"], Job.Status.PENDING.value) + self.assertEqual(job_report_running["status"], Job.STATUSES.PENDING.value) self.assertEqual(job_report_running["analyzer_reports"], []) self.assertIsNone(job_report_running["finished_analysis_time"]) time.sleep(1) @@ -183,12 +183,12 @@ async def test_job_running(self, *args, **kwargs): self.assertEqual(job_analyzer_terminated["id"], 1029) self.assertEqual(job_analyzer_terminated["observable_name"], "test.com") self.assertEqual( - job_analyzer_terminated["status"], Job.Status.PENDING.value + job_analyzer_terminated["status"], Job.STATUSES.PENDING.value ) self.assertIsNotNone(job_analyzer_terminated["analyzer_reports"]) self.assertIsNone(job_analyzer_terminated["finished_analysis_time"]) # terminate job (force status) - job.status = Job.Status.REPORTED_WITHOUT_FAILS + job.status = Job.STATUSES.REPORTED_WITHOUT_FAILS await sync_to_async(job.save)() await sync_to_async(job_set_final_status)(1029) time.sleep(1) @@ -197,7 +197,8 @@ async def test_job_running(self, *args, **kwargs): self.assertEqual(job_report_terminated["id"], 1029) self.assertEqual(job_report_terminated["observable_name"], "test.com") self.assertEqual( - job_report_terminated["status"], Job.Status.REPORTED_WITHOUT_FAILS.value + job_report_terminated["status"], + Job.STATUSES.REPORTED_WITHOUT_FAILS.value, ) self.assertIsNotNone(job_report_terminated["analyzer_reports"]) self.assertIsNotNone(job_report_terminated["finished_analysis_time"]) @@ -206,7 +207,7 @@ async def test_job_killed(self, *args, **kwargs): await sync_to_async(Job.objects.create)( id=1030, user=self.user, - status=Job.Status.RUNNING.value, + status=Job.STATUSES.RUNNING.value, observable_name="test.com", observable_classification=ObservableTypes.DOMAIN, ) @@ -224,7 +225,7 @@ async def test_job_killed(self, *args, **kwargs): job_running = await communicator.receive_json_from() self.assertEqual(job_running["id"], 1030) self.assertEqual(job_running["observable_name"], "test.com") - self.assertEqual(job_running["status"], Job.Status.RUNNING.value) + self.assertEqual(job_running["status"], Job.STATUSES.RUNNING.value) time.sleep(1) await sync_to_async(self.client.patch)("/api/jobs/1030/kill") @@ -233,4 +234,4 @@ async def test_job_killed(self, *args, **kwargs): job_killed = await communicator.receive_json_from() self.assertEqual(job_killed["id"], 1030) self.assertEqual(job_killed["observable_name"], "test.com") - self.assertEqual(job_killed["status"], Job.Status.KILLED.value) + self.assertEqual(job_killed["status"], Job.STATUSES.KILLED.value) diff --git a/tests/api_app/visualizers_manager/passive_dns/test_analyzer_extractor.py b/tests/api_app/visualizers_manager/passive_dns/test_analyzer_extractor.py index 70958c97..2fe802d7 100644 --- a/tests/api_app/visualizers_manager/passive_dns/test_analyzer_extractor.py +++ b/tests/api_app/visualizers_manager/passive_dns/test_analyzer_extractor.py @@ -24,7 +24,7 @@ def setUpClass(cls) -> None: super().setUpClass() cls.job = Job.objects.create( user=cls.user, - status=Job.Status.RUNNING.value, + status=Job.STATUSES.RUNNING.value, observable_name="195.22.26.248", observable_classification=ObservableTypes.IP, received_request_time=datetime.datetime.now(), @@ -108,7 +108,7 @@ def setUpClass(cls) -> None: super().setUpClass() cls.job = Job.objects.create( user=cls.user, - status=Job.Status.RUNNING.value, + status=Job.STATUSES.RUNNING.value, observable_name="test.com", observable_classification=ObservableTypes.DOMAIN, received_request_time=datetime.datetime.now(), @@ -198,7 +198,7 @@ def setUpClass(cls) -> None: super().setUpClass() cls.job = Job.objects.create( user=cls.user, - status=Job.Status.RUNNING.value, + status=Job.STATUSES.RUNNING.value, observable_name="test.com", observable_classification=ObservableTypes.DOMAIN, received_request_time=datetime.datetime.now(), @@ -306,7 +306,7 @@ def setUpClass(cls) -> None: super().setUpClass() cls.job = Job.objects.create( user=cls.user, - status=Job.Status.RUNNING.value, + status=Job.STATUSES.RUNNING.value, observable_name="www.farsightsecurity.com", observable_classification=ObservableTypes.DOMAIN, received_request_time=datetime.datetime.now(), @@ -384,7 +384,7 @@ def setUpClass(cls) -> None: super().setUpClass() cls.job = Job.objects.create( user=cls.user, - status=Job.Status.RUNNING.value, + status=Job.STATUSES.RUNNING.value, observable_name="test.com", observable_classification=ObservableTypes.DOMAIN, received_request_time=datetime.datetime.now(), @@ -459,7 +459,7 @@ def setUpClass(cls) -> None: super().setUpClass() cls.job = Job.objects.create( user=cls.user, - status=Job.Status.RUNNING.value, + status=Job.STATUSES.RUNNING.value, observable_name="test.com", observable_classification=ObservableTypes.DOMAIN, received_request_time=datetime.datetime.now(), @@ -534,7 +534,7 @@ def setUpClass(cls) -> None: super().setUpClass() cls.job = Job.objects.create( user=cls.user, - status=Job.Status.RUNNING.value, + status=Job.STATUSES.RUNNING.value, observable_name="test.com", observable_classification=ObservableTypes.DOMAIN, received_request_time=datetime.datetime.now(), diff --git a/tests/api_app/visualizers_manager/test_classes.py b/tests/api_app/visualizers_manager/test_classes.py index 73588f9c..64f49302 100644 --- a/tests/api_app/visualizers_manager/test_classes.py +++ b/tests/api_app/visualizers_manager/test_classes.py @@ -11,6 +11,7 @@ from api_app.visualizers_manager.classes import ( VisualizableBase, VisualizableBool, + VisualizableDownload, VisualizableHorizontalList, VisualizableLevel, VisualizableLevelSize, @@ -113,6 +114,74 @@ def test_disable(self): self.assertEqual(vo.to_dict(), expected_result) +class VisualizableDownloadTestCase(CustomTestCase): + def test_to_dict(self): + vo = VisualizableDownload( + value="hello.txt", + payload="hello, world", + link="https://test.com", + disable=True, + description="description-test", + ) + expected_result = { + "alignment": "center", + "disable": True, + "type": "download", + "value": "hello.txt", + "size": "auto", + "link": "https://test.com", + "copy_text": "", + "payload": "hello, world", + "mimetype": "text/plain", + "description": "description-test", + "add_metadata_in_description": True, + } + self.assertEqual(vo.to_dict(), expected_result) + + def test_empty(self): + vo = VisualizableDownload( + value="", + payload="", + disable=False, + ) + expected_result = { + "alignment": "center", + "disable": False, + "type": "download", + "value": "", + "size": "auto", + "link": "", + "copy_text": "", + "payload": "", + "mimetype": "application/x-empty", + "description": "", + "add_metadata_in_description": True, + } + self.assertEqual(vo.to_dict(), expected_result) + + def test_disable(self): + vo = VisualizableDownload( + value="", + payload="hello, world", + size=VisualizableSize.S_3, + disable=True, + ) + expected_result = { + "alignment": "center", + "disable": True, + "type": "download", + "value": "", + "size": "3", + "link": "", + "payload": "hello, world", + "copy_text": "", + "description": "", + "mimetype": "text/plain", + "add_metadata_in_description": True, + } + self.assertEqual(vo.to_dict(), expected_result) + + class VisualizableBoolTestCase(CustomTestCase): def test_to_dict(self): vo = VisualizableBool(value="test", disable=False) diff --git a/tests/test_crons.py b/tests/test_crons.py index 10122577..abe1060b 100644 --- a/tests/test_crons.py +++ b/tests/test_crons.py @@ -34,7 +34,7 @@ def test_check_stuck_analysis(self): _job = Job.objects.create( user=self.user, - status=Job.Status.RUNNING.value, + status=Job.STATUSES.RUNNING.value, observable_name="8.8.8.8", observable_classification=ObservableTypes.IP, received_request_time=now(), @@ -45,12 +45,12 @@ def test_check_stuck_analysis(self): _job.save() self.assertCountEqual(check_stuck_analysis(), [_job.pk]) - _job.status = Job.Status.PENDING.value + _job.status = Job.STATUSES.PENDING.value _job.save() self.assertCountEqual(check_stuck_analysis(check_pending=False), []) self.assertCountEqual(check_stuck_analysis(check_pending=True), [_job.pk]) - _job.status = Job.Status.ANALYZERS_RUNNING.value + _job.status = Job.STATUSES.ANALYZERS_RUNNING.value _job.save() self.assertCountEqual(check_stuck_analysis(check_pending=False), [_job.pk]) _job.delete() @@ -60,7 +60,7 @@ def test_remove_old_jobs(self): _job = Job.objects.create( user=self.user, - status=Job.Status.FAILED.value, + status=Job.STATUSES.FAILED.value, observable_name="8.8.8.8", observable_classification=ObservableTypes.IP, received_request_time=now(), diff --git a/tests/threat_matrix/test_tasks.py b/tests/threat_matrix/test_tasks.py index 5b44fe15..1518f48c 100644 --- a/tests/threat_matrix/test_tasks.py +++ b/tests/threat_matrix/test_tasks.py @@ -11,6 +11,8 @@ from api_app.models import Job, LastElasticReportUpdate, PythonModule from api_app.pivots_manager.models import PivotConfig, PivotReport from api_app.visualizers_manager.models import VisualizerConfig, VisualizerReport +from certego_saas.apps.organization.membership import Membership +from certego_saas.apps.organization.organization import Organization from certego_saas.apps.user.models import User from tests import CustomTestCase from tests.mock_utils import MockResponseNoOp @@ -24,8 +26,19 @@ class SendElasticTestCase(CustomTestCase): def setUp(self): + self.user, _ = User.objects.get_or_create( + username="test_elastic_user", + email="elastic@khulnasoft.com", + password="test", + ) + self.organization, _ = Organization.objects.get_or_create( + name="test_elastic_org" + ) + self.membership, _ = Membership.objects.get_or_create( + user=self.user, organization=self.organization, is_owner=True + ) self.job = Job.objects.create( - observable_name="dns.google.com", tlp="AMBER", user=User.objects.first() + observable_name="dns.google.com", tlp="AMBER", user=self.user ) AnalyzerReport.objects.create( # valid config=AnalyzerConfig.objects.get( @@ -37,7 +50,7 @@ def setUp(self): job=self.job, start_time=datetime.datetime(2024, 10, 29, 10, 49, tzinfo=datetime.UTC), end_time=datetime.datetime(2024, 10, 29, 10, 59, tzinfo=datetime.UTC), - status=AnalyzerReport.Status.FAILED, + status=AnalyzerReport.STATUSES.FAILED, errors=["error1", "error2"], task_id=uuid(), parameters={}, @@ -52,7 +65,7 @@ def setUp(self): job=self.job, start_time=datetime.datetime(2024, 10, 29, 10, 49, tzinfo=datetime.UTC), end_time=datetime.datetime(2024, 10, 29, 10, 59, tzinfo=datetime.UTC), - status=AnalyzerReport.Status.KILLED, + status=AnalyzerReport.STATUSES.KILLED, task_id=uuid(), parameters={}, ) @@ -70,7 +83,7 @@ def setUp(self): end_time=datetime.datetime( 2024, 9, 29, 10, 58, 59, tzinfo=datetime.timezone.utc ), - status=AnalyzerReport.Status.SUCCESS, + status=AnalyzerReport.STATUSES.SUCCESS, report={"observable": "dns.google.com", "malicious": False}, task_id=uuid(), parameters={}, @@ -83,7 +96,7 @@ def setUp(self): ) ), job=self.job, - status=AnalyzerReport.Status.RUNNING, + status=AnalyzerReport.STATUSES.RUNNING, start_time=datetime.datetime(2024, 10, 29, 10, 49, tzinfo=datetime.UTC), end_time=datetime.datetime(2024, 10, 29, 10, 59, tzinfo=datetime.UTC), task_id=uuid(), @@ -99,7 +112,7 @@ def setUp(self): job=self.job, start_time=datetime.datetime(2024, 10, 29, 10, 49, tzinfo=datetime.UTC), end_time=datetime.datetime(2024, 10, 29, 10, 59, tzinfo=datetime.UTC), - status=ConnectorReport.Status.SUCCESS, + status=ConnectorReport.STATUSES.SUCCESS, task_id=uuid(), report={ "subject": "Subject", @@ -119,7 +132,7 @@ def setUp(self): job=self.job, start_time=datetime.datetime(2024, 10, 29, 10, 49, tzinfo=datetime.UTC), end_time=datetime.datetime(2024, 10, 29, 10, 59, tzinfo=datetime.UTC), - status=IngestorReport.Status.SUCCESS, + status=IngestorReport.STATUSES.SUCCESS, task_id=uuid(), report={}, parameters={}, @@ -134,7 +147,7 @@ def setUp(self): job=self.job, start_time=datetime.datetime(2024, 10, 29, 10, 49, tzinfo=datetime.UTC), end_time=datetime.datetime(2024, 10, 29, 10, 59, tzinfo=datetime.UTC), - status=PivotReport.Status.SUCCESS, + status=PivotReport.STATUSES.SUCCESS, task_id=uuid(), report={"job_id": [1], "created": True, "motivation": None}, parameters={}, @@ -149,7 +162,7 @@ def setUp(self): job=self.job, start_time=datetime.datetime(2024, 10, 29, 10, 49, tzinfo=datetime.UTC), end_time=datetime.datetime(2024, 10, 29, 10, 59, tzinfo=datetime.UTC), - status=VisualizerReport.Status.SUCCESS, + status=VisualizerReport.STATUSES.SUCCESS, task_id=uuid(), report={ "level_position": 1, @@ -170,6 +183,9 @@ def tearDown(self): PivotReport.objects.all().delete() VisualizerReport.objects.all().delete() LastElasticReportUpdate.objects.all().delete() + self.user.delete() + self.organization.delete() + self.membership.delete() @override_settings(ELASTICSEARCH_DSL_ENABLED=True) @override_settings(ELASTICSEARCH_DSL_HOST="https://elasticsearch:9200") @@ -190,9 +206,15 @@ def test_initial(self, *args, **kwargs): "_op_type": "index", "_index": "plugin-report-analyzer-report-2024-10-29", "_source": { + "user": {"username": "test_elastic_user"}, + "membership": { + "is_admin": False, + "is_owner": True, + "organization": {"name": "test_elastic_org"}, + }, "config": { "name": "DNS0_EU_Malicious_Detector", - "plugin_name": "Analyzer", + "plugin_name": "analyzer", }, "job": {"id": self.job.id}, "start_time": datetime.datetime( @@ -210,9 +232,15 @@ def test_initial(self, *args, **kwargs): "_op_type": "index", "_index": "plugin-report-analyzer-report-2024-10-29", "_source": { + "user": {"username": "test_elastic_user"}, + "membership": { + "is_admin": False, + "is_owner": True, + "organization": {"name": "test_elastic_org"}, + }, "config": { "name": "Quad9_Malicious_Detector", - "plugin_name": "Analyzer", + "plugin_name": "analyzer", }, "job": {"id": self.job.id}, "start_time": datetime.datetime( @@ -230,9 +258,15 @@ def test_initial(self, *args, **kwargs): "_op_type": "index", "_index": "plugin-report-connector-report-2024-10-29", "_source": { + "user": {"username": "test_elastic_user"}, + "membership": { + "is_admin": False, + "is_owner": True, + "organization": {"name": "test_elastic_org"}, + }, "config": { "name": "AbuseSubmitter", - "plugin_name": "Connector", + "plugin_name": "connector", }, "job": {"id": self.job.id}, "start_time": datetime.datetime( @@ -255,9 +289,17 @@ def test_initial(self, *args, **kwargs): "_op_type": "index", "_index": "plugin-report-pivot-report-2024-10-29", "_source": { + "user": { + "username": "test_elastic_user", + }, + "membership": { + "is_owner": True, + "is_admin": False, + "organization": {"name": "test_elastic_org"}, + }, "config": { "name": "AbuseIpToSubmission", - "plugin_name": "Pivot", + "plugin_name": "pivot", }, "job": {"id": self.job.id}, "start_time": datetime.datetime( @@ -303,9 +345,15 @@ def test_update(self, *args, **kwargs): "_index": "plugin-report-analyzer-report-2024-10-29", "_op_type": "index", "_source": { + "user": {"username": "test_elastic_user"}, + "membership": { + "is_admin": False, + "is_owner": True, + "organization": {"name": "test_elastic_org"}, + }, "config": { "name": "DNS0_EU_Malicious_Detector", - "plugin_name": "Analyzer", + "plugin_name": "analyzer", }, "end_time": datetime.datetime( 2024, 10, 29, 10, 59, tzinfo=datetime.timezone.utc @@ -323,9 +371,15 @@ def test_update(self, *args, **kwargs): "_index": "plugin-report-analyzer-report-2024-10-29", "_op_type": "index", "_source": { + "user": {"username": "test_elastic_user"}, + "membership": { + "is_admin": False, + "is_owner": True, + "organization": {"name": "test_elastic_org"}, + }, "config": { "name": "Quad9_Malicious_Detector", - "plugin_name": "Analyzer", + "plugin_name": "analyzer", }, "end_time": datetime.datetime( 2024, 10, 29, 10, 59, tzinfo=datetime.timezone.utc @@ -343,9 +397,15 @@ def test_update(self, *args, **kwargs): "_index": "plugin-report-connector-report-2024-10-29", "_op_type": "index", "_source": { + "user": {"username": "test_elastic_user"}, + "membership": { + "is_admin": False, + "is_owner": True, + "organization": {"name": "test_elastic_org"}, + }, "config": { "name": "AbuseSubmitter", - "plugin_name": "Connector", + "plugin_name": "connector", }, "end_time": datetime.datetime( 2024, 10, 29, 10, 59, tzinfo=datetime.timezone.utc @@ -368,9 +428,15 @@ def test_update(self, *args, **kwargs): "_index": "plugin-report-pivot-report-2024-10-29", "_op_type": "index", "_source": { + "user": {"username": "test_elastic_user"}, + "membership": { + "is_admin": False, + "is_owner": True, + "organization": {"name": "test_elastic_org"}, + }, "config": { "name": "AbuseIpToSubmission", - "plugin_name": "Pivot", + "plugin_name": "pivot", }, "end_time": datetime.datetime( 2024, 10, 29, 10, 59, tzinfo=datetime.timezone.utc diff --git a/threat_matrix/settings/__init__.py b/threat_matrix/settings/__init__.py index 6b9d8796..011449e4 100644 --- a/threat_matrix/settings/__init__.py +++ b/threat_matrix/settings/__init__.py @@ -42,6 +42,7 @@ "api_app.pivots_manager", "api_app.ingestors_manager", "api_app.investigations_manager", + "api_app.data_model_manager", # auth "rest_email_auth", # performance debugging diff --git a/threat_matrix/tasks.py b/threat_matrix/tasks.py index e526f4a3..aa631763 100644 --- a/threat_matrix/tasks.py +++ b/threat_matrix/tasks.py @@ -130,9 +130,9 @@ def check_stuck_analysis(minutes_ago: int = 25, check_pending: bool = False): def fail_job(job): logger.error( f"found stuck analysis, job_id:{job.id}." - f"Setting the job to status {Job.Status.FAILED.value}'" + f"Setting the job to status {Job.STATUSES.FAILED.value}'" ) - job.status = Job.Status.FAILED.value + job.status = Job.STATUSES.FAILED.value job.finished_analysis_time = now() job.save(update_fields=["status", "finished_analysis_time"]) @@ -145,9 +145,9 @@ def fail_job(job): jobs_id_stuck = [] for running_job in running_jobs: jobs_id_stuck.append(running_job.id) - if running_job.status == Job.Status.RUNNING.value: + if running_job.status == Job.STATUSES.RUNNING.value: fail_job(running_job) - elif running_job.status == Job.Status.PENDING.value: + elif running_job.status == Job.STATUSES.PENDING.value: # the job can be pending for 2 cycles of this function if running_job.received_request_time < ( now() - datetime.timedelta(minutes=(minutes_ago * 2) + 1) @@ -266,7 +266,7 @@ def job_pipeline( + list(job.pivotreports.all()) + list(job.visualizerreports.all()) ): - report.status = report.Status.FAILED.value + report.status = report.STATUSES.FAILED.value report.save() @@ -304,7 +304,7 @@ def run_plugin( except Exception as e: logger.exception(e) config.reports.filter(job__pk=job_id).update( - status=plugin.report_model.Status.FAILED.value + status=plugin.report_model.STATUSES.FAILED.value ) job = Job.objects.get(pk=job_id) JobConsumer.serialize_and_send_job(job) @@ -445,9 +445,21 @@ def _convert_report_to_elastic_document( f"{now().date()}" ), "_source": { + "user": {"username": report.user.username}, + "membership": ( + { + "is_owner": report.user.membership.is_owner, + "is_admin": report.user.membership.is_admin, + "organization": { + "name": report.user.membership.organization.name, + }, + } + if report.user.has_membership() + else {} + ), "config": { "name": report.config.name, - "plugin_name": report.config.plugin_name, + "plugin_name": report.config.plugin_name.lower(), }, "job": {"id": report.job.id}, "start_time": report.start_time,