diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 9bc01527..73b6ec66 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,45 +1,51 @@ # Changelog -[**Upgrade Guide**](https://khulnasoft.github.io/ThreatMatrix-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.1.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.1.0) +## [v6.2.0](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v6.2.0) +#### TODO: DRAFT CHANGELOG -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: +### 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: - [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/devsec-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/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 -- 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/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. 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 +56,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/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** 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 +84,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/devsec-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/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 -- 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 +128,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/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. +* 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/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 **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 +212,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/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) -- 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/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 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/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** - -- 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/devsec-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 +323,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/devsec-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 +359,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 +368,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/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. 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 +411,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 +418,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/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 - 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 +467,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/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/ThreatMatrix-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) +- 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/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. ## [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 +505,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 +522,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/devsec-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/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/ThreatMatrix-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/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/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 + ## [v3.2.4](https://github.com/khulnasoft/ThreatMatrix/releases/tag/v3.2.4) **Notes:** @@ -667,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/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/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) @@ -778,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/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/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/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/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. @@ -794,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/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/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:** @@ -808,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/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/devsec-docs/advanced_usage/#customize-analyzer-execution). **REST API changes:** @@ -870,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/ThreatMatrix-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 @@ -949,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/ThreatMatrix-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/ThreatMatrix-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/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/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/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/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 @@ -975,7 +927,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/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 @@ -1106,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/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/devsec-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..6cb983af 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/devsec-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/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 137744be..d22f3c35 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/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/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/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/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/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/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 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..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,8 +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 ``` +- [ ] 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/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..cfa61307 100644 --- a/.github/workflows/pull_request_automation.yml +++ b/.github/workflows/pull_request_automation.yml @@ -123,7 +123,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..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/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/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/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/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/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/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/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/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/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..2d87c297 --- /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") + 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): + 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") + 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): + 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/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/0130_analyzer_config_nvd_cve.py b/api_app/analyzers_manager/migrations/0130_analyzer_config_nvd_cve.py new file mode 100644 index 00000000..360fd119 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0130_analyzer_config_nvd_cve.py @@ -0,0 +1,136 @@ +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": "nvd_cve.NVDDetails", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "NVD_CVE", + "description": "[NationalVunerabilityDatabase(NVD)](https://nvd.nist.gov/developers/vulnerabilities) is the U.S. government repository of standards based vulnerability management data represented using the Security Content Automation Protocol (SCAP). This data enables automation of vulnerability management, security measurement, and compliance. The NVD includes databases of security checklist references, security-related software flaws, product names, and impact metrics.", + "disabled": False, + "soft_time_limit": 60, + "routing_key": "default", + "health_check_status": True, + "type": "observable", + "docker_based": False, + "maximum_tlp": "AMBER", + "observable_supported": ["generic"], + "supported_filetypes": [], + "run_hash": False, + "run_hash_type": "", + "not_supported_filetypes": [], + "model": "analyzers_manager.AnalyzerConfig", +} + +params = [ + { + "python_module": { + "module": "nvd_cve.NVDDetails", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "nvd_api_key", + "type": "str", + "description": "API Key is optional. In case you want to increase the request quota to 50 requests in a rolling 30 second window, you need API key. Get yours at [NIST](https://nvd.nist.gov/developers/request-an-api-key)", + "is_secret": True, + "required": False, + } +] + +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", "0063_singleton_and_elastic_report"), + ("analyzers_manager", "0129_analyzer_config_phishing_extractor"), + ] + + 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 b0977efd..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 @@ -91,6 +221,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"]: @@ -120,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 @@ -128,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) @@ -187,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/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/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/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/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/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/nvd_cve.py b/api_app/analyzers_manager/observable_analyzers/nvd_cve.py new file mode 100644 index 00000000..32b10cbe --- /dev/null +++ b/api_app/analyzers_manager/observable_analyzers/nvd_cve.py @@ -0,0 +1,132 @@ +import re + +import requests + +from api_app.analyzers_manager.classes import AnalyzerRunException, ObservableAnalyzer +from tests.mock_utils import MockUpResponse, if_mock_connections, patch + + +class NVDDetails(ObservableAnalyzer): + url: str = "https://services.nvd.nist.gov/rest/json/cves/2.0" + _nvd_api_key: str = None + cve_pattern = r"^CVE-\d{4}-\d{4,7}$" + + @classmethod + def update(self) -> bool: + pass + + def run(self): + headers = {} + if self._nvd_api_key: + headers.update({"apiKey": self._nvd_api_key}) + + try: + # Validate if CVE format is correct E.g CVE-2014-1234 or cve-2022-1234567 + if not re.match( + self.cve_pattern, self.observable_name, flags=re.IGNORECASE + ): + raise ValueError(f"Invalid CVE format: {self.observable_name}") + + params = {"cveId": self.observable_name.upper()} + response = requests.get(url=self.url, params=params, headers=headers) + response.raise_for_status() + + except ValueError as e: + raise AnalyzerRunException(e) + except requests.RequestException as e: + raise AnalyzerRunException(e) + + return response.json() + + @classmethod + def _monkeypatch(cls): + patches = [ + 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, + ), + ) + ) + ] + return super()._monkeypatch(patches=patches) 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_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/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/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..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: @@ -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/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/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..8786d11c --- /dev/null +++ b/api_app/ingestors_manager/ingestors/virus_total.py @@ -0,0 +1,324 @@ +import logging +from typing import Any, Dict, 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 + + 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 + + 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"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: + 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/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/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/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 198e5b43..449c0716 100644 --- a/api_app/mixins.py +++ b/api_app/mixins.py @@ -1,8 +1,18 @@ +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.constants import ObservableTypes +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 +76,578 @@ def list(self, request, *args, **kwargs): cache.touch(cache_name, timeout=60 * 60 * 24 * 7) return Response(data) + + +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 _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, 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 + 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 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 + # 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..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,6 +1419,25 @@ def process_time(self) -> float: secs = (self.end_time - self.start_time).total_seconds() return round(secs, 2) + 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: + 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): """ @@ -1787,3 +1806,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/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/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/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/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..31a1a65e 100644 --- a/api_app/pivots_manager/pivots/any_compare.py +++ b/api_app/pivots_manager/pivots/any_compare.py @@ -8,16 +8,21 @@ class AnyCompare(Compare): def should_run(self) -> Tuple[bool, Optional[str]]: - if result := self.related_reports.filter( - status=self.report_model.Status.SUCCESS.value - ).first(): + for report in self.related_reports.filter( + status=self.report_model.STATUSES.SUCCESS.value + ): try: - self._value = self._get_value(self.field_to_compare) - except (RuntimeError, ValueError) as e: - return False, str(e) + self._value = report.get_value( + report.report, self.field_to_compare.split(".") + ) + 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..ece0fcef 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 ( @@ -40,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._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.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/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/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/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/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..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 @@ -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, ) @@ -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: @@ -693,7 +698,9 @@ def get_configurations(self) -> AbstractConfigQuerySet: Returns: AbstractConfigQuerySet: The queryset of configurations. """ - return self.model.config.objects.filter(pk__in=self.values("config__pk")) + return self.model.config.field.related_model.objects.filter( + pk__in=self.values("config_id") + ) class ModelWithOwnershipQuerySet: @@ -934,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 new file mode 100644 index 00000000..4670a069 --- /dev/null +++ b/api_app/serializers/elastic.py @@ -0,0 +1,72 @@ +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__) + + +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 = "" + 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=supported_plugin_name_list, + 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 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.ListField(child=serializers.CharField()) + report = serializers.JSONField() diff --git a/api_app/serializers/job.py b/api_app/serializers/job.py index 28f8e2dc..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( @@ -478,6 +478,7 @@ class Meta: "playbook", "status", "received_request_time", + "is_sample", ] playbook = rfs.SlugRelatedField( @@ -538,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)) @@ -565,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]: @@ -603,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 @@ -629,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, @@ -647,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 @@ -748,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) @@ -772,7 +780,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): @@ -1005,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 @@ -1053,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/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..1ba238ce 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, name="plugin_report_queries"), # router viewsets path("", include(router.urls)), # Plugins @@ -50,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 f0a8afbb..4a1ebfca 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, @@ -415,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. @@ -452,7 +463,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 @@ -536,11 +547,41 @@ 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) + @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, @@ -562,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() @@ -656,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) @@ -724,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_observable_name(self, request): + def aggregate_top_playbook(self, request): """ - Aggregate jobs by observable name. + Aggregate playbooks by usage. Returns: - - Aggregated count of jobs for each observable name. + - Aggregated count of playbooks for each one. """ return self.__aggregation_response_dynamic( - "observable_name", False, users=self.get_org_members(request) + "playbook_to_execute__name", users=self.get_org_members(request) ) @action( - url_path="aggregate/md5", + url_path="aggregate/top_user", detail=False, methods=["GET"], ) @cache_action_response(timeout=60 * 5) - def aggregate_md5(self, request): + def aggregate_top_user(self, request): """ - Aggregate jobs by MD5 hash. + Aggregate Users by usage. Returns: - - Aggregated count of jobs for each MD5 hash. + - Aggregated count of users for each one. """ - # this is for file return self.__aggregation_response_dynamic( - "md5", False, users=self.get_org_members(request) + "user__username", users=self.get_org_members(request) + ) + + @action( + url_path="aggregate/top_tlp", + detail=False, + methods=["GET"], + ) + @cache_action_response(timeout=60 * 5) + def aggregate_top_tlp(self, request): + """ + Aggregate TLPs by usage. + + Returns: + - Aggregated count of TLPs for each one. + """ + return self.__aggregation_response_dynamic( + "tlp", users=self.get_org_members(request) ) @staticmethod @@ -836,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 @@ -1174,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 @@ -1251,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"}) @@ -1287,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"} @@ -1525,3 +1590,96 @@ 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 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): + """ + 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() + + # 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, 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) + ) + 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 + 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/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/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/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..eb677743 --- /dev/null +++ b/configuration/elastic_search_mappings/plugin_report.json @@ -0,0 +1,69 @@ +{ + "index_patterns": [ + "plugin-report-*" + ], + "settings" : { + "number_of_shards" : 1, + "number_of_replicas": 0 + }, + "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": { + "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..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/ThreatMatrix-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 new file mode 100755 index 00000000..7ef744ab --- /dev/null +++ b/create_elastic_certs @@ -0,0 +1,24 @@ +#!/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 && + 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..2cf0643b 100644 --- a/docker/elasticsearch.override.yml +++ b/docker/elasticsearch.override.yml @@ -1,19 +1,31 @@ services: uwsgi: depends_on: - - elasticsearch + elasticsearch: + condition: service_healthy + 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..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: 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..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/ThreatMatrix-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 c89ee56a..2e43ca08 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,41 @@ "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", + "@types/jest": "^29.5.14", "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 +70,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 +99,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 +111,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 +173,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 +225,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 +245,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 +275,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 +317,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 +352,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 +368,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 +403,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 +453,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 +467,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 +481,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 +510,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 +524,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 +540,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 +692,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 +706,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 +721,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 +735,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 +771,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 +850,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 +894,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 +908,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 +924,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 +940,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 +954,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 +968,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 +983,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 +998,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 +1017,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 +1032,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 +1046,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 +1061,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 +1074,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 +1104,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 +1119,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 +1148,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 +1163,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 +1179,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 +1193,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 +1207,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 +1221,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 +1235,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 +1250,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 +1266,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 +1283,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 +1298,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 +1313,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 +1327,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 +1341,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 +1355,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 +1371,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 +1386,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 +1400,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 +1415,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 +1429,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 +1444,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 +1460,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 +1488,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 +1502,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 +1520,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 +1534,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 +1549,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 +1564,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 +1597,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 +1611,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 +1626,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 +1640,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 +1654,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 +1685,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 +1699,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 +1714,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 +1729,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 +1744,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 +1840,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 +1876,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 +1912,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 +1929,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 +1959,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 +2284,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 +2455,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 +2482,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 +2518,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 +2544,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 +2570,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 +3430,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 +3813,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 +3827,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 +3841,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 +3861,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 +3879,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 +3895,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 +3909,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 +3998,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 +4366,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 +4786,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 +4804,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 +4850,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 +4878,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": "*" } @@ -4851,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", @@ -5405,6 +5612,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 +5994,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 +6019,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 +6065,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 +6135,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 +6264,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 +6323,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 +6834,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 +6852,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 +6976,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 +7110,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 +7405,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 +8348,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 +8469,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 +8729,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 +8893,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 +9027,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 +9179,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 +9220,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 +9294,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 +9969,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 +11228,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 +11385,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 +11422,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 +11521,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 +14648,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 +15844,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 +15863,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 +15916,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 +15974,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 +16108,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 +16145,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 +16193,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 +17842,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 +18355,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 +18456,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 +18470,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 +18486,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 +18615,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 +18629,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 +20302,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 +20415,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 +20425,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 +20453,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 +20470,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 +20596,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 +20625,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 +20679,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 +20725,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 +20740,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 +21132,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 +21186,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 +21825,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 +21875,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 +21913,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 +23089,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 +23109,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 +24548,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 +24568,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 +24621,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 +24696,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 +24785,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 +24820,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 +24970,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 +24978,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 +24987,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 +25019,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 +25074,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 +25100,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.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-plugin-utils": "^7.24.7" + "@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 +25252,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 +25454,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 +25521,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 +25573,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 +25695,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 +25724,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 +25748,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 +25764,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 +25952,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 +26090,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 +26111,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 +26134,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 +26157,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 +26172,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 +26831,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 +26993,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 +27029,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 +27043,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 +27055,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 +27124,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 +27367,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 +27725,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 +27743,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 +27789,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 +27817,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": "*" } @@ -27664,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", @@ -28090,6 +28416,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 +28736,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 +28755,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 +28786,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 +28832,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 +28918,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 +28966,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 +29357,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 +29449,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 +29529,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 +29770,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 +30417,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 +30501,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 +30701,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 +30832,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 +30933,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 +31155,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 +31182,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 +31234,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 +31302,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 +31609,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 +32511,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 +32625,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 +32656,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 +32719,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 +35033,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 +35843,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 +35898,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 +35944,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 +36041,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 +36063,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 +36096,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 +37081,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 +37452,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 +37533,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 +37547,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 +37556,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 +37659,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 +38938,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 +39012,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 +39022,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 +39045,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 +39160,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 +39226,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 +39263,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 +39539,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 +40082,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 +40125,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 +40154,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 +41028,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 +41042,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..d9ee1a52 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,25 @@ ] }, "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", + "@types/jest": "^29.5.14", "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..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/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..a8ffd9e0 --- /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/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 52b72ef5..29238f08 100644 --- a/frontend/src/components/home/Home.jsx +++ b/frontend/src/components/home/Home.jsx @@ -12,23 +12,22 @@ 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/intel-owl-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/intel-owl-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/intel-owl-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", 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/result/bar/JobActionBar.jsx b/frontend/src/components/jobs/result/bar/JobActionBar.jsx index dd3bc3ce..e9d6c3d1 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, @@ -18,6 +16,7 @@ import { retryJobIcon, downloadReportIcon, } from "../../../common/icon/icons"; +import { fileDownload } from "../../../../utils/files"; export function JobActionsBar({ job }) { // routers @@ -31,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; @@ -53,33 +42,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/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 = ( (
), 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/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 857055b1..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/ThreatMatrix-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/constants/miscConst.js b/frontend/src/constants/miscConst.js index 19519e90..cfa69b75 100644 --- a/frontend/src/constants/miscConst.js +++ b/frontend/src/constants/miscConst.js @@ -13,3 +13,11 @@ export const TLPDescriptions = Object.freeze({ export const HACKER_MEME_STRING = "LoOk At YoU hAcKeR a PaThEtIc CrEaTuRe Of MeAt AnD bOnE"; + +export const HTTPMethods = Object.freeze({ + GET: "get", + POST: "post", + PUT: "put", + PATCH: "patch", + DELETE: "delete", +}); diff --git a/frontend/src/constants/pluginConst.js b/frontend/src/constants/pluginConst.js index d2b13c01..67a0e348 100644 --- a/frontend/src/constants/pluginConst.js +++ b/frontend/src/constants/pluginConst.js @@ -33,3 +33,12 @@ export const PluginConfigTypesNumeric = Object.freeze({ PARAMETER: "1", SECRET: "2", }); + +export const AllPluginSupportedTypes = Object.freeze({ + IP: "ip", + URL: "url", + DOMAIN: "domain", + HASH: "hash", + GENERIC: "generic", + FILE: "file", +}); diff --git a/frontend/src/layouts/AppHeader.jsx b/frontend/src/layouts/AppHeader.jsx index 57ef7c00..9dabe501 100644 --- a/frontend/src/layouts/AppHeader.jsx +++ b/frontend/src/layouts/AppHeader.jsx @@ -19,9 +19,10 @@ import { RiGuideLine, RiTwitterXFill, } from "react-icons/ri"; -import { FaGithub, FaGoogle, FaLinkedin } from "react-icons/fa"; +import { FaGithub, FaGoogle, FaLinkedin, FaList } from "react-icons/fa"; import { IoSearch } from "react-icons/io5"; import { TbReport } from "react-icons/tb"; +import { BsPeopleFill, BsSliders } from "react-icons/bs"; // lib import { NavLink, AxiosLoadingBar } from "@certego/certego-ui"; @@ -39,37 +40,7 @@ import UserMenu from "./widgets/UserMenu"; import NotificationPopoverButton from "../components/jobs/notification/NotificationPopoverButton"; import { useAuthStore } from "../stores/useAuthStore"; import { useGuideContext } from "../contexts/GuideContext"; - -const authLinks = ( - <> - - - - - 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 */} -