diff --git a/.cloudignore b/.cloudignore new file mode 100644 index 0000000..bc4dd39 --- /dev/null +++ b/.cloudignore @@ -0,0 +1,342 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,go,macos,terraform,node,angular,react +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,go,macos,terraform,node,angular,react + +secrets/ + +### Angular ### +## Angular ## +# compiled output +dist/ +tmp/ +app/**/*.js +app/**/*.js.map + +# dependencies +node_modules/ +bower_components/ + +# IDEs and editors +.idea/ + +# misc +.sass-cache/ +connect.lock/ +coverage/ +libpeerconnection.log/ +npm-debug.log +testem.log +typings/ +.angular/ + +# e2e +e2e/*.js +e2e/*.map + +# System Files +.DS_Store/ + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### Go Patch ### +/vendor/ +/Godeps/ + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local +.env.fake + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### react ### +.DS_* +**/*.backup.* +**/*.back.* + +node_modules + +*.sublime* + +psd +thumb +sketch + +### Terraform ### +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +.vscode/*.code-snippets + +# Ignore code-workspaces +*.code-workspace + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,go,macos,terraform,node,angular,react + +# readd removed values +# Snyk cache. +.dccache +# terraform plan +tf.plan +.vscode/launch.json +src/storage/main.go +gitlab_sa.key + +# Cypress +cypress/screenshots +cypress/videos + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Test certificates +src/test/certs/* + +# Nix Shell +/shell.nix + +# Linter output +modron-lint.xml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ca809ac --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +**/node_modules +**/.angular +/.git +/terraform \ No newline at end of file diff --git a/.git-cc.yaml b/.git-cc.yaml new file mode 100644 index 0000000..4c48cde --- /dev/null +++ b/.git-cc.yaml @@ -0,0 +1,11 @@ +scopes: + collector: Collector changes + docker: Docker related changes + nagatha: Nagatha related changes + otel: OpenTelemetry related changes + rules: Changes to the rules + scc: Security Command Center related changes + server: Changes to the Modron backend + storage: Changes to the storage layer + terraform: Terraform related changes + ui: UI changes diff --git a/.gitignore b/.gitignore index 68188c5..bc4dd39 100644 --- a/.gitignore +++ b/.gitignore @@ -175,6 +175,7 @@ web_modules/ .env.test.local .env.production.local .env.local +.env.fake # parcel-bundler cache (https://parceljs.org/) .cache @@ -308,7 +309,6 @@ terraform.rc .dccache # terraform plan tf.plan -src/storage/bigquerystorage/bqsa_key.json .vscode/launch.json src/storage/main.go gitlab_sa.key @@ -331,3 +331,12 @@ cypress/videos *.launch .settings/ *.sublime-workspace + +# Test certificates +src/test/certs/* + +# Nix Shell +/shell.nix + +# Linter output +modron-lint.xml diff --git a/.golangci.yml b/.golangci.yml index 1c67b1c..2ea66a9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,18 +1,34 @@ linters: disable-all: true enable: - - deadcode + - contextcheck - errcheck + - errorlint + - gocritic + - mnd + - gosec - gosimple - govet - ineffassign + - misspell + - nestif + - nilerr + - nilnil + - revive - staticcheck - - structcheck - typecheck - unused - - varcheck + - wastedassign +issues: + exclude-dirs: + - "terraform" run: timeout: 5m - skip-dirs: - - "terraform" + +output: + formats: + - format: colored-line-number + path: stdout + - format: junit-xml + path: modron-lint.xml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..66916f6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,65 @@ +# Changelog + +## v1.0.0 + +### Structure + +- Support Resource Group Hierarchy + +### Observations + +- Add [Risk Score](docs/RISK_SCORE.md) to observations, calculated from the severity of the observation (as defined in the rule) and the impact of the observation (detected from the environment) +- Collectors can now collect observations + +### Stats + +- Improved stats view +- Improved export to CSV + +### GCP + +- Add support for [Security Command Center (SCC)](https://cloud.google.com/security-command-center/docs/concepts-security-command-center-overview) +- Start collecting Kubernetes resources + +### Storage + +- Use [GORM](https://gorm.io/) for both the PSQL and SQLite storage backends +- Use SQLite for the in-memory database for testing + +### Performance + +- Increase performance overall by optimizing the DB queries, parallelizing the scans, and reducing the number of external calls +- Introduce rate limiting for the collectors + +### Observability + +- Use [logrus](https://github.com/sirupsen/logrus) with structured logging for GCP Logging (Stackdriver) +- Add support for OpenTelemetry + - Add an otel-collector to receive traces and metrics + - Send traces to [Google Cloud Trace](https://cloud.google.com/trace) + - Send metrics to [Google Cloud Monitoring](https://cloud.google.com/monitoring) + +### UI + +- Completely rework the UI with an improved design +- Show observations as a table, sorted by Risk Score by default +- Add a detailed view dialog for the observations + +### Misc + +- Use [`go-arg`](https://github.com/alexflint/go-arg) for the CLI arguments / environment variables +- Switch to [buf](https://buf.build/) for the protobuf generation +- Bug fixes +- Upgrade to Go 1.23 +- Rules now support external configuration + +## v0.2 + +- Moved to go 1.19 +- Added automated runs for scans +- Fixed issue where last reported observation would still appear even if newer scans reported no observations +- Fixed group member ship resolution when checking for accesses to GCP projects + +## v0.1 + +- Initial public release diff --git a/README.md b/README.md index 2155a0d..069e592 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,143 @@ # Modron - Cloud security compliance -![Modron logo](images/modron.png) +

+Modron Logo +

-``` -” -We are the ultimate law. All other law is tainted when compared to us. -We are order. All other order disappears when held to our light. -We are structure. All other structure crumbles when brought against us. -We are perfect law. -” -— A spokesmodron -``` +> _We are the ultimate law. All other law is tainted when compared to us. +> We are order. All other order disappears when held to our light. +> We are structure. All other structure crumbles when brought against us. +> We are perfect law._ +> +> — A spokesmodron + +Monte Cook, Colin McComb (1997-10-28). The Great Modron March. Edited by Michele Carter. (TSR, Inc.), p. 26. ISBN +0-7869-0648-0. -Monte Cook, Colin McComb (1997-10-28). The Great Modron March. Edited by Michele Carter. (TSR, Inc.), p. 26. ISBN 0-7869-0648-0. -The rise of cloud computing has sharply increased the number of resources that need to be managed in a production environment. This has increased the load on security teams. At the same time, vulnerability and compliance scanning on the cloud have made little progress. The process of inventory, data collection, analysis and remediation have scaled up, but did not evolve to manage the scale and diversity of cloud computing assets. Numerous security tools still assume that maintaining inventory, collecting data, looking at results and fixing issues is performed by the same person. This leads to increased pressure on teams already overwhelmed by the size of their infrastructure. +## Introduction -Maintaining a secure cloud infrastructure is surprisingly hard. Cloud computing came with the promise of automation and ease of use, yet there is a lot of progress to be made on both of these fronts. Infrastructure security also suffered from the explosion of assets under management and lack of security controls on new and existing assets. +Modron is a cloud security compliance tool. It is designed to help organizations manage their cloud infrastructure +and ensure that it is compliant with their security policies. -Modron addresses the inventory and ownership issues raising with large cloud infrastructure, as well as the scalability of the remediation process by resolving ownership of assets and handling communication with different asset owners. -Modron still has the security practitioners and leadership teams in mind and provides organization wide statistics about the reported issues. +Users can navigate the Modron UI and view their resource groups, together with the respective observations. +Resource Groups that require attention are immediately visible and users can dig deeper to assess the observations. -Designed with multi cloud and scalability in mind, Modron is based on GCP today. The model allows for writing detection rules once and apply them across multiple platforms. +Resource Group Observations -## Taxonomy +A detailed explanation of why Modron was created can be read on [the original blog post](https://nianticlabs.com/news/modron). + +## Problem Statement + +The rise of cloud computing has sharply increased the number of resources that need to be managed in a production +environment. This has increased the load on security teams. At the same time, vulnerability and compliance scanning on +the cloud have made little progress. The process of inventory, data collection, analysis and remediation have scaled up, +but did not evolve to manage the scale and diversity of cloud computing assets. Numerous security tools still assume +that maintaining inventory, collecting data, looking at results and fixing issues is performed by the same person. This +leads to increased pressure on teams already overwhelmed by the size of their infrastructure. + +Maintaining a secure cloud infrastructure is surprisingly hard. Cloud computing came with the promise of automation and +ease of use, yet there is a lot of progress to be made on both of these fronts. Infrastructure security also suffered +from the explosion of assets under management and lack of security controls on new and existing assets. + +Modron addresses the inventory and ownership issues raising with large cloud infrastructure, as well as the scalability +of the remediation process by resolving ownership of assets and handling communication with different asset owners. +Modron still has the security practitioners and leadership teams in mind and provides organization wide statistics about +the reported issues. + +Designed with multi cloud and scalability in mind, Modron is based on GCP today. The model allows for writing detection +rules once and apply them across multiple platforms. + +## The Modron solution + +With the help of Modron, organizations can: +- Automatically collect data from their cloud infrastructure +- Run security rules against the collected data +- Notify the owners of the resources that are not compliant with the security rules +- Provide a personalized dashboard to visualize the compliance status of the organization +- Provide engineers information on how to remediate the issues + +### Analyzing the Resource Group observations + +Through the Modron UI, users can view a list of their resource groups. By clicking on a resource group, they can +see a list of observations that have been made against that resource group: + +Resource Group Observations + +This view provides a list of observations for the resource group. Each observation has an associated "Risk Score" that +is computed taking into consideration the severity of the rule that generated the observation and the environment in +which the resource group is running. This allows users to prioritize their remediation efforts. + +### Expanding a single observation + +By expanding a single observation, users can see more details about it - including remediation steps. + +Single Observation + +When available, a command is provided to enable the user to quickly remediate the issue. + +### Risk Score + +Each observation has an associated Risk Score. This score is computed based on the severity of the rule that generated the +observation (Severity) and the environment in which the resource group is running (Impact). + +Risk Scores range from *INFO* to *CRITICAL* (slightly adapted from the [CVSS v3.x Ratings](https://nvd.nist.gov/vuln-metrics/cvss)). +By also analyzing the impact of the observation, the risk score can be used to prioritize remediation efforts: an +observation in an environment containing customer data (e.g: production) will be considered more critical than the same +finding in an environment containing only test data (e.g: dev). + +Risk Score computation -A *Resource* is an entity existing in the cloud platform. A resource can be a VM instance, a service account, a kubernetes clusters, etc. +The details on how we compute and define the Risk Score are available in the [Risk Score documentation](docs/RISK_SCORE.md). -A *Resource group* is the smallest administrative grouping of resources, usually administered by the same individual or group of individuals. On GCP this corresponds to a Project, on Azure to a Resource Group. +### Statistics -A *Rule* is the implementation of a desired state for a given resource or set of resources. A rule applies only to a predefined set of resources, compares the state of the resource with the expected state. If these states differ, the rule generates one or more *observation*. +Via the statistics page, users can view a list of all the rules that have been run against their resource groups, +together with the results. Each rule can be exported to a CSV file for further analysis. -An *Observation* is an instance of a difference between the state of a resource and its expected state at a given timestamp. +Statistics -A *Collection* defines the action of fetching all data from the cloud platforms. This data is then stored in the database, ready to run the scan. +By expanding the "List of the observations", users can see all the matching observations for that rule, regardless of +the resource group they belong to. +This tool can be used by security teams to understand the impact of a rule across the organization, and which +issues to tackle first. -A *Scan* defines the action of running a set of *rules* against a set of *resource groups*. *Observations* resulting of that scan are added to the database. There is no guarantee that all observations of the same scan will have the same timestamp. +### Notifications -*Nagatha* is the notification system associated with Modron. It aggregates notifications going to the same recipient over a given time frame and sends a notification to that user. +A remediation cannot be effective if the right people are not informed. +Modron sends notifications to the owners of the resource groups that have observations via [Nagatha](https://github.com/nianticlabs/nagatha). -A *Notification* is an instance of a message sent to an owner of a *resource group* for a given *observation*. +Users will receive periodically a notification with the list of observations that they need to address. Nagatha +will take care of delivering the notification via Slack and Email: -An *Exception* is one owner of a *resource group* opting out of *notifications* for a specific *rule*. Exceptions *must* have an expiration date and cannot be set forever. This limitation can be bypassed by accessing the nagatha service directly. + + + + + + + + + +
+ Notification + + Notification +

Email Notification

Slack Notification

+ +## Taxonomy + +| Term | Definition | +|:---------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Resource | An entity existing in the cloud platform. A resource can be a VM instance, a service account, a kubernetes clusters, etc. | +| Resource group | The smallest administrative grouping of resources, usually administered by the same individual or group of individuals. On GCP this corresponds to a Project, on Azure to a Resource Group. | +| Rule | The implementation of a desired state for a given resource or set of resources. A rule applies only to a predefined set of resources, compares the state of the resource with the expected state. If these states differ, the rule generates one or more *observation*s. | +| Observation | Instance of a difference between the state of a resource and its expected state at a given timestamp. | +| Collection | The action of fetching all data from the cloud platforms. This data is then stored in the database, ready to run the scan. | +| Scan | The action of running a set of *rules* against a set of *resource groups*. *Observations* resulting of that scan are added to the database. There is no guarantee that all observations of the same scan will have the same timestamp. | +| Nagatha | The notification system associated with Modron. It aggregates notifications going to the same recipient over a given time frame and sends a notification to that user. | +| Notification | An instance of a message sent to an owner of a *resource group* for a given *observation*. | +| Exception | An owner of a *resource group* opting out of *notifications* for a specific *rule*. Exceptions *must* have an expiration date and cannot be set forever. This limitation can be bypassed by accessing the Nagaatha service directly. | ## Process @@ -50,90 +146,178 @@ Modron follows the process of any security scanning engine: ![Scanning process](images/scan_process.png) Except that in most scanning engines, the inventory and remediation parts are left as an exercise for the user. -In Modron, inventory is taken care of by identifying automatically the owners of a resource group based on the people that have the permission to act on it, as the remediation is largely facilitated by running the communication with the different resource group owners. - -* *Collector*: The collector fetches the data from the cloud platforms. This code must be implemented for each supported code platform separately. It takes care of the inventory and data collection parts of the process. -* *Rule engine*: The rule engine runs the rules against all collected resources and generates observations. Notifications are sent to Nagatha for each observation. -* *Nagatha* receives all the notifications for all observations, aggregates, deduplicates and limits the rate of notification. It also applies the exceptions provided by the user. +In Modron, inventory is taken care of by identifying automatically the owners of a resource group based on the people +that have the permission to act on it, as the remediation is largely facilitated by running the communication with the +different resource group owners. + +* *Collector*: The collector fetches the data from the cloud platforms. This code must be implemented for each supported + code platform separately. It takes care of the inventory and data collection parts of the process. +* *Rule engine*: The rule engine runs the rules against all collected resources and generates observations. + Notifications are sent to Nagatha for each observation. +* *Nagatha* receives all the notifications for all observations, aggregates, deduplicates and limits the rate of + notification. It also applies the exceptions provided by the user. + +## Architecture + +### GCP + +

+ +

+ +### Modron + +```mermaid +flowchart LR + User --> ui + ui[Modron UI] + ui --> Modron + subgraph Modron + coll[Collector] + acl[ACL Fetcher] + re[Rule Engine] + re -->|Check rule| re + end + + re -->|Create Observation| psql + coll --->|fetch| gcp + acl --->|fetch| gcp + coll --->|store| psql + gcp[GCP APIs] + psql[(PSQL)] + re --->|Fetch resources| psql + re --->|Create Notification| Nagatha + slack(Slack) + email(Email) + Nagatha --> slack + Nagatha --> email +``` ## Getting started In order to install Modron & Nagatha, you'll need to: -1. Build the modron images: - * in [src/](src): `gcloud builds builds submit . --tag gcr.io/your-project/modron:prod --timeout 900` - * in [src/ui/](src/ui): `gcloud builds builds submit . --tag gcr.io/your-project/modron-ui:prod --timeout 900` - * in [nagatha/](nagatha): `gcloud builds submit . --tag gcr.io/your-project/nagatha:dev --timeout=900` +1. Build the modron images following [Building the images](#building-the-images) below. 1. Create a copy of [main.tf.example](terraform/dev/main.tf.example) and edit it with your own configuration 1. Run `tf plan --out tf.plan` in the [dev folder](terraform/dev/) * This could need multiple occurrences as setting up resources on GCP takes time. -1. Create a copy of [tf.tfvars.json.example](nagatha/terraform/tf.tfvars.json.example) and edit it with your own configuration +1. Create a copy of [tf.tfvars.json.example](nagatha/terraform/tf.tfvars.json.example) and edit it with your own + configuration 1. Run `tf plan --out tf.plan` in the [nagatha folder](nagatha/terraform/) 1. Assign the permissions to the Modron runner as mentioned in [permissions](#permissions) -## Logo +### Building the images -Generated with Dall-E with "logo art of a victorian cubical robot in a tuxedo with a top hat and holding binoculars" +This assumes you have an Artifact registry in your GCP project following this format: +``` +us-central1-docker.pkg.dev/$PROJECT_ID/modron +``` -## Start developing on Modron +#### Modron Backend -### Infrastructure +The Modron backend image can be built using [Cloud Build](https://cloud.google.com/build/docs): +```bash +gcloud \ + --project "$PROJECT_ID" \ + builds submit \ + --config cloudbuild.yaml \ + --substitutions=_TAG_REF_1=dev-$(date +%s),_TAG_REF_2=dev +``` -Here is an overview of Modron's infrastructure: +#### Modron UI -![Modron infrastructure](images/infrastructure.png) +```bash +gcloud \ + --project "$PROJECT_ID" \ + builds submit \ + --config cloudbuild-ui.yaml \ + --substitutions=_TAG_REF_1=dev-$(date +%s),_TAG_REF_2=dev +``` -Both Modron Cloud Run run in the same [modron project](https://console.cloud.google.com/home/dashboard?project=modron). -Nagatha runs in a [separate project](https://console.cloud.google.com/home/dashboard?project=nagatha). -There is a dev container. This project is meant to be opened with [VSCode](https://code.visualstudio.com/). +## Development of Modron + +### Requirements To run this project you'll need: * Docker * Go * The Google SDK -* A protobuf compiler * npm +* terraform -The dev container provides these tools. Upon starting, vscode will ask if you want to reopen the project in the dev container, accept. +### Getting started -If you have problems with your git configuration inside the container, set `remote.containers.copyGitConfig` to true. -https://github.com/microsoft/vscode-remote-release/issues/6124 +#### Generate the protobuf files -## Permissions +We'll use a Docker image that contains [`buf`](https://buf.build/) and some protoc plugins to generate the protobuf +files. +We'll call this image `bufbuild` - it needs to be built only once: -The Modron service is meant to work at the organization level on GCP. In order to access the data it needs to run the analysis, the Modron runner service account will need the following permissions at the organization level: +```bash +docker build -t bufbuild -f docker/Dockerfile.buf . +``` + +Now, this image can be used to generate the protobuf files: +```bash +docker run -v "$PWD:/app" -w "/app" bufbuild generate ``` - "apikeys.keys.list", - "cloudasset.assets.searchAllIamPolicies", - "compute.backendServices.list", - "compute.instances.list", - "compute.regions.list", - "compute.sslCertificates.list", - "compute.sslPolicies.list", - "compute.subnetworks.list", - "compute.targetHttpsProxies.list", - "compute.targetHttpsProxies.list", - "compute.targetSslProxies.list", - "compute.urlMaps.list", - "compute.zones.list", - "container.clusters.list", - "iam.serviceAccounts.list", - "iam.serviceAccountKeys.list", - "monitoring.metricDescriptors.get", - "monitoring.metricDescriptors.list", - "monitoring.timeSeries.list", - "resourcemanager.projects.getIamPolicy", - "serviceusage.services.get", - "storage.buckets.list", - "storage.buckets.getIamPolicy", + + + We don't use the buf plugins because we might encounter some + rate limits + + +### Formatting + +You can format your code using: +- `gofmt -w ./` +- `terraform fmt -recursive` +- `eslint` + +#### IDE + +You can configure your IDE to format Terraform code by following these guides: +- [Use Terraform formatter on IDEA-based IDEs](https://www.jetbrains.com/help/idea/terraform.html#use-terraform-formatter) +- [Terraform extension for VSCode - Formatting](https://marketplace.visualstudio.com/items?itemName=hashicorp.terraform#formatting) + +## Permissions + +The Modron service is meant to work at the organization level on GCP. In order to access the data it needs to run the +analysis, the Modron runner service account will need the following permissions at the organization level: + +```plain +apikeys.keys.list +cloudasset.assets.searchAllIamPolicies +compute.backendServices.list +compute.instances.list +compute.regions.list +compute.sslCertificates.list +compute.sslPolicies.list +compute.subnetworks.list +compute.targetHttpsProxies.list +compute.targetHttpsProxies.list +compute.targetSslProxies.list +compute.urlMaps.list +compute.zones.list +container.clusters.list +iam.serviceAccounts.list +iam.serviceAccountKeys.list +iam.serviceAccounts.getIamPolicy +monitoring.metricDescriptors.get +monitoring.metricDescriptors.list +monitoring.timeSeries.list +resourcemanager.projects.getIamPolicy +serviceusage.services.get +storage.buckets.list +storage.buckets.getIamPolicy ``` It is recommended to create a custom role with these permissions. For that you can use this terraform stanza: -``` +```hcl resource "google_organization_iam_custom_role" "modron_lister" { org_id = var.org_id role_id = "ModronSecurityLister" @@ -169,6 +353,8 @@ resource "google_organization_iam_custom_role" "modron_lister" { ## Debug +### GoSec + Run gosec as run by gitlab: ``` @@ -192,36 +378,82 @@ To run the integration test, you'll need a self signed certificate for the notif ``` openssl req -x509 -newkey rsa:4096 -keyout key.pem -nodes -out cert.pem -sha256 -days 365 -subj '/CN=modron_test' -addext "subjectAltName = DNS:modron_test" -docker-compose up --build --exit-code-from "modron_test" --abort-on-container-exit +docker compose up --build --exit-code-from "modron_test" --abort-on-container-exit ``` ### UI Integration test ``` -docker-compose -f docker-compose.ui.yaml up --build --exit-code-from "modron_test" --abort-on-container-exit +docker compose -f docker-compose.ui.yaml up --build --exit-code-from "modron_test" --abort-on-container-exit ``` ### Running locally +#### Log in to GCP + +In order to use the Google Cloud APIs, you need to log in to GCP as if you were using a service account: + +```bash +gcloud auth application-default login +``` + +Check out the [`gcloud` docs](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login) for more +information. +If you don't log in using the above command, the collector might fail with an error similar to: + +```plain +"invalid_grant" "reauth related error (invalid_rapt)" "https://support.google.com/a/answer/9368756" +``` + +#### Start the docker-compose stack + Use this docker command to spin up a local deployment via docker-compose (will rebuild on every run): + ``` -docker-compose -f docker-compose.ui.yaml up --build +docker compose -f docker-compose.ui.yaml up --build ``` -In case you want to clean up all the created images, services and volumes (e.g. if you suspect a caching issue or if a service does not properly shut down): + +In case you want to clean up all the created images, services and volumes (e.g. if you suspect a caching issue or if a +service does not properly shut down): + ``` -docker-compose rm -fsv # remove all images, services and volumes if needed +docker compose rm -fsv # remove all images, services and volumes if needed ``` +#### Use Docker by itself -Alternative: Use the docker command to run modron locally (against a dev project): +As an alternative you can use the following docker command to run Modron locally (against a dev project): ``` chmod 644 ~/.config/gcloud/application_default_credentials.json docker build -f Dockerfile.db -t modron-db:latest . -docker run -e POSTGRES_PASSWORD="docker-test-password" -e POSTGRES_USER="modron" -e POSTGRES_DB="modron" -e PG_DATA="tmp_data/" -t modron-db:latest -p 5432 -GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/application_default_credentials.json PORT="8080" GCP_PROJECT_ID=modron-dev OPERATION_TABLE_ID="operations" OBSERVATION_TABLE_ID="observations" RESOURCE_TABLE_ID="resources" RUN_AUTOMATED_SCANS="false" ORG_SUFFIX="@example.com" STORAGE="SQL" DB_MAX_CONNECTIONS="1" SQL_BACKEND_DRIVER="postgres" SQL_CONNECT_STRING="host=localhost port=5432 user=modron password=docker-test-password database=modron sslmode=disable" go run . --logtostderr +docker run -e POSTGRES_PASSWORD="docker-test-password" -e POSTGRES_USER="modron" -e POSTGRES_DB="modron" -e PG_DATA="tmp_data/" -t postgres:latest -p 5432 +GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/application_default_credentials.json PORT="8080" RUN_AUTOMATED_SCANS="false" ORG_SUFFIX="@nianticlabs.com" STORAGE="SQL" DB_MAX_CONNECTIONS="1" SQL_BACKEND_DRIVER="postgres" SQL_CONNECT_STRING="host=localhost port=5432 user=modron password=docker-test-password database=modron sslmode=disable" go run . ``` +## Telemetry + +Modron supports [OpenTelemetry](https://opentelemetry.io/docs/) and expects a GRPC OTEL collector to be running +alongside the deployment. We currently export traces and metrics through this collector. + +The collector (`otel-collector`) can be configured to forward the telemetry data to other exporters - by default +these are Google Cloud Monitoring for the production environment and Prometheus / Jaeger for the local deployment. + +### Checking the telemetry data locally + +When running Modron locally, we suggest to start the auxiliary services by running: + +```bash +docker-compose -f docker-compose.dev.yaml up -d +``` + +This will start everything you need to get started to develop locally for Modron: + +- [Jaeger](http://127.0.0.1:16686/) +- [Prometheus](http://127.0.0.1:9090/) +- `otel-collector` running on `127.0.0.1:4317` (GRPC) +- PostgreSQL running on `127.0.0.1:5432` + ## Future developments * Provide an historical view of the reported issues. @@ -231,4 +463,4 @@ GOOGLE_APPLICATION_CREDENTIALS=~/.config/gcloud/application_default_credentials. ## Security -Report any security issue to [security@example.com](mailto:security@example.com). +Report any security issue to [security@nianticlabs.com](mailto:security@nianticlabs.com). diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..7f7b406 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,30 @@ +version: v2 +managed: + enabled: true + +plugins: + - local: protoc-gen-go + out: src/proto/generated + - local: protoc-gen-go-grpc + out: src/proto/generated + - local: protoc-gen-js + out: src/ui/client/src/proto/ + opt: import_style=commonjs,binary + - local: protoc-gen-grpc-web + out: src/ui/client/src/proto/ + opt: + - import_style=typescript + - mode=grpcweb + +inputs: + - directory: ./src/proto + - directory: ./src/nagatha/proto + - module: buf.build/googleapis/googleapis:8bc2c51e08c447cd8886cdea48a73e14 + paths: + - google/api + - google/rpc + - google/longrunning + - module: buf.build/k8s/api:8f68e41b943c4de8a5e9c9a921c889a7 + paths: + - k8s.io/api/core + - k8s.io/apimachinery/ \ No newline at end of file diff --git a/buf.lock b/buf.lock new file mode 100644 index 0000000..5373d39 --- /dev/null +++ b/buf.lock @@ -0,0 +1,9 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/googleapis/googleapis + commit: 8bc2c51e08c447cd8886cdea48a73e14 + digest: b5:b7e0ac9d192bd0eae88160101269550281448c51f25121cd0d51957661a350aab07001bc145fe9029a8da10b99ff000ae5b284ecaca9c75f2a99604a04d9b4ab + - name: buf.build/k8s/api + commit: 8f68e41b943c4de8a5e9c9a921c889a7 + digest: b5:0c188e351df7b094d6a5412f4cd5f097fbf1a32d4a2d4c42b83774e168961447e6e706e4ebf241a13b94493aa6cbe08dc8abd03e2a1f8207ac7620bf186030c8 diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..f0a19ec --- /dev/null +++ b/buf.yaml @@ -0,0 +1,10 @@ +version: v2 +modules: + - path: src/proto + - path: src/nagatha/proto +deps: + - buf.build/googleapis/googleapis + - buf.build/k8s/api +lint: + use: + - DEFAULT \ No newline at end of file diff --git a/cloudbuild-ui.yaml b/cloudbuild-ui.yaml new file mode 100644 index 0000000..3683f49 --- /dev/null +++ b/cloudbuild-ui.yaml @@ -0,0 +1,12 @@ +steps: + - name: 'gcr.io/cloud-builders/docker' + args: + - build + - --tag=us-central1-docker.pkg.dev/$PROJECT_ID/modron/modron-ui:$_TAG_REF_1 + - --tag=us-central1-docker.pkg.dev/$PROJECT_ID/modron/modron-ui:$_TAG_REF_2 + - -f + - ./src/ui/Dockerfile + - . +images: + - "us-central1-docker.pkg.dev/$PROJECT_ID/modron/modron-ui:$_TAG_REF_1" + - "us-central1-docker.pkg.dev/$PROJECT_ID/modron/modron-ui:$_TAG_REF_2" diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..e30b208 --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,12 @@ +steps: + - name: 'gcr.io/cloud-builders/docker' + args: + - build + - --tag=us-central1-docker.pkg.dev/$PROJECT_ID/modron/modron:$_TAG_REF_1 + - --tag=us-central1-docker.pkg.dev/$PROJECT_ID/modron/modron:$_TAG_REF_2 + - -f + - ./src/Dockerfile + - . +images: + - "us-central1-docker.pkg.dev/$PROJECT_ID/modron/modron:$_TAG_REF_1" + - "us-central1-docker.pkg.dev/$PROJECT_ID/modron/modron:$_TAG_REF_2" \ No newline at end of file diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 0000000..8a2aa9e --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,53 @@ +version: '3' + +services: + postgres_db: + container_name: postgres_db + image: postgres:16 + restart: always + environment: + POSTGRES_USER: "modron" + POSTGRES_PASSWORD: "modron" + POSTGRES_DB: "modron" + PGDATA: "/tmp/" + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U modron"] + interval: 1s + timeout: 2s + retries: 5 + tmpfs: + - /tmp + + jaeger: + image: jaegertracing/all-in-one:1.59 + ports: + - "16686:16686" + environment: + COLLECTOR_OTLP_ENABLED: true + COLLECTOR_OTLP_GRPC_HOST_PORT: 0.0.0.0:4317 + networks: + - otel + + prometheus: + image: prom/prometheus:latest + command: + - --web.enable-remote-write-receiver + ports: + - "9090:9090" + networks: + - otel + + otel-collector: + image: otel/opentelemetry-collector:0.108.0 + command: + - --config=/etc/otel/config.yaml + ports: + - "4317:4317" + volumes: + - ./otel/config:/etc/otel + networks: + - otel +networks: + otel: {} diff --git a/docker-compose.ui.yaml b/docker-compose.ui.yaml index 2149ee9..aa5b14f 100644 --- a/docker-compose.ui.yaml +++ b/docker-compose.ui.yaml @@ -1,5 +1,3 @@ -version: '3' - services: modron_proxy: container_name: modron_proxy @@ -16,30 +14,35 @@ services: modron_fake: container_name: modron_fake - build: src/ + build: + context: . + dockerfile: src/Dockerfile environment: RUN_AUTOMATED_SCANS: "false" COLLECTOR: "FAKE" DB_MAX_CONNECTIONS: "1" GRPC_TRACE: "all" GRPC_VERBOSITY: "DEBUG" - OBSERVATION_TABLE_ID: "observations" - OPERATION_TABLE_ID: "operations" - ORG_ID: "0123456789" + LISTEN_ADDR: "0.0.0.0" + ORG_ID: "111111111111" ORG_SUFFIX: "@example.com" PORT: 8080 - RESOURCE_TABLE_ID: "resources" SQL_BACKEND_DRIVER: "postgres" SQL_CONNECT_STRING: "host=postgres_db port=5432 user=modron password=docker-test-password database=modron sslmode=disable" STORAGE: "SQL" + TAG_CUSTOMER_DATA: 111111111111/customer_data + TAG_EMPLOYEE_DATA: 111111111111/employee_data + TAG_ENVIRONMENT: 111111111111/environment networks: - modron depends_on: - - postgres_db + postgres_db: + condition: service_healthy modron_ui: container_name: modron_ui - build: ./src/ui + build: + dockerfile: src/ui/Dockerfile environment: ENVIRONMENT: "E2E_TESTING" DIST_PATH: "./ui" @@ -50,8 +53,8 @@ services: modron_test: container_name: modron_test build: - context: ./src/ui/client - dockerfile: Dockerfile.e2e + context: . + dockerfile: ./src/ui/client/Dockerfile.e2e depends_on: - modron_proxy environment: @@ -68,15 +71,18 @@ services: postgres_db: container_name: postgres_db - build: - context: src/ - dockerfile: Dockerfile.db + image: postgres:14-bookworm restart: always environment: POSTGRES_USER: "modron" POSTGRES_PASSWORD: "docker-test-password" POSTGRES_DB: "modron" PGDATA: "/tmp/" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U modron"] + interval: 1s + timeout: 2s + retries: 5 tmpfs: - /tmp networks: diff --git a/docker-compose.yaml b/docker-compose.yaml index 594ddb0..c4f7258 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,17 +1,20 @@ -version: '3' - services: postgres_db: container_name: postgres_db - build: - context: src/ - dockerfile: Dockerfile.db + image: postgres:14-bookworm restart: always + ports: + - "5432:5432" environment: POSTGRES_USER: "modron" POSTGRES_PASSWORD: "docker-test-password" POSTGRES_DB: "modron" PGDATA: "/tmp/" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U modron"] + interval: 1s + timeout: 2s + retries: 5 tmpfs: - /tmp networks: @@ -19,36 +22,39 @@ services: modron_fake: container_name: modron_fake - build: src/ + build: + context: . + dockerfile: src/Dockerfile environment: COLLECTOR: "FAKE" DB_BATCH_SIZE: "1" DB_MAX_CONNECTIONS: "1" - ENVIRONMENT: "E2E_GRPC_TESTING" - GLOG_v: "10" + IS_E2E_GRPC_TEST: "true" + LISTEN_ADDR: "0.0.0.0" NOTIFICATION_SERVICE: "modron_test:8082" - OBSERVATION_TABLE_ID: "observations" - OPERATION_TABLE_ID: "operations" - ORG_ID: "0123456789" + ORG_ID: "111111111111" ORG_SUFFIX: "@example.com" PORT: 8081 - RESOURCE_TABLE_ID: "resources" RUN_AUTOMATED_SCANS: "false" SQL_BACKEND_DRIVER: "postgres" SQL_CONNECT_STRING: "host=postgres_db port=5432 user=modron password=docker-test-password database=modron sslmode=disable" STORAGE: "SQL" + TAG_CUSTOMER_DATA: 111111111111/customer_data + TAG_EMPLOYEE_DATA: 111111111111/employee_data + TAG_ENVIRONMENT: 111111111111/environment ports: - "8081:8081" networks: - modron depends_on: - - postgres_db + postgres_db: + condition: service_healthy modron_test: container_name: e2e_test build: - context: src/ - dockerfile: Dockerfile.e2e + context: . + dockerfile: src/Dockerfile.e2e environment: BACKEND_ADDRESS: "modron:8080" FAKE_BACKEND_ADDRESS: "modron_fake:8081" diff --git a/docker/Dockerfile.buf b/docker/Dockerfile.buf new file mode 100644 index 0000000..626f41f --- /dev/null +++ b/docker/Dockerfile.buf @@ -0,0 +1,51 @@ +FROM ubuntu:24.04 +ARG BUF_VERSION="1.46.0" +ARG BUF_MINISIG_PUBKEY="RWQ/i9xseZwBVE7pEniCNjlNOeeyp4BQgdZDLQcAohxEAH5Uj5DEKjv6" +ARG PROTOBUF_JS_VERSION="3.21.4" +ARG GRPC_WEB_VERSION="1.5.0" +ARG GRPC_GATEWAY_VERSION="2.23.0" + +RUN apt-get update && \ + apt-get install -y \ + protoc-gen-go \ + protoc-gen-go-grpc \ + curl \ + wget \ + minisign \ + perl +WORKDIR /build + +# buf +RUN wget -q "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-$(uname -s)-$(uname -m)" && \ + wget -q "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/sha256.txt" && \ + wget -q "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/sha256.txt.minisig" && \ + minisign -Vm sha256.txt -P "$BUF_MINISIG_PUBKEY" && \ + shasum -a 256 -c sha256.txt --ignore-missing && \ + mv "buf-$(uname -s)-$(uname -m)" /usr/local/bin/buf && \ + chmod +x /usr/local/bin/buf && \ + rm * + +RUN bash -c "ARCH=$(dpkg --print-architecture); if [ \"\$ARCH\" = \"arm64\" ]; then ARCH=\"aarch_64\"; fi; echo -n \$ARCH > /tmp/arch" + +# protobuf-javascript +RUN wget -q -O /tmp/protobuf-javascript.tar.gz "https://github.com/protocolbuffers/protobuf-javascript/releases/download/v${PROTOBUF_JS_VERSION}/protobuf-javascript-${PROTOBUF_JS_VERSION}-$(uname -s | tr "[:upper:]" "[:lower:]")-$(cat /tmp/arch).tar.gz" && \ + mkdir /tmp/protobuf-javascript && \ + tar -xzvf /tmp/protobuf-javascript.tar.gz -C /tmp/protobuf-javascript && \ + mv /tmp/protobuf-javascript/bin/protoc-gen-js /usr/local/bin/protoc-gen-js && \ + rm -rf /tmp/protobuf-javascript + +# protoc-gen-grpc-web +RUN wget -q -O /usr/local/bin/protoc-gen-grpc-web "https://github.com/grpc/grpc-web/releases/download/${GRPC_WEB_VERSION}/protoc-gen-grpc-web-${GRPC_WEB_VERSION}-$(uname -s | tr "[:upper:]" "[:lower:]")-$(uname -m)" && \ + chmod +x /usr/local/bin/protoc-gen-grpc-web + +# protoc-gen-grpc-gateway +RUN wget -q -O /usr/local/bin/protoc-gen-grpc-gateway \ + "https://github.com/grpc-ecosystem/grpc-gateway/releases/download/v${GRPC_GATEWAY_VERSION}/protoc-gen-grpc-gateway-v${GRPC_GATEWAY_VERSION}-$(uname -s | tr "[:upper:]" "[:lower:]")-$(dpkg --print-architecture)" && \ + chmod a+x /usr/local/bin/protoc-gen-grpc-gateway + +# protoc-gen-openapiv2 +RUN wget -q -O /usr/local/bin/protoc-gen-openapiv2 \ + "https://github.com/grpc-ecosystem/grpc-gateway/releases/download/v${GRPC_GATEWAY_VERSION}/protoc-gen-openapiv2-v${GRPC_GATEWAY_VERSION}-$(uname -s | tr "[:upper:]" "[:lower:]")-$(dpkg --print-architecture)" && \ + chmod a+x /usr/local/bin/protoc-gen-openapiv2 + +ENTRYPOINT [ "buf" ] diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md new file mode 100644 index 0000000..178b55d --- /dev/null +++ b/docs/FINDINGS.md @@ -0,0 +1,165 @@ +# Findings + +## API_KEY_WITH_OVERBROAD_SCOPE +The API key is granting access to too many different scopes, or is not limited at all in what actions it allows. +A malicious actor in possession of this key would be able to make a lot of damages to your infrastructure. + +### Recommendation +Limit the scope of the API key to the smallest set of actions required by the user of this key to run properly. +A list of scope is available in the [Google documentation](https://developers.google.com/identity/protocols/oauth2/scopes). + +## BUCKET_IS_PUBLIC +A public bucket means that the content of this bucket is accessible to anybody on the internet. +Make sure that the content of this bucket is actually intended to be public. + +> [!WARNING] +> Do not assume that files with a cryptic name will never be found. These files will eventually be found. If files should stay private, then they should be hosted in a private bucket. + +### Recommendation +Make this bucket private + +OR + +Make sure that the content of this bucket is intended to be public + +## CLUSTER_NODES_HAVE_PUBLIC_IPS +On GCP, this means that you have a public cluster. There is no reason to have a public cluster today. Services that should be publicly accessible should be exposed using a load balancer. + +For Airflow and Dataflow clusters, there is an option to set when starting the flows to use private cluster. + +### Recommendation + +> [!NOTE] +> There is no way to transform a public cluster into a private one. + +1. Create a new private cluster matching the specifications of the existing one +2. Migrate your workloads to the new cluster +3. Delete the old public cluster. + +## CROSS_PROJECT_PERMISSIONS +The resource is controlled by an account defined in another project. +This circumvents the isolation provided by a project. + +### Recommendation +Use only accounts defined in the project to grant write and admin access to a resource. + +## DATABASE_ALLOWS_UNENCRYPTED_CONNECTIONS +All connections to a database should use an encrypted connection. No clear text communication between a workload and a database should be allowed. + +### Recommendation +Configure your database to allow only encrypted connections + +## DATABASE_AUTHORIZED_NETWORKS_NOT_SET +Anyone can connect to this database without limitations. + +### Recommendation +Add a list of IP or IP networks from which you expect connections and allow only connections from these networks. +This is also valid if your database is only available to internal IPs. + +## EXPORTED_KEY_EXPIRY_TOO_LONG +An exported key has been around for too long. + +Exported keys are immutable credentials that grant whoever has access to them a time unbounded access to our infrastructure. As people come and go, it is recommended to regularly rotate credentials to reduce the risk associated with leaks and malicious activity. + +### Recommendation + +- Rotate these credentials by deleting the existing one and creating a new one +- Create a process, possibly automated, to rotate credentials in the future and run this process regularly (every 3-6 months) + +## EXPORTED_KEY_WITH_ADMIN_PRIVILEGES +An exported key in that resource group grants administrative privileges. Read EXPORTED_KEY_EXPIRY_TOO_LONG and add to the risk the fact that these credentials grant access to deployments, databases and possibly user-data. + +### Recommendation + +1. Remove the admin privileges of that service account or create a new service account with limited privileges +2. Rotate the key after this has been done + +## HUMAN_WITH_OVERPRIVILEGED_BASIC_ROLE +A human user or a group has one of the following permissions at the project level: + +- Owner +- Editor +- Viewer +- Security Admin + +### Recommendation + +Use less privileged roles for humans. The principle of least privilege should be applied to all users. + +## LOAD_BALANCER_MIN_TLS_VERSION_TOO_OLD +The load balancer supports a deprecated TLS version. +The TLS version was deprecated because it supports broken cryptographic primitives. + +### Recommendation +Define an SSL Policy at the project level or for the load balancer specifically that with a minimum TLS version +of 1.2 and a MODERN or RESTRICTED profile. +See [defining an SSL policy](https://cloud.google.com/load-balancing/docs/ssl-policies-concepts#defining_an_ssl_policy) for more information. + +## LOAD_BALANCER_USER_MANAGED_CERTIFICATE +A load balancer has been found with a user generated certificate. + +User generated certificates have multiple risks: +- The cryptographic material associated with that load balancer must be manually managed, leaving the door open to: + - Certificate expiry if the certificates are not renewed in time + - Credentials leakage if anybody that has access to the cryptographic material is compromised or bad intended + +- The generation of the private key has usually been done on a private machine where + - the Pseudo Random Number Generator has not been verified + - the entropy might have been too low + +This weakens the encryption of the communication between the clients and the load balancer + +### Recommendation +Migrate to a Google Managed certificate. + +## KUBERNETES_VULNERABILITY_SCANNING_DISABLED + +The GKE cluster is not using the [workload vulnerability scanning feature](https://cloud.google.com/kubernetes-engine/docs/concepts/about-workload-vulnerability-scanning). +This means that container image vulnerabilities aren’t surfaced, or are only partially surfaced. + +Follow the steps in [Automatically scan workloads for known vulnerabilities](https://cloud.google.com/kubernetes-engine/docs/how-to/security-posture-vulnerability-scanning) +to enable it. Modron also suggests a command to run based on the name of your cluster and the project it is in. + +## MASTER_AUTHORIZED_NETWORKS_NOT_SET +The administration interface of your Kubernetes cluster is available anyone who has access to a Google IP, or anybody on the internet as anybody can create a VM on GCP. + +### Recommendation + +Restrict the access to the Kubernetes API to a list of IP or IP networks from which you expect connections. + +## OUTDATED_KUBERNETES_VERSION + +The version of Kubernetes running in that cluster is not supported anymore. +Running outdated software is the first source of compromise. +Running up-to-date software is the first barrier of defence against known vulnerabilities. + +### Recommendation + +- Update your Kubernetes cluster to a supported version +- Onboard into release channel to benefit from automated updates in the future. + +## PRIVATE_GOOGLE_ACCESS_DISABLED +The mentioned network contains some subnets that can have a preferred routing to the Google APIs without going through the Internet. It is recommended to use this routing pattern for security and latency reasons. + +### Recommendation +Enable Private Google Access on all your subnetworks. + +## SERVICE_ACCOUNT_TOO_HIGH_PRIVILEGES +This service account has too high privileges. In general we try to avoid granting too high privileges to service accounts. +Often time, permissions are granted at the project level where they should be granted at a more granular level. +For instance, Service Account Token Creator at the project level allows for privilege escalation as it allows the +service account that has this permission to get a token for any other service account in that project. + +Granting this permissions at the service account level only allows that service account to get a token +for another specific service account. + +### Recommendation +Limit the permissions of that service account to the strict minimum set of permissions required for this service +account to run the tasks it is running. + +## UNUSED_EXPORTED_CREDENTIALS +An exported credential is still valid but has not been used in a while. + +### Recommendation +It is recommended to delete unused credentials and regenerate a new set when they are needed to prevent leaking +of credentials and unauthorised access to our infrastructure. diff --git a/docs/RISK_SCORE.md b/docs/RISK_SCORE.md new file mode 100644 index 0000000..b9fda96 --- /dev/null +++ b/docs/RISK_SCORE.md @@ -0,0 +1,120 @@ +# Risk Score + +## Definition + +The Risk Score is an indicator that is used to calculate the priority to assign to observations. +We compute this score as an indicative measure of the risk given the conditions in which the observation was created. + +The Risk Score ranges from INFO to CRITICAL. + +## Severity + +### CVSS v3.x + +CVSS v3.x defines a qualitative measure of severity of a vulnerability. +[As they define in their own documentation](https://nvd.nist.gov/vuln-metrics/cvss), **CVSS is not a measure of risk**. + +CVSS v3.x defines the severity scoring as follows: + +| Severity | Score Range | +|----------|:-----------:| +| NONE | 0.0 | +| LOW | 0.1 - 3.9 | +| MEDIUM | 4.0 - 6.9 | +| HIGH | 7.0 - 8.9 | +| CRITICAL | 9.0 - 10.0 | + +### Modron + +In Modron, the Severity matches the CVSS v3.x severity levels - with the exception of the "NONE" severity that +is called "INFO". Rules can define the severity of the observations they create: +```go +ob := &pb.Observation{ + Name: "BUCKET_IS_PUBLIC", + ExpectedValue: "private", + ObservedValue: "public", + Remediation: &pb.Remediation{ + Description: "Bucket foo is publicly accessible", + Recommendation: "Unless strictly needed, restrict the IAM policy of the bucket", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, // <-- We define the severity here +} +``` + +| Severity | +|----------| +| INFO | +| LOW | +| MEDIUM | +| HIGH | +| CRITICAL | + +#### UI representation + +In the UI we represents the severities (together with the risk score and the impact) using the following icons: + +

+Severities +

+ +The screenshot includes a "?" severity - this is a very special case that is used when the severity is not known and +indicates a bug in the code if it is displayed in the UI. + + +#### Re-using the CVSS v3.x score + +When available, we use the CVSS v3.x score to map it to the Modron severity (external sources) - internally in Modron we +use the scale directly (e.g: a rule defines its observations to be LOW severity) + +## Impact + +Since the CVSS score (and the Modron Severity) do not measure risk, we use "facts" to calculate the _Impact_ of +an observation. The impact, when combined with the severity, is used to determine the Risk Score of an observation. + +### Definition + +An _Impact_ can only assume three values: LOW, MEDIUM or HIGH. + +To define the impact, we collect context about the environment in which the observation was generated that we call "facts". +We always take the highest impact defined by the list of facts to determine the final impact. + +### Facts + +An example of a fact is "workload is from the production environment". This is a fact that can make a misconfiguration more impactful. +By leveraging these informations we can increase or decrease the risk score. + +#### Real-world example + +| Kind | Example | Decision | +|-------------|--------------------------------------------------------------|----------------| +| Observation | A misconfigured SQL database is accessible from the internet | Severity: HIGH | +| Fact | The database contains sensitive information | Impact: HIGH | +| Fact | The database is not used | Impact: LOW | +| Fact | The database is not used in production | Impact: LOW | + +The rule "SQL database is accessible from the internet" defines the severity of the observations as HIGH. +One of the facts sets the impact to be HIGH, while the other two facts set the impact to be LOW: we always take +the worst case scenario when defining the impact, so the final impact is HIGH. + +We now have the Severity (HIGH) and the Impact (HIGH). We can now calculate the Risk Score. + +## Risk Score + +The Risk Score calculation is straight-forward: + +- If the impact is **MEDIUM**, the risk score is equal to the severity, +- If the impact is **HIGH**, the risk score is one category higher than the severity (e.g: **MEDIUM** -> **HIGH**) +- If the impact is **LOW**, the risk score is one category lower than the severity (e.g: **MEDIUM** -> **LOW**) + +We can therefore define a "Risk Score matrix" as follows: + +

+Risk Score Matrix +

+ +### Examples + +- A HIGH severity observation with a HIGH impact will have a CRITICAL risk score +- A HIGH severity observation with a MEDIUM impact will have a HIGH risk score +- A HIGH severity observation with a LOW impact will have a MEDIUM risk score +- An INFO severity observation with a LOW impact will have an INFO risk score (can't get lower than INFO) diff --git a/docs/pics/architecture.svg b/docs/pics/architecture.svg new file mode 100644 index 0000000..c1366d1 --- /dev/null +++ b/docs/pics/architecture.svg @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + +
+
+
+ modron +
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ modron-grpc-web +
Cloud Run +
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+ modron-ui +
Cloud Run +
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ modron +
Cloud Load Balancing +
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + +
+
+
+ /api +
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + +
+
+
+ /* +
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+ IAP +
Identity-Aware Proxy +
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+ modron +
Cloud SQL +
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + +
+
+
+ https://modron.example.com +
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GCP API + + + + + + + + + + + + + +
+
+
+ Collect +
+
+
+
+ +
+
+
+
+
+ + + + + + + Nagatha + + + + + + + + + + + + + +
+
+
+ Notify +
+
+
+
+ +
+
+
+
+
+
+
+
+
diff --git a/docs/pics/modron.svg b/docs/pics/modron.svg new file mode 100644 index 0000000..04eb461 --- /dev/null +++ b/docs/pics/modron.svg @@ -0,0 +1,75 @@ + + +image/svg+xml diff --git a/docs/pics/risk_score_matrix.png b/docs/pics/risk_score_matrix.png new file mode 100644 index 0000000..9959320 Binary files /dev/null and b/docs/pics/risk_score_matrix.png differ diff --git a/docs/pics/severities.png b/docs/pics/severities.png new file mode 100644 index 0000000..8bc51da Binary files /dev/null and b/docs/pics/severities.png differ diff --git a/docs/screenshots/home.jpg b/docs/screenshots/home.jpg new file mode 100644 index 0000000..f7b11e7 Binary files /dev/null and b/docs/screenshots/home.jpg differ diff --git a/docs/screenshots/nagatha-1.jpg b/docs/screenshots/nagatha-1.jpg new file mode 100644 index 0000000..886c6bb Binary files /dev/null and b/docs/screenshots/nagatha-1.jpg differ diff --git a/docs/screenshots/nagatha-2.jpg b/docs/screenshots/nagatha-2.jpg new file mode 100644 index 0000000..744ef4d Binary files /dev/null and b/docs/screenshots/nagatha-2.jpg differ diff --git a/docs/screenshots/observations.jpg b/docs/screenshots/observations.jpg new file mode 100644 index 0000000..c78625b Binary files /dev/null and b/docs/screenshots/observations.jpg differ diff --git a/docs/screenshots/risk-score.jpg b/docs/screenshots/risk-score.jpg new file mode 100644 index 0000000..3b8407b Binary files /dev/null and b/docs/screenshots/risk-score.jpg differ diff --git a/docs/screenshots/single-observation.jpg b/docs/screenshots/single-observation.jpg new file mode 100644 index 0000000..4ef4c93 Binary files /dev/null and b/docs/screenshots/single-observation.jpg differ diff --git a/docs/screenshots/stats.jpg b/docs/screenshots/stats.jpg new file mode 100644 index 0000000..6898376 Binary files /dev/null and b/docs/screenshots/stats.jpg differ diff --git a/env.example b/env.example new file mode 100644 index 0000000..2a2d814 --- /dev/null +++ b/env.example @@ -0,0 +1,25 @@ +ADMIN_GROUPS=modron-admins@example.com +COLLECTORS=gcp +LABEL_TO_EMAIL_REGEXP=(.*)_(.*?)_(.*?) +LABEL_TO_EMAIL_SUBSTITUTION=$1@$2.$3 +LOG_FORMAT=text +LOG_LEVEL=info +LOG_ALL_SQL_QUERIES=false +ORG_ID=111111111111 +ORG_SUFFIX=example.com +PERSISTENT_CACHE=true +PERSISTENT_CACHE_TIMEOUT=48h +PORT=4201 +PROJECT_ID=modron-project-id +RUN_AUTOMATED_SCANS=false +SKIP_IAP=true +SQL_CONNECT_STRING=postgres://modron:modron@127.0.0.1:5432/modron?sslmode=disable + +OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317 +OTEL_EXPORTER_OTLP_INSECURE=true +OTEL_LOG_LEVEL=debug +OTEL_SERVICE_NAME=modron + +TAG_CUSTOMER_DATA=111111111111/customer_data +TAG_EMPLOYEE_DATA=111111111111/employee_data +TAG_ENVIRONMENT=111111111111/environment diff --git a/go.mod b/go.mod index be939b4..77df434 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ // This file is required for gosec to work. module github.com/nianticlabs/modron -go 1.21 +go 1.23.2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/otel/config/config.yaml b/otel/config/config.yaml new file mode 100644 index 0000000..ca414f1 --- /dev/null +++ b/otel/config/config.yaml @@ -0,0 +1,34 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 +processors: + batch: + +exporters: + otlp: + endpoint: "jaeger:4317" + tls: + insecure: true + prometheusremotewrite: + endpoint: "http://prometheus:9090/api/v1/write" + +extensions: + health_check: + pprof: + zpages: + +service: + extensions: [health_check, pprof, zpages] + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [otlp] + metrics: + receivers: [otlp] + processors: [] + exporters: [prometheusremotewrite] \ No newline at end of file diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..55f2d3a --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +/modron-lint.xml diff --git a/src/Dockerfile b/src/Dockerfile index 3b16f40..649d96a 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -1,20 +1,20 @@ -ARG GOVERSION=1.21 +ARG GOVERSION=1.23 -FROM alpine:latest as ca-certificates_builder +FROM alpine:latest AS ca-certificates_builder RUN apk add --no-cache ca-certificates -FROM golang:${GOVERSION} as modron_builder -ENV GOPATH /go -WORKDIR /app -COPY go.* ./ -COPY proto/go.* ./proto/ +FROM golang:${GOVERSION} AS modron_builder +ENV GOPATH=/go +WORKDIR /app/src +COPY src/go.* /app/src/ +COPY src/proto/generated /app/src/proto/generated/ RUN go mod download -COPY . ./ +COPY ./src/ /app/src/ RUN CGO_ENABLED=0 go build -v -o modron FROM scratch WORKDIR /app COPY --from=ca-certificates_builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -COPY --from=modron_builder /app/modron /app/modron +COPY --from=modron_builder /app/src/modron /app/modron USER 101:101 -ENTRYPOINT ["/app/modron", "--logtostderr"] +ENTRYPOINT ["/app/modron"] diff --git a/src/Dockerfile.e2e b/src/Dockerfile.e2e index ca3556b..8373822 100644 --- a/src/Dockerfile.e2e +++ b/src/Dockerfile.e2e @@ -1,24 +1,19 @@ # We have to keep this file here otherwise we can't depend on the shared proto. # Docker prevents including files above the Dockerfile directory (.. forbidden). -ARG GOVERSION=1.21 +ARG GOVERSION=1.23 -FROM golang:${GOVERSION} as builder -ENV GOPATH /go -WORKDIR /app -COPY test/go.* e2e_test_dir/ -COPY proto/ ./proto -WORKDIR /app/e2e_test_dir +FROM golang:${GOVERSION} AS builder +ENV GOPATH=/go +WORKDIR /app/src/test/ +COPY ./src/test/go.* /app/src/test/ +COPY ./src/proto/ /app/src/proto RUN go mod download -COPY test/* ./ -RUN mkdir certs -RUN openssl req -x509 -newkey rsa:4096 -keyout certs/key.pem -nodes -out certs/cert.pem -sha256 -days 1 -subj '/CN=modron_test' -addext "subjectAltName = DNS:modron_test" +COPY ./src/test/* /app/src/test/ RUN CGO_ENABLED=0 go test -c -v -o test FROM scratch WORKDIR /app/stats WORKDIR /app/secrets WORKDIR /app -COPY --from=builder /app/e2e_test_dir/test /app/test -COPY --from=builder /app/e2e_test_dir/certs/cert.pem /app/cert.pem -COPY --from=builder /app/e2e_test_dir/certs/key.pem /app/key.pem +COPY --from=builder /app/src/test/test /app/test ENTRYPOINT ["/app/test", "--test.short"] diff --git a/src/README.md b/src/README.md index f7d673d..4e64e95 100644 --- a/src/README.md +++ b/src/README.md @@ -2,8 +2,8 @@ ## Build modron and push to Google Cloud Registry -``` -gcloud builds submit . --tag gcr.io/modron-dev/modron:dev --timeout=900 +```bash +gcloud builds submit . --tag us-central1.docker.pkg.dev/$PROJECT_ID/modron/modron:dev --timeout=900 ``` This applies the label `dev` on the image you're pushing. @@ -11,26 +11,33 @@ This image is expected to run on modron-dev environment. Deploy to cloud run dev: -``` -DEV_RUNNER_SA_NAME=$PROJECT-runner@$PROJECT.iam.gserviceaccount.com -gcloud run deploy modron-grpc-web-dev --platform=managed --image=gcr.io/modron-dev/modron:dev --region=us-central1 --service-account=$DEV_RUNNER_SA_NAME -gcloud run services update-traffic modron-ui --to-revisions LATEST=100 --region=us-central1 +```bash +DEV_RUNNER_SA_NAME=$PROJECT_ID-runner@$PROJECT_ID.iam.gserviceaccount.com +REGION=us-central1 +gcloud run deploy \ + modron-grpc-web-dev \ + --platform=managed \ + --image="$REGION.docker.pkg.dev/$PROJECT_ID/modron/modron:dev" \ + --region="$REGION" \ + --service-account="$DEV_RUNNER_SA_NAME" +gcloud run services update-traffic modron-ui --to-revisions LATEST=100 --region="$REGION" ``` ## Debug To debug RPC issues, set the two following environment variables: -``` +```bash export GRPC_GO_LOG_VERBOSITY_LEVEL=99 export GRPC_GO_LOG_SEVERITY_LEVEL=info ``` ## Update libraries -``` -CYPRESS_CACHE_FOLDER=/tmp npm upgrade -CYPRESS_CACHE_FOLDER=/tmp npm install +```bash +export CYPRESS_CACHE_FOLDER=/tmp +npm upgrade +npm install ``` -Note: Cypress tries to write to /root/.cache which doesn't work. This is why we need to set the environment variable. +Note: Cypress tries to write to `/root/.cache` which doesn't work. This is why we need to set the environment variable. diff --git a/src/acl/fakeacl/checkerFake.go b/src/acl/fakeacl/checkerFake.go index 9853052..fc57889 100644 --- a/src/acl/fakeacl/checkerFake.go +++ b/src/acl/fakeacl/checkerFake.go @@ -1,29 +1,31 @@ package fakeacl import ( - "os" - - "github.com/golang/glog" + "github.com/sirupsen/logrus" "golang.org/x/net/context" + "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" ) type GcpCheckerFake struct{} +var log = logrus.StandardLogger().WithField(constants.LogKeyPkg, "fakeacl") +var _ model.Checker = (*GcpCheckerFake)(nil) + func New() model.Checker { - glog.Warningf("If you see this on production, contact security%s", os.Getenv(constants.OrgSuffixEnvVar)) + log.Warnf("If you see this on production, contact security") return &GcpCheckerFake{} } -func (checker *GcpCheckerFake) GetAcl() map[string]map[string]struct{} { +func (checker *GcpCheckerFake) GetACL() model.ACLCache { return nil } -func (checker *GcpCheckerFake) GetValidatedUser(ctx context.Context) (string, error) { +func (checker *GcpCheckerFake) GetValidatedUser(_ context.Context) (string, error) { return "", nil } -func (checker *GcpCheckerFake) ListResourceGroupNamesOwned(ctx context.Context) (map[string]struct{}, error) { +func (checker *GcpCheckerFake) ListResourceGroupNamesOwned(_ context.Context) (map[string]struct{}, error) { return map[string]struct{}{"projects/modron-test": {}}, nil } diff --git a/src/acl/gcpacl/cache.go b/src/acl/gcpacl/cache.go new file mode 100644 index 0000000..fe0a42d --- /dev/null +++ b/src/acl/gcpacl/cache.go @@ -0,0 +1,55 @@ +package gcpacl + +import ( + "encoding/json" + "errors" + "os" + "time" + + "github.com/nianticlabs/modron/src/model" +) + +type FSACLCache struct { + LastUpdate time.Time `json:"last_update"` + Content model.ACLCache `json:"content"` +} + +var localACLCacheFile = os.TempDir() + "/modron-acl-cache.json" + +const ownerRWPermissions = 0600 + +func (checker *GcpChecker) getLocalACLCache() (*FSACLCache, error) { + log.Tracef("getting ACL cache from %s", localACLCacheFile) + f, err := os.Open(localACLCacheFile) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, nil //nolint:nilnil + } + return nil, err + } + defer f.Close() + + var fsACLCache FSACLCache + if err := json.NewDecoder(f).Decode(&fsACLCache); err != nil { + return nil, err + } + return &fsACLCache, nil +} + +func (checker *GcpChecker) saveLocalACLCache(res model.ACLCache) error { + log.Tracef("saving ACL cache to %s", localACLCacheFile) + fsACLCache := FSACLCache{ + LastUpdate: time.Now(), + Content: res, + } + f, err := os.OpenFile(localACLCacheFile, os.O_CREATE|os.O_WRONLY, ownerRWPermissions) + if err != nil { + return err + } + defer f.Close() + return json.NewEncoder(f).Encode(fsACLCache) +} + +func (checker *GcpChecker) deleteLocalACLCache() error { + return os.Remove(localACLCacheFile) +} diff --git a/src/acl/gcpacl/cache_test.go b/src/acl/gcpacl/cache_test.go new file mode 100644 index 0000000..9c49363 --- /dev/null +++ b/src/acl/gcpacl/cache_test.go @@ -0,0 +1,80 @@ +package gcpacl + +import ( + "context" + "errors" + "os" + "testing" + "time" + + "github.com/nianticlabs/modron/src/collector/testcollector" + "github.com/nianticlabs/modron/src/model" + + "github.com/google/go-cmp/cmp" +) + +func clearCache(t *testing.T) { + t.Helper() + if err := os.Remove(localACLCacheFile); err != nil { + if !errors.Is(err, os.ErrNotExist) { + t.Fatalf("cannot delete cache: %v", err) + } + } +} + +func TestCache(t *testing.T) { + clearCache(t) + defer clearCache(t) + + checker := GcpChecker{ + cfg: Config{PersistentCache: true, PersistentCacheTimeout: time.Second * 10}, + } + collector := testcollector.TestCollector{} + checker.collector = &collector + fsACLCache, err := checker.getLocalACLCache() + if err != nil { + t.Fatalf("getLocalACLCache: %v", err) + } + if fsACLCache != nil { + t.Fatalf("getLocalACLCache should be empty") + } + ctx := context.Background() + aclStoreTime := time.Now() + if err := checker.loadACLCache(ctx); err != nil { + t.Fatalf("loadACLCache: %v", err) + } + if checker.aclCache == nil { + t.Fatalf("checker.aclCache should be initialized") + } + + expectedCache := model.ACLCache{ + "*": { + "projects/modron-test": {}, + "projects/super-secret": {}, + }, + "user@example.com": { + "projects/modron-test": {}, + }, + } + if !cmp.Equal(expectedCache, checker.aclCache) { + t.Errorf("aclCache mismatch: %v", cmp.Diff(expectedCache, checker.aclCache)) + } + + fsACLCache, err = checker.getLocalACLCache() + if err != nil { + t.Fatalf("getLocalACLCache: %v", err) + } + if fsACLCache == nil { + t.Fatalf("getLocalACLCache should be initialized, but it's nil") + } + + if diff := cmp.Diff(checker.aclCache, fsACLCache.Content); diff != "" { + t.Errorf("aclCache mismatch (-want +got):\n%s", diff) + } + if !aclStoreTime.Before(fsACLCache.LastUpdate) { + t.Errorf("the filesystem cache should have been updated, its last update was %v", fsACLCache.LastUpdate) + } + if aclStoreTime.Add(time.Second * 10).Before(fsACLCache.LastUpdate) { + t.Fatalf("the filesystem cache should not be older than 10 seconds from the moment we started (lastUpdate: %v)", fsACLCache.LastUpdate) + } +} diff --git a/src/acl/gcpacl/checker.go b/src/acl/gcpacl/checker.go index 92206f9..8a3ad51 100644 --- a/src/acl/gcpacl/checker.go +++ b/src/acl/gcpacl/checker.go @@ -2,13 +2,13 @@ package gcpacl import ( "fmt" - "os" - "strconv" + "strings" "time" + "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/golang/glog" + "github.com/sirupsen/logrus" "golang.org/x/net/context" "google.golang.org/api/cloudidentity/v1" "google.golang.org/api/idtoken" @@ -16,20 +16,22 @@ import ( ) const ( - AclUpdateIntervalSecEnvVar = "ACL_UPDATE_INTERVAL_SEC" + defaultACLUpdateInterval = 5 * time.Minute ) -var aclUpdateIntervalSec = func() int { - intVar, err := strconv.Atoi(os.Getenv(AclUpdateIntervalSecEnvVar)) - if err != nil { - return 5 * 60 - } - return intVar -}() +var ( + log = logrus.StandardLogger().WithField(constants.LogKeyPkg, "gcpacl") +) type Config struct { - CacheTimeout time.Duration AdminGroups []string + CacheTimeout time.Duration + // PersistentCache is a flag to enable/disable the local ACL cache. + PersistentCache bool + // PersistentCacheTimeout is the amount of time we keep the ACLs on the filesystem before we fetch them again. + PersistentCacheTimeout time.Duration + // SkipIap enables or disables the IAP check - when this is enabled, all users are considered admins. Of course this should be always disabled in prod. + SkipIap bool } type GcpChecker struct { @@ -37,45 +39,57 @@ type GcpChecker struct { collector model.Collector cloudIdentitySvc *cloudidentity.Service - aclCache map[string]map[string]struct{} + aclCache model.ACLCache adminsCache map[string]ACLCacheEntry - adminGroupsIds []string + adminGroupsIDs []string } -func (checker *GcpChecker) GetAcl() map[string]map[string]struct{} { +func (checker *GcpChecker) GetACL() model.ACLCache { return checker.aclCache } func New(ctx context.Context, collector model.Collector, cfg Config) (model.Checker, error) { - cisvc, err := cloudidentity.NewService(ctx) + cloudIdentitySvc, err := cloudidentity.NewService(ctx) if err != nil { return nil, err } gcpChecker := GcpChecker{ collector: collector, cfg: cfg, - cloudIdentitySvc: cisvc, - aclCache: make(map[string]map[string]struct{}), + cloudIdentitySvc: cloudIdentitySvc, + aclCache: make(model.ACLCache), adminsCache: make(map[string]ACLCacheEntry), - adminGroupsIds: []string{}, + adminGroupsIDs: []string{}, } for _, ag := range cfg.AdminGroups { - group, err := cisvc.Groups.Lookup().GroupKeyId(ag).Do() + groupEmail := strings.TrimPrefix(ag, "group:") + group, err := cloudIdentitySvc.Groups.Lookup().GroupKeyId(groupEmail).Do() if err != nil { - glog.Errorf("cannot fetch group %q: %v", ag, err) + log.Errorf("cannot fetch group %q: %v", ag, err) continue } - gcpChecker.adminGroupsIds = append(gcpChecker.adminGroupsIds, group.Name) + gcpChecker.adminGroupsIDs = append(gcpChecker.adminGroupsIDs, group.Name) } - if err := gcpChecker.loadAclCache(ctx); err != nil { - return nil, err + if cfg.PersistentCache { + log.Debugf("using on-disk ACL cache") + if err := gcpChecker.loadACLCache(ctx); err != nil { + return nil, err + } + } else { + log.Debugf("using in-memory ACL cache") + var res model.ACLCache + res, err = gcpChecker.getACLAndStore(ctx) + if err != nil { + return nil, err + } + gcpChecker.aclCache = res } gcpChecker.updateACLs(ctx) return &gcpChecker, nil } func (checker *GcpChecker) updateACLs(ctx context.Context) { - ticker := time.NewTicker(time.Duration(aclUpdateIntervalSec) * time.Second) + ticker := time.NewTicker(defaultACLUpdateInterval) go func() { for { select { @@ -83,8 +97,8 @@ func (checker *GcpChecker) updateACLs(ctx context.Context) { ticker.Stop() return case <-ticker.C: - if err := checker.loadAclCache(ctx); err != nil { - glog.Warningf("cannot update ACLs: %v", err) + if err := checker.loadACLCache(ctx); err != nil { + log.Warnf("cannot update ACLs: %v", err) } } } @@ -92,12 +106,18 @@ func (checker *GcpChecker) updateACLs(ctx context.Context) { } func (checker *GcpChecker) ListResourceGroupNamesOwned(ctx context.Context) (map[string]struct{}, error) { + adminResources := checker.aclCache["*"] + if checker.cfg.SkipIap { + // When we decide to skip the IAP check (insecure, only for local development), we return all resources as admin. + log.Warnf("IAP check is disabled, users are all admins. If you see this in production, reach out to the security team.") + return adminResources, nil + } user, err := checker.GetValidatedUser(ctx) if err != nil { return nil, err } if checker.isAdmin(user) { - return checker.aclCache["*"], nil + return adminResources, nil } if _, ok := checker.aclCache[user]; !ok { return map[string]struct{}{}, nil @@ -105,15 +125,42 @@ func (checker *GcpChecker) ListResourceGroupNamesOwned(ctx context.Context) (map return checker.aclCache[user], nil } -func (checker *GcpChecker) loadAclCache(ctx context.Context) error { +func (checker *GcpChecker) loadACLCache(ctx context.Context) error { + aclFsCache, err := checker.getLocalACLCache() + if err != nil { + return err + } + if aclFsCache != nil { + if time.Since(aclFsCache.LastUpdate) < checker.cfg.PersistentCacheTimeout { + log.Tracef("ACL cache hit") + checker.aclCache = aclFsCache.Content + return nil + } + if err := checker.deleteLocalACLCache(); err != nil { + return fmt.Errorf("ACL cache: %w", err) + } + } + log.Tracef("ACL cache miss") res, err := checker.collector.ListResourceGroupAdmins(ctx) if err != nil { return err } checker.aclCache = res + if err := checker.saveLocalACLCache(res); err != nil { + return fmt.Errorf("save ACL cache: %w", err) + } return nil } +func (checker *GcpChecker) getACLAndStore(ctx context.Context) (model.ACLCache, error) { + res, err := checker.collector.ListResourceGroupAdmins(ctx) + if err != nil { + return nil, err + } + checker.aclCache = res + return res, nil +} + func (checker *GcpChecker) GetValidatedUser(ctx context.Context) (string, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { @@ -135,25 +182,25 @@ func (checker *GcpChecker) GetValidatedUser(ctx context.Context) (string, error) return user, nil } -func (c *GcpChecker) isAdmin(user string) bool { +func (checker *GcpChecker) isAdmin(user string) bool { // We do some memoizing here as this might be called a lot over a short period of time. - if v, ok := c.adminsCache[user]; ok { - if v.time.Add(c.cfg.CacheTimeout).After(time.Now()) { + if v, ok := checker.adminsCache[user]; ok { + if v.time.Add(checker.cfg.CacheTimeout).After(time.Now()) { return v.access } } - for _, g := range c.adminGroupsIds { - resp, err := c.cloudIdentitySvc.Groups.Memberships.CheckTransitiveMembership(g).Query(fmt.Sprintf("member_key_id == '%s'", user)).Do() + for _, g := range checker.adminGroupsIDs { + resp, err := checker.cloudIdentitySvc.Groups.Memberships.CheckTransitiveMembership(g).Query(fmt.Sprintf("member_key_id == '%s'", user)).Do() if err != nil { - glog.Warningf("cannot check membership: %v", err) - } else { - if resp.HasMembership { - c.adminsCache[user] = ACLCacheEntry{time: time.Now(), access: true} - return true - } + log.Warnf("cannot check membership: %v", err) + continue + } + if resp.HasMembership { + checker.adminsCache[user] = ACLCacheEntry{time: time.Now(), access: true} + return true } } - c.adminsCache[user] = ACLCacheEntry{time: time.Now(), access: false} + checker.adminsCache[user] = ACLCacheEntry{time: time.Now(), access: false} return false } diff --git a/src/acl/gcpacl/checker_real_test.go b/src/acl/gcpacl/checker_real_test.go new file mode 100644 index 0000000..5f7f6a2 --- /dev/null +++ b/src/acl/gcpacl/checker_real_test.go @@ -0,0 +1,37 @@ +//go:build integration + +package gcpacl + +import ( + "context" + "os" + "testing" + + "github.com/nianticlabs/modron/src/collector/gcpcollector" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/storage/memstorage" +) + +func TestCheckerReal(t *testing.T) { + orgID := os.Getenv("ORG_ID") + orgSuffix := os.Getenv("ORG_SUFFIX") + if orgID == "" || orgSuffix == "" { + t.Fatalf("ORG_ID and ORG_SUFFIX are required, orgID=%q, orgSuffix=%q", orgID, orgSuffix) + } + + ctx := context.Background() + storage := memstorage.New() + gcpCollector, err := gcpcollector.New(ctx, storage, orgID, orgSuffix, []string{}, risk.TagConfig{}, []string{}) + if err != nil { + t.Error(err) + } + + checker, err := New(ctx, gcpCollector, Config{}) + if err != nil { + t.Fatal(err) + } + + if _, err := checker.ListResourceGroupNamesOwned(ctx); err != nil { + t.Error(err) + } +} diff --git a/src/acl/gcpacl/checker_test.go b/src/acl/gcpacl/checker_test.go index 8e6ad0b..651fa20 100644 --- a/src/acl/gcpacl/checker_test.go +++ b/src/acl/gcpacl/checker_test.go @@ -2,26 +2,17 @@ package gcpacl import ( "context" - "fmt" "os" "testing" "github.com/nianticlabs/modron/src/collector/gcpcollector" - "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/risk" "github.com/nianticlabs/modron/src/storage/memstorage" "google.golang.org/grpc/metadata" ) func TestMain(m *testing.M) { - if orgIdEnv := os.Getenv(constants.OrgIdEnvVar); orgIdEnv == "" { - os.Setenv(constants.OrgIdEnvVar, fmt.Sprintf("%s%s", constants.GCPOrgIdPrefix, "012345678912")) - defer os.Unsetenv(constants.OrgIdEnvVar) - } - if orgSuffixEnv := os.Getenv(constants.OrgSuffixEnvVar); orgSuffixEnv == "" { - os.Setenv(constants.OrgSuffixEnvVar, "example.com") - defer os.Unsetenv(constants.OrgSuffixEnvVar) - } os.Exit(m.Run()) } @@ -32,7 +23,7 @@ func TestInvalidNoToken(t *testing.T) { ctx := context.Background() storage := memstorage.New() - gcpCollector := gcpcollector.NewFake(ctx, storage) + gcpCollector := gcpcollector.NewFake(ctx, storage, risk.TagConfig{}) checker, err := New(ctx, gcpCollector, Config{}) if err != nil { @@ -45,14 +36,10 @@ func TestInvalidNoToken(t *testing.T) { } func TestInvalidParseToken(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode: need GCP credentials") - } - ctx := context.Background() - ctx = metadata.NewIncomingContext(ctx, metadata.New( - map[string]string{"Authorization": "QxMzZjMjAyYjhkMjkwNTgwYjE2NWMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiIzMjU1NTk0MDU1OS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImF6cCI6InNlcnZpY2VhY2NvdW50bmFtZUBzZWMtZXNhbGltYmVuaS1hcGkta2V5LXRlc3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbCI6InNlcnZpY2VhY2NvdW50bmFtZUBzZWMtZXNhbGltYmVuaS1hcGkta2V5LXRlc3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZXhwIjoxNjU5NTIxODcxLCJpYXQiOjE2NTk1MTgyNzEsImlzcyI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbSIsInN1YiI6IjEwNTM5OTA4NzA5OTk1NDg5MDA0MyJ9.LHwLeuAa6jZc7pFPhtFLvsKMg56vPHrm83dfVukLxycopz6CzkpFZoF_DXFRKQ-myQs4KMMd44loi29te5vfnAL9aMXwvySFEcjESOIE_SXPND3Q5FBlfRfWoSLFjGsGhqLhNwKQn-tvjynkdxmtopL4qVmhAFpgTqNA4u8b7l7cWsl3zoudPZMy8mi5pIUetWH5jpj7OaPyv9pVaQ-LaXaLUQkD8bx0bL3Tjhu9yu2IP2Z06jFR9mN-fJ60F05kMJ6Y4HquDXNjm8HCNrXfMGHBcKMUzE3wAaOIG4PoGI81t43dPWpUIUg07RS5tG5uxuWIrgJddxJYpYCWOhGjog"})) + ctx := metadata.NewIncomingContext(context.Background(), metadata.New( + map[string]string{"Authorization": "Bearer xyz.abc.123"})) storage := memstorage.New() - gcpCollector := gcpcollector.NewFake(ctx, storage) + gcpCollector := gcpcollector.NewFake(ctx, storage, risk.TagConfig{}) checker, err := New(ctx, gcpCollector, Config{}) if err != nil { @@ -63,25 +50,3 @@ func TestInvalidParseToken(t *testing.T) { t.Error("expected error: checker parsed a jwt tokenId that is invalid") } } - -func TestCheckerReal(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode: need GCP credentials") - } - - ctx := context.Background() - storage := memstorage.New() - gcpCollector, err := gcpcollector.New(ctx, storage) - if err != nil { - t.Error(err) - } - - checker, err := New(ctx, gcpCollector, Config{}) - if err != nil { - t.Fatal(err) - } - - if _, err := checker.ListResourceGroupNamesOwned(ctx); err != nil { - t.Error(err) - } -} diff --git a/src/collector/collector.go b/src/collector/collector.go new file mode 100644 index 0000000..ac0e2f5 --- /dev/null +++ b/src/collector/collector.go @@ -0,0 +1,18 @@ +package collector + +type Type string + +const ( + Gcp Type = "gcp" + Fake Type = "fake" +) + +var validCollectors = []Type{Gcp, Fake} + +func ValidCollectors() []string { + var collectors []string + for _, c := range validCollectors { + collectors = append(collectors, string(c)) + } + return collectors +} diff --git a/src/collector/gcpcollector/api_fake.go b/src/collector/gcpcollector/api_fake.go new file mode 100644 index 0000000..34a660a --- /dev/null +++ b/src/collector/gcpcollector/api_fake.go @@ -0,0 +1,1295 @@ +package gcpcollector + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "golang.org/x/net/context" + "google.golang.org/api/apikeys/v2" + "google.golang.org/api/cloudasset/v1" + "google.golang.org/api/cloudresourcemanager/v3" + "google.golang.org/api/compute/v1" + "google.golang.org/api/container/v1" + "google.golang.org/api/googleapi" + "google.golang.org/api/iam/v1" + "google.golang.org/api/monitoring/v3" + "google.golang.org/api/securitycenter/v1" + "google.golang.org/api/spanner/v1" + sqladmin "google.golang.org/api/sqladmin/v1beta4" + "google.golang.org/api/storage/v1" + k8s_io_api_core_v1 "k8s.io/api/core/v1" + + "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/model" + "github.com/nianticlabs/modron/src/risk" +) + +const oneYear = time.Hour * 24 * 365 + +func NewFake(_ context.Context, storage model.Storage, tagConfig risk.TagConfig) *GCPCollector { + fakeAPI := GCPAPIFake{} + m := initMetrics() + return &GCPCollector{ + allowedSccCategories: map[string]struct{}{ + "SQL_PUBLIC_IP": {}, + // We intentionally exclude GKE_RUN_AS_NONROOT to test that we don't include it in the ListSccFindings + // "GKE_RUN_AS_NONROOT": {}, + "GKE_RUNTIME_OS_VULNERABILITY": {}, + }, + api: &fakeAPI, + storage: storage, + metrics: m, + tagConfig: tagConfig, + } +} + +type GCPAPIFake struct{} + +func (api *GCPAPIFake) GetServiceAccountIAMPolicy(_ context.Context, _ string) (*iam.Policy, error) { + return &iam.Policy{ + Bindings: []*iam.Binding{ + { + Members: []string{"user:user-1@example.com"}, + Role: "roles/iam.serviceAccountUser", + }, + }, + }, nil +} +func (api *GCPAPIFake) ListAPIKeys(_ context.Context, _ string) ([]*apikeys.V2Key, error) { + return []*apikeys.V2Key{ + { + Name: "api-key-unrestricted-0", + Restrictions: nil, + }, + { + Name: "api-key-unrestricted-1", + Restrictions: &apikeys.V2Restrictions{ + ApiTargets: nil, + }, + }, + { + Name: "api-key-with-overbroad-scope-1", + Restrictions: &apikeys.V2Restrictions{ + ApiTargets: []*apikeys.V2ApiTarget{ + { + Service: "iamcredentials.googleapis.com", + }, + { + Service: "storage_api", + }, + { + Service: "apikeys", + }, + }, + }, + }, + { + Name: "api-key-without-overbroad-scope", + Restrictions: &apikeys.V2Restrictions{ + ApiTargets: []*apikeys.V2ApiTarget{ + { + Service: "bigquerystorage.googleapis.com", + }, + }, + }, + }, + }, nil +} + +func (api *GCPAPIFake) ListBackendServices(_ context.Context, _ string) ([]*compute.BackendService, error) { + return []*compute.BackendService{ + { + Name: "backend-svc-internal", + LoadBalancingScheme: "INTERNAL", + SelfLink: "/links/backend-svc-internal", + Iap: &compute.BackendServiceIAP{ + Enabled: true, + Oauth2ClientId: "client-id@client.example.com", + }, + }, + { + Name: "backend-svc-external-no-modern", + LoadBalancingScheme: "EXTERNAL", + SelfLink: "/links/backend-svc-external-no-modern", + Iap: &compute.BackendServiceIAP{ + Enabled: true, + Oauth2ClientId: "client-id@client.example.com", + }, + }, + { + Name: "backend-svc-external-modern", + LoadBalancingScheme: "EXTERNAL", + SelfLink: "/links/backend-svc-external-modern", + Iap: &compute.BackendServiceIAP{ + Enabled: true, + Oauth2ClientId: "client-id@client.example.com", + }, + }, + { + Name: "backend-svc-no-iap", + LoadBalancingScheme: "EXTERNAL", + SelfLink: "/links/backend-svc-no-iap", + Iap: &compute.BackendServiceIAP{ + Enabled: false, + }, + }, + { + Name: "backend-svc-iap", + LoadBalancingScheme: "EXTERNAL", + SelfLink: "/links/backend-svc-iap", + Iap: &compute.BackendServiceIAP{ + Enabled: true, + Oauth2ClientId: "client-id@client.example.com", + }, + }, + { + Name: "backend-svc-no-fe", + LoadBalancingScheme: "EXTERNAL", + SelfLink: "/links/backend-svc-no-fe", + Iap: &compute.BackendServiceIAP{ + Enabled: true, + Oauth2ClientId: "client-id@client.example.com", + }, + }, + }, nil +} + +func (api *GCPAPIFake) ListBuckets(_ context.Context, _ string) ([]*storage.Bucket, error) { + creationTimestamp := time.Now().Format(time.RFC3339) + return []*storage.Bucket{ + { + Name: "bucket-public", + Id: "bucket-public", + TimeCreated: creationTimestamp, + Encryption: &storage.BucketEncryption{}, + RetentionPolicy: &storage.BucketRetentionPolicy{}, + IamConfiguration: &storage.BucketIamConfiguration{ + UniformBucketLevelAccess: &storage.BucketIamConfigurationUniformBucketLevelAccess{}, + }, + }, + { + Name: "bucket-2", + Id: "bucket-2", + TimeCreated: creationTimestamp, + Encryption: &storage.BucketEncryption{}, + RetentionPolicy: &storage.BucketRetentionPolicy{}, + IamConfiguration: &storage.BucketIamConfiguration{ + UniformBucketLevelAccess: &storage.BucketIamConfigurationUniformBucketLevelAccess{}, + }, + }, + { + Name: "bucket-public-allusers", + Id: "bucket-public-allusers", + TimeCreated: creationTimestamp, + Encryption: &storage.BucketEncryption{}, + RetentionPolicy: &storage.BucketRetentionPolicy{}, + IamConfiguration: &storage.BucketIamConfiguration{ + UniformBucketLevelAccess: &storage.BucketIamConfigurationUniformBucketLevelAccess{}, + }, + }, + { + Name: "bucket-accessible-from-other-project", + Id: "bucket-accessible-from-other-project", + TimeCreated: creationTimestamp, + Encryption: &storage.BucketEncryption{}, + RetentionPolicy: &storage.BucketRetentionPolicy{}, + IamConfiguration: &storage.BucketIamConfiguration{ + UniformBucketLevelAccess: &storage.BucketIamConfigurationUniformBucketLevelAccess{}, + }, + }, + }, nil +} + +func (api *GCPAPIFake) ListBucketsIamPolicy(bucketID string) (*storage.Policy, error) { + iamPolicies := map[string]*storage.Policy{ + "bucket-public": { + Bindings: []*storage.PolicyBindings{ + { + Role: "roles/storage.objectViewer", + Members: []string{ + "allAuthenticatedUsers", + }, + }, + }, + }, + "bucket-2": { + Bindings: []*storage.PolicyBindings{ + { + Role: "roles/storage.objectViewer", + Members: []string{ + "serviceAccount:account-1@modron-test.iam.gserviceaccount.com", + }, + }, + { + Role: "roles/storage.objectViewer", + Members: []string{ + "serviceAccount:account-2@modron-test.iam.gserviceaccount.com", + }, + }, + }, + }, + "bucket-public-allusers": { + Bindings: []*storage.PolicyBindings{ + { + Role: "roles/storage.objectViewer", + Members: []string{ + "allUsers", + }, + }, + }, + }, + "bucket-accessible-from-other-project": { + Bindings: []*storage.PolicyBindings{ + { + Role: "roles/storage.legacyBucketOwner", + Members: []string{ + "serviceAccount:account-3@modron-other-test.iam.gserviceaccount.com", + }, + }, + }, + }, + } + if iamPolicy, ok := iamPolicies[bucketID]; ok { + return iamPolicy, nil + } + return nil, fmt.Errorf("invalid bucket id %q", bucketID) +} + +func (api *GCPAPIFake) ListCertificates(_ context.Context, _ string) ([]*compute.SslCertificate, error) { + creationTimestamp := time.Now() + sslCertificatesList := []*compute.SslCertificate{ + { + Name: "cert-managed", + Type: "MANAGED", + CreationTimestamp: creationTimestamp.Format(time.RFC3339), + ExpireTime: creationTimestamp.Add(oneYear).Format(time.RFC3339), + SelfLink: "/links/cert-managed", + Certificate: strings.ReplaceAll(` + -----BEGIN CERTIFICATE----- + MIIFTTCCAzUCCQDBvVCMwjjyWjANBgkqhkiG9w0BAQUFADBVMRAwDgYDVQQLDAdV + bmtub3duMRAwDgYDVQQKDAdVbmtub3duMRAwDgYDVQQHDAdVbmtub3duMRAwDgYD + VQQIDAd1bmtub3duMQswCQYDVQQGEwJDSDAeFw0yMjA3MTkwOTEwMDRaFw0yMzA3 + MTkwOTEwMDRaMHwxJTAjBgNVBAMMHGRvbWFpbi0xLm1vZHJvbi5uaWFudGljLnRl + YW0xEDAOBgNVBAsMB1Vua25vd24xEDAOBgNVBAoMB1Vua25vd24xEDAOBgNVBAcM + B1Vua25vd24xEDAOBgNVBAgMB3Vua25vd24xCzAJBgNVBAYTAkNIMIICIjANBgkq + hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7b8cRiT7Wy7/uyY3sowwEBpWd8V6tzQP + 00ISr6K8Yi5boIbJ8sO17DQttOckfmRl6/itzDrZnaaJ/0Jxh7WfifVvxACyDdEZ + QxVUf/FOoyw9qUeI4PH7tPogW6Zkc10L2i7v6fvz6FzT1QTRRlX74X8AD3F/9I8X + w5SGfvfCnWJHGiY3cp29hezUkqhfuPUDhN+vRnQuyoxjib2BBtiWTwr4+t2kfWep + db6LuIZ6fLcfFN9CCow6YfK2Q0kw9VdPsVr8YkDMdCEKoyJKQcKIB4B5BfMGTH0F + 8op9nxIgNJ6K6LgpUtOWQBvAKIzRpnJQfq7wfqWLEp8P4F7VWq/ysLP7tJnMc643 + c397n7y+DpGGHCB/jfrg/Uu6rzpLiDwZaFeNSU9MQ074oZ+bEpJRFb40FEKK+Qov + ytXE/f7oC+5hPnDKPN1DDYZAMw2cMzyL0W3/lOi+X3HuxWCDieNgVbvfWea9ejfC + NuA86OrzELyHqqTXw7jr1rIdNlPjcU4G0mAuqsfHBD42wc406OBL45zKe+Icu7dt + 3ps/dx58ZroYOVqWEo+lfAG9F9hktX9HJUhVGzTLFjsd0UzeGvPHLgL2Y/GlHyK1 + kym4tDFzDLuk4jG7G20ctaIdjbhh0UDp0uVmCZY5r78h1mQzObXFkeup2VdI+yIe + bN1o6Po8nAkCAwEAATANBgkqhkiG9w0BAQUFAAOCAgEAtPEBotLk5ucJ70wpnPX2 + agRWJ8MpJvqnUP5iEVv9iJlD2EnUSU+E9YuuaMipw+F7g6BUFx39/ZQmzqR3Jh1c + gPaNU5YdVWqHPnukCMXKWfvN8WJPLyrZaJenjn/nFwFnEBsle6JtCQJ6okEXwD1V + LQoopVfqkXyYVICupOZhcTa/6MB59tUOUzOy5LnBZj3XGEQXE67eA+eDg1vfivDS + ux1H1eopE978RtGArmnZCkuUxxv39aEDWbN58tFb7MRcy43GuK3GdPlP9bUh02+d + f9dmpLWgrnxyub8tqK9bV3A/POHk3KLY1bUc5ZZFJVM3rR0Y87P38bPcOfcvb/H2 + SI9H7UjWMI5+K+DwZNL00h0N9EgHxcewslav7JTWAm1SSmMrUOLLHWhlAOsKWpAt + f67dWGWem4df0hrAk4kyyWlBDssDNgh9zN2VXewTZd4j8S5Sr9pTzVMGlTaIpCWn + bRKfJpVEKgEAzmVBnmLEyKcX32LFeDIt+JfZcIjEzzxkMQhtcrYfDZOJGs9J5rh1 + M0ovQVnQiVfVRIyt/TiRkuRIDAcOcwO1np3IPTz63oO3iEkMqWbh4z6+ho/3j3cm + gFNrdll08NWC5hmcCIwv6hHk7DlLVXzrDP3ZLNm7JcW2gwygn8BgQSu1SLAlaM3c + R1UzrGiyiwwbtyAKYwrn+A0= + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIFJjCCAw4CCQCEKQM+pKbyBTANBgkqhkiG9w0BAQsFADBVMRAwDgYDVQQLDAdV + bmtub3duMRAwDgYDVQQKDAdVbmtub3duMRAwDgYDVQQHDAdVbmtub3duMRAwDgYD + VQQIDAd1bmtub3duMQswCQYDVQQGEwJDSDAeFw0yMjA3MTkwOTA5MzZaFw0yMzA3 + MTkwOTA5MzZaMFUxEDAOBgNVBAsMB1Vua25vd24xEDAOBgNVBAoMB1Vua25vd24x + EDAOBgNVBAcMB1Vua25vd24xEDAOBgNVBAgMB3Vua25vd24xCzAJBgNVBAYTAkNI + MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA8fqY2Tdvu+fBAlUhMvPu + 3KPSjUMS2Oe1rfhRGHT/oIIp+oQenAKkUnlkJoct+UKiDijg4ovFKUjenxWqu66n + mCEUFGtBWZrIPlczhMm+vuQipMy6VTV+8wgMFd40wkupxwPNhTN1tk7wsOqu2bQy + FMquqJGU4LAUCuUuu95p2OaKxBzKcK2zhJKUEPuqZJED9UPdurwUfoNMbekh4YlO + WDmbVGJDOcs8o9fxeAgS7uGL7Y1G53BZ3ZVj3kZ4puAbvRd7g3mxiFKgY0V/6IOb + Cj4x3L4HJry2KNnaRj1/MxELAv5STRo5Sa7CTXUmKaxyEsgK2+ek5ispo8w1Efvk + Q7oQiKoCmtmgRWIroRHPOahBldk82CCiQHGJV5gCEL+n62OJb51uM51Amwr1pBLm + 6p3PflLUp8nIfUGUw845T8KfdZPz7nOif+IGfch1nZZJN1tuw9YcfKbjsh62xi+/ + nN4B2KFQl89MgCjWm+TQDoP4ToIS1+BlB+DXPy8zLwa+sUSNMawXf9LIYV7wWjD+ + Snk/8IceCaxF3SW5EjausKQ+cYXN6LecOlL5Aw9I1++PuZ4VTfbtC+BFvRUP/apb + FzzLziMwxlhC2LV6lMJN6V6T+pM1HnNDPv0SuUCO/lzI/NKC42llmS3xUSMMoZNo + QrU3ClZk2Fs0z8/qc0ycb/cCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEASL9Td1DU + NdtfPpW39Uwia5hnG6rnRz37EcqWup+V9zzvzOFc1FnRPItNPRJLmJoQ9CLAWJVG + yrE/bcODLbyGeGC6vvRhTcpriij99kEjy36cxm+XqkSBUYRqUs87jLvZrdhPaSKq + P1gj3LK7LE7nCYuP6zVLrnrVxlVbeKXIs5Pcaa9sYR9oi+hGDRn6bcDj4y7qixxW + LuVYnjyHs/pKb+75DRyaDFF7u4VlXcqGH2t8F+ZipNzMT2mx7sr+xQkpsbJQRVSL + Cx4ih2TbcyqApLU50JgjtbQYvMOngB+NI2LgJ/VOzokSLGG9YbblfYMPggQYaUXC + bDQuPvQCG0hQpqBKgluk65AmCba4BCRLLUy01U1i7ScxtmtWyn1HSLyJmxGkCGxc + OWL4qMDIGtgE32Es2WW8VSfFpH7n85hFx6Z93tVTgjxWQn6t2cAu17qbgVuI81mp + gpKRYgexWtC/K2bftPGrjajWSsRTIM1JZd6awtjBdbgvLeu02MERQQ5wZ7a9Ee1X + EjKOG1vj2c95sMiuwebY+evXZ5najnNsdYwfSXyX1hULt1R59hxPcuYVig1qM+ch + oRU0QKlNW30K+RQPb/ZGMFODsaYNOxvgAQSSeOQjyoVVHm5ZBZoU3LY98M9X2kFx + FbGm99HuLTXv1ReyURGzjxZIAqHd6hnX5wk= + -----END CERTIFICATE----- + `, "\t", ""), + }, + { + Name: "cert-self-managed", + Type: "SELF_MANAGED", + CreationTimestamp: creationTimestamp.Format(time.RFC3339), + ExpireTime: creationTimestamp.Add(oneYear).Format(time.RFC3339), + SelfLink: "/links/cert-self-managed", + Certificate: strings.ReplaceAll(` + -----BEGIN CERTIFICATE----- + MIIFTTCCAzUCCQD9AMCeW12GEDANBgkqhkiG9w0BAQUFADBVMRAwDgYDVQQLDAdV + bmtub3duMRAwDgYDVQQKDAdVbmtub3duMRAwDgYDVQQHDAdVbmtub3duMRAwDgYD + VQQIDAd1bmtub3duMQswCQYDVQQGEwJDSDAeFw0yMjA3MTkwOTAwNDBaFw0yMzA3 + MTkwOTAwNDBaMHwxJTAjBgNVBAMMHGRvbWFpbi0wLm1vZHJvbi5uaWFudGljLnRl + YW0xEDAOBgNVBAsMB1Vua25vd24xEDAOBgNVBAoMB1Vua25vd24xEDAOBgNVBAcM + B1Vua25vd24xEDAOBgNVBAgMB3Vua25vd24xCzAJBgNVBAYTAkNIMIICIjANBgkq + hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0no57IbQwzRNJ61/FoBPknTqa24oNATG + DU/oY4bTQCOPC7bEc/5IAPQ8Fhd2u3HsI5rkkUGAeQCAnEymB5z+RMG4uGZk7CKS + mozKFhf2asSSehWENYCORc2sM5DRMC+7mOHl1+Gs3uIpZhq0C2L+HAbyXi9TE8bu + lyyD/bk65ocGfSsajHJr6VzlKI/YZFzty6VXlSafxcEVbUrZ//YqtogCHQHN5d9Y + sw+5tYehGqi0O1XPQ+KkJ4PmP9yD46MAtPtiQwBRClA7G68jJ3j50stG/A59Otvj + COUApdQeL9VwHhNh9t4TQZaZcGLFb9qyNKW2hhJP63cQTPQ7asTagKemdSB0zwoT + LKZia67bG4PXExLwPuxI12oFug/g0AGdrmlJiIjXGvZPyEJ7bc/6hUxWfEpfo77v + brkndn2aJTS98HI3R+eBCbCkYK6yWBJmAV1RjcNNGlPOIHxWeJtnG90L/n7vNIL1 + LtzK/W9cTLAH1PkexeSmOEJqYC0FihjkhXZM0KUZFjjT5z5wdZsDX1f+q/6Yvo/R + 2OphVI5GfY/W5Jc2mzsGzkiHi3epow0MEtbEJ4c12afxKuUGqwKyO8T92DOVZ2BS + iew/DZ673hbek3C4sVq+c4fCIxYOLL/fqeL5r+yh7a+tAGlIxU1fpSodanu+ksQU + T7TqJFH0LXECAwEAATANBgkqhkiG9w0BAQUFAAOCAgEANm6bCAju+jm9sI/Pry1u + KS6SRMNX0ne0knjDRXO0lHjMz095xmEQA/Wu6dpPkhMo7QdYsZ0bntIgt4Go/1qx + 3rZUnP9B77BqPsLxqgBfMxryZVADQMEvsUkr0yK9g6crEQ/aSu5h9hEiSX1FA+9y + Jy00TwxS2NcTPd1AuFa5/lXZaw2Iu9nwZ4/+2QuZrjmZfE28gcUGb1GDcBMzcqZM + 8O4J4Xogi5DSzQLucPkBX8uD1ibEn1Cs16Kzq9X+45M3zPWwNnV4yM+38ZxU20Do + gDTKR2Md5JByhKt+8TSe1S+fEg5cZwGj8P3LCUFUAPjloHu59sjmHcc21MHhS3JQ + TpqQJgLpo3bdwbhUsvenXSUsk08e0PnvaIym6ALgDku/ZWYLmkGKHDSWE4otFDkF + BUbDHHxSuH8Pk5eNSOf7rfFmDk7r9Hj3ryqMZf8xq+kIHmzNESAQskFScBPj3iY3 + mCb7p2/gEmSddYR7TtDG+J1au4+sVDkFd9dIrMVhwZuY19m0S6TqpJ3pp9p6OBoq + d8ZyTuiP6LTehRiFBQrFA7LGhU14pPIVbOS5PuP2W91DzL9ZfKwsQ/Tr08ZhOH58 + ocnbZWQWQ6NEZzsnrwuyNa7DxLUfDc3Itfg94oYy4YSO7SdrifJWAvHlqV69CZ8K + G67Se2laEbw8sNaehw/0mpE= + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIFJjCCAw4CCQDlQR4bAQ4wBzANBgkqhkiG9w0BAQsFADBVMRAwDgYDVQQLDAdV + bmtub3duMRAwDgYDVQQKDAdVbmtub3duMRAwDgYDVQQHDAdVbmtub3duMRAwDgYD + VQQIDAd1bmtub3duMQswCQYDVQQGEwJDSDAeFw0yMjA3MTkwOTAwMTZaFw0yMzA3 + MTkwOTAwMTZaMFUxEDAOBgNVBAsMB1Vua25vd24xEDAOBgNVBAoMB1Vua25vd24x + EDAOBgNVBAcMB1Vua25vd24xEDAOBgNVBAgMB3Vua25vd24xCzAJBgNVBAYTAkNI + MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx1GS+PP1If+ShSuJeIHm + jCTAr7d8nExf3hZ1x41+MaJfDx6XSFsyqDi9rad2KGnI7x3OFmLP7AfIhVfN+mYB + HcCST5YfPMuyaLa2roLZHaz0ZzFYhexMsK5Lq/QnaW/Exoo/lY2Rxu8uYLHOX28i + 6lQPka52Wi8D8sWHfUMUMxjoaV97LYFYsz8ngqeN11bvXkDoo5Tp0CWRb1o4LoBt + cf8ub5PJH8YW5LBglC6rTR1Q9H9YX6gqnSrP3vqqfkVdqPLTlSILqCgqxvKqbjIb + RkMlkzaiqNf3rz5iC8br0ZfKWgs/Jzvhea+K3J5Y3YbwExjttUekhXIUZqbzAfNQ + TIAUKfLlmmJfywbmI+xgUfSxNAPwgASjnxmkbAXsVW8SjUvnnLsbla6SyS1a+bwe + 1OP0gmwjtfeGN7QSCtU+GSZ/3RP1mfluog2acHR7HfRi2dzVyabGPKe1FbU/QFIu + dtl6YSXvKUFM1mC5lIj0s05vTaszw7JKAEQaVizaDCFt/d3xI9oaQFi1Z6W/DxmB + 6GS98iQ9ydLatEXCipfmTrJhxf9mbRE8Z4NVTg4kEllNwcV9W1yRqdVnfLcGe6TB + lpfk63PYBsLfuB6KVEyu3hGwOfP4UVNE/A/BfyYYVKWobR7L4GzOxmm24ikSo6fq + HhNOTKogByaSoXBfdm8g1WcCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAWrVESQoE + UXRxoUOxEKhS9UgsQrf70k8wHgwgEGFUATPFTfMInWhrQ+VMj4ImSxDOT5tDLADq + 0hU/h0oQ2XkC14YVpcF835Vt2mCPaRugPzjxzU/Ky9Ie39izZeBvG2orCthEglof + FtVZGc3vCmWEXs7zPhSwx2BsZNw0xVMLg6lTok7wVcf66lW/1PWQp8dKwwZlSvHg + VgRLfmH2yisEak0euw1zMYRs4GdwMxUC69ImremBG1MrAQdyvp1ZM7XamyLFZivg + UTnVMDLduHub+IpITnl2IYgqvMdATpL9h4n036WvYvu2qP765j0ZW0yvBMFSVS3/ + eKVEn3NqK3nFZGlo4W/i3mElbFtd+q8mxQiI1S9hF1W1yTuVPDfsVYzO+wWOQHdk + b4XoWikC2eq98zMp9wWPFrnbNFiTLTllWKUYWZQgx7UrkA+wmtKOAwxiY0tADMHg + IwLHGUhHpIG5ErJX7AKFUShb3jSqujOkU8Bmtr0W2jd+uYGp8MWT8d5drrO2aVAW + CdBMmRly672Sy1Y7MTZjLykrMEdsnmXosvIvzPWzbqAjsTJQR3OKSFMaBO+lxCXs + n+WngS5fO6hiTKqf1fjDSeBhOlpywVV8h0ONMNF0TIHyydJEYbIlZBajER3dUIZs + muOKyutYtJqW5tqke8N7Yy9oDUlqtt6gnFE= + -----END CERTIFICATE----- + `, "\t", ""), + }, + { + Name: "cert-managed-2", + Type: "MANAGED", + CreationTimestamp: creationTimestamp.Format(time.RFC3339), + ExpireTime: creationTimestamp.Add(oneYear).Format(time.RFC3339), + SelfLink: "/links/cert-managed-2", + Certificate: strings.ReplaceAll(` + -----BEGIN CERTIFICATE----- + MIIFTTCCAzUCCQCUhTr1JbteOjANBgkqhkiG9w0BAQUFADBVMRAwDgYDVQQLDAdV + bmtub3duMRAwDgYDVQQKDAdVbmtub3duMRAwDgYDVQQHDAdVbmtub3duMRAwDgYD + VQQIDAd1bmtub3duMQswCQYDVQQGEwJDSDAeFw0yMjA3MTkwOTExMjlaFw0yMzA3 + MTkwOTExMjlaMHwxJTAjBgNVBAMMHGRvbWFpbi0yLm1vZHJvbi5uaWFudGljLnRl + YW0xEDAOBgNVBAsMB1Vua25vd24xEDAOBgNVBAoMB1Vua25vd24xEDAOBgNVBAcM + B1Vua25vd24xEDAOBgNVBAgMB3Vua25vd24xCzAJBgNVBAYTAkNIMIICIjANBgkq + hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuiFSKID/n0ozLBdVrxCjyyHq9jEsbOWw + CmcJ7nbrsiP9w1qceljMg1/IvqaqHySoPvxurp8Vxq9OXgWVJzmCuMyoCUMsXEyt + K9+CcCz0AMoaxY5uO2+ehpm2SPnx+p8zfKqH6n9Heto+YU2IQYJTkbAIEKXSJib4 + iDutY7AlCAzlMqFXZpxyPBQfgFvM744WE/FOhv3ASdJ7TTqK8vuulSk46zgYe/hz + jBevt8MUIyE9dsQ/eIL4HsS+OofkDU4onsTJVrMpetZD0KwB6lBrdVLkIbn5mWHD + 2ONRLuuncQPLkyc1LEjY5+RYj2KUrXX+3jByco7e4pFDDJjFKXDl/MujCNtWtb9z + SaR362Ic6/93CkvmCiuS9IeqrMv8ZniM7HOtSs4Zq/e9Ym4/YJ5BbzrbSK8pMvhE + dsSENgxlSGCVBrs1DLBSmJ89qX3Q+Y4ejJq97Tzs9yR6MEycFYKOGX4FdwkfcZb6 + Fi+v13D+9x8WUtehTOcap9jXbiACSzGbkVbD0Q/MlEPhj9erkyQQFwhaLTZCoEo9 + SpMH/pJt8n4AYtfl1Xrw8yLjv93n54MNjMDOELMJluPAP4GfkAOtS/ck3gT/Cm53 + SW76ocPTXrN0cvybl5ShM0coC+jukbTbBUQ7eiv37LEcsZCj5CNW4/ee/Vrn1XlI + TtBrN9lZVjUCAwEAATANBgkqhkiG9w0BAQUFAAOCAgEAfKrlr72uUmD0CNmQctP4 + dAN0vuIkNC5aRW8P3p8Ba7urgWo53r2f25gxbpW/ESRT0vgMChHhoKzgP65o67n2 + 8neOIedfJPTxZRlKqsc1Yhlp+FtVgnrgk0oTZupYqIHT4L98uFzNkhZo7FFHOLzA + rrbMABXgRs8umW7OMsjBw2akbRwxbPwalM8NGvgzH1zAUz5oMq9s1AqFHITdLDvu + /CWM/vBJGXqkQ/+uHYrOP1E8hrAZtQFEQ9rJVR3fKLAqAcSLBmB0q94zbXoBq/W8 + uZyXdrItA1COb8oeHSzJejcGEPl6VhfsgKTHoSwWCvplPZ8SOP4g8XohqXT9aakR + ism2Fmc9yewA7GOzU4vw/6Oqaka6MwoyIUUbb7Mt6rdcE+gJiqfujwyilt+DIkx+ + D7Ex+meotSTP+xWIJbaeoNxJ/M19gGH0M9FxlIoYr/flYCSkUNlGy5EFSclI7VTz + GlOCJmBICrj3VDP3Q4iHv8O7DErAv+oHYf7j53/jg2mIDeIVJRjihzwYBaZN6dLd + sVmCwzd/ZntJlu2II2PnrR5UxsfmpevMSgMrChSOKh/mfPGVNF78r7QXawkrndZa + nymkGGSoll+Shlv5MpKB9PR4XfMM14dyuE568AmSbMnGPhYqSmauXDSSXnNi7KtT + kNOsaHc3Uaw1jIi2BOwpJfY= + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIFJjCCAw4CCQDCXGx9MFOr3zANBgkqhkiG9w0BAQsFADBVMRAwDgYDVQQLDAdV + bmtub3duMRAwDgYDVQQKDAdVbmtub3duMRAwDgYDVQQHDAdVbmtub3duMRAwDgYD + VQQIDAd1bmtub3duMQswCQYDVQQGEwJDSDAeFw0yMjA3MTkwOTExMjBaFw0yMzA3 + MTkwOTExMjBaMFUxEDAOBgNVBAsMB1Vua25vd24xEDAOBgNVBAoMB1Vua25vd24x + EDAOBgNVBAcMB1Vua25vd24xEDAOBgNVBAgMB3Vua25vd24xCzAJBgNVBAYTAkNI + MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6HkB1jv4AjGmAgkwlozD + zmkTQiomPS6jWiQBec58j8s0WhLlWwvPKkv090kpY13lGk8FF8OR+wb7J7x6AZl6 + 03+PzacBGsXDiRkQ08HYSZ0pW/hRZFQ7UEq00luUZut7xzebbnCDqvSz/C8sGuZk + V4o9wWiOkDRD2szHRqmiHjZeHePCy/3xEPDb/OMuwXmjnS9qkMYtsCLtub54QmJZ + 3fdol86wSobkydhQ8vvIJqfxmNX3bhJUx6PDNAjoyOgxX+YBMLyDDHRuXFH7mj5H + Nv0ZDmuVgMGrgzAmWoGIfFRP/R8rGvpscPX0GrhQdoroyLAkvDZiAYnK/Fx8N8Kz + AHpfxQQIR62X9vuLWtUCdvV0qo0GG2+QCAws4n3BM6TwXkCkQcpg6pIJgtvk/avN + hVAQcLlul5ohnAVPlMV6/cs+UnOTn8pkCvE3G1JuaSHjsHELropXMeYbrUX5Q+Kd + 9JGxtC/sUIAmAL2YrFxI6tC9RFCCK2HJxZMh012wcwz/HSSrT4yXv+P61OUK0mSo + cvttUjgpGE5Z0hvyRMEq/UIwuWjNymcO+8f62Cn0v4EM1bh5XS5F0d8ILZjKU4JX + Bqi7eOWd5MFCS3Q8pdCOvWtxefMDgZb1MYwLyVcsuaHIdrVtzeAEieqD/rbVoXe7 + 0oQ8bfjDyWOGbpusqwCxCnECAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAvGLv5q7w + vZl/UPxIhs1HWEbGaNiOXsiRXYbs6tgViABcsjjZkErlv8oo3KSkf6yfXI55y6tn + wKqP2RgKuTrWaTp63h6EDGHnLl0X+Nq6FGnxwiRaJ2iYzwvC2mEDVtEgod7DtRdI + 7xRqufLdHJEm8uUm+EfCjWaamnAqc2RCGU50Dezn7Tif88eAMCMMu0Nqg+HCr3zM + G+/a2OyFxUNZRWGRkthQjQTjPcTu/uBuxtLMzG8BqfpAM8XCH26Q0zxnD4g4NLuo + 03932aEaNFDHfyQJ4sFbH65+1EzudAt2emWS3g519+/0UU9Lthb6Y/aMvnR37Q9x + dh9GLl+PkDpE8GOZGuavwkNCyvKTGNwYRpQrK8fal6e2sTyKObzmn+s0tkvputy1 + mp/DbMIIXykFRZ8Y2Aps6pgSjJBuI1HBR3nAX+J1fTAjUghEMkbt/N9MheUDQfzh + hO9Qgo27PltMyUPOuhclLKHIZLsJrgSf8dFfWHpzSYFhtPr2gNTaSWuD6UxLKPHr + bz9GjrScMtDzB5n8alcySomoATWP5wnPArb6wJyg8pfyrb/43VZeaWCDhPYDVj1i + A/klwbs/a2YF0w72ZTd1aydFct5ONPcYhcY/4Zip5JZT5SCzWNaNTp8UL5TTv4zn + y4rzKfl2JQSqXBbOdR4KUDN0uhXFqPDEyK4= + -----END CERTIFICATE----- + `, "\t", ""), + }, + } + return sslCertificatesList, nil +} + +func (api *GCPAPIFake) ListCloudSQLDatabases(_ context.Context, _ string) ([]*sqladmin.DatabaseInstance, error) { + autoResize := true + return []*sqladmin.DatabaseInstance{ + { + Name: "cloudsql-test-db-ok", + InstanceType: "CLOUD_SQL_INSTANCE", + ConnectionName: "test-connection", + DatabaseVersion: "TEST_VERSION", + Settings: &sqladmin.Settings{ + IpConfiguration: &sqladmin.IpConfiguration{ + RequireSsl: true, + AuthorizedNetworks: []*sqladmin.AclEntry{ + { + Value: "127.0.0.1/32", + }, + }, + }, + StorageAutoResize: &autoResize, + }, + }, + { + Name: "cloudsql-test-db-public-and-authorized-networks", + InstanceType: "CLOUD_SQL_INSTANCE", + ConnectionName: "test-connection", + DatabaseVersion: "TEST_VERSION", + Settings: &sqladmin.Settings{ + IpConfiguration: &sqladmin.IpConfiguration{ + RequireSsl: true, + Ipv4Enabled: true, + AuthorizedNetworks: []*sqladmin.AclEntry{ + { + Value: "127.0.0.1/32", + }, + }, + }, + StorageAutoResize: &autoResize, + }, + }, + { + Name: "cloudsql-test-db-public-and-no-authorized-networks", + InstanceType: "CLOUD_SQL_INSTANCE", + ConnectionName: "test-connection", + DatabaseVersion: "TEST_VERSION", + Settings: &sqladmin.Settings{ + IpConfiguration: &sqladmin.IpConfiguration{ + RequireSsl: true, + Ipv4Enabled: true, + }, + StorageAutoResize: &autoResize, + }, + }, + { + Name: "cloudsql-report-not-enforcing-tls", + InstanceType: "CLOUD_SQL_INSTANCE", + ConnectionName: "test-connection", + DatabaseVersion: "TEST_VERSION", + Settings: &sqladmin.Settings{ + IpConfiguration: &sqladmin.IpConfiguration{ + RequireSsl: false, + AuthorizedNetworks: []*sqladmin.AclEntry{ + { + Value: "127.0.0.1/32", + }, + }, + }, + StorageAutoResize: nil, + }, + }, + }, nil +} + +func (api *GCPAPIFake) ListClustersByZone(context.Context, string, string) ([]*container.Cluster, error) { + return []*container.Cluster{}, nil +} + +func (api *GCPAPIFake) ListFoldersIamPolicy(context.Context, string) (*cloudresourcemanager.Policy, error) { + return &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "roles/owner", + Members: []string{ + "user:account-1@example.com", + "user:account-2@example.com", + }, + }, + { + Role: "roles/test2", + Members: []string{ + "account-2@example.com", + }, + }, + { + Role: "roles/iam.serviceAccountAdmin", + Members: []string{ + "account-1@example.com", + }, + }, + { + Role: "roles/dataflow.admin", + Members: []string{ + "account-1@example.com", + }, + }, + { + Role: "roles/viewer", + Members: []string{ + "account-2@example.com", + }, + }, + }, + }, nil +} + +func (api *GCPAPIFake) ListInstances(_ context.Context, _ string) ([]*compute.Instance, error) { + return []*compute.Instance{ + { + Name: "instance-1", + Id: 0, + NetworkInterfaces: []*compute.NetworkInterface{ + { + NetworkIP: "192.168.0.1", + AccessConfigs: []*compute.AccessConfig{ + { + NatIP: "240.241.242.243", + }, + }, + }, + }, + ServiceAccounts: []*compute.ServiceAccount{ + { + Email: "account-1@modron-test", + }, + }, + }, + }, nil +} + +func (api *GCPAPIFake) ListNamespaces(_ context.Context, _ string) ([]*cloudasset.ResourceSearchResult, error) { + return []*cloudasset.ResourceSearchResult{ + { + AssetType: "k8s.io/Namespace", + CreateTime: time.Now().UTC().Format(time.RFC3339), + Name: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster/k8s/namespaces/modron-test-namespace", + ParentFullResourceName: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster", + }, + { + AssetType: "k8s.io/Namespace", + CreateTime: time.Now().UTC().Format(time.RFC3339), + Name: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster/k8s/namespaces/modron-test-namespace-other", + ParentFullResourceName: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster", + }, + }, nil +} + +func (api *GCPAPIFake) ListPods(_ context.Context, _ string) ([]*cloudasset.ResourceSearchResult, error) { + podspec := &k8s_io_api_core_v1.PodSpec{ + Containers: []k8s_io_api_core_v1.Container{ + {Name: "fake-1"}, + }, + } + podspecBytes, err := json.Marshal(podspec) + if err != nil { + return nil, err + } + return []*cloudasset.ResourceSearchResult{ + { + AssetType: "k8s.io/Pod", + CreateTime: time.Now().UTC().Format(time.RFC3339), + Name: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster/k8s/namespaces/modron-test-namespace/pods/modron-pod-test-name-1", + ParentFullResourceName: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster/k8s/namespaces/modron-test-namespace", + State: "Running", + VersionedResources: []*cloudasset.VersionedResource{{Resource: podspecBytes}}, + }, + { + AssetType: "k8s.io/Pod", + CreateTime: time.Now().UTC().Format(time.RFC3339), + Name: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster/k8s/namespaces/modron-test-namespace/pods/modron-pod-test-name-2", + ParentFullResourceName: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster/k8s/namespaces/modron-test-namespace", + State: "Pending", + VersionedResources: []*cloudasset.VersionedResource{{Resource: podspecBytes}}, + }, + }, nil + +} + +func (api *GCPAPIFake) ListOrganizationsIamPolicy(_ context.Context, _ string) (*cloudresourcemanager.Policy, error) { + return &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "roles/owner", + Members: []string{ + "user:account-1@example.com", + "user:account-2@example.com", + }, + }, + { + Role: "roles/test2", + Members: []string{ + "account-2@example.com", + }, + }, + { + Role: "roles/iam.serviceAccountAdmin", + Members: []string{ + "account-1@example.com", + }, + }, + { + Role: "roles/dataflow.admin", + Members: []string{ + "account-1@example.com", + }, + }, + { + Role: "roles/viewer", + Members: []string{ + "account-2@example.com", + }, + }, + }, + }, nil +} + +func (api *GCPAPIFake) ListProjectIamPolicy(_ context.Context, name string) (*cloudresourcemanager.Policy, error) { + iamPolicies := map[string]*cloudresourcemanager.Policy{ + "modron-test": { + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "roles/owner", + Members: []string{ + "user:owner1@example.com", + "user:owner2@example.com", + }, + }, + { + Role: "roles/test2", + Members: []string{ + "serviceAccount:account-2@modron-test.iam.gserviceaccount.com", + }, + }, + { + Role: "roles/iam.serviceAccountAdmin", + Members: []string{ + "serviceAccount:account-1@modron-test.iam.gserviceaccount.com", + }, + }, + { + Role: "roles/dataflow.admin", + Members: []string{ + "serviceAccount:account-3@modron-other-test.iam.gserviceaccount.com", + }, + }, + { + Role: "roles/iam.serviceAccountAdmin", + Members: []string{ + "serviceAccount:account-3@modron-other-test.iam.gserviceaccount.com", + }, + }, + { + Role: "roles/viewer", + Members: []string{ + "serviceAccount:account-2@modron-test.iam.gserviceaccount.com", + }, + }, + }, + }, + "modron-other-test": { + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "roles/owner", + Members: []string{ + "user:owner1@example.com", + "user:owner2@example.com", + }, + }, + { + Role: "roles/test3", + Members: []string{ + "serviceAccount:account-2@modron-other-test.iam.gserviceaccount.com", + }, + }, + { + Role: "roles/iam.serviceAccountAdmin", + Members: []string{ + "serviceAccount:account-1@modron-other-test.iam.gserviceaccount.com", + }, + }, + { + Role: "roles/dataflow.admin", + Members: []string{ + "serviceAccount:account-3@modron-test.iam.gserviceaccount.com", + }, + }, + { + Role: "roles/viewer", + Members: []string{ + "serviceAccount:account-2@modron-test.iam.gserviceaccount.com", + }, + }, + }, + }, + } + + iamPolicy, ok := iamPolicies[constants.ResourceWithoutProjectsPrefix(name)] + if ok { + return iamPolicy, nil + } + return nil, fmt.Errorf("invalid project id %q", name) +} + +func (api *GCPAPIFake) ListRegions(_ context.Context, _ string) ([]*compute.Region, error) { + return []*compute.Region{ + {Name: "region-1"}, + {Name: "region-2"}, + {Name: "region-3"}, + }, nil +} + +func (api *GCPAPIFake) ListResourceGroups(_ context.Context, rgNames []string) ([]*cloudasset.ResourceSearchResult, error) { + inSearchQuery := make(map[string]struct{}) + for _, rgName := range rgNames { + inSearchQuery[rgName] = struct{}{} + } + var ret []*cloudasset.ResourceSearchResult + for _, rg := range []*cloudasset.ResourceSearchResult{ + { + Name: "projects/modron-test", + Project: "projects/modron-test", + State: "ACTIVE", + Organization: "organizations/1111", + ParentFullResourceName: "//cloudresourcemanager.googleapis.com/folders/234", + ParentAssetType: "cloudresourcemanager.googleapis.com/folder", + Folders: []string{ + "folders/234", + "folders/123", + }, + Labels: map[string]string{ + "contact1": "user-1_example_com", + "contact2": "user-2_example_com", + }, + }, + { + Name: "projects/modron-other-test", + Project: "projects/modron-other-test", + State: "ACTIVE", + Organization: "organizations/1111", + ParentFullResourceName: "//cloudresourcemanager.googleapis.com/folders/123", + ParentAssetType: "cloudresourcemanager.googleapis.com/folder", + Folders: []string{ + "folders/123", + }, + }, + { + Name: "projects/pending-deletion", + Project: "projects/pending-deletion", + State: "PENDING_DELETION", + }, + { + Name: "folders/123", + DisplayName: "General", + Project: "folders/123", + State: "ACTIVE", + Organization: "organizations/1111", + ParentFullResourceName: "//cloudresourcemanager.googleapis.com/organizations/1111", + ParentAssetType: "cloudresourcemanager.googleapis.com/organization", + }, + { + Name: "folders/234", + DisplayName: "Production", + Project: "folders/234", + State: "ACTIVE", + Organization: "organizations/1111", + ParentFullResourceName: "//cloudresourcemanager.googleapis.com/folders/123", + ParentAssetType: "cloudresourcemanager.googleapis.com/folder", + Folders: []string{ + "folders/123", + }, + Tags: []*cloudasset.Tag{ + { + TagKey: "111111111111/environment", + TagValue: "111111111111/environment/prod", + }, + }, + Labels: map[string]string{ + "environment": "prod", + }, + }, + { + Name: "organizations/1111", + Project: "organizations/1111", + State: "ACTIVE", + }, + } { + if rgNames == nil { + ret = append(ret, rg) + continue + } + if _, ok := inSearchQuery[rg.Name]; ok { + ret = append(ret, rg) + } + } + return ret, nil +} + +func (api *GCPAPIFake) ListSccFindings(_ context.Context, _ string) ([]*securitycenter.Finding, error) { + return []*securitycenter.Finding{ + { + CanonicalName: "projects/12345/sources/123/findings/0cb62f16f7a1469e86e851b5d36648f2", + CreateTime: "2024-05-23T08:36:06.385Z", + EventTime: "2024-05-23T08:36:06.245Z", + FindingClass: "THREAT", + LogEntries: []*securitycenter.LogEntry{ + { + CloudLoggingEntry: &securitycenter.CloudLoggingEntry{ + InsertId: "d000000", + LogId: "requests", + ResourceContainer: "projects/project-id", + Timestamp: "2024-05-23T08:36:01.719346Z", + ForceSendFields: nil, + NullFields: nil, + }, + }, + }, + MitreAttack: &securitycenter.MitreAttack{ + PrimaryTactic: "INITIAL_ACCESS", + }, + Mute: "UNDEFINED", + MuteUpdateTime: "1970-01-01T00:00:00Z", + Category: "Initial Access: Log4j Compromise Attempt", + Name: "projects/12345/sources/123/findings/0cb62f16f7a1469e86e851b5d36648f2", + Parent: "organizations/0000000/sources/123", + ParentDisplayName: "Event Threat Detection", + ResourceName: "//compute.googleapis.com/projects/project-id/global/urlMaps/my-url-map", + Severity: "LOW", + SourceProperties: googleapi.RawMessage("{}"), + State: "ACTIVE", + }, + { + CanonicalName: "projects/12345/sources/123/findings/30ecf06efe6604a2d6f38ccba56d339d", + Contacts: map[string]securitycenter.ContactDetails{ + "security": { + Contacts: []*securitycenter.Contact{ + {Email: "user-1@example.com"}, + {Email: "user-2@example.com"}, + }, + }, + "technical": { + Contacts: []*securitycenter.Contact{ + {Email: "tech-1@example.com"}, + {Email: "tech-2@example.com"}, + {Email: "tech-3@example.com"}, + }, + }, + }, + CreateTime: "2024-05-23T08:36:06.385Z", + Description: "You can explicitly allow a container to run as the root user if the runAsUser or the USER directive in the image specifies the root user. The lack of preventive security controls when running as the root user increases the risk of container escape.", + NextSteps: "**Apply the following steps to your affected workloads:**\n1. Open the manifest for each affected workload.\n2. Set the following restricted fields to one of the allowed values:\n\n**Restricted Fields**\n- spec.securityContext.runAsNonRoot\n- spec.containers[*].securityContext.runAsNonRoot\n- spec.initContainers[*].securityContext.runAsNonRoot\n- spec.ephemeralContainers[*].securityContext.runAsNonRoot\n\n**Allowed Values**\n- true\n", + EventTime: "2024-05-23T08:36:06.245Z", + ExternalUri: "https://console.cloud.google.com/welcome?project=project-id", + FindingClass: "MISCONFIGURATION", + Mute: "UNDEFINED", + MuteUpdateTime: "1970-01-01T00:00:00Z", + Category: "GKE_RUN_AS_NONROOT", + Name: "projects/12345/sources/123/findings/30ecf06efe6604a2d6f38ccba56d339d", + Parent: "organizations/0000000/sources/456", + ParentDisplayName: "GKE Security Posture", + ResourceName: "//container.googleapis.com/projects/modron-test/locations/us-central1-b/clusters/app", + Severity: "MEDIUM", + SourceProperties: googleapi.RawMessage("{}"), + State: "ACTIVE", + }, + { + CanonicalName: "projects/12345/sources/123/findings/48230f1978594ffb9d09a3cb1fe5e1b3", + Contacts: map[string]securitycenter.ContactDetails{ + "security": { + Contacts: []*securitycenter.Contact{ + {Email: "user-1@example.com"}, + {Email: "user-2@example.com"}, + }, + }, + "technical": { + Contacts: []*securitycenter.Contact{ + {Email: "tech-1@example.com"}, + {Email: "tech-2@example.com"}, + {Email: "tech-3@example.com"}, + }, + }, + }, + CreateTime: "2024-07-16T05:28:00.395Z", + EventTime: time.Now().Format(time.RFC3339), + Description: "To lower your attack surface, Cloud SQL databases should not have public IPs. Private IPs provide improved network security and lower latency for your application.", + NextSteps: "Go to https://console.cloud.google.com/sql/instances/xyz/connections?project=project-id and click the \"Networking\" tab. Uncheck the \"Public IP\" checkbox and click \"SAVE\". If your instance is not configured to use a private IP, you will first have to enable private IP by following the instructions here: https://cloud.google.com/sql/docs/mysql/configure-private-ip#existing-private-instance", + ExternalUri: "https://console.cloud.google.com/welcome?project=project-id", + FindingClass: "MISCONFIGURATION", + Mute: "UNDEFINED", + MuteUpdateTime: "1970-01-01T00:00:00Z", + Category: "SQL_PUBLIC_IP", + Name: "projects/12345/sources/123/findings/48230f1978594ffb9d09a3cb1fe5e1b3", + Parent: "organizations/0000000/sources/456", + ParentDisplayName: "Security Health Analytics", + ResourceName: "//cloudsql.googleapis.com/projects/project-id/instances/xyz", + Severity: "MEDIUM", + SourceProperties: googleapi.RawMessage("{\"ScannerName\": \"SQL_SCANNER\"}"), + State: "ACTIVE", + }, + { + CanonicalName: "projects/1234/sources/5678/locations/global/findings/0000", + Name: "projects/1234/sources/5678/locations/global/findings/0000", + Category: "GKE_RUNTIME_OS_VULNERABILITY", + FindingClass: "VULNERABILITY", + ResourceName: "//container.googleapis.com/projects/modron0test/locations/us-west1/clusters/my-cluster", + Severity: "SEVERITY_UNSPECIFIED", + State: "ACTIVE", + CreateTime: "2024-05-19T13:20:02.112Z", + EventTime: "2024-07-04T15:47:51.202Z", + Vulnerability: &securitycenter.Vulnerability{ + Cve: &securitycenter.Cve{ + Cvssv3: &securitycenter.Cvssv3{ + BaseScore: 5.5, //nolint:mnd + AttackComplexity: "ATTACK_COMPLEXITY_LOW", + AttackVector: "ATTACK_VECTOR_LOCAL", + AvailabilityImpact: "IMPACT_HIGH", + ConfidentialityImpact: "IMPACT_NONE", + IntegrityImpact: "IMPACT_NONE", + PrivilegesRequired: "PRIVILEGES_REQUIRED_LOW", + Scope: "SCOPE_UNCHANGED", + UserInteraction: "USER_INTERACTION_NONE", + }, + ExploitationActivity: "NO_KNOWN", + Id: "CVE-2024-35863", + Impact: "LOW", + ObservedInTheWild: false, + References: nil, + UpstreamFixAvailable: true, + ZeroDay: false, + }, + OffendingPackage: &securitycenter.Package{ + CpeUri: "cpe:/o:debian:debian_linux:12", + PackageName: "linux", + PackageType: "OS", + PackageVersion: "6.1.76-1", + }, + FixedPackage: &securitycenter.Package{ + CpeUri: "cpe:/o:debian:debian_linux:12", + PackageName: "linux", + PackageType: "OS", + PackageVersion: "6.1.85-1", + }, + SecurityBulletin: nil, + }, + Description: "In the Linux kernel, the following vulnerability has been resolved: smb: client: fix potential UAF in is_valid_oplock_break() Skip sessions that are being teared down (status == SES_EXITING) to avoid UAF.", + NextSteps: "Use the following resources to help you mitigate CVE-2024-35863.\n\n**More information about CVE-2024-35863**\n* https://security-tracker.debian.org/tracker/CVE-2024-35863\n* https://access.redhat.com/security/cve/CVE-2024-35863\n* https://www.suse.com/security/cve/CVE-2024-35863\n* http://people.ubuntu.com/~ubuntu-security/cve/CVE-2024-35863\n\n**Fixed location**\ncpe:/o:debian:debian_linux:12\n\n**Fixed package**\nlinux\n\n**Fixed version**\n6.1.85-1\n", + Kubernetes: &securitycenter.Kubernetes{ + Objects: []*securitycenter.Object{ + { + Kind: "Deployment", + Ns: "default", + Name: "my-deployment", + Containers: []*securitycenter.Container{ + { + Name: "us-west1-docker.pkg.dev/project-id/my-image:latest", + ImageId: "us-west1-docker.pkg.dev/project-id/my-image:latest", + }, + }, + }, + }, + }, + }, + }, nil +} + +func (api *GCPAPIFake) ListServiceAccount(_ context.Context, name string) ([]*iam.ServiceAccount, error) { + if name == "modron-other-test" { + return []*iam.ServiceAccount{ + { + Email: "account-3@modron-other-test", + }, + }, nil + } + return []*iam.ServiceAccount{ + { + Email: "user:account-1@modron-test", + }, + { + Email: "user:account-2@modron-test", + }, + }, nil +} + +func (api *GCPAPIFake) ListServiceAccountKeys(context.Context, string) ([]*iam.ServiceAccountKey, error) { + return []*iam.ServiceAccountKey{}, nil +} + +func (api *GCPAPIFake) ListServiceAccountKeyUsage(_ context.Context, _ string, _ *monitoring.QueryTimeSeriesRequest) *monitoring.ProjectsTimeSeriesQueryCall { + return &monitoring.ProjectsTimeSeriesQueryCall{} +} + +func (api *GCPAPIFake) ListSpannerDatabases(_ context.Context, _ string) ([]*spanner.Database, error) { + return []*spanner.Database{ + { + Name: "spanner-test-db-1", + }, + }, nil +} + +func (api *GCPAPIFake) ListSslPolicies(_ context.Context, _ string) ([]*compute.SslPolicy, error) { + return []*compute.SslPolicy{ + { + Kind: "compute#sslPolicy", + CreationTimestamp: "2021-10-04T02:19:20.925-07:00", + SelfLink: "https://www.googleapis.com/compute/v1/projects/modron-test/global/sslPolicies/modern-ssl-policy", + Name: "modern-ssl-policy", + Profile: "MODERN", + MinTlsVersion: "TLS_1_2", + }, + }, nil +} + +func (api *GCPAPIFake) ListSubNetworksByRegion(_ context.Context, _ string, region string) ([]*compute.Subnetwork, error) { + if region == "region-1" { + return []*compute.Subnetwork{ + { + Name: "subnetwork-private-access-should-not-be-reported", + IpCidrRange: "IpCdrRange1", + Purpose: "PRIVATE", + PrivateIpGoogleAccess: true, + }, + { + Name: "subnetwork-no-private-access-should-be-reported", + IpCidrRange: "IpCdrRange1", + Purpose: "PRIVATE", + PrivateIpGoogleAccess: false, + }, + { + Name: "psc-network-should-not-be-reported", + IpCidrRange: "IpCdrRange1", + Purpose: "PRIVATE_SERVICE_CONNECT", + PrivateIpGoogleAccess: false, + }, + }, nil + } + return []*compute.Subnetwork{}, nil +} + +func (api *GCPAPIFake) ListTargetHTTPSProxies(_ context.Context, _ string) ([]*compute.TargetHttpsProxy, error) { + return []*compute.TargetHttpsProxy{ + { + Name: "proxy-internal", + SslCertificates: []string{"/links/cert-managed"}, + UrlMap: "/links/url-map-internal", // TODO: Update all these links to match the value from the GCP API + }, { + Name: "proxy-external-no-modern", + SslCertificates: []string{"/links/cert-managed"}, + UrlMap: "/links/url-map-external-no-modern", // TODO: Update all these links to match the value from the GCP API + }, { + Name: "proxy-one-cert", + SslCertificates: []string{"/links/cert-managed"}, + UrlMap: "/links/url-map-no-modern", // TODO: Update all these links to match the value from the GCP API + }, + { + Name: "proxy-one-cert-modern-ssl", + SslCertificates: []string{"/links/cert-managed"}, + UrlMap: "/links/url-map-external-modern", + SslPolicy: "https://www.googleapis.com/compute/v1/projects/modron-test/global/sslPolicies/modern-ssl-policy", + }, + { + Name: "proxy-one-cert-modern-ssl", + SslCertificates: []string{"/links/cert-managed"}, + UrlMap: "/links/url-map-no-iap-modern", + SslPolicy: "https://www.googleapis.com/compute/v1/projects/modron-test/global/sslPolicies/modern-ssl-policy", + }, + { + Name: "proxy-one-cert-modern-ssl", + SslCertificates: []string{"/links/cert-managed"}, + UrlMap: "/links/url-map-iap-modern", + SslPolicy: "https://www.googleapis.com/compute/v1/projects/modron-test/global/sslPolicies/modern-ssl-policy", + }, + { + Name: "proxy-multi-cert", + SslCertificates: []string{"/links/cert-self-managed", "/links/cert-managed-2"}, + UrlMap: "/links/url-map-2", + SslPolicy: "https://www.googleapis.com/compute/v1/projects/modron-test/global/sslPolicies/modern-ssl-policy", + }, + }, nil +} + +func (api *GCPAPIFake) ListTargetSslProxies(_ context.Context, _ string) ([]*compute.TargetSslProxy, error) { + return []*compute.TargetSslProxy{ + { + Name: "sslproxy-0", + Service: "/links/backend-svc-external-modern", + SslCertificates: []string{"/links/cert-managed"}, + SslPolicy: "https://www.googleapis.com/compute/v1/projects/modron-test/global/sslPolicies/modern-ssl-policy", + }, + }, nil +} + +func (api *GCPAPIFake) ListURLMaps(_ context.Context, _ string) ([]*compute.UrlMap, error) { + return []*compute.UrlMap{ + { + Name: "url-map-external-modern", + DefaultService: "/links/backend-svc-external-modern", + SelfLink: "/links/url-map-external-modern", + }, + { + Name: "url-map-iap-modern", + DefaultService: "/links/backend-svc-iap", + SelfLink: "/links/url-map-iap-modern", + }, + { + Name: "url-map-no-iap-modern", + DefaultService: "/links/backend-svc-no-iap", + SelfLink: "/links/url-map-no-iap-modern", + }, + { + Name: "url-map-external-no-modern", + DefaultService: "/links/backend-svc-external-no-modern", + SelfLink: "/links/url-map-external-no-modern", + }, + { + Name: "url-map-internal", + DefaultService: "/links/backend-svc-internal", + SelfLink: "/links/url-map-internal", + }, + { + Name: "url-map-2", + DefaultService: "/links/backend-svc-external", + SelfLink: "/links/url-map-external", + PathMatchers: []*compute.PathMatcher{ + { + Name: "url-map-2-path", + DefaultService: "/links/backend-svc-external", + PathRules: []*compute.PathRule{ + { + Service: "/links/backend-svc-external", + Paths: []string{ + "some/where/*", + }, + }, + }, + }, + { + Name: "another-path-0", + DefaultService: "/links/backend-svc-internal", + }, + }, + }, + }, nil +} + +func (api *GCPAPIFake) ListUsersInGroup(_ context.Context, group string) ([]string, error) { + groups := map[string][]string{ + "groups/emptyGroup": {}, + "groups/group1": {"groups/group1/memberships/user1", "groups/group1/memberships/group2"}, + "groups/group2": {"groups/group2/memberships/user2"}, + } + g, ok := groups[group] + if ok { + return g, nil + } + return nil, fmt.Errorf("group %q doesn't exist", group) +} + +func (api *GCPAPIFake) ListZones(_ context.Context, _ string) ([]*compute.Zone, error) { + return []*compute.Zone{ + {Name: "zone-1"}, + {Name: "zone-2"}, + {Name: "zone-3"}, + }, nil +} + +func (api *GCPAPIFake) SearchIamPolicy(_ context.Context, _ string, query string) ([]*cloudasset.IamPolicySearchResult, error) { + switch { + case strings.Contains(query, "//cloudresourcemanager.googleapis.com/projects/"): + return []*cloudasset.IamPolicySearchResult{ + { + Policy: &cloudasset.Policy{ + Bindings: []*cloudasset.Binding{ + { + Members: []string{"owner@example.com"}, + Role: "roles/owner", + }, + }, + }, + Resource: "//cloudresourcemanager.googleapis.com/projects/project-1", + }, + }, nil + case strings.Contains(query, "//cloudresourcemanager.googleapis.com/folders/"): + return []*cloudasset.IamPolicySearchResult{ + { + Policy: &cloudasset.Policy{ + Bindings: []*cloudasset.Binding{ + { + Members: []string{"owner@example.com"}, + Role: "roles/owner", + }, + }, + }, + Resource: "//cloudresourcemanager.googleapis.com/folders/123", + }, + }, nil + case strings.Contains(query, "//cloudresourcemanager.googleapis.com/organizations/"): + return []*cloudasset.IamPolicySearchResult{ + { + Policy: &cloudasset.Policy{ + Bindings: []*cloudasset.Binding{ + { + Members: []string{"owner@example.com"}, + Role: "roles/owner", + }, + }, + }, + Resource: "//cloudresourcemanager.googleapis.com/organizations/11111", + }, + }, nil + default: + return nil, fmt.Errorf("invalid query %q", query) + } +} diff --git a/src/collector/gcpcollector/api_gcp.go b/src/collector/gcpcollector/api_gcp.go new file mode 100644 index 0000000..74df8c7 --- /dev/null +++ b/src/collector/gcpcollector/api_gcp.go @@ -0,0 +1,608 @@ +package gcpcollector + +import ( + "errors" + "fmt" + "math" + "math/rand" + "strings" + "time" + + "go.opentelemetry.io/otel/attribute" + "golang.org/x/net/context" + + // TODO: Use cloud.google.com packages + "google.golang.org/api/apikeys/v2" + "google.golang.org/api/cloudasset/v1" + "google.golang.org/api/cloudidentity/v1" + "google.golang.org/api/cloudresourcemanager/v3" + "google.golang.org/api/compute/v1" + "google.golang.org/api/container/v1" + "google.golang.org/api/googleapi" + "google.golang.org/api/iam/v1" + "google.golang.org/api/monitoring/v3" + "google.golang.org/api/securitycenter/v1" + "google.golang.org/api/spanner/v1" + sqladmin "google.golang.org/api/sqladmin/v1beta4" + "google.golang.org/api/storage/v1" + + "github.com/nianticlabs/modron/src/constants" +) + +const ( + projectAssetType = "cloudresourcemanager.googleapis.com/Project" + folderAssetType = "cloudresourcemanager.googleapis.com/Folder" + organizationAssetType = "cloudresourcemanager.googleapis.com/Organization" + k8sPodAssetType = "k8s.io/Pod" + k8sNamespaceAssetType = "k8s.io/Namespace" + excludeSysProjectsQuery = "NOT additionalAttributes.projectId:sys-" + searchForProject = `name="//cloudresourcemanager.googleapis.com/%s"` +) + +type GroupCacheEntry struct { + Creation time.Time + Members []string +} + +var ( + listGroupCache = map[string]GroupCacheEntry{} +) + +type GCPApi interface { + GetServiceAccountIAMPolicy(ctx context.Context, name string) (*iam.Policy, error) + ListAPIKeys(ctx context.Context, name string) ([]*apikeys.V2Key, error) + ListBackendServices(ctx context.Context, name string) ([]*compute.BackendService, error) + ListBuckets(ctx context.Context, name string) ([]*storage.Bucket, error) + ListBucketsIamPolicy(bucketID string) (*storage.Policy, error) + ListCertificates(ctx context.Context, name string) ([]*compute.SslCertificate, error) + ListCloudSQLDatabases(ctx context.Context, name string) ([]*sqladmin.DatabaseInstance, error) + ListClustersByZone(ctx context.Context, name string, zone string) ([]*container.Cluster, error) + ListFoldersIamPolicy(ctx context.Context, name string) (*cloudresourcemanager.Policy, error) + ListInstances(ctx context.Context, name string) ([]*compute.Instance, error) + ListNamespaces(ctx context.Context, name string) ([]*cloudasset.ResourceSearchResult, error) + ListPods(ctx context.Context, name string) ([]*cloudasset.ResourceSearchResult, error) + ListOrganizationsIamPolicy(ctx context.Context, name string) (*cloudresourcemanager.Policy, error) + ListProjectIamPolicy(ctx context.Context, name string) (*cloudresourcemanager.Policy, error) + ListRegions(ctx context.Context, name string) ([]*compute.Region, error) + ListResourceGroups(ctx context.Context, rgNames []string) ([]*cloudasset.ResourceSearchResult, error) + ListServiceAccount(ctx context.Context, name string) ([]*iam.ServiceAccount, error) + ListServiceAccountKeys(ctx context.Context, name string) ([]*iam.ServiceAccountKey, error) + ListServiceAccountKeyUsage(ctx context.Context, resourceGroup string, request *monitoring.QueryTimeSeriesRequest) *monitoring.ProjectsTimeSeriesQueryCall + ListSccFindings(ctx context.Context, name string) ([]*securitycenter.Finding, error) + ListSpannerDatabases(ctx context.Context, name string) ([]*spanner.Database, error) + ListSslPolicies(ctx context.Context, name string) ([]*compute.SslPolicy, error) + ListSubNetworksByRegion(ctx context.Context, name string, region string) ([]*compute.Subnetwork, error) + ListTargetHTTPSProxies(ctx context.Context, name string) ([]*compute.TargetHttpsProxy, error) + ListTargetSslProxies(ctx context.Context, name string) ([]*compute.TargetSslProxy, error) + ListURLMaps(ctx context.Context, name string) ([]*compute.UrlMap, error) + ListUsersInGroup(ctx context.Context, group string) ([]string, error) + ListZones(ctx context.Context, name string) ([]*compute.Zone, error) + // TODO: Get rid of the scope argument since it's not used + SearchIamPolicy(ctx context.Context, scope string, query string) ([]*cloudasset.IamPolicySearchResult, error) +} + +type GCPApiReal struct { + // TODO: Support multiple orgID in the future + orgID string + scope string + apiKeyService *apikeys.Service + cloudAssetService rateLimitedCloudAssetV1Service + cloudIdentityService *cloudidentity.Service + cloudResourceManagerService *cloudresourcemanager.Service + computeService *compute.Service + containerService *container.Service + iamService *iam.Service + monitoringService *monitoring.Service + securityCenterService *securitycenter.Service + spannerService *spanner.Service + sqlAdminService *sqladmin.Service + storageService *storage.Service +} + +func detailedGoogleError(e error, apiDetail string) error { + if e == nil { + return nil + } + var gErr *googleapi.Error + if errors.As(e, &gErr) { + gErr.Message = fmt.Sprintf("%s: %s", apiDetail, gErr.Message) + return gErr + } + return e +} + +func NewGCPApiReal(ctx context.Context, orgID string) (GCPApi, error) { + apiKeyService, err := apikeys.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("apikeys.NewService : %w", err) + } + cloudAssetService, err := cloudasset.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("cloudasset.NewService : %w", err) + } + cloudIdentityService, err := cloudidentity.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("cloudidentity.NewService : %w", err) + } + cloudresourcemanagerService, err := cloudresourcemanager.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("cloudresourcemanager.NewService : %w", err) + } + computeService, err := compute.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("compute.NewService : %w", err) + } + containerService, err := container.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("container.NewService : %w", err) + } + iamService, err := iam.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("iam.NewService : %w", err) + } + monitoringService, err := monitoring.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("monitoring.NewService: %w", err) + } + securitycenterService, err := securitycenter.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("securitycenter.NewService: %w", err) + } + spannerService, err := spanner.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("spanner.NewService: %w", err) + } + sqladminService, err := sqladmin.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("sqladmin.NewService: %w", err) + } + storageService, err := storage.NewService(ctx) + if err != nil { + return nil, fmt.Errorf("storage.NewService : %w", err) + } + + return &GCPApiReal{ + orgID: orgID, + scope: constants.GCPOrgIDPrefix + orgID, + apiKeyService: apiKeyService, + cloudAssetService: newRateLimitedCloudAssetInventoryV1(cloudAssetService.V1), + cloudIdentityService: cloudIdentityService, + cloudResourceManagerService: cloudresourcemanagerService, + computeService: computeService, + containerService: containerService, + iamService: iamService, + monitoringService: monitoringService, + securityCenterService: securitycenterService, + spannerService: spannerService, + sqlAdminService: sqladminService, + storageService: storageService, + }, nil +} + +func (api *GCPApiReal) GetServiceAccountIAMPolicy(ctx context.Context, name string) (policy *iam.Policy, err error) { + policy, err = api.iamService.Projects.ServiceAccounts.GetIamPolicy(name).Context(ctx).Do() + return policy, detailedGoogleError(err, "ServiceAccounts.GetIamPolicy") +} + +func (api *GCPApiReal) ListAPIKeys(ctx context.Context, name string) (apiKeys []*apikeys.V2Key, err error) { + err = api.apiKeyService.Projects.Locations.Keys.List(constants.ResourceWithProjectsPrefix(name)). + Context(ctx). + PageSize(apiKeysPageSize). + Pages(ctx, func(vlkr *apikeys.V2ListKeysResponse) error { + apiKeys = append(apiKeys, vlkr.Keys...) + return nil + }) + log.Debugf("%s fetched %d api keys", name, len(apiKeys)) + return apiKeys, detailedGoogleError(err, "ApiKey.List") +} + +func (api *GCPApiReal) ListBackendServices(ctx context.Context, name string) (backendSvcs []*compute.BackendService, err error) { + err = api.computeService.BackendServices.AggregatedList(constants.ResourceWithoutProjectsPrefix(name)).Context(ctx).Pages(ctx, func(bsal *compute.BackendServiceAggregatedList) error { + for _, be := range bsal.Items { + backendSvcs = append(backendSvcs, be.BackendServices...) + } + return nil + }) + log.Debugf("%s fetched %d backend services", name, len(backendSvcs)) + return backendSvcs, detailedGoogleError(err, "BackendServices.AggregatedList") +} + +func (api *GCPApiReal) ListBuckets(ctx context.Context, name string) (buckets []*storage.Bucket, err error) { + err = api.storageService.Buckets.List(constants.ResourceWithoutProjectsPrefix(name)).Context(ctx).Pages(ctx, func(b *storage.Buckets) error { + buckets = append(buckets, b.Items...) + return nil + }) + log.Debugf("%s fetched %d buckets", name, len(buckets)) + return buckets, detailedGoogleError(err, "Buckets.List") +} + +func (api *GCPApiReal) ListBucketsIamPolicy(bucketID string) (*storage.Policy, error) { + iamPolicies, err := api.storageService.Buckets.GetIamPolicy(bucketID).Do() + log.Debugf("fetched bucket %s iampolicy", bucketID) + return iamPolicies, detailedGoogleError(err, "Buckets.GetIamPolicy") +} + +func (api *GCPApiReal) ListCertificates(ctx context.Context, name string) (certs []*compute.SslCertificate, err error) { + err = api.computeService.SslCertificates.AggregatedList(constants.ResourceWithoutProjectsPrefix(name)).Context(ctx).Pages(ctx, func(scal *compute.SslCertificateAggregatedList) error { + for _, c := range scal.Items { + certs = append(certs, c.SslCertificates...) + } + return nil + }) + log.Debugf("%s fetched %d certificates", name, len(certs)) + return certs, detailedGoogleError(err, "SslCertificates.AggregatedList") +} + +func (api *GCPApiReal) ListCloudSQLDatabases(ctx context.Context, name string) (instances []*sqladmin.DatabaseInstance, err error) { + instanceSvc := sqladmin.NewInstancesService(api.sqlAdminService) + err = instanceSvc.List(constants.ResourceWithoutProjectsPrefix(name)).Context(ctx).Pages(ctx, func(ilr *sqladmin.InstancesListResponse) error { + instances = append(instances, ilr.Items...) + return nil + }) + log.Debugf("%s fetched %d cloudSql databases", name, len(instances)) + return instances, detailedGoogleError(err, "SqlAdmin.Instances.List") +} + +func (api *GCPApiReal) ListClustersByZone(ctx context.Context, name string, zone string) (clusters []*container.Cluster, err error) { + resp, err := api.containerService.Projects.Zones.Clusters.List(constants.ResourceWithoutProjectsPrefix(name), zone).Context(ctx).Do() + if err != nil { + return nil, detailedGoogleError(err, "Clusters.List") + } + log.Debugf("%s fetched %d clusters", name, len(clusters)) + return resp.Clusters, nil +} + +func (api *GCPApiReal) ListFoldersIamPolicy(ctx context.Context, name string) (*cloudresourcemanager.Policy, error) { + resp, err := api.cloudResourceManagerService.Folders. + GetIamPolicy(name, new(cloudresourcemanager.GetIamPolicyRequest)). + Context(ctx). + Do() + log.Debugf("fetched folder %s iam policy", name) + return resp, detailedGoogleError(err, "Folders.GetIamPolicy") +} + +func (api *GCPApiReal) ListInstances(ctx context.Context, name string) (instances []*compute.Instance, err error) { + err = api.computeService.Instances.AggregatedList(constants.ResourceWithoutProjectsPrefix(name)).Context(ctx).Pages(ctx, func(ial *compute.InstanceAggregatedList) error { + for _, ia := range ial.Items { + instances = append(instances, ia.Instances...) + } + return nil + }) + log.Debugf("%s fetched %d instances", name, len(instances)) + return instances, detailedGoogleError(err, "Instances.AggregatedList") +} + +func (api *GCPApiReal) ListNamespaces(ctx context.Context, scope string) (namespaces []*cloudasset.ResourceSearchResult, err error) { + if scope == "" { + // We don't list namespaces for the whole org, this is a too large amount of data. + return nil, fmt.Errorf("ListNamespaces: name is empty") + } + searchAllResources, err := api.cloudAssetService.SearchAllResources(ctx, scope) + if err != nil { + return nil, fmt.Errorf("SearchAllResources: %w", err) + } + err = searchAllResources.AssetTypes(k8sNamespaceAssetType). + PageSize(pageSize). + Pages(ctx, func(sarr *cloudasset.SearchAllResourcesResponse) error { + namespaces = append(namespaces, sarr.Results...) + return nil + }) + log.Debugf("%q fetched %d namespaces", scope, len(namespaces)) + return namespaces, detailedGoogleError(err, "ListNamespaces") +} + +func (api *GCPApiReal) ListPods(ctx context.Context, scope string) (pods []*cloudasset.ResourceSearchResult, err error) { + if scope == "" { + // We don't list namespaces for the whole org, this is a too large amount of data. + return nil, fmt.Errorf("ListPods: name is empty") + } + searchAllResources, err := api.cloudAssetService.SearchAllResources(ctx, scope) + if err != nil { + return nil, fmt.Errorf("SearchAllResources: %w", err) + } + err = searchAllResources.ReadMask("*").AssetTypes(k8sPodAssetType).Context(ctx).Pages(ctx, func(sarr *cloudasset.SearchAllResourcesResponse) error { + for _, r := range sarr.Results { + if r.State == "Running" { + pods = append(pods, r) + } + } + return nil + }) + log.Debugf("%q fetched %d pods", scope, len(pods)) + return pods, detailedGoogleError(err, "ListPods") +} + +func (api *GCPApiReal) ListOrganizationsIamPolicy(ctx context.Context, name string) (*cloudresourcemanager.Policy, error) { + resp, err := api.cloudResourceManagerService.Organizations. + GetIamPolicy(name, new(cloudresourcemanager.GetIamPolicyRequest)). + Context(ctx). + Do() + log.Debugf("fetched organization %s iam policy", name) + return resp, detailedGoogleError(err, "Organizations.GetIamPolicy") +} + +func (api *GCPApiReal) ListProjectIamPolicy(ctx context.Context, name string) (*cloudresourcemanager.Policy, error) { + resp, err := api.cloudResourceManagerService.Projects. + GetIamPolicy(constants.ResourceWithProjectsPrefix(name), &cloudresourcemanager.GetIamPolicyRequest{}). + Context(ctx). + Do() + log.Debugf("fetched project %s iam policy", name) + return resp, detailedGoogleError(err, "Project.GetIamPolicy") +} + +func (api *GCPApiReal) ListRegions(ctx context.Context, name string) (regions []*compute.Region, err error) { + ctx, span := tracer.Start(ctx, "ListRegions") + span.SetAttributes( + attribute.String(constants.TraceKeyName, name), + ) + defer span.End() + err = api.computeService.Regions. + List(constants.ResourceWithoutProjectsPrefix(name)). + Context(ctx). + Pages(ctx, func(rl *compute.RegionList) error { + regions = append(regions, rl.Items...) + return nil + }) + log.Debugf("%s fetched %d regions", name, len(regions)) + return regions, detailedGoogleError(err, "Regions.List") +} + +func (api *GCPApiReal) ListResourceGroups(ctx context.Context, rgNames []string) (resourceGroups []*cloudasset.ResourceSearchResult, err error) { + searchAllResources, err := api.cloudAssetService.SearchAllResources(ctx, api.scope) + if err != nil { + return nil, fmt.Errorf("SearchAllResources: %w", err) + } + call := searchAllResources. + AssetTypes(projectAssetType, folderAssetType, organizationAssetType). + Context(ctx) + rgNamesMap := make(map[string]struct{}) + for _, rgName := range rgNames { + rgNamesMap[rgName] = struct{}{} + } + var query string + if len(rgNames) == 1 { + query = fmt.Sprintf(searchForProject, rgNames[0]) + } else { + query = excludeSysProjectsQuery + } + err = call.Query(query). + Context(ctx). + Pages(ctx, func(sarr *cloudasset.SearchAllResourcesResponse) error { + for _, r := range sarr.Results { + if r.State == "ACTIVE" { + if len(rgNames) > 0 { + if _, ok := rgNamesMap[strings.TrimPrefix(r.Name, cloudResourceManagerPrefix)]; ok { + resourceGroups = append(resourceGroups, r) + } + } else { + resourceGroups = append(resourceGroups, r) + } + } + } + return nil + }) + log.WithField("rg_names", rgNames).Debugf("fetched %d resourcegroups", len(resourceGroups)) + return resourceGroups, detailedGoogleError(err, "ListResourceGroups") +} + +func (api *GCPApiReal) ListSccFindings(ctx context.Context, name string) (findings []*securitycenter.Finding, err error) { + ctx, span := tracer.Start(ctx, "ListSccFindings") + defer span.End() + err = api.securityCenterService.Projects.Sources.Findings.List(name+"/sources/-"). + Context(ctx). + Filter("state=\"ACTIVE\" AND NOT finding_class=\"THREAT\""). + PageSize(sccPageSize). + Pages(ctx, func(flr *securitycenter.ListFindingsResponse) error { + ctx, span := tracer.Start(ctx, "ListSccFindingsPage") + defer span.End() + if err := rlSCC.Wait(ctx); err != nil { + return err + } + for _, fr := range flr.ListFindingsResults { + findings = append(findings, fr.Finding) + } + return nil + }) + return +} + +func (api *GCPApiReal) ListServiceAccount(ctx context.Context, name string) (serviceaccounts []*iam.ServiceAccount, err error) { + err = api.iamService.Projects.ServiceAccounts.List(constants.ResourceWithProjectsPrefix(name)).Context(ctx). + Pages(ctx, func(lsar *iam.ListServiceAccountsResponse) error { + serviceaccounts = append(serviceaccounts, lsar.Accounts...) + return nil + }) + log.Debugf("%s fetched %d service accounts", name, len(serviceaccounts)) + return serviceaccounts, detailedGoogleError(err, "ServiceAccounts.List") +} + +func (api *GCPApiReal) ListServiceAccountKeys(ctx context.Context, name string) (keys []*iam.ServiceAccountKey, err error) { + resp, err := api.iamService.Projects.ServiceAccounts.Keys. + List(constants.ResourceWithProjectsPrefix(name)). + Context(ctx). + Do() + if err != nil { + return nil, detailedGoogleError(err, "ServiceAccounts.Keys.List") + } + keys = append(keys, resp.Keys...) + log.Debugf("%s fetched %d service account keys", name, len(keys)) + return keys, nil +} + +func (api *GCPApiReal) ListServiceAccountKeyUsage(ctx context.Context, resourceGroup string, request *monitoring.QueryTimeSeriesRequest) *monitoring.ProjectsTimeSeriesQueryCall { + return api.monitoringService.Projects.TimeSeries. + Query(constants.ResourceWithProjectsPrefix(resourceGroup), request). + Context(ctx) +} + +func (api *GCPApiReal) ListSpannerDatabases(ctx context.Context, name string) (dbs []*spanner.Database, err error) { + instanceSvc := spanner.NewProjectsInstancesService(api.spannerService) + err = instanceSvc.List(constants.ResourceWithoutProjectsPrefix(name)). + Context(ctx). + Pages(ctx, func(lir *spanner.ListInstancesResponse) error { + for _, instance := range lir.Instances { + databaseSvc := spanner.NewProjectsInstancesDatabasesService(api.spannerService) + return databaseSvc.List(instance.Name).Pages(ctx, func(ldr *spanner.ListDatabasesResponse) error { + dbs = append(dbs, ldr.Databases...) + return nil + }) + } + return nil + }) + log.Debugf("%s fetched %d spanner databases", name, len(dbs)) + return dbs, detailedGoogleError(err, "Spanner.Instances.List") +} + +func (api *GCPApiReal) ListSslPolicies(ctx context.Context, name string) (policies []*compute.SslPolicy, err error) { + err = api.computeService.SslPolicies. + AggregatedList(constants.ResourceWithoutProjectsPrefix(name)). + Context(ctx). + Pages(ctx, func(spal *compute.SslPoliciesAggregatedList) error { + for _, i := range spal.Items { + policies = append(policies, i.SslPolicies...) + } + return nil + }) + log.Debugf("%s fetched %d ssl policies", name, len(policies)) + return policies, detailedGoogleError(err, "SslPolicies,AggregatedList") +} + +func (api *GCPApiReal) ListSubNetworksByRegion(ctx context.Context, name string, region string) (subnetworks []*compute.Subnetwork, err error) { + if err := rlSubnetRanges.Wait(ctx); err != nil { + return nil, fmt.Errorf("snRangesLimiter.Wait: %w", err) + } + err = api.computeService.Subnetworks. + List(constants.ResourceWithoutProjectsPrefix(name), region). + Context(ctx). + Pages(ctx, func(sl *compute.SubnetworkList) error { + subnetworks = append(subnetworks, sl.Items...) + return nil + }) + log.Debugf("%s region %s fetched %d subnetworks", name, region, len(subnetworks)) + return subnetworks, detailedGoogleError(err, "Subnetworks.List") +} + +func (api *GCPApiReal) ListTargetHTTPSProxies(ctx context.Context, name string) (proxies []*compute.TargetHttpsProxy, err error) { + err = api.computeService.TargetHttpsProxies. + AggregatedList(constants.ResourceWithoutProjectsPrefix(name)). + Context(ctx). + Pages(ctx, func(thpal *compute.TargetHttpsProxyAggregatedList) error { + for _, p := range thpal.Items { + proxies = append(proxies, p.TargetHttpsProxies...) + } + return nil + }) + log.Debugf("%s fetched %d https proxies", name, len(proxies)) + return proxies, detailedGoogleError(err, "TargetHttpsProxies.AggregatedList") +} + +func (api *GCPApiReal) ListTargetSslProxies(ctx context.Context, name string) (proxies []*compute.TargetSslProxy, err error) { + err = api.computeService.TargetSslProxies. + List(constants.ResourceWithoutProjectsPrefix(name)). + Context(ctx). + Pages(ctx, func(tspl *compute.TargetSslProxyList) error { + proxies = append(proxies, tspl.Items...) + return nil + }) + log.Debugf("%s fetched %d ssl proxies", name, len(proxies)) + return proxies, detailedGoogleError(err, "TargetSslProxies.List") +} + +func (api *GCPApiReal) ListURLMaps(ctx context.Context, name string) (maps []*compute.UrlMap, err error) { + err = api.computeService.UrlMaps. + AggregatedList(constants.ResourceWithoutProjectsPrefix(name)). + Context(ctx). + Pages(ctx, func(umal *compute.UrlMapsAggregatedList) error { + for _, m := range umal.Items { + maps = append(maps, m.UrlMaps...) + } + return nil + }) + log.Debugf("%s fetched %d url maps", name, len(maps)) + return maps, detailedGoogleError(err, "UrlMaps.AggregatedList") +} + +func (api *GCPApiReal) ListUsersInGroup(ctx context.Context, group string) (groupMembers []string, err error) { + group = strings.TrimPrefix(group, "group:") + if e, ok := listGroupCache[group]; ok && time.Since(e.Creation) < time.Second*300 { + return e.Members, nil + } + groupID, err := api.cloudIdentityService.Groups.Lookup(). + Context(ctx). + GroupKeyId(group). + Do() + if err != nil { + return []string{}, fmt.Errorf("group lookup: %w", err) + } + err = api.cloudIdentityService.Groups.Memberships. + List(groupID.Name). + Context(ctx). + PageSize(pageSize). + Pages(ctx, func(lmr *cloudidentity.ListMembershipsResponse) error { + for _, m := range lmr.Memberships { + switch m.Type { + case "GROUP": + transitiveMembers, err := api.ListUsersInGroup(ctx, m.PreferredMemberKey.Id) + if err != nil { + return err + } + groupMembers = append(groupMembers, transitiveMembers...) + default: + groupMembers = append(groupMembers, m.PreferredMemberKey.Id) + } + } + return nil + }) + log.Debugf("%s fetched %d group members", group, len(groupMembers)) + listGroupCache[group] = GroupCacheEntry{Creation: time.Now(), Members: groupMembers} + return groupMembers, detailedGoogleError(err, "CloudIdentity.Groups.Memberships.List") +} + +func (api *GCPApiReal) ListZones(ctx context.Context, name string) (zones []*compute.Zone, err error) { + err = api.computeService.Zones. + List(constants.ResourceWithoutProjectsPrefix(name)). + Context(ctx). + Pages(ctx, func(zl *compute.ZoneList) error { + zones = append(zones, zl.Items...) + return nil + }) + log.Debugf("%s fetched %d zones", name, len(zones)) + return zones, detailedGoogleError(err, "Zones.List") +} + +func (api *GCPApiReal) SearchIamPolicy(ctx context.Context, scope string, query string) (iamPolicies []*cloudasset.IamPolicySearchResult, err error) { + attemptNumber := 1 + var waitSec float64 + start := time.Now() + for { + // TODO: Change scope to api.scope and get rid of the scope parameter + searchAllIamPolicies, err := api.cloudAssetService.SearchAllIamPolicies(ctx, scope) + if err != nil { + return nil, fmt.Errorf("SearchAllIamPolicies: %w", err) + } + err = searchAllIamPolicies. + Query(query). + Context(ctx). + Pages(ctx, func(resp *cloudasset.SearchAllIamPoliciesResponse) error { + iamPolicies = append(iamPolicies, resp.Results...) + return nil + }) + if err == nil { + break + } + if time.Since(start) > maxSecAcrossAttempts { + return nil, fmt.Errorf("SearchIamPolicy.tooLong: after %v seconds %w", time.Since(start), detailedGoogleError(err, fmt.Sprintf("IamPolicies.SearchAll.Query %q", query))) + } + if attemptNumber >= maxAttemptNumber { + return nil, fmt.Errorf("SearchIamPolicy.maxAttempt: after %v attempts %w", attemptNumber, detailedGoogleError(err, fmt.Sprintf("IamPolicies.SearchAll.Query %q", query))) + } + if !isErrorCodeRetryable(getErrorCode(err)) { + return nil, fmt.Errorf("SearchIamPolicy.notRetryableCode: %w", detailedGoogleError(err, fmt.Sprintf("IamPolicies.SearchAll.Query %q", query))) + } + waitSec = math.Min(math.Pow(2, float64(attemptNumber))+rand.Float64(), maxSecBtwAttempts) //nolint:mnd,gosec + time.Sleep(time.Duration(waitSec) * time.Second) + attemptNumber++ + } + log.Debugf("fetched %d iam policies", len(iamPolicies)) + return iamPolicies, nil +} diff --git a/src/collector/gcpcollector/api_key.go b/src/collector/gcpcollector/api_key.go new file mode 100644 index 0000000..13d88c3 --- /dev/null +++ b/src/collector/gcpcollector/api_key.go @@ -0,0 +1,52 @@ +package gcpcollector + +import ( + "fmt" + + "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/constants" + pb "github.com/nianticlabs/modron/src/proto/generated" + + "golang.org/x/net/context" + "google.golang.org/api/apikeys/v2" +) + +const ( + globalProjectResourceID = "%s/locations/global" +) + +func (collector *GCPCollector) ListAPIKeys(ctx context.Context, rgName string) (apiKeys []*pb.Resource, err error) { + name := fmt.Sprintf(globalProjectResourceID, constants.ResourceWithProjectsPrefix(rgName)) + + keys, err := collector.api.ListAPIKeys(ctx, name) + if err != nil { + return nil, err + } + for _, key := range keys { + // TODO : handle other types of GCP API keys restrictions + // example : BrowserKeyRestrictions , AndroidKeyRestrictions , etc.. + scopes := getAPIKeyScopes(key) + apiKeys = append(apiKeys, &pb.Resource{ + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, + Name: key.Name, + Parent: rgName, + Type: &pb.Resource_ApiKey{ + ApiKey: &pb.APIKey{ + Scopes: scopes, + }, + }, + }) + } + return apiKeys, err +} + +func getAPIKeyScopes(key *apikeys.V2Key) (scopes []string) { + if key.Restrictions == nil || key.Restrictions.ApiTargets == nil { + return nil + } + for _, apiTarget := range key.Restrictions.ApiTargets { + scopes = append(scopes, apiTarget.Service) + } + return scopes +} diff --git a/src/collector/gcpcollector/bucket.go b/src/collector/gcpcollector/bucket.go index 29ec108..9138cfd 100644 --- a/src/collector/gcpcollector/bucket.go +++ b/src/collector/gcpcollector/bucket.go @@ -7,7 +7,7 @@ import ( "time" "github.com/nianticlabs/modron/src/common" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" "golang.org/x/net/context" "google.golang.org/protobuf/types/known/durationpb" @@ -29,13 +29,12 @@ func getAccessType(members []string) pb.Bucket_AccessType { } // TODO: Check the ACL to detect if the bucket is public if uniform bucket-level access is disabled. -func (collector *GCPCollector) ListBuckets(ctx context.Context, resourceGroup *pb.Resource) ([]*pb.Resource, error) { - res, err := collector.api.ListBuckets(ctx, resourceGroup.Name) +func (collector *GCPCollector) ListBuckets(ctx context.Context, rgName string) (buckets []*pb.Resource, err error) { + res, err := collector.api.ListBuckets(ctx, rgName) if err != nil { return nil, err } - buckets := []*pb.Resource{} removeDefaultBindings := func(members []string) (filteredList []string) { for _, member := range members { if strings.HasPrefix(member, "projectViewer:") || strings.HasPrefix(member, "projectOwner:") || strings.HasPrefix(member, "projectEditor:") { @@ -52,7 +51,7 @@ func (collector *GCPCollector) ListBuckets(ctx context.Context, resourceGroup *p } accessType := pb.Bucket_ACCESS_UNKNOWN - permissions := []*pb.Permission{} + var permissions []*pb.Permission for _, binding := range iamPolicy.Bindings { bindingMembers := removeDefaultBindings(binding.Members) permissions = append(permissions, &pb.Permission{ @@ -91,10 +90,10 @@ func (collector *GCPCollector) ListBuckets(ctx context.Context, resourceGroup *p } } buckets = append(buckets, &pb.Resource{ - Uid: common.GetUUID(3), - ResourceGroupName: resourceGroup.Name, - Name: formatResourceName(bucket.Name, bucket.Id), - Parent: resourceGroup.Name, + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, + Name: bucket.Name, + Parent: rgName, IamPolicy: &pb.IamPolicy{ Permissions: permissions, }, diff --git a/src/collector/gcpcollector/cloudsql.go b/src/collector/gcpcollector/cloudsql.go index a720444..97db54d 100644 --- a/src/collector/gcpcollector/cloudsql.go +++ b/src/collector/gcpcollector/cloudsql.go @@ -1,24 +1,25 @@ package gcpcollector import ( + sqladmin "google.golang.org/api/sqladmin/v1beta4" + "github.com/nianticlabs/modron/src/common" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" "golang.org/x/net/context" ) -func (collector *GCPCollector) ListCloudSqlDatabases(ctx context.Context, resourceGroup *pb.Resource) ([]*pb.Resource, error) { - dbs, err := collector.api.ListCloudSqlDatabases(ctx, resourceGroup.Name) +func (collector *GCPCollector) ListCloudSQLDatabases(ctx context.Context, rgName string) (resources []*pb.Resource, err error) { + dbs, err := collector.api.ListCloudSQLDatabases(ctx, rgName) if err != nil { return nil, err } - resources := []*pb.Resource{} for _, instance := range dbs { dbResource := &pb.Resource{ - Uid: common.GetUUID(3), - ResourceGroupName: resourceGroup.Name, + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, Name: instance.Name, - Parent: resourceGroup.Name, + Parent: rgName, Type: &pb.Resource_Database{ Database: &pb.Database{ Type: "cloudsql", @@ -28,47 +29,53 @@ func (collector *GCPCollector) ListCloudSqlDatabases(ctx context.Context, resour }, } if instance.Settings != nil { - if instance.Settings.IpConfiguration != nil { - dbResource.GetDatabase().TlsRequired = instance.Settings.IpConfiguration.RequireSsl - if instance.Settings.IpConfiguration.AuthorizedNetworks != nil { - dbResource.GetDatabase().AuthorizedNetworksSettingAvailable = pb.Database_AUTHORIZED_NETWORKS_SET - authorizedNetworks := []string{} - for _, n := range instance.Settings.IpConfiguration.AuthorizedNetworks { - authorizedNetworks = append(authorizedNetworks, n.Value) - } - dbResource.GetDatabase().AuthorizedNetworks = authorizedNetworks - } else { - dbResource.GetDatabase().AuthorizedNetworksSettingAvailable = pb.Database_AUTHORIZED_NETWORKS_NOT_SET - } - if instance.Settings.IpConfiguration.Ipv4Enabled { - dbResource.GetDatabase().IsPublic = true - } - } - if instance.Settings.StorageAutoResize != nil { - dbResource.GetDatabase().AutoResize = *instance.Settings.StorageAutoResize - } - if instance.Settings.BackupConfiguration != nil { - dbResource.GetDatabase().BackupConfig = pb.Database_BACKUP_CONFIG_MANAGED - } else { - dbResource.GetDatabase().BackupConfig = pb.Database_BACKUP_CONFIG_DISABLED - } - switch instance.Settings.AvailabilityType { - case "ZONAL": - dbResource.GetDatabase().AvailabilityType = pb.Database_HA_ZONAL - case "REGIONAL": - dbResource.GetDatabase().AvailabilityType = pb.Database_HA_REGIONAL - default: - dbResource.GetDatabase().AvailabilityType = pb.Database_HA_UNKNOWN - } + setDbResourceSettings(instance, dbResource) } if instance.DiskEncryptionStatus != nil { dbResource.GetDatabase().Encryption = pb.Database_ENCRYPTION_USER_MANAGED } else { dbResource.GetDatabase().Encryption = pb.Database_ENCRYPTION_MANAGED } - resources = append(resources, dbResource) } return resources, nil } + +func setDbResourceSettings(instance *sqladmin.DatabaseInstance, dbResource *pb.Resource) { + db := dbResource.GetDatabase() + settings := instance.Settings + ipConfig := settings.IpConfiguration + if ipConfig != nil { + db.TlsRequired = ipConfig.RequireSsl + if ipConfig.AuthorizedNetworks == nil { + db.AuthorizedNetworksSettingAvailable = pb.Database_AUTHORIZED_NETWORKS_NOT_SET + } else { + db.AuthorizedNetworksSettingAvailable = pb.Database_AUTHORIZED_NETWORKS_SET + var authorizedNetworks []string + for _, n := range ipConfig.AuthorizedNetworks { + authorizedNetworks = append(authorizedNetworks, n.Value) + } + db.AuthorizedNetworks = authorizedNetworks + } + if ipConfig.Ipv4Enabled { + db.IsPublic = true + } + } + if settings.StorageAutoResize != nil { + db.AutoResize = *settings.StorageAutoResize + } + if settings.BackupConfiguration != nil { + db.BackupConfig = pb.Database_BACKUP_CONFIG_MANAGED + } else { + db.BackupConfig = pb.Database_BACKUP_CONFIG_DISABLED + } + switch settings.AvailabilityType { + case "ZONAL": + db.AvailabilityType = pb.Database_HA_ZONAL + case "REGIONAL": + db.AvailabilityType = pb.Database_HA_REGIONAL + default: + db.AvailabilityType = pb.Database_HA_UNKNOWN + } +} diff --git a/src/collector/gcpcollector/collector.go b/src/collector/gcpcollector/collector.go new file mode 100644 index 0000000..ec1f3b7 --- /dev/null +++ b/src/collector/gcpcollector/collector.go @@ -0,0 +1,367 @@ +package gcpcollector + +import ( + "errors" + "fmt" + "reflect" + "regexp" + "runtime" + "strings" + "sync" + "time" + + "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "golang.org/x/net/context" + "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/utils" +) + +const ( + maxParallelCollections = 50 + uuidGenRetries = 3 +) + +var ( + log = logrus.StandardLogger().WithField(constants.LogKeyPkg, "gcpcollector") + sysGcpProjectRegex = regexp.MustCompile("^sys-[0-9]+") + tracer = otel.Tracer("github.com/nianticlabs/modron/src/collector/gcpcollector") + validGcpResourceGroupRegex = regexp.MustCompile(`((organizations|folders)/\d+|(projects/[-a-z0-9.:]+))`) +) + +type GCPCollector struct { + allowedSccCategories map[string]struct{} + additionalAdminRolesMap map[constants.Role]struct{} + api GCPApi + orgID string + orgSuffix string + storage model.Storage + tagConfig risk.TagConfig + + metrics metrics +} + +func (collector *GCPCollector) CollectAndStoreAll(ctx context.Context, collectID string, resourceGroupNames []string, preCollectedRgs []*pb.Resource) error { + ctx, span := tracer.Start(ctx, "CollectAndStoreAll") + defer span.End() + resourceGroupNames = filterValidResourceGroupNames(resourceGroupNames) + collectAndStoreSemaphore := make(chan struct{}, maxParallelCollections) + errGroup := new(errgroup.Group) + for _, rgName := range resourceGroupNames { + collectAndStoreSemaphore <- struct{}{} + errGroup.Go(func() error { + ctx, span := tracer.Start(ctx, "CollectAndStoreAllInRg", + trace.WithNewRoot(), + trace.WithLinks(trace.Link{SpanContext: trace.SpanContextFromContext(ctx)}), + trace.WithAttributes(attribute.String(constants.TraceKeyResourceGroup, rgName)), + ) + defer span.End() + defer func() { <-collectAndStoreSemaphore }() + log := log.WithField("resource_group", rgName) + if err := collector.collectAndStoreAllInRg(ctx, collectID, rgName, preCollectedRgs); err != nil { + span.RecordError(err) + log.WithError(err).Errorf("Failed to collect and store for resource group %s: %v", rgName, err) + return err + } + return nil + }) + } + return errGroup.Wait() +} + +func New( + ctx context.Context, + storage model.Storage, + orgID string, + orgSuffix string, + additionalAdminRoles []string, + tagConfig risk.TagConfig, + allowedSccCategories []string, +) (model.Collector, error) { + if strings.HasPrefix(orgID, constants.GCPOrgIDPrefix) { + strippedOrgID := strings.TrimPrefix(orgID, constants.GCPOrgIDPrefix) + log.Warnf("orgID \"%s\" is deprecated, use \"%s\" instead", orgID, strippedOrgID) + orgID = strippedOrgID + } + api, err := NewGCPApiReal(ctx, orgID) + if err != nil { + return nil, fmt.Errorf("container.NewService: %w", err) + } + m := initMetrics() + additionalAdminRolesMap := map[constants.Role]struct{}{} + for _, role := range additionalAdminRoles { + additionalAdminRolesMap[constants.ToRole(role)] = struct{}{} + } + allowedSccCategoriesMap := map[string]struct{}{} + for _, category := range allowedSccCategories { + allowedSccCategoriesMap[category] = struct{}{} + } + return &GCPCollector{ + allowedSccCategories: allowedSccCategoriesMap, + api: api, + additionalAdminRolesMap: additionalAdminRolesMap, + storage: storage, + orgID: orgID, + orgSuffix: orgSuffix, + metrics: m, + tagConfig: tagConfig, + }, nil +} + +func filterValidResourceGroupNames(resourceGroupNames []string) (filteredNames []string) { + for _, name := range resourceGroupNames { + if sysGcpProjectRegex.MatchString(name) { + continue + } + if !validGcpResourceGroupRegex.MatchString(name) { + log.Warnf("invalid resource group name: %q", name) + continue + } + filteredNames = append(filteredNames, name) + } + return filteredNames +} + +// collectAndStoreResources should not be used directly, use collectAndStoreAllInRg instead +func (collector *GCPCollector) collectAndStoreResources(ctx context.Context, collectID string, rgName string) []error { + ctx, span := tracer.Start(ctx, "collectAndStoreResources") + span.SetAttributes( + attribute.String(constants.TraceKeyCollectID, collectID), + attribute.String(constants.TraceKeyResourceGroup, rgName), + ) + defer span.End() + rg, err := collector.GetResourceGroupWithIamPolicy(ctx, collectID, rgName) + if err != nil { + return []error{err} + } + resources, errArr := collector.ListResourceGroupResources(ctx, collectID, rgName) + resources = append(resources, rg) + if _, err := collector.storage.BatchCreateResources(ctx, resources); err != nil { + errArr = append(errArr, err) + } + log.Infof("%s found %+v resources", rgName, len(resources)) + return errArr +} + +func (collector *GCPCollector) collectAndStoreObservations(ctx context.Context, collectID string, rgName string, preCollectedRgs []*pb.Resource) []error { + ctx, span := tracer.Start(ctx, "collectAndStoreObservations") + span.SetAttributes( + attribute.String(constants.TraceKeyCollectID, collectID), + attribute.String(constants.TraceKeyResourceGroup, rgName), + ) + defer span.End() + rg, err := collector.GetResourceGroupWithIamPolicy(ctx, collectID, rgName) + if err != nil { + return []error{err} + } + obs, errArr := collector.ListResourceGroupObservations(ctx, collectID, rgName) + if len(errArr) > 0 { + return errArr + } + + rgHierarchy, err := utils.ComputeRgHierarchy(preCollectedRgs) + if err != nil { + return []error{fmt.Errorf("computeRgHierarchy: %w", err)} + } + + impact, reason := risk.GetImpact(collector.tagConfig, rgHierarchy, rgName) + for k, o := range obs { + obs[k].Impact = impact + obs[k].ImpactReason = reason + obs[k].RiskScore = risk.GetRiskScore(impact, o.Severity) + } + if _, err = collector.storage.BatchCreateObservations(ctx, obs); err != nil { + errArr = append(errArr, err) + } + log.Infof("%s found %+v observations", rg.Name, len(obs)) + collector.logCollectionStatus(ctx, collectID, rgName, pb.Operation_COMPLETED, "") + return errArr +} + +func (collector *GCPCollector) ListResourceGroupResources(ctx context.Context, collectID string, rgName string) ([]*pb.Resource, []error) { + ctx, span := tracer.Start(ctx, "ListResourceGroupResources") + defer span.End() + span.SetAttributes( + attribute.String(constants.TraceKeyCollectID, collectID), + attribute.String(constants.TraceKeyResourceGroup, rgName), + ) + projectCollectors := []GenericCollector[*pb.Resource]{ + collector.ListAPIKeys, + collector.ListBuckets, + collector.ListCloudSQLDatabases, + collector.ListKubernetesClusters, + collector.ListKubernetesNamespaces, + collector.ListKubernetesPods, + collector.ListLoadBalancers, + collector.ListNetworks, + collector.ListServiceAccounts, + collector.ListSpannerDatabases, + collector.ListVMInstances, + } + // TODO: Add organization collectors, if needed + var organizationCollectors []GenericCollector[*pb.Resource] + + collectors, errArr := chooseCollectors(rgName, projectCollectors, organizationCollectors) + var res []*pb.Resource + resMutex := sync.Mutex{} + errArrMutex := sync.Mutex{} + wg := sync.WaitGroup{} + for _, collector := range collectors { + wg.Add(1) + go func() { + ctx, span := tracer.Start(ctx, "RunCollector") + defer wg.Done() + defer span.End() + collValue := reflect.ValueOf(collector) + functionName := runtime.FuncForPC(collValue.Pointer()).Name() + log := log.WithField(constants.LogKeyCollector, functionName) + span.SetAttributes( + attribute.String(constants.TraceKeyCollector, functionName), + ) + collectedResources, err := collector.ExponentialBackoffRun(ctx, rgName) + if err != nil { + log.WithError(err).Errorf("ExponentialBackoffRun: %v", err) + span.RecordError(err) + span.SetStatus(codes.Error, "ExponentialBackoffRun failed") + errArrMutex.Lock() + errArr = append(errArr, err) + errArrMutex.Unlock() + } + for _, r := range collectedResources { + r.CollectionUid = collectID + r.Timestamp = timestamppb.Now() + resMutex.Lock() + res = append(res, r) + resMutex.Unlock() + } + }() + } + wg.Wait() + return res, errArr +} + +func (collector *GCPCollector) ListResourceGroupObservations(ctx context.Context, collectID string, rgName string) (obs []*pb.Observation, errArr []error) { + projectCollectors := []GenericCollector[*pb.Observation]{ + collector.ListSccFindings, + } + var organizationCollectors []GenericCollector[*pb.Observation] + var collectors []GenericCollector[*pb.Observation] + collectors, errArr = chooseCollectors(rgName, projectCollectors, organizationCollectors) + for _, collector := range collectors { + cLogger := log. + WithFields(logrus.Fields{ + constants.LogKeyCollector: fmt.Sprintf("%T", collector), + constants.LogKeyResourceGroup: rgName, + }) + cLogger.Info("Collecting observations") + collectedObs, err := collector.ExponentialBackoffRun(ctx, rgName) + if err != nil { + errArr = append(errArr, err) + cLogger.WithError(err).Errorf("Failed to collect some observations") + } else { + for _, o := range collectedObs { + o.CollectionId = utils.RefOrNull(collectID) + if o.Timestamp == nil { + o.Timestamp = timestamppb.Now() + } + obs = append(obs, o) + } + } + cLogger.Infof("Collection complete") + } + return +} + +func chooseCollectors[T any]( + rgName string, + projectCollectors []GenericCollector[T], + orgCollectors []GenericCollector[T], +) (collectors []GenericCollector[T], errors []error) { + switch { + case strings.HasPrefix(rgName, constants.GCPFolderIDPrefix): + collectors = []GenericCollector[T]{} + case strings.HasPrefix(rgName, constants.GCPOrgIDPrefix): + collectors = orgCollectors + case strings.HasPrefix(rgName, constants.GCPProjectsNamePrefix): + collectors = projectCollectors + default: + errors = append(errors, fmt.Errorf("no collectors for %q", rgName)) + return nil, errors + } + return +} + +func (collector *GCPCollector) logCollectionStatus(ctx context.Context, collectID, resourceGroupName string, status pb.Operation_Status, reason string) { + ctx, span := tracer.Start(ctx, "logCollectionStatus") + defer span.End() + log. + WithField(constants.LogKeyCollectID, collectID). + WithField(constants.LogKeyResourceGroup, resourceGroupName). + Infof("Logging collection status: %s", status.String()) + if err := collector.storage.AddOperationLog(ctx, + []*pb.Operation{{ + Id: collectID, + ResourceGroup: resourceGroupName, + Type: "collection", + StatusTime: timestamppb.New(time.Now()), + Status: status, + Reason: reason, + }}); err != nil { + span.RecordError(err) + log.Warnf("add operation log: %v", err) + } +} + +func (collector *GCPCollector) collectAndStoreAllInRg(ctx context.Context, collectID string, rgName string, preCollectedRgs []*pb.Resource) error { + ctx, span := tracer.Start(ctx, "collectAndStoreAllInRg") + defer span.End() + span.SetAttributes( + attribute.String(constants.TraceKeyCollectID, collectID), + attribute.String(constants.TraceKeyResourceGroup, rgName), + ) + collector.logCollectionStatus(ctx, collectID, rgName, pb.Operation_STARTED, "") + collectLogger := log. + WithFields(logrus.Fields{ + constants.LogKeyCollectID: collectID, + constants.LogKeyResourceGroup: rgName, + }) + errGroup := new(errgroup.Group) + errGroup.Go(func() error { + collectLogger.Infof("Starting collect resources") + collectErrs := collector.collectAndStoreResources(ctx, collectID, rgName) + collectLogger.WithError(errors.Join(collectErrs...)).Infof("Done collecting resources") + return errors.Join(collectErrs...) + }) + + errGroup.Go(func() error { + collectLogger.Infof("Starting collect observations") + collectErrs := collector.collectAndStoreObservations(ctx, collectID, rgName, preCollectedRgs) + collectLogger.WithError(errors.Join(collectErrs...)).Infof("Done collecting observations") + return errors.Join(collectErrs...) + }) + flushOps := func() { + // Flush the ops log after the collection is done + if err := collector.storage.FlushOpsLog(ctx); err != nil { + // If we cannot flush the ops log, it's not a big deal: + // the next time Modron starts, the pending operations are marked as complete. + collectLogger.Warnf("flush ops log: %v", err) + } + } + defer flushOps() + if err := errGroup.Wait(); err != nil { + collectLogger.Errorf("collectAndStoreAllInRg: %v", err) + collector.logCollectionStatus(ctx, collectID, rgName, pb.Operation_FAILED, err.Error()) + return err + } + collector.logCollectionStatus(ctx, collectID, rgName, pb.Operation_COMPLETED, "") + return nil +} diff --git a/src/collector/gcpcollector/collector_call.go b/src/collector/gcpcollector/collector_call.go new file mode 100644 index 0000000..8a2dadf --- /dev/null +++ b/src/collector/gcpcollector/collector_call.go @@ -0,0 +1,52 @@ +package gcpcollector + +import ( + "errors" + "time" + + "google.golang.org/api/googleapi" +) + +const ( + maxAttemptNumber = 100 + maxSecBtwAttempts = 30. + maxSecAcrossAttempts = time.Duration(3600 * time.Second) +) + +var ( + // Retry following errors: + // * 408: Request timeout + // * 429: Too many requests + // * 5XX: Server errors + retryableErrorCode = []int{408, 429, 500, 502, 503, 504} + // We are not interested in the following codes: + // * 403: Sometimes returned for non-existing resources. + // * 404: A resource can be tracked by modron and then deleted. + skippableErrorCodes = []int{403, 404} +) + +func getErrorCode(err error) int { + var e *googleapi.Error + if errors.As(err, &e) { + return e.Code + } + return 0 +} + +func isErrorCodeRetryable(errorCode int) bool { + for _, code := range retryableErrorCode { + if code == errorCode { + return true + } + } + return false +} + +func isErrorCodeSkippable(errorCode int) bool { + for _, code := range skippableErrorCodes { + if code == errorCode { + return true + } + } + return false +} diff --git a/src/collector/gcpcollector/collector_generic.go b/src/collector/gcpcollector/collector_generic.go new file mode 100644 index 0000000..41808dc --- /dev/null +++ b/src/collector/gcpcollector/collector_generic.go @@ -0,0 +1,43 @@ +package gcpcollector + +import ( + "context" + "fmt" + "math" + "math/rand" + "time" +) + +type GenericCollector[T any] func(ctx context.Context, rgName string) ([]T, error) + +func (call GenericCollector[T]) ExponentialBackoffRun(ctx context.Context, rgName string) ([]T, error) { + attemptNumber := 1 + var waitSec float64 + start := time.Now() + for { + collected, err := call.Run(ctx, rgName) + if err == nil { + return collected, nil + } + if !isErrorCodeRetryable(getErrorCode(err)) { + return nil, fmt.Errorf("ExponentialBackoffRun.notRetryableCode: %w ", err) + } + if time.Since(start) > maxSecAcrossAttempts { + return nil, fmt.Errorf("ExponentialBackoffRun.tooLong: after %v seconds %w ", time.Since(start), err) + } + if attemptNumber >= maxAttemptNumber { + return nil, fmt.Errorf("ExponentialBackoffRun.maxAttempt: after %v attempts %w ", attemptNumber, err) + } + waitSec = math.Min(math.Pow(2, float64(attemptNumber))+rand.Float64(), maxSecBtwAttempts) //nolint:mnd,gosec + time.Sleep(time.Duration(waitSec) * time.Second) + attemptNumber++ + } +} + +func (call GenericCollector[T]) Run(ctx context.Context, rgName string) ([]T, error) { + resources, err := call(ctx, rgName) + if isErrorCodeSkippable(getErrorCode(err)) { + return []T{}, nil + } + return resources, err +} diff --git a/src/collector/gcpcollector/collector_integration_test.go b/src/collector/gcpcollector/collector_integration_test.go new file mode 100644 index 0000000..4c53589 --- /dev/null +++ b/src/collector/gcpcollector/collector_integration_test.go @@ -0,0 +1,94 @@ +//go:build integration + +package gcpcollector + +import ( + "context" + "encoding/json" + "os" + "testing" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" + + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/storage/memstorage" +) + +func getCollector(ctx context.Context, t *testing.T) (model.Collector, model.Storage) { + storage := memstorage.New() + logrus.StandardLogger().SetLevel(logrus.DebugLevel) + logrus.StandardLogger().SetFormatter(&logrus.TextFormatter{ + ForceColors: true, + }) + + orgID := os.Getenv("ORG_ID") + if orgID == "" { + t.Fatalf("ORG_ID is empty") + } + orgSuffix := os.Getenv("ORG_SUFFIX") + if orgSuffix == "" { + t.Fatalf("ORG_SUFFIX is empty") + } + tagConfig := risk.TagConfig{} + coll, err := New(ctx, storage, orgID, orgSuffix, []string{}, tagConfig, []string{}) + if err != nil { + t.Fatalf("failed to create collector: %v", err) + } + return coll, storage +} + +func TestGetResourceGroups(t *testing.T) { + ctx := context.Background() + coll, _ := getCollector(ctx, t) + rgs, err := coll.ListResourceGroups(ctx, nil) + if err != nil { + t.Fatalf("failed to list resource groups: %v", err) + } + if len(rgs) == 0 { + t.Fatalf("no resource groups found") + } + t.Logf("rg=%+v", rgs) +} + +func TestGetSpecificResourceGroups(t *testing.T) { + ctx := context.Background() + coll, _ := getCollector(ctx, t) + rgs, err := coll.ListResourceGroups(ctx, []string{ + "projects/modron-dev", + "projects/modron", + }) + if err != nil { + t.Fatalf("failed to list resource groups: %v", err) + } + if len(rgs) != 2 { + t.Fatalf("expected 2 resource groups, got %d", len(rgs)) + } + t.Logf("rg=%+v", rgs) +} + +func TestCollect(t *testing.T) { + ctx := context.Background() + coll, storage := getCollector(ctx, t) + collectID := uuid.NewString() + err := coll.CollectAndStoreAll(ctx, collectID, []string{"projects/modron-dev"}, []*pb.Resource{}) + if err != nil { + t.Fatalf("failed to collect: %v", err) + } + + resources, err := storage.GetChildrenOfResource( + ctx, collectID, "", proto.String("ResourceGroup"), + ) + if err != nil { + t.Fatalf("GetChildrenOfResource: %v", err) + } + encoded, err := json.Marshal(resources) + if err != nil { + t.Fatalf("failed to marshal resources: %v", err) + } + + t.Logf("resources=%s", encoded) +} diff --git a/src/collector/gcpcollector/collector_rg_call.go b/src/collector/gcpcollector/collector_rg_call.go new file mode 100644 index 0000000..a24ace4 --- /dev/null +++ b/src/collector/gcpcollector/collector_rg_call.go @@ -0,0 +1,43 @@ +package gcpcollector + +import ( + "context" + "fmt" + "math" + "math/rand" + "time" + + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +// TODO: find a way to avoid code duplication: +// (the implementation of CollectorResourceGroupCall.ExponentialBackoffRun is very similar to GenericCollector.ExponentialBackoffRun) +type CollectorResourceGroupCall func(ctx context.Context, collectID string, rgName string) (*pb.Resource, error) + +func (call CollectorResourceGroupCall) ExponentialBackoffRun(ctx context.Context, collectID string, rgName string) (*pb.Resource, error) { + attemptNumber := 0 + var waitSec float64 + start := time.Now() + for { + resources, err := call.Run(ctx, collectID, rgName) + if err == nil { + return resources, nil + } + if !isErrorCodeRetryable(getErrorCode(err)) { + return nil, fmt.Errorf("ExponentialBackoffRun.notRetryableCode: %w ", err) + } + if time.Since(start) > maxSecAcrossAttempts { + return nil, fmt.Errorf("ExponentialBackoffRun.tooLong: after %v seconds %w", time.Since(start), err) + } + if attemptNumber >= maxAttemptNumber { + return nil, fmt.Errorf("ExponentialBackoffRun.maxAttempt: after %v attempts %w", attemptNumber, err) + } + waitSec = math.Min(math.Pow(2, float64(attemptNumber))+rand.Float64(), maxSecBtwAttempts) //nolint:mnd,gosec + time.Sleep(time.Duration(waitSec) * time.Second) + attemptNumber++ + } +} + +func (call CollectorResourceGroupCall) Run(ctx context.Context, collectID string, rgName string) (*pb.Resource, error) { + return call(ctx, collectID, rgName) +} diff --git a/src/collector/gcpcollector/collector_test.go b/src/collector/gcpcollector/collector_test.go new file mode 100644 index 0000000..30b799b --- /dev/null +++ b/src/collector/gcpcollector/collector_test.go @@ -0,0 +1,482 @@ +package gcpcollector + +import ( + "context" + "flag" + "reflect" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/storage/memstorage" +) + +var ( + collectorTestProjectID string + projectListFile string +) + +func init() { + flag.StringVar(&collectorTestProjectID, "projectId", testProjectID, "GCP project Id") + flag.StringVar(&projectListFile, "projectIdList", "resourceGroupList.txt", "GCP project Id list") +} + +const ( + testProjectID = "projects/modron-test" + collectID = "collectID-1" +) + +func TestResourceGroupResources(t *testing.T) { + ctx := context.Background() + storage := memstorage.New() + gcpCollector := NewFake(ctx, storage, risk.TagConfig{}) + + resourceGroup, err := gcpCollector.GetResourceGroupWithIamPolicy(ctx, collectID, testProjectID) + if err != nil { + t.Fatalf("No resourceGroup found: %v", err) + } + if resourceGroup.CollectionUid != collectID { + t.Errorf("wrong collectUid, want %v got %v", collectID, resourceGroup.CollectionUid) + } + + resourcesCollected, errArr := gcpCollector.ListResourceGroupResources(ctx, collectID, resourceGroup.Name) + for _, err := range errArr { + t.Errorf("%v", err) + } + + for _, r := range resourcesCollected { + if r.CollectionUid != collectID { + t.Errorf("wrong collectUid, want %v got %v", collectID, r.CollectionUid) + } + } + + wantResourcesCollected := 28 // TODO: Create a better test for this functionality + if len(resourcesCollected) != wantResourcesCollected { + t.Errorf("resources collected: got %d, want %d", len(resourcesCollected), wantResourcesCollected) + } +} + +func TestResourceGroup(t *testing.T) { + ctx := context.Background() + storage := memstorage.New() + gcpCollector := NewFake(ctx, storage, risk.TagConfig{}) + resourceGroup, err := gcpCollector.GetResourceGroupWithIamPolicy(ctx, collectID, testProjectID) + if err != nil { + t.Fatalf("No resourceGroup found: %v", err) + } + + if resourceGroup.Name != testProjectID { + t.Errorf("wrong resourceGroup Name: %v", resourceGroup.Name) + } + if len(resourceGroup.IamPolicy.Permissions) != 6 { + t.Errorf("iam policy count: got %d, want %d", len(resourceGroup.IamPolicy.Permissions), 5) + } + if resourceGroup.CollectionUid != collectID { + t.Errorf("wrong collectUid, want %v got %v", collectID, resourceGroup.CollectionUid) + } +} + +func modronTestResource(name string) *pb.Resource { + return &pb.Resource{ + Name: name, + Parent: "projects/modron-test", + ResourceGroupName: "projects/modron-test", + } +} + +func TestCollectAndStore(t *testing.T) { + logrus.StandardLogger().SetLevel(logrus.DebugLevel) + logrus.StandardLogger().SetFormatter(&logrus.TextFormatter{ForceColors: true}) + ctx := context.Background() + storage := memstorage.New() + gcpCollector := NewFake(ctx, storage, risk.TagConfig{}) + limitFilter := model.StorageFilter{ + Limit: 100, + } + + for _, testResourceID := range []string{"organizations/1111", testProjectID} { + err := gcpCollector.collectAndStoreAllInRg(ctx, collectID, testResourceID, nil) + if err != nil { + t.Errorf("collectAndStoreResources(ctx, %s, %s): %v", collectID, testResourceID, err) + } + } + + if err := storage.FlushOpsLog(ctx); err != nil { + t.Errorf("flush ops log: %v", err) + } + + got, err := storage.ListResources(ctx, limitFilter) + if err != nil { + t.Errorf("error storing resources: %v", err) + } + + for _, r := range got { + if r.CollectionUid != collectID { + t.Errorf("wrong collectUid, want %v got %v", collectID, r.CollectionUid) + } + } + + want := []*pb.Resource{ + { + Name: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster/k8s/namespaces/modron-test-namespace", + Parent: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster", + ResourceGroupName: "projects/modron-test", + }, + { + Name: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster/k8s/namespaces/modron-test-namespace-other", + Parent: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster", + ResourceGroupName: "projects/modron-test", + }, + modronTestResource("api-key-unrestricted-0"), + modronTestResource("api-key-unrestricted-1"), + modronTestResource("api-key-with-overbroad-scope-1"), + modronTestResource("api-key-without-overbroad-scope"), + modronTestResource("backend-svc-external-modern"), + modronTestResource("backend-svc-external-no-modern"), + modronTestResource("backend-svc-iap"), + modronTestResource("backend-svc-internal"), + modronTestResource("backend-svc-no-iap"), + { + Name: "bucket-2", + Parent: "projects/modron-test", + ResourceGroupName: "projects/modron-test", + IamPolicy: &pb.IamPolicy{ + Permissions: []*pb.Permission{ + { + Role: "storage.objectViewer", + Principals: []string{ + "serviceAccount:account-1@modron-test.iam.gserviceaccount.com", + }, + }, + { + Role: "storage.objectViewer", + Principals: []string{ + "serviceAccount:account-2@modron-test.iam.gserviceaccount.com", + }, + }, + }, + }, + }, + { + Name: "bucket-accessible-from-other-project", + Parent: "projects/modron-test", + ResourceGroupName: "projects/modron-test", + IamPolicy: &pb.IamPolicy{ + Permissions: []*pb.Permission{ + { + Role: "storage.legacyBucketOwner", + Principals: []string{ + "serviceAccount:account-3@modron-other-test.iam.gserviceaccount.com", + }, + }, + }, + }, + }, + { + Name: "bucket-public", + Parent: "projects/modron-test", + ResourceGroupName: "projects/modron-test", + IamPolicy: &pb.IamPolicy{ + Permissions: []*pb.Permission{ + { + Role: "storage.objectViewer", + Principals: []string{ + "allAuthenticatedUsers", + }, + }, + }, + }, + }, + { + Name: "bucket-public-allusers", + Parent: "projects/modron-test", + ResourceGroupName: "projects/modron-test", + IamPolicy: &pb.IamPolicy{ + Permissions: []*pb.Permission{ + { + Role: "storage.objectViewer", + Principals: []string{ + "allUsers", + }, + }, + }, + }, + }, + modronTestResource("cloudsql-report-not-enforcing-tls"), + modronTestResource("cloudsql-test-db-ok"), + modronTestResource("cloudsql-test-db-public-and-authorized-networks"), + modronTestResource("cloudsql-test-db-public-and-no-authorized-networks"), + // Groups belong to another parent resource - they shouldn't show up here as they have not been scanned + // as part of the collection phase for the specified resourcegroups (they're one level up) + modronTestResource("instance-1"), + { + Name: "modron-pod-test-name-1", + ResourceGroupName: "projects/modron-test", + Parent: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster/k8s/namespaces/modron-test-namespace", + }, + { + Name: "modron-pod-test-name-2", + ResourceGroupName: "projects/modron-test", + Parent: "//container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster/k8s/namespaces/modron-test-namespace", + }, + { + Name: "organizations/1111", + ResourceGroupName: "organizations/1111", + Link: "https://console.cloud.google.com/welcome?organizationId=1111", + IamPolicy: &pb.IamPolicy{ + Resource: nil, + Permissions: []*pb.Permission{ + { + Role: "owner", + Principals: []string{ + "user:account-1@example.com", + "user:account-2@example.com", + }, + }, + { + Role: "test2", + Principals: []string{ + "account-2@example.com", + }, + }, + { + Role: "iam.serviceAccountAdmin", + Principals: []string{ + "account-1@example.com", + }, + }, + { + Role: "dataflow.admin", + Principals: []string{ + "account-1@example.com", + }, + }, + { + Role: "viewer", + Principals: []string{ + "account-2@example.com", + }, + }, + }, + }, + }, + { + Name: "projects/modron-test", + ResourceGroupName: "projects/modron-test", + Link: "https://console.cloud.google.com/welcome?project=modron-test", + Parent: "folders/234", + Ancestors: []string{ + "folders/234", "folders/123", "organizations/1111", + }, + IamPolicy: &pb.IamPolicy{ + Permissions: []*pb.Permission{ + { + Role: "owner", + Principals: []string{"user:owner1@example.com", "user:owner2@example.com"}, + }, + { + Role: "test2", + Principals: []string{"serviceAccount:account-2@modron-test.iam.gserviceaccount.com"}, + }, + /* + `{role:"iam.serviceAccountAdmin", principals:["account-1@modron-test.iam.gserviceaccount.com"]}`, + `{role:"dataflow.admin", principals:["account-3@modron-other-test.iam.gserviceaccount.com"]}`, + `{role:"iam.serviceAccountAdmin", principals:["account-3@modron-other-test.iam.gserviceaccount.com"]}`, + `{role:"viewer", principals:["account-2@modron-test.iam.gserviceaccount.com"]}`, + */ + { + Role: "iam.serviceAccountAdmin", + Principals: []string{ + "serviceAccount:account-1@modron-test.iam.gserviceaccount.com", + }, + }, + { + Role: "dataflow.admin", + Principals: []string{ + "serviceAccount:account-3@modron-other-test.iam.gserviceaccount.com", + }, + }, + { + Role: "iam.serviceAccountAdmin", + Principals: []string{ + "serviceAccount:account-3@modron-other-test.iam.gserviceaccount.com", + }, + }, + { + Role: "viewer", + Principals: []string{ + "serviceAccount:account-2@modron-test.iam.gserviceaccount.com", + }, + }, + }, + }, + Labels: map[string]string{ + "contact1": "user-1_example_com", + "contact2": "user-2_example_com", + }, + }, + modronTestResource("psc-network-should-not-be-reported"), + modronTestResource("spanner-test-db-1"), + modronTestResource("subnetwork-no-private-access-should-be-reported"), + modronTestResource("subnetwork-private-access-should-not-be-reported"), + { + Name: "user:account-1@modron-test", + Parent: "projects/modron-test", + ResourceGroupName: "projects/modron-test", + IamPolicy: &pb.IamPolicy{ + Permissions: []*pb.Permission{ + { + Role: "iam.serviceAccountUser", + Principals: []string{"user:user-1@example.com"}, + }, + }, + }, + }, + { + Name: "user:account-2@modron-test", + Parent: "projects/modron-test", + ResourceGroupName: "projects/modron-test", + IamPolicy: &pb.IamPolicy{ + Permissions: []*pb.Permission{ + { + Role: "iam.serviceAccountUser", + Principals: []string{"user:user-1@example.com"}, + }, + }, + }, + }, + } + + if diff := cmp.Diff(want, got, protocmp.Transform(), + protocmp.IgnoreOneofs(&pb.Resource{}, "type"), + protocmp.IgnoreFields(&pb.Resource{}, "uid", "collection_uid", "timestamp"), + ); diff != "" { + t.Errorf("resources collected: -want +got\n%s", diff) + } +} + +func msgIsTimestamp(x reflect.Value) bool { + if !x.IsValid() || x.IsZero() || x.IsNil() { + return false + } + return x.Interface().(protocmp.Message).Descriptor().FullName() == "google.protobuf.Timestamp" +} + +func TestCollectAndStoreObservations(t *testing.T) { + logrus.StandardLogger().SetLevel(logrus.DebugLevel) + logrus.StandardLogger().SetFormatter(&logrus.TextFormatter{ForceColors: true}) + ctx := context.Background() + storage := memstorage.New() + gcpCollector := NewFake(ctx, storage, risk.TagConfig{}) + collectID := uuid.NewString() + + if err := gcpCollector.CollectAndStoreAll(ctx, collectID, []string{testProjectID}, nil); err != nil { + t.Fatalf("collectAndStoreObservations: %v", err) + } + if err := storage.AddOperationLog(ctx, []*pb.Operation{ + { + Id: collectID, + ResourceGroup: testProjectID, + Type: "scan", + Status: pb.Operation_COMPLETED, + }, + }); err != nil { + t.Fatalf("add operation log: %v", err) + } + if err := storage.FlushOpsLog(ctx); err != nil { + t.Errorf("flush ops log: %v", err) + } + got, err := storage.ListObservations(ctx, model.StorageFilter{Limit: 100}) + if err != nil { + t.Errorf("error storing observations: %v", err) + } + + want := []*pb.Observation{ + { + Name: "SQL_PUBLIC_IP", + CollectionId: proto.String(collectID), + Timestamp: timestamppb.Now(), + Remediation: &pb.Remediation{ + Description: "To lower your attack surface, Cloud SQL databases should not have public IPs. Private IPs provide improved network security and lower latency for your application.", + Recommendation: "Go to https://console.cloud.google.com/sql/instances/xyz/connections?project=project-id and click the \"Networking\" tab. Uncheck the \"Public IP\" checkbox and click \"SAVE\". If your instance is not configured to use a private IP, you will first have to enable private IP by following the instructions here: https://cloud.google.com/sql/docs/mysql/configure-private-ip#existing-private-instance", + }, + ResourceRef: &pb.ResourceRef{ + CloudPlatform: pb.CloudPlatform_GCP, + ExternalId: proto.String("//cloudsql.googleapis.com/projects/project-id/instances/xyz"), + GroupName: testProjectID, + }, + ExternalId: proto.String("//securitycenter.googleapis.com/projects/12345/sources/123/findings/48230f1978594ffb9d09a3cb1fe5e1b3"), + Source: pb.Observation_SOURCE_SCC, + Severity: pb.Severity_SEVERITY_MEDIUM, + + // We have no information about the folders here, so the impact is MEDIUM and the risk score is equal + // to the severity. + Impact: pb.Impact_IMPACT_MEDIUM, + RiskScore: pb.Severity_SEVERITY_MEDIUM, + Category: pb.Observation_CATEGORY_MISCONFIGURATION, + }, + } + if diff := cmp.Diff(want, got, protocmp.Transform(), + protocmp.IgnoreFields(&pb.Observation{}, "uid"), + cmpopts.EquateApproxTime(10*time.Second), + // Approximate comparison of timestamppb timestamps. + // https://github.com/golang/protobuf/issues/1347 + cmp.FilterPath( + func(p cmp.Path) bool { + if p.Last().Type() == reflect.TypeOf(protocmp.Message{}) { + a, b := p.Last().Values() + return msgIsTimestamp(a) && msgIsTimestamp(b) + } + return false + }, + cmp.Transformer("timestamppb", func(t protocmp.Message) time.Time { + if t["seconds"] == nil { + return time.Time{} + } + return time.Unix(t["seconds"].(int64), 0).UTC() + }), + ), + ); diff != "" { + t.Errorf("observations collected: -want +got\n%s", diff) + } +} + +func TestResourceGroupRegex(t *testing.T) { + validRGNames := []string{ + "projects/google.com:xyz", + "organizations/111111111111", + "folders/111111111111", + "folders/11111111111", + "projects/hello-world", + } + invalidRGNames := []string{ + "projects/!", + "organizations/example", + "folders/test", + } + for _, v := range validRGNames { + t.Run(v, func(t *testing.T) { + if !validGcpResourceGroupRegex.MatchString(v) { + t.Errorf("expected %s to be valid", v) + } + }) + } + + for _, v := range invalidRGNames { + t.Run(v, func(t *testing.T) { + if validGcpResourceGroupRegex.MatchString(v) { + t.Errorf("expected %s to be invalid", v) + } + }) + } +} diff --git a/src/collector/gcpcollector/kubernetes_cluster.go b/src/collector/gcpcollector/kubernetes_cluster.go new file mode 100644 index 0000000..7348645 --- /dev/null +++ b/src/collector/gcpcollector/kubernetes_cluster.go @@ -0,0 +1,202 @@ +package gcpcollector + +import ( + "encoding/json" + "strings" + "time" + + "golang.org/x/net/context" + "google.golang.org/api/container/v1" + "google.golang.org/protobuf/types/known/timestamppb" + "k8s.io/api/core/v1" + apimachineryv1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/constants" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" +) + +func (collector *GCPCollector) ListKubernetesClusters(ctx context.Context, rgName string) (kubernetesClusters []*pb.Resource, err error) { + clusters, err := collector.api.ListClustersByZone(ctx, rgName, "-") + if err != nil { + return nil, err + } + for _, cluster := range clusters { + nodeVersion := "" + for _, nodePool := range cluster.NodePools { + nodeVersion = nodePool.Version + } + var masterAuthorizedNetworks []string + if cluster.MasterAuthorizedNetworksConfig != nil { + for _, cidrBlock := range cluster.MasterAuthorizedNetworksConfig.CidrBlocks { + masterAuthorizedNetworks = append(masterAuthorizedNetworks, cidrBlock.CidrBlock) + } + } + privateCluster := false + if cluster.PrivateClusterConfig != nil { + privateCluster = cluster.PrivateClusterConfig.EnablePrivateNodes + } + + kubernetesClusters = append(kubernetesClusters, &pb.Resource{ + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, + Name: cluster.Name, + Parent: rgName, + Type: &pb.Resource_KubernetesCluster{ + KubernetesCluster: &pb.KubernetesCluster{ + Location: cluster.Location, + PrivateCluster: privateCluster, + MasterAuthorizedNetworks: masterAuthorizedNetworks, + MasterVersion: cluster.CurrentMasterVersion, + NodesVersion: nodeVersion, + Security: &pb.KubernetesCluster_Security{ + VulnerabilityScanning: toPbSecurityVulnScanningType(cluster.SecurityPostureConfig), + }, + }, + }, + }) + } + + return kubernetesClusters, nil +} + +func toPbSecurityVulnScanningType(config *container.SecurityPostureConfig) pb.KubernetesCluster_Security_VulnScanning { + if config == nil { + return pb.KubernetesCluster_Security_VULN_SCAN_DISABLED + } + + // https://cloud.google.com/kubernetes-engine/docs/reference/rest/v1beta1/projects.locations.clusters#Cluster.VulnerabilityMode + switch strings.ToUpper(config.VulnerabilityMode) { + case "VULNERABILITY_DISABLED": + return pb.KubernetesCluster_Security_VULN_SCAN_DISABLED + case "VULNERABILITY_BASIC": + return pb.KubernetesCluster_Security_VULN_SCAN_BASIC + case "VULNERABILITY_ENTERPRISE": + return pb.KubernetesCluster_Security_VULN_SCAN_ADVANCED + } + return pb.KubernetesCluster_Security_VULN_SCAN_UNKNOWN +} + +func (collector *GCPCollector) ListKubernetesNamespaces(ctx context.Context, rgName string) (namespaces []*pb.Resource, err error) { + ns, err := collector.api.ListNamespaces(ctx, rgName) + if err != nil { + return nil, err + } + for _, n := range ns { + createTime := parseTimeOrZero(n.CreateTime) + namespaces = append(namespaces, &pb.Resource{ + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, + Parent: n.ParentFullResourceName, + Name: n.Name, + Type: &pb.Resource_Namespace{ + Namespace: &pb.Namespace{ + Cluster: n.ParentFullResourceName, + CreationTime: timestamppb.New(createTime), + }, + }, + }) + } + return namespaces, nil +} + +func (collector *GCPCollector) ListKubernetesPods(ctx context.Context, rgName string) (pods []*pb.Resource, err error) { + ps, err := collector.api.ListPods(ctx, rgName) + if err != nil { + return nil, err + } + for _, p := range ps { + var pod v1.Pod + if len(p.VersionedResources) == 0 { + log.WithField(constants.LogKeyResourceGroup, rgName). + Warnf("no versioned resources found for pod %q", p.Name) + continue + } + if err := json.Unmarshal(p.VersionedResources[0].Resource, &pod); err != nil { + return nil, err + } + removeSensitiveFields(&pod) + + createTime := parseTimeOrZero(p.CreateTime) + + // Cleanup some fields we don't need in ObjectMeta + pod.ObjectMeta.ManagedFields = []apimachineryv1.ManagedFieldsEntry{} + + pods = append(pods, &pb.Resource{ + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, + Parent: p.ParentFullResourceName, + Name: utils.GetHumanReadableName(p.Name), + Type: &pb.Resource_Pod{ + Pod: &pb.Pod{ + // Extract the cluster name from the namespace name. + // Namespace names follow this format: + // "//container.googleapis.com/projects/project-id/locations/location/clusters/cluster-name/k8s/namespaces/namespace-name", + Cluster: removeNamespaceFromResourceName(p.ParentFullResourceName), + CreationTime: timestamppb.New(createTime), + Namespace: p.ParentFullResourceName, + Phase: phaseFromString(p.State), + Spec: &pod.Spec, + ObjectMeta: &pod.ObjectMeta, + }, + }, + }) + } + return pods, nil +} + +func removeNamespaceFromResourceName(resourceName string) string { + index := strings.LastIndex(resourceName, "/k8s/namespaces/") + if index == -1 { + return resourceName + } + return resourceName[:index] +} + +// removeSensitiveFields removes certain fields of the pod spec that might contain sensitive information. +// generally this shouldn't be a problem as secrets shouldn't be stored in the pod spec directly (but rather in a Secret), +// but since there is a likelihood that this might happen, we remove these fields just in case. +// TODO: remove this function and replace it with a more specific check based on entropy - and create observations for these cases +func removeSensitiveFields(pod *v1.Pod) { + for i := range pod.Spec.Containers { + for k, e := range pod.Spec.Containers[i].Env { + if e.Value != "" { + pod.Spec.Containers[i].Env[k].Value = "REDACTED" + } + } + } + for i := range pod.Spec.InitContainers { + for k, e := range pod.Spec.InitContainers[i].Env { + if e.Value != "" { + pod.Spec.InitContainers[i].Env[k].Value = "REDACTED" + } + } + } +} + +func parseTimeOrZero(timeString string) time.Time { + t, err := time.Parse(time.RFC3339, timeString) + if err != nil { + log.Errorf("cannot parse namespace creation time %q: %v", timeString, err) + return time.Time{} + } + return t +} + +func phaseFromString(s string) pb.Pod_Phase { + switch strings.ToUpper(s) { + case "RUNNING": + return pb.Pod_RUNNING + case "PENDING": + return pb.Pod_PENDING + case "SUCCEEDED": + return pb.Pod_SUCCEEDED + case "FAILED": + return pb.Pod_FAILED + case "UNKNOWN": + return pb.Pod_UNKNOWN + default: + return pb.Pod_UNKNOWN_PHASE + } +} diff --git a/src/collector/gcpcollector/kubernetes_cluster_test.go b/src/collector/gcpcollector/kubernetes_cluster_test.go new file mode 100644 index 0000000..62a95d1 --- /dev/null +++ b/src/collector/gcpcollector/kubernetes_cluster_test.go @@ -0,0 +1,106 @@ +//go:build integration + +package gcpcollector_test + +import ( + "context" + "fmt" + "os" + "testing" + + "k8s.io/api/core/v1" + + "github.com/nianticlabs/modron/src/collector/gcpcollector" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/storage/memstorage" + "github.com/nianticlabs/modron/src/utils" +) + +func getKubernetesClusterCollector(t *testing.T) (*pb.Resource, *gcpcollector.GCPCollector) { + t.Helper() + ctx := context.Background() + storage := memstorage.New() + orgID := os.Getenv("ORG_ID") + projectID := os.Getenv("PROJECT_ID") + if orgID == "" { + t.Skip("ORG_ID not set, skipping") + } + if projectID == "" { + t.Skip("PROJECT_ID not set, skipping") + } + orgSuffix := "" + tagConfig := risk.TagConfig{} + coll, err := gcpcollector.New(ctx, storage, orgID, orgSuffix, []string{}, tagConfig, []string{}) + if err != nil { + t.Fatal(err) + } + rsrc := &pb.Resource{ + Name: "projects/" + projectID, + ResourceGroupName: "projects/xyz", + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: projectID, + }, + }, + } + return rsrc, coll.(*gcpcollector.GCPCollector) +} + +func TestKubernetesCluster_ListKubernetesPods(t *testing.T) { + rsrc, coll := getKubernetesClusterCollector(t) + res, err := coll.ListKubernetesPods(context.Background(), rsrc.Name) + if err != nil { + t.Fatal(err) + } + if len(res) == 0 { + t.Fatal("No kubernetes pods found") + } + + for _, p := range res { + pod := p.GetPod() + if pod == nil { + t.Fatal("expected pod") + } + fmt.Printf("Pod %s\n", utils.GetHumanReadableName(p.Name)) + if pod.Spec == nil { + t.Fatal("pod spec cannot be nil") + } + + fmt.Printf("\tNamespace\t%s\n", utils.GetHumanReadableName(pod.Namespace)) + + if len(pod.Spec.Containers) == 0 { + t.Fatal("expected at least one container") + } + + for _, c := range pod.Spec.InitContainers { + fmt.Printf("\tInit Container\t%s\n", c.Name) + verifyEnvVarsAreRedacted(t, c.Env) + } + for _, c := range pod.Spec.Containers { + fmt.Printf("\tContainer\t%s\n", c.Name) + verifyEnvVarsAreRedacted(t, c.Env) + } + } +} + +func TestKubernetesCluster_ListClusters(t *testing.T) { + rsrc, coll := getKubernetesClusterCollector(t) + clusters, err := coll.ListKubernetesClusters(context.Background(), rsrc.Name) + if err != nil { + t.Fatal(err) + } + + for _, c := range clusters { + k8sCluster := c.GetKubernetesCluster() + t.Logf("Cluster %s:\n%+v\n", utils.GetHumanReadableName(c.Name), k8sCluster) + } +} + +func verifyEnvVarsAreRedacted(t *testing.T, vars []v1.EnvVar) { + for _, e := range vars { + if e.Value != "" && e.Value != "REDACTED" { + t.Errorf("expected env value for %s to be empty or REDACTED, got %s", e.Name, e.Value) + } + } +} diff --git a/src/collector/gcpcollector/load_balancer.go b/src/collector/gcpcollector/load_balancer.go new file mode 100644 index 0000000..e473444 --- /dev/null +++ b/src/collector/gcpcollector/load_balancer.go @@ -0,0 +1,325 @@ +package gcpcollector + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "regexp" + "time" + + "github.com/nianticlabs/modron/src/common" + pb "github.com/nianticlabs/modron/src/proto/generated" + + "golang.org/x/net/context" + "google.golang.org/api/compute/v1" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + externalMatch = regexp.MustCompile("EXTERNAL") + internalMatch = regexp.MustCompile("INTERNAL") + defaultSSLPolicy = &pb.SslPolicy{ // Default is COMPATIBLE with min TLS1.0 https://cloud.google.com/load-balancing/docs/ssl-policies-concepts + // TODO: Get this via the API + MinTlsVersion: pb.SslPolicy_TLS_1_0, + Profile: pb.SslPolicy_COMPATIBLE, + CreationDate: timestamppb.New(time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)), + Name: "GcpDefaultSslPolicy", + } + policyProfileMap = map[string]pb.SslPolicy_Profile{ + "COMPATIBLE": pb.SslPolicy_COMPATIBLE, + "MODERN": pb.SslPolicy_MODERN, + "RESTRICTED": pb.SslPolicy_RESTRICTED, + "CUSTOM": pb.SslPolicy_CUSTOM, + } + policyMinTLSVersionMap = map[string]pb.SslPolicy_MinTlsVersion{ + "TLS_1_0": pb.SslPolicy_TLS_1_0, + "TLS_1_1": pb.SslPolicy_TLS_1_1, + "TLS_1_2": pb.SslPolicy_TLS_1_2, + "TLS_1_3": pb.SslPolicy_TLS_1_3, + } +) + +func certsFromPemChain(pemChain string) (certs []*x509.Certificate, err error) { + if len(pemChain) == 0 { + return nil, fmt.Errorf("certificate chain is empty") + } + + next := []byte(pemChain) + for len(next) > 0 { + block, rest := pem.Decode(next) + if block == nil { + err = fmt.Errorf("unable to decode PEM-encoded certificate chain") + rest = nil + } else { + if cert, parseErr := x509.ParseCertificate(block.Bytes); parseErr != nil { + err = fmt.Errorf("X509 parsing: %w", parseErr) + } else { + certs = append(certs, cert) + } + } + next = rest + } + return +} + +func getSslPolicyForService( + service *compute.BackendService, + proxies []*compute.TargetHttpsProxy, + urlMaps []*compute.UrlMap, + sslPolicies []*compute.SslPolicy) (*pb.SslPolicy, error) { + + getSslPolicyWithServiceMatched := func(proxy *compute.TargetHttpsProxy) (*compute.SslPolicy, error) { + for _, sslPolicy := range sslPolicies { + if sslPolicy.SelfLink == proxy.SslPolicy { + return sslPolicy, nil + } + } + return nil, fmt.Errorf("sslPolicy for proxy %s not found", proxy.Name) + } + + handlePathMatchers := func(pathMatchers []*compute.PathMatcher, proxy *compute.TargetHttpsProxy) (*compute.SslPolicy, error) { + var sslPolicy *compute.SslPolicy + var err error + for _, pathMatch := range pathMatchers { + sslPolicy = nil + if service.SelfLink == pathMatch.DefaultService { + sslPolicy, err = getSslPolicyWithServiceMatched(proxy) + } else { + for _, pathRule := range pathMatch.PathRules { + if pathRule.Service == service.SelfLink { + sslPolicy, err = getSslPolicyWithServiceMatched(proxy) + break + } + } + } + if sslPolicy != nil { + return sslPolicy, err + } + } + return nil, fmt.Errorf("sslPolicy for proxy %s not found", proxy.Name) + } + + getPolicyForProxy := func(proxy *compute.TargetHttpsProxy) (*compute.SslPolicy, error) { + for _, urlMap := range urlMaps { + if proxy.UrlMap == urlMap.SelfLink && service.SelfLink == urlMap.DefaultService { + return getSslPolicyWithServiceMatched(proxy) + } + if proxy.UrlMap == urlMap.SelfLink { + sslPolicy, err := handlePathMatchers(urlMap.PathMatchers, proxy) + if sslPolicy != nil { + return sslPolicy, err + } + } + for _, pathMatch := range urlMap.PathMatchers { + if proxy.UrlMap == urlMap.SelfLink && service.SelfLink == pathMatch.DefaultService { + sslPolicy, err := getSslPolicyWithServiceMatched(proxy) + if sslPolicy != nil { + return sslPolicy, err + } + } + } + } + return nil, fmt.Errorf("sslPolicy for proxy %s not found", proxy.Name) + } + + usedPolicy := defaultSSLPolicy + for _, proxy := range proxies { + if proxy.SslPolicy != "" { + policy, err := getPolicyForProxy(proxy) + if err != nil { + // proxy uses the GCP Default Policy + continue + } + timeStamp, err := time.Parse(time.RFC3339, policy.CreationTimestamp) + if err != nil { + log.Errorf("SslPolicy %s: %s. %v", policy.Name, policy.CreationTimestamp, err) + continue + } + usedPolicy = &pb.SslPolicy{ + CreationDate: timestamppb.New(timeStamp), + Name: policy.Name, + Profile: policyProfileMap[policy.Profile], + CustomFeatures: policy.CustomFeatures, + EnabledFeatures: policy.EnabledFeatures, + MinTlsVersion: policyMinTLSVersionMap[policy.MinTlsVersion], + } + break + } + } + return usedPolicy, nil +} + +func getBackendServiceCerts( + service *compute.BackendService, + proxies []*compute.TargetHttpsProxy, + certs map[string]*compute.SslCertificate, + urlMaps []*compute.UrlMap, +) (serviceCerts []*compute.SslCertificate, err error) { + getCertsForURLMap := func(proxy *compute.TargetHttpsProxy, urlMap *compute.UrlMap) []*compute.SslCertificate { + var newCerts []*compute.SslCertificate + // TODO: `DefaultService` is not enough. Check `HostRules` too. + if proxy.UrlMap == urlMap.SelfLink && urlMap.DefaultService == service.SelfLink { + for _, url := range proxy.SslCertificates { + newCerts = append(newCerts, certs[url]) + } + } + return newCerts + } + getCertsForProxy := func(proxy *compute.TargetHttpsProxy) []*compute.SslCertificate { + var newCerts []*compute.SslCertificate + for _, urlMap := range urlMaps { + newCerts = append(newCerts, getCertsForURLMap(proxy, urlMap)...) + } + return newCerts + } + for _, proxy := range proxies { + serviceCerts = append(serviceCerts, getCertsForProxy(proxy)...) + } + return serviceCerts, nil +} + +func loadBalancerFromBackendService( + service *compute.BackendService, + proxiesByScope []*compute.TargetHttpsProxy, + certs map[string]*compute.SslCertificate, + urlMapsByScope []*compute.UrlMap, + sslPoliciesByScope []*compute.SslPolicy, +) (*pb.LoadBalancer, error) { + // Check whether there is a frontend for the backendservice + frontEndFound := false +FeCheck: + for _, proxy := range proxiesByScope { + for _, urlMap := range urlMapsByScope { + if proxy.UrlMap == urlMap.SelfLink && urlMap.DefaultService == service.SelfLink { + frontEndFound = true + break FeCheck + } + } + } + if !frontEndFound { + return nil, fmt.Errorf("no frontend defined for backend %q", service.Name) + } + loadBalancerType := pb.LoadBalancer_UNKNOWN_TYPE + if externalMatch.MatchString(service.LoadBalancingScheme) { + loadBalancerType = pb.LoadBalancer_EXTERNAL + } + if internalMatch.MatchString(service.LoadBalancingScheme) { + loadBalancerType = pb.LoadBalancer_INTERNAL + } + + var serviceCerts []*compute.SslCertificate + newServiceCerts, err := getBackendServiceCerts( + service, + proxiesByScope, + certs, + urlMapsByScope, + ) + if err != nil { + return nil, fmt.Errorf("unable to retrieve certificates for backend service %q: %w", service.Name, err) + } + serviceCerts = append(serviceCerts, newServiceCerts...) + var pbCerts []*pb.Certificate + for _, cert := range serviceCerts { + certType, err := common.TypeFromSslCertificate(cert) + if err != nil { + return nil, fmt.Errorf("retrieve %q: %w", cert.Name, err) + } + creationDate, err := time.Parse(time.RFC3339, cert.CreationTimestamp) + if err != nil { + return nil, fmt.Errorf("creation timestamp of %q: %w", cert.Name, err) + } + expirationDate, err := time.Parse(time.RFC3339, cert.ExpireTime) + if err != nil { + return nil, fmt.Errorf("expiration timestamp of certificate %q: %w", cert.Name, err) + } + // Parse the certificate chain. The certificate at index 0 is the leaf certificate. + certs, err := certsFromPemChain(cert.Certificate) + if err != nil { + return nil, fmt.Errorf("parse certificate chain of %q: %w", cert.Name, err) + } + pbCerts = append(pbCerts, &pb.Certificate{ + Type: certType, + DomainName: certs[0].Subject.CommonName, + SubjectAlternativeNames: certs[0].DNSNames, + CreationDate: timestamppb.New(creationDate), + ExpirationDate: timestamppb.New(expirationDate), + Issuer: certs[0].Issuer.CommonName, + SignatureAlgorithm: certs[0].SignatureAlgorithm.String(), + PemCertificateChain: cert.Certificate, + }) + } + usedSslPolicy, err := getSslPolicyForService( + service, + proxiesByScope, + urlMapsByScope, + sslPoliciesByScope, + ) + if err != nil { + return nil, err + } + var iap *pb.IAP + if service.Iap != nil { + iap = &pb.IAP{ + Enabled: service.Iap.Enabled, + CliendId: service.Iap.Oauth2ClientId, + } + } + + return &pb.LoadBalancer{ + Type: loadBalancerType, + Certificates: pbCerts, + SslPolicy: usedSslPolicy, + Iap: iap, + }, nil +} + +// TODO: Retrieve certificates for TCP/SSL LBs as well. This will require retrieving `TargetSslProxies`. +func (collector *GCPCollector) ListLoadBalancers(ctx context.Context, rgName string) (loadBalancers []*pb.Resource, err error) { + targetHTTPSProxies, err := collector.api.ListTargetHTTPSProxies(ctx, rgName) + if err != nil { + return nil, err + } + urlMaps, err := collector.api.ListURLMaps(ctx, rgName) + if err != nil { + return nil, err + } + certs, err := collector.api.ListCertificates(ctx, rgName) + if err != nil { + return nil, err + } + certsByURL := make(map[string]*compute.SslCertificate) + for _, cert := range certs { + certsByURL[cert.SelfLink] = cert + } + backendServices, err := collector.api.ListBackendServices(ctx, rgName) + if err != nil { + return nil, err + } + sslPolicies, err := collector.api.ListSslPolicies(ctx, rgName) + if err != nil { + return nil, err + } + + for _, backendService := range backendServices { + if lb, err := loadBalancerFromBackendService( + backendService, + targetHTTPSProxies, + certsByURL, + urlMaps, + sslPolicies, + ); err != nil { + log.Infof("no LB for backend service %s: %v", backendService.Name, err) + } else { + loadBalancers = append(loadBalancers, &pb.Resource{ + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, + Name: backendService.Name, + Parent: rgName, + Type: &pb.Resource_LoadBalancer{ + LoadBalancer: lb, + }, + }) + } + } + return loadBalancers, nil +} diff --git a/src/collector/gcpcollector/metrics.go b/src/collector/gcpcollector/metrics.go new file mode 100644 index 0000000..b9ab821 --- /dev/null +++ b/src/collector/gcpcollector/metrics.go @@ -0,0 +1,26 @@ +package gcpcollector + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" + + "github.com/nianticlabs/modron/src/constants" +) + +var meter = otel.Meter("github.com/nianticlabs/modron/src/collector/gcpcollector") + +type metrics struct { + SccCollectedObservations metric.Int64Counter +} + +func initMetrics() metrics { + sccCollectedObsCounter, err := meter.Int64Counter(constants.MetricsPrefix+"scc_collected_observations", + metric.WithDescription("Number of collected observations from SCC"), + ) + if err != nil { + log.Errorf("failed to create scc_collected_observations counter: %v", err) + } + return metrics{ + SccCollectedObservations: sccCollectedObsCounter, + } +} diff --git a/src/collector/gcpcollector/network.go b/src/collector/gcpcollector/network.go index 48a8604..0013993 100644 --- a/src/collector/gcpcollector/network.go +++ b/src/collector/gcpcollector/network.go @@ -1,8 +1,15 @@ package gcpcollector import ( + "fmt" + "sync" + + "go.opentelemetry.io/otel/attribute" + "golang.org/x/sync/errgroup" + "github.com/nianticlabs/modron/src/common" - "github.com/nianticlabs/modron/src/pb" + "github.com/nianticlabs/modron/src/constants" + pb "github.com/nianticlabs/modron/src/proto/generated" "golang.org/x/net/context" ) @@ -11,44 +18,72 @@ var subnetworkPurposeList = map[string]struct{}{ "PRIVATE": {}, } -func (collector *GCPCollector) ListNetworks(ctx context.Context, resourceGroup *pb.Resource) ([]*pb.Resource, error) { - regions, err := collector.api.ListRegions(ctx, resourceGroup.Name) +func (collector *GCPCollector) ListNetworks(ctx context.Context, rgName string) (networks []*pb.Resource, err error) { + ctx, span := tracer.Start(ctx, "ListNetworks") + span.SetAttributes( + attribute.String(constants.TraceKeyResourceGroup, rgName), + ) + defer span.End() + regions, err := collector.api.ListRegions(ctx, rgName) if err != nil { return nil, err } - - networkIps := map[string][]string{} - networkIds := map[string][]uint64{} - networkGoogleAccessV4 := map[string]bool{} + errGroup := new(errgroup.Group) + networkIPs := sync.Map{} + networkGoogleAccessV4 := sync.Map{} for _, region := range regions { - subNetworks, err := collector.api.ListSubNetworksByRegion(ctx, resourceGroup.Name, region.Name) - if err != nil { - return nil, err - } - for _, subNetwork := range subNetworks { - networkIds[subNetwork.Name] = append(networkIds[subNetwork.Name], subNetwork.Id) - networkIps[subNetwork.Name] = append(networkIps[subNetwork.Name], subNetwork.IpCidrRange) - if _, ok := subnetworkPurposeList[subNetwork.Purpose]; ok { - networkGoogleAccessV4[subNetwork.Name] = networkGoogleAccessV4[subNetwork.Name] || subNetwork.PrivateIpGoogleAccess - } else { - networkGoogleAccessV4[subNetwork.Name] = true - } - } + errGroup.Go(func() error { + return collector.fetchRegion(ctx, rgName, region.Name, &networkIPs, &networkGoogleAccessV4) + }) + } + if err := errGroup.Wait(); err != nil { + return nil, fmt.Errorf("failed to fetch regions: %w", err) } - networks := []*pb.Resource{} - for netName, Ips := range networkIps { + networkIPs.Range(func(netName, value interface{}) bool { + hasGoogleAccess, ok := networkGoogleAccessV4.Load(netName) + if !ok { + hasGoogleAccess = false + } networks = append(networks, &pb.Resource{ - Uid: common.GetUUID(3), - ResourceGroupName: resourceGroup.Name, - Name: formatResourceName(netName, networkIds[netName]), - Parent: resourceGroup.Name, + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, + Name: netName.(string), + Parent: rgName, Type: &pb.Resource_Network{ Network: &pb.Network{ - GcpPrivateGoogleAccessV4: networkGoogleAccessV4[netName], - Ips: Ips, + GcpPrivateGoogleAccessV4: hasGoogleAccess.(bool), + Ips: value.([]string), }, }, }) - } + return true + }) return networks, nil } + +func (collector *GCPCollector) fetchRegion( + ctx context.Context, + rgName, regionName string, + networkIPs, networkGoogleAccessV4 *sync.Map, +) error { + subNetworks, err := collector.api.ListSubNetworksByRegion(ctx, rgName, regionName) + if err != nil { + return fmt.Errorf("failed to list subnetworks in region %s: %w", regionName, err) + } + for _, subNetwork := range subNetworks { + netIPs, _ := networkIPs.LoadOrStore(subNetwork.Name, []string{}) + netIPs = append(netIPs.([]string), subNetwork.IpCidrRange) + networkIPs.Store(subNetwork.Name, netIPs) + + netGoogleAccessV4, ok := networkGoogleAccessV4.Load(subNetwork.Name) + if !ok { + netGoogleAccessV4 = false + } + if _, ok := subnetworkPurposeList[subNetwork.Purpose]; ok { + networkGoogleAccessV4.Store(subNetwork.Name, netGoogleAccessV4.(bool) || subNetwork.PrivateIpGoogleAccess) + } else { + networkGoogleAccessV4.Store(subNetwork.Name, true) + } + } + return nil +} diff --git a/src/collector/gcpcollector/network_test.go b/src/collector/gcpcollector/network_test.go new file mode 100644 index 0000000..a805d7a --- /dev/null +++ b/src/collector/gcpcollector/network_test.go @@ -0,0 +1,119 @@ +package gcpcollector + +import ( + "context" + "sort" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "google.golang.org/api/compute/v1" + "google.golang.org/protobuf/testing/protocmp" + + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/storage/memstorage" +) + +type testSlowGCPAPI struct { + GCPApi +} + +func (t *testSlowGCPAPI) ListRegions(_ context.Context, _ string) ([]*compute.Region, error) { + return []*compute.Region{ + { + Name: "us-central1", + }, + { + Name: "us-west1", + }, + { + Name: "us-east1", + }, + }, nil +} + +func (t *testSlowGCPAPI) ListSubNetworksByRegion(_ context.Context, _ string, region string) ([]*compute.Subnetwork, error) { + time.Sleep(1 * time.Second) + return []*compute.Subnetwork{ + { + Name: "subnet-" + region, + Purpose: "PRIVATE", + }, + }, nil +} + +func TestListNetworks(t *testing.T) { + ctx := context.Background() + st := memstorage.New() + tagConfig := risk.TagConfig{ + Environment: "111111111111/environment", + EmployeeData: "111111111111/employee_data", + CustomerData: "111111111111/customer_data", + } + coll, err := New(ctx, st, "111111", "example.com", []string{}, tagConfig, []string{}) + if err != nil { + t.Fatalf("failed to create collector: %v", err) + } + gcpColl := coll.(*GCPCollector) + gcpColl.api = &testSlowGCPAPI{} + got, err := coll.(*GCPCollector).ListNetworks(ctx, "projects/test-rg") + if err != nil { + t.Fatalf("failed to list networks: %v", err) + } + sort.Sort(resourceByName(got)) + want := []*pb.Resource{ + { + Name: "subnet-us-central1", + Parent: "projects/test-rg", + ResourceGroupName: "projects/test-rg", + Type: &pb.Resource_Network{ + Network: &pb.Network{ + Ips: []string{""}, + GcpPrivateGoogleAccessV4: false, + }, + }, + }, + { + Name: "subnet-us-east1", + Parent: "projects/test-rg", + ResourceGroupName: "projects/test-rg", + Type: &pb.Resource_Network{ + Network: &pb.Network{ + Ips: []string{""}, + GcpPrivateGoogleAccessV4: false, + }, + }, + }, + { + Name: "subnet-us-west1", + Parent: "projects/test-rg", + ResourceGroupName: "projects/test-rg", + Type: &pb.Resource_Network{ + Network: &pb.Network{ + Ips: []string{""}, + GcpPrivateGoogleAccessV4: false, + }, + }, + }, + } + if diff := cmp.Diff(want, got, protocmp.Transform(), protocmp.IgnoreFields(&pb.Resource{}, "uid")); diff != "" { + t.Errorf("ListNetworks() mismatch (-want +got):\n%s", diff) + } +} + +type resourceByName []*pb.Resource + +func (r resourceByName) Len() int { + return len(r) +} + +func (r resourceByName) Less(i, j int) bool { + return r[i].Name < r[j].Name +} + +func (r resourceByName) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +var _ sort.Interface = resourceByName(nil) diff --git a/src/collector/gcpcollector/ratelimiter.go b/src/collector/gcpcollector/ratelimiter.go new file mode 100644 index 0000000..b6008da --- /dev/null +++ b/src/collector/gcpcollector/ratelimiter.go @@ -0,0 +1,62 @@ +package gcpcollector + +import ( + "context" + "time" + + "golang.org/x/time/rate" + "google.golang.org/api/cloudasset/v1" +) + +const ( + // https://cloud.google.com/asset-inventory/docs/quota + cloudAssetSearchResourcesQuotaPerMinute = 350 + cloudAssetSearchIamQuotaPerMinute = 350 + cloudAssetSearchBurst = 1 + apiKeysPageSize = 300 + pageSize = 500 // Page size is capped at 500 even if a larger value is given. + sccPageSize = 1000 // The maximum number of results to return in a single response. Default is 10, minimum is 1, maximum is 1000. + sccReadBurst = 1 + sccReadRequestsPerMinute = 1000 + listSubnetsRangeQuota = 10_000 + listSubnetsRangeBurst = 5_000 +) + +var ( + rlResources = rate.NewLimiter(rate.Every(time.Minute/cloudAssetSearchResourcesQuotaPerMinute), cloudAssetSearchBurst) + rlIam = rate.NewLimiter(rate.Every(time.Minute/cloudAssetSearchIamQuotaPerMinute), cloudAssetSearchBurst) + rlSCC = rate.NewLimiter(rate.Every(time.Minute/sccReadRequestsPerMinute), sccReadBurst) + rlSubnetRanges = rate.NewLimiter(rate.Every(time.Minute/listSubnetsRangeQuota), listSubnetsRangeBurst) +) + +// newRateLimitedCloudAssetInventoryV1 returns a service similar to the CloudAssetV1Service that is rate limited according +// to the Cloud Asset Inventory API quotas. +func newRateLimitedCloudAssetInventoryV1(svc *cloudasset.V1Service) rateLimitedCloudAssetV1Service { + return &rateLimitedCAI{ + svc: svc, + } +} + +type rateLimitedCAI struct { + svc *cloudasset.V1Service +} + +// rateLimitedCloudAssetV1Service is an interface that implements rate limiting on the original CloudAssetV1Service +type rateLimitedCloudAssetV1Service interface { + SearchAllResources(ctx context.Context, scope string) (*cloudasset.V1SearchAllResourcesCall, error) + SearchAllIamPolicies(ctx context.Context, scope string) (*cloudasset.V1SearchAllIamPoliciesCall, error) +} + +func (r *rateLimitedCAI) SearchAllResources(ctx context.Context, scope string) (*cloudasset.V1SearchAllResourcesCall, error) { + if err := rlResources.Wait(ctx); err != nil { + return nil, err + } + return r.svc.SearchAllResources(scope).PageSize(pageSize), nil +} + +func (r *rateLimitedCAI) SearchAllIamPolicies(ctx context.Context, scope string) (*cloudasset.V1SearchAllIamPoliciesCall, error) { + if err := rlIam.Wait(ctx); err != nil { + return nil, err + } + return r.svc.SearchAllIamPolicies(scope).PageSize(pageSize), nil +} diff --git a/src/collector/gcpcollector/resource_group.go b/src/collector/gcpcollector/resource_group.go new file mode 100644 index 0000000..f211a7d --- /dev/null +++ b/src/collector/gcpcollector/resource_group.go @@ -0,0 +1,241 @@ +package gcpcollector + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + "google.golang.org/api/cloudasset/v1" + "google.golang.org/api/cloudresourcemanager/v3" + + "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +const ( + projectResourcePrefix = "//cloudresourcemanager.googleapis.com/projects/" + folderResourcePrefix = "//cloudresourcemanager.googleapis.com/folders/" + organizationResourcePrefix = "//cloudresourcemanager.googleapis.com/organizations/" + cloudResourceManagerPrefix = "//cloudresourcemanager.googleapis.com/" + welcomePage = "https://console.cloud.google.com/welcome" +) + +var ( + // Syntax https://cloud.google.com/asset-inventory/docs/query-syntax + projectOwnersQuery = fmt.Sprintf("resource:%s*", projectResourcePrefix) + foldersOwnersQuery = fmt.Sprintf("resource:%s*", folderResourcePrefix) + organizationsOwnersQuery = fmt.Sprintf("resource:%s*", organizationResourcePrefix) +) + +func (collector *GCPCollector) GetResourceGroupWithIamPolicy(ctx context.Context, collectID string, rgName string) (res *pb.Resource, err error) { + ctx, span := tracer.Start(ctx, "GetResourceGroup") + defer span.End() + + rgs, err := collector.ListResourceGroupsWithIamPolicies(ctx, []string{rgName}) + if err != nil { + return nil, err + } + if len(rgs) != 1 { + return nil, fmt.Errorf("found %d resource groups for %s, expected 1", len(rgs), rgName) + } + rgs[0].CollectionUid = collectID + return rgs[0], nil +} + +func (collector *GCPCollector) ListResourceGroupsWithIamPolicies(ctx context.Context, rgNames []string) ([]*pb.Resource, error) { + ctx, span := tracer.Start(ctx, "ListResourceGroupsWithIamPolicies") + defer span.End() + rgs, err := collector.ListResourceGroups(ctx, rgNames) + if err != nil { + return nil, fmt.Errorf("ListResourceGroups: %w", err) + } + for i, rg := range rgs { + var resp *cloudresourcemanager.Policy + switch { + case strings.HasPrefix(rg.Name, constants.GCPFolderIDPrefix): + resp, err = collector.api.ListFoldersIamPolicy(ctx, rg.Name) + case strings.HasPrefix(rg.Name, constants.GCPOrgIDPrefix): + resp, err = collector.api.ListOrganizationsIamPolicy(ctx, rg.Name) + default: // Default to project + resp, err = collector.api.ListProjectIamPolicy(ctx, rg.Name) + } + if err != nil { + return nil, fmt.Errorf("cannot get IAM policies for resource group %q: %w", rg.Name, err) + } + var permissions []*pb.Permission + for _, binding := range resp.Bindings { + permissions = append(permissions, &pb.Permission{ + Role: constants.ToRole(binding.Role).String(), + Principals: binding.Members, + }) + } + rgs[i].IamPolicy = &pb.IamPolicy{ + Permissions: permissions, + } + } + return rgs, nil +} + +func (collector *GCPCollector) ListResourceGroups(ctx context.Context, rgNames []string) (rgs []*pb.Resource, err error) { + ctx, span := tracer.Start(ctx, "ListResourceGroups") + defer span.End() + resourceGroups, err := collector.api.ListResourceGroups(ctx, rgNames) + if err != nil { + return nil, err + } + for _, rg := range resourceGroups { + var ancestors []string + ancestors = append(ancestors, rg.Folders...) + if rg.Organization != "" { + ancestors = append(ancestors, rg.Organization) + } + if rg.State != "ACTIVE" { + log.Warnf("resource group %s is not active", rg.Name) + continue + } + rgName := strings.TrimPrefix(rg.Name, cloudResourceManagerPrefix) + res := &pb.Resource{ + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, + Name: rgName, + DisplayName: rg.DisplayName, + Parent: strings.TrimPrefix(rg.ParentFullResourceName, cloudResourceManagerPrefix), + Link: getResourceGroupLink(rg.Name), + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: strings.TrimPrefix(rg.Name, cloudResourceManagerPrefix), + Name: rg.DisplayName, + }, + }, + Labels: rg.Labels, + Tags: toKV(rg.Tags), + Ancestors: ancestors, + } + rgs = append(rgs, res) + } + return rgs, nil +} + +func toKV(tags []*cloudasset.Tag) map[string]string { + kv := make(map[string]string) + for _, tag := range tags { + kv[tag.TagKey] = tag.TagValue + } + return kv +} + +func getResourceGroupLink(resourceName string) string { + parts := strings.SplitN(resourceName, "/", 2) //nolint:mnd + if len(parts) != 2 { //nolint:mnd + return "" + } + switch parts[0] { + case "projects": + return welcomePage + "?project=" + parts[1] + case "folders": + return welcomePage + "?folder=" + parts[1] + case "organizations": + return welcomePage + "?organizationId=" + parts[1] + } + return "" +} + +func (collector *GCPCollector) ListResourceGroupNames(ctx context.Context) (rgNames []string, err error) { + ctx, span := tracer.Start(ctx, "ListResourceGroupNames") + defer span.End() + resourceGroups, err := collector.ListResourceGroups(ctx, nil) + if err != nil { + return nil, err + } + for _, rg := range resourceGroups { + rgNames = append(rgNames, strings.TrimPrefix(rg.Name, cloudResourceManagerPrefix)) + } + log.Infof("found %d names", len(rgNames)) + return rgNames, nil +} + +func (collector *GCPCollector) listResourceGroupAdmins( + ctx context.Context, + resourceGroupAdmins map[string]map[string]struct{}, + iamPolicyResult []*cloudasset.IamPolicySearchResult, +) model.ACLCache { + ctx, span := tracer.Start(ctx, "listResourceGroupAdmins") + defer span.End() + for _, res := range iamPolicyResult { + if res.Policy == nil { + continue + } + resourceID := strings.TrimPrefix(res.Resource, cloudResourceManagerPrefix) + if strings.HasPrefix(resourceID, constants.GCPProjectsNamePrefix+constants.GCPSysProjectPrefix) { + log.Debugf("skipping system project %q", resourceID) + continue + } + if strings.HasPrefix(resourceID, constants.GCPFolderIDPrefix) { + // TODO: remove this once we support the hierarchical structure of GCP + log.Debugf("skipping folder %q", resourceID) + continue + } + // Allow admins to see all the resources, regardless on whether they are owned by someone or not. + resourceGroupAdmins["*"][resourceID] = struct{}{} + for _, binding := range res.Policy.Bindings { + theRole := constants.Role(strings.TrimPrefix(binding.Role, constants.GCPRolePrefix)) + _, hasAdminRole := constants.AdminRoles[theRole] + _, hasAdditionalAdminRole := collector.additionalAdminRolesMap[theRole] + if !hasAdminRole && !hasAdditionalAdminRole { + continue + } + + for _, u := range binding.Members { + users := []string{u} + var err error + if strings.HasPrefix(u, constants.GCPAccountGroupPrefix) && strings.HasSuffix(u, collector.orgSuffix) { + users, err = collector.api.ListUsersInGroup(ctx, u) + if err != nil { + log.Warnf("cannot list users in group %q: %v", u, err) + continue + } + } + for _, user := range users { + user = strings.TrimPrefix(user, constants.GCPUserAccountPrefix) + if strings.HasSuffix(user, collector.orgSuffix) { + if _, ok := resourceGroupAdmins[user]; !ok { + resourceGroupAdmins[user] = make(map[string]struct{}) + } + resourceGroupAdmins[user][resourceID] = struct{}{} + } + } + } + } + } + return resourceGroupAdmins +} + +func (collector *GCPCollector) ListResourceGroupAdmins(ctx context.Context) (model.ACLCache, error) { + ctx, span := tracer.Start(ctx, "ListResourceGroupAdmins") + defer span.End() + scope := constants.GCPOrgIDPrefix + collector.orgID + + resp, err := collector.api.SearchIamPolicy(ctx, scope, projectOwnersQuery) + if err != nil { + return nil, err + } + resourceGroupAdmins := model.ACLCache{} + resourceGroupAdmins["*"] = map[string]struct{}{} + resourceGroupAdmins = collector.listResourceGroupAdmins(ctx, resourceGroupAdmins, resp) + + resp, err = collector.api.SearchIamPolicy(ctx, scope, foldersOwnersQuery) + if err != nil { + return nil, err + } + resourceGroupAdmins = collector.listResourceGroupAdmins(ctx, resourceGroupAdmins, resp) + + resp, err = collector.api.SearchIamPolicy(ctx, scope, organizationsOwnersQuery) + if err != nil { + return nil, err + } + resourceGroupAdmins = collector.listResourceGroupAdmins(ctx, resourceGroupAdmins, resp) + + return resourceGroupAdmins, nil +} diff --git a/src/collector/gcpcollector/resource_group_test.go b/src/collector/gcpcollector/resource_group_test.go new file mode 100644 index 0000000..8e2b5d0 --- /dev/null +++ b/src/collector/gcpcollector/resource_group_test.go @@ -0,0 +1,31 @@ +package gcpcollector + +import "testing" + +func TestGetResourceGroupLink(t *testing.T) { + tc := []struct { + name string + expected string + }{ + { + name: "projects/project-1", + expected: "https://console.cloud.google.com/welcome?project=project-1", + }, + { + name: "folders/1000000", + expected: "https://console.cloud.google.com/welcome?folder=1000000", + }, + { + name: "organizations/1000000", + expected: "https://console.cloud.google.com/welcome?organizationId=1000000", + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + if got := getResourceGroupLink(tt.name); got != tt.expected { + t.Errorf("GetResourceGroupLink() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/src/collector/gcpcollector/scc_findings.go b/src/collector/gcpcollector/scc_findings.go new file mode 100644 index 0000000..4060959 --- /dev/null +++ b/src/collector/gcpcollector/scc_findings.go @@ -0,0 +1,233 @@ +package gcpcollector + +import ( + "context" + "fmt" + "regexp" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + + "github.com/google/uuid" + "google.golang.org/api/securitycenter/v1" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/constants" + modronmetric "github.com/nianticlabs/modron/src/metric" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" +) + +// https://cloud.google.com/security-command-center/docs/finding-classes +// https://cloud.google.com/security-command-center/docs/reference/rest/v1/organizations.sources.findings#findingclass +var reportingClasses = map[string]struct{}{ + "VULNERABILITY": {}, + "MISCONFIGURATION": {}, + "TOXIC_COMBINATION": {}, +} + +// This is a list of SCC categories that relate to containers +// we use this to filter out some packages that in the specified category do not make sense +// For example, the package "linux" is not relevant in the context of a container image +// because the kernel being used is not the one from the container image but the one from the host, thus +// we exclude it from `GKE_RUNTIME_OS_VULNERABILITY` +var skipPackagesByCategories = map[string]map[string]struct{}{ + "GKE_RUNTIME_OS_VULNERABILITY": { + "linux": {}, + }, + "GKE_RUNTIME_LANG_VULNERABILITY": {}, +} + +func (collector *GCPCollector) ListSccFindings(ctx context.Context, rgName string) (observations []*pb.Observation, err error) { + rgLogger := log.WithField(constants.LogKeyResourceGroup, rgName) + rgLogger.Info("Listing SCC findings") + sccFindings, err := collector.api.ListSccFindings(ctx, rgName) + if err != nil { + rgLogger.WithError(err).Errorf("Error listing SCC findings") + return nil, err + } + rgLogger.Infof("Found %d SCC findings", len(sccFindings)) + + for _, v := range sccFindings { + if _, ok := reportingClasses[v.FindingClass]; !ok { + continue + } + if _, ok := collector.allowedSccCategories[v.Category]; !ok { + log.Infof("Skipping SCC finding with category %q", v.Category) + continue + } + + if v.Vulnerability != nil && v.Vulnerability.OffendingPackage != nil { + offPkg := v.Vulnerability.OffendingPackage + if pbc, ok := skipPackagesByCategories[v.Category]; ok { + if _, ok := pbc[offPkg.PackageName]; ok { + log. + WithField(modronmetric.KeyCategory, v.Category). + WithField(modronmetric.KeyOffendingPackage, offPkg). + Infof("Skipping SCC finding because it has been excluded") + continue + } + } + } + + observations = append(observations, FindingToObservation(v, rgName)) + collector.metrics.SccCollectedObservations. + Add(ctx, 1, metric.WithAttributes( + attribute.String(modronmetric.KeyCategory, v.Category), + attribute.String(modronmetric.KeySeverity, v.Severity), + )) + } + rgLogger.Infof("Collected %d observations", len(observations)) + return +} + +func FindingToObservation(v *securitycenter.Finding, rgName string) *pb.Observation { + obsTime, err := time.Parse(time.RFC3339, v.EventTime) + if err != nil { + log.Warnf("unable to parse time: %v", err) + obsTime = time.Now() + } + return enrichObservation(&pb.Observation{ + Uid: uuid.NewString(), + Timestamp: timestamppb.New(obsTime), + Name: v.Category, + ExpectedValue: nil, + ObservedValue: nil, + Remediation: getRemediation(v), + ResourceRef: &pb.ResourceRef{ + GroupName: rgName, + ExternalId: utils.RefOrNull(v.ResourceName), + CloudPlatform: pb.CloudPlatform_GCP, + }, + ExternalId: utils.RefOrNull(fmt.Sprintf("//securitycenter.googleapis.com/%s", v.CanonicalName)), + Severity: fromSccSeverity(v.Severity), + Source: pb.Observation_SOURCE_SCC, + Category: fromSccFindingClass(v.FindingClass), + }, v) +} + +func getPackageValue(pkg *securitycenter.Package) *structpb.Value { + if pkg == nil { + return nil + } + return structpb.NewStringValue(fmt.Sprintf("%s %s", pkg.PackageName, pkg.PackageVersion)) +} + +var kubernetesClusterRegex = regexp.MustCompile("^//container.googleapis.com/projects/[^/]+/locations/[^/]+/clusters/([^/]+)$") + +// EnrichObservation enriches the observation by modifying some fields based on the finding +func enrichObservation(p *pb.Observation, v *securitycenter.Finding) *pb.Observation { + if v == nil { + return p + } + obsLogger := log. + WithField("observation", p.Name). + WithField("scc_finding_name", v.Name). + WithField("scc_finding_class", v.FindingClass). + WithField("scc_finding_category", v.Category) + if v.Vulnerability != nil { + if v.Vulnerability.FixedPackage != nil { + p.ExpectedValue = getPackageValue(v.Vulnerability.FixedPackage) + } + if v.Vulnerability.OffendingPackage != nil { + p.ObservedValue = getPackageValue(v.Vulnerability.OffendingPackage) + } + } + if v.Kubernetes != nil { //nolint:nestif + // We have a Kubernetes reference, let's use it + if kubernetesClusterRegex.MatchString(v.ResourceName) { + if len(v.Kubernetes.Objects) > 0 { + obj := v.Kubernetes.Objects[0] + extID := "" + if p.ResourceRef.ExternalId != nil { + extID = *p.ResourceRef.ExternalId + } + switch obj.Kind { + case "Deployment": + extID = fmt.Sprintf("%s/k8s/namespaces/%s/apps/deployments/%s", extID, obj.Ns, obj.Name) + case "DaemonSet": + extID = fmt.Sprintf("%s/k8s/namespaces/%s/apps/daemonsets/%s", extID, obj.Ns, obj.Name) + case "NodePool": + extID = fmt.Sprintf("%s/nodePools/%s", extID, obj.Name) + default: + obsLogger.Warnf("unhandled Kubernetes object kind: %s", obj.Kind) + } + p.ResourceRef.ExternalId = utils.RefOrNull(extID) + } else { + obsLogger.Warnf("no Kubernetes objects found for %s", v.ResourceName) + } + } + } + return p +} + +func getRemediation(v *securitycenter.Finding) *pb.Remediation { + if v == nil { + return nil + } + vuln := v.Vulnerability + if vuln != nil { + if vuln.OffendingPackage != nil && vuln.FixedPackage != nil { + return &pb.Remediation{ + Description: fmt.Sprintf("%s in %s %s%s: update to %s %s", + vuln.Cve.Id, + vuln.OffendingPackage.PackageName, vuln.OffendingPackage.PackageVersion, + getContainerImage(v.Kubernetes), + vuln.FixedPackage.PackageName, vuln.FixedPackage.PackageVersion, + ) + "\n\n" + v.Description, + Recommendation: v.NextSteps, + } + } + } + return &pb.Remediation{ + Description: v.Description, + Recommendation: v.NextSteps, + } +} + +func getContainerImage(kubernetes *securitycenter.Kubernetes) string { + if kubernetes == nil { + return "" + } + + if len(kubernetes.Objects) == 0 { + return "" + } + + firstObj := kubernetes.Objects[0] + if len(firstObj.Containers) == 0 { + return "" + } + + // TODO: We assume the first container of the first object is vulnerable - this might be wrong + return fmt.Sprintf(" (%s)", firstObj.Containers[0].ImageId) +} + +// https://cloud.google.com/security-command-center/docs/reference/rest/v1/organizations.sources.findings#findingclass +func fromSccFindingClass(findingClass string) pb.Observation_Category { + switch findingClass { + case "VULNERABILITY": + return pb.Observation_CATEGORY_VULNERABILITY + case "MISCONFIGURATION": + return pb.Observation_CATEGORY_MISCONFIGURATION + case "TOXIC_COMBINATION": + return pb.Observation_CATEGORY_TOXIC_COMBINATION + } + return pb.Observation_CATEGORY_UNKNOWN +} + +func fromSccSeverity(severity string) pb.Severity { + switch severity { + case "CRITICAL": + return pb.Severity_SEVERITY_CRITICAL + case "HIGH": + return pb.Severity_SEVERITY_HIGH + case "MEDIUM": + return pb.Severity_SEVERITY_MEDIUM + case "LOW": + return pb.Severity_SEVERITY_LOW + } + return pb.Severity_SEVERITY_UNKNOWN +} diff --git a/src/collector/gcpcollector/scc_findings_test.go b/src/collector/gcpcollector/scc_findings_test.go new file mode 100644 index 0000000..d5e4370 --- /dev/null +++ b/src/collector/gcpcollector/scc_findings_test.go @@ -0,0 +1,223 @@ +package gcpcollector + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "google.golang.org/api/securitycenter/v1" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" + + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/storage/memstorage" +) + +var ( + vuln1 = &securitycenter.Finding{ + CanonicalName: "projects/1234/sources/5678/locations/global/findings/0000", + Name: "projects/1234/sources/5678/locations/global/findings/0000", + Category: "GKE_RUNTIME_OS_VULNERABILITY", + ResourceName: "//container.googleapis.com/projects/modron0test/locations/us-west1/clusters/my-cluster", + Severity: "SEVERITY_UNSPECIFIED", + State: "ACTIVE", + CreateTime: "2024-05-19T13:20:02.112Z", + EventTime: "2024-07-04T15:47:51.202Z", + Vulnerability: &securitycenter.Vulnerability{ + Cve: &securitycenter.Cve{ + Cvssv3: &securitycenter.Cvssv3{ + BaseScore: 5.5, + AttackComplexity: "ATTACK_COMPLEXITY_LOW", + AttackVector: "ATTACK_VECTOR_LOCAL", + AvailabilityImpact: "IMPACT_HIGH", + ConfidentialityImpact: "IMPACT_NONE", + IntegrityImpact: "IMPACT_NONE", + PrivilegesRequired: "PRIVILEGES_REQUIRED_LOW", + Scope: "SCOPE_UNCHANGED", + UserInteraction: "USER_INTERACTION_NONE", + }, + ExploitationActivity: "NO_KNOWN", + Id: "CVE-2024-35863", + Impact: "LOW", + ObservedInTheWild: false, + References: nil, + UpstreamFixAvailable: true, + ZeroDay: false, + }, + OffendingPackage: &securitycenter.Package{ + CpeUri: "cpe:/o:debian:debian_linux:12", + PackageName: "linux", + PackageType: "OS", + PackageVersion: "6.1.76-1", + }, + FixedPackage: &securitycenter.Package{ + CpeUri: "cpe:/o:debian:debian_linux:12", + PackageName: "linux", + PackageType: "OS", + PackageVersion: "6.1.85-1", + }, + SecurityBulletin: nil, + }, + Description: "In the Linux kernel, the following vulnerability has been resolved: smb: client: fix potential UAF in is_valid_oplock_break() Skip sessions that are being teared down (status == SES_EXITING) to avoid UAF.", + NextSteps: "Use the following resources to help you mitigate CVE-2024-35863.\n\n**More information about CVE-2024-35863**\n* https://security-tracker.debian.org/tracker/CVE-2024-35863\n* https://access.redhat.com/security/cve/CVE-2024-35863\n* https://www.suse.com/security/cve/CVE-2024-35863\n* http://people.ubuntu.com/~ubuntu-security/cve/CVE-2024-35863\n\n**Fixed location**\ncpe:/o:debian:debian_linux:12\n\n**Fixed package**\nlinux\n\n**Fixed version**\n6.1.85-1\n", + Kubernetes: &securitycenter.Kubernetes{ + Objects: []*securitycenter.Object{ + { + Kind: "Deployment", + Ns: "default", + Name: "my-deployment", + Containers: []*securitycenter.Container{ + { + Name: "us-west1-docker.pkg.dev/project-id/my-image:latest", + ImageId: "us-west1-docker.pkg.dev/project-id/my-image:latest", + }, + }, + }, + }, + }, + } +) + +func TestFindingToObservation(t *testing.T) { + got := FindingToObservation(vuln1, "projects/modron-test") + + want := &pb.Observation{ + ExpectedValue: structpb.NewStringValue("linux 6.1.85-1"), + ObservedValue: structpb.NewStringValue("linux 6.1.76-1"), + Uid: "projects/1234/sources/5678/locations/global/findings/0000", + Name: "GKE_RUNTIME_OS_VULNERABILITY", + Remediation: &pb.Remediation{ + Description: "CVE-2024-35863 in linux 6.1.76-1 (us-west1-docker.pkg.dev/project-id/my-image:latest): update to linux 6.1.85-1\n\nIn the Linux kernel, the following vulnerability has been resolved: smb: client: fix potential UAF in is_valid_oplock_break() Skip sessions that are being teared down (status == SES_EXITING) to avoid UAF.", + Recommendation: "Use the following resources to help you mitigate CVE-2024-35863.\n\n**More information about CVE-2024-35863**\n* https://security-tracker.debian.org/tracker/CVE-2024-35863\n* https://access.redhat.com/security/cve/CVE-2024-35863\n* https://www.suse.com/security/cve/CVE-2024-35863\n* http://people.ubuntu.com/~ubuntu-security/cve/CVE-2024-35863\n\n**Fixed location**\ncpe:/o:debian:debian_linux:12\n\n**Fixed package**\nlinux\n\n**Fixed version**\n6.1.85-1\n", + }, + ResourceRef: &pb.ResourceRef{ + GroupName: "projects/modron-test", + ExternalId: proto.String("//container.googleapis.com/projects/modron0test/locations/us-west1/clusters/my-cluster/k8s/namespaces/default/apps/deployments/my-deployment"), + CloudPlatform: pb.CloudPlatform_GCP, + }, + ExternalId: proto.String("//securitycenter.googleapis.com/projects/1234/sources/5678/locations/global/findings/0000"), + Source: pb.Observation_SOURCE_SCC, + } + + if diff := cmp.Diff(want, got, protocmp.Transform(), protocmp.IgnoreFields(&pb.Observation{}, "timestamp", "uid")); diff != "" { + t.Errorf("FindingToObservation() mismatch (-want +got):\n%s", diff) + } +} + +func TestListSccFindings(t *testing.T) { + otelMeter := otel.GetMeterProvider() + var mr *metric.ManualReader + if otelMeter == nil { + mr = metric.NewManualReader() + otel.SetMeterProvider( + metric.NewMeterProvider( + metric.WithReader(mr), + ), + ) + } + + ctx := context.Background() + storage := memstorage.New() + gcpCollector := NewFake(ctx, storage, risk.TagConfig{}) + got, err := gcpCollector.ListSccFindings(ctx, "projects/project-id") + if err != nil { + t.Fatalf("ListSccFindings: %v", err) + } + + want := []*pb.Observation{ + { + Name: "SQL_PUBLIC_IP", + Remediation: &pb.Remediation{ + Description: "To lower your attack surface, Cloud SQL databases should not have public IPs. Private IPs provide improved network security and lower latency for your application.", + Recommendation: "Go to https://console.cloud.google.com/sql/instances/xyz/connections?project=project-id and click the \"Networking\" tab. Uncheck the \"Public IP\" checkbox and click \"SAVE\". If your instance is not configured to use a private IP, you will first have to enable private IP by following the instructions here: https://cloud.google.com/sql/docs/mysql/configure-private-ip#existing-private-instance", + }, + ExternalId: proto.String("//securitycenter.googleapis.com/projects/12345/sources/123/findings/48230f1978594ffb9d09a3cb1fe5e1b3"), + Source: pb.Observation_SOURCE_SCC, + Severity: pb.Severity_SEVERITY_MEDIUM, + Category: pb.Observation_CATEGORY_MISCONFIGURATION, + ResourceRef: &pb.ResourceRef{ + ExternalId: proto.String("//cloudsql.googleapis.com/projects/project-id/instances/xyz"), + CloudPlatform: pb.CloudPlatform_GCP, + GroupName: "projects/project-id", + }, + }, + } + if diff := cmp.Diff(want, got, protocmp.Transform(), protocmp.IgnoreFields(&pb.Observation{}, "timestamp", "uid")); diff != "" { + t.Errorf("ListSccFindings() mismatch (-want +got):\n%s", diff) + } + + if mr != nil { + resourceMetrics := metricdata.ResourceMetrics{} + if err := mr.Collect(context.Background(), &resourceMetrics); err != nil { + t.Fatalf("Collect: %v", err) + } + + metricScope := "github.com/nianticlabs/modron/src/collector/gcpcollector" + gotMetrics := getMetrics(resourceMetrics, metricScope) + if gotMetrics == nil { + t.Fatalf("no metrics found for scope %q", metricScope) + } + + wantMetrics := []metricdata.Metrics{ + { + Name: "modron_scc_collected_observations", + Description: "Number of collected observations from SCC", + Data: metricdata.Sum[int64]{ + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet( + attribute.String("category", "SQL_PUBLIC_IP"), + attribute.String("severity", "MEDIUM"), + ), + Value: 1, + }, + }, + }, + }, + } + + if diff := cmp.Diff(wantMetrics, gotMetrics, metricsCmpOpts()...); diff != "" { + t.Errorf("ResourceMetrics mismatch (-want +got):\n%s", diff) + } + } else { + t.Log("The test was run in parallel mode, thus we'll not check the result of the metrics (partial!)") + } + +} + +// Adapted from https://github.com/temporalio/temporal/blob/8752a90c5b15851d81c7aff98e0fe94a6cb57d13/common/metrics/otel_metrics_handler_test.go#L200-L218 +func metricsCmpOpts() []cmp.Option { + return []cmp.Option{ + cmp.Comparer(func(e1, e2 metricdata.Extrema[int64]) bool { + v1, ok1 := e1.Value() + v2, ok2 := e2.Value() + return ok1 && ok2 && v1 == v2 + }), + cmp.Comparer(func(a1, a2 attribute.Set) bool { + return a1.Equals(&a2) + }), + cmpopts.SortSlices(func(x, y metricdata.Metrics) bool { + return x.Name < y.Name + }), + cmpopts.IgnoreFields(metricdata.DataPoint[int64]{}, "StartTime", "Time"), + cmpopts.IgnoreFields(metricdata.DataPoint[float64]{}, "StartTime", "Time"), + cmpopts.IgnoreFields(metricdata.Sum[int64]{}, "Temporality", "IsMonotonic"), + cmpopts.IgnoreFields(metricdata.HistogramDataPoint[int64]{}, "StartTime", "Time", "Bounds"), + } +} + +func getMetrics(resourceMetrics metricdata.ResourceMetrics, s string) []metricdata.Metrics { + for _, v := range resourceMetrics.ScopeMetrics { + if v.Scope.Name == s { + return v.Metrics + } + } + return nil +} diff --git a/src/collector/gcpcollector/service_account.go b/src/collector/gcpcollector/service_account.go new file mode 100644 index 0000000..b93dcc3 --- /dev/null +++ b/src/collector/gcpcollector/service_account.go @@ -0,0 +1,153 @@ +package gcpcollector + +import ( + "fmt" + "time" + + "google.golang.org/api/iam/v1" + + "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/constants" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" + + "golang.org/x/net/context" + "google.golang.org/api/monitoring/v3" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + serviceAccountResourcePath = "%s/serviceAccounts/%s" + serviceAccountKeyUnusedMaxDelay = "100d" + monitoringPageSize = 1000 +) + +func toIamPolicy(policy *iam.Policy) *pb.IamPolicy { + if policy == nil { + return nil + } + iamPolicy := &pb.IamPolicy{} + for _, binding := range policy.Bindings { + role := constants.ToRole(binding.Role).String() + iamPolicy.Permissions = append(iamPolicy.Permissions, &pb.Permission{ + Role: role, + Principals: binding.Members, + }) + } + return iamPolicy +} + +func (collector *GCPCollector) ListServiceAccounts(ctx context.Context, rgName string) (serviceAccounts []*pb.Resource, err error) { + name := constants.ResourceWithProjectsPrefix(rgName) + serviceAccountsList, err := collector.api.ListServiceAccount(ctx, name) + if err != nil { + return nil, err + } + for _, account := range serviceAccountsList { + iamPolicy, err := collector.api.GetServiceAccountIAMPolicy(ctx, account.Name) + if err != nil { + log.Warnf("cannot get IAM policies for service account %q: %v", account.Email, err) + } + serviceAccounts = append(serviceAccounts, &pb.Resource{ + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, + Name: account.Email, + Parent: rgName, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{}, + }, + }, + IamPolicy: toIamPolicy(iamPolicy), + }) + } + resources := serviceAccounts + for _, serviceAccount := range serviceAccounts { + userKeys, err := collector.GetServiceAccountKeys(ctx, rgName, serviceAccount) + if err != nil { + return nil, err + } + resources = append(resources, userKeys...) + } + return resources, nil +} + +// Note: also update the Service Account passed by reference by adding its service accountkeys +func (collector *GCPCollector) GetServiceAccountKeys(ctx context.Context, rgName string, serviceAccount *pb.Resource) (userKeys []*pb.Resource, err error) { + name := fmt.Sprintf(serviceAccountResourcePath, constants.ResourceWithProjectsPrefix(rgName), serviceAccount.Name) + + keys, err := collector.api.ListServiceAccountKeys(ctx, name) + if err != nil { + return nil, err + } + for _, key := range keys { + if key.KeyType != "USER_MANAGED" { + continue + } + + keyCreationDate, err := time.Parse(time.RFC3339, key.ValidAfterTime) + if err != nil { + return nil, fmt.Errorf("serviceAccountKey.CreationDate: %w", err) + } + keyExpirationDate, err := time.Parse(time.RFC3339, key.ValidBeforeTime) + if err != nil { + return nil, fmt.Errorf("serviceAccountKey.ExpirationDate: %w", err) + } + + keyExported := &pb.ExportedCredentials{ + CreationDate: timestamppb.New(keyCreationDate), + ExpirationDate: timestamppb.New(keyExpirationDate), + } + + keyID := utils.GetKeyID(key.Name) + lastUsage, err := collector.GetServiceAccountKeyLastUsage(ctx, rgName, keyID) + if err != nil { + log.Warnf("cannot get key usage %q: %v", key.Name, err) + // Need to return here as otherwise the object is created with the default time value EPOCH. + return nil, fmt.Errorf("serviceAccountKey usage: %w", err) + } + keyExported.LastUsage = timestamppb.New(lastUsage) + serviceAccount.GetServiceAccount().ExportedCredentials = append(serviceAccount.GetServiceAccount().ExportedCredentials, keyExported) + userKeys = append(userKeys, &pb.Resource{ + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, + Name: key.Name, + Parent: serviceAccount.Name, + Type: &pb.Resource_ExportedCredentials{ + ExportedCredentials: keyExported, + }, + }) + } + return userKeys, nil +} + +func (collector *GCPCollector) GetServiceAccountKeyLastUsage(ctx context.Context, rgName string, keyID string) (time.Time, error) { + request := new(monitoring.QueryTimeSeriesRequest) + request.Query = fmt.Sprintf( + "fetch iam_service_account | metric 'iam.googleapis.com/service_account/key/authn_events_count' | filter (metric.key_id == '%s') | within %s", + keyID, + serviceAccountKeyUnusedMaxDelay, + ) + request.PageSize = monitoringPageSize + + lastUsage := time.Time{} + err := collector.api.ListServiceAccountKeyUsage(ctx, rgName, request). + Pages(ctx, func(qtsr *monitoring.QueryTimeSeriesResponse) error { + for _, timeData := range qtsr.TimeSeriesData { + for _, pd := range timeData.PointData { + timeF, err := time.Parse(time.RFC3339, pd.TimeInterval.EndTime) + if err != nil { + return fmt.Errorf("GetServiceAccountKeyUsage.convertingTime: %w", err) + } + if timeF.After(lastUsage) { + lastUsage = timeF + } + } + } + return nil + }) + if err != nil { + return time.Time{}, fmt.Errorf("GetServiceAccountKeyUsage: query: %q, err: %w", request.Query, err) + } + return lastUsage, nil +} diff --git a/src/collector/gcpcollector/service_account_test.go b/src/collector/gcpcollector/service_account_test.go new file mode 100644 index 0000000..49b62c0 --- /dev/null +++ b/src/collector/gcpcollector/service_account_test.go @@ -0,0 +1,30 @@ +//go:build integration + +package gcpcollector + +import ( + "context" + "os" + "testing" + + "github.com/nianticlabs/modron/src/constants" +) + +func TestListServiceAccounts(t *testing.T) { + ctx := context.Background() + coll, _ := getCollector(ctx, t) + project := constants.GCPProjectsNamePrefix + os.Getenv("PROJECT_ID") + accounts, err := coll.(*GCPCollector).ListServiceAccounts(ctx, project) + if err != nil { + t.Fatalf("ListServiceAccounts failed: %v", err) + } + if len(accounts) == 0 { + t.Fatalf("ListServiceAccounts returned 0 accounts") + } + for _, account := range accounts { + t.Logf("ServiceAccount: %s", account.Name) + if account.IamPolicy != nil && len(account.IamPolicy.Permissions) > 0 { + t.Logf("\tIAM Policy: %v", account.IamPolicy) + } + } +} diff --git a/src/collector/gcpcollector/spanner.go b/src/collector/gcpcollector/spanner.go index c82297c..87ef29e 100644 --- a/src/collector/gcpcollector/spanner.go +++ b/src/collector/gcpcollector/spanner.go @@ -3,25 +3,24 @@ package gcpcollector import ( "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/constants" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" "golang.org/x/net/context" ) -func (collector *GCPCollector) ListSpannerDatabases(ctx context.Context, resourceGroup *pb.Resource) ([]*pb.Resource, error) { - name := constants.ResourceWithProjectsPrefix(resourceGroup.Name) +func (collector *GCPCollector) ListSpannerDatabases(ctx context.Context, rgName string) (resources []*pb.Resource, err error) { + name := constants.ResourceWithProjectsPrefix(rgName) dbs, err := collector.api.ListSpannerDatabases(ctx, name) if err != nil { return nil, err } - resources := []*pb.Resource{} for _, database := range dbs { dbResource := &pb.Resource{ // TODO: Collect IAM Policy - Uid: common.GetUUID(3), - ResourceGroupName: resourceGroup.Name, + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, Name: database.Name, - Parent: resourceGroup.Name, + Parent: rgName, Type: &pb.Resource_Database{ Database: &pb.Database{ Type: "spanner", diff --git a/src/collector/gcpcollector/vm_instance.go b/src/collector/gcpcollector/vm_instance.go new file mode 100644 index 0000000..5a20dc1 --- /dev/null +++ b/src/collector/gcpcollector/vm_instance.go @@ -0,0 +1,43 @@ +package gcpcollector + +import ( + "github.com/nianticlabs/modron/src/common" + pb "github.com/nianticlabs/modron/src/proto/generated" + + "golang.org/x/net/context" +) + +func (collector *GCPCollector) ListVMInstances(ctx context.Context, rgName string) (vmInstances []*pb.Resource, err error) { + instances, err := collector.api.ListInstances(ctx, rgName) + if err != nil { + return nil, err + } + for _, instance := range instances { + name := instance.Name + privateIP, publicIP := "", "" + for _, networkInterface := range instance.NetworkInterfaces { + privateIP = networkInterface.NetworkIP + for _, accessConfig := range networkInterface.AccessConfigs { + publicIP = accessConfig.NatIP + } + } + serviceAccountName := "" + for _, sa := range instance.ServiceAccounts { + serviceAccountName = sa.Email + } + vmInstances = append(vmInstances, &pb.Resource{ + Uid: common.GetUUID(uuidGenRetries), + ResourceGroupName: rgName, + Name: name, + Parent: rgName, + Type: &pb.Resource_VmInstance{ + VmInstance: &pb.VmInstance{ + PublicIp: publicIP, + PrivateIp: privateIP, + Identity: serviceAccountName, + }, + }, + }) + } + return vmInstances, nil +} diff --git a/src/collector/testcollector/testcollector.go b/src/collector/testcollector/testcollector.go new file mode 100644 index 0000000..02a6a16 --- /dev/null +++ b/src/collector/testcollector/testcollector.go @@ -0,0 +1,56 @@ +package testcollector + +import ( + "fmt" + + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + + "golang.org/x/net/context" +) + +var _ model.Collector = (*TestCollector)(nil) + +var errNotImplemented = fmt.Errorf("not implemented") + +type TestCollector struct{} + +func (t TestCollector) CollectAndStoreAll(_ context.Context, _ string, _ []string, _ []*pb.Resource) error { + return errNotImplemented +} + +func (t TestCollector) ListResourceGroupObservations(_ context.Context, _ string, _ string) ([]*pb.Observation, []error) { + return nil, []error{errNotImplemented} +} + +func (t TestCollector) GetResourceGroupWithIamPolicy(_ context.Context, _ string, _ string) (*pb.Resource, error) { + return nil, errNotImplemented +} + +func (t TestCollector) ListResourceGroups(_ context.Context, _ []string) ([]*pb.Resource, error) { + return nil, errNotImplemented +} + +func (t TestCollector) ListResourceGroupsWithIamPolicies(_ context.Context, _ []string) ([]*pb.Resource, error) { + return nil, errNotImplemented +} + +func (t TestCollector) ListResourceGroupNames(_ context.Context) ([]string, error) { + return nil, errNotImplemented +} + +func (t TestCollector) ListResourceGroupAdmins(_ context.Context) (model.ACLCache, error) { + return model.ACLCache{ + "*": { + "projects/modron-test": {}, + "projects/super-secret": {}, + }, + "user@example.com": { + "projects/modron-test": {}, + }, + }, nil +} + +func (t TestCollector) ListResourceGroupResources(_ context.Context, _ string, _ string) ([]*pb.Resource, []error) { + return nil, []error{errNotImplemented} +} diff --git a/src/common/protoutils.go b/src/common/protoutils.go index 8821b8d..9432554 100644 --- a/src/common/protoutils.go +++ b/src/common/protoutils.go @@ -9,21 +9,23 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" ) const ( - ResourceApiKey = "ApiKey" + ResourceAPIKey = "ApiKey" ResourceBucket = "Bucket" - ResourceExportedCredentials = "ExportedCredentials" + ResourceDatabase = "Database" + ResourceExportedCredentials = "ExportedCredentials" //nolint:gosec + ResourceGroup = "Group" ResourceKubernetesCluster = "KubernetesCluster" ResourceLoadBalancer = "LoadBalancer" + ResourceNamespace = "Namespace" ResourceNetwork = "Network" + ResourcePod = "Pod" ResourceResourceGroup = "ResourceGroup" ResourceServiceAccount = "ServiceAccount" - ResourceVmInstance = "VmInstance" - ResourceDatabase = "Database" - ResourceGroup = "Group" + ResourceVMInstance = "VmInstance" ) // See `Ssl` @@ -33,39 +35,6 @@ const ( CertificateUnknown = "TYPE_UNSPECIFIED" ) -func TypeFromResourceAsString(rsrc *pb.Resource) (ty string, err error) { - if rsrc == nil { - return "", fmt.Errorf("resource must not be nil") - } - switch rsrc.Type.(type) { - case *pb.Resource_ApiKey: - ty = ResourceApiKey - case *pb.Resource_Bucket: - ty = ResourceBucket - case *pb.Resource_ExportedCredentials: - ty = ResourceExportedCredentials - case *pb.Resource_KubernetesCluster: - ty = ResourceKubernetesCluster - case *pb.Resource_LoadBalancer: - ty = ResourceLoadBalancer - case *pb.Resource_Network: - ty = ResourceNetwork - case *pb.Resource_ResourceGroup: - ty = ResourceResourceGroup - case *pb.Resource_ServiceAccount: - ty = ResourceServiceAccount - case *pb.Resource_VmInstance: - ty = ResourceVmInstance - case *pb.Resource_Database: - ty = ResourceDatabase - case *pb.Resource_Group: - ty = ResourceGroup - default: - err = fmt.Errorf("unknown resource type %q", rsrc.Type) - } - return -} - func TypeFromSslCertificate(cert *compute.SslCertificate) (ty pb.Certificate_Type, err error) { switch cert.Type { case CertificateManaged: @@ -82,13 +51,13 @@ func TypeFromSslCertificate(cert *compute.SslCertificate) (ty pb.Certificate_Typ // TODO: Cast without (un)marshaling if possible. func ResourceFromStructValue(value *structpb.Value) (*pb.Resource, error) { - valueJson, err := protojson.Marshal(value) + valueJSON, err := protojson.Marshal(value) if err != nil { return nil, err } rsrc := &pb.Resource{} - if err := protojson.Unmarshal(valueJson[:], rsrc); err != nil { + if err := protojson.Unmarshal(valueJSON, rsrc); err != nil { return nil, err } @@ -97,13 +66,13 @@ func ResourceFromStructValue(value *structpb.Value) (*pb.Resource, error) { // TODO: Cast without (un)marshaling if possible. func StructValueFromResource(rsrc *pb.Resource) (*structpb.Value, error) { - rsrcJson, err := protojson.Marshal(rsrc) + rsrcJSON, err := protojson.Marshal(rsrc) if err != nil { return nil, err } value := &structpb.Value{} - if err := protojson.Unmarshal(rsrcJson[:], value); err != nil { + if err := protojson.Unmarshal(rsrcJSON, value); err != nil { return nil, err } diff --git a/src/constants/constants.go b/src/constants/constants.go index 8fc1db8..4c62a74 100644 --- a/src/constants/constants.go +++ b/src/constants/constants.go @@ -2,29 +2,86 @@ package constants import ( "strings" + + pb "github.com/nianticlabs/modron/src/proto/generated" ) +type Role string + const ( - OrgIdEnvVar = "ORG_ID" - OrgSuffixEnvVar = "ORG_SUFFIX" + GCPEditorRole Role = "editor" + GCPOwnerRole Role = "owner" + GCPSecurityAdminRole Role = "iam.securityAdmin" + GCPViewerRole Role = "viewer" +) - GCPEditorRole = "editor" - GCPOwnerRole = "owner" - GCPSecurityAdminRole = "iam.securityAdmin" +func (r Role) String() string { + return string(r) +} - GCPOrgIdPrefix = "organizations/" - GCPFolderIdPrefix = "folders/" +const ( + GCPOrgIDPrefix = "organizations/" + GCPFolderIDPrefix = "folders/" GCPProjectsNamePrefix = "projects/" GCPRolePrefix = "roles/" + GCPSysProjectPrefix = "sys-" GCPAccountGroupPrefix = "group:" GCPServiceAccountPrefix = "serviceAccount:" GCPUserAccountPrefix = "user:" + + MetricsPrefix = "modron_" + + ResourceLabelCustomerData = "customer_data" + ResourceLabelEmployeeData = "employee_data" + + LabelContact1 = "contact1" + LabelContact2 = "contact2" + + ImpactEmployeeData = pb.Impact_IMPACT_MEDIUM + ImpactCustomerData = pb.Impact_IMPACT_HIGH +) + +const ( + LogKeyCollectID = "collect_id" + LogKeyCollector = "collector" + LogKeyObservationName = "observation_name" + LogKeyObservationUID = "observation_uid" + LogKeyPkg = "package" + LogKeyResourceGroup = "resource_group" + LogKeyResourceGroupNames = "resource_group_names" + LogKeyRule = "rule" + LogKeyScanID = "scan_id" +) + +const ( + TraceKeyCollectID = "collect_id" + TraceKeyCollector = "collector" + TraceKeyMethod = "method" + TraceKeyName = "name" + TraceKeyNumNotifications = "num_notifications" + TraceKeyNumObservations = "num_observations" + TraceKeyNumResources = "num_resources" + TraceKeyObservationUID = "observation_uid" + TraceKeyPath = "path" + TraceKeyResourceGroup = "resource_group" + TraceKeyResourceGroupNames = "resource_group_names" + TraceKeyRule = "rule" + TraceKeyScanID = "scan_id" + TraceKeyScanType = "scan_type" ) -var AdminRoles = map[string]struct{}{ - strings.ToLower(GCPOwnerRole): {}, - strings.ToLower(GCPEditorRole): {}, - strings.ToLower(GCPSecurityAdminRole): {}, +const ( + MetricKeyStatus = "status" +) + +var AdminRoles = map[Role]struct{}{ + GCPOwnerRole: {}, + GCPEditorRole: {}, + GCPSecurityAdminRole: {}, +} + +func ToRole(role string) Role { + return Role(strings.TrimPrefix(role, GCPRolePrefix)) } func ResourceWithProjectsPrefix(resourceName string) string { diff --git a/src/constants/context.go b/src/constants/context.go new file mode 100644 index 0000000..7ed0944 --- /dev/null +++ b/src/constants/context.go @@ -0,0 +1,8 @@ +package constants + +type runnerCtxKey string + +const ( + CollectIDKey runnerCtxKey = "collect_id" + ScanIDKey runnerCtxKey = "scan_id" +) diff --git a/src/constants/gcp_sa_projects.go b/src/constants/gcp_sa_projects.go new file mode 100644 index 0000000..9c49768 --- /dev/null +++ b/src/constants/gcp_sa_projects.go @@ -0,0 +1,254 @@ +package constants + +// GCPServiceAgentsProjects is generated using the utils/gcp_service_agents tool. +// The result (out.json) is then passed to jq as follows, and the content copy & pasted here +// jq -r '.projects[] | "\"" + . + "\"" + ": {},"' out.json | clipcopy +var GCPServiceAgentsProjects = map[string]struct{}{ + "appsheet-prod-service-accounts": {}, + "bigquery-encryption": {}, + "cloud-cdn-fill": {}, + "cloud-filer": {}, + "cloud-memcache-sa": {}, + "cloud-ml.google.com": {}, + "cloud-redis": {}, + "cloud-tpu": {}, + "cloudcomposer-accounts": {}, + "compute-system": {}, + "container-analysis": {}, + "container-engine-robot": {}, + "containerregistry": {}, + "crashlytics-bigquery-prod": {}, + "dataflow-service-producer-prod": {}, + "dataproc-accounts": {}, + "dlp-api": {}, + "fcm-bq-export-prod": {}, + "firebase-rules": {}, + "firebase-sa-management": {}, + "gae-api-prod.google.com": {}, + "gcf-admin-robot": {}, + "gcp-gae-service": {}, + "gcp-ri-aiplatform": {}, + "gcp-ri-identitypool": {}, + "gcp-sa-accessapproval": {}, + "gcp-sa-adsdatahub": {}, + "gcp-sa-aiplatform": {}, + "gcp-sa-aiplatform-cc": {}, + "gcp-sa-aiplatform-ft": {}, + "gcp-sa-aiplatform-is": {}, + "gcp-sa-aiplatform-re": {}, + "gcp-sa-aiplatform-vm": {}, + "gcp-sa-alloydb": {}, + "gcp-sa-anthos": {}, + "gcp-sa-anthosaudit": {}, + "gcp-sa-anthosconfigmanagement": {}, + "gcp-sa-anthosidentityservice": {}, + "gcp-sa-anthospolicycontroller": {}, + "gcp-sa-anthossupport": {}, + "gcp-sa-apigateway": {}, + "gcp-sa-apigateway-mgmt": {}, + "gcp-sa-apigee": {}, + "gcp-sa-apigeeregistry": {}, + "gcp-sa-apihub": {}, + "gcp-sa-apikeys": {}, + "gcp-sa-apim": {}, + "gcp-sa-appdevexperience": {}, + "gcp-sa-apphub": {}, + "gcp-sa-artifactregistry": {}, + "gcp-sa-asm-hpsa": {}, + "gcp-sa-assuredoss": {}, + "gcp-sa-assuredworkloads": {}, + "gcp-sa-audit-manager": {}, + "gcp-sa-automl": {}, + "gcp-sa-backupdr": {}, + "gcp-sa-backupdr-pr": {}, + "gcp-sa-backupdr-run": {}, + "gcp-sa-bigquery-condel": {}, + "gcp-sa-bigquery-consp": {}, + "gcp-sa-bigqueryconnection": {}, + "gcp-sa-bigquerydatatransfer": {}, + "gcp-sa-bigqueryri": {}, + "gcp-sa-bigqueryspark": {}, + "gcp-sa-bigquerytardis": {}, + "gcp-sa-bigtable": {}, + "gcp-sa-binaryauthorization": {}, + "gcp-sa-bms": {}, + "gcp-sa-bne": {}, + "gcp-sa-bundles": {}, + "gcp-sa-ccai-cmek": {}, + "gcp-sa-ccaip": {}, + "gcp-sa-ccinsights-cmek": {}, + "gcp-sa-certificatemanager": {}, + "gcp-sa-chronicle": {}, + "gcp-sa-chronicle-soar": {}, + "gcp-sa-chronicle-spanner": {}, + "gcp-sa-chronicle-sv": {}, + "gcp-sa-cloud-cw": {}, + "gcp-sa-cloud-cw-cmek": {}, + "gcp-sa-cloud-ekg": {}, + "gcp-sa-cloud-sql": {}, + "gcp-sa-cloud-trace": {}, + "gcp-sa-cloudaicompanion": {}, + "gcp-sa-cloudasset": {}, + "gcp-sa-cloudbatch": {}, + "gcp-sa-cloudbuild": {}, + "gcp-sa-cloudcontrolspartner": {}, + "gcp-sa-clouddeploy": {}, + "gcp-sa-cloudkms": {}, + "gcp-sa-cloudoptim": {}, + "gcp-sa-cloudscheduler": {}, + "gcp-sa-cloudtasks": {}, + "gcp-sa-compute-usage": {}, + "gcp-sa-config": {}, + "gcp-sa-configdelivery": {}, + "gcp-sa-connectors": {}, + "gcp-sa-contactcenterinsights": {}, + "gcp-sa-containerscanning": {}, + "gcp-sa-containersec": {}, + "gcp-sa-dataconnectors": {}, + "gcp-sa-dataform": {}, + "gcp-sa-datafusion": {}, + "gcp-sa-datalabeling": {}, + "gcp-sa-datamigration": {}, + "gcp-sa-datapipelines": {}, + "gcp-sa-dataplex": {}, + "gcp-sa-dataprocrmnode": {}, + "gcp-sa-datastream": {}, + "gcp-sa-datastudio": {}, + "gcp-sa-dep": {}, + "gcp-sa-devconnect": {}, + "gcp-sa-dialogflow": {}, + "gcp-sa-dialogflow-cmek": {}, + "gcp-sa-discoveryengine": {}, + "gcp-sa-dns": {}, + "gcp-sa-edgecontainer": {}, + "gcp-sa-edgecontainercluster": {}, + "gcp-sa-edgecontainergcr": {}, + "gcp-sa-effectivepolicy": {}, + "gcp-sa-ekms": {}, + "gcp-sa-endpoints": {}, + "gcp-sa-eventarc": {}, + "gcp-sa-firebase": {}, + "gcp-sa-firebaseappcheck": {}, + "gcp-sa-firebaseapphosting": {}, + "gcp-sa-firebasedatabase": {}, + "gcp-sa-firebaseml": {}, + "gcp-sa-firebasemods": {}, + "gcp-sa-firebasestorage": {}, + "gcp-sa-firestore": {}, + "gcp-sa-firewallinsights": {}, + "gcp-sa-fs-spanner": {}, + "gcp-sa-gkebackup": {}, + "gcp-sa-gkedataplanev2": {}, + "gcp-sa-gkehub": {}, + "gcp-sa-gkemulticloud": {}, + "gcp-sa-gkemulticloudcontainer": {}, + "gcp-sa-gkemulticloudcpmachine": {}, + "gcp-sa-gkemulticloudnpmachine": {}, + "gcp-sa-gkenode": {}, + "gcp-sa-gkeonprem": {}, + "gcp-sa-gsuiteaddons": {}, + "gcp-sa-healthcare": {}, + "gcp-sa-iap": {}, + "gcp-sa-identitytoolkit": {}, + "gcp-sa-integrations": {}, + "gcp-sa-issuerswitch": {}, + "gcp-sa-ivs": {}, + "gcp-sa-krmapihosting": {}, + "gcp-sa-krmapihosting-dataplane": {}, + "gcp-sa-ktd-control": {}, + "gcp-sa-ktd-hpsa": {}, + "gcp-sa-lifesciences": {}, + "gcp-sa-livestream": {}, + "gcp-sa-logging": {}, + "gcp-sa-looker": {}, + "gcp-sa-managedflink": {}, + "gcp-sa-managedkafka": {}, + "gcp-sa-mcmetering": {}, + "gcp-sa-mcsd": {}, + "gcp-sa-memorystore": {}, + "gcp-sa-meshconfig": {}, + "gcp-sa-meshcontrolplane": {}, + "gcp-sa-meshdataplane": {}, + "gcp-sa-metastore": {}, + "gcp-sa-mi": {}, + "gcp-sa-migcenter": {}, + "gcp-sa-monitoring": {}, + "gcp-sa-monitoring-notification": {}, + "gcp-sa-multiclusteringress": {}, + "gcp-sa-netapp": {}, + "gcp-sa-networkactions": {}, + "gcp-sa-networkconnectivity": {}, + "gcp-sa-networkmanagement": {}, + "gcp-sa-networksecurity": {}, + "gcp-sa-notebooks": {}, + "gcp-sa-notebooksecurityscanner": {}, + "gcp-sa-nss-hpsa": {}, + "gcp-sa-oci": {}, + "gcp-sa-ondemandscanning": {}, + "gcp-sa-osconfig": {}, + "gcp-sa-osconfig-rollout": {}, + "gcp-sa-othercloudcfg": {}, + "gcp-sa-pam": {}, + "gcp-sa-parallelstore": {}, + "gcp-sa-playbooks": {}, + "gcp-sa-privateca": {}, + "gcp-sa-prod-bigqueryomni": {}, + "gcp-sa-prod-dai-core": {}, + "gcp-sa-pubsub": {}, + "gcp-sa-pubsublite": {}, + "gcp-sa-rbe": {}, + "gcp-sa-recommendationengine": {}, + "gcp-sa-remotebuild": {}, + "gcp-sa-retail": {}, + "gcp-sa-riskmanager": {}, + "gcp-sa-rma": {}, + "gcp-sa-routeoptim": {}, + "gcp-sa-runapps": {}, + "gcp-sa-scc-notification": {}, + "gcp-sa-scc-vmtd": {}, + "gcp-sa-secretmanager": {}, + "gcp-sa-securewebproxy": {}, + "gcp-sa-securitycenter": {}, + "gcp-sa-servicedirectory": {}, + "gcp-sa-servicemesh": {}, + "gcp-sa-sourcemanager": {}, + "gcp-sa-spanner": {}, + "gcp-sa-spectrumsas": {}, + "gcp-sa-speech": {}, + "gcp-sa-storageinsights": {}, + "gcp-sa-stream": {}, + "gcp-sa-tpu": {}, + "gcp-sa-transcoder": {}, + "gcp-sa-transferappliance": {}, + "gcp-sa-translation": {}, + "gcp-sa-v1-remediator": {}, + "gcp-sa-vertex-agent": {}, + "gcp-sa-vertex-bp": {}, + "gcp-sa-vertex-es": {}, + "gcp-sa-vertex-eval": {}, + "gcp-sa-vertex-ex": {}, + "gcp-sa-vertex-ex-cc": {}, + "gcp-sa-vertex-mm": {}, + "gcp-sa-vertex-nb": {}, + "gcp-sa-vertex-op": {}, + "gcp-sa-vertex-rag": {}, + "gcp-sa-vertex-shtune": {}, + "gcp-sa-vertex-tune": {}, + "gcp-sa-visionai": {}, + "gcp-sa-vmmigration": {}, + "gcp-sa-vmwareengine": {}, + "gcp-sa-vpcaccess": {}, + "gcp-sa-websecurityscanner": {}, + "gcp-sa-workflows": {}, + "gcp-sa-workloadmanager": {}, + "gcp-sa-workstations": {}, + "gcp-sa-workstationsvm": {}, + "gs-project-accounts": {}, + "performance-bq-export-prod": {}, + "remotebuildexecution": {}, + "security-center-api": {}, + "serverless-robot-prod": {}, + "service-consumer-management": {}, + "service-networking": {}, + "storage-transfer-service": {}, +} diff --git a/src/engine/framework.go b/src/engine/framework.go index 2d661ff..864c231 100644 --- a/src/engine/framework.go +++ b/src/engine/framework.go @@ -6,7 +6,6 @@ import ( "encoding/base64" "fmt" "net/http" - "sync" "time" "golang.org/x/oauth2" @@ -17,8 +16,9 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" ) type TransportProvider func(ctx context.Context, cluster *container.Cluster) (http.RoundTripper, error) @@ -28,8 +28,6 @@ type Storage struct { } var ( - storage *Storage - memoizationMap sync.Map cleanupInterval = 30 * time.Minute cleanupTicker = time.NewTicker(cleanupInterval) ) @@ -39,8 +37,15 @@ type cachedResource struct { timestamp time.Time } -func GetResource(ctx context.Context, resourceName string) (*pb.Resource, error) { - if cache, exists := memoizationMap.Load(resourceName); exists { +func (e *RuleEngine) GetChildren(ctx context.Context, parent string) ([]*pb.Resource, error) { + filter := model.StorageFilter{ + ParentNames: []string{parent}, + } + return e.storage.ListResources(ctx, filter) +} + +func (e *RuleEngine) GetResource(ctx context.Context, resourceName string) (*pb.Resource, error) { + if cache, exists := e.memoizationMap.Load(resourceName); exists { res := cache.(*cachedResource) return res.resource, nil } @@ -49,7 +54,13 @@ func GetResource(ctx context.Context, resourceName string) (*pb.Resource, error) Limit: 1, ResourceNames: []string{resourceName}, } - res, err := storage.Storage.ListResources(ctx, filter) + collectionID, ok := ctx.Value(constants.CollectIDKey).(string) + if ok { + filter.OperationID = collectionID + } else { + log.Warnf("collection ID not found in context") + } + res, err := e.storage.ListResources(ctx, filter) if err != nil { return nil, fmt.Errorf("resource %q could not be fetched: %w", resourceName, err) } @@ -61,26 +72,21 @@ func GetResource(ctx context.Context, resourceName string) (*pb.Resource, error) resource: res[0], timestamp: time.Now(), } - memoizationMap.Store(resourceName, cachedRes) - + e.memoizationMap.Store(resourceName, cachedRes) return res[0], nil } -func init() { - go startCacheCleanup() -} - -func startCacheCleanup() { +func (e *RuleEngine) startCacheCleanup() { for range cleanupTicker.C { - clearExpiredResources() + e.clearExpiredResources() } } -func clearExpiredResources() { - memoizationMap.Range(func(key, value interface{}) bool { +func (e *RuleEngine) clearExpiredResources() { + e.memoizationMap.Range(func(key, value interface{}) bool { cachedRes := value.(*cachedResource) if time.Since(cachedRes.timestamp) >= cleanupInterval { - memoizationMap.Delete(key) + e.memoizationMap.Delete(key) } return true }) @@ -89,7 +95,7 @@ func clearExpiredResources() { func GetKubernetesClient(ctx context.Context, clusterName string, httpClient *http.Client, getTransport TransportProvider) (*kubernetes.Clientset, error) { tokenSource, err := google.DefaultTokenSource(ctx, compute.CloudPlatformScope) if err != nil { - return nil, fmt.Errorf("failed to get a token source: %v", err) + return nil, fmt.Errorf("failed to get a token source: %w", err) } if httpClient == http.DefaultClient { httpClient = oauth2.NewClient(ctx, tokenSource) @@ -97,14 +103,14 @@ func GetKubernetesClient(ctx context.Context, clusterName string, httpClient *ht } containerService, err := container.NewService(ctx, option.WithHTTPClient(httpClient)) if err != nil { - return nil, fmt.Errorf("could not create client for Google Container Engine: %v", err) + return nil, fmt.Errorf("could not create client for Google Container Engine: %w", err) } cluster, err := containerService.Projects.Locations.Clusters.Get(clusterName).Context(ctx).Do() if err != nil { - return nil, fmt.Errorf("cluster %q: %v", clusterName, err) + return nil, fmt.Errorf("cluster %q: %w", clusterName, err) } - // This is a very ugly dependency injection but we have to do it otherwise unittesting would require a complete oauth2 backend. + // This is a very ugly dependency injection, but we have to do it otherwise the unit test would require a complete oauth2 backend. tr, err := getTransport(ctx, cluster) if err != nil { return nil, err @@ -118,7 +124,7 @@ func GetKubernetesClient(ctx context.Context, clusterName string, httpClient *ht kubeHTTPClient, ) if err != nil { - return nil, fmt.Errorf("kubernetes HTTP client could not be created: %v", err) + return nil, fmt.Errorf("kubernetes HTTP client could not be created: %w", err) } return kubeClient, nil } @@ -126,13 +132,13 @@ func GetKubernetesClient(ctx context.Context, clusterName string, httpClient *ht func GetOauthTransport(ctx context.Context, cluster *container.Cluster) (http.RoundTripper, error) { tokenSource, err := google.DefaultTokenSource(ctx, compute.CloudPlatformScope) if err != nil { - return nil, fmt.Errorf("failed to get a token source: %v", err) + return nil, fmt.Errorf("failed to get a token source: %w", err) } // Connect to Kubernetes using OAuth authentication, trusting its CA. caPool := x509.NewCertPool() caCertPEM, err := base64.StdEncoding.DecodeString(cluster.MasterAuth.ClusterCaCertificate) if err != nil { - return nil, fmt.Errorf("invalid base64 in ClusterCaCertificate: %v", err) + return nil, fmt.Errorf("invalid base64 in ClusterCaCertificate: %w", err) } caPool.AppendCertsFromPEM(caCertPEM) return &oauth2.Transport{ diff --git a/src/engine/framework_test.go b/src/engine/framework_test.go index 6b9a980..133d40c 100644 --- a/src/engine/framework_test.go +++ b/src/engine/framework_test.go @@ -4,12 +4,12 @@ import ( "testing" "github.com/nianticlabs/modron/src/common" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" "github.com/google/go-cmp/cmp" "google.golang.org/protobuf/testing/protocmp" - structpb "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/structpb" ) var resources = []*pb.Resource{ diff --git a/src/engine/rules/api_key_overbroad_scope.go b/src/engine/rules/api_key_overbroad_scope.go index 352a16a..47f1cd7 100644 --- a/src/engine/rules/api_key_overbroad_scope.go +++ b/src/engine/rules/api_key_overbroad_scope.go @@ -7,17 +7,18 @@ import ( "github.com/google/uuid" "golang.org/x/exp/slices" + "google.golang.org/protobuf/proto" - "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) -const ApiKeyOverbroadScopeRuleName = "API_KEY_WITH_OVERBROAD_SCOPE" +const apiKeyOverbroadScopeRuleName = "API_KEY_WITH_OVERBROAD_SCOPE" //nolint:gosec // TODO: Complete and/or remove excess scopes. // Uncommon scopes for API keys that should be marked as overbroad. @@ -35,20 +36,20 @@ var overbroadScopes = []string{ "vmmigration.googleapis.com", } -type ApiKeyOverbroadScopeRule struct { +type APIKeyOverbroadScopeRule struct { info model.RuleInfo } func init() { - AddRule(NewApiKeyOverbroadScopeRule()) + AddRule(NewAPIKeyOverbroadScopeRule()) } -func NewApiKeyOverbroadScopeRule() model.Rule { - return &ApiKeyOverbroadScopeRule{ +func NewAPIKeyOverbroadScopeRule() model.Rule { + return &APIKeyOverbroadScopeRule{ info: model.RuleInfo{ - Name: ApiKeyOverbroadScopeRuleName, - AcceptedResourceTypes: []string{ - common.ResourceApiKey, + Name: apiKeyOverbroadScopeRuleName, + AcceptedResourceTypes: []proto.Message{ + &pb.APIKey{}, }, }, } @@ -60,7 +61,7 @@ func toURLKey(name string) string { return name[strings.LastIndex(name, "/")+1:] } -func (r *ApiKeyOverbroadScopeRule) Check(ctx context.Context, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { +func (r *APIKeyOverbroadScopeRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { key := rsrc.GetApiKey() // If the key has no scopes, it is unrestricted. @@ -68,7 +69,7 @@ func (r *ApiKeyOverbroadScopeRule) Check(ctx context.Context, rsrc *pb.Resource) ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue("restricted"), ObservedValue: structpb.NewStringValue("unrestricted"), @@ -80,12 +81,13 @@ func (r *ApiKeyOverbroadScopeRule) Check(ctx context.Context, rsrc *pb.Resource) constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), ), Recommendation: fmt.Sprintf( - "Restrict API key [%q](https://console.cloud.google.com/apis/credentials/key/%s?project=%s) strictly to the APIs it is supposed to call.", + "Restrict API key [%q](https://console.cloud.google.com/apis/credentials/key/%s?project=%s) strictly to the APIs it is supposed to call. [More details available in our documentation.](https://github.com/nianticlabs/modron/blob/main/docs/FINDINGS.md)", toURLKey(rsrc.Name), toURLKey(rsrc.Name), constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), ), }, + Severity: pb.Severity_SEVERITY_MEDIUM, } obs = append(obs, ob) return @@ -96,7 +98,7 @@ func (r *ApiKeyOverbroadScopeRule) Check(ctx context.Context, rsrc *pb.Resource) ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue(""), ObservedValue: structpb.NewStringValue(scope), @@ -109,13 +111,14 @@ func (r *ApiKeyOverbroadScopeRule) Check(ctx context.Context, rsrc *pb.Resource) scope, ), Recommendation: fmt.Sprintf( - "Remove scope %q from API key [%q](https://console.cloud.google.com/apis/credentials/key/%s?project=%s) unless it is used.", + "Remove scope %q from API key [%q](https://console.cloud.google.com/apis/credentials/key/%s?project=%s) unless it is used. [More details available in our documentation.](https://github.com/nianticlabs/modron/blob/main/docs/FINDINGS.md)", scope, toURLKey(rsrc.Name), toURLKey(rsrc.Name), constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), ), }, + Severity: pb.Severity_SEVERITY_MEDIUM, } obs = append(obs, ob) } @@ -124,6 +127,6 @@ func (r *ApiKeyOverbroadScopeRule) Check(ctx context.Context, rsrc *pb.Resource) return } -func (r *ApiKeyOverbroadScopeRule) Info() *model.RuleInfo { +func (r *APIKeyOverbroadScopeRule) Info() *model.RuleInfo { return &r.info } diff --git a/src/engine/rules/api_key_overbroad_scope_test.go b/src/engine/rules/api_key_overbroad_scope_test.go index 8150509..60207cf 100644 --- a/src/engine/rules/api_key_overbroad_scope_test.go +++ b/src/engine/rules/api_key_overbroad_scope_test.go @@ -3,12 +3,12 @@ package rules import ( "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) func TestCheckDetectsOverbroadScope(t *testing.T) { @@ -68,37 +68,60 @@ func TestCheckDetectsOverbroadScope(t *testing.T) { }, } - got := TestRuleRun(t, resources, []model.Rule{NewApiKeyOverbroadScopeRule()}) + res1 := pb.Resource{Name: "api-key-with-overbroad-scope-1", + Type: &pb.Resource_ApiKey{ + ApiKey: &pb.APIKey{ + Scopes: []string{ + "iamcredentials.googleapis.com", + "storage_api", + "apikeys", + }, + }, + }, + ResourceGroupName: "projects/project-0", + Parent: "projects/project-0", + } - // Expected values are ordered lexicographically. want := []*pb.Observation{ { - Name: ApiKeyOverbroadScopeRuleName, - Resource: &pb.Resource{ - Name: "api-key-unrestricted-0", + Name: apiKeyOverbroadScopeRuleName, + ResourceRef: utils.GetResourceRef(&res1), + ExpectedValue: structpb.NewStringValue(""), + ObservedValue: structpb.NewStringValue("iamcredentials.googleapis.com"), + Remediation: &pb.Remediation{ + Description: "API key [\"api-key-with-overbroad-scope-1\"](https://console.cloud.google.com/apis/credentials/key/api-key-with-overbroad-scope-1?project=project-0) may have over-broad scope \"iamcredentials.googleapis.com\"", + Recommendation: "Remove scope \"iamcredentials.googleapis.com\" from API key [\"api-key-with-overbroad-scope-1\"](https://console.cloud.google.com/apis/credentials/key/api-key-with-overbroad-scope-1?project=project-0) unless it is used. [More details available in our documentation.](https://github.com/nianticlabs/modron/blob/main/docs/FINDINGS.md)", }, - ExpectedValue: structpb.NewStringValue("restricted"), - ObservedValue: structpb.NewStringValue("unrestricted"), + Severity: pb.Severity_SEVERITY_MEDIUM, }, { - Name: ApiKeyOverbroadScopeRuleName, - Resource: &pb.Resource{ - Name: "api-key-with-overbroad-scope-1", - }, + Name: apiKeyOverbroadScopeRuleName, + ResourceRef: utils.GetResourceRef(&res1), ExpectedValue: structpb.NewStringValue(""), - ObservedValue: structpb.NewStringValue("iamcredentials.googleapis.com"), + ObservedValue: structpb.NewStringValue("apikeys"), + Remediation: &pb.Remediation{ + Description: "API key [\"api-key-with-overbroad-scope-1\"](https://console.cloud.google.com/apis/credentials/key/api-key-with-overbroad-scope-1?project=project-0) may have over-broad scope \"apikeys\"", + Recommendation: "Remove scope \"apikeys\" from API key [\"api-key-with-overbroad-scope-1\"](https://console.cloud.google.com/apis/credentials/key/api-key-with-overbroad-scope-1?project=project-0) unless it is used. [More details available in our documentation.](https://github.com/nianticlabs/modron/blob/main/docs/FINDINGS.md)", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, }, { - Name: ApiKeyOverbroadScopeRuleName, - Resource: &pb.Resource{ - Name: "api-key-with-overbroad-scope-1", + Name: apiKeyOverbroadScopeRuleName, + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-2"), + GroupName: "projects/project-0", + ExternalId: proto.String("api-key-unrestricted-0"), + CloudPlatform: pb.CloudPlatform_GCP, }, - ExpectedValue: structpb.NewStringValue(""), - ObservedValue: structpb.NewStringValue("apikeys"), + ExpectedValue: structpb.NewStringValue("restricted"), + ObservedValue: structpb.NewStringValue("unrestricted"), + Remediation: &pb.Remediation{ + Description: "API key [\"api-key-unrestricted-0\"](https://console.cloud.google.com/apis/credentials/key/api-key-unrestricted-0?project=project-0) is unrestricted, which allows it to be used against any enabled GCP API", + Recommendation: "Restrict API key [\"api-key-unrestricted-0\"](https://console.cloud.google.com/apis/credentials/key/api-key-unrestricted-0?project=project-0) strictly to the APIs it is supposed to call. [More details available in our documentation.](https://github.com/nianticlabs/modron/blob/main/docs/FINDINGS.md)", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, }, } - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewAPIKeyOverbroadScopeRule()}, want) } diff --git a/src/engine/rules/bucket_is_public.go b/src/engine/rules/bucket_is_public.go index a2faf00..7e23d4a 100644 --- a/src/engine/rules/bucket_is_public.go +++ b/src/engine/rules/bucket_is_public.go @@ -4,12 +4,12 @@ import ( "context" "fmt" - "github.com/golang/glog" "github.com/google/uuid" + "google.golang.org/protobuf/proto" - "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -29,21 +29,21 @@ func NewBucketIsPublicRule() model.Rule { return &BucketIsPublicRule{ info: model.RuleInfo{ Name: BucketIsPublicRuleName, - AcceptedResourceTypes: []string{ - common.ResourceBucket, + AcceptedResourceTypes: []proto.Message{ + &pb.Bucket{}, }, }, } } -func (r *BucketIsPublicRule) Check(ctx context.Context, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { +func (r *BucketIsPublicRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { bk := rsrc.GetBucket() if bk.AccessType == pb.Bucket_PUBLIC { ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue(pb.Bucket_PRIVATE.String()), ObservedValue: structpb.NewStringValue(bk.AccessType.String()), @@ -59,10 +59,11 @@ func (r *BucketIsPublicRule) Check(ctx context.Context, rsrc *pb.Resource) (obs rsrc.Name, ), }, + Severity: pb.Severity_SEVERITY_MEDIUM, } obs = append(obs, ob) } else if bk.AccessType == pb.Bucket_ACCESS_UNKNOWN { - glog.Warningf("unknown access type for bucket %q", rsrc.Name) + log.Warnf("unknown access type for bucket %q", rsrc.Name) } return diff --git a/src/engine/rules/bucket_is_public_test.go b/src/engine/rules/bucket_is_public_test.go index 2275c4a..eac0d60 100644 --- a/src/engine/rules/bucket_is_public_test.go +++ b/src/engine/rules/bucket_is_public_test.go @@ -3,14 +3,12 @@ package rules import ( "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - - "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" - + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" ) func TestCheckDetectsPublicBucket(t *testing.T) { @@ -74,26 +72,37 @@ func TestCheckDetectsPublicBucket(t *testing.T) { want := []*pb.Observation{ { Name: BucketIsPublicRuleName, - Resource: &pb.Resource{ - Name: "public-bucket-2-is-detected", + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-1"), + GroupName: "projects/project-0", + ExternalId: proto.String("public-bucket-2-is-detected"), + CloudPlatform: pb.CloudPlatform_GCP, }, ObservedValue: structpb.NewStringValue("PUBLIC"), ExpectedValue: structpb.NewStringValue("PRIVATE"), + Remediation: &pb.Remediation{ + Description: "Bucket [\"public-bucket-2-is-detected\"](https://console.cloud.google.com/storage/browser/public-bucket-2-is-detected) is publicly accessible", + Recommendation: "Unless strictly needed, restrict the IAM policy of bucket [\"public-bucket-2-is-detected\"](https://console.cloud.google.com/storage/browser/public-bucket-2-is-detected) to prevent unconditional access by anyone. For more details, see [here](https://cloud.google.com/storage/docs/using-public-access-prevention)", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, }, { Name: BucketIsPublicRuleName, - Resource: &pb.Resource{ - Name: "public-bucket-1-is-detected", + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-2"), + GroupName: "projects/project-0", + ExternalId: proto.String("public-bucket-1-is-detected"), + CloudPlatform: pb.CloudPlatform_GCP, }, ObservedValue: structpb.NewStringValue("PUBLIC"), ExpectedValue: structpb.NewStringValue("PRIVATE"), + Remediation: &pb.Remediation{ + Description: "Bucket [\"public-bucket-1-is-detected\"](https://console.cloud.google.com/storage/browser/public-bucket-1-is-detected) is publicly accessible", + Recommendation: "Unless strictly needed, restrict the IAM policy of bucket [\"public-bucket-1-is-detected\"](https://console.cloud.google.com/storage/browser/public-bucket-1-is-detected) to prevent unconditional access by anyone. For more details, see [here](https://cloud.google.com/storage/docs/using-public-access-prevention)", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, }, } - got := TestRuleRun(t, resources, []model.Rule{NewBucketIsPublicRule()}) - - // Check that the observations are correct. - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewBucketIsPublicRule()}, want) } diff --git a/src/engine/rules/cluster_nodes_have_public_ips.go b/src/engine/rules/cluster_nodes_have_public_ips.go index 5560b20..ac62690 100644 --- a/src/engine/rules/cluster_nodes_have_public_ips.go +++ b/src/engine/rules/cluster_nodes_have_public_ips.go @@ -5,11 +5,12 @@ import ( "fmt" "github.com/google/uuid" + "google.golang.org/protobuf/proto" - "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -29,21 +30,21 @@ func NewClusterNodesHavePublicIpsRule() model.Rule { return &ClusterNodesHavePublicIpsRule{ info: model.RuleInfo{ Name: ClusterNodesHavePublicIps, - AcceptedResourceTypes: []string{ - common.ResourceKubernetesCluster, + AcceptedResourceTypes: []proto.Message{ + &pb.KubernetesCluster{}, }, }, } } -func (r *ClusterNodesHavePublicIpsRule) Check(ctx context.Context, rsrc *pb.Resource) ([]*pb.Observation, []error) { +func (r *ClusterNodesHavePublicIpsRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) ([]*pb.Observation, []error) { k8s := rsrc.GetKubernetesCluster() - obs := []*pb.Observation{} + var obs []*pb.Observation if !k8s.PrivateCluster { ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue("private"), ObservedValue: structpb.NewStringValue("public"), @@ -59,6 +60,7 @@ func (r *ClusterNodesHavePublicIpsRule) Check(ctx context.Context, rsrc *pb.Reso constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), ), }, + Severity: pb.Severity_SEVERITY_HIGH, } obs = append(obs, ob) } diff --git a/src/engine/rules/cluster_nodes_have_public_ips_test.go b/src/engine/rules/cluster_nodes_have_public_ips_test.go index 974e8d4..4345ead 100644 --- a/src/engine/rules/cluster_nodes_have_public_ips_test.go +++ b/src/engine/rules/cluster_nodes_have_public_ips_test.go @@ -3,11 +3,11 @@ package rules import ( "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" ) func TestPublicClusterNodesDetection(t *testing.T) { @@ -42,21 +42,25 @@ func TestPublicClusterNodesDetection(t *testing.T) { }, } - got := TestRuleRun(t, resources, []model.Rule{NewClusterNodesHavePublicIpsRule()}) - // Expected values are ordered lexicographically. want := []*pb.Observation{ { Name: ClusterNodesHavePublicIps, - Resource: &pb.Resource{ - Name: "public-cluster", + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-0"), + GroupName: "projects/project-0", + ExternalId: proto.String("public-cluster"), + CloudPlatform: pb.CloudPlatform_GCP, }, ExpectedValue: structpb.NewStringValue("private"), ObservedValue: structpb.NewStringValue("public"), + Remediation: &pb.Remediation{ + Description: "Cluster [\"public-cluster\"](https://console.cloud.google.com/kubernetes/list/overview?project=project-0) has a public IP, which could make it accessible by anyone on the internet", + Recommendation: "Unless strictly needed, redeploy cluster [\"public-cluster\"](https://console.cloud.google.com/kubernetes/list/overview?project=project-0) as a [private cluster](https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters)", + }, + Severity: pb.Severity_SEVERITY_HIGH, }, } - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewClusterNodesHavePublicIpsRule()}, want) } diff --git a/src/engine/rules/common.go b/src/engine/rules/common.go index 4aaf724..f3278e1 100644 --- a/src/engine/rules/common.go +++ b/src/engine/rules/common.go @@ -4,12 +4,26 @@ import ( "regexp" "strings" - "github.com/nianticlabs/modron/src/pb" + "github.com/nianticlabs/modron/src/constants" + pb "github.com/nianticlabs/modron/src/proto/generated" + + "github.com/sirupsen/logrus" ) +var log = logrus.StandardLogger().WithField(constants.LogKeyPkg, "rules") + func getAccountRoles(perm *pb.Permission, account string) (roles []string) { for _, principal := range perm.Principals { - if strings.EqualFold(principal, account) { + if strings.HasPrefix(principal, PrincipalDeleted) { + continue + } + s := strings.Split(principal, ":") + if len(s) != 2 { // nolint:mnd + log.Warn("invalid principal in org policy: ", principal) + continue + } + p := strings.Split(principal, ":")[1] + if strings.EqualFold(p, account) { roles = append(roles, perm.Role) } } @@ -17,12 +31,7 @@ func getAccountRoles(perm *pb.Permission, account string) (roles []string) { } const ( - PrincipalServiceAccount = "serviceAccount" - PrincipalUser = "user" - PrincipalGroup = "group" - PrincipalAllUsers = "allUsers" - PrincipalAllAuthenticatedUsers = "allAuthenticatedUsers" - PrincipalDomain = "domain" + PrincipalDeleted = "deleted:" ) // TODO: Add SelfLink and HumanReadableName field to Protobuf and move this logic to the collector. diff --git a/src/engine/rules/container_running.go b/src/engine/rules/container_running.go new file mode 100644 index 0000000..3436ef5 --- /dev/null +++ b/src/engine/rules/container_running.go @@ -0,0 +1,114 @@ +package rules + +import ( + "context" + "fmt" + "strings" + + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" +) + +const ( + ContainerRunningRuleName = "CONTAINER_NOT_RUNNING" + separator = "@@" +) + +type ContainerRunningConfig struct { + // RequiredContainers is a map of namespaces to a list of prefixes of the pods that should be running: for example {"default": ["my-pod-"]} + RequiredContainers map[string][]string `json:"requiredContainers"` +} + +var ( + found = map[string]bool{} +) + +type ContainerRunningRule struct { + info model.RuleInfo +} + +func init() { + AddRule(NewContainerRunningRule()) +} + +func NewContainerRunningRule() model.Rule { + return &ContainerRunningRule{ + info: model.RuleInfo{ + Name: ContainerRunningRuleName, + AcceptedResourceTypes: []proto.Message{ + &pb.KubernetesCluster{}, + }, + }, + } +} + +func (r *ContainerRunningRule) Check(ctx context.Context, e model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { + var cfg ContainerRunningConfig + if err := utils.GetRuleConfig(ctx, e, r.info.Name, &cfg); err != nil { + return nil, []error{fmt.Errorf("unable to parse rule config: %w", err)} + } + clusterChildren, err := e.GetChildren(ctx, rsrc.Name) + if err != nil { + errs = append(errs, err) + return obs, errs + } + + for _, namespace := range clusterChildren { + // This is safe, if the function returns an error, the value will be "" + if n, _ := utils.TypeFromResource(namespace); n != common.ResourceNamespace { + continue + } + if _, ok := cfg.RequiredContainers[namespace.Name]; !ok { + // There are no pods to check in this namespace. + continue + } + + pods, err := e.GetChildren(ctx, namespace.Name) + if err != nil { + errs = append(errs, err) + } + for _, pod := range pods { + if p, _ := utils.TypeFromResource(pod); p != common.ResourcePod { + continue + } + for _, prefix := range cfg.RequiredContainers[namespace.Name] { + if pod.GetPod().GetPhase() == pb.Pod_RUNNING && strings.HasPrefix(pod.Name, prefix) { + found[namespace.Name+separator+prefix] = true + } + } + } + } + + for namespace, prefixes := range cfg.RequiredContainers { + for _, prefix := range prefixes { + if !found[namespace+separator+prefix] { + // TODO: Improve the observation by reporting the state of the existing pod if any. + obs = append(obs, &pb.Observation{ + Uid: uuid.NewString(), + Timestamp: timestamppb.Now(), + ResourceRef: utils.GetResourceRef(rsrc), + Name: r.Info().Name, + ExpectedValue: structpb.NewStringValue(prefix), + ObservedValue: structpb.NewStringValue(""), + Remediation: &pb.Remediation{ + Description: fmt.Sprintf("Cluster %s doesn't run any running container starting with %s", rsrc.DisplayName, prefix), + Recommendation: "This cluster is likely missing an important part of infrastructure. Check the cluster configuration or reach out to your tech support or SRE.", + }, + Severity: pb.Severity_SEVERITY_LOW, + }) + } + } + } + return +} + +func (r *ContainerRunningRule) Info() *model.RuleInfo { + return &r.info +} diff --git a/src/engine/rules/container_running_test.go b/src/engine/rules/container_running_test.go new file mode 100644 index 0000000..f2d8ec4 --- /dev/null +++ b/src/engine/rules/container_running_test.go @@ -0,0 +1,81 @@ +package rules + +import ( + "testing" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +func TestCheckContainerNotRunning(t *testing.T) { + resources := []*pb.Resource{ + { + Name: testProjectName, + Parent: "", + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{}, + }, + }, + { + Name: "Cluster", + DisplayName: "cluster-display-name", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_KubernetesCluster{ + KubernetesCluster: &pb.KubernetesCluster{}, + }, + }, + { + Name: "Namespace", + Parent: "Cluster", + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_Namespace{ + Namespace: &pb.Namespace{}, + }, + }, + } + + want := []*pb.Observation{ + { + Name: ContainerRunningRuleName, + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-0"), + GroupName: "projects/project-0", + ExternalId: proto.String("Cluster"), + CloudPlatform: pb.CloudPlatform_GCP, + }, + ObservedValue: structpb.NewStringValue(""), + ExpectedValue: structpb.NewStringValue("pod-prefix-1-"), + Remediation: &pb.Remediation{ + Description: "Cluster cluster-display-name doesn't run any running container starting with pod-prefix-1-", + Recommendation: "This cluster is likely missing an important part of infrastructure. Check the cluster configuration or reach out to your tech support or SRE.", + }, + Severity: pb.Severity_SEVERITY_LOW, + }, + { + Name: ContainerRunningRuleName, + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-0"), + GroupName: "projects/project-0", + ExternalId: proto.String("Cluster"), + CloudPlatform: pb.CloudPlatform_GCP, + }, + ObservedValue: structpb.NewStringValue(""), + ExpectedValue: structpb.NewStringValue("pod-prefix-2-"), + Remediation: &pb.Remediation{ + Description: "Cluster cluster-display-name doesn't run any running container starting with pod-prefix-2-", + Recommendation: "This cluster is likely missing an important part of infrastructure. Check the cluster configuration or reach out to your tech support or SRE.", + }, + Severity: pb.Severity_SEVERITY_LOW, + }, + } + + TestRuleRun(t, resources, []model.Rule{NewContainerRunningRule()}, want) +} diff --git a/src/engine/rules/cross_environment_permission.go b/src/engine/rules/cross_environment_permission.go new file mode 100644 index 0000000..133b2c5 --- /dev/null +++ b/src/engine/rules/cross_environment_permission.go @@ -0,0 +1,112 @@ +package rules + +import ( + "context" + "fmt" + "strings" + + "github.com/google/uuid" + "google.golang.org/api/cloudidentity/v1" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/utils" +) + +const CrossEnvironmentPermissionsRuleName = "CROSS_ENVIRONMENT_PERMISSIONS" + +type CrossEnvironmentPermissionsRule struct { + info model.RuleInfo + cloudIdentityService *cloudidentity.Service +} + +func init() { + AddRule(NewCrossEnvironmentPermissionsRule()) +} + +func NewCrossEnvironmentPermissionsRule() model.Rule { + return &CrossEnvironmentPermissionsRule{ + info: model.RuleInfo{ + Name: CrossEnvironmentPermissionsRuleName, + AcceptedResourceTypes: []proto.Message{ + &pb.KubernetesCluster{}, + &pb.ServiceAccount{}, + &pb.Bucket{}, + // &pb.Database{}, // TODO: Uncomment when we start collecting DBs IAM Policies + &pb.ResourceGroup{}, + }, + }, + cloudIdentityService: nil, + } +} + +func (r *CrossEnvironmentPermissionsRule) Check(ctx context.Context, e model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { + log = log.WithField("rule", r.info.Name) + policy := rsrc.IamPolicy + if policy == nil { + return nil, []error{fmt.Errorf("resource %s has no IAM policy", rsrc.Name)} + } + + hierarchy, err := e.GetHierarchy(ctx, rsrc.CollectionUid) + if err != nil { + return nil, []error{err} + } + + myEnv := risk.GetEnvironment(e.GetTagConfig(), hierarchy, rsrc.ResourceGroupName) + principals := make(map[string]string) + for _, perm := range policy.Permissions { + for _, p := range perm.Principals { + if !strings.HasPrefix(p, constants.GCPServiceAccountPrefix) { + log.Debugf("skipping principal %s", p) + continue + } + saEmail := strings.TrimPrefix(p, constants.GCPServiceAccountPrefix) + saProject := utils.GetGCPProjectFromSAEmail(saEmail) + if saProject == "" { + log.Warnf("got an invalid project for service account %s", saEmail) + continue + } + if utils.IsGCPServiceAccountProject(saProject) { + log.Debugf("service account %s is a GCP service account", saEmail) + continue + } + if saProject == strings.TrimPrefix(rsrc.ResourceGroupName, constants.GCPProjectsNamePrefix) { + log.Debugf("service account %s is in the same project as the resource", saEmail) + continue + } + // Cross project + env := risk.GetEnvironment(e.GetTagConfig(), hierarchy, constants.GCPProjectsNamePrefix+saProject) + if env != myEnv { + principals[saEmail] = env + } + } + } + + for principal, otherEnv := range principals { + obs = append(obs, &pb.Observation{ + Uid: uuid.NewString(), + Timestamp: timestamppb.Now(), + Name: r.info.Name, + ExpectedValue: structpb.NewStringValue(myEnv), + ObservedValue: structpb.NewStringValue(otherEnv), + Remediation: &pb.Remediation{ + Description: fmt.Sprintf("%s is in a different environment than the resource %q", principal, rsrc.Name), + Recommendation: fmt.Sprintf("Revoke the access of %q to the resource %q", principal, rsrc.Name), + }, + ResourceRef: utils.GetResourceRef(rsrc), + Category: pb.Observation_CATEGORY_MISCONFIGURATION, + Severity: pb.Severity_SEVERITY_HIGH, + }) + } + + return obs, errs +} + +func (r *CrossEnvironmentPermissionsRule) Info() *model.RuleInfo { + return &r.info +} diff --git a/src/engine/rules/cross_project_permissions.go b/src/engine/rules/cross_project_permissions.go index 067f016..eb666b2 100644 --- a/src/engine/rules/cross_project_permissions.go +++ b/src/engine/rules/cross_project_permissions.go @@ -5,18 +5,17 @@ import ( "fmt" "strings" - "github.com/golang/glog" "github.com/google/uuid" "golang.org/x/exp/slices" "google.golang.org/api/cloudidentity/v1" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/constants" - "github.com/nianticlabs/modron/src/engine" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) const CrossProjectPermissionsRuleName = "CROSS_PROJECT_PERMISSIONS" @@ -55,9 +54,9 @@ type CrossProjectPermissionsRule struct { } type FindingPrincipal struct { - account string - group string - role string + account string + projectID string + role string } func init() { @@ -68,11 +67,12 @@ func NewCrossProjectPermissionsRule() model.Rule { return &CrossProjectPermissionsRule{ info: model.RuleInfo{ Name: CrossProjectPermissionsRuleName, - AcceptedResourceTypes: []string{ - common.ResourceServiceAccount, - common.ResourceBucket, - common.ResourceDatabase, - common.ResourceResourceGroup, + AcceptedResourceTypes: []proto.Message{ + &pb.KubernetesCluster{}, + &pb.ServiceAccount{}, + &pb.Bucket{}, + // &pb.Database{}, // TODO: Uncomment when we start collecting DBs IAM Policies + &pb.ResourceGroup{}, }, }, cloudIdentityService: nil, @@ -80,148 +80,94 @@ func NewCrossProjectPermissionsRule() model.Rule { } func getResourceSpecificString(rsrc *pb.Resource) string { - switch rsrc.Type.(type) { - case *pb.Resource_Bucket: - return "storage bucket [%q](https://console.cloud.google.com/storage/browser/%s)" - case *pb.Resource_Database: - return "database [%q](https://console.cloud.google.com/spanner/instances/%s/details/databases)" - case *pb.Resource_ServiceAccount: - return "service account [%q](https://console.cloud.google.com/iam-admin/serviceaccounts?project=%s)" - case *pb.Resource_ResourceGroup: - return "project [%q](https://console.cloud.google.com/welcome?project=%s)" - default: - return "" - } + resourceLink := utils.LinkGCPResource(rsrc) + return fmt.Sprintf("%s [%s](%s)", resourceLink.Type, resourceLink.Name, resourceLink.URL) } -func getRemediationByResourceType(rsrc *pb.Resource, fp *FindingPrincipal) pb.Remediation { +func getRemediationByResourceType(rsrc *pb.Resource, fp *FindingPrincipal) *pb.Remediation { resourceString := getResourceSpecificString(rsrc) - var throughGroup string - var recommendation string - var recommendationTemplate string - var linkContent string - switch rsrc.Type.(type) { - case *pb.Resource_Bucket, *pb.Resource_Database: - linkContent = rsrc.Name - case *pb.Resource_ServiceAccount, *pb.Resource_ResourceGroup: - linkContent = constants.ResourceWithoutProjectsPrefix(rsrc.Name) - } - if fp.group != "" { - throughGroup = fmt.Sprintf(". The user is part of the group %s", fp.group) - recommendationTemplate = - "Remove the account %q from the group %q, remove the group from the " + - resourceString + - " or replace the principal %q with a principal created in the project %q that grants it the **smallest set of permissions** needed to operate" - recommendation = fmt.Sprintf(recommendationTemplate, - fp.account, - fp.group, - rsrc.Name, - linkContent, - fp.account, - constants.ResourceWithoutProjectsPrefix(rsrc.Parent), - ) - } else { - throughGroup = "" - recommendationTemplate = - "Replace the principal %q controlling the " + - resourceString + - " with a principal created in the project %q that grants it the **smallest set of permissions** needed to operate" - recommendation = fmt.Sprintf( - recommendationTemplate, - fp.account, - rsrc.Name, - linkContent, - constants.ResourceWithoutProjectsPrefix(rsrc.Parent), - ) - } + principalString := getResourceSpecificString(&pb.Resource{ + Name: fp.account, + ResourceGroupName: constants.ResourceWithProjectsPrefix(fp.projectID), + Type: &pb.Resource_ServiceAccount{}, + }) + recommendation := fmt.Sprintf( + "Replace the %s controlling %s with a principal created in the project %q that grants it the **smallest set of permissions** needed to operate.", + principalString, + resourceString, + rsrc.ResourceGroupName, + ) switch rsrc.Type.(type) { case *pb.Resource_Bucket, *pb.Resource_Database, *pb.Resource_ServiceAccount: - descriptionTemplate := "The " + resourceString + " is controlled by the principal %q with role %s defined in another project%s" - return pb.Remediation{ + return &pb.Remediation{ Description: fmt.Sprintf( - descriptionTemplate, - rsrc.Name, - linkContent, - fp.account, + "The %s is controlled by the %s with role `%s` defined in project %q", + resourceString, + principalString, fp.role, - throughGroup, + fp.projectID, ), Recommendation: recommendation, } case *pb.Resource_ResourceGroup: - descriptionTemplate := - "The " + - resourceString + - " gives the principal %q vast permissions through the role %s%s This principal is defined in another project which means that anybody with rights in that project can use it to control the resources in this one" - return pb.Remediation{ + return &pb.Remediation{ Description: fmt.Sprintf( - descriptionTemplate, - rsrc.Name, - linkContent, - fp.account, + "The %s gives the %s vast permissions through the role `%s`.\n"+ + "This principal is defined in project %q, which means that anybody with rights in that project can use it to control the resources in this one", + resourceString, + principalString, fp.role, - "."+throughGroup, + fp.projectID, ), Recommendation: recommendation, } default: - return pb.Remediation{} + return &pb.Remediation{} } } -func isCrossProjectServiceAccount(ctx context.Context, user string, rsrc *pb.Resource) (bool, error) { - svcAccnt := strings.Split(user, "@") +func isCrossProjectServiceAccount(user string, sourceProjectID string) (bool, string) { if user == "allUsers" || user == "allAuthenticatedUsers" { - return false, nil + return false, "" } - if len(svcAccnt) < 2 { - glog.Warningf("unknown service account %q", user) - return false, nil + svcAccnt := strings.Split(user, "@") + if len(svcAccnt) < 2 { //nolint:mnd + log.Warnf("unknown service account %q", user) + return false, "" } - _, contained := googleManagedAccountSuffixes[svcAccnt[1]] - var resourceId string - switch rsrc.Type.(type) { // ResourceGroup is only available on a Resource_ResourceGroup and not on others - case *pb.Resource_ResourceGroup: - resourceId = rsrc.GetResourceGroup().Identifier - default: - parent, err := engine.GetResource(ctx, rsrc.Parent) - if err != nil { - err = fmt.Errorf("could not get parent %v of resource %v", rsrc.Parent, rsrc.Name) - glog.Error(err) - return false, err - } - resourceId = parent.GetResourceGroup().Identifier + if !strings.HasSuffix(user, "iam.gserviceaccount.com") { + return false, "" + } + saProject := utils.GetGCPProjectFromSAEmail(user) + if saProject == "" { + log.Warnf("could not get project from service account %q", user) + return false, "" } - return strings.HasSuffix(user, ".gserviceaccount.com") && // It should be a service account - !strings.HasPrefix(strings.TrimPrefix(user, constants.GCPServiceAccountPrefix), resourceId) && // Should not be an account with the project prefix - !strings.Contains(user, constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName)) && // The service account was created in another project - !strings.HasSuffix(user, "@cloudservices.gserviceaccount.com") && - !contained, nil + if utils.IsGCPServiceAccountProject(saProject) { + return false, "" + } + if saProject == sourceProjectID { + return false, "" + } + return true, saProject } func (r *CrossProjectPermissionsRule) createObservation(rsrc *pb.Resource, fp *FindingPrincipal) *pb.Observation { - remediation := getRemediationByResourceType(rsrc, fp) - observed_value := structpb.NewStringValue(fmt.Sprintf("%q with role %s", fp.account, fp.role)) - if fp.group != "" { - observed_value = structpb.NewStringValue(fmt.Sprintf("%q > %q", fp.group, fp.account)) - } - ob := &pb.Observation{ + return &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, - ExpectedValue: structpb.NewStringValue(""), - ObservedValue: observed_value, - Remediation: &remediation, + ExpectedValue: structpb.NewStringValue(strings.TrimPrefix(rsrc.ResourceGroupName, constants.GCPProjectsNamePrefix)), + ObservedValue: structpb.NewStringValue(fp.projectID), + Remediation: getRemediationByResourceType(rsrc, fp), + Severity: pb.Severity_SEVERITY_MEDIUM, } - - return ob } -func (r *CrossProjectPermissionsRule) Check(ctx context.Context, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { +func (r *CrossProjectPermissionsRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { policy := rsrc.IamPolicy - if policy == nil { return nil, []error{fmt.Errorf("resource %s has no IAM policy", rsrc.Name)} } @@ -229,19 +175,20 @@ func (r *CrossProjectPermissionsRule) Check(ctx context.Context, rsrc *pb.Resour for _, perm := range policy.Permissions { if slices.Contains(rolesToWatch, perm.Role) { for _, principal := range perm.GetPrincipals() { - // TODO: Check for cross project users in groups - shouldFlag, err := isCrossProjectServiceAccount(ctx, principal, rsrc) - if err != nil { - errs = append(errs, err) - continue - } - if shouldFlag { - obs = append(obs, r.createObservation(rsrc, &FindingPrincipal{principal, "", perm.Role})) + // TODO: Check for cross project service accounts in groups + xProjectSA, projectID := isCrossProjectServiceAccount( + principal, + strings.TrimPrefix( + rsrc.ResourceGroupName, + constants.GCPProjectsNamePrefix, + ), + ) + if xProjectSA { + obs = append(obs, r.createObservation(rsrc, &FindingPrincipal{principal, projectID, perm.Role})) } } } } - return obs, errs } diff --git a/src/engine/rules/cross_project_permissions_test.go b/src/engine/rules/cross_project_permissions_test.go index cf1efbe..693a5d5 100644 --- a/src/engine/rules/cross_project_permissions_test.go +++ b/src/engine/rules/cross_project_permissions_test.go @@ -4,20 +4,20 @@ import ( "fmt" "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "google.golang.org/protobuf/types/known/structpb" "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) func TestCheckDetectsCrossProjectAccount(t *testing.T) { - collectId := uuid.NewString() + collectID := uuid.NewString() testProjectName1 := "projects/project-1" testProjectIdentifier := "12345678" + bucketName := "bucket1" accountZeroProjectZero := fmt.Sprintf("serviceAccount:account-0@%s.iam.gserviceaccount.com", constants.ResourceWithoutProjectsPrefix(testProjectName)) devAccountProjectZero := fmt.Sprintf("serviceAccount:%s-compute@developer.gserviceaccount.com", testProjectIdentifier) accountOneProjectOne := fmt.Sprintf("serviceAccount:account-1@%s.iam.gserviceaccount.com", constants.ResourceWithoutProjectsPrefix(testProjectName1)) @@ -27,170 +27,242 @@ func TestCheckDetectsCrossProjectAccount(t *testing.T) { googleServiceAcc := "serviceAccount:service-123455@gcp-sa-firebase.iam.gserviceaccount.com" // TODO: Add a test case that involves a group - resources := []*pb.Resource{ - { - Name: testProjectName, - Parent: "", - ResourceGroupName: testProjectName, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{ - Resource: nil, - Permissions: []*pb.Permission{ - { - Role: "iam.serviceAccountAdmin", - Principals: []string{ - accountZeroProjectZero, - }, + testProjectNameResource := &pb.Resource{ + Name: testProjectName, + Parent: "", + ResourceGroupName: testProjectName, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{ + Resource: nil, + Permissions: []*pb.Permission{ + { + Role: "iam.serviceAccountAdmin", + Principals: []string{ + accountZeroProjectZero, }, - { - Role: "dataflow.admin", - Principals: []string{ - accountOneProjectOne, - devAccountProjectZero, - }, + }, + { + Role: "dataflow.admin", + Principals: []string{ + accountOneProjectOne, + devAccountProjectZero, }, - { - Role: "viewer", - Principals: []string{ - accountOneProjectOne, - }, + }, + { + Role: "viewer", + Principals: []string{ + accountOneProjectOne, }, }, }, - Type: &pb.Resource_ResourceGroup{ - ResourceGroup: &pb.ResourceGroup{ - Identifier: testProjectIdentifier, - }, + }, + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: testProjectIdentifier, }, }, - { - Name: testProjectName1, - Parent: "", - ResourceGroupName: testProjectName1, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{ - Resource: nil, - Permissions: []*pb.Permission{ - { - Role: "compute.admin", - Principals: []string{ - accountTwoProjectOne, - accountThreeProjectZero, - googleServiceAcc, - }, + } + + testProjectName1Resource := &pb.Resource{ + Name: testProjectName1, + Parent: "", + ResourceGroupName: testProjectName1, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{ + Resource: nil, + Permissions: []*pb.Permission{ + { + Role: "compute.admin", + Principals: []string{ + accountTwoProjectOne, + accountThreeProjectZero, + googleServiceAcc, }, }, }, - Type: &pb.Resource_ResourceGroup{ - ResourceGroup: &pb.ResourceGroup{ - Identifier: "9876543221", - }, + }, + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: "9876543221", }, }, - { - Name: accountZeroProjectZero, - Parent: testProjectName, - ResourceGroupName: testProjectName, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ServiceAccount{ - ServiceAccount: &pb.ServiceAccount{ - ExportedCredentials: []*pb.ExportedCredentials{}, + } + + bucketResource := &pb.Resource{ + Name: bucketName, + Parent: testProjectName, + ResourceGroupName: testProjectName, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{ + Resource: nil, + Permissions: []*pb.Permission{ + { + Role: "storage.admin", + Principals: []string{ + devAccountProjectZero, + accountOneProjectOne, + }, }, }, }, - { - Name: accountOneProjectOne, - Parent: testProjectName1, - ResourceGroupName: testProjectName1, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{ - Resource: nil, - Permissions: []*pb.Permission{ - { - Role: "iam.serviceAccountTokenCreator", - Principals: []string{ - accountTwoProjectOne, - accountThreeProjectZero, - invalidAccount, - }, + Type: &pb.Resource_Bucket{ + Bucket: &pb.Bucket{}, + }, + } + + accountZeroResource := &pb.Resource{ + Name: accountZeroProjectZero, + Parent: testProjectName, + ResourceGroupName: testProjectName, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{}, + }, + }, + } + + accountOneResource := &pb.Resource{ + Name: accountOneProjectOne, + Parent: testProjectName1, + ResourceGroupName: testProjectName1, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{ + Resource: nil, + Permissions: []*pb.Permission{ + { + Role: "iam.serviceAccountTokenCreator", + Principals: []string{ + accountTwoProjectOne, + accountThreeProjectZero, + invalidAccount, }, }, }, - Type: &pb.Resource_ServiceAccount{ - ServiceAccount: &pb.ServiceAccount{ - ExportedCredentials: []*pb.ExportedCredentials{}, - }, + }, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{}, }, }, - { - Name: accountTwoProjectOne, - Parent: testProjectName1, - ResourceGroupName: testProjectName1, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ServiceAccount{ - ServiceAccount: &pb.ServiceAccount{ - ExportedCredentials: []*pb.ExportedCredentials{}, - }, + } + + accountTwoResource := &pb.Resource{ + Name: accountTwoProjectOne, + Parent: testProjectName1, + ResourceGroupName: testProjectName1, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{}, }, }, - { - Name: accountThreeProjectZero, - Parent: testProjectName, - ResourceGroupName: testProjectName, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ServiceAccount{ - ServiceAccount: &pb.ServiceAccount{ - ExportedCredentials: []*pb.ExportedCredentials{}, - }, + } + + accountThreeResource := &pb.Resource{ + Name: accountThreeProjectZero, + Parent: testProjectName, + ResourceGroupName: testProjectName, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{}, }, }, - { - Name: googleServiceAcc, - Parent: testProjectName1, - ResourceGroupName: testProjectName1, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ServiceAccount{ - ServiceAccount: &pb.ServiceAccount{ - ExportedCredentials: []*pb.ExportedCredentials{}, - }, + } + + googleServiceAccountResource := &pb.Resource{ + Name: googleServiceAcc, + Parent: testProjectName1, + ResourceGroupName: testProjectName1, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{}, }, }, } + devAccountProjectZeroResource := &pb.Resource{ + Name: devAccountProjectZero, + Parent: testProjectName, + ResourceGroupName: testProjectName, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{}, + }, + }, + } + + resources := []*pb.Resource{ + testProjectNameResource, + testProjectName1Resource, + bucketResource, + accountZeroResource, + accountOneResource, + accountTwoResource, + accountThreeResource, + googleServiceAccountResource, + devAccountProjectZeroResource, + } + + project0 := "[project-0](https://console.cloud.google.com/welcome?project=project-0)" + project1 := "[project-1](https://console.cloud.google.com/welcome?project=project-1)" + bucket1 := "[bucket1](https://console.cloud.google.com/storage/browser/bucket1)" + saAccount1 := "[account-1@project-1.iam.gserviceaccount.com](https://console.cloud.google.com/iam-admin/serviceaccounts/details/account-1@project-1.iam.gserviceaccount.com?project=project-1)" + saAccount3 := "[account-3@project-0.iam.gserviceaccount.com](https://console.cloud.google.com/iam-admin/serviceaccounts/details/account-3@project-0.iam.gserviceaccount.com?project=project-0)" want := []*pb.Observation{ { - Name: CrossProjectPermissionsRuleName, - Resource: &pb.Resource{ - Name: testProjectName, + Name: CrossProjectPermissionsRuleName, + ResourceRef: utils.GetResourceRef(testProjectNameResource), + ExpectedValue: structpb.NewStringValue("project-0"), + ObservedValue: structpb.NewStringValue("project-1"), + Remediation: &pb.Remediation{ + Description: `The project ` + project0 + ` gives the service account ` + saAccount1 + ` vast permissions through the role ` + "`dataflow.admin`.\n" + `This principal is defined in project "project-1", which means that anybody with rights in that project can use it to control the resources in this one`, + Recommendation: `Replace the service account ` + saAccount1 + ` controlling project ` + project0 + ` with a principal created in the project "projects/project-0" that grants it the **smallest set of permissions** needed to operate.`, + }, + Severity: pb.Severity_SEVERITY_MEDIUM, + }, + { + Name: CrossProjectPermissionsRuleName, + ResourceRef: utils.GetResourceRef(bucketResource), + ExpectedValue: structpb.NewStringValue("project-0"), + ObservedValue: structpb.NewStringValue("project-1"), + Remediation: &pb.Remediation{ + Description: `The bucket ` + bucket1 + ` is controlled by the service account ` + saAccount1 + ` with role ` + "`storage.admin`" + ` defined in project "project-1"`, + Recommendation: `Replace the service account ` + saAccount1 + ` controlling bucket ` + bucket1 + ` with a principal created in the project "projects/project-0" that grants it the **smallest set of permissions** needed to operate.`, }, - ExpectedValue: structpb.NewStringValue(""), - ObservedValue: structpb.NewStringValue(fmt.Sprintf("%q with role %s", accountOneProjectOne, "dataflow.admin")), + Severity: pb.Severity_SEVERITY_MEDIUM, }, { - Name: CrossProjectPermissionsRuleName, - Resource: &pb.Resource{ - Name: testProjectName1, + Name: CrossProjectPermissionsRuleName, + ResourceRef: utils.GetResourceRef(testProjectName1Resource), + ExpectedValue: structpb.NewStringValue("project-1"), + ObservedValue: structpb.NewStringValue("project-0"), + Remediation: &pb.Remediation{ + Description: `The project ` + project1 + ` gives the service account ` + saAccount3 + ` vast permissions through the role ` + "`compute.admin`.\n" + `This principal is defined in project "project-0", which means that anybody with rights in that project can use it to control the resources in this one`, + Recommendation: `Replace the service account ` + saAccount3 + ` controlling project ` + project1 + ` with a principal created in the project "projects/project-1" that grants it the **smallest set of permissions** needed to operate.`, }, - ExpectedValue: structpb.NewStringValue(""), - ObservedValue: structpb.NewStringValue(fmt.Sprintf("%q with role %s", accountThreeProjectZero, "compute.admin")), + Severity: pb.Severity_SEVERITY_MEDIUM, }, { - Name: CrossProjectPermissionsRuleName, - Resource: &pb.Resource{ - Name: accountOneProjectOne, + Name: CrossProjectPermissionsRuleName, + ResourceRef: utils.GetResourceRef(accountOneResource), + ExpectedValue: structpb.NewStringValue("project-1"), + ObservedValue: structpb.NewStringValue("project-0"), + Remediation: &pb.Remediation{ + Description: `The service account ` + saAccount1 + ` is controlled by the service account ` + saAccount3 + ` with role ` + "`iam.serviceAccountTokenCreator`" + ` defined in project "project-0"`, + Recommendation: `Replace the service account ` + saAccount3 + ` controlling service account ` + saAccount1 + ` with a principal created in the project "projects/project-1" that grants it the **smallest set of permissions** needed to operate.`, }, - ExpectedValue: structpb.NewStringValue(""), - ObservedValue: structpb.NewStringValue(fmt.Sprintf("%q with role %s", accountThreeProjectZero, "iam.serviceAccountTokenCreator")), + Severity: pb.Severity_SEVERITY_MEDIUM, }, } - got := TestRuleRun(t, resources, []model.Rule{NewCrossProjectPermissionsRule()}) - - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewCrossProjectPermissionsRule()}, want) } diff --git a/src/engine/rules/database_allows_unencrypted_connections.go b/src/engine/rules/database_allows_unencrypted_connections.go index 3ce7267..4de8715 100644 --- a/src/engine/rules/database_allows_unencrypted_connections.go +++ b/src/engine/rules/database_allows_unencrypted_connections.go @@ -5,11 +5,13 @@ import ( "fmt" "github.com/google/uuid" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) const DatabaseAllowsUnencryptedConnections = "DATABASE_ALLOWS_UNENCRYPTED_CONNECTIONS" @@ -26,14 +28,14 @@ func NewDatabaseAllowsUnencryptedConnectionsRule() model.Rule { return &DatabaseAllowsUnencryptedConnectionsRule{ info: model.RuleInfo{ Name: DatabaseAllowsUnencryptedConnections, - AcceptedResourceTypes: []string{ - common.ResourceDatabase, + AcceptedResourceTypes: []proto.Message{ + &pb.Database{}, }, }, } } -func (r *DatabaseAllowsUnencryptedConnectionsRule) Check(ctx context.Context, rsrc *pb.Resource) ([]*pb.Observation, []error) { +func (r *DatabaseAllowsUnencryptedConnectionsRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) ([]*pb.Observation, []error) { db := rsrc.GetDatabase() obs := []*pb.Observation{} @@ -45,7 +47,7 @@ func (r *DatabaseAllowsUnencryptedConnectionsRule) Check(ctx context.Context, rs ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewBoolValue(true), ObservedValue: structpb.NewBoolValue(false), @@ -59,6 +61,7 @@ func (r *DatabaseAllowsUnencryptedConnectionsRule) Check(ctx context.Context, rs getGcpReadableResourceName(rsrc.Name), ), }, + Severity: pb.Severity_SEVERITY_MEDIUM, } obs = append(obs, ob) } diff --git a/src/engine/rules/database_allows_unencrypted_connections_test.go b/src/engine/rules/database_allows_unencrypted_connections_test.go index a85a415..2b44ee3 100644 --- a/src/engine/rules/database_allows_unencrypted_connections_test.go +++ b/src/engine/rules/database_allows_unencrypted_connections_test.go @@ -3,16 +3,27 @@ package rules import ( "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" ) func TestCheckDetectsDatabaseAllowsUnencryptedConnections(t *testing.T) { + databaseNoForceTLS := &pb.Resource{ + Name: "database-no-force-tls", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_Database{ + Database: &pb.Database{ + Type: "cloudsql", + Version: "123", + TlsRequired: false, + }, + }, + } resources := []*pb.Resource{ { Name: testProjectName, @@ -23,19 +34,7 @@ func TestCheckDetectsDatabaseAllowsUnencryptedConnections(t *testing.T) { ResourceGroup: &pb.ResourceGroup{}, }, }, - { - Name: "database-no-force-tls", - Parent: testProjectName, - ResourceGroupName: testProjectName, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_Database{ - Database: &pb.Database{ - Type: "cloudsql", - Version: "123", - TlsRequired: false, - }, - }, - }, + databaseNoForceTLS, { Name: "database-force-tls", Parent: testProjectName, @@ -53,19 +52,17 @@ func TestCheckDetectsDatabaseAllowsUnencryptedConnections(t *testing.T) { want := []*pb.Observation{ { - Name: DatabaseAllowsUnencryptedConnections, - Resource: &pb.Resource{ - Name: "database-no-force-tls", - }, + Name: DatabaseAllowsUnencryptedConnections, + ResourceRef: utils.GetResourceRef(databaseNoForceTLS), ObservedValue: structpb.NewBoolValue(false), ExpectedValue: structpb.NewBoolValue(true), + Remediation: &pb.Remediation{ + Description: "Database database-no-force-tls allows for unencrypted connections.", + Recommendation: "Enable the require SSL setting in the database settings to allow only encrypted connections to database-no-force-tls.", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, }, } - got := TestRuleRun(t, resources, []model.Rule{NewDatabaseAllowsUnencryptedConnectionsRule()}) - - // Check that the observations are correct. - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewDatabaseAllowsUnencryptedConnectionsRule()}, want) } diff --git a/src/engine/rules/database_authorized_network_not_set_test.go b/src/engine/rules/database_authorized_network_not_set_test.go index 789cbeb..afffe9b 100644 --- a/src/engine/rules/database_authorized_network_not_set_test.go +++ b/src/engine/rules/database_authorized_network_not_set_test.go @@ -3,16 +3,28 @@ package rules import ( "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" ) func TestCheckDetectsDatabaseAuthorizedNetworksNotSet(t *testing.T) { + databasePublicAndNoAuthorizedNetworks := &pb.Resource{ + Name: "database-public-and-no-authorized-networks", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_Database{ + Database: &pb.Database{ + Type: "cloudsql", + Version: "123", + AuthorizedNetworksSettingAvailable: pb.Database_AUTHORIZED_NETWORKS_NOT_SET, + IsPublic: true, + }, + }, + } resources := []*pb.Resource{ { Name: testProjectName, @@ -23,20 +35,7 @@ func TestCheckDetectsDatabaseAuthorizedNetworksNotSet(t *testing.T) { ResourceGroup: &pb.ResourceGroup{}, }, }, - { - Name: "database-public-and-no-authorized-networks", - Parent: testProjectName, - ResourceGroupName: testProjectName, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_Database{ - Database: &pb.Database{ - Type: "cloudsql", - Version: "123", - AuthorizedNetworksSettingAvailable: pb.Database_AUTHORIZED_NETWORKS_NOT_SET, - IsPublic: true, - }, - }, - }, + databasePublicAndNoAuthorizedNetworks, { Name: "database-private-no-authorized-networks", Parent: testProjectName, @@ -68,19 +67,17 @@ func TestCheckDetectsDatabaseAuthorizedNetworksNotSet(t *testing.T) { want := []*pb.Observation{ { - Name: DatabaseAuthorizedNetworksNotSet, - Resource: &pb.Resource{ - Name: "database-public-and-no-authorized-networks", - }, + Name: DatabaseAuthorizedNetworksNotSet, + ResourceRef: utils.GetResourceRef(databasePublicAndNoAuthorizedNetworks), ObservedValue: structpb.NewStringValue("AUTHORIZED_NETWORKS_NOT_SET"), ExpectedValue: structpb.NewStringValue("AUTHORIZED_NETWORKS_SET"), + Remediation: &pb.Remediation{ + Description: "Database database-public-and-no-authorized-networks is reachable from any IP on the Internet.", + Recommendation: "Enable the authorized network setting in the database settings to restrict what networks can access database-public-and-no-authorized-networks.", + }, + Severity: pb.Severity_SEVERITY_HIGH, }, } - got := TestRuleRun(t, resources, []model.Rule{NewDatabaseAuthorizedNetworksNotSetRule()}) - - // Check that the observations are correct. - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewDatabaseAuthorizedNetworksNotSetRule()}, want) } diff --git a/src/engine/rules/database_authorized_networks_not_set.go b/src/engine/rules/database_authorized_networks_not_set.go index 42bedc3..620e339 100644 --- a/src/engine/rules/database_authorized_networks_not_set.go +++ b/src/engine/rules/database_authorized_networks_not_set.go @@ -5,11 +5,13 @@ import ( "fmt" "github.com/google/uuid" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) const DatabaseAuthorizedNetworksNotSet = "DATABASE_AUTHORIZED_NETWORKS_NOT_SET" @@ -26,14 +28,14 @@ func NewDatabaseAuthorizedNetworksNotSetRule() model.Rule { return &DatabaseAuthorizedNetworksNotSetRule{ info: model.RuleInfo{ Name: DatabaseAuthorizedNetworksNotSet, - AcceptedResourceTypes: []string{ - common.ResourceDatabase, + AcceptedResourceTypes: []proto.Message{ + &pb.Database{}, }, }, } } -func (r *DatabaseAuthorizedNetworksNotSetRule) Check(ctx context.Context, rsrc *pb.Resource) ([]*pb.Observation, []error) { +func (r *DatabaseAuthorizedNetworksNotSetRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) ([]*pb.Observation, []error) { db := rsrc.GetDatabase() obs := []*pb.Observation{} @@ -45,7 +47,7 @@ func (r *DatabaseAuthorizedNetworksNotSetRule) Check(ctx context.Context, rsrc * ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue(pb.Database_AUTHORIZED_NETWORKS_SET.String()), ObservedValue: structpb.NewStringValue(pb.Database_AUTHORIZED_NETWORKS_NOT_SET.String()), @@ -59,6 +61,7 @@ func (r *DatabaseAuthorizedNetworksNotSetRule) Check(ctx context.Context, rsrc * getGcpReadableResourceName(rsrc.Name), ), }, + Severity: pb.Severity_SEVERITY_HIGH, } obs = append(obs, ob) } diff --git a/src/engine/rules/exported_key_expiry_too_long.go b/src/engine/rules/exported_key_expiry_too_long.go index 10c2980..f026ecc 100644 --- a/src/engine/rules/exported_key_expiry_too_long.go +++ b/src/engine/rules/exported_key_expiry_too_long.go @@ -6,12 +6,14 @@ import ( "time" "github.com/google/uuid" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) const ( @@ -31,14 +33,14 @@ func NewExportedKeyIsTooOldRule() model.Rule { return &ExportedKeyIsTooOldRule{ info: model.RuleInfo{ Name: ExportedKeyIsTooOld, - AcceptedResourceTypes: []string{ - common.ResourceExportedCredentials, + AcceptedResourceTypes: []proto.Message{ + &pb.ExportedCredentials{}, }, }, } } -func (r *ExportedKeyIsTooOldRule) Check(ctx context.Context, rsrc *pb.Resource) ([]*pb.Observation, []error) { +func (r *ExportedKeyIsTooOldRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) ([]*pb.Observation, []error) { expiryMonths := 6 ec := rsrc.GetExportedCredentials() obs := []*pb.Observation{} @@ -47,7 +49,7 @@ func (r *ExportedKeyIsTooOldRule) Check(ctx context.Context, rsrc *pb.Resource) ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue("later creation date"), ObservedValue: structpb.NewStringValue(ec.CreationDate.AsTime().Format(timeFormat)), @@ -64,6 +66,7 @@ func (r *ExportedKeyIsTooOldRule) Check(ctx context.Context, rsrc *pb.Resource) expiryMonths, ), }, + Severity: pb.Severity_SEVERITY_MEDIUM, } obs = append(obs, ob) diff --git a/src/engine/rules/exported_key_expiry_too_long_test.go b/src/engine/rules/exported_key_expiry_too_long_test.go index 6e8f8f0..3554385 100644 --- a/src/engine/rules/exported_key_expiry_too_long_test.go +++ b/src/engine/rules/exported_key_expiry_too_long_test.go @@ -4,12 +4,12 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) func TestExportedKeyTooOld(t *testing.T) { @@ -17,6 +17,19 @@ func TestExportedKeyTooOld(t *testing.T) { yesterday := now.Add(time.Hour * -24) tomorrow := now.Add(time.Hour * 24) oneYearAgo := now.Add(-time.Hour * 24 * 365) + + outdatedExportedKey := &pb.Resource{ + Name: "outdated-exported-key", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ExportedCredentials{ + ExportedCredentials: &pb.ExportedCredentials{ + CreationDate: timestamppb.New(oneYearAgo), + ExpirationDate: timestamppb.New(tomorrow), + }, + }, + } resources := []*pb.Resource{ { Name: testProjectName, @@ -39,35 +52,23 @@ func TestExportedKeyTooOld(t *testing.T) { }, }, }, - { - Name: "outdated-exported-key", - Parent: testProjectName, - ResourceGroupName: testProjectName, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ExportedCredentials{ - ExportedCredentials: &pb.ExportedCredentials{ - CreationDate: timestamppb.New(oneYearAgo), - ExpirationDate: timestamppb.New(tomorrow), - }, - }, - }, + outdatedExportedKey, } - got := TestRuleRun(t, resources, []model.Rule{NewExportedKeyIsTooOldRule()}) - // Expected values are ordered lexicographically. want := []*pb.Observation{ { - Name: ExportedKeyIsTooOld, - Resource: &pb.Resource{ - Name: "outdated-exported-key", - }, + Name: ExportedKeyIsTooOld, + ResourceRef: utils.GetResourceRef(outdatedExportedKey), ExpectedValue: structpb.NewStringValue("later creation date"), ObservedValue: structpb.NewStringValue(oneYearAgo.Format("2006-01-02 15:04:05 +0000 UTC")), + Remediation: &pb.Remediation{ + Description: "Exported key [\"outdated-exported-key\"](https://console.cloud.google.com/apis/credentials?project=project-0) is too long lived", + Recommendation: "Rotate the exported key [\"outdated-exported-key\"](https://console.cloud.google.com/apis/credentials?project=project-0) every 6 months", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, }, } - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewExportedKeyIsTooOldRule()}, want) } diff --git a/src/engine/rules/exported_key_with_admin_privileges.go b/src/engine/rules/exported_key_with_admin_privileges.go index 7d8e8bd..b5778ee 100644 --- a/src/engine/rules/exported_key_with_admin_privileges.go +++ b/src/engine/rules/exported_key_with_admin_privileges.go @@ -5,13 +5,14 @@ import ( "fmt" "github.com/google/uuid" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/constants" - "github.com/nianticlabs/modron/src/engine" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) const ExportedKeyWithAdminPrivileges = "EXPORTED_KEY_WITH_ADMIN_PRIVILEGES" @@ -41,18 +42,18 @@ func NewExportedKeyWithAdminPrivilegesRule() model.Rule { return &ExportedKeyWithAdminPrivilegesRule{ info: model.RuleInfo{ Name: ExportedKeyWithAdminPrivileges, - AcceptedResourceTypes: []string{ - common.ResourceServiceAccount, + AcceptedResourceTypes: []proto.Message{ + &pb.ServiceAccount{}, }, }, } } -func (r *ExportedKeyWithAdminPrivilegesRule) Check(ctx context.Context, rsrc *pb.Resource) ([]*pb.Observation, []error) { +func (r *ExportedKeyWithAdminPrivilegesRule) Check(ctx context.Context, e model.Engine, rsrc *pb.Resource) ([]*pb.Observation, []error) { sa := rsrc.GetServiceAccount() - rsrcGroup, err := engine.GetResource(ctx, rsrc.Parent) + rsrcGroup, err := e.GetResource(ctx, rsrc.Parent) if err != nil { - return nil, []error{fmt.Errorf("error retrieving resource group of resource %q: %v", rsrc.Name, err)} + return nil, []error{fmt.Errorf("error retrieving resource group of resource %q: %w", rsrc.Name, err)} } policy := rsrcGroup.IamPolicy hasAdminRoles := false @@ -75,7 +76,7 @@ func (r *ExportedKeyWithAdminPrivilegesRule) Check(ctx context.Context, rsrc *pb ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue("0 keys"), ObservedValue: structpb.NewStringValue(fmt.Sprintf("%v keys", nbEx)), @@ -92,6 +93,7 @@ func (r *ExportedKeyWithAdminPrivilegesRule) Check(ctx context.Context, rsrc *pb constants.ResourceWithoutProjectsPrefix(rsrcGroup.Name), ), }, + Severity: pb.Severity_SEVERITY_CRITICAL, } obs = append(obs, ob) } diff --git a/src/engine/rules/exported_key_with_admin_privileges_test.go b/src/engine/rules/exported_key_with_admin_privileges_test.go index 0a7e419..9c4de7f 100644 --- a/src/engine/rules/exported_key_with_admin_privileges_test.go +++ b/src/engine/rules/exported_key_with_admin_privileges_test.go @@ -3,45 +3,75 @@ package rules import ( "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) func TestExportedKeyWithAdminPrivileges(t *testing.T) { + account1 := &pb.Resource{ + Name: "account-1", + Parent: testProjectName, + ResourceGroupName: testProjectName, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{ + {CreationDate: timestamppb.Now()}, + }, + }, + }, + } + account2 := &pb.Resource{ + Name: "account-2", + Parent: testProjectName, + ResourceGroupName: testProjectName, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{ + {CreationDate: timestamppb.Now()}, + {CreationDate: timestamppb.Now()}, + }, + }, + }, + } + resources := []*pb.Resource{ { Name: testProjectName, ResourceGroupName: testProjectName, - CollectionUid: collectId, + CollectionUid: collectID, IamPolicy: &pb.IamPolicy{ Resource: nil, Permissions: []*pb.Permission{ { Role: "owner", Principals: []string{ - "account-1", + "serviceAccount:account-1", }, }, { Role: "iam.securityAdmin", Principals: []string{ - "account-2", + "serviceAccount:account-2", }, }, { Role: "viewer", Principals: []string{ - "account-3-no-admin-privileges", + "serviceAccount:account-3-no-admin-privileges", }, }, { Role: "editor", Principals: []string{ - "account-no-exported-credentials", + "serviceAccount:account-no-exported-credentials", }, }, }, @@ -50,40 +80,13 @@ func TestExportedKeyWithAdminPrivileges(t *testing.T) { ResourceGroup: &pb.ResourceGroup{}, }, }, - { - Name: "account-1", - Parent: testProjectName, - ResourceGroupName: testProjectName, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ServiceAccount{ - ServiceAccount: &pb.ServiceAccount{ - ExportedCredentials: []*pb.ExportedCredentials{ - {CreationDate: timestamppb.Now()}, - }, - }, - }, - }, - { - Name: "account-2", - Parent: testProjectName, - ResourceGroupName: testProjectName, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ServiceAccount{ - ServiceAccount: &pb.ServiceAccount{ - ExportedCredentials: []*pb.ExportedCredentials{ - {CreationDate: timestamppb.Now()}, - {CreationDate: timestamppb.Now()}, - }, - }, - }, - }, + account1, + account2, { Name: "account-3-no-admin-privileges", Parent: testProjectName, ResourceGroupName: testProjectName, - CollectionUid: collectId, + CollectionUid: collectID, IamPolicy: &pb.IamPolicy{}, Type: &pb.Resource_ServiceAccount{ ServiceAccount: &pb.ServiceAccount{ @@ -99,7 +102,7 @@ func TestExportedKeyWithAdminPrivileges(t *testing.T) { Name: "account-no-exported-credentials", Parent: testProjectName, ResourceGroupName: testProjectName, - CollectionUid: collectId, + CollectionUid: collectID, IamPolicy: &pb.IamPolicy{}, Type: &pb.Resource_ServiceAccount{ ServiceAccount: &pb.ServiceAccount{ @@ -111,26 +114,28 @@ func TestExportedKeyWithAdminPrivileges(t *testing.T) { want := []*pb.Observation{ { - Name: ExportedKeyWithAdminPrivileges, - Resource: &pb.Resource{ - Name: "account-1", - }, + Name: ExportedKeyWithAdminPrivileges, + ResourceRef: utils.GetResourceRef(account1), ExpectedValue: structpb.NewStringValue("0 keys"), ObservedValue: structpb.NewStringValue("1 keys"), + Remediation: &pb.Remediation{ + Description: "Service account [\"account-1\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=project-0) has 1 exported keys with admin privileges", + Recommendation: "Avoid exporting keys of service accounts with admin privileges, they can be copied and used outside of Niantic. Revoke the exported key by clicking on service account [\"account-1\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=project-0), switch to the KEYS tab and delete the exported key. Instead of exporting keys, make use of [workload identity](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity) or similar concepts", + }, + Severity: pb.Severity_SEVERITY_CRITICAL, }, { - Name: ExportedKeyWithAdminPrivileges, - Resource: &pb.Resource{ - Name: "account-2", - }, + Name: ExportedKeyWithAdminPrivileges, + ResourceRef: utils.GetResourceRef(account2), ExpectedValue: structpb.NewStringValue("0 keys"), ObservedValue: structpb.NewStringValue("2 keys"), + Remediation: &pb.Remediation{ + Description: "Service account [\"account-2\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=project-0) has 2 exported keys with admin privileges", + Recommendation: "Avoid exporting keys of service accounts with admin privileges, they can be copied and used outside of Niantic. Revoke the exported key by clicking on service account [\"account-2\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=project-0), switch to the KEYS tab and delete the exported key. Instead of exporting keys, make use of [workload identity](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity) or similar concepts", + }, + Severity: pb.Severity_SEVERITY_CRITICAL, }, } - got := TestRuleRun(t, resources, []model.Rule{NewExportedKeyWithAdminPrivilegesRule()}) - - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewExportedKeyWithAdminPrivilegesRule()}, want) } diff --git a/src/engine/rules/human_with_overprivileged_basic_role.go b/src/engine/rules/human_with_overprivileged_basic_role.go new file mode 100644 index 0000000..7323686 --- /dev/null +++ b/src/engine/rules/human_with_overprivileged_basic_role.go @@ -0,0 +1,95 @@ +package rules + +import ( + "context" + "fmt" + "strings" + + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" +) + +const HumanWithOverprivilegedBasicRole = "HUMAN_WITH_OVERPRIVILEGED_BASIC_ROLE" + +// Define basic roles. +var basicRoles = map[constants.Role]struct{}{ + constants.GCPEditorRole: {}, + constants.GCPOwnerRole: {}, + constants.GCPSecurityAdminRole: {}, + constants.GCPViewerRole: {}, +} + +type HumanWithOverprivilegedBasicRoleRule struct { + info model.RuleInfo +} + +func init() { + AddRule(NewHumanWithOverprivilegedBasicRoleRule()) +} + +func NewHumanWithOverprivilegedBasicRoleRule() model.Rule { + return &HumanWithOverprivilegedBasicRoleRule{ + info: model.RuleInfo{ + Name: HumanWithOverprivilegedBasicRole, + AcceptedResourceTypes: []proto.Message{ + &pb.ResourceGroup{}, + }, + }, + } +} + +func (r *HumanWithOverprivilegedBasicRoleRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) ([]*pb.Observation, []error) { + policy := rsrc.GetIamPolicy() + + hasBasicRoles := map[string][]string{} + if policy != nil { + for _, perm := range policy.Permissions { + if _, ok := basicRoles[constants.ToRole(perm.Role)]; !ok { + continue + } + for _, principal := range perm.Principals { + // We intentionally don't check service accounts. + // Service accounts could be member of the group, this would make a false positive if the group + // only has service accounts as members. + if strings.HasPrefix(principal, constants.GCPUserAccountPrefix) || strings.HasPrefix(principal, constants.GCPAccountGroupPrefix) { + hasBasicRoles[principal] = append(hasBasicRoles[principal], perm.Role) + } + } + } + } + + obs := []*pb.Observation{} + for principal, roles := range hasBasicRoles { + ob := &pb.Observation{ + Uid: uuid.NewString(), + Timestamp: timestamppb.Now(), + ResourceRef: utils.GetResourceRef(rsrc), + Name: r.Info().Name, + ExpectedValue: structpb.NewStringValue("No basic roles"), + ObservedValue: structpb.NewStringValue(strings.Join(roles, ", ")), + Remediation: &pb.Remediation{ + Description: fmt.Sprintf( + "Human account or group %s has overprivileged basic roles on project [%s](https://console.cloud.google.com/iam-admin/iam?project=%s)", + principal, + constants.ResourceWithoutProjectsPrefix(rsrc.GetResourceGroupName()), + constants.ResourceWithoutProjectsPrefix(rsrc.GetResourceGroupName()), + ), + Recommendation: "Consider assigning \"Developer\" to the editors and \"Owner\" to the owners instead of using basic roles.", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, + } + obs = append(obs, ob) + } + return obs, nil +} + +func (r *HumanWithOverprivilegedBasicRoleRule) Info() *model.RuleInfo { + return &r.info +} diff --git a/src/engine/rules/human_with_overprivileged_basic_role_test.go b/src/engine/rules/human_with_overprivileged_basic_role_test.go new file mode 100644 index 0000000..4bdd20c --- /dev/null +++ b/src/engine/rules/human_with_overprivileged_basic_role_test.go @@ -0,0 +1,154 @@ +package rules + +import ( + "testing" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +func TestHumanWithOverprivilegedBasicRole(t *testing.T) { + iamPolicy := &pb.IamPolicy{ + Resource: &pb.Resource{ + Name: testProjectName, + }, + Permissions: []*pb.Permission{ + { + Role: "owner", + Principals: []string{ + "user:human-account-owner", + "group:group-owner", + }, + }, + { + Role: "iam.securityAdmin", + Principals: []string{ + "user:human-account-securityadmin", + }, + }, + { + Role: "viewer", + Principals: []string{ + "user:human-account-viewer", + }, + }, + { + Role: "editor", + Principals: []string{ + "user:human-account-editor", + }, + }, + { + Role: "non-basic", + Principals: []string{ + "human-account-non-basic", + }, + }, + }, + } + resources := []*pb.Resource{ + { + Name: testProjectName, + Parent: "", + ResourceGroupName: testProjectName, + CollectionUid: collectID, + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{}, + }, + IamPolicy: iamPolicy, + }, + } + + want := []*pb.Observation{ + { + ScanUid: proto.String("unit-test-scan"), + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-0"), + ExternalId: proto.String("projects/project-0"), + GroupName: "projects/project-0", + CloudPlatform: pb.CloudPlatform_GCP, + }, + Name: "HUMAN_WITH_OVERPRIVILEGED_BASIC_ROLE", + ExpectedValue: structpb.NewStringValue("No basic roles"), + ObservedValue: structpb.NewStringValue("owner"), + Remediation: &pb.Remediation{ + Description: "Human account or group user:human-account-owner has overprivileged basic roles on project [project-0](https://console.cloud.google.com/iam-admin/iam?project=project-0)", + Recommendation: "Consider assigning \"Developer\" to the editors and \"Owner\" to the owners instead of using basic roles.", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, + }, + { + ScanUid: proto.String("unit-test-scan"), + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-0"), + ExternalId: proto.String("projects/project-0"), + GroupName: "projects/project-0", + CloudPlatform: pb.CloudPlatform_GCP, + }, + Name: "HUMAN_WITH_OVERPRIVILEGED_BASIC_ROLE", + ExpectedValue: structpb.NewStringValue("No basic roles"), + ObservedValue: structpb.NewStringValue("iam.securityAdmin"), + Remediation: &pb.Remediation{ + Description: "Human account or group user:human-account-securityadmin has overprivileged basic roles on project [project-0](https://console.cloud.google.com/iam-admin/iam?project=project-0)", + Recommendation: "Consider assigning \"Developer\" to the editors and \"Owner\" to the owners instead of using basic roles.", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, + }, + { + ScanUid: proto.String("unit-test-scan"), + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-0"), + ExternalId: proto.String("projects/project-0"), + GroupName: "projects/project-0", + CloudPlatform: pb.CloudPlatform_GCP, + }, + Name: "HUMAN_WITH_OVERPRIVILEGED_BASIC_ROLE", + ExpectedValue: structpb.NewStringValue("No basic roles"), + ObservedValue: structpb.NewStringValue("owner"), + Remediation: &pb.Remediation{ + Description: "Human account or group group:group-owner has overprivileged basic roles on project [project-0](https://console.cloud.google.com/iam-admin/iam?project=project-0)", + Recommendation: "Consider assigning \"Developer\" to the editors and \"Owner\" to the owners instead of using basic roles.", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, + }, + { + ScanUid: proto.String("unit-test-scan"), + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-0"), + ExternalId: proto.String("projects/project-0"), + GroupName: "projects/project-0", + CloudPlatform: pb.CloudPlatform_GCP, + }, + Name: "HUMAN_WITH_OVERPRIVILEGED_BASIC_ROLE", + ExpectedValue: structpb.NewStringValue("No basic roles"), + ObservedValue: structpb.NewStringValue("viewer"), + Remediation: &pb.Remediation{ + Description: "Human account or group user:human-account-viewer has overprivileged basic roles on project [project-0](https://console.cloud.google.com/iam-admin/iam?project=project-0)", + Recommendation: "Consider assigning \"Developer\" to the editors and \"Owner\" to the owners instead of using basic roles.", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, + }, + { + ScanUid: proto.String("unit-test-scan"), + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-0"), + ExternalId: proto.String("projects/project-0"), + GroupName: "projects/project-0", + CloudPlatform: pb.CloudPlatform_GCP, + }, + Name: "HUMAN_WITH_OVERPRIVILEGED_BASIC_ROLE", + ExpectedValue: structpb.NewStringValue("No basic roles"), + ObservedValue: structpb.NewStringValue("editor"), + Remediation: &pb.Remediation{ + Description: "Human account or group user:human-account-editor has overprivileged basic roles on project [project-0](https://console.cloud.google.com/iam-admin/iam?project=project-0)", + Recommendation: "Consider assigning \"Developer\" to the editors and \"Owner\" to the owners instead of using basic roles.", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, + }, + } + + TestRuleRun(t, resources, []model.Rule{NewHumanWithOverprivilegedBasicRoleRule()}, want) +} diff --git a/src/engine/rules/iap_disabled.go b/src/engine/rules/iap_disabled.go new file mode 100644 index 0000000..fd450fb --- /dev/null +++ b/src/engine/rules/iap_disabled.go @@ -0,0 +1,74 @@ +package rules + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + + "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" + + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const IAPDisabledRuleName = "IAP_DISABLED" + +type IAPDisabledRule struct { + info model.RuleInfo +} + +func init() { + AddRule(NewIAPDisabledRule()) +} + +func NewIAPDisabledRule() model.Rule { + return &IAPDisabledRule{ + info: model.RuleInfo{ + Name: IAPDisabledRuleName, + AcceptedResourceTypes: []proto.Message{ + &pb.LoadBalancer{}, + }, + }, + } +} + +func (r *IAPDisabledRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { + lb := rsrc.GetLoadBalancer() + + // TODO: Add port and name validation to have more accurate detection. + if !lb.GetIap().GetEnabled() { + ob := &pb.Observation{ + Uid: uuid.NewString(), + Timestamp: timestamppb.Now(), + ResourceRef: utils.GetResourceRef(rsrc), + Name: r.Info().Name, + ExpectedValue: structpb.NewBoolValue(true), + ObservedValue: structpb.NewBoolValue(false), + Remediation: &pb.Remediation{ + Description: fmt.Sprintf( + "IAP is disabled on Load Balancer [%q](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=%s) which exposes internal resources on the internet", + getGcpReadableResourceName(rsrc.Name), + constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), + ), + Recommendation: fmt.Sprintf( + "Enable IAP on Load Balancer [%q](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=%s) to secure the access to internal resources and prevent unauthorized access.", + getGcpReadableResourceName(rsrc.Name), + constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), + ), + }, + Severity: pb.Severity_SEVERITY_MEDIUM, + } + obs = append(obs, ob) + } + + return +} + +func (r *IAPDisabledRule) Info() *model.RuleInfo { + return &r.info +} diff --git a/src/engine/rules/iap_disabled_test.go b/src/engine/rules/iap_disabled_test.go new file mode 100644 index 0000000..6ba25bd --- /dev/null +++ b/src/engine/rules/iap_disabled_test.go @@ -0,0 +1,79 @@ +package rules + +import ( + "testing" + + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" + + "google.golang.org/protobuf/types/known/structpb" +) + +func TestIAPDisabledRule(t *testing.T) { + iapDisabledResource := &pb.Resource{ + Name: "load-balancer-with-iap-disabled", + ResourceGroupName: testProjectName, + Type: &pb.Resource_LoadBalancer{ + LoadBalancer: &pb.LoadBalancer{ + Type: pb.LoadBalancer_EXTERNAL, + Iap: &pb.IAP{ + Enabled: false, + }, + }, + }, + } + iapUnspecifiedResource := &pb.Resource{ + Name: "load-balancer-with-iap-unspecified", + ResourceGroupName: testProjectName, + Type: &pb.Resource_LoadBalancer{ + LoadBalancer: &pb.LoadBalancer{ + Type: pb.LoadBalancer_EXTERNAL, + }, + }, + } + + resources := []*pb.Resource{ + { + Name: "load-balancer-with-iap-enabled", + ResourceGroupName: testProjectName, + Type: &pb.Resource_LoadBalancer{ + LoadBalancer: &pb.LoadBalancer{ + Type: pb.LoadBalancer_EXTERNAL, + Iap: &pb.IAP{ + Enabled: true, + }, + }, + }, + }, + iapDisabledResource, + iapUnspecifiedResource, + } + + want := []*pb.Observation{ + { + Name: IAPDisabledRuleName, + ResourceRef: utils.GetResourceRef(iapDisabledResource), + ExpectedValue: structpb.NewBoolValue(true), + ObservedValue: structpb.NewBoolValue(false), + Remediation: &pb.Remediation{ + Description: "IAP is disabled on Load Balancer [\"load-balancer-with-iap-disabled\"](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=project-0) which exposes internal resources on the internet", + Recommendation: "Enable IAP on Load Balancer [\"load-balancer-with-iap-disabled\"](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=project-0) to secure the access to internal resources and prevent unauthorized access.", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, + }, + { + Name: IAPDisabledRuleName, + ResourceRef: utils.GetResourceRef(iapUnspecifiedResource), + ExpectedValue: structpb.NewBoolValue(true), + ObservedValue: structpb.NewBoolValue(false), + Remediation: &pb.Remediation{ + Description: "IAP is disabled on Load Balancer [\"load-balancer-with-iap-unspecified\"](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=project-0) which exposes internal resources on the internet", + Recommendation: "Enable IAP on Load Balancer [\"load-balancer-with-iap-unspecified\"](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=project-0) to secure the access to internal resources and prevent unauthorized access.", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, + }, + } + + TestRuleRun(t, resources, []model.Rule{NewIAPDisabledRule()}, want) +} diff --git a/src/engine/rules/kubernetes_vulnerability_scanning.go b/src/engine/rules/kubernetes_vulnerability_scanning.go new file mode 100644 index 0000000..e009f03 --- /dev/null +++ b/src/engine/rules/kubernetes_vulnerability_scanning.go @@ -0,0 +1,75 @@ +package rules + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" +) + +func init() { + AddRule(NewKubernetesVulnerabilityScanningDisabledRule()) +} + +const KubernetesVulnerabilityScanningDisabledRuleName = "KUBERNETES_VULNERABILITY_SCANNING_DISABLED" + +type KubernetesVulnerabilityScanningDisabled struct { +} + +func (k KubernetesVulnerabilityScanningDisabled) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { + k8s := rsrc.GetKubernetesCluster() + if k8s.Security == nil { + errs = append(errs, fmt.Errorf("no security configuration provided for the cluster")) + return + } + switch k8s.Security.VulnerabilityScanning { + case pb.KubernetesCluster_Security_VULN_SCAN_DISABLED, pb.KubernetesCluster_Security_VULN_SCAN_UNKNOWN: + obs = append(obs, &pb.Observation{ + Uid: uuid.NewString(), + Timestamp: timestamppb.Now(), + Name: k.Info().Name, + ResourceRef: utils.GetResourceRef(rsrc), + Remediation: &pb.Remediation{ + Description: "Vulnerability scanning is disabled for this cluster", + Recommendation: "Enable the vulnerability scanning for this cluster by following the [Enable advanced vulnerability insights](https://cloud.google.com/kubernetes-engine/docs/how-to/security-posture-vulnerability-scanning#enable-advanced-insights) section of the GKE docs.\n\n" + enableGkeVulnScanCmd(rsrc), + }, + ExpectedValue: structpb.NewStringValue("basic"), + ObservedValue: structpb.NewStringValue(k8s.Security.VulnerabilityScanning.String()), + Severity: pb.Severity_SEVERITY_HIGH, + }) + case pb.KubernetesCluster_Security_VULN_SCAN_BASIC, pb.KubernetesCluster_Security_VULN_SCAN_ADVANCED: + // All good + default: + errs = append(errs, fmt.Errorf("unknown vulnerability scanning type: %s", k8s.Security.VulnerabilityScanning)) + } + return +} + +func enableGkeVulnScanCmd(k8sRsrc *pb.Resource) string { + return fmt.Sprintf( + "```gcloud container clusters update %q --project=%q --location=%q --workload-vulnerability-scanning=standard```", + k8sRsrc.Name, + utils.StripProjectsPrefix(k8sRsrc.ResourceGroupName), + k8sRsrc.GetKubernetesCluster().Location, + ) +} + +func (k KubernetesVulnerabilityScanningDisabled) Info() *model.RuleInfo { + return &model.RuleInfo{ + Name: KubernetesVulnerabilityScanningDisabledRuleName, + AcceptedResourceTypes: []proto.Message{ + &pb.KubernetesCluster{}, + }, + } +} + +func NewKubernetesVulnerabilityScanningDisabledRule() model.Rule { + return &KubernetesVulnerabilityScanningDisabled{} +} diff --git a/src/engine/rules/kubernetes_vulnerability_scanning_e2e_test.go b/src/engine/rules/kubernetes_vulnerability_scanning_e2e_test.go new file mode 100644 index 0000000..e5bb306 --- /dev/null +++ b/src/engine/rules/kubernetes_vulnerability_scanning_e2e_test.go @@ -0,0 +1,19 @@ +//go:build integration + +package rules + +import ( + "testing" + + "github.com/nianticlabs/modron/src/model" +) + +func TestKubernetesVulnerabilityScanningRuleE2E(t *testing.T) { + obs, err := TestE2ERuleRun(t, []model.Rule{NewKubernetesVulnerabilityScanningDisabledRule()}) + if err != nil { + t.Fatalf("TestE2ERuleRun unexpected error: %v", err) + } + for _, o := range obs { + t.Logf("Observation: %v", o) + } +} diff --git a/src/engine/rules/kubernetes_vulnerability_scanning_test.go b/src/engine/rules/kubernetes_vulnerability_scanning_test.go new file mode 100644 index 0000000..44c75f8 --- /dev/null +++ b/src/engine/rules/kubernetes_vulnerability_scanning_test.go @@ -0,0 +1,89 @@ +package rules + +import ( + "testing" + "time" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" +) + +var k8sVulnScanningCluster = &pb.Resource{ + Uid: "my-uuid", + CollectionUid: "scan-id", + Timestamp: timestamppb.New(time.Unix(0, 0)), + DisplayName: "KubernetesCluster", + Link: "", + Name: "my-cluster", + Parent: "projects/my-project-id", + ResourceGroupName: "projects/my-project-id", + IamPolicy: nil, + Type: &pb.Resource_KubernetesCluster{ + KubernetesCluster: &pb.KubernetesCluster{ + MasterAuthorizedNetworks: []string{ + "1.1.1.1/32", + "100.100.0.5/32", + }, + PrivateCluster: false, + MasterVersion: "1.28.9-gke.1209000", + NodesVersion: "1.28.9-gke.1209000", + Location: "us-central1-b", + Security: &pb.KubernetesCluster_Security{ + VulnerabilityScanning: pb.KubernetesCluster_Security_VULN_SCAN_DISABLED, + }, + }, + }, +} + +func TestKubernetesCluster_VulnerabilityScanningDisabled(t *testing.T) { + rsrc := proto.Clone(k8sVulnScanningCluster).(*pb.Resource) + rsrc.GetKubernetesCluster().Security.VulnerabilityScanning = pb.KubernetesCluster_Security_VULN_SCAN_DISABLED + + want := []*pb.Observation{ + { + ScanUid: proto.String("unit-test-scan"), + ResourceRef: utils.GetResourceRef(rsrc), + Name: "KUBERNETES_VULNERABILITY_SCANNING_DISABLED", + Remediation: &pb.Remediation{ + Description: "Vulnerability scanning is disabled for this cluster", + Recommendation: "Enable the vulnerability scanning for this cluster by following the [Enable advanced vulnerability insights](https://cloud.google.com/kubernetes-engine/docs/how-to/security-posture-vulnerability-scanning#enable-advanced-insights) section of the GKE docs.\n\n```gcloud container clusters update \"my-cluster\" --project=\"my-project-id\" --location=\"us-central1-b\" --workload-vulnerability-scanning=standard```", + }, + ExpectedValue: structpb.NewStringValue("basic"), + ObservedValue: structpb.NewStringValue("VULN_SCAN_DISABLED"), + Severity: pb.Severity_SEVERITY_HIGH, + }, + } + TestRuleRun(t, []*pb.Resource{rsrc}, []model.Rule{NewKubernetesVulnerabilityScanningDisabledRule()}, want) +} + +func TestKubernetesCluster_VulnerabilityScanningUnknown(t *testing.T) { + rsrc := proto.Clone(k8sVulnScanningCluster).(*pb.Resource) + rsrc.GetKubernetesCluster().Security.VulnerabilityScanning = pb.KubernetesCluster_Security_VULN_SCAN_UNKNOWN + + want := []*pb.Observation{ + { + ScanUid: proto.String("unit-test-scan"), + ResourceRef: utils.GetResourceRef(rsrc), + Name: "KUBERNETES_VULNERABILITY_SCANNING_DISABLED", + Remediation: &pb.Remediation{ + Description: "Vulnerability scanning is disabled for this cluster", + Recommendation: "Enable the vulnerability scanning for this cluster by following the [Enable advanced vulnerability insights](https://cloud.google.com/kubernetes-engine/docs/how-to/security-posture-vulnerability-scanning#enable-advanced-insights) section of the GKE docs.\n\n```gcloud container clusters update \"my-cluster\" --project=\"my-project-id\" --location=\"us-central1-b\" --workload-vulnerability-scanning=standard```", + }, + ExpectedValue: structpb.NewStringValue("basic"), + ObservedValue: structpb.NewStringValue("VULN_SCAN_UNKNOWN"), + Severity: pb.Severity_SEVERITY_HIGH, + }, + } + TestRuleRun(t, []*pb.Resource{rsrc}, []model.Rule{NewKubernetesVulnerabilityScanningDisabledRule()}, want) +} + +func TestKubernetesCluster_VulnerabilityScanningBasic(t *testing.T) { + rsrc := proto.Clone(k8sVulnScanningCluster).(*pb.Resource) + rsrc.GetKubernetesCluster().Security.VulnerabilityScanning = pb.KubernetesCluster_Security_VULN_SCAN_BASIC + TestRuleRun(t, []*pb.Resource{rsrc}, []model.Rule{NewKubernetesVulnerabilityScanningDisabledRule()}, nil) +} diff --git a/src/engine/rules/lb_tls_cert_expiring_soon.go b/src/engine/rules/lb_tls_cert_expiring_soon.go new file mode 100644 index 0000000..8e6da27 --- /dev/null +++ b/src/engine/rules/lb_tls_cert_expiring_soon.go @@ -0,0 +1,87 @@ +package rules + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + + "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" + + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const LoadBalancerTLSCertExpiringSoonRuleName = "LOAD_BALANCER_TLS_CERTIFICATE_EXPIRING_SOON" + +const ( + hoursInADay = 24 + oneDay = hoursInADay * time.Hour + certExpiryWarning = 90 * oneDay // 90 days +) + +type LoadBalancerTLSCertExpiringSoonRule struct { + info model.RuleInfo +} + +func init() { + AddRule(NewLoadBalancerTLSCertExpiringSoonRule()) +} + +func NewLoadBalancerTLSCertExpiringSoonRule() model.Rule { + return &LoadBalancerTLSCertExpiringSoonRule{ + info: model.RuleInfo{ + Name: LoadBalancerTLSCertExpiringSoonRuleName, + AcceptedResourceTypes: []proto.Message{ + &pb.LoadBalancer{}, + }, + }, + } +} + +func (r *LoadBalancerTLSCertExpiringSoonRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { + lb := rsrc.GetLoadBalancer() + + if lb.Type == pb.LoadBalancer_EXTERNAL && lb.SslPolicy != nil { + for _, cert := range lb.Certificates { + daysUntilExpiry := int(time.Until(cert.ExpirationDate.AsTime()).Hours() / hoursInADay) + if cert.Type == pb.Certificate_IMPORTED { + if time.Until(cert.ExpirationDate.AsTime()) < certExpiryWarning { + obs = append(obs, &pb.Observation{ + Uid: uuid.NewString(), + Timestamp: timestamppb.Now(), + ResourceRef: utils.GetResourceRef(rsrc), + Name: r.Info().Name, + ExpectedValue: structpb.NewStringValue(fmt.Sprintf("More than %d days", int(certExpiryWarning.Hours()/hoursInADay))), + ObservedValue: structpb.NewStringValue(fmt.Sprintf("%d days", daysUntilExpiry)), + Remediation: &pb.Remediation{ + Description: fmt.Sprintf( + "The TLS certificate for load balancer [%q](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=%s) will expire in %d days. Certificates should be renewed before expiry to avoid service disruptions.", + getGcpReadableResourceName(rsrc.Name), + constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), + daysUntilExpiry, + ), + Recommendation: fmt.Sprintf( + "Renew the TLS certificate for load balancer [%q](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=%s). go/renew-certificate has information on how to proceed.", + getGcpReadableResourceName(rsrc.Name), + constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), + ), + }, + Severity: pb.Severity_SEVERITY_HIGH, + }) + } + } + } + } + + return obs, errs +} + +func (r *LoadBalancerTLSCertExpiringSoonRule) Info() *model.RuleInfo { + return &r.info +} diff --git a/src/engine/rules/lb_tls_cert_expiring_soon_test.go b/src/engine/rules/lb_tls_cert_expiring_soon_test.go new file mode 100644 index 0000000..5705e25 --- /dev/null +++ b/src/engine/rules/lb_tls_cert_expiring_soon_test.go @@ -0,0 +1,106 @@ +package rules + +import ( + "testing" + "time" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +func TestCheckDetectExpiringCertificate(t *testing.T) { + in10Days := timestamppb.New(time.Now().Add(10 * 24 * time.Hour)) + resources := []*pb.Resource{ + { + Name: "lb-cert-expiring-in-10-days", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_LoadBalancer{ + LoadBalancer: &pb.LoadBalancer{ + Type: pb.LoadBalancer_EXTERNAL, + Certificates: []*pb.Certificate{ + { + ExpirationDate: in10Days, + Type: pb.Certificate_IMPORTED, + }, + }, + SslPolicy: &pb.SslPolicy{ + MinTlsVersion: pb.SslPolicy_TLS_1_2, + Profile: pb.SslPolicy_MODERN, + Name: "Great SSL Policy", + }, + }, + }, + }, + { + Name: "lb-cert-expiring-in-10-days-managed-no-report", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_LoadBalancer{ + LoadBalancer: &pb.LoadBalancer{ + Type: pb.LoadBalancer_EXTERNAL, + Certificates: []*pb.Certificate{ + { + ExpirationDate: in10Days, + Type: pb.Certificate_MANAGED, + }, + }, + SslPolicy: &pb.SslPolicy{ + MinTlsVersion: pb.SslPolicy_TLS_1_2, + Profile: pb.SslPolicy_MODERN, + Name: "Great SSL Policy", + }, + }, + }, + }, + { + Name: "lb-cert-expiring-in-10-days-internal-no-report", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_LoadBalancer{ + LoadBalancer: &pb.LoadBalancer{ + Type: pb.LoadBalancer_INTERNAL, + Certificates: []*pb.Certificate{ + { + ExpirationDate: in10Days, + Type: pb.Certificate_IMPORTED, + }, + }, + SslPolicy: &pb.SslPolicy{ + MinTlsVersion: pb.SslPolicy_TLS_1_2, + Profile: pb.SslPolicy_MODERN, + Name: "Great SSL Policy", + }, + }, + }, + }, + } + + want := []*pb.Observation{ + { + Name: LoadBalancerTLSCertExpiringSoonRuleName, + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("uuid-0"), + ExternalId: proto.String("lb-cert-expiring-in-10-days"), + GroupName: testProjectName, + CloudPlatform: pb.CloudPlatform_GCP, + }, + Remediation: &pb.Remediation{ + Description: "The TLS certificate for load balancer [\"lb-cert-expiring-in-10-days\"](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=project-0) will expire in 9 days. Certificates should be renewed before expiry to avoid service disruptions.", + Recommendation: "Renew the TLS certificate for load balancer [\"lb-cert-expiring-in-10-days\"](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=project-0). go/renew-certificate has information on how to proceed.", + }, + ExpectedValue: structpb.NewStringValue("More than 90 days"), + ObservedValue: structpb.NewStringValue("9 days"), + Severity: pb.Severity_SEVERITY_HIGH, + }, + } + + TestRuleRun(t, resources, []model.Rule{NewLoadBalancerTLSCertExpiringSoonRule()}, want) +} diff --git a/src/engine/rules/lb_tls_min_version_too_old.go b/src/engine/rules/lb_tls_min_version_too_old.go index 0e9c2dd..161ed6b 100644 --- a/src/engine/rules/lb_tls_min_version_too_old.go +++ b/src/engine/rules/lb_tls_min_version_too_old.go @@ -5,20 +5,21 @@ import ( "fmt" "github.com/google/uuid" + "google.golang.org/protobuf/proto" - "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) -const LbMinTlsVersionTooOldRuleName = "LOAD_BALANCER_MIN_TLS_VERSION_TOO_OLD" +const lbMinTLSVersionTooOldRule = "LOAD_BALANCER_MIN_TLS_VERSION_TOO_OLD" var ( - minTlsVersion = pb.SslPolicy_TLS_1_2 + minTLSVersion = pb.SslPolicy_TLS_1_2 protoToVersionMap = map[pb.SslPolicy_MinTlsVersion]string{ pb.SslPolicy_TLS_1_0: "TLS 1.0", pb.SslPolicy_TLS_1_1: "TLS 1.1", @@ -27,37 +28,40 @@ var ( } ) -type LbMinTlsVersionTooOldRule struct { +type LbMinTLSVersionTooOldRule struct { info model.RuleInfo } func init() { - AddRule(NewLbMinTlsVersionTooOldRule()) + AddRule(NewLbMinTLSVersionTooOldRule()) } -func NewLbMinTlsVersionTooOldRule() model.Rule { - return &LbMinTlsVersionTooOldRule{ +func NewLbMinTLSVersionTooOldRule() model.Rule { + return &LbMinTLSVersionTooOldRule{ info: model.RuleInfo{ - Name: LbMinTlsVersionTooOldRuleName, - AcceptedResourceTypes: []string{ - common.ResourceLoadBalancer, + Name: lbMinTLSVersionTooOldRule, + AcceptedResourceTypes: []proto.Message{ + &pb.LoadBalancer{}, }, }, } } -func (r *LbMinTlsVersionTooOldRule) Check(ctx context.Context, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { +func (r *LbMinTLSVersionTooOldRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { lb := rsrc.GetLoadBalancer() - sslPolicy := lb.SslPolicy - if lb.Type == pb.LoadBalancer_EXTERNAL && sslPolicy.MinTlsVersion < minTlsVersion { + if sslPolicy == nil { + return nil, []error{fmt.Errorf("SSL policy is nil")} + } + + if lb.Type == pb.LoadBalancer_EXTERNAL && sslPolicy.MinTlsVersion < minTLSVersion { obs = append(obs, &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, - ExpectedValue: structpb.NewStringValue(protoToVersionMap[minTlsVersion]), + ExpectedValue: structpb.NewStringValue(protoToVersionMap[minTLSVersion]), ObservedValue: structpb.NewStringValue(protoToVersionMap[sslPolicy.MinTlsVersion]), Remediation: &pb.Remediation{ Description: fmt.Sprintf( @@ -69,15 +73,16 @@ func (r *LbMinTlsVersionTooOldRule) Check(ctx context.Context, rsrc *pb.Resource "Configure an SSL policy for the load balancer [%q](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=%s) that has a Minimum TLS version of %s and uses e.g. the \"MODERN\" or \"RESTRICTED\" configuration", getGcpReadableResourceName(rsrc.Name), constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), - protoToVersionMap[minTlsVersion], + protoToVersionMap[minTLSVersion], ), }, + Severity: pb.Severity_SEVERITY_HIGH, }) } return obs, errs } -func (r *LbMinTlsVersionTooOldRule) Info() *model.RuleInfo { +func (r *LbMinTLSVersionTooOldRule) Info() *model.RuleInfo { return &r.info } diff --git a/src/engine/rules/lb_tls_min_version_too_old_test.go b/src/engine/rules/lb_tls_min_version_too_old_test.go index a201168..9473c5e 100644 --- a/src/engine/rules/lb_tls_min_version_too_old_test.go +++ b/src/engine/rules/lb_tls_min_version_too_old_test.go @@ -3,14 +3,31 @@ package rules import ( "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/types/known/structpb" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) func TestCheckDetectTooOldMinTlsVersion(t *testing.T) { + lbGcpDefault := &pb.Resource{ + Name: "lb-gcp-default", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_LoadBalancer{ + LoadBalancer: &pb.LoadBalancer{ + Type: pb.LoadBalancer_EXTERNAL, + SslPolicy: &pb.SslPolicy{ + MinTlsVersion: pb.SslPolicy_TLS_1_0, + Profile: pb.SslPolicy_COMPATIBLE, + Name: "GCP Default", + }, + }, + }, + } + resources := []*pb.Resource{ { Name: testProjectName, @@ -37,22 +54,7 @@ func TestCheckDetectTooOldMinTlsVersion(t *testing.T) { }, }, }, - { - Name: "lb-gcp-default", - Parent: testProjectName, - ResourceGroupName: testProjectName, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_LoadBalancer{ - LoadBalancer: &pb.LoadBalancer{ - Type: pb.LoadBalancer_EXTERNAL, - SslPolicy: &pb.SslPolicy{ - MinTlsVersion: pb.SslPolicy_TLS_1_0, - Profile: pb.SslPolicy_COMPATIBLE, - Name: "GCP Default", - }, - }, - }, - }, + lbGcpDefault, { Name: "lb-gcp-default-internal", Parent: testProjectName, @@ -73,18 +75,17 @@ func TestCheckDetectTooOldMinTlsVersion(t *testing.T) { want := []*pb.Observation{ { - Name: LbMinTlsVersionTooOldRuleName, - Resource: &pb.Resource{ - Name: "lb-gcp-default", - }, + Name: lbMinTLSVersionTooOldRule, + ResourceRef: utils.GetResourceRef(lbGcpDefault), ExpectedValue: structpb.NewStringValue(protoToVersionMap[pb.SslPolicy_TLS_1_2]), ObservedValue: structpb.NewStringValue(protoToVersionMap[pb.SslPolicy_TLS_1_0]), + Remediation: &pb.Remediation{ + Description: "The load balancer [\"lb-gcp-default\"](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=project-0) allows connections over an outdated TLS protocol version. Outdated protocol versions use ciphers which can be attacked by dedicated threat actors", + Recommendation: "Configure an SSL policy for the load balancer [\"lb-gcp-default\"](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=project-0) that has a Minimum TLS version of TLS 1.2 and uses e.g. the \"MODERN\" or \"RESTRICTED\" configuration", + }, + Severity: pb.Severity_SEVERITY_HIGH, }, } - got := TestRuleRun(t, resources, []model.Rule{NewLbMinTlsVersionTooOldRule()}) - // Check that the observations are correct. - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewLbMinTLSVersionTooOldRule()}, want) } diff --git a/src/engine/rules/lb_user_managed_cert.go b/src/engine/rules/lb_user_managed_cert.go index 92ebf8e..673411c 100644 --- a/src/engine/rules/lb_user_managed_cert.go +++ b/src/engine/rules/lb_user_managed_cert.go @@ -5,11 +5,12 @@ import ( "fmt" "github.com/google/uuid" + "google.golang.org/protobuf/proto" - "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -29,14 +30,14 @@ func NewLbUserManagedCertRule() model.Rule { return &LbUserManagedCertRule{ info: model.RuleInfo{ Name: LbUserManagedCertRuleName, - AcceptedResourceTypes: []string{ - common.ResourceLoadBalancer, + AcceptedResourceTypes: []proto.Message{ + &pb.LoadBalancer{}, }, }, } } -func (r *LbUserManagedCertRule) Check(ctx context.Context, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { +func (r *LbUserManagedCertRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { lb := rsrc.GetLoadBalancer() for _, cert := range lb.Certificates { @@ -51,10 +52,10 @@ func (r *LbUserManagedCertRule) Check(ctx context.Context, rsrc *pb.Resource) (o ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, - ExpectedValue: structpb.NewNumberValue(float64(pb.Certificate_MANAGED)), - ObservedValue: structpb.NewNumberValue(float64(cert.Type)), + ExpectedValue: structpb.NewStringValue(pb.Certificate_MANAGED.String()), + ObservedValue: structpb.NewStringValue(cert.Type.String()), Remediation: &pb.Remediation{ Description: fmt.Sprintf( "Load balancer [%q](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=%s) has user-managed certificate issued by %q for the domain %q", @@ -69,6 +70,7 @@ func (r *LbUserManagedCertRule) Check(ctx context.Context, rsrc *pb.Resource) (o constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), ), }, + Severity: pb.Severity_SEVERITY_INFO, } obs = append(obs, ob) } diff --git a/src/engine/rules/lb_user_managed_cert_test.go b/src/engine/rules/lb_user_managed_cert_test.go index 43f75da..7ee6c3f 100644 --- a/src/engine/rules/lb_user_managed_cert_test.go +++ b/src/engine/rules/lb_user_managed_cert_test.go @@ -1,52 +1,37 @@ package rules import ( - "context" - "strings" + "errors" + "fmt" "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "google.golang.org/protobuf/testing/protocmp" - - "github.com/nianticlabs/modron/src/engine" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" - "github.com/nianticlabs/modron/src/storage/memstorage" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) func TestCheckDetectsUserManagedCertificate(t *testing.T) { - resources := []*pb.Resource{ - { - Name: testProjectName, - Parent: "", - ResourceGroupName: testProjectName, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ResourceGroup{ - ResourceGroup: &pb.ResourceGroup{}, - }, - }, - { - Name: "lb-imported-cert-should-be-detected", - Parent: testProjectName, - ResourceGroupName: testProjectName, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_LoadBalancer{ - LoadBalancer: &pb.LoadBalancer{ - Type: pb.LoadBalancer_EXTERNAL, - Certificates: []*pb.Certificate{ - { - Type: pb.Certificate_IMPORTED, - DomainName: "domain-0.github.com/nianticlabs/modron", - SubjectAlternativeNames: []string{}, - CreationDate: ×tamppb.Timestamp{}, - ExpirationDate: ×tamppb.Timestamp{}, - Issuer: "", - SignatureAlgorithm: "sha1WithRSAEncryption", - PemCertificateChain: ` + lbImportedCert := &pb.Resource{ + Name: "lb-imported-cert-should-be-detected", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_LoadBalancer{ + LoadBalancer: &pb.LoadBalancer{ + Type: pb.LoadBalancer_EXTERNAL, + Certificates: []*pb.Certificate{ + { + Type: pb.Certificate_IMPORTED, + DomainName: "domain-0.modron.example.com", + SubjectAlternativeNames: []string{}, + CreationDate: ×tamppb.Timestamp{}, + ExpirationDate: ×tamppb.Timestamp{}, + Issuer: "", + SignatureAlgorithm: "sha1WithRSAEncryption", + PemCertificateChain: ` -----BEGIN CERTIFICATE----- MIIFTTCCAzUCCQD9AMCeW12GEDANBgkqhkiG9w0BAQUFADBVMRAwDgYDVQQLDAdV bmtub3duMRAwDgYDVQQKDAdVbmtub3duMRAwDgYDVQQHDAdVbmtub3duMRAwDgYD @@ -109,11 +94,22 @@ func TestCheckDetectsUserManagedCertificate(t *testing.T) { muOKyutYtJqW5tqke8N7Yy9oDUlqtt6gnFE= -----END CERTIFICATE----- `, - }, }, }, }, }, + } + resources := []*pb.Resource{ + { + Name: testProjectName, + Parent: "", + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{}, + }, + }, + lbImportedCert, { Name: "lb-managed-cert-should-not-be-detected", Parent: testProjectName, @@ -125,7 +121,7 @@ func TestCheckDetectsUserManagedCertificate(t *testing.T) { Certificates: []*pb.Certificate{ { Type: pb.Certificate_MANAGED, - DomainName: "domain-1.github.com/nianticlabs/modron", + DomainName: "domain-1.modron.example.com", SubjectAlternativeNames: []string{}, CreationDate: ×tamppb.Timestamp{}, ExpirationDate: ×tamppb.Timestamp{}, @@ -201,29 +197,30 @@ func TestCheckDetectsUserManagedCertificate(t *testing.T) { }, } - want := []*structpb.Value{ - structpb.NewNumberValue(float64(pb.Certificate_MANAGED)), - } - - obs := TestRuleRun(t, resources, []model.Rule{NewLbUserManagedCertRule()}) - - got := []*structpb.Value{} - for _, ob := range obs { - got = append(got, ob.ExpectedValue) + want := []*pb.Observation{ + { + Name: LbUserManagedCertRuleName, + ObservedValue: structpb.NewStringValue("IMPORTED"), + ExpectedValue: structpb.NewStringValue("MANAGED"), + ResourceRef: utils.GetResourceRef(lbImportedCert), + Remediation: &pb.Remediation{ + Description: "Load balancer [\"lb-imported-cert-should-be-detected\"](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=project-0) has user-managed certificate issued by \"\" for the domain \"domain-0.modron.example.com\"", + Recommendation: "Configure a platform-managed certificate for load balancer [\"lb-imported-cert-should-be-detected\"](https://console.cloud.google.com/net-services/loadbalancing/list/loadBalancers?project=project-0) to ensure lower management overhead, better security and prevent outages caused by certificate expiry", + }, + Severity: pb.Severity_SEVERITY_INFO, + }, } - // Check that the observations are correct. - if diff := cmp.Diff(want, got, protocmp.Transform(), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewLbUserManagedCertRule()}, want) } func TestCheckDetectsUnknownCertificate(t *testing.T) { resources := []*pb.Resource{ { - Name: "projects/project-1", - Parent: "folders/234", - IamPolicy: &pb.IamPolicy{}, + Name: "projects/project-1", + Parent: "folders/234", + IamPolicy: &pb.IamPolicy{}, + ResourceGroupName: "projects/project-1", Type: &pb.Resource_ResourceGroup{ ResourceGroup: &pb.ResourceGroup{}, }, @@ -239,7 +236,7 @@ func TestCheckDetectsUnknownCertificate(t *testing.T) { Certificates: []*pb.Certificate{ { Type: pb.Certificate_UNKNOWN, - DomainName: "domain-2.github.com/nianticlabs/modron", + DomainName: "domain-2.modron.example.com", SubjectAlternativeNames: []string{}, CreationDate: ×tamppb.Timestamp{}, ExpirationDate: ×tamppb.Timestamp{}, @@ -315,21 +312,9 @@ func TestCheckDetectsUnknownCertificate(t *testing.T) { }, } - storage := memstorage.New() - storageCtx := context.Background() - if _, err := storage.BatchCreateResources(storageCtx, resources); err != nil { - t.Errorf("AddResources unexpected error: %v", err) - } - - re := engine.New(storage, []model.Rule{NewLbUserManagedCertRule()}, []string{}) - reCtx := context.Background() - _, err := re.CheckRules(reCtx, "", []string{"projects/project-1"}) - if len(err) != 1 { - t.Fatalf("len(err) got %d, want %d", len(err), 1) - } - for _, e := range err { - if !strings.Contains(e.Error(), "unknown type") { - t.Errorf("CheckRules unexpected error string, got %q, want %q", e, "*unknown type") - } - } + TestRuleShouldFail(t, resources, []model.Rule{NewLbUserManagedCertRule()}, []error{ + fmt.Errorf("execution of rule LOAD_BALANCER_USER_MANAGED_CERTIFICATE failed: %w", + errors.New("certificate issued by \"\" for the domain \"domain-2.modron.example.com\" is of unknown type"), + ), + }) } diff --git a/src/engine/rules/master_authorized_neworks_not_set.go b/src/engine/rules/master_authorized_neworks_not_set.go index 002e3ce..8f1021e 100644 --- a/src/engine/rules/master_authorized_neworks_not_set.go +++ b/src/engine/rules/master_authorized_neworks_not_set.go @@ -5,11 +5,12 @@ import ( "fmt" "github.com/google/uuid" + "google.golang.org/protobuf/proto" - "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -29,21 +30,21 @@ func NewMasterAuthorizedNetworksNotSetRule() model.Rule { return &MasterAuthorizedNetworksNotSetRule{ info: model.RuleInfo{ Name: MasterAuthorizedNetworksNotSet, - AcceptedResourceTypes: []string{ - common.ResourceKubernetesCluster, + AcceptedResourceTypes: []proto.Message{ + &pb.KubernetesCluster{}, }, }, } } -func (r *MasterAuthorizedNetworksNotSetRule) Check(ctx context.Context, rsrc *pb.Resource) ([]*pb.Observation, []error) { +func (r *MasterAuthorizedNetworksNotSetRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) ([]*pb.Observation, []error) { k8s := rsrc.GetKubernetesCluster() - obs := []*pb.Observation{} + var obs []*pb.Observation if len(k8s.MasterAuthorizedNetworks) < 1 { ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue("not empty"), ObservedValue: structpb.NewStringValue("empty"), @@ -59,6 +60,7 @@ func (r *MasterAuthorizedNetworksNotSetRule) Check(ctx context.Context, rsrc *pb constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), ), }, + Severity: pb.Severity_SEVERITY_HIGH, } obs = append(obs, ob) } diff --git a/src/engine/rules/master_authorized_neworks_not_set_test.go b/src/engine/rules/master_authorized_neworks_not_set_test.go index d43a16a..44f6638 100644 --- a/src/engine/rules/master_authorized_neworks_not_set_test.go +++ b/src/engine/rules/master_authorized_neworks_not_set_test.go @@ -3,15 +3,26 @@ package rules import ( "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/types/known/structpb" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) func TestMasterAuthorizedNetworksNotSet(t *testing.T) { notSetResourceName := "master-authorized-networks-not-set" + notSetResource := &pb.Resource{ + Name: notSetResourceName, + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_KubernetesCluster{ + KubernetesCluster: &pb.KubernetesCluster{ + MasterAuthorizedNetworks: []string{}, + }, + }, + } resources := []*pb.Resource{ { Name: testProjectName, @@ -33,34 +44,22 @@ func TestMasterAuthorizedNetworksNotSet(t *testing.T) { }, }, }, - { - Name: notSetResourceName, - Parent: testProjectName, - ResourceGroupName: testProjectName, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_KubernetesCluster{ - KubernetesCluster: &pb.KubernetesCluster{ - MasterAuthorizedNetworks: []string{}, - }, - }, - }, + notSetResource, } want := []*pb.Observation{ { - Name: MasterAuthorizedNetworksNotSet, - Resource: &pb.Resource{ - Name: notSetResourceName, - }, + Name: MasterAuthorizedNetworksNotSet, + ResourceRef: utils.GetResourceRef(notSetResource), ExpectedValue: structpb.NewStringValue("not empty"), ObservedValue: structpb.NewStringValue("empty"), + Remediation: &pb.Remediation{ + Description: "Cluster [\"master-authorized-networks-not-set\"](https://console.cloud.google.com/kubernetes/list/overview?project=project-0) does not have a [Master Authorized Network](https://cloud.google.com/kubernetes-engine/docs/how-to/authorized-networks#create_cluster) set. Without this setting, the cluster control plane is accessible to anyone", + Recommendation: "Set a [Master Authorized Network](https://cloud.google.com/kubernetes-engine/docs/how-to/authorized-networks#create_cluster) network range for cluster [\"master-authorized-networks-not-set\"](https://console.cloud.google.com/kubernetes/list/overview?project=project-0)", + }, + Severity: pb.Severity_SEVERITY_HIGH, }, } - got := TestRuleRun(t, resources, []model.Rule{NewMasterAuthorizedNetworksNotSetRule()}) - - // Check that the observations are correct. - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewMasterAuthorizedNetworksNotSetRule()}, want) } diff --git a/src/engine/rules/outdated_kubernetes_version.go b/src/engine/rules/outdated_kubernetes_version.go index b1f1c1b..68e43f8 100644 --- a/src/engine/rules/outdated_kubernetes_version.go +++ b/src/engine/rules/outdated_kubernetes_version.go @@ -7,11 +7,12 @@ import ( "strings" "github.com/google/uuid" + "google.golang.org/protobuf/proto" - "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -20,7 +21,7 @@ import ( const ( OutDatedKubernetesVersion = "OUTDATED_KUBERNETES_VERSION" // https://cloud.google.com/kubernetes-engine/docs/release-schedule - currentK8sVersion = 1.23 + currentK8sVersion = 1.27 ) type OutDatedKubernetesVersionRule struct { @@ -35,17 +36,17 @@ func NewOutDatedKubernetesVersionRule() model.Rule { return &OutDatedKubernetesVersionRule{ info: model.RuleInfo{ Name: OutDatedKubernetesVersion, - AcceptedResourceTypes: []string{ - common.ResourceKubernetesCluster, + AcceptedResourceTypes: []proto.Message{ + &pb.KubernetesCluster{}, }, }, } } -func (r *OutDatedKubernetesVersionRule) Check(ctx context.Context, rsrc *pb.Resource) ([]*pb.Observation, []error) { +func (r *OutDatedKubernetesVersionRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) ([]*pb.Observation, []error) { k8s := rsrc.GetKubernetesCluster() - obs := []*pb.Observation{} - errs := []error{} + var obs []*pb.Observation + var errs []error if k8s == nil { errs = append(errs, fmt.Errorf("no kubernetes cluster resource provided")) return obs, errs @@ -71,7 +72,7 @@ func (r *OutDatedKubernetesVersionRule) Check(ctx context.Context, rsrc *pb.Reso ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue(fmt.Sprintf("version > %.2f", currentK8sVersion)), ObservedValue: structpb.NewStringValue(k8s.MasterVersion), @@ -88,6 +89,7 @@ func (r *OutDatedKubernetesVersionRule) Check(ctx context.Context, rsrc *pb.Reso currentK8sVersion, ), }, + Severity: pb.Severity_SEVERITY_HIGH, } obs = append(obs, ob) } @@ -96,7 +98,7 @@ func (r *OutDatedKubernetesVersionRule) Check(ctx context.Context, rsrc *pb.Reso ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue(fmt.Sprintf("version > %.2f", currentK8sVersion)), ObservedValue: structpb.NewStringValue(k8s.NodesVersion), @@ -113,6 +115,7 @@ func (r *OutDatedKubernetesVersionRule) Check(ctx context.Context, rsrc *pb.Reso currentK8sVersion, ), }, + Severity: pb.Severity_SEVERITY_HIGH, } obs = append(obs, ob) } diff --git a/src/engine/rules/outdated_kubernetes_version_test.go b/src/engine/rules/outdated_kubernetes_version_test.go index 9027b9c..8734325 100644 --- a/src/engine/rules/outdated_kubernetes_version_test.go +++ b/src/engine/rules/outdated_kubernetes_version_test.go @@ -4,14 +4,27 @@ import ( "fmt" "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/types/known/structpb" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) func TestOutdatedKubernetesVersionDetection(t *testing.T) { + clusterWithOutdatedNodesVersion := &pb.Resource{ + Name: "cluster-with-outdated-nodes-version", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_KubernetesCluster{ + KubernetesCluster: &pb.KubernetesCluster{ + PrivateCluster: true, + MasterVersion: "1.27.10-gke.600", + NodesVersion: "1.15.10-gke.600", + }, + }, + } resources := []*pb.Resource{ { Name: testProjectName, @@ -35,36 +48,22 @@ func TestOutdatedKubernetesVersionDetection(t *testing.T) { }, }, }, - { - Name: "cluster-with-outdated-nodes-version", - Parent: testProjectName, - ResourceGroupName: testProjectName, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_KubernetesCluster{ - KubernetesCluster: &pb.KubernetesCluster{ - PrivateCluster: true, - MasterVersion: "1.27.10-gke.600", - NodesVersion: "1.15.10-gke.600", - }, - }, - }, + clusterWithOutdatedNodesVersion, } want := []*pb.Observation{ { - Name: OutDatedKubernetesVersion, - Resource: &pb.Resource{ - Name: "cluster-with-outdated-nodes-version", - }, + Name: OutDatedKubernetesVersion, + ResourceRef: utils.GetResourceRef(clusterWithOutdatedNodesVersion), ExpectedValue: structpb.NewStringValue(fmt.Sprintf("version > %.2f", currentK8sVersion)), ObservedValue: structpb.NewStringValue("1.15.10-gke.600"), + Remediation: &pb.Remediation{ + Description: "Cluster [\"cluster-with-outdated-nodes-version\"](https://console.cloud.google.com/kubernetes/list/overview?project=project-0) uses an outdated Kubernetes version", + Recommendation: "Update the Kubernetes version on cluster [\"cluster-with-outdated-nodes-version\"](https://console.cloud.google.com/kubernetes/list/overview?project=project-0) to at least 1.27. For more details on this process, see [this article](https://cloud.google.com/kubernetes-engine/docs/how-to/upgrading-a-cluster)", + }, + Severity: pb.Severity_SEVERITY_HIGH, }, } - got := TestRuleRun(t, resources, []model.Rule{NewOutDatedKubernetesVersionRule()}) - - // Check that the observations are correct. - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewOutDatedKubernetesVersionRule()}, want) } diff --git a/src/engine/rules/private_google_access_disabled.go b/src/engine/rules/private_google_access_disabled.go index 21a684d..870fa69 100644 --- a/src/engine/rules/private_google_access_disabled.go +++ b/src/engine/rules/private_google_access_disabled.go @@ -5,12 +5,14 @@ import ( "fmt" "github.com/google/uuid" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) const PrivateGoogleAccessDisabled = "PRIVATE_GOOGLE_ACCESS_DISABLED" @@ -27,22 +29,22 @@ func NewPrivateGoogleAccessDisabledRule() model.Rule { return &PrivateGoogleAccessDisabledRule{ info: model.RuleInfo{ Name: PrivateGoogleAccessDisabled, - AcceptedResourceTypes: []string{ - common.ResourceNetwork, + AcceptedResourceTypes: []proto.Message{ + &pb.Network{}, }, }, } } -func (r *PrivateGoogleAccessDisabledRule) Check(ctx context.Context, rsrc *pb.Resource) ([]*pb.Observation, []error) { +func (r *PrivateGoogleAccessDisabledRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) ([]*pb.Observation, []error) { net := rsrc.GetNetwork() - obs := []*pb.Observation{} + var obs []*pb.Observation if !net.GcpPrivateGoogleAccessV4 { ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue("enabled"), ObservedValue: structpb.NewStringValue("disabled"), @@ -60,6 +62,7 @@ func (r *PrivateGoogleAccessDisabledRule) Check(ctx context.Context, rsrc *pb.Re constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), ), }, + Severity: pb.Severity_SEVERITY_LOW, } obs = append(obs, ob) diff --git a/src/engine/rules/private_google_access_disabled_test.go b/src/engine/rules/private_google_access_disabled_test.go index c56b1b0..4aa9ad5 100644 --- a/src/engine/rules/private_google_access_disabled_test.go +++ b/src/engine/rules/private_google_access_disabled_test.go @@ -3,16 +3,26 @@ package rules import ( "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" ) func TestCheckDetectsPrivateGoogleAccessDisabled(t *testing.T) { + networkNoPrivateAccess := &pb.Resource{ + Name: "network-no-private-access", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_Network{ + Network: &pb.Network{ + Ips: []string{"8.8.4.4"}, + GcpPrivateGoogleAccessV4: false, + }, + }, + } resources := []*pb.Resource{ { Name: testProjectName, @@ -23,18 +33,7 @@ func TestCheckDetectsPrivateGoogleAccessDisabled(t *testing.T) { ResourceGroup: &pb.ResourceGroup{}, }, }, - { - Name: "network-no-private-access", - Parent: testProjectName, - ResourceGroupName: testProjectName, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_Network{ - Network: &pb.Network{ - Ips: []string{"8.8.4.4"}, - GcpPrivateGoogleAccessV4: false, - }, - }, - }, + networkNoPrivateAccess, { Name: "network-private-access", Parent: testProjectName, @@ -51,19 +50,17 @@ func TestCheckDetectsPrivateGoogleAccessDisabled(t *testing.T) { want := []*pb.Observation{ { - Name: PrivateGoogleAccessDisabled, - Resource: &pb.Resource{ - Name: "network-no-private-access", - }, + Name: PrivateGoogleAccessDisabled, + ResourceRef: utils.GetResourceRef(networkNoPrivateAccess), ObservedValue: structpb.NewStringValue("disabled"), ExpectedValue: structpb.NewStringValue("enabled"), + Remediation: &pb.Remediation{ + Description: "Network [\"network-no-private-access\"](https://console.cloud.google.com/networking/networks/details/network-no-private-access?project=project-0) has [Private Google Access](https://cloud.google.com/vpc/docs/configure-private-google-access) disabled. Private Google Access allows the workloads to access Google APIs via a private network which is safer than going over the public Internet", + Recommendation: "Enable [Private Google Access](https://cloud.google.com/vpc/docs/configure-private-google-access) for Network [\"network-no-private-access\"](https://console.cloud.google.com/networking/networks/details/network-no-private-access?project=project-0)", + }, + Severity: pb.Severity_SEVERITY_LOW, }, } - got := TestRuleRun(t, resources, []model.Rule{NewPrivateGoogleAccessDisabledRule()}) - - // Check that the observations are correct. - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewPrivateGoogleAccessDisabledRule()}, want) } diff --git a/src/engine/rules/registry.go b/src/engine/rules/registry.go index d8050d5..afc0f62 100644 --- a/src/engine/rules/registry.go +++ b/src/engine/rules/registry.go @@ -10,6 +10,11 @@ import ( var rules sync.Map func AddRule(r model.Rule) { + ruleName := r.Info().Name + _, ok := rules.Load(ruleName) + if ok { + panic(fmt.Sprintf("rule %q already exists", ruleName)) + } rules.Store(r.Info().Name, r) } @@ -21,9 +26,8 @@ func GetRule(name string) (model.Rule, error) { } func GetRules() []model.Rule { - rulesSnapshot := []model.Rule{} - - rules.Range(func(name, rule any) bool { + var rulesSnapshot []model.Rule + rules.Range(func(_, rule any) bool { rulesSnapshot = append(rulesSnapshot, rule.(model.Rule)) return true }) diff --git a/src/engine/rules/registry_test.go b/src/engine/rules/registry_test.go index a1df937..414eb50 100644 --- a/src/engine/rules/registry_test.go +++ b/src/engine/rules/registry_test.go @@ -4,8 +4,10 @@ import ( "context" "testing" + "google.golang.org/protobuf/proto" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" ) type TestRule struct { @@ -16,12 +18,12 @@ func NewTestRule(name string) *TestRule { return &TestRule{ info: model.RuleInfo{ Name: name, - AcceptedResourceTypes: []string{}, + AcceptedResourceTypes: []proto.Message{}, }, } } -func (r *TestRule) Check(ctx context.Context, rsrc *pb.Resource) ([]*pb.Observation, []error) { +func (r *TestRule) Check(context.Context, model.Engine, *pb.Resource) ([]*pb.Observation, []error) { return []*pb.Observation{{}}, nil } diff --git a/src/engine/rules/svc_account_too_high_privileges.go b/src/engine/rules/svc_account_too_high_privileges.go index 9252992..25ad229 100644 --- a/src/engine/rules/svc_account_too_high_privileges.go +++ b/src/engine/rules/svc_account_too_high_privileges.go @@ -6,12 +6,12 @@ import ( "github.com/google/uuid" "golang.org/x/exp/slices" + "google.golang.org/protobuf/proto" - "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/constants" - "github.com/nianticlabs/modron/src/engine" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -46,21 +46,21 @@ func NewTooHighPrivilegesRule() model.Rule { return &TooHighPrivilegesRule{ info: model.RuleInfo{ Name: TooHighPrivilegesRuleName, - AcceptedResourceTypes: []string{ - common.ResourceServiceAccount, + AcceptedResourceTypes: []proto.Message{ + &pb.ServiceAccount{}, }, }, } } -func (r *TooHighPrivilegesRule) Check(ctx context.Context, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { - rsrcGroup, err := engine.GetResource(ctx, rsrc.Parent) +func (r *TooHighPrivilegesRule) Check(ctx context.Context, e model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { + rsrcGroup, err := e.GetResource(ctx, rsrc.Parent) if err != nil { - errs = append(errs, fmt.Errorf("error retrieving resource group of resource %q: %v", rsrc.Name, err)) + errs = append(errs, fmt.Errorf("error retrieving resource group of resource %q: %w", rsrc.Name, err)) return } policy := rsrcGroup.IamPolicy - roles := []string{} + var roles []string if policy != nil { for _, perm := range policy.Permissions { @@ -78,7 +78,7 @@ func (r *TooHighPrivilegesRule) Check(ctx context.Context, rsrc *pb.Resource) (o ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue(""), ObservedValue: structpb.NewStringValue(role), @@ -99,6 +99,7 @@ func (r *TooHighPrivilegesRule) Check(ctx context.Context, rsrc *pb.Resource) (o dangerousRoles, ), }, + Severity: pb.Severity_SEVERITY_MEDIUM, } obs = append(obs, ob) } diff --git a/src/engine/rules/svc_account_too_high_privileges_test.go b/src/engine/rules/svc_account_too_high_privileges_test.go index a689812..e39ca86 100644 --- a/src/engine/rules/svc_account_too_high_privileges_test.go +++ b/src/engine/rules/svc_account_too_high_privileges_test.go @@ -3,48 +3,104 @@ package rules import ( "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "google.golang.org/protobuf/types/known/structpb" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) const ( - collectId = "collectId-1" + collectID = "collectID-1" ) func TestCheckDetectsHighPrivilege(t *testing.T) { // Because of the new memoization we need a specific project name for this test. - testProjectName := "projects/test-project" + uuid.NewString() - testProjectName1 := "projects/test-project1" + uuid.NewString() + testProjectName := "projects/check-detects-high-privilege-0" + testProjectName1 := "projects/check-detects-high-privilege-1" + + account0 := &pb.Resource{ + Uid: uuid.NewString(), + Name: "account-0", + Parent: testProjectName, + ResourceGroupName: testProjectName, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{}, + }, + }, + } + + account1 := &pb.Resource{ + Uid: uuid.NewString(), + Name: "account-1", + Parent: testProjectName, + ResourceGroupName: testProjectName, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{}, + }, + }, + } + + account2 := &pb.Resource{ + Uid: uuid.NewString(), + Name: "account-2", + Parent: testProjectName1, + ResourceGroupName: testProjectName1, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{}, + }, + }, + } + + account3 := &pb.Resource{ + Uid: uuid.NewString(), + Name: "account-3", + Parent: testProjectName1, + ResourceGroupName: testProjectName1, + CollectionUid: collectID, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ServiceAccount{ + ServiceAccount: &pb.ServiceAccount{ + ExportedCredentials: []*pb.ExportedCredentials{}, + }, + }, + } + resources := []*pb.Resource{ { Name: testProjectName, Parent: "folders/123", ResourceGroupName: testProjectName, - CollectionUid: collectId, + CollectionUid: collectID, IamPolicy: &pb.IamPolicy{ Resource: nil, Permissions: []*pb.Permission{ { Role: "iam.serviceAccountAdmin", Principals: []string{ - "account-0", + "serviceAccount:account-0", }, }, { Role: "dataflow.admin", Principals: []string{ - "account-1", + "serviceAccount:account-1", }, }, { Role: "viewer", Principals: []string{ - "account-1", + "serviceAccount:account-1", }, }, }, @@ -57,15 +113,15 @@ func TestCheckDetectsHighPrivilege(t *testing.T) { Name: testProjectName1, Parent: "folders/234", ResourceGroupName: testProjectName1, - CollectionUid: collectId, + CollectionUid: collectID, IamPolicy: &pb.IamPolicy{ Resource: nil, Permissions: []*pb.Permission{ { Role: "iam.serviceAccountUser", Principals: []string{ - "account-2", - "account-3", + "serviceAccount:account-2", + "serviceAccount:account-3", }, }, }, @@ -74,98 +130,58 @@ func TestCheckDetectsHighPrivilege(t *testing.T) { ResourceGroup: &pb.ResourceGroup{}, }, }, - { - Uid: uuid.NewString(), - Name: "account-0", - Parent: testProjectName, - ResourceGroupName: testProjectName, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ServiceAccount{ - ServiceAccount: &pb.ServiceAccount{ - ExportedCredentials: []*pb.ExportedCredentials{}, - }, - }, - }, - { - Uid: uuid.NewString(), - Name: "account-1", - Parent: testProjectName, - ResourceGroupName: testProjectName, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ServiceAccount{ - ServiceAccount: &pb.ServiceAccount{ - ExportedCredentials: []*pb.ExportedCredentials{}, - }, - }, - }, - { - Uid: uuid.NewString(), - Name: "account-2", - Parent: testProjectName1, - ResourceGroupName: testProjectName1, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ServiceAccount{ - ServiceAccount: &pb.ServiceAccount{ - ExportedCredentials: []*pb.ExportedCredentials{}, - }, - }, - }, - { - Uid: uuid.NewString(), - Name: "account-3", - Parent: testProjectName1, - ResourceGroupName: testProjectName1, - CollectionUid: collectId, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_ServiceAccount{ - ServiceAccount: &pb.ServiceAccount{ - ExportedCredentials: []*pb.ExportedCredentials{}, - }, - }, - }, + account0, + account1, + account2, + account3, } want := []*pb.Observation{ { - Name: TooHighPrivilegesRuleName, - Resource: &pb.Resource{ - Name: "account-0", - }, + Name: TooHighPrivilegesRuleName, + ResourceRef: utils.GetResourceRef(account0), ExpectedValue: structpb.NewStringValue(""), ObservedValue: structpb.NewStringValue("iam.serviceAccountAdmin"), + Remediation: &pb.Remediation{ + Description: "Service account [\"account-0\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=check-detects-high-privilege-0) has over-broad role \"iam.serviceAccountAdmin\"", + Recommendation: "Replace the role \"iam.serviceAccountAdmin\" for service account [\"account-0\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=check-detects-high-privilege-0) with a predefined or custom role that grants it the **smallest set of permissions** needed to operate. This role **cannot** be any of the following: `[editor owner composer.admin dataproc.admin dataproc.editor dataflow.admin dataflow.developer iam.serviceAccountAdmin iam.serviceAccountUser iam.serviceAccountTokenCreator]` *Hint: The Security insights column can help you reduce the amount of permissions*", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, }, { - Name: TooHighPrivilegesRuleName, - Resource: &pb.Resource{ - Name: "account-1", - }, + Name: TooHighPrivilegesRuleName, + ResourceRef: utils.GetResourceRef(account1), ExpectedValue: structpb.NewStringValue(""), ObservedValue: structpb.NewStringValue("dataflow.admin"), + Remediation: &pb.Remediation{ + Description: "Service account [\"account-1\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=check-detects-high-privilege-0) has over-broad role \"dataflow.admin\"", + Recommendation: "Replace the role \"dataflow.admin\" for service account [\"account-1\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=check-detects-high-privilege-0) with a predefined or custom role that grants it the **smallest set of permissions** needed to operate. This role **cannot** be any of the following: `[editor owner composer.admin dataproc.admin dataproc.editor dataflow.admin dataflow.developer iam.serviceAccountAdmin iam.serviceAccountUser iam.serviceAccountTokenCreator]` *Hint: The Security insights column can help you reduce the amount of permissions*", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, }, { - Name: TooHighPrivilegesRuleName, - Resource: &pb.Resource{ - Name: "account-2", - }, + Name: TooHighPrivilegesRuleName, + ResourceRef: utils.GetResourceRef(account2), ExpectedValue: structpb.NewStringValue(""), ObservedValue: structpb.NewStringValue("iam.serviceAccountUser"), + Remediation: &pb.Remediation{ + Description: "Service account [\"account-2\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=check-detects-high-privilege-1) has over-broad role \"iam.serviceAccountUser\"", + Recommendation: "Replace the role \"iam.serviceAccountUser\" for service account [\"account-2\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=check-detects-high-privilege-1) with a predefined or custom role that grants it the **smallest set of permissions** needed to operate. This role **cannot** be any of the following: `[editor owner composer.admin dataproc.admin dataproc.editor dataflow.admin dataflow.developer iam.serviceAccountAdmin iam.serviceAccountUser iam.serviceAccountTokenCreator]` *Hint: The Security insights column can help you reduce the amount of permissions*", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, }, { - Name: TooHighPrivilegesRuleName, - Resource: &pb.Resource{ - Name: "account-3", - }, + Name: TooHighPrivilegesRuleName, + ResourceRef: utils.GetResourceRef(account3), ExpectedValue: structpb.NewStringValue(""), ObservedValue: structpb.NewStringValue("iam.serviceAccountUser"), + Remediation: &pb.Remediation{ + Description: "Service account [\"account-3\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=check-detects-high-privilege-1) has over-broad role \"iam.serviceAccountUser\"", + Recommendation: "Replace the role \"iam.serviceAccountUser\" for service account [\"account-3\"](https://console.cloud.google.com/iam-admin/serviceaccounts?project=check-detects-high-privilege-1) with a predefined or custom role that grants it the **smallest set of permissions** needed to operate. This role **cannot** be any of the following: `[editor owner composer.admin dataproc.admin dataproc.editor dataflow.admin dataflow.developer iam.serviceAccountAdmin iam.serviceAccountUser iam.serviceAccountTokenCreator]` *Hint: The Security insights column can help you reduce the amount of permissions*", + }, + Severity: pb.Severity_SEVERITY_MEDIUM, }, } - got := TestRuleRun(t, resources, []model.Rule{NewTooHighPrivilegesRule()}) - - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewTooHighPrivilegesRule()}, want) } diff --git a/src/engine/rules/testing.go b/src/engine/rules/testing.go index 358e674..32b2dc1 100644 --- a/src/engine/rules/testing.go +++ b/src/engine/rules/testing.go @@ -2,87 +2,174 @@ package rules import ( "context" - "fmt" + "encoding/json" + "errors" "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" - "google.golang.org/protobuf/types/known/structpb" "github.com/nianticlabs/modron/src/engine" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" "github.com/nianticlabs/modron/src/storage/memstorage" + "github.com/nianticlabs/modron/src/utils" ) const testProjectName = "projects/project-0" var observationsSorter = func(lhs, rhs *pb.Observation) bool { - return lhs.Resource.Name < rhs.Resource.Name + if lhs.ResourceRef == nil || rhs.ResourceRef == nil { + return lhs.Remediation.Description < rhs.Remediation.Description + } + + if lhs.ResourceRef.ExternalId == nil || rhs.ResourceRef.ExternalId == nil { + return lhs.Remediation.Description < rhs.Remediation.Description + } + + if *lhs.ResourceRef.ExternalId < *rhs.ResourceRef.ExternalId { + return true + } else if *lhs.ResourceRef.ExternalId > *rhs.ResourceRef.ExternalId { + return false + } + return lhs.Remediation.Description < rhs.Remediation.Description +} + +func mustMarshal[T any](v T) json.RawMessage { + b, err := json.Marshal(v) + if err != nil { + panic(err) + } + return b } -func TestRuleRun(t *testing.T, resources []*pb.Resource, rules []model.Rule) []*pb.Observation { +func testRuleRunHelper(t *testing.T, resources []*pb.Resource, rules []model.Rule) ([]*pb.Observation, []error) { t.Helper() ctx := context.Background() - storage := memstorage.New() + + allGroups := utils.GroupsFromResources(resources) + + // Fake a collection event + collectID := uuid.NewString() + for i := range resources { + resources[i].CollectionUid = collectID + } + // Fake a collection completed event, otherwise the resources cannot be found + now := time.Now() + for _, group := range allGroups { + err := storage.AddOperationLog(ctx, []*pb.Operation{{ + Id: collectID, + ResourceGroup: group, + Type: "collection", + StatusTime: timestamppb.New(now), + Status: pb.Operation_STARTED, + Reason: "", + }}) + if err != nil { + t.Fatalf("AddOperationLog unexpected error: %v", err) + } + } + // Flush Ops Log + if err := storage.FlushOpsLog(ctx); err != nil { + t.Fatalf("FlushOpsLog unexpected error: %v", err) + } if _, err := storage.BatchCreateResources(ctx, resources); err != nil { t.Fatalf("AddResources unexpected error: %v", err) } + end := time.Now() + for _, group := range allGroups { + err := storage.AddOperationLog(ctx, []*pb.Operation{{ + Id: collectID, + ResourceGroup: group, + Type: "collection", + StatusTime: timestamppb.New(end), + Status: pb.Operation_COMPLETED, + Reason: "", + }}) + if err != nil { + t.Fatalf("AddOperationLog unexpected error: %v", err) + } + } + // Flush Ops Log + if err := storage.FlushOpsLog(ctx); err != nil { + t.Fatalf("FlushOpsLog unexpected error: %v", err) + } - allGroups := groupsFromResources(resources) - - obs, err := engine.New(storage, rules, []string{}).CheckRules(ctx, "unit-test-scan", allGroups) + scanID := uuid.NewString() + e, err := engine.New(storage, rules, map[string]json.RawMessage{ + "CONTAINER_NOT_RUNNING": mustMarshal(ContainerRunningConfig{ + RequiredContainers: map[string][]string{ + "namespace-1": {"pod-prefix-1-", "pod-prefix-2-"}, + }, + }), + }, []string{}, risk.TagConfig{ + Environment: "111111111111/environment", + EmployeeData: "111111111111/employee_data", + CustomerData: "111111111111/customer_data", + }) if err != nil { - t.Fatalf("CheckRules unexpected error: %v", err) + t.Fatalf("New unexpected error: %v", err) } - return obs + return e.CheckRules(ctx, scanID, "", allGroups, nil) } -func groupsFromResources(resources []*pb.Resource) (allGroups []string) { - resourceGroups := map[string]struct{}{} - for _, r := range resources { - switch r.Type.(type) { - case *pb.Resource_ResourceGroup: - resourceGroups[r.Name] = struct{}{} - } - } - for k := range resourceGroups { - allGroups = append(allGroups, k) +func errorStrings(errs []error) []string { + var errStrs []string + for _, err := range errs { + errStrs = append(errStrs, err.Error()) } - return allGroups + return errStrs } -func observationComparer(o1, o2 *pb.Observation) bool { - if o1 == nil || o2 == nil { - return false - } - if fmt.Sprintf("%T", o1.ExpectedValue) != fmt.Sprintf("%T", o2.ExpectedValue) { - return false - } - if fmt.Sprintf("%T", o1.ObservedValue) != fmt.Sprintf("%T", o2.ObservedValue) { - return false - } - if o1.Name != o2.Name { - return false +func TestRuleShouldFail(t *testing.T, resources []*pb.Resource, rules []model.Rule, expectedErr []error) { + _, err := testRuleRunHelper(t, resources, rules) + if diff := cmp.Diff(errorStrings(expectedErr), errorStrings(err)); diff != "" { + t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) } - if o1.Resource.Name != o2.Resource.Name { - return false +} + +func TestRuleRun(t *testing.T, resources []*pb.Resource, rules []model.Rule, want []*pb.Observation) { + got, errArr := testRuleRunHelper(t, resources, rules) + if len(errArr) > 0 { + t.Fatalf("CheckRules unexpected error: %v", errors.Join(errArr...)) } - switch o1.ExpectedValue.Kind.(type) { - case *structpb.Value_StringValue: - if o1.ExpectedValue.GetStringValue() == o2.ExpectedValue.GetStringValue() && o1.ObservedValue.GetStringValue() == o2.ObservedValue.GetStringValue() { - return true + for _, obs := range got { + if obs.Uid == "" { + t.Errorf("CheckRules unexpected empty UID") } - return false - case *structpb.Value_NumberValue: - if o1.ExpectedValue.GetNumberValue() == o2.ExpectedValue.GetNumberValue() && o1.ObservedValue.GetNumberValue() == o2.ObservedValue.GetNumberValue() { - return true + if obs.Timestamp == nil { + t.Errorf("CheckRules unexpected nil timestamp") } - return false - case *structpb.Value_BoolValue: - if o1.ExpectedValue.GetBoolValue() == o2.ExpectedValue.GetBoolValue() && o1.ObservedValue.GetBoolValue() == o2.ObservedValue.GetBoolValue() { - return true + if obs.Severity == pb.Severity_SEVERITY_UNKNOWN { + t.Errorf("CheckRules unexpected unknown severity") } - return false - default: - panic(fmt.Sprintf("comparison for type %T not implemented", o1.ExpectedValue.Kind)) + } + + // We add some fields to `want` so that we don't have to change the test data every time + // we modify one "meta" field: + for i, ob := range want { + ob.Source = pb.Observation_SOURCE_MODRON + ob.RiskScore = ob.Severity + ob.Impact = pb.Impact_IMPACT_MEDIUM + want[i] = ob + } + + if diff := cmp.Diff(want, got, protocmp.Transform(), protocmp.IgnoreFields( + &pb.Observation{}, + "timestamp", + "uid", + "scan_uid", + ), + protocmp.IgnoreFields(&pb.ResourceRef{}, "uid"), + protocmp.IgnoreUnknown(), + cmpopts.SortSlices(observationsSorter), + ); diff != "" { + t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) } } diff --git a/src/engine/rules/testing_e2e.go b/src/engine/rules/testing_e2e.go new file mode 100644 index 0000000..01099b9 --- /dev/null +++ b/src/engine/rules/testing_e2e.go @@ -0,0 +1,61 @@ +package rules + +import ( + "context" + "encoding/json" + "errors" + "os" + "testing" + + "github.com/sirupsen/logrus" + + "github.com/nianticlabs/modron/src/collector/gcpcollector" + "github.com/nianticlabs/modron/src/engine" + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/storage/memstorage" +) + +func TestE2ERuleRun(t *testing.T, rules []model.Rule) ([]*pb.Observation, error) { + t.Helper() + logrus.StandardLogger().SetLevel(logrus.DebugLevel) + projectID := os.Getenv("PROJECT_ID") + if projectID == "" { + t.Skip("PROJECT_ID is not set") + } + orgID := os.Getenv("ORG_ID") + if orgID == "" { + t.Skip("ORG_ID is not set") + } + orgSuffix := os.Getenv("ORG_SUFFIX") + if orgSuffix == "" { + t.Skip("ORG_SUFFIX is not set") + } + tagConfig := risk.TagConfig{ + Environment: "111111111111/environment", + EmployeeData: "111111111111/employee_data", + CustomerData: "111111111111/customer_data", + } + qualifiedProjectID := "projects/" + projectID + ctx := context.Background() + storage := memstorage.New() + collector, err := gcpcollector.New(ctx, storage, orgID, orgSuffix, []string{}, tagConfig, []string{}) + if err != nil { + t.Fatalf("NewCollector unexpected error: %v", err) + } + + if err := collector.CollectAndStoreAll(ctx, "test-collect", []string{qualifiedProjectID}, nil); err != nil { + t.Fatalf("collectAndStoreResources unexpected error: %v", err) + } + + e, err := engine.New(storage, rules, map[string]json.RawMessage{}, []string{}, tagConfig) + if err != nil { + t.Fatalf("NewEngine unexpected error: %v", err) + } + obs, errArr := e.CheckRules(ctx, "unit-test-scan", "", []string{qualifiedProjectID}, nil) + if errArr != nil { + return nil, errors.Join(errArr...) + } + return obs, nil +} diff --git a/src/engine/rules/unused_exported_credentials.go b/src/engine/rules/unused_exported_credentials.go index 99bbf62..4e6ed86 100644 --- a/src/engine/rules/unused_exported_credentials.go +++ b/src/engine/rules/unused_exported_credentials.go @@ -6,21 +6,27 @@ import ( "time" "github.com/google/uuid" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/nianticlabs/modron/src/common" - "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) const ( - UnusedExportedCredentials = "UNUSED_EXPORTED_CREDENTIALS" + unusedExportedCredentials = "UNUSED_EXPORTED_CREDENTIALS" //nolint:gosec // If you increase this value, also fetch the metric over a longer timeframe in the collector. oldestUsageVerificationMonths = 3 + sevenDays = 7 * time.Hour * 24 +) + +const ( + oneMonth = time.Hour * 24 * 30 ) -var oldestUsage = time.Now().Add(time.Duration(-oldestUsageVerificationMonths) * time.Hour * 24 * 30) +var oldestUsage = time.Now().Add(time.Duration(-oldestUsageVerificationMonths) * oneMonth) type UnusedExportedCredentialsRule struct { info model.RuleInfo @@ -33,42 +39,47 @@ func init() { func NewUnusedExportedCredentialsRule() model.Rule { return &UnusedExportedCredentialsRule{ info: model.RuleInfo{ - Name: UnusedExportedCredentials, - AcceptedResourceTypes: []string{ - common.ResourceExportedCredentials, + Name: unusedExportedCredentials, + AcceptedResourceTypes: []proto.Message{ + &pb.ExportedCredentials{}, }, }, } } -func (r *UnusedExportedCredentialsRule) Check(ctx context.Context, rsrc *pb.Resource) ([]*pb.Observation, []error) { +func (r *UnusedExportedCredentialsRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) ([]*pb.Observation, []error) { ec := rsrc.GetExportedCredentials() - obs := []*pb.Observation{} + var obs []*pb.Observation if ec.LastUsage == nil { // If there is no last usage value, we don't report anything. return []*pb.Observation{}, []error{} } - if ec.LastUsage.AsTime().Before(oldestUsage) { + if time.Since(ec.CreationDate.AsTime()) > sevenDays && ec.LastUsage.AsTime().Before(oldestUsage) { ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue(fmt.Sprintf("%s <", oldestUsage.Format(time.RFC3339))), ObservedValue: structpb.NewStringValue(ec.LastUsage.AsTime().Format(time.RFC3339)), Remediation: &pb.Remediation{ Description: fmt.Sprintf( - "Exported key [%q](https://console.cloud.google.com/apis/credentials?project=%s) has not been used in the last %d months", - getGcpReadableResourceName(rsrc.Name), - constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), + "Exported key `%s` of [%q](https://console.cloud.google.com/iam-admin/serviceaccounts/details/%s/keys?project=%s) has not been used in the last %d months", + utils.GetKeyID(rsrc.Name), + utils.GetServiceAccountNameFromKeyRef(rsrc.Name), + utils.GetServiceAccountNameFromKeyRef(rsrc.Name), + utils.StripProjectsPrefix(rsrc.ResourceGroupName), oldestUsageVerificationMonths, ), Recommendation: fmt.Sprintf( - "Consider deleting the exported key [%q](https://console.cloud.google.com/apis/credentials?project=%s), which is no longer in use", - getGcpReadableResourceName(rsrc.Name), - constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), + "Consider deleting the exported key `%s` of [%q](https://console.cloud.google.com/iam-admin/serviceaccounts/details/%s/keys?project=%s) which is no longer in use", + utils.GetKeyID(rsrc.Name), + utils.GetServiceAccountNameFromKeyRef(rsrc.Name), + utils.GetServiceAccountNameFromKeyRef(rsrc.Name), + utils.StripProjectsPrefix(rsrc.ResourceGroupName), ), }, + Severity: pb.Severity_SEVERITY_HIGH, } obs = append(obs, ob) } diff --git a/src/engine/rules/unused_exported_credentials_test.go b/src/engine/rules/unused_exported_credentials_test.go index 7aadb4c..6249a99 100644 --- a/src/engine/rules/unused_exported_credentials_test.go +++ b/src/engine/rules/unused_exported_credentials_test.go @@ -5,12 +5,12 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" ) func TestUnusedExportedKey(t *testing.T) { @@ -19,6 +19,21 @@ func TestUnusedExportedKey(t *testing.T) { oneYearAgo := now.Add(-time.Hour * 24 * 365) threeMonthsAndOneDay := now.Add(-time.Hour * 24 * 91) oneYearAhead := now.Add(time.Hour * 24 * 365) + + resourceNotUsedInALongTime := &pb.Resource{ + Name: testProjectName + "/serviceAccounts/unused-svc-account@project-id.iam.gserviceaccount.com/keys/d88bb32b79ee4193a05ee178447e09a4", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_ExportedCredentials{ + ExportedCredentials: &pb.ExportedCredentials{ + CreationDate: timestamppb.New(oneYearAgo), + ExpirationDate: ×tamppb.Timestamp{Seconds: oneYearAhead.Unix(), Nanos: 0}, + LastUsage: timestamppb.New(threeMonthsAndOneDay), + }, + }, + } + resources := []*pb.Resource{ { Name: testProjectName, @@ -29,8 +44,9 @@ func TestUnusedExportedKey(t *testing.T) { ResourceGroup: &pb.ResourceGroup{}, }, }, + resourceNotUsedInALongTime, { - Name: "not-used-in-a-long-time", + Name: "used-yesterday", Parent: testProjectName, ResourceGroupName: testProjectName, IamPolicy: &pb.IamPolicy{}, @@ -38,40 +54,39 @@ func TestUnusedExportedKey(t *testing.T) { ExportedCredentials: &pb.ExportedCredentials{ CreationDate: timestamppb.New(oneYearAgo), ExpirationDate: ×tamppb.Timestamp{Seconds: oneYearAhead.Unix(), Nanos: 0}, - LastUsage: timestamppb.New(threeMonthsAndOneDay), + LastUsage: timestamppb.New(yesterday), }, }, }, { - Name: "used-yesterday", + Name: "created-yesterday-unused-do-not-report", Parent: testProjectName, ResourceGroupName: testProjectName, IamPolicy: &pb.IamPolicy{}, Type: &pb.Resource_ExportedCredentials{ ExportedCredentials: &pb.ExportedCredentials{ - CreationDate: timestamppb.New(oneYearAgo), + CreationDate: timestamppb.New(yesterday), ExpirationDate: ×tamppb.Timestamp{Seconds: oneYearAhead.Unix(), Nanos: 0}, - LastUsage: timestamppb.New(yesterday), + LastUsage: nil, }, }, }, } - got := TestRuleRun(t, resources, []model.Rule{NewUnusedExportedCredentialsRule()}) - // Expected values are ordered lexicographically. want := []*pb.Observation{ { - Name: UnusedExportedCredentials, - Resource: &pb.Resource{ - Name: "not-used-in-a-long-time", - }, + Name: unusedExportedCredentials, + ResourceRef: utils.GetResourceRef(resourceNotUsedInALongTime), ExpectedValue: structpb.NewStringValue(fmt.Sprintf("%s <", oldestUsage.Format(time.RFC3339))), ObservedValue: structpb.NewStringValue(threeMonthsAndOneDay.Format(time.RFC3339)), + Remediation: &pb.Remediation{ + Description: "Exported key `d88bb32b79ee4193a05ee178447e09a4` of [\"unused-svc-account@project-id.iam.gserviceaccount.com\"](https://console.cloud.google.com/iam-admin/serviceaccounts/details/unused-svc-account@project-id.iam.gserviceaccount.com/keys?project=project-0) has not been used in the last 3 months", + Recommendation: "Consider deleting the exported key `d88bb32b79ee4193a05ee178447e09a4` of [\"unused-svc-account@project-id.iam.gserviceaccount.com\"](https://console.cloud.google.com/iam-admin/serviceaccounts/details/unused-svc-account@project-id.iam.gserviceaccount.com/keys?project=project-0) which is no longer in use", + }, + Severity: pb.Severity_SEVERITY_HIGH, }, } - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewUnusedExportedCredentialsRule()}, want) } diff --git a/src/engine/rules/vm_has_public_ip.go b/src/engine/rules/vm_has_public_ip.go index 5494c1f..1dd3ac5 100644 --- a/src/engine/rules/vm_has_public_ip.go +++ b/src/engine/rules/vm_has_public_ip.go @@ -6,11 +6,12 @@ import ( "strings" "github.com/google/uuid" + "google.golang.org/protobuf/proto" - "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -30,21 +31,21 @@ func NewVMHasPublicIPRule() model.Rule { return &VMHasPublicIPRule{ info: model.RuleInfo{ Name: VMHasPublicIPRuleName, - AcceptedResourceTypes: []string{ - common.ResourceVmInstance, + AcceptedResourceTypes: []proto.Message{ + &pb.VmInstance{}, }, }, } } -func (r *VMHasPublicIPRule) Check(ctx context.Context, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { +func (r *VMHasPublicIPRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { vm := rsrc.GetVmInstance() if vm.PublicIp != "" && !strings.HasPrefix(rsrc.GetName(), "gke-") && len([]rune(rsrc.GetName())) <= 30 { ob := &pb.Observation{ Uid: uuid.NewString(), Timestamp: timestamppb.Now(), - Resource: rsrc, + ResourceRef: utils.GetResourceRef(rsrc), Name: r.Info().Name, ExpectedValue: structpb.NewStringValue("empty"), ObservedValue: structpb.NewStringValue(vm.PublicIp), @@ -59,6 +60,7 @@ func (r *VMHasPublicIPRule) Check(ctx context.Context, rsrc *pb.Resource) (obs [ constants.ResourceWithoutProjectsPrefix(rsrc.ResourceGroupName), ), }, + Severity: pb.Severity_SEVERITY_HIGH, } obs = append(obs, ob) } diff --git a/src/engine/rules/vm_has_public_ip_test.go b/src/engine/rules/vm_has_public_ip_test.go index acb8ca5..22107e8 100644 --- a/src/engine/rules/vm_has_public_ip_test.go +++ b/src/engine/rules/vm_has_public_ip_test.go @@ -3,16 +3,25 @@ package rules import ( "testing" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "google.golang.org/protobuf/types/known/structpb" ) func TestCheckVMHasPublicIP(t *testing.T) { + publicIPResource := &pb.Resource{ + Name: "public-ip", + Parent: testProjectName, + ResourceGroupName: testProjectName, + IamPolicy: &pb.IamPolicy{}, + Type: &pb.Resource_VmInstance{ + VmInstance: &pb.VmInstance{ + PublicIp: "8.8.8.8", + }, + }, + } resources := []*pb.Resource{ { Name: testProjectName, @@ -23,17 +32,7 @@ func TestCheckVMHasPublicIP(t *testing.T) { ResourceGroup: &pb.ResourceGroup{}, }, }, - { - Name: "public-ip", - Parent: testProjectName, - ResourceGroupName: testProjectName, - IamPolicy: &pb.IamPolicy{}, - Type: &pb.Resource_VmInstance{ - VmInstance: &pb.VmInstance{ - PublicIp: "8.8.8.8", - }, - }, - }, + publicIPResource, { Name: "gke-public-ip", Parent: testProjectName, @@ -69,19 +68,17 @@ func TestCheckVMHasPublicIP(t *testing.T) { want := []*pb.Observation{ { - Name: VMHasPublicIPRuleName, - Resource: &pb.Resource{ - Name: "public-ip", - }, + Name: VMHasPublicIPRuleName, + ResourceRef: utils.GetResourceRef(publicIPResource), ObservedValue: structpb.NewStringValue("8.8.8.8"), ExpectedValue: structpb.NewStringValue("empty"), + Remediation: &pb.Remediation{ + Description: "VM \"public-ip\" has a public IP assigned", + Recommendation: "Compute instances should not be configured to have external IP addresses. Update network-settings of [public-ip](https://console.cloud.google.com/compute/instances?project=project-0). You can connect to Linux VMs that do not have public IP addresses by using Identity-Aware Proxy for TCP forwarding. [Learn more](https://cloud.google.com/compute/docs/instances/connecting-advanced#sshbetweeninstances)", + }, + Severity: pb.Severity_SEVERITY_HIGH, }, } - got := TestRuleRun(t, resources, []model.Rule{NewVMHasPublicIPRule()}) - - // Check that the observations are correct. - if diff := cmp.Diff(want, got, cmp.Comparer(observationComparer), cmpopts.SortSlices(observationsSorter)); diff != "" { - t.Errorf("CheckRules unexpected diff (-want, +got): %v", diff) - } + TestRuleRun(t, resources, []model.Rule{NewVMHasPublicIPRule()}, want) } diff --git a/src/engine/runner.go b/src/engine/runner.go index 279ccb0..685d475 100644 --- a/src/engine/runner.go +++ b/src/engine/runner.go @@ -2,22 +2,61 @@ package engine import ( "context" + "encoding/json" + "errors" "fmt" "sync" "time" - "github.com/golang/glog" - "golang.org/x/exp/slices" + "github.com/google/uuid" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/types/known/timestamppb" - "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/constants" + modronmetric "github.com/nianticlabs/modron/src/metric" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/utils" + + "github.com/sirupsen/logrus" +) + +var ( + log = logrus.StandardLogger().WithField(constants.LogKeyPkg, "engine") + meter = otel.Meter("github.com/nianticlabs/modron/src/engine") + tracer = otel.Tracer("github.com/nianticlabs/modron/src/engine") +) + +const ( + checkRulesBufferSize = 100 ) type RuleEngine struct { excludedRules []string + metrics metrics rules []model.Rule + ruleConfigs map[string]json.RawMessage storage model.Storage + + tagConfig risk.TagConfig + // memoizationMap is our caching layer that holds the resources that have been fetched from the storage. + // In the current implementation, multiple engines will not share their cache. + // Ideally, to avoid fetching multiple times the same resource, we would want to share the cache between engines. + memoizationMap sync.Map + // rgHierarchyMap is a map containing the resource group hierarchy for each collection ID. + rgHierarchyMap sync.Map +} + +var _ model.Engine = (*RuleEngine)(nil) + +type metrics struct { + RulesDuration metric.Float64Histogram + CheckRulesDuration metric.Float64Histogram + CreateObservation metric.Int64Counter } type CheckRuleResult struct { @@ -26,30 +65,52 @@ type CheckRuleResult struct { errs []error } -func New(s model.Storage, rules []model.Rule, excludedRules []string) *RuleEngine { - storage = &Storage{s} - glog.Infof("new rule engine with %d rules", len(rules)) - return &RuleEngine{ +func New( + s model.Storage, + rules []model.Rule, + ruleConfigs map[string]json.RawMessage, + excludedRules []string, + tagConfig risk.TagConfig, +) (*RuleEngine, error) { + log.Debugf("new rule engine with %d rules", len(rules)) + e := &RuleEngine{ excludedRules: excludedRules, rules: rules, storage: s, + ruleConfigs: ruleConfigs, + tagConfig: tagConfig, } + err := e.initMetrics() + if err != nil { + return nil, err + } + go e.startCacheCleanup() + return e, nil } // Checks that the supplied rule applies to the provided resources. func (e *RuleEngine) checkRule(ctx context.Context, r model.Rule, resources []*pb.Resource) (obs []*pb.Observation, errs []error) { + ctx, span := tracer.Start(ctx, "checkRule") + span.SetAttributes( + attribute.String(constants.TraceKeyRule, r.Info().Name), + ) + defer span.End() for _, rsrc := range resources { - t, err := common.TypeFromResourceAsString(rsrc) + t, err := utils.TypeFromResource(rsrc) if err != nil { - errs = append(errs, fmt.Errorf("could not retrieve type from resource %q: %v", rsrc, err)) + errs = append(errs, fmt.Errorf("could not retrieve type from resource %q: %w", rsrc, err)) continue } - if !slices.Contains(r.Info().AcceptedResourceTypes, t) { + acceptedResourceTypes := map[string]struct{}{} + for _, at := range r.Info().AcceptedResourceTypes { + acceptedResourceTypes[string(at.ProtoReflect().Type().Descriptor().FullName())] = struct{}{} + } + if _, ok := acceptedResourceTypes[t]; !ok { errs = append(errs, fmt.Errorf("resource type %q is not accepted by rule %s", t, r.Info().Name)) continue } - newObs, newErrs := r.Check(ctx, rsrc) + newObs, newErrs := r.Check(ctx, e, rsrc) if len(newErrs) > 0 { errs = append(errs, newErrs...) } else { @@ -60,17 +121,76 @@ func (e *RuleEngine) checkRule(ctx context.Context, r model.Rule, resources []*p } func (e *RuleEngine) checkRuleAsync(ctx context.Context, r model.Rule, resources []*pb.Resource, ch chan *CheckRuleResult) { + ctx, span := tracer.Start(ctx, "checkRuleAsync", + trace.WithAttributes(attribute.String(constants.TraceKeyRule, r.Info().Name)), + ) + log := log.WithField("rule", r.Info().Name) + defer span.End() + start := time.Now() ret := &CheckRuleResult{ rule: r, obs: nil, errs: nil, } ret.obs, ret.errs = e.checkRule(ctx, r, resources) + // Risk Score calculation + for k, obs := range ret.obs { + ret.obs[k].RiskScore, ret.obs[k].Impact, ret.obs[k].ImpactReason = e.computeRsImpact(ctx, obs) + } + + status := modronmetric.StatusSuccess + if len(ret.errs) > 0 { + status = modronmetric.StatusError + log.Errorf("rule execution failed: %v", ret.errs) + } + e.metrics.RulesDuration. + Record(ctx, time.Since(start).Seconds(), + metric.WithAttributes( + attribute.String(modronmetric.KeyRule, r.Info().Name), + attribute.String(modronmetric.KeyStatus, status), + ), + ) ch <- ret } +// computeRsImpactSeverity calculates the risk score by taking into account some external factors such as +// the parent resource group labels (e.g: environment: prod). +// It then returns the determined impact and the original severity +func (e *RuleEngine) computeRsImpact(ctx context.Context, obs *pb.Observation) (riskScore pb.Severity, impact pb.Impact, reason string) { + riskScore = pb.Severity_SEVERITY_UNKNOWN + impact = pb.Impact_IMPACT_UNKNOWN + + collectID, ok := ctx.Value(constants.CollectIDKey).(string) + if !ok { + log.Errorf("no %s in context", constants.CollectIDKey) + return + } + mapVal, ok := e.rgHierarchyMap.Load(collectID) + if !ok { + log.Errorf("no resource group hierarchy found for %q", collectID) + return + } + rgHierarchy, ok := mapVal.(map[string]*pb.RecursiveResource) + if !ok { + log.Errorf("resource group hierarchy is not a map") + return + } + if obs.ResourceRef == nil { + log.Errorf("observation %q has no resource ref", obs.Uid) + return + } + if obs.ResourceRef.GroupName == "" { + log.Errorf("observation %q has no group name", obs.Uid) + return + } + impact, reason = risk.GetImpact(e.tagConfig, rgHierarchy, obs.ResourceRef.GroupName) + return risk.GetRiskScore(impact, obs.Severity), impact, reason +} + // Fetches accepted resources and runs each rule in the engine asynchronously. func (e *RuleEngine) checkRulesAsync(ctx context.Context, resourceGroups []string, ch chan *CheckRuleResult) (errs []error) { + ctx, span := tracer.Start(ctx, "checkRulesAsync") + defer span.End() wg := sync.WaitGroup{} for _, r := range e.rules { isExcluded := false @@ -81,36 +201,81 @@ func (e *RuleEngine) checkRulesAsync(ctx context.Context, resourceGroups []strin } } if isExcluded { - glog.V(5).Infof("rule %s excluded", r.Info().Name) + log.Infof("rule %s excluded", r.Info().Name) continue } wg.Add(1) go func(r model.Rule) { + ctx, span := tracer.Start(ctx, "goCheckRuleAsync", + trace.WithNewRoot(), + trace.WithAttributes( + attribute.String(constants.TraceKeyRule, r.Info().Name), + attribute.StringSlice(constants.TraceKeyResourceGroupNames, resourceGroups), + ), + trace.WithLinks(trace.LinkFromContext(ctx)), + ) + defer span.End() types := r.Info().AcceptedResourceTypes + acceptedTypes := utils.ProtoAcceptsTypes(types) filter := model.StorageFilter{ - ResourceTypes: types, + ResourceTypes: acceptedTypes, ResourceGroupNames: resourceGroups, + OperationID: ctx.Value(constants.CollectIDKey).(string), } if resources, err := e.storage.ListResources(ctx, filter); err != nil { - errs = append(errs, fmt.Errorf("listing accepted resources: %+v", err)) + log.Errorf("listing accepted resources: %v", err) + span.RecordError(err) + errs = append(errs, fmt.Errorf("listing accepted resources: %w", err)) } else if len(resources) < 1 { - errs = append(errs, fmt.Errorf("no resources for %+v", filter)) + log.Warnf("no resources found") + span.RecordError(err) + errs = append(errs, fmt.Errorf("no resources found")) } else { + span.SetAttributes(attribute.Int(constants.TraceKeyNumResources, len(resources))) e.checkRuleAsync(ctx, r, resources, ch) } - glog.V(5).Infof("done with rule %s", r.Info().Name) + log.Infof("done with rule %s", r.Info().Name) wg.Done() }(r) } - glog.V(5).Infof("waiting for rules to finish") + log.Infof("waiting for rules to finish") wg.Wait() return errs } -// Checks that all the supplied rules apply to resources belonging to `resourceGroups`. -func (e *RuleEngine) CheckRules(ctx context.Context, scanId string, resourceGroups []string) (obs []*pb.Observation, errs []error) { - e.logScanStatus(ctx, scanId, resourceGroups, model.OperationStarted) - checkCh := make(chan *CheckRuleResult, 100) +// CheckRules checks that all the supplied rules apply to resources belonging to `resourceGroups`. +func (e *RuleEngine) CheckRules(ctx context.Context, scanID string, collectID string, resourceGroups []string, preCollectedRgs []*pb.Resource) (obs []*pb.Observation, errs []error) { + ctx, span := tracer.Start(ctx, "CheckRules") + defer span.End() + log := log.WithFields(logrus.Fields{ + constants.LogKeyScanID: scanID, + constants.LogKeyCollectID: collectID, + constants.LogKeyResourceGroupNames: resourceGroups, + }) + log.Infof("Start CheckRules") + defer log.Infof("End CheckRules") + e.logScanStatus(ctx, scanID, resourceGroups, pb.Operation_STARTED) + ctx = context.WithValue(ctx, constants.ScanIDKey, scanID) + ctx = context.WithValue(ctx, constants.CollectIDKey, collectID) + start := time.Now() + failed := func() { + e.logScanStatus(ctx, scanID, resourceGroups, pb.Operation_CANCELLED) + e.metrics.CheckRulesDuration.Record(ctx, + time.Since(start).Seconds(), + metric.WithAttributes( + attribute.String(modronmetric.KeyStatus, modronmetric.StatusCancelled), + ), + ) + } + + // Get Resource Group hierarchy and store it in the scan-specific cache + rgHierarchy, _ := utils.ComputeRgHierarchy(preCollectedRgs) + e.rgHierarchyMap.Store(collectID, rgHierarchy) + defer func() { + e.rgHierarchyMap.Delete(collectID) + }() + + checkCh := make(chan *CheckRuleResult, checkRulesBufferSize) wg := sync.WaitGroup{} wg.Add(1) go func() { @@ -118,9 +283,9 @@ func (e *RuleEngine) CheckRules(ctx context.Context, scanId string, resourceGrou select { case <-ctx.Done(): errs = append(errs, fmt.Errorf("context cancelled: %w", ctx.Err())) - e.logScanStatus(ctx, scanId, resourceGroups, model.OperationCancelled) + failed() if err := e.storage.FlushOpsLog(ctx); err != nil { - glog.Errorf("flushing operation log: %v", err) + log.Errorf("flushing operation log: %v", err) } break case res, ok := <-checkCh: @@ -129,14 +294,28 @@ func (e *RuleEngine) CheckRules(ctx context.Context, scanId string, resourceGrou break } for _, err := range res.errs { - errs = append(errs, fmt.Errorf("execution of rule %v failed: %w", res.rule, err)) + errs = append(errs, fmt.Errorf("execution of rule %v failed: %w", res.rule.Info().Name, err)) } for _, ob := range res.obs { - ob.ScanUid = scanId + ob.ScanUid = utils.RefOrNull(scanID) + ob.Source = pb.Observation_SOURCE_MODRON + + if ob.Uid == "" { + log.Errorf("observation from rule %s has no UUID", res.rule.Info().Name) + ob.Uid = uuid.NewString() + } } + status := modronmetric.StatusSuccess if _, err := e.storage.BatchCreateObservations(ctx, res.obs); err != nil { + status = modronmetric.StatusError errs = append(errs, fmt.Errorf("creation of observations for rule %v failed: %w", res.rule, err)) } + e.metrics.CreateObservation.Add(ctx, int64(len(res.obs)), + metric.WithAttributes( + attribute.String(modronmetric.KeyRule, res.rule.Info().Name), + attribute.String(modronmetric.KeyStatus, status), + ), + ) obs = append(obs, res.obs...) } if checkCh == nil { @@ -151,25 +330,92 @@ func (e *RuleEngine) CheckRules(ctx context.Context, scanId string, resourceGrou err := e.checkRulesAsync(ctx, resourceGroups, checkCh) if len(err) > 0 { errs = append(errs, err...) - glog.Warningf("rules run for %v : %v", resourceGroups, err) + log.WithError(errors.Join(errs...)).Warnf("rules run for with errors") } - glog.V(5).Infof("closing channel") + log.Tracef("closing channel") close(checkCh) wg.Done() }() - glog.V(5).Infof("waiting for scan %s to finish", scanId) + log.Infof("waiting for scan %q to finish", scanID) wg.Wait() - e.logScanStatus(ctx, scanId, resourceGroups, model.OperationCompleted) + e.logScanStatus(ctx, scanID, resourceGroups, pb.Operation_COMPLETED) + log.Infof("scan %q done", scanID) + e.metrics.CheckRulesDuration.Record(ctx, + time.Since(start).Seconds(), + metric.WithAttributes(attribute.String(modronmetric.KeyStatus, modronmetric.StatusCompleted)), + ) return } -func (e *RuleEngine) logScanStatus(ctx context.Context, scanId string, resourceGroups []string, status model.OperationStatus) { - ops := []model.Operation{} - glog.V(5).Infof("scan %s status %s for %+v", scanId, status, resourceGroups) +func (e *RuleEngine) GetRules() []model.Rule { + return e.rules +} + +func (e *RuleEngine) GetRuleConfig(_ context.Context, ruleName string) (json.RawMessage, error) { + v, ok := e.ruleConfigs[ruleName] + if !ok { + return nil, fmt.Errorf("no configuration found for rule %q", ruleName) + } + return v, nil +} + +func (e *RuleEngine) GetHierarchy(_ context.Context, collID string) (map[string]*pb.RecursiveResource, error) { + v, ok := e.rgHierarchyMap.Load(collID) + if !ok { + return nil, fmt.Errorf("no hierarchy found for %q", collID) + } + return v.(map[string]*pb.RecursiveResource), nil +} + +func (e *RuleEngine) logScanStatus(ctx context.Context, scanID string, resourceGroups []string, status pb.Operation_Status) { + var ops []*pb.Operation + log.Infof("scan %q status %s for %+v", scanID, status, resourceGroups) for _, resourceGroup := range resourceGroups { - ops = append(ops, model.Operation{ID: scanId, ResourceGroup: resourceGroup, OpsType: "scan", StatusTime: time.Now(), Status: status}) + ops = append(ops, &pb.Operation{ + Id: scanID, + ResourceGroup: resourceGroup, + Type: "scan", + StatusTime: timestamppb.New(time.Now()), + Status: status, + }) } if err := e.storage.AddOperationLog(ctx, ops); err != nil { - glog.Warningf("log operation: %v", err) + log.Warnf("log operation: %v", err) + } +} + +func (e *RuleEngine) initMetrics() error { + rulesDurationHist, err := meter.Float64Histogram( + constants.MetricsPrefix+"rules_duration", + metric.WithDescription("Duration of rules execution"), + metric.WithUnit("s"), + ) + if err != nil { + return err } + checkRulesDurationHist, err := meter.Float64Histogram( + constants.MetricsPrefix+"check_rules_duration", + metric.WithDescription("Duration of check_rules operations"), + metric.WithUnit("s"), + ) + if err != nil { + return err + } + createObservationCounter, err := meter.Int64Counter( + constants.MetricsPrefix+"create_observation", + metric.WithDescription("Number of observations created"), + ) + if err != nil { + return err + } + e.metrics = metrics{ + RulesDuration: rulesDurationHist, + CheckRulesDuration: checkRulesDurationHist, + CreateObservation: createObservationCounter, + } + return nil +} + +func (e *RuleEngine) GetTagConfig() risk.TagConfig { + return e.tagConfig } diff --git a/src/engine/runner_integration_test.go b/src/engine/runner_integration_test.go new file mode 100644 index 0000000..5767549 --- /dev/null +++ b/src/engine/runner_integration_test.go @@ -0,0 +1,66 @@ +//go:build integration + +package engine_test + +import ( + "context" + "encoding/json" + "errors" + "os" + "testing" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + + "github.com/nianticlabs/modron/src/collector/gcpcollector" + "github.com/nianticlabs/modron/src/engine" + "github.com/nianticlabs/modron/src/engine/rules" + "github.com/nianticlabs/modron/src/model" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/storage/memstorage" +) + +func TestCrossEnvironmentRuleIntegration(t *testing.T) { + rgNames := []string{ + "projects/modron-dev", + } + ctx := context.Background() + st := memstorage.New() + collectID := uuid.NewString() + scanID := uuid.NewString() + + logrus.StandardLogger().SetLevel(logrus.DebugLevel) + logrus.StandardLogger().SetFormatter(&logrus.TextFormatter{ + ForceColors: true, + }) + + orgID := os.Getenv("ORG_ID") + orgSuffix := os.Getenv("ORG_SUFFIX") + if orgID == "" || orgSuffix == "" { + t.Fatalf("ORG_ID and ORG_SUFFIX are required for this test") + } + tagConfig := risk.TagConfig{ + Environment: "111111111111/environment", + EmployeeData: "111111111111/employee_data", + CustomerData: "111111111111/customer_data", + } + + // Collect resources + c, err := gcpcollector.New(ctx, st, orgID, orgSuffix, []string{}, tagConfig, []string{}) + if err != nil { + t.Fatalf("failed to create collector: %v", err) + } + if err := c.CollectAndStoreAll(ctx, collectID, rgNames, nil); err != nil { + t.Fatalf("failed to collect resources: %v", err) + } + + e, _ := engine.New(st, []model.Rule{ + rules.NewCrossEnvironmentPermissionsRule(), + }, map[string]json.RawMessage{}, []string{}, tagConfig) + obs, errArr := e.CheckRules(ctx, scanID, collectID, rgNames, nil) + err = errors.Join(errArr...) + if err != nil { + t.Fatalf("failed to check rules: %v", err) + } + t.Logf("Observations: %v", obs) +} diff --git a/src/engine/runner_test.go b/src/engine/runner_test.go index ca977c5..2196ea9 100644 --- a/src/engine/runner_test.go +++ b/src/engine/runner_test.go @@ -2,37 +2,63 @@ package engine import ( "context" + "encoding/json" "fmt" "strings" "testing" + "time" + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/sirupsen/logrus" "golang.org/x/exp/slices" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" - "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" "github.com/nianticlabs/modron/src/storage/memstorage" + "github.com/nianticlabs/modron/src/utils" ) type TestRule struct { info model.RuleInfo } +var impactMap = map[string]pb.Impact{ + "prod": pb.Impact_IMPACT_HIGH, + "pre-prod": pb.Impact_IMPACT_MEDIUM, + "dev": pb.Impact_IMPACT_LOW, + "playground": pb.Impact_IMPACT_LOW, +} + func NewTestRule() *TestRule { return &TestRule{ info: model.RuleInfo{ Name: "TEST_RULE", - AcceptedResourceTypes: []string{common.ResourceApiKey, common.ResourceServiceAccount}, + AcceptedResourceTypes: []proto.Message{&pb.APIKey{}, &pb.ServiceAccount{}}, }, } } -func (r *TestRule) Check(ctx context.Context, rsrc *pb.Resource) ([]*pb.Observation, []error) { +func (r *TestRule) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) ([]*pb.Observation, []error) { if strings.Contains(rsrc.Name, "fail") { - return nil, []error{fmt.Errorf(rsrc.Name)} - } else { - return []*pb.Observation{{Name: rsrc.Name, Resource: &pb.Resource{}}}, nil + return nil, []error{fmt.Errorf("%s", rsrc.Name)} } + return []*pb.Observation{{ + Uid: uuid.NewString(), + Name: rsrc.Name, + ResourceRef: &pb.ResourceRef{ + Uid: proto.String(rsrc.Uid), + GroupName: rsrc.ResourceGroupName, + CloudPlatform: pb.CloudPlatform_GCP, + ExternalId: nil, + }, + Severity: pb.Severity_SEVERITY_LOW, + }}, nil } func (r *TestRule) Info() *model.RuleInfo { @@ -49,12 +75,39 @@ type TestRule2 struct { info model.RuleInfo } +type TestRuleBucketPublic struct{} + +func (t TestRuleBucketPublic) Check(_ context.Context, _ model.Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) { + if rsrc.GetBucket().GetAccessType() == pb.Bucket_PUBLIC { + obs = append(obs, &pb.Observation{ + Uid: uuid.NewString(), + Name: rsrc.Name, + Severity: pb.Severity_SEVERITY_HIGH, + ResourceRef: &pb.ResourceRef{ + Uid: proto.String(rsrc.Uid), + GroupName: rsrc.ResourceGroupName, + CloudPlatform: pb.CloudPlatform_GCP, + }, + }) + } + return +} + +func (t TestRuleBucketPublic) Info() *model.RuleInfo { + return &model.RuleInfo{ + Name: "TEST_RULE_BUCKET_PUBLIC", + AcceptedResourceTypes: []proto.Message{&pb.Bucket{}}, + } +} + +var _ model.Rule = (*TestRuleBucketPublic)(nil) + func NewTestRule1(name string) *TestRule1 { return &TestRule1{ Rule: NewTestRule(), info: model.RuleInfo{ Name: name, - AcceptedResourceTypes: []string{common.ResourceVmInstance, common.ResourceLoadBalancer}, + AcceptedResourceTypes: []proto.Message{&pb.VmInstance{}, &pb.LoadBalancer{}}, }, } } @@ -64,7 +117,7 @@ func NewTestRule2(name string) *TestRule2 { Rule: NewTestRule(), info: model.RuleInfo{ Name: name, - AcceptedResourceTypes: []string{common.ResourceVmInstance, common.ResourceNetwork}, + AcceptedResourceTypes: []proto.Message{&pb.VmInstance{}, &pb.Network{}}, }, } } @@ -81,7 +134,54 @@ func TestCheckRuleHandlesAllResourcesCorrectly(t *testing.T) { storage := memstorage.New() rule1 := NewTestRule1("TEST_RULE1") rule2 := NewTestRule2("TEST_RULE2") + collectionUID := uuid.NewString() resources := []*pb.Resource{ + { + Name: "projects/project-0", + Parent: "folders/folder-0", + ResourceGroupName: "projects/project-0", + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: "projects/project-0", + Name: "project-0", + }, + }, + Tags: map[string]string{ + "111111111111/customer_data": "111111111111/customer_data/yes", + }, + }, + { + Name: "folders/folder-0", + Parent: "organizations/1", + ResourceGroupName: "folders/folder-0", + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: "folders/folder-0", + Name: "Dev", + }, + }, + Tags: map[string]string{ + "111111111111/environment": "111111111111/environment/development", + "111111111111/employee_data": "111111111111/employee_data/no", + "111111111111/customer_data": "111111111111/customer_data/no", + }, + }, + { + Name: "organizations/1", + Parent: "", + ResourceGroupName: "organizations/1", + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: "organizations/1", + Name: "ACME Inc.", + }, + }, + Tags: map[string]string{ + "111111111111/environment": "111111111111/environment/production", + "111111111111/customer_data": "111111111111/customer_data/yes", + "111111111111/employee_data": "111111111111/employee_data/yes", + }, + }, { Name: "instance-0", Parent: "projects/project-0", @@ -138,25 +238,35 @@ func TestCheckRuleHandlesAllResourcesCorrectly(t *testing.T) { }, }, } + rgNames := map[string]struct{}{} + for i, res := range resources { + resources[i].CollectionUid = collectionUID + rgNames[res.ResourceGroupName] = struct{}{} + } rules := []model.Rule{rule1, rule2} ctx := context.Background() + registerCollectOperation(ctx, t, resources, storage, collectionUID) if _, err := storage.BatchCreateResources(ctx, resources); err != nil { t.Fatalf(`unexpected error: "%v"`, err) } - re := New(storage, rules, []string{}) + re, _ := New(storage, rules, map[string]json.RawMessage{}, []string{}, risk.TagConfig{ + Environment: "111111111111/environment", + EmployeeData: "111111111111/employee_data", + CustomerData: "111111111111/customer_data", + }) type Want struct { value string isErrorString bool } - want := []Want{} + var want []Want for _, rsrc := range resources { for _, rule := range rules { - if ty, err := common.TypeFromResourceAsString(rsrc); err != nil { - t.Errorf("common.TypeFromResourceAsString unexpected error: %v", err) - } else if slices.Contains(rule.Info().AcceptedResourceTypes, ty) { + if ty, err := utils.TypeFromResource(rsrc); err != nil { + t.Errorf("common.TypeFromResource unexpected error: %v", err) + } else if slices.Contains(utils.ProtoAcceptsTypes(rule.Info().AcceptedResourceTypes), ty) { want = append(want, Want{rsrc.Name, strings.Contains(rsrc.Name, "fail")}) } } @@ -171,7 +281,7 @@ func TestCheckRuleHandlesAllResourcesCorrectly(t *testing.T) { return 0 }) - obs, errs := re.CheckRules(ctx, "", []string{"projects/project-0", "projects/project-1"}) + obs, errs := re.CheckRules(ctx, uuid.NewString(), collectionUID, []string{"projects/project-0", "projects/project-1"}, nil) if len(obs) != 5 { t.Errorf("len(obs) got %d, want %d", len(obs), 5) } @@ -212,6 +322,340 @@ func TestCheckRuleHandlesAllResourcesCorrectly(t *testing.T) { } } +func TestCheckRuleSeverity(t *testing.T) { + st := memstorage.New() + logrus.StandardLogger().SetLevel(logrus.DebugLevel) + logrus.StandardLogger().SetFormatter(&logrus.TextFormatter{ + ForceColors: true, + }) + ctx := context.Background() + collID := uuid.NewString() + + resources := []*pb.Resource{ + { + Name: "organizations/1", + Parent: "", + ResourceGroupName: "organizations/1", + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: "organizations/1", + Name: "ACME Inc.", + }, + }, + Tags: map[string]string{ + "111111111111/environment": "111111111111/environment/prod", + "111111111111/customer_data": "111111111111/customer_data/yes", + "111111111111/employee_data": "111111111111/employee_data/yes", + }, + CollectionUid: collID, + }, + { + Name: "folders/dev-folder", + Parent: "organizations/1", + ResourceGroupName: "folders/dev-folder", + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: "folders/dev-folder", + Name: "Dev", + }, + }, + Tags: map[string]string{ + "111111111111/environment": "111111111111/environment/dev", + "111111111111/customer_data": "111111111111/customer_data/no", + "111111111111/employee_data": "111111111111/employee_data/no", + }, + }, + { + Name: "folders/pre-prod-folder", + Parent: "organizations/1", + ResourceGroupName: "folders/pre-prod-folder", + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: "folders/pre-prod-folder", + Name: "Pre-Prod", + }, + }, + Tags: map[string]string{ + "111111111111/environment": "111111111111/environment/pre-prod", + "111111111111/customer_data": "111111111111/customer_data/yes", + "111111111111/employee_data": "111111111111/employee_data/no", + }, + }, + { + Name: "projects/project-0", + Parent: "folders/dev-folder", + ResourceGroupName: "projects/project-0", + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: "projects/project-0", + Name: "project-0", + }, + }, + Tags: map[string]string{ + "111111111111/customer_data": "111111111111/customer_data/no", + }, + }, + { + Name: "bucket-0", + Parent: "projects/project-0", + ResourceGroupName: "projects/project-0", + Type: &pb.Resource_Bucket{ + Bucket: &pb.Bucket{ + AccessType: pb.Bucket_PUBLIC, + }, + }, + }, + { + Name: "projects/project-1", + Parent: "folders/pre-prod-folder", + ResourceGroupName: "projects/project-1", + Type: &pb.Resource_ResourceGroup{ + ResourceGroup: &pb.ResourceGroup{ + Identifier: "projects/project-1", + Name: "project-1", + }, + }, + Tags: map[string]string{ + "111111111111/customer_data": "111111111111/customer_data/no", + "111111111111/employee_data": "111111111111/employee_data/no", + }, + }, + { + Name: "bucket-1", + Parent: "projects/project-1", + ResourceGroupName: "projects/project-1", + Type: &pb.Resource_Bucket{ + Bucket: &pb.Bucket{ + AccessType: pb.Bucket_PUBLIC, + }, + }, + }, + } + for k := range resources { + resources[k].CollectionUid = collID + } + registerCollectOperation(ctx, t, resources, st, collID) + if _, err := st.BatchCreateResources(ctx, resources); err != nil { + t.Fatalf(`unexpected error: "%v"`, err) + } + _ = st.FlushOpsLog(ctx) + engine, _ := New(st, []model.Rule{TestRuleBucketPublic{}}, map[string]json.RawMessage{}, []string{}, risk.TagConfig{ + ImpactMap: impactMap, + Environment: "111111111111/environment", + EmployeeData: "111111111111/employee_data", + CustomerData: "111111111111/customer_data", + }) + scanID := uuid.NewString() + got, err := engine.CheckRules(ctx, scanID, collID, []string{"projects/project-0", "projects/project-1"}, resources) + if err != nil { + t.Fatalf(`unexpected error: "%v"`, err) + } + want := []*pb.Observation{ + { + Name: "bucket-0", + ScanUid: proto.String(scanID), + Source: pb.Observation_SOURCE_MODRON, + ResourceRef: &pb.ResourceRef{ + GroupName: "projects/project-0", + CloudPlatform: pb.CloudPlatform_GCP, + }, + Impact: pb.Impact_IMPACT_LOW, + ImpactReason: "environment=dev", + Severity: pb.Severity_SEVERITY_HIGH, + RiskScore: pb.Severity_SEVERITY_MEDIUM, + }, + { + Name: "bucket-1", + ScanUid: proto.String(scanID), + Source: pb.Observation_SOURCE_MODRON, + ResourceRef: &pb.ResourceRef{ + GroupName: "projects/project-1", + CloudPlatform: pb.CloudPlatform_GCP, + }, + Impact: pb.Impact_IMPACT_MEDIUM, + ImpactReason: "environment=pre-prod", + Severity: pb.Severity_SEVERITY_HIGH, + RiskScore: pb.Severity_SEVERITY_HIGH, + }, + } + if diff := cmp.Diff(want, got, protocmp.Transform(), + protocmp.IgnoreFields(&pb.ResourceRef{}, "uid"), + protocmp.IgnoreFields(&pb.Observation{}, "uid"), + ); diff != "" { + t.Errorf(`unexpected diff (-want +got): %s`, diff) + } +} + +func registerCollectOperation(ctx context.Context, t *testing.T, resources []*pb.Resource, storage model.Storage, collectionUID string) { + now := time.Now() + for _, group := range utils.GroupsFromResources(resources) { + err := storage.AddOperationLog(ctx, []*pb.Operation{{ + Id: collectionUID, + ResourceGroup: group, + Type: "collection", + Status: pb.Operation_STARTED, + StatusTime: timestamppb.New(now), + }}) + if err != nil { + t.Fatalf(`unexpected error: "%v"`, err) + } + } + // Flush + if err := storage.FlushOpsLog(ctx); err != nil { + t.Fatalf(`unexpected error: "%v"`, err) + } + now = time.Now() + for _, group := range utils.GroupsFromResources(resources) { + err := storage.AddOperationLog(ctx, []*pb.Operation{{ + Id: collectionUID, + ResourceGroup: group, + Type: "collection", + Status: pb.Operation_COMPLETED, + StatusTime: timestamppb.New(now), + }}) + if err != nil { + t.Fatalf(`unexpected error: "%v"`, err) + } + } + // Flush + if err := storage.FlushOpsLog(ctx); err != nil { + t.Fatalf(`unexpected error: "%v"`, err) + } +} + +func getRecursiveResource(recRes *pb.RecursiveResource, children []*pb.RecursiveResource) *pb.RecursiveResource { + newRecRes := proto.Clone(recRes).(*pb.RecursiveResource) + newRecRes.Children = children + return newRecRes +} + +func TestGetImpact(t *testing.T) { + logrus.StandardLogger().SetLevel(logrus.DebugLevel) + logrus.StandardLogger().SetFormatter(&logrus.TextFormatter{ + ForceColors: true, + }) + org1 := &pb.RecursiveResource{Name: "organizations/1", + DisplayName: "ACME Inc.", + Type: "ResourceGroup", + Parent: "", + Tags: map[string]string{ + "111111111111/environment": "111111111111/environment/prod", + "111111111111/customer_data": "111111111111/customer_data/yes", + "111111111111/employee_data": "111111111111/employee_data/yes", + }, + Children: []*pb.RecursiveResource{ + { + Name: "folders/dev-folder", + DisplayName: "Dev", + Type: "ResourceGroup", + Parent: "organizations/1", + Labels: map[string]string{ + "111111111111/environment": "111111111111/environment/dev", + "111111111111/customer_data": "111111111111/customer_data/no", + "111111111111/employee_data": "111111111111/employee_data/no", + }, + }, + }, + } + folder1 := &pb.RecursiveResource{ + Name: "folders/dev-folder", + DisplayName: "Dev", + Type: "ResourceGroup", + Parent: "organizations/1", + Tags: map[string]string{ + "111111111111/environment": "111111111111/environment/dev", + "111111111111/customer_data": "111111111111/customer_data/no", + "111111111111/employee_data": "111111111111/employee_data/no", + }, + } + folder2 := &pb.RecursiveResource{ + Name: "folders/prod-folder", + DisplayName: "111111111111/environment/prod", + Type: "ResourceGroup", + Parent: "organizations/1", + Tags: map[string]string{ + "111111111111/environment": "111111111111/environment/prod", + "111111111111/customer_data": "111111111111/customer_data/yes", + "111111111111/employee_data": "111111111111/employee_data/yes", + }, + } + prj0 := &pb.RecursiveResource{ + Name: "projects/project-0", + DisplayName: "Project 0", + Type: "ResourceGroup", + Parent: "folders/dev-folder", + Tags: map[string]string{ + "111111111111/customer_data": "111111111111/customer_data/no", + }, + } + prj1 := &pb.RecursiveResource{ + Name: "projects/project-1", + DisplayName: "Project 1", + Type: "ResourceGroup", + Parent: "folders/prod-folder", + Labels: map[string]string{ + "contact1": "alice@example.com", + "contact2": "bob@example.com", + }, + } + prj2 := &pb.RecursiveResource{ + Name: "projects/project-2", + DisplayName: "Project 2", + Type: "ResourceGroup", + Parent: "folders/dev-folder", + Tags: map[string]string{ + "111111111111/customer_data": "111111111111/customer_data/yes", + }, + } + prj3 := &pb.RecursiveResource{ + Name: "projects/project-3", + DisplayName: "Project 3", + Type: "ResourceGroup", + Parent: "folders/dev-folder", + Tags: map[string]string{ + "111111111111/employee_data": "111111111111/employee_data/yes", + }, + } + // A project that is a direct child of the organization, with no default labels + prj4 := &pb.RecursiveResource{ + Name: "projects/project-4", + DisplayName: "Project 4", + Type: "ResourceGroup", + Parent: "organizations/1", + } + + rgHierarchy := map[string]*pb.RecursiveResource{ + "": org1, + "organizations/1": getRecursiveResource(org1, []*pb.RecursiveResource{prj4}), + "folders/dev-folder": getRecursiveResource(folder1, []*pb.RecursiveResource{prj0, prj2, prj3}), + "folders/prod-folder": getRecursiveResource(folder2, []*pb.RecursiveResource{prj1}), + "projects/project-0": prj0, + "projects/project-1": prj1, + "projects/project-2": prj2, + "projects/project-3": prj3, + "projects/project-4": prj4, + } + + testForImpact(t, rgHierarchy, "projects/project-0", pb.Impact_IMPACT_LOW) + testForImpact(t, rgHierarchy, "projects/project-1", constants.ImpactCustomerData) + testForImpact(t, rgHierarchy, "projects/project-2", pb.Impact_IMPACT_HIGH) + testForImpact(t, rgHierarchy, "projects/project-3", constants.ImpactEmployeeData) + testForImpact(t, rgHierarchy, "projects/project-4", pb.Impact_IMPACT_HIGH) +} + +func testForImpact(t *testing.T, rgHierarchy map[string]*pb.RecursiveResource, rgName string, want pb.Impact) { + t.Helper() + got, _ := risk.GetImpact(risk.TagConfig{ + ImpactMap: impactMap, + Environment: "111111111111/environment", + EmployeeData: "111111111111/employee_data", + CustomerData: "111111111111/customer_data", + }, rgHierarchy, rgName) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf(`unexpected diff for %s (-want +got): %s`, rgName, diff) + } +} + // TODO fix flaky test. // func TestCheckRulesHandlesCheckCancellation(t *testing.T) { // rules := []model.Rule{ diff --git a/src/go.mod b/src/go.mod index fef8803..269c440 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,90 +1,139 @@ module github.com/nianticlabs/modron/src -go 1.21 +go 1.23.2 -replace github.com/nianticlabs/modron/src/pb => ./proto/ - -require github.com/improbable-eng/grpc-web v0.15.0 +replace github.com/nianticlabs/modron/src/proto/generated => ./proto/generated require ( - github.com/golang/glog v1.1.2 - github.com/google/go-cmp v0.5.9 - github.com/google/uuid v1.3.1 + cloud.google.com/go/longrunning v0.6.2 + github.com/alexflint/go-arg v1.5.1 + github.com/gogo/protobuf v1.3.2 + github.com/google/go-cmp v0.6.0 + github.com/google/uuid v1.6.0 + github.com/h2non/gock v1.2.0 + github.com/improbable-eng/grpc-web v0.15.0 github.com/lib/pq v1.10.9 - golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 - golang.org/x/net v0.14.0 - golang.org/x/oauth2 v0.11.0 - google.golang.org/api v0.138.0 - google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d - google.golang.org/grpc v1.57.0 - google.golang.org/protobuf v1.31.0 - k8s.io/client-go v0.28.1 - modernc.org/sqlite v1.25.0 - github.com/nianticlabs/modron/src/pb v0.0.0-00010101000000-000000000000 + github.com/sirupsen/logrus v1.9.3 + github.com/testcontainers/testcontainers-go v0.32.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.32.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 + go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 + go.opentelemetry.io/otel v1.31.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 + go.opentelemetry.io/otel/metric v1.31.0 + go.opentelemetry.io/otel/sdk v1.31.0 + go.opentelemetry.io/otel/sdk/metric v1.31.0 + go.opentelemetry.io/otel/trace v1.31.0 + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c + golang.org/x/net v0.30.0 + golang.org/x/oauth2 v0.23.0 + golang.org/x/sync v0.8.0 + golang.org/x/time v0.7.0 + google.golang.org/api v0.203.0 + google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.35.1 + gorm.io/driver/postgres v1.5.9 + gorm.io/driver/sqlite v1.5.6 + gorm.io/gorm v1.25.12 + gorm.io/plugin/opentelemetry v0.1.8 + k8s.io/api v0.31.2 + k8s.io/apimachinery v0.31.2 + k8s.io/client-go v0.31.2 + github.com/nianticlabs/modron/src/proto/generated v0.0.0-00010101000000-000000000000 ) require ( - cloud.google.com/go/compute v1.23.0 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/gin-gonic/gin v1.8.1 // indirect - github.com/go-logr/logr v1.2.4 // indirect - github.com/go-openapi/jsonpointer v0.20.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.4 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect + cloud.google.com/go/auth v0.10.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect + cloud.google.com/go/compute/metadata v0.5.2 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.12.5 // indirect + github.com/alexflint/go-scalar v1.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/containerd/containerd v1.7.20 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/desertbit/timer v1.0.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.1.2+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-sqlite3 v1.14.24 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rs/cors v1.9.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.12.0 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/term v0.12.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.28.1 // indirect - k8s.io/apimachinery v0.28.1 // indirect - k8s.io/klog/v2 v2.100.1 // indirect - k8s.io/kube-openapi v0.0.0-20230901164831-6c774f458599 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect - lukechampine.com/uint128 v1.3.0 // indirect - modernc.org/cc/v3 v3.41.0 // indirect - modernc.org/ccgo/v3 v3.16.15 // indirect - modernc.org/libc v1.24.1 // indirect - modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.7.1 // indirect - modernc.org/opt v0.1.3 // indirect - modernc.org/strutil v1.2.0 // indirect - modernc.org/token v1.1.0 // indirect - nhooyr.io/websocket v1.8.7 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 // indirect + k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect + nhooyr.io/websocket v1.8.17 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..1c4f142 --- /dev/null +++ b/src/go.sum @@ -0,0 +1,854 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/auth v0.9.8 h1:+CSJ0Gw9iVeSENVCKJoLHhdUykDgXSc4Qn+gu2BRtR8= +cloud.google.com/go/auth v0.9.8/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth v0.10.0 h1:tWlkvFAh+wwTOzXIjrwM64karR1iTBZ/GRr0S/DULYo= +cloud.google.com/go/auth v0.10.0/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= +cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk= +cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs= +cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= +cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= +cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= +cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.12.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0= +github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= +github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= +github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= +github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/containerd v1.7.20 h1:Sl6jQYk3TRavaU83h66QMbI2Nqg9Jm6qzwX57Vsn1SQ= +github.com/containerd/containerd v1.7.20/go.mod h1:52GsS5CwquuqPuLncsXwG0t2CiUce+KsNHJZQJvAgR0= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= +github.com/desertbit/timer v1.0.1 h1:yRpYNn5Vaaj6QXecdLMPMJsW81JLiI1eokUft5nBmeo= +github.com/desertbit/timer v1.0.1/go.mod h1:htRrYeY5V/t4iu1xCJ5XsQvp4xve8QulXXctAzxqcwE= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY= +github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= +github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI= +github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJHDHqxHS801UIuhqGl6QdSAEJvtausosHSdazIo= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME= +github.com/testcontainers/testcontainers-go v0.32.0/go.mod h1:CRHrzHLQhlXUsa5gXjTOfqIEJcrK5+xMDmBr/WMI88E= +github.com/testcontainers/testcontainers-go/modules/postgres v0.32.0 h1:ZE4dTdswj3P0j71nL+pL0m2e5HTXJwPoIFr+DDgdPaU= +github.com/testcontainers/testcontainers-go/modules/postgres v0.32.0/go.mod h1:njrNuyuoF2fjhVk6TG/R3Oeu82YwfYkbf5WVTyBXhV4= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0/go.mod h1:LqaApwGx/oUmzsbqxkzuBvyoPpkxk3JQWnqfVrJ3wCA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 h1:yMkBS9yViCc7U7yeLzJPM2XizlfdVvBRSmsQDWu6qc0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0/go.mod h1:n8MR6/liuGB5EmTETUBeU5ZgqMOlqKRxUaqPQBOANZ8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/contrib/instrumentation/runtime v0.55.0 h1:GotCpbh7YkCHdFs+hYMdvAEyGsBZifFognqrOnBwyJM= +go.opentelemetry.io/contrib/instrumentation/runtime v0.55.0/go.mod h1:6b0AS55EEPj7qP44khqF5dqTUq+RkakDMShFaW1EcA4= +go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 h1:s7wHG+t8bEoH7ibWk1nk682h7EoWLJ5/8j+TSO3bX/o= +go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0/go.mod h1:Q8Hsv3d9DwryfIl+ebj4mY81IYVRSPy4QfxroVZwqLo= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0 h1:WypxHH02KX2poqqbaadmkMYalGyy/vil4HE4PM4nRJc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0/go.mod h1:U79SV99vtvGSEBeeHnpgGJfTsnsdkWLpPN/CcHAzBSI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 h1:m0yTiGDLUvVYaTFbAvCkVYIYcvwKt3G7OLoN77NUs/8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0/go.mod h1:wBQbT4UekBfegL2nx0Xk1vBcnzyBPsIVm9hRG4fYcr4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792jZO1bo4BXkM= +go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ= +google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw= +google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU= +google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU= +google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4= +google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU= +google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 h1:2oV8dfuIkM1Ti7DwXc0BJfnwr9csz4TDXI9EmiI+Rbw= +google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38/go.mod h1:vuAjtvlwkDKF6L1GQ0SokiRLCGFfeBUXWr/aFFkHACc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= +google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= +gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= +gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gorm.io/plugin/opentelemetry v0.1.6 h1:+qFdvyBoaB6i9mJsToAUyAwO40WFAH2GHBzIEb9eSSg= +gorm.io/plugin/opentelemetry v0.1.6/go.mod h1:TYGUagk7h8WwuCsDDznEzznY31PP3+NRpfh6FH7Yqfs= +gorm.io/plugin/opentelemetry v0.1.8 h1:uX3deb3w71mufbx8iY9buiGh+4HJjhItRNisZIy1fDY= +gorm.io/plugin/opentelemetry v0.1.8/go.mod h1:TYGUagk7h8WwuCsDDznEzznY31PP3+NRpfh6FH7Yqfs= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= +k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= +k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= +k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= +k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= +k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= +k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= +k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= +k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= +k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 h1:1dWzkmJrrprYvjGwh9kEUxmcUV/CtNU8QM7h1FLWQOo= +k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA= +k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 h1:MErs8YA0abvOqJ8gIupA1Tz6PKXYUw34XsGlA7uSL1k= +k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094/go.mod h1:7ioBJr1A6igWjsR2fxq2EZ0mlMwYLejazSIc2bzMp2U= +k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA= +k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= +nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/src/log.go b/src/log.go new file mode 100644 index 0000000..e24bf22 --- /dev/null +++ b/src/log.go @@ -0,0 +1,91 @@ +package main + +import ( + "encoding/json" + "strings" + "time" + + "github.com/sirupsen/logrus" +) + +type LogFormat string + +const ( + LogFormatJSON LogFormat = "json" + LogFormatText LogFormat = "text" +) + +// setLogLevel sets the logrus log level +func setLogLevel() { + switch strings.ToLower(args.LogLevel) { + case "trace": + log.SetLevel(logrus.TraceLevel) + case "debug": + log.SetLevel(logrus.DebugLevel) + case "info": + log.SetLevel(logrus.InfoLevel) + case "warning": + log.SetLevel(logrus.WarnLevel) + case "error": + log.SetLevel(logrus.ErrorLevel) + } +} + +type gcpFormatter struct{} + +func (g gcpFormatter) Format(entry *logrus.Entry) ([]byte, error) { + output := map[string]any{} + output["severity"] = toGcpSeverity(entry.Level) + output["message"] = entry.Message + output["timestamp"] = entry.Time.Format(time.RFC3339Nano) + if len(entry.Data) > 0 { + output["labels"] = entry.Data + } + + b, err := json.Marshal(output) + if err != nil { + return nil, err + } + b = append(b, '\n') + return b, nil +} + +func toGcpSeverity(level logrus.Level) string { + switch level { + case logrus.TraceLevel, logrus.DebugLevel: + return "DEBUG" + case logrus.InfoLevel: + return "INFO" + case logrus.WarnLevel: + return "WARNING" + case logrus.ErrorLevel: + return "ERROR" + case logrus.FatalLevel, logrus.PanicLevel: + return "CRITICAL" + default: + return "DEFAULT" + } +} + +var gcpFormat = &gcpFormatter{} +var textFormatter = &logrus.TextFormatter{} + +// setLogFormat sets the format of the logs +func setLogFormat() { + var formatter logrus.Formatter + invalidFormatter := false + switch args.LogFormat { + case LogFormatJSON: + formatter = gcpFormat + case LogFormatText: + formatter = textFormatter + default: + formatter = textFormatter + invalidFormatter = true + } + + logrus.SetFormatter(formatter) + if invalidFormatter { + log.Errorf("invalid log format, using %s", LogFormatText) + } +} diff --git a/src/lognotifier/lognotifier.go b/src/lognotifier/lognotifier.go index b238a93..3e36389 100644 --- a/src/lognotifier/lognotifier.go +++ b/src/lognotifier/lognotifier.go @@ -2,46 +2,65 @@ package lognotifier import ( "context" + "errors" - "github.com/golang/glog" + "github.com/nianticlabs/modron/src/constants" "github.com/nianticlabs/modron/src/model" + + "github.com/sirupsen/logrus" ) func New() model.NotificationService { return &LogNotifier{} } +var log = logrus.StandardLogger().WithField(constants.LogKeyPkg, "lognotifier") + type LogNotifier struct { exceptions []model.Exception } -func (ln *LogNotifier) CreateNotification(ctx context.Context, notification model.Notification) (model.Notification, error) { - glog.Infof("create notification: %+v", notification) +func (ln *LogNotifier) BatchCreateNotifications(ctx context.Context, notifications []model.Notification) ([]model.Notification, error) { + var resultNotifications []model.Notification + var errArr []error + for _, v := range notifications { + notif, err := ln.CreateNotification(ctx, v) + if err != nil { + errArr = append(errArr, err) + continue + } + resultNotifications = append(resultNotifications, notif) + } + return resultNotifications, errors.Join(errArr...) +} + +func (ln *LogNotifier) CreateNotification(_ context.Context, notification model.Notification) (model.Notification, error) { + log.Infof("create notification: %+v", notification) return notification, nil } -func (ln *LogNotifier) GetException(ctx context.Context, uuid string) (model.Exception, error) { - glog.Infof("get exception called with %q", uuid) - return model.Exception{Uuid: uuid}, nil +func (ln *LogNotifier) GetException(_ context.Context, uuid string) (model.Exception, error) { + log.Infof("get exception called with %q", uuid) + return model.Exception{UUID: uuid}, nil } -func (ln *LogNotifier) CreateException(ctx context.Context, exception model.Exception) (model.Exception, error) { - glog.Infof("create exception %+v", exception) +func (ln *LogNotifier) CreateException(_ context.Context, exception model.Exception) (model.Exception, error) { + log.Infof("create exception %+v", exception) ln.exceptions = append(ln.exceptions, exception) return exception, nil } -func (ln *LogNotifier) UpdateException(ctx context.Context, exception model.Exception) (model.Exception, error) { - glog.Infof("update exception: %+v", exception) +func (ln *LogNotifier) UpdateException(_ context.Context, exception model.Exception) (model.Exception, error) { + log.Infof("update exception: %+v", exception) return exception, nil } -func (ln *LogNotifier) DeleteException(ctx context.Context, id string) error { - glog.Infof("delete exception %q", id) +func (ln *LogNotifier) DeleteException(_ context.Context, id string) error { + log.Infof("delete exception %q", id) return nil } -func (ln *LogNotifier) ListExceptions(ctx context.Context, userEmail string, pageSize int32, pageToken string) ([]model.Exception, error) { - glog.Infof("list exceptions for user %q", userEmail) +func (ln *LogNotifier) ListExceptions(_ context.Context, userEmail string, _ int32, _ string) ([]model.Exception, error) { + log.Infof("list exceptions for user %q", userEmail) return ln.exceptions, nil } diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..76071ef --- /dev/null +++ b/src/main.go @@ -0,0 +1,277 @@ +package main + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/alexflint/go-arg" + "github.com/improbable-eng/grpc-web/go/grpcweb" + "github.com/sirupsen/logrus" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/contrib/instrumentation/runtime" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/nianticlabs/modron/src/collector" + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +var args struct { + AdditionalAdminRoles []string `arg:"--additional-admin-roles,env:ADDITIONAL_ADMIN_ROLES" help:"Comma separated list of roles that are considered administrators of a resource group"` + AdminGroups []string `arg:"--admin-groups,env:ADMIN_GROUPS" help:"Comma separated list of groups that are allowed to see all projects"` + AllowedSccCategories []string `arg:"--allowed-scc-categories,env:ALLOWED_SCC_CATEGORIES" help:"Comma separated list of SCC categories that are allowed to create observations"` + CollectAndScanInterval time.Duration `arg:"--collect-and-scan-interval,env:COLLECT_AND_SCAN_INTERVAL" help:"Interval between collecting and scanning (example: 3h)" default:"6h"` + // TODO: Collector should be a list, as we might want to support more + Collector collector.Type `arg:"--collector,env:COLLECTOR" help:"Specify which collector to use" default:"gcp"` + DbBatchSize int32 `arg:"--db-batch-size,env:DB_BATCH_SIZE" help:"Number of records to insert in a single batch" default:"32"` + DbConnectionMaxIdleTime time.Duration `arg:"--db-connection-max-idle-time,env:DB_CONNECTION_MAX_IDLE_TIME" help:"Maximum amount of time a connection may be idle" default:"30s"` + DbConnectionMaxLifetime time.Duration `arg:"--db-connection-max-lifetime,env:DB_CONNECTION_MAX_LIFETIME" help:"Maximum amount of time a connection may be reused" default:"1h"` + DbMaxConnections int `arg:"--db-max-connections,env:DB_MAX_CONNECTIONS" help:"Maximum number of connections to the database" default:"10"` + DbMaxIdleConnections int `arg:"--db-max-idle-connections,env:DB_MAX_IDLE_CONNECTIONS" help:"Maximum number of idle connections to the database" default:"10"` + DisableTelemetry bool `arg:"--disable-telemetry,env:DISABLE_TELEMETRY" help:"Disable OTEL telemetry" default:"false"` + Environment string `arg:"--environment,env:ENVIRONMENT" help:"Environment (development, production)" default:"development"` + ExcludedRules []string `arg:"--excluded-rules,env:EXCLUDED_RULES" help:"Comma separated list of rules to exclude from the scan."` + ImpactMap string `arg:"--impact-map,env:IMPACT_MAP" help:"JSON map that maps the environment name to the impact level" default:"{}"` + IsE2EGrpcTest bool `arg:"--is-e2e-grpc-test,env:IS_E2E_GRPC_TEST" help:"Is this an end-to-end gRPC test" default:"false"` + LabelToEmailRegexp string `arg:"--label-to-email-regexp,env:LABEL_TO_EMAIL_REGEXP" help:"Regular expression to extract email from labels" default:"(.*)_(.*?)_(.*?)$"` + LabelToEmailSubst string `arg:"--label-to-email-substitution,env:LABEL_TO_EMAIL_SUBSTITUTION" help:"Substitution to apply to the email extracted from labels" default:"$1@$2.$3"` + ListenAddr string `arg:"--listen-addr,env:LISTEN_ADDR" help:"Address to listen on" default:"127.0.0.1"` + LogFormat LogFormat `arg:"--log-format,env:LOG_FORMAT" help:"Log format (json,text)" default:"json"` + LogLevel string `arg:"--log-level,env:LOG_LEVEL" help:"Log level (trace,debug,info,warning,error)" default:"info"` + LogAllSQLQueries bool `arg:"--log-all-sql-queries,env:LOG_ALL_SQL_QUERIES" help:"Log all SQL queries" default:"false"` + NotificationInterval time.Duration `arg:"--notification-interval,env:NOTIFICATION_INTERVAL" help:"Interval between notifications (minimum: 24h)" default:"24h"` + NotificationService string `arg:"--notification-service,env:NOTIFICATION_SERVICE" help:"Address of the notification service"` + NotificationServiceClientID string `arg:"--notification-service-client-id,env:NOTIFICATION_SERVICE_CLIENT_ID" help:"Client ID for the notification service"` + OrgID string `arg:"--org-id,env:ORG_ID,required" help:"Organization ID"` + OrgSuffix string `arg:"--org-suffix,env:ORG_SUFFIX,required" help:"Organization suffix (e.g: @example.com)"` + PersistentCache bool `arg:"--persistent-cache,env:PERSISTENT_CACHE" help:"Use a persistent ACL cache that will be stored on the temporary directory" default:"false"` + PersistentCacheTimeout time.Duration `arg:"--persistent-cache-timeout,env:PERSISTENT_CACHE_TIMEOUT" help:"Amount of time to keep the ACLs on the filesystem before we fetch them again" default:"5m"` + Port int32 `arg:"--port,env:PORT" help:"Port to listen on" default:"8080"` + RuleConfigs string `arg:"--rule-configs,env:RULE_CONFIGS" help:"A map of rule names to their JSON configuration" default:"{}"` + RunAutomatedScans bool `arg:"--run-automated-scans,env:RUN_AUTOMATED_SCANS" help:"Run automated scans" default:"true"` + SelfURL string `arg:"--self-url,env:SELF_URL" help:"URL of Modron - to be used when sending notifications" default:"https://modron"` + SkipIAP bool `arg:"--skip-iap,env:SKIP_IAP" help:"Skip IAP authentication" default:"false"` + SQLBackendDriver string `arg:"--sql-backend-driver,env:SQL_BACKEND_DRIVER" help:"SQL backend driver (postgres)" default:"postgres"` + SQLConnectionString string `arg:"--sql-connection-string,env:SQL_CONNECT_STRING" help:"SQL connection string" default:""` // TODO: Find where SQL_CONNECT_STRING is used and change it to SQL_CONNECTION_STRING for the future + Storage string `arg:"--storage,env:STORAGE" help:"Storage type (memory,sql)" default:"sql"` + TagCustomerData string `arg:"--tag-customer-data,env:TAG_CUSTOMER_DATA,required" help:"Tag to use to define the customer data (e.g: 111111111/customer_data)"` + TagEmployeeData string `arg:"--tag-employee-data,env:TAG_EMPLOYEE_DATA,required" help:"Tag to use to define the employee data (e.g: 111111111/employee_data)"` + TagEnvironment string `arg:"--tag-environment,env:TAG_ENVIRONMENT,required" help:"Tag to use to define the environment (e.g: 111111111/environment)"` +} + +var ( + start = time.Now() + log = logrus.StandardLogger() + tracer trace.Tracer +) + +const ( + ExitCodeOK = iota + ExitCodeInvalidArgs + ExitCodeFailedToListen + ExitCodeFailedToCreateServer + ExitCodeFailedToServeGRPC + ExitCodeFailedToServeHTTP +) + +const ( + HTTPHeaderReadTimeout = 5 * time.Second +) + +func main() { + arg.MustParse(&args) + setLogLevel() + setLogFormat() + + log.Debugf("validating arguments..") + if err := validateArgs(); err != nil { + log.Error(err) + os.Exit(ExitCodeInvalidArgs) + } + log.Debugf("starting") + doMain() +} + +func doMain() { + mainCtx, cancel := context.WithCancel(context.Background()) + tp, mp := initTracer(mainCtx) + defer func() { + _ = tp.Shutdown(mainCtx) + _ = mp.Shutdown(mainCtx) + }() + + defer log.Tracef("net.Listen on port %d", args.Port) + // Handle SIGINT (for Ctrl+C) and SIGTERM (for Cloud Run) signals + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + go func() { + sig := <-c + log.Infof("received signal: %+v", sig) + cancel() + }() + + go func() { + ctx, span := tracer.Start(mainCtx, "setup-grpc") + log.Tracef("Setting up GRPC") + // Use insecure credentials since communication is encrypted and authenticated via + // HTTPS end-to-end (i.e., from client to Cloud Run ingress). + var opts = []grpc.ServerOption{ + grpc.Creds(insecure.NewCredentials()), + } + opts = append( + opts, + grpc.StatsHandler(otelgrpc.NewServerHandler()), + ) + grpcServer := grpc.NewServer(opts...) // nosemgrep: go.grpc.security.grpc-server-insecure-connection.grpc-server-insecure-connection + log.Tracef("Creating newServer") + srv, err := newServer(ctx) + if err != nil { + log.Errorf("server creation: %v", err) + os.Exit(ExitCodeFailedToCreateServer) + } + log.Tracef("Registering Modron Service Server") + pb.RegisterModronServiceServer(grpcServer, srv) + log.Tracef("Registering Modron Notification Service Server") + pb.RegisterNotificationServiceServer(grpcServer, srv) + log.Infof("server starting on port %d", args.Port) + if args.RunAutomatedScans { + log.Tracef("Starting scheduled runner") + go srv.ScheduledRunner(mainCtx) + } + span.End() + if args.IsE2EGrpcTest { + log.Warnf("E2E gRPC test mode enabled") + // TODO: Unfortunately we need this as the GRPC-Web is different from the GRPC protocol. + // This is used only in the integration test that doesn't have a GRPC-Web client. + // We should look into https://github.com/improbable-eng/grpc-web and check how we can implement a golang GRPC-web client. + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", args.Port)) + if err != nil { + log.Errorf("failed to listen: %v", err) + os.Exit(ExitCodeFailedToListen) + } + if err := grpcServer.Serve(lis); err != nil { + log.Errorf("error while listening: %v", err) + os.Exit(ExitCodeFailedToServeGRPC) + } + } else { + log.Infof("starting gRPC-Web server") + grpcWebServer := grpcweb.WrapServer(grpcServer, withCors()...) + log.Debugf("time until start: %v", time.Since(start)) + httpServer := &http.Server{ + Addr: fmt.Sprintf("%s:%d", args.ListenAddr, args.Port), + ReadHeaderTimeout: HTTPHeaderReadTimeout, + Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/healthz": + w.WriteHeader(http.StatusOK) + default: + otelhttp.NewMiddleware("grpc-web")(grpcWebServer).ServeHTTP(w, req) + } + }), + } + if err := httpServer.ListenAndServe(); err != nil { + log.Errorf("error while listening: %v", err) + os.Exit(ExitCodeFailedToServeHTTP) + } + } + }() + + <-mainCtx.Done() + log.Infof("server stopped") +} + +func newTraceProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider { + r, err := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName("Modron"), + ), + ) + + if err != nil { + panic(err) + } + + return sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithResource(r), + sdktrace.WithSpanProcessor(sdktrace.NewBatchSpanProcessor(exp)), + ) +} + +func newTraceExporter(ctx context.Context) (*otlptrace.Exporter, error) { + return otlptrace.New(ctx, + otlptracegrpc.NewClient(otlptracegrpc.WithInsecure()), + ) +} + +func newMetricExporter(ctx context.Context) (sdkmetric.Exporter, error) { + return otlpmetricgrpc.New(ctx, + otlpmetricgrpc.WithInsecure(), + ) +} + +func initTracer(ctx context.Context) (*sdktrace.TracerProvider, *sdkmetric.MeterProvider) { + if args.DisableTelemetry { + log.Warnf("telemetry is disabled!") + tp := sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.NeverSample())) + tracer = tp.Tracer("github.com/nianticlabs/modron") + return tp, sdkmetric.NewMeterProvider() + } + traceExp, err := newTraceExporter(ctx) + if err != nil { + log.Fatalf("failed to initialize exporter: %v", err) + } + metricExp, err := newMetricExporter(ctx) + if err != nil { + log.Fatalf("failed to initialize metric exporter: %v", err) + } + // Create a new tracer provider with a batch span processor and the given exporter. + tp := newTraceProvider(traceExp) + mp := newMeterProvider(ctx, metricExp) + + if err := runtime.Start(); err != nil { + log.Fatalf("failed to start runtime metrics: %v", err) + } + + otel.SetTracerProvider(tp) + otel.SetMeterProvider(mp) + otel.SetTextMapPropagator(propagation.TraceContext{}) + + tracer = tp.Tracer("github.com/nianticlabs/modron") + return tp, mp +} + +func newMeterProvider(ctx context.Context, exp sdkmetric.Exporter) *sdkmetric.MeterProvider { + rsrc, err := resource.New(ctx) + if err != nil { + log.Fatalf("failed to create resource: %v", err) + return nil + } + + return sdkmetric.NewMeterProvider( + sdkmetric.WithResource(rsrc), + sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exp, + sdkmetric.WithInterval(time.Second), + sdkmetric.WithProducer(runtime.NewProducer()), + )), + ) +} diff --git a/src/metric/keys.go b/src/metric/keys.go new file mode 100644 index 0000000..6d82520 --- /dev/null +++ b/src/metric/keys.go @@ -0,0 +1,13 @@ +package metric + +const ( + KeyCategory = "category" + KeyCount = "count" + KeyMethod = "method" + KeyOffendingPackage = "offending_package" + KeyPath = "path" + KeyRecipient = "recipient" + KeyRule = "rule" + KeySeverity = "severity" + KeyStatus = "status" +) diff --git a/src/metric/status.go b/src/metric/status.go new file mode 100644 index 0000000..bedbb11 --- /dev/null +++ b/src/metric/status.go @@ -0,0 +1,10 @@ +package metric + +type Status = string + +const ( + StatusCancelled Status = "cancelled" + StatusCompleted Status = "completed" + StatusError Status = "error" + StatusSuccess Status = "success" +) diff --git a/src/model/acl.go b/src/model/acl.go index 6ea63f2..c6950a5 100644 --- a/src/model/acl.go +++ b/src/model/acl.go @@ -2,8 +2,13 @@ package model import "golang.org/x/net/context" +// ACLCache is a map of users to a map of resource names: {"user@example.com": {"projects/xyz": {}}} +// the reason why the last part is a struct{} is that we don't care about the value, we only care about the key +// and a struct{} is the smallest value we can use. +type ACLCache map[string]map[string]struct{} + type Checker interface { - GetAcl() map[string]map[string]struct{} + GetACL() ACLCache GetValidatedUser(ctx context.Context) (string, error) ListResourceGroupNamesOwned(ctx context.Context) (map[string]struct{}, error) } diff --git a/src/model/collector.go b/src/model/collector.go index 0b3a0fb..1ee795f 100644 --- a/src/model/collector.go +++ b/src/model/collector.go @@ -2,15 +2,18 @@ package model import ( "golang.org/x/net/context" - "github.com/nianticlabs/modron/src/pb" + + pb "github.com/nianticlabs/modron/src/proto/generated" ) type Collector interface { - CollectAndStoreAllResourceGroupResources(ctx context.Context, collectId string, resourceGroupNames []string) []error - CollectAndStoreResources(ctx context.Context, collectId string, resourecGroupId string) []error - GetResourceGroup(ctx context.Context, collectId string, resourecGroupId string) (*pb.Resource, error) - ListResourceGroups(ctx context.Context, name string) ([]*pb.Resource, error) + CollectAndStoreAll(ctx context.Context, collectID string, resourceGroupNames []string, preCollectedRgs []*pb.Resource) error + + GetResourceGroupWithIamPolicy(ctx context.Context, collectID string, rgName string) (*pb.Resource, error) + ListResourceGroups(ctx context.Context, rgNames []string) ([]*pb.Resource, error) + ListResourceGroupsWithIamPolicies(ctx context.Context, rgNames []string) ([]*pb.Resource, error) ListResourceGroupNames(ctx context.Context) ([]string, error) - ListResourceGroupAdmins(ctx context.Context) (map[string]map[string]struct{}, error) - ListResourceGroupResources(ctx context.Context, collectId string, resourecGroup *pb.Resource) ([]*pb.Resource, []error) + ListResourceGroupAdmins(ctx context.Context) (ACLCache, error) + ListResourceGroupResources(ctx context.Context, collectID string, rgName string) ([]*pb.Resource, []error) + ListResourceGroupObservations(ctx context.Context, collectID string, rgName string) ([]*pb.Observation, []error) } diff --git a/src/model/engine.go b/src/model/engine.go new file mode 100644 index 0000000..9fbca55 --- /dev/null +++ b/src/model/engine.go @@ -0,0 +1,20 @@ +package model + +import ( + "context" + "encoding/json" + + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" +) + +type Engine interface { + GetChildren(ctx context.Context, parent string) ([]*pb.Resource, error) + GetResource(ctx context.Context, resourceName string) (*pb.Resource, error) + GetHierarchy(ctx context.Context, collectionID string) (map[string]*pb.RecursiveResource, error) + GetTagConfig() risk.TagConfig + + CheckRules(ctx context.Context, scanID string, collectID string, groups []string, preCollectedRGs []*pb.Resource) ([]*pb.Observation, []error) + GetRuleConfig(ctx context.Context, ruleName string) (json.RawMessage, error) + GetRules() []Rule +} diff --git a/src/model/exception.go b/src/model/exception.go index befbb1e..a415c55 100644 --- a/src/model/exception.go +++ b/src/model/exception.go @@ -5,11 +5,12 @@ import ( "golang.org/x/net/context" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/nianticlabs/modron/src/pb" + + pb "github.com/nianticlabs/modron/src/proto/generated" ) type Exception struct { - Uuid string `json:"uuid,omitempty"` + UUID string `json:"uuid,omitempty"` SourceSystem string `json:"sourceSystem,omitempty"` UserEmail string `json:"userEmail,omitempty"` NotificationName string `json:"notification_name,omitempty"` @@ -19,7 +20,7 @@ type Exception struct { } type Notification struct { - Uuid string `json:"uuid,omitempty"` + UUID string `json:"uuid,omitempty"` SourceSystem string `json:"sourceSystem,omitempty"` Name string `json:"name,omitempty"` Recipient string `json:"recipient,omitempty"` @@ -31,7 +32,7 @@ type Notification struct { func (e *Exception) ToProto() *pb.NotificationException { return &pb.NotificationException{ - Uuid: e.Uuid, + Uuid: e.UUID, SourceSystem: e.SourceSystem, UserEmail: e.UserEmail, NotificationName: e.NotificationName, @@ -43,7 +44,7 @@ func (e *Exception) ToProto() *pb.NotificationException { func ExceptionFromProto(p *pb.NotificationException) Exception { return Exception{ - Uuid: p.Uuid, + UUID: p.Uuid, SourceSystem: p.SourceSystem, UserEmail: p.UserEmail, NotificationName: p.NotificationName, @@ -54,6 +55,7 @@ func ExceptionFromProto(p *pb.NotificationException) Exception { } type NotificationService interface { + BatchCreateNotifications(ctx context.Context, notifications []Notification) ([]Notification, error) CreateNotification(ctx context.Context, notification Notification) (Notification, error) GetException(ctx context.Context, uuid string) (Exception, error) diff --git a/src/model/model.go b/src/model/model.go index ab68a99..47f5114 100644 --- a/src/model/model.go +++ b/src/model/model.go @@ -3,30 +3,38 @@ package model import ( "context" - "github.com/nianticlabs/modron/src/pb" + "google.golang.org/protobuf/proto" + + pb "github.com/nianticlabs/modron/src/proto/generated" ) -// Base interface to be implemented by rules. A `Rule` takes a resource, checks -// its observed values against an expected reference value, and creates an -// observation if it identifies a discrepancy, which may include a remediation for -// resolving it. +// Rule is the interface that is implemented by the rules. +// A `Rule` takes a resource, checks its observed values against an expected reference value, +// and creates an observation if it identifies a discrepancy, which may include a remediation for resolving it. type Rule interface { - // Performs a rule-dependent check on a resource and, in case it detects an anomaly, + // Check Performs a rule-dependent check on a resource and, in case it detects an anomaly, // returns a list of observations. The method MUST return nil in case either it did // not create any observations or detect any errors. - Check(ctx context.Context, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) - // Returns the associated `RuleInfo` data. + Check(ctx context.Context, engine Engine, rsrc *pb.Resource) (obs []*pb.Observation, errs []error) + // Info returns the associated RuleInfo data. Info() *RuleInfo } type RuleInfo struct { - // Human readable name of the rule, e.g., "EXPOSED_INFRASTRUCTURE_WITH_ADMIN_PRIVILEGES". + // Human-readable name of the rule, e.g., "EXPOSED_INFRASTRUCTURE_WITH_ADMIN_PRIVILEGES". Name string // Types of resource this rule accepts as an input to `Check`. This helps the rule engine // fetch in advance all the resources the rule needs to perform check(s) against. - AcceptedResourceTypes []string + AcceptedResourceTypes []proto.Message } type RuleEngine interface { - CheckRules(ctx context.Context, scanId string, resourceGroups []string) (obs []*pb.Observation, errs []error) + CheckRules( + ctx context.Context, + scanID string, + collectID string, + resourceGroups []string, + preCollectedRgs []*pb.Resource, + ) (obs []*pb.Observation, errs []error) + GetRules() []Rule } diff --git a/src/model/stateManager.go b/src/model/stateManager.go index dc92be0..768149b 100644 --- a/src/model/stateManager.go +++ b/src/model/stateManager.go @@ -1,16 +1,16 @@ package model import ( - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" ) type StateManager interface { - GetCollectState(collectId string) pb.RequestStatus - GetScanState(scanId string) pb.RequestStatus + GetCollectState(collectID string) pb.RequestStatus + GetScanState(scanID string) pb.RequestStatus - AddScan(scanId string, resourceGroupNames []string) []string - EndScan(scanId string, resourceGroupNames []string) + AddScan(scanID string, resourceGroupNames []string) []string + EndScan(scanID string, resourceGroupNames []string) - AddCollect(collectId string, resourceGroupNames []string) []string - EndCollect(collectId string, resourceGroupNames []string) + AddCollect(collectID string, resourceGroupNames []string) []string + EndCollect(collectID string, resourceGroupNames []string) } diff --git a/src/model/storage.go b/src/model/storage.go index 89145fd..9f17604 100644 --- a/src/model/storage.go +++ b/src/model/storage.go @@ -5,7 +5,7 @@ import ( "context" "time" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" ) type StorageFilter struct { @@ -25,8 +25,9 @@ type Storage interface { ListResources(ctx context.Context, filter StorageFilter) ([]*pb.Resource, error) BatchCreateObservations(ctx context.Context, observations []*pb.Observation) ([]*pb.Observation, error) ListObservations(ctx context.Context, filter StorageFilter) ([]*pb.Observation, error) + GetChildrenOfResource(ctx context.Context, collectID string, parentResourceName string, resourceType *string) (map[string]*pb.RecursiveResource, error) - AddOperationLog(ctx context.Context, ops []Operation) error + AddOperationLog(ctx context.Context, ops []*pb.Operation) error FlushOpsLog(ctx context.Context) error PurgeIncompleteOperations(ctx context.Context) error } diff --git a/src/nagatha/convert.go b/src/nagatha/convert.go index bc9f415..3c097cb 100644 --- a/src/nagatha/convert.go +++ b/src/nagatha/convert.go @@ -1,13 +1,15 @@ package nagatha import ( - "google.golang.org/protobuf/types/known/timestamppb" "github.com/nianticlabs/modron/src/model" + "github.com/nianticlabs/modron/src/proto/generated/nagatha" + + "google.golang.org/protobuf/types/known/timestamppb" ) -func exceptionModelFromNagathaProto(ex *Exception) model.Exception { +func exceptionModelFromNagathaProto(ex *nagatha.Exception) model.Exception { return model.Exception{ - Uuid: ex.Uuid, + UUID: ex.Uuid, SourceSystem: ex.SourceSystem, UserEmail: ex.UserEmail, NotificationName: ex.NotificationName, @@ -17,9 +19,9 @@ func exceptionModelFromNagathaProto(ex *Exception) model.Exception { } } -func exceptionNagathaProtoFromModel(ex model.Exception) *Exception { - return &Exception{ - Uuid: ex.Uuid, +func exceptionNagathaProtoFromModel(ex model.Exception) *nagatha.Exception { + return &nagatha.Exception{ + Uuid: ex.UUID, SourceSystem: ex.SourceSystem, UserEmail: ex.UserEmail, NotificationName: ex.NotificationName, diff --git a/src/nagatha/nagatha.go b/src/nagatha/nagatha.go index 798d8dd..ac8febb 100644 --- a/src/nagatha/nagatha.go +++ b/src/nagatha/nagatha.go @@ -2,47 +2,129 @@ package nagatha import ( "context" + "errors" "fmt" + "time" - "google.golang.org/protobuf/types/known/durationpb" + "github.com/nianticlabs/modron/src/constants" + modronmetric "github.com/nianticlabs/modron/src/metric" "github.com/nianticlabs/modron/src/model" + "github.com/nianticlabs/modron/src/proto/generated/nagatha" + + "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + + "golang.org/x/oauth2" + "google.golang.org/protobuf/types/known/durationpb" ) +var meter = otel.Meter("github.com/nianticlabs/modron/src/nagatha") +var tracer = otel.Tracer("github.com/nianticlabs/modron/src/nagatha") +var log = logrus.StandardLogger().WithField(constants.LogKeyPkg, "nagatha") + const ( sourceSystem = "modron" - clientID = "143415353591-bsmr7ii98a2493kts699289n2ommqi07.apps.googleusercontent.com" + batchSize = 1000 ) -func New(ctx context.Context, addr string) (model.NotificationService, error) { - c, err := NewNagathaClient(addr) +func New(addr, modronURL string, tokenSource oauth2.TokenSource) (model.NotificationService, error) { + c, err := newNagathaClient(addr, tokenSource) if err != nil { return nil, err } - return &Service{ - client: c, - }, nil + s := &Service{ + client: c, + modronURL: modronURL, + } + err = s.initMetrics() + return s, err } type Service struct { - model.NotificationService - client *NagathaClient + client *Client + modronURL string + metrics metrics +} + +type metrics struct { + ClientReqDuration metric.Float64Histogram + NotificationDuration metric.Float64Histogram } func (svc *Service) CreateNotification(ctx context.Context, notification model.Notification) (model.Notification, error) { + ctx, span := tracer.Start(ctx, "CreateNotification") + defer span.End() + start := time.Now() if notification.Name == "" { return model.Notification{}, fmt.Errorf("name can't be empty") } - err := svc.client.CreateNotification(ctx, &Notification{ + notif, err := svc.client.CreateNotification(ctx, &nagatha.Notification{ SourceSystem: sourceSystem, Name: notification.Name, - UserEmail: notification.Recipient, + Recipient: notification.Recipient, Content: notification.Content, Interval: durationpb.New(notification.Interval), }) + status := modronmetric.StatusSuccess if err != nil { - return model.Notification{}, err + status = modronmetric.StatusError + } + svc.metrics.NotificationDuration. + Record(ctx, time.Since(start).Seconds(), + metric.WithAttributes( + attribute.String(modronmetric.KeyStatus, status), + attribute.String(modronmetric.KeyRecipient, notification.Recipient), + ), + ) + return notif, err +} + +func (svc *Service) BatchCreateNotifications(ctx context.Context, notifications []model.Notification) ([]model.Notification, error) { + ctx, span := tracer.Start(ctx, "BatchCreateNotifications") + defer span.End() + start := time.Now() + var resultNotifications []model.Notification + var errArr []error + status := "success" + for i := 0; i < len(notifications); i += batchSize { + end := i + batchSize + if end > len(notifications) { + end = len(notifications) + } + notificationsProto := make([]*nagatha.Notification, 0, end-i) + for _, n := range notifications[i:end] { + if n.Recipient == "" { + log.Warnf("recipient empty for notification %s", n.Name) + continue + } + notificationsProto = append(notificationsProto, &nagatha.Notification{ + SourceSystem: sourceSystem, + Name: n.Name, + Recipient: n.Recipient, + Content: n.Content, + Interval: durationpb.New(n.Interval), + }) + } + notif, err := svc.client.BatchCreateNotifications(ctx, notificationsProto) + if err != nil { + status = "error" + log.Warnf("error creating notifications: %v", err) + errArr = append(errArr, err) + continue + } + resultNotifications = append(resultNotifications, notif...) + log.Infof("%d notifications remaining", len(notifications)-end) } - return model.Notification{}, nil + svc.metrics.NotificationDuration. + Record(ctx, time.Since(start).Seconds(), + metric.WithAttributes( + attribute.String(modronmetric.KeyStatus, status), + attribute.Int(modronmetric.KeyCount, len(notifications)), + ), + ) + return resultNotifications, errors.Join(errArr...) } func (svc *Service) GetException(ctx context.Context, uuid string) (model.Exception, error) { @@ -76,3 +158,23 @@ func (svc *Service) ListExceptions(ctx context.Context, userEmail string, pageSi } return exceptions, nil } + +func (svc *Service) initMetrics() error { + notificationDurationHistogram, err := meter.Float64Histogram(constants.MetricsPrefix + "notifications_sent_total") + if err != nil { + return err + } + clientReqDurationHist, err := meter.Float64Histogram( + constants.MetricsPrefix+"client_requests_duration", + metric.WithDescription("Duration of client requests in seconds"), + metric.WithUnit("s"), + ) + if err != nil { + return err + } + svc.metrics = metrics{ + NotificationDuration: notificationDurationHistogram, + ClientReqDuration: clientReqDurationHist, + } + return nil +} diff --git a/src/nagatha/notification.go b/src/nagatha/notification.go new file mode 100644 index 0000000..ca666b5 --- /dev/null +++ b/src/nagatha/notification.go @@ -0,0 +1,39 @@ +package nagatha + +import ( + "time" + + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/proto/generated/nagatha" +) + +func NotificationFromObservation(contact string, interval time.Duration, obs *pb.Observation) model.Notification { + return model.Notification{ + SourceSystem: "modron", + Name: obs.Name, + Recipient: contact, + Content: formatNotificationContent(obs), + Interval: interval, + } +} + +func notificationFromProto(p *nagatha.Notification) model.Notification { + return model.Notification{ + UUID: p.Uuid, + SourceSystem: p.SourceSystem, + Name: p.Name, + Recipient: p.Recipient, + Content: p.Content, + CreatedOn: p.CreatedOn.AsTime(), + SentOn: p.SentOn.AsTime(), + Interval: p.Interval.AsDuration(), + } +} + +func formatNotificationContent(obs *pb.Observation) string { + var out string + out += obs.Remediation.Description + "\n\n" + out += obs.Remediation.Recommendation + " \n \n" + return out +} diff --git a/src/nagatha/notification_test.go b/src/nagatha/notification_test.go new file mode 100644 index 0000000..0532220 --- /dev/null +++ b/src/nagatha/notification_test.go @@ -0,0 +1,43 @@ +package nagatha_test + +import ( + "testing" + "time" + + "github.com/gogo/protobuf/proto" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/model" + "github.com/nianticlabs/modron/src/nagatha" + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +func TestNotificationFromObservation(t *testing.T) { + pbObs := pb.Observation{ + Uid: "47fc94f3-b6e9-4ae5-b719-c8dd2157744b", + ScanUid: proto.String("2f86b47d-a386-4f4e-88a1-786f2a572ad8"), + Timestamp: timestamppb.New(time.Now()), + ResourceRef: &pb.ResourceRef{ + Uid: proto.String("79bc4bd2-a454-4837-8a09-0d769cb36d0f"), + GroupName: "projects/some-project", + }, + Name: "DATABASE_ALLOWS_UNENCRYPTED_CONNECTIONS", + Remediation: &pb.Remediation{ + Description: "Database example-psql allows for unencrypted connections.", + Recommendation: "Enable the require SSL setting in the database settings to allow only encrypted connections to example-psql.", + }, + } + + want := model.Notification{ + SourceSystem: "modron", + Name: "DATABASE_ALLOWS_UNENCRYPTED_CONNECTIONS", + Content: "Database example-psql allows for unencrypted connections.\n\nEnable the require SSL setting in the database settings to allow only encrypted connections to example-psql. \n \n", + Recipient: "test@example.com", + Interval: 24 * time.Hour, + } + got := nagatha.NotificationFromObservation("test@example.com", 24*time.Hour, &pbObs) + if diff := cmp.Diff(&got, &want); diff != "" { + t.Errorf("NotificationFromObservation() mismatch (-got +want):\n%s", diff) + } +} diff --git a/src/nagatha/proto/nagatha.proto b/src/nagatha/proto/nagatha.proto new file mode 100644 index 0000000..a189640 --- /dev/null +++ b/src/nagatha/proto/nagatha.proto @@ -0,0 +1,167 @@ +syntax = "proto3"; + +// You may want to read https://google.aip.dev/general first. + +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/longrunning/operations.proto"; + +option go_package = "./nagatha"; +package com.nianticlabs.nagatha; + +message Exception { + string uuid = 1; + string source_system = 2; + string user_email = 3; + string notification_name = 4; + string justification = 5; + google.protobuf.Timestamp created_on_time = 6; + google.protobuf.Timestamp valid_until_time = 7; +} + +message Notification { + string uuid = 1; + string source_system = 2; + string name = 3; + string recipient = 4; + string content = 5; + google.protobuf.Timestamp created_on = 6; + google.protobuf.Timestamp sent_on = 7; + google.protobuf.Duration interval = 8; +} + +message BatchCreateNotificationsRequest { + repeated Notification notifications = 1; +} + +message BatchCreateNotificationsResponse { + repeated Notification notifications = 1; +} + +service Nagatha { + rpc CreateNotification(CreateNotificationRequest) returns (Notification) { + option (google.api.http) = { + post : "/v2/notifications" + body : "notification" + }; + }; + + rpc BatchCreateNotifications(BatchCreateNotificationsRequest) returns (google.longrunning.Operation) { + option (google.api.http) = { + post : "/v2/notifications:batchCreate" + body : "*" + }; + option (google.longrunning.operation_info) = { + response_type : "BatchCreateNotificationsResponse" + metadata_type : "OperationMetadata" + }; + }; + + rpc GetException(GetExceptionRequest) returns (Exception) { + option (google.api.http) = { + get : "/v2/exceptions/{uuid}" + }; + }; + rpc CreateException(CreateExceptionRequest) returns (Exception) { + option (google.api.http) = { + post : "/v2/exceptions" + body : "exception" + }; + }; + rpc UpdateException(UpdateExceptionRequest) returns (Exception) { + option (google.api.http) = { + patch : "/v2/exceptions/{exception.uuid}" + body : "exception" + }; + }; + rpc DeleteException(DeleteExceptionRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete : "/v2/exceptions/{uuid}" + }; + }; + rpc ListExceptions(ListExceptionsRequest) returns (ListExceptionsResponse) { + option (google.api.http) = { + get : "/v2/exceptions" + }; + }; + + rpc NotifyUser(NotifyUserRequest) returns (NotifyUserResponse) { + option (google.api.http) = { + post : "/v2/notifyUser" + body : "*" + }; + }; + rpc NotifyAll(NotifyAllRequest) returns (google.longrunning.Operation) { + option (google.api.http) = { + get : "/v2/notifyAll" + }; + option (google.longrunning.operation_info) = { + response_type : "NotifyAllResponse" + metadata_type : "OperationMetadata" + }; + }; + + rpc ListOperations(google.longrunning.ListOperationsRequest) returns (google.longrunning.ListOperationsResponse) { + option (google.api.http) = { + post: "/v2/{name=operations}" + }; + option (google.api.method_signature) = "name,filter"; + } + + // Gets the latest state of a long-running operation. Clients can use this + // method to poll the operation result at intervals as recommended by the API + // service. + rpc GetOperation(google.longrunning.GetOperationRequest) returns (google.longrunning.Operation) { + option (google.api.http) = { + get: "/v2/{name=operations/**}" + }; + option (google.api.method_signature) = "name"; + } +} + +message CreateNotificationRequest { Notification notification = 1; } + +message GetExceptionRequest { string uuid = 1; } + +message CreateExceptionRequest { Exception exception = 1; } + +message UpdateExceptionRequest { + Exception exception = 1; + + google.protobuf.FieldMask update_mask = 2; +} + +message DeleteExceptionRequest { string uuid = 1; } + +message ListExceptionsRequest { + string user_email = 1; + + int32 page_size = 2; + + string page_token = 3; +} + +message ListExceptionsResponse { + repeated Exception exceptions = 1; + + string next_page_token = 2; +} + +message NotifyAllRequest {} + +// NotifyAll is a long running operation. +// https://google.aip.dev/151 +message NotifyAllResponse { bool has_completed = 1; } + +message NotifyUserRequest { + string user_email = 1; + string title = 2; + string content = 3; + string source_system = 4; +} + +message NotifyUserResponse {} diff --git a/src/nagatha/rest.go b/src/nagatha/rest.go index 7651d25..321e1a6 100644 --- a/src/nagatha/rest.go +++ b/src/nagatha/rest.go @@ -2,38 +2,62 @@ package nagatha import ( "bytes" - "context" "crypto/tls" "crypto/x509" "fmt" "io" "net/http" + "net/url" "time" - "github.com/golang/glog" - "google.golang.org/api/idtoken" + "cloud.google.com/go/longrunning/autogen/longrunningpb" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + "golang.org/x/net/context" + "golang.org/x/oauth2" "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" + + "github.com/nianticlabs/modron/src/constants" + modronmetric "github.com/nianticlabs/modron/src/metric" + "github.com/nianticlabs/modron/src/model" + "github.com/nianticlabs/modron/src/proto/generated/nagatha" ) type ContextKey string const ( authorizationHeader = "Authorization" + httpRequestTimeout = 10 * time.Second + apiVersion = "v2" ) -type NagathaClient struct { - client *http.Client +type clientMetrics struct { + RequestDuration metric.Float64Histogram +} - addr string +type Client struct { + addr string + client *http.Client + tokenSource oauth2.TokenSource + metrics clientMetrics } var ( opts = make([]http.Transport, 0) ) -func NewNagathaClient(addr string) (*NagathaClient, error) { +func apiPath(path string) string { + return "/" + apiVersion + path +} + +func newNagathaClient(addr string, tokenSource oauth2.TokenSource) (*Client, error) { + if tokenSource == nil { + return nil, fmt.Errorf("tokenSource cannot be nil") + } cp, err := x509.SystemCertPool() if err != nil { return nil, fmt.Errorf("cert pool: %w", err) @@ -45,131 +69,229 @@ func NewNagathaClient(addr string) (*NagathaClient, error) { MinVersion: tls.VersionTLS13, }, }) - return &NagathaClient{ - client: &http.Client{Timeout: 10 * time.Second}, - addr: addr, - }, nil + c := &Client{ + client: &http.Client{ + Timeout: httpRequestTimeout, + Transport: otelhttp.NewTransport(http.DefaultTransport), + }, + addr: addr, + tokenSource: tokenSource, + } + err = c.initMetrics() + return c, err +} + +func (c *Client) CreateNotification(ctx context.Context, notification *nagatha.Notification) (model.Notification, error) { + var resultNotification nagatha.Notification + err := c.sendRequest(ctx, http.MethodPost, apiPath("/notifications"), notification, &resultNotification) + return notificationFromProto(&resultNotification), err +} + +func (c *Client) BatchCreateNotifications(ctx context.Context, notifications []*nagatha.Notification) ([]model.Notification, error) { + ctx, span := tracer.Start(ctx, "BatchCreateNotifications") + defer span.End() + var resp longrunningpb.Operation + err := c.sendRequest(ctx, + http.MethodPost, + apiPath("/notifications:batchCreate"), + &nagatha.BatchCreateNotificationsRequest{ + Notifications: notifications, + }, + &resp, + ) + if err != nil { + return nil, err + } + operationID := resp.Name + + deadline := time.Now().Add(5 * time.Minute) //nolint:mnd + for { + if time.Now().After(deadline) { + return nil, fmt.Errorf("operation %q timeout", operationID) + } + op, err := c.GetOperation(ctx, &longrunningpb.GetOperationRequest{ + Name: operationID, + }) + if err != nil { + return nil, fmt.Errorf("get operation %q: %w", operationID, err) + } + if op.Done { + if op.GetError() != nil { + return nil, fmt.Errorf("operation %q failed: %v", operationID, op.GetError()) + } + var result nagatha.BatchCreateNotificationsResponse + if err := op.GetResponse().UnmarshalTo(&result); err != nil { + return nil, fmt.Errorf("unmarshal response: %w", err) + } + var resultNotifications []model.Notification + for _, n := range result.Notifications { + resultNotifications = append(resultNotifications, notificationFromProto(n)) + } + return resultNotifications, nil + } + log.Debugf("waiting for operation %q to complete", operationID) + time.Sleep(1 * time.Second) + } } -func (c *NagathaClient) CreateNotification(ctx context.Context, notification *Notification) error { - return c.sendRequest(ctx, http.MethodPost, "/v1/notification", notification, nil) +func (c *Client) GetOperation(ctx context.Context, req *longrunningpb.GetOperationRequest) (*longrunningpb.Operation, error) { + var operation longrunningpb.Operation + err := c.sendRequest(ctx, http.MethodGet, apiPath("/"+url.PathEscape(req.Name)), nil, &operation) + return &operation, err } -func (c *NagathaClient) GetException(ctx context.Context, uuid string) (*Exception, error) { - request := &GetExceptionRequest{ +func (c *Client) GetException(ctx context.Context, uuid string) (*nagatha.Exception, error) { + request := &nagatha.GetExceptionRequest{ Uuid: uuid, } - response := &Exception{} - if err := c.sendRequest(ctx, http.MethodGet, "/v1/exception", request, response); err != nil { + response := &nagatha.Exception{} + if err := c.sendRequest(ctx, http.MethodGet, apiPath("/exceptions/"+url.PathEscape(uuid)), request, response); err != nil { return nil, err } return response, nil } -func (c *NagathaClient) CreateException(ctx context.Context, exception *Exception) error { - return c.sendRequest(ctx, http.MethodPost, "/v1/exception", exception, nil) +func (c *Client) CreateException(ctx context.Context, exception *nagatha.Exception) error { + return c.sendRequest(ctx, http.MethodPost, apiPath("/exceptions"), exception, nil) } -func (c *NagathaClient) UpdateException(ctx context.Context, exception *Exception, updateMask *field_mask.FieldMask) error { - request := &UpdateExceptionRequest{ +func (c *Client) UpdateException(ctx context.Context, exception *nagatha.Exception, updateMask *field_mask.FieldMask) error { + request := &nagatha.UpdateExceptionRequest{ Exception: exception, UpdateMask: updateMask, } - return c.sendRequest(ctx, http.MethodPatch, "/v1/exception", request, nil) + return c.sendRequest(ctx, http.MethodPatch, apiPath("/exceptions/"+url.PathEscape(exception.Uuid)), request, nil) } -func (c *NagathaClient) DeleteException(ctx context.Context, uuid string) error { - request := &DeleteExceptionRequest{ +func (c *Client) DeleteException(ctx context.Context, uuid string) error { + request := &nagatha.DeleteExceptionRequest{ Uuid: uuid, } - return c.sendRequest(ctx, http.MethodDelete, "/v1/exception", request, nil) + return c.sendRequest(ctx, http.MethodDelete, apiPath("/exceptions/"+url.PathEscape(uuid)), request, nil) } -func (c *NagathaClient) ListExceptions(ctx context.Context, userEmail string, pageSize int32, pageToken string) (*ListExceptionsResponse, error) { - request := &ListExceptionsRequest{ +func (c *Client) ListExceptions(ctx context.Context, userEmail string, pageSize int32, pageToken string) (*nagatha.ListExceptionsResponse, error) { + request := &nagatha.ListExceptionsRequest{ UserEmail: userEmail, PageSize: pageSize, PageToken: pageToken, } - response := &ListExceptionsResponse{} - if err := c.sendRequest(ctx, http.MethodGet, "/v1/exceptions", request, response); err != nil { + response := &nagatha.ListExceptionsResponse{} + if err := c.sendRequest(ctx, http.MethodGet, apiPath("/exceptions"), request, response); err != nil { return nil, err } return response, nil } -func (c *NagathaClient) sendRequest(ctx context.Context, method, path string, request proto.Message, response proto.Message) error { +type RequestError struct { + StatusCode int + Message string +} + +func (r RequestError) Error() string { + return fmt.Sprintf("request failed with status code %d: %s", r.StatusCode, r.Message) +} + +var _ error = (*RequestError)(nil) + +func (c *Client) sendRequest(ctx context.Context, method, path string, request, response proto.Message) error { + ctx, span := tracer.Start(ctx, "sendRequest", + trace.WithAttributes( + attribute.String(constants.TraceKeyMethod, method), + attribute.String(constants.TraceKeyPath, path), + ), + ) + defer span.End() addr := c.addr + path var httpRequest *http.Request var requestBody []byte var err error + reqStart := time.Now() // Serialize request message to JSON if method == http.MethodPost { requestBody, err = protoToJSON(request) if err != nil { - return fmt.Errorf("failed to serialize request message: %v", err) + return fmt.Errorf("failed to serialize request message: %w", err) } - glog.V(10).Infof("nagatha request: %+v", string(requestBody)) - httpRequest, err = http.NewRequest(method, addr, bytes.NewReader(requestBody)) + log.Tracef("nagatha request: %+v", string(requestBody)) + httpRequest, err = http.NewRequestWithContext(ctx, method, addr, bytes.NewReader(requestBody)) if err != nil { - return fmt.Errorf("failed to create HTTP request: %v", err) + return fmt.Errorf("failed to create HTTP request: %w", err) } httpRequest.Header.Set("Content-Type", "application/json") } else if method == http.MethodGet { - httpRequest, err = http.NewRequest(method, addr, nil) + httpRequest, err = http.NewRequestWithContext(ctx, method, addr, nil) if err != nil { - return fmt.Errorf("failed to create HTTP request: %v", err) + return fmt.Errorf("failed to create HTTP request: %w", err) } } - httpResponse, err := c.client.Do(addAuthentication(ctx, httpRequest)) + httpRequest = c.addAuthentication(httpRequest) + httpResponse, err := c.client.Do(httpRequest) if err != nil { - return fmt.Errorf("HTTP request failed: %v", err) + return fmt.Errorf("HTTP request failed: %w", err) } + defer func() { + c.metrics.RequestDuration. + Record(ctx, time.Since(reqStart).Seconds(), + metric.WithAttributes( + attribute.Int(modronmetric.KeyStatus, httpResponse.StatusCode), + attribute.String(modronmetric.KeyMethod, method), + attribute.String(modronmetric.KeyPath, path), + ), + ) + }() defer httpResponse.Body.Close() // Read response body responseBody, err := io.ReadAll(httpResponse.Body) if err != nil { - return fmt.Errorf("failed to read response body: %v", err) + return fmt.Errorf("failed to read response body: %w", err) } // Check HTTP status code if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 { - return fmt.Errorf("request failed with status code %d: %s", httpResponse.StatusCode, string(responseBody)) + return RequestError{ + StatusCode: httpResponse.StatusCode, + Message: string(responseBody), + } } // Parse response JSON if response != nil { if err := protojson.Unmarshal(responseBody, response); err != nil { - return fmt.Errorf("failed to parse response JSON: %v", err) + return fmt.Errorf("failed to parse response JSON: %w", err) } } return nil } -func protoToJSON(message proto.Message) ([]byte, error) { - return protojson.Marshal(message) +func (c *Client) addAuthentication(request *http.Request) *http.Request { + token, err := c.tokenSource.Token() + if err != nil { + log.Errorf("TokenSource.Token: %v", err) + } else { + request.Header.Set(authorizationHeader, "Bearer "+token.AccessToken) + } + return request } -func addAuthentication(ctx context.Context, req *http.Request) *http.Request { - // Create an identity token. - // With a global TokenSource tokens would be reused and auto-refreshed at need. - // A given TokenSource is specific to the audience. - tokenSource, err := idtoken.NewTokenSource(ctx, clientID) +func (c *Client) initMetrics() error { + clientReqDurationHist, err := meter.Float64Histogram( + constants.MetricsPrefix+"nagatha_client_request_duration_seconds", + metric.WithDescription("Duration of Nagatha client requests"), + ) if err != nil { - glog.Warningf("idtoken.NewTokenSource: %v", err) - } else { - token, err := tokenSource.Token() - if err != nil { - glog.Warningf("TokenSource.Token: %v", err) - } else { - req.Header.Set(authorizationHeader, "Bearer "+token.AccessToken) - return req - } + return err + } + c.metrics = clientMetrics{ + RequestDuration: clientReqDurationHist, } - glog.Warningf("no authentication added for context") - return req + return nil +} + +func protoToJSON(message proto.Message) ([]byte, error) { + return protojson.Marshal(message) } diff --git a/src/nagatha/rest_test.go b/src/nagatha/rest_test.go new file mode 100644 index 0000000..d072cb6 --- /dev/null +++ b/src/nagatha/rest_test.go @@ -0,0 +1,163 @@ +package nagatha_test + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/h2non/gock" + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" + + "github.com/nianticlabs/modron/src/model" + "github.com/nianticlabs/modron/src/nagatha" +) + +const testEndpoint = "https://nagatha.localhost" + +func mockNagathaCreateNotification() func() { + gock.New(testEndpoint). + Post("/v2/notifications"). + MatchHeader("Authorization", "Bearer hunter2"). + Reply(200). + JSON(map[string]any{"uuid": "notification-uuid"}) + return gock.Off +} + +func mockNagathaCreateBatchNotifications() func() { + gock.New(testEndpoint). + Post("/v2/notifications:batchCreate"). + MatchHeader("Authorization", "Bearer hunter2"). + Reply(200). + JSON(map[string]any{ + "name": "operations/my-operation-1", + "done": false, + }) + gock.New(testEndpoint). + Get("/v2/operations/my-operation-1"). + MatchHeader("Authorization", "Bearer hunter2"). + Reply(200). + JSON(map[string]any{ + "name": "my-operation-1", + "done": false, + }) + gock.New(testEndpoint). + Get("/v2/operations/my-operation-1"). + MatchHeader("Authorization", "Bearer hunter2"). + Reply(200). + JSON(map[string]any{ + "name": "my-operation-1", + "done": false, + }) + gock.New(testEndpoint). + Get("/v2/operations/my-operation-1"). + MatchHeader("Authorization", "Bearer hunter2"). + Reply(200). + JSON(map[string]any{ + "name": "operations/my-operation-1", + "done": true, + "response": map[string]any{ + "@type": "com.nianticlabs.nagatha.BatchCreateNotificationsResponse", + "notifications": []map[string]any{ + { + "uuid": "notification-uuid-1", + }, + { + "uuid": "notification-uuid-2", + }, + }, + }, + }) + return gock.Off +} + +func getClient(t *testing.T) model.NotificationService { + t.Helper() + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "hunter2"}) + c, err := nagatha.New(testEndpoint, "modron.localhost", ts) + if err != nil { + t.Fatalf("New: %v", err) + } + return c +} +func TestClient_CreateNagathaNotification_WithToken(t *testing.T) { + ctx := context.Background() + off := mockNagathaCreateNotification() + defer off() + + c := getClient(t) + got, err := c.CreateNotification(ctx, model.Notification{ + SourceSystem: "modron", + Name: "this is a test", + Recipient: "user@example.com", + Content: "notification content", + CreatedOn: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + Interval: 24 * time.Hour, + }) + if err != nil { + t.Fatalf("CreateNotification: %v", err) + } + want := model.Notification{ + UUID: "notification-uuid", + CreatedOn: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), + SentOn: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("notification mismatch (-want, +got): %s", diff) + } +} + +func TestClient_BatchCreateNotifications(t *testing.T) { + ctx := context.Background() + off := mockNagathaCreateBatchNotifications() + defer off() + logrus.StandardLogger().SetLevel(logrus.DebugLevel) + + c := getClient(t) + notifications := []model.Notification{ + { + SourceSystem: "modron", + Name: "this is a test", + Recipient: "user@example.com", + Content: "notification content", + CreatedOn: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "this is another test", + Recipient: "user@example.com", + Content: "notification content", + CreatedOn: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + Interval: 24 * time.Hour, + }, + } + got, err := c.BatchCreateNotifications(ctx, notifications) + if err != nil { + t.Fatalf("BatchCreateNotifications: %v", err) + } + + want := []model.Notification{ + { + UUID: "notification-uuid-1", + SentOn: time.UnixMilli(0), + CreatedOn: time.UnixMilli(0), + }, + { + UUID: "notification-uuid-2", + SentOn: time.UnixMilli(0), + CreatedOn: time.UnixMilli(0), + }, + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("notifications mismatch (-want, +got):\n%s", diff) + } +} + +func TestClient_CreateNagathaNotification_WithoutToken(t *testing.T) { + if _, err := nagatha.New(testEndpoint, "modron.localhost", nil); err == nil { + t.Fatalf("nagatha.New: got nil, want error") + } +} diff --git a/src/proto/.gitignore b/src/proto/.gitignore new file mode 100644 index 0000000..bf9aea0 --- /dev/null +++ b/src/proto/.gitignore @@ -0,0 +1,3 @@ +generated/* +!generated/go.mod +!generated/go.sum diff --git a/src/proto/generated/go.mod b/src/proto/generated/go.mod new file mode 100644 index 0000000..689f8bb --- /dev/null +++ b/src/proto/generated/go.mod @@ -0,0 +1,33 @@ +module github.com/nianticlabs/modron/src/proto/generated + +go 1.23.2 + +require ( + cloud.google.com/go/longrunning v0.6.1 + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 + google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.35.1 + k8s.io/api v0.31.2 + k8s.io/apimachinery v0.31.2 +) + +require ( + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect +) diff --git a/src/proto/generated/go.sum b/src/proto/generated/go.sum new file mode 100644 index 0000000..42446b9 --- /dev/null +++ b/src/proto/generated/go.sum @@ -0,0 +1,129 @@ +cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= +cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= +k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= +k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= +k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= +k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/src/proto/modron.proto b/src/proto/modron.proto index bc84bff..3e9da6f 100644 --- a/src/proto/modron.proto +++ b/src/proto/modron.proto @@ -11,32 +11,85 @@ import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; -option go_package = "./pb"; +import "k8s.io/api/core/v1/generated.proto"; +import "k8s.io/apimachinery/pkg/apis/meta/v1/generated.proto"; -message ExportedCredentials { +option go_package = "./"; + +message APIKey {repeated string scopes = 1;} + +// TODO: Consider adding the following: +// - Object versioning policy +message Bucket { + // Object retention policy. + message RetentionPolicy { + // The duration for which objects in the bucket need to be retained. + google.protobuf.Duration period = 1; + // If true, the policy cannot be modified. + bool is_locked = 2; + } + // Server Side Encryption (SSE) policy. + message EncryptionPolicy { + // If true, SSE is enabled for the bucket. Note that SSE is always enabled + // in GCP. + bool is_enabled = 1; + // If true, a Customer-Managed Key (CMK) is used to encrypt objects in the + // bucket instead of a default key provided by a platform Key Management + // Service (KMS). + bool is_key_customer_managed = 2; + } + enum AccessType { + ACCESS_UNKNOWN = 0; + PRIVATE = 1; + PUBLIC = 2; + } + enum AccessControlType { + ACCESS_CONTROL_UNKNOWN = 0; + NON_UNIFORM = 1; + UNIFORM = 2; + } google.protobuf.Timestamp creation_date = 1; - google.protobuf.Timestamp expiration_date = 2; - google.protobuf.Timestamp last_usage = 3; + // The retention policy for objects in the bucket. + optional RetentionPolicy retention_policy = 2; + // The SSE policy for the bucket. + optional EncryptionPolicy encryption_policy = 3; + // If true, the bucket is publicly accessible. + AccessType access_type = 4; + // If true, Access Control Lists (ACLs) are enabled for the bucket. In GCP, + // this entails that uniform bucket-level access is disabled. + AccessControlType access_control_type = 5; } -message VmInstance { - string public_ip = 1; - string private_ip = 2; - // ServiceAccount.Name - string identity = 3; -} +message Certificate { + enum Type { + UNKNOWN = 0; + // Certificate managed by the user and imported into the platform. + IMPORTED = 1; + // Certificate managed by the platform. + MANAGED = 2; + } + Type type = 1; -message Network { - repeated string ips = 1; - bool gcp_private_google_access_v4 = 2; -} + // Fully-qualified domain name bound to the certificate. + string domain_name = 2; -message KubernetesCluster { - repeated string master_authorized_networks = 1; - bool private_cluster = 2; - string master_version = 3; - string nodes_version = 4; - string location = 5; + // The list of alternative domain names bound to the certificate. + // See https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6. + repeated string subject_alternative_names = 3; + + google.protobuf.Timestamp creation_date = 4; + google.protobuf.Timestamp expiration_date = 5; + + // The name of the certificate authority that issued the certificate. + string issuer = 6; + + // The algorithm that was used by the issuer to sign the certificate. + string signature_algorithm = 7; + + // The chain starts with the leaf certificate and continues with the + // remaining endorsing certificates in the chain of trust, if any. + // See https://datatracker.ietf.org/doc/html/rfc1421.html. + string pem_certificate_chain = 8; } message Database { @@ -94,6 +147,12 @@ message Database { bool is_public = 12; } +message ExportedCredentials { + google.protobuf.Timestamp creation_date = 1; + google.protobuf.Timestamp expiration_date = 2; + google.protobuf.Timestamp last_usage = 3; +} + message IamGroup { message EntityKey { @@ -144,96 +203,50 @@ message IamGroup { optional DynamicGroupMetadata dynamic_group_metadata = 9; } -// TODO: Consider adding the following: -// - Object versioning policy -message Bucket { - // Object retention policy. - message RetentionPolicy { - // The duration for which objects in the bucket need to be retained. - google.protobuf.Duration period = 1; - // If true, the policy cannot be modified. - bool is_locked = 2; - } - // Server Side Encryption (SSE) policy. - message EncryptionPolicy { - // If true, SSE is enabled for the bucket. Note that SSE is always enabled - // in GCP. - bool is_enabled = 1; - // If true, a Customer-Managed Key (CMK) is used to encrypt objects in the - // bucket instead of a default key provided by a platform Key Management - // Service (KMS). - bool is_key_customer_managed = 2; - } - enum AccessType { - ACCESS_UNKNOWN = 0; - PRIVATE = 1; - PUBLIC = 2; - } - enum AccessControlType { - ACCESS_CONTROL_UNKNOWN = 0; - NON_UNIFORM = 1; - UNIFORM = 2; - } - google.protobuf.Timestamp creation_date = 1; - // The retention policy for objects in the bucket. - optional RetentionPolicy retention_policy = 2; - // The SSE policy for the bucket. - optional EncryptionPolicy encryption_policy = 3; - // If true, the bucket is publicly accessible. - AccessType access_type = 4; - // If true, Access Control Lists (ACLs) are enabled for the bucket. In GCP, - // this entails that uniform bucket-level access is disabled. - AccessControlType access_control_type = 5; -} - -message APIKey { repeated string scopes = 1; } - -message Permission { - string role = 1; - repeated string principals = 2; -} - message IamPolicy { // Resource this IAM policy is attached to. Resource resource = 1; repeated Permission permissions = 2; } -message SslPolicy { - enum MinTlsVersion { - MinTlsVersion_UNKNOWN = 0; - TLS_1_0 = 1; - TLS_1_1 = 2; - TLS_1_2 = 3; - TLS_1_3 = 4; - } - enum Profile { - Profile_UNKNOWN = 0; - COMPATIBLE = 1; - MODERN = 2; - RESTRICTED = 3; - CUSTOM = 4; - } - google.protobuf.Timestamp creation_date = 1; - string name = 2; - Profile profile = 3; - MinTlsVersion minTlsVersion = 4; - repeated string enabledFeatures = 5; - repeated string customFeatures = 6; -} +message KubernetesCluster { + repeated string master_authorized_networks = 1; + bool private_cluster = 2; + string master_version = 3; + string nodes_version = 4; + string location = 5; -message ServiceAccount { - repeated ExportedCredentials exported_credentials = 1; -} + message Security { + enum VulnScanning { + /* + This enum is used to represent whether Vulnerability Scanning is enabled in the Kubernetes cluster. + Some Cloud Providers provide an option to do scanning at the cluster level (e.g: GCP w/ GKE), others + provide it at the registry level (e.g: AWS ECR image scanning, Azure Defender for Containers). + + This enum is purely related to the scanning capabilities at the cluster level. + We consider "BASIC" the bare minimum check provided by the cloud provider, this is for example a check + for OS vulnerabilities in the container image. + We consider "ADVANCED" to be a more in-depth check of the container image, for example by also scanning + dependencies (e.g: Go dependencies) for known vulnerabilities. + + GCP uses the "Standard" (here BASIC) and "Enterprise" (here ADVANCED) tiers for this. + If a cloud provider has more than these tiers, we consider everything that is not the maximum tier to be "BASIC". + Of course we can extend this enum if needed, but for the moment the current distinction should be enough. + + AWS: https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html (not exactly the same, registry-level) + GCP: https://cloud.google.com/kubernetes-engine/docs/how-to/security-posture-vulnerability-scanning#vuln-scanning-tiers + Azure: https://learn.microsoft.com/en-us/azure/defender-for-cloud/defender-for-containers-enable or https://learn.microsoft.com/en-us/azure/defender-for-cloud/support-matrix-defender-for-containers#vulnerability-assessment + */ + VULN_SCAN_UNKNOWN = 0; + VULN_SCAN_DISABLED = 1; + VULN_SCAN_BASIC = 2; // GCP: Standard, AWS: Basic + VULN_SCAN_ADVANCED = 3; // GCP: Enterprise, AWS: Enhanced + } -// ResourceGroup designates the smallest administrative grouping of resources. -message ResourceGroup { - // Environment describes the environment of this resource group. For instance - // prod, dev, etc. - string environment = 1; - // Number describes an ID used by the platform to identify the Resource Group. - // In GCP this is the project number. - string identifier = 2; + VulnScanning vulnerability_scanning = 1; + } + + Security security = 6; } message LoadBalancer { @@ -246,38 +259,158 @@ message LoadBalancer { Type type = 1; repeated Certificate certificates = 2; SslPolicy sslPolicy = 3; + IAP iap = 4; } -message Certificate { - enum Type { +message IAP { + bool enabled = 1; + string cliend_id = 2; +} + +message Namespace { + string cluster = 1; + google.protobuf.Timestamp creation_time = 2; +} + +message Network { + repeated string ips = 1; + bool gcp_private_google_access_v4 = 2; +} + +message Operation { + string id = 1; + string resource_group = 2; + string type = 3; + google.protobuf.Timestamp status_time = 4; + Status status = 5; + string reason = 6; + + enum Status { UNKNOWN = 0; - // Certificate managed by the user and imported into the platform. - IMPORTED = 1; - // Certificate managed by the platform. - MANAGED = 2; + STARTED = 1; + CANCELLED = 2; + COMPLETED = 3; + FAILED = 4; } - Type type = 1; +} - // Fully-qualified domain name bound to the certificate. - string domain_name = 2; +message Observation { + enum Category { + CATEGORY_UNKNOWN = 0; + CATEGORY_VULNERABILITY = 1; + CATEGORY_MISCONFIGURATION = 2; + CATEGORY_TOXIC_COMBINATION = 3; + } - // The list of alternative domain names bound to the certificate. - // See https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6. - repeated string subject_alternative_names = 3; + enum Source { + SOURCE_UNKNOWN = 0; + SOURCE_MODRON = 1; + SOURCE_SCC = 2; + } - google.protobuf.Timestamp creation_date = 4; - google.protobuf.Timestamp expiration_date = 5; - // The name of the certificate authority that issued the certificate. - string issuer = 6; + string uid = 1; + // The identifier of the scan that detected this observation. + // As we can have observations created at the "collection" phase, this field might be empty and the scan_uid + // field will be populated instead. + optional string scan_uid = 2; + google.protobuf.Timestamp timestamp = 3; + Resource deprecated_resource = 4; // Deprecated, we have the resource in the DB already and duplicating it here is useless + // Human readable name of the observation. + string name = 5; + // Value found in the configuration that causes the issue. + google.protobuf.Value expected_value = 6; + google.protobuf.Value observed_value = 7; + Remediation remediation = 8; - // The algorithm that was used by the issuer to sign the certificate. - string signature_algorithm = 7; + // DATABASE FIELDS + // ---------------------------- + // These fields will be stored as columns in the DB or will be populated by the DB + ResourceRef resource_ref = 9; + // When observations are fetched from an external API, we do that at the collection phase, which is why we + // store here a collection_id. + optional string collection_id = 10; + // An ID that can be used in the external system (source) to identify this observation + optional string external_id = 11; + // Source defines where the observation comes from + Source source = 12; + Severity risk_score = 13; + Category category = 14; + + Severity severity = 15; + Impact impact = 16; + // impact_reason defines why we've attributed this impact to the observation + string impact_reason = 17; +} - // The chain starts with the leaf certificate and continues with the - // remaining endorsing certificates in the chain of trust, if any. - // See https://datatracker.ietf.org/doc/html/rfc1421.html. - string pem_certificate_chain = 8; +enum Severity { + SEVERITY_UNKNOWN = 0; + SEVERITY_INFO = 1; + SEVERITY_LOW = 2; + SEVERITY_MEDIUM = 3; + SEVERITY_HIGH = 4; + SEVERITY_CRITICAL = 5; +} + +message ResourceRef { + optional string uid = 1; // This is the UUID of the resource, as stored in the Modron DB + string group_name = 2; // This is the ResourceGroupName identifier + + // This is the identifier used by the cloud platform to uniquely identify the resource, for example + //container.googleapis.com/projects/modron-test/locations/us-central1/clusters/modron-test-cluster/k8s/namespaces/modron-test-namespace + optional string external_id = 3; + + // This is the cloud platform, for example "GCP" or "AWS" + CloudPlatform cloud_platform = 4; +} + +enum CloudPlatform { + PLATFORM_UNKNOWN = 0; + GCP = 1; + AWS = 2; + AZURE = 3; +} + +enum Impact { + IMPACT_UNKNOWN = 0; + IMPACT_LOW = 1; + IMPACT_MEDIUM = 2; + IMPACT_HIGH = 3; +} + +message Permission { + string role = 1; + repeated string principals = 2; +} + +message Pod { + string cluster = 1; + string namespace = 2; + google.protobuf.Timestamp creation_time = 3; + enum Phase { + UNKNOWN_PHASE = 0; + PENDING = 1; + RUNNING = 2; + SUCCEEDED = 3; + FAILED = 4; + UNKNOWN = 5; + } + + Phase phase = 4; + k8s.io.api.core.v1.PodSpec spec = 5; + k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta object_meta = 6; +} + +message RecursiveResource { + string uuid = 1; + string name = 2; + string display_name = 3; + string type = 4; + string parent = 5; + map labels = 6; + map tags = 7; + + repeated RecursiveResource children = 8; } message Resource { @@ -299,8 +432,19 @@ message Resource { string resource_group_name = 8; // IamPolicy describes the IAM policy associated with that resource. IamPolicy iam_policy = 9; + + map labels = 10; + map tags = 11; + + // ancestors is a list of resource groups that are ancestors of this resource. + // Since RGs can only have one parent, this is an UNORDERED list of all the + // ancestors of this resource. The tree must be reconstructed by + // analyzing the relationship between the resource groups. + repeated string ancestors = 12; + // Types should be generic enough that they can match types of different cloud // providers. + reserved 111; // Old IAMGroup oneof type { VmInstance vm_instance = 100; Network network = 101; @@ -313,29 +457,58 @@ message Resource { Bucket bucket = 108; Certificate certificate = 109; Database database = 110; - IamGroup group = 111; + Namespace namespace = 112; + Pod pod = 113; } } +// ResourceGroup designates the smallest administrative grouping of resources. +message ResourceGroup { + reserved 1; + // Number describes an ID used by the platform to identify the Resource Group. + // In GCP this is the project number. + string identifier = 2; + string name = 3; +} + message Remediation { string description = 1; string recommendation = 2; } -message Observation { - string uid = 1; - string scan_uid = 2; - google.protobuf.Timestamp timestamp = 3; - Resource resource = 4; - // Human readable name of the observation. - string name = 5; - // Value found in the configuration that causes the issue. - google.protobuf.Value expected_value = 6; - google.protobuf.Value observed_value = 7; - Remediation remediation = 8; +message ServiceAccount { + repeated ExportedCredentials exported_credentials = 1; } -message ScanResultsList { repeated Observation observations = 1; } +message SslPolicy { + enum MinTlsVersion { + MinTlsVersion_UNKNOWN = 0; + TLS_1_0 = 1; + TLS_1_1 = 2; + TLS_1_2 = 3; + TLS_1_3 = 4; + } + enum Profile { + Profile_UNKNOWN = 0; + COMPATIBLE = 1; + MODERN = 2; + RESTRICTED = 3; + CUSTOM = 4; + } + google.protobuf.Timestamp creation_date = 1; + string name = 2; + Profile profile = 3; + MinTlsVersion minTlsVersion = 4; + repeated string enabledFeatures = 5; + repeated string customFeatures = 6; +} + +message VmInstance { + string public_ip = 1; + string private_ip = 2; + // ServiceAccount.Name + string identity = 3; +} service ModronService { // Scanning a project is a long running operation. We don't expect the user @@ -344,6 +517,8 @@ service ModronService { // here, but it's quite an overhead for the first implementation. Performs a // collection, followed by a scan, on the requested resource groups rpc CollectAndScan(CollectAndScanRequest) returns (CollectAndScanResponse); + rpc CollectAndScanAll(CollectAndScanAllRequest) returns (CollectAndScanResponse); + // List the latest observations resource groups rpc ListObservations(ListObservationsRequest) returns (ListObservationsResponse); @@ -380,7 +555,8 @@ message GetStatusCollectAndScanRequest { string scan_id = 2; } -message CollectAndScanRequest { repeated string resource_group_names = 1; } +message CollectAndScanRequest {repeated string resource_group_names = 1;} +message CollectAndScanAllRequest {} message CollectAndScanResponse { string collect_id = 1; @@ -393,7 +569,12 @@ message ListObservationsRequest { repeated string resource_group_names = 3; } -message CreateObservationRequest { Observation observation = 1; } +message ListObservationsResponse { + repeated ResourceGroupObservationsPair resource_groups_observations = 1; + string next_page_token = 2; +} + +message CreateObservationRequest {Observation observation = 1;} // we use this pair to get information about the rules that have no observations message RuleObservationPair { @@ -408,7 +589,10 @@ message ResourceGroupObservationsPair { repeated RuleObservationPair rules_observations = 2; } -message ListObservationsResponse { - repeated ResourceGroupObservationsPair resource_groups_observations = 1; - string next_page_token = 2; -} +message ScanResultsList {repeated Observation observations = 1;} + +enum ScanType { + SCAN_TYPE_UNKNOWN = 0; + SCAN_TYPE_PARTIAL = 1; + SCAN_TYPE_FULL = 2; +} \ No newline at end of file diff --git a/src/proto/notification.proto b/src/proto/notification.proto index f05e561..9ccd12e 100644 --- a/src/proto/notification.proto +++ b/src/proto/notification.proto @@ -4,7 +4,7 @@ import "google/protobuf/empty.proto"; import "google/protobuf/field_mask.proto"; import "google/protobuf/timestamp.proto"; -option go_package = "./pb"; +option go_package = "./"; message NotificationException { string uuid = 1; diff --git a/src/risk/risk.go b/src/risk/risk.go new file mode 100644 index 0000000..f6207c8 --- /dev/null +++ b/src/risk/risk.go @@ -0,0 +1,206 @@ +package risk + +import ( + "fmt" + "strings" + + "github.com/sirupsen/logrus" + + "github.com/nianticlabs/modron/src/constants" + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +const ( + tagKeyParts = 2 + tagValueParts = 3 +) + +var log = logrus.StandardLogger().WithField(constants.LogKeyPkg, "risk") + +type TagConfig struct { + ImpactMap map[string]pb.Impact + Environment string + EmployeeData string + CustomerData string +} + +func GetRiskScore(impact pb.Impact, severity pb.Severity) pb.Severity { + log.Debugf("risk score: impact=%s, severity=%s", impact, severity) + switch impact { + case pb.Impact_IMPACT_HIGH: + switch severity { + case pb.Severity_SEVERITY_CRITICAL: + return pb.Severity_SEVERITY_CRITICAL + case pb.Severity_SEVERITY_HIGH: + return pb.Severity_SEVERITY_CRITICAL + case pb.Severity_SEVERITY_MEDIUM: + return pb.Severity_SEVERITY_HIGH + case pb.Severity_SEVERITY_LOW: + return pb.Severity_SEVERITY_MEDIUM + case pb.Severity_SEVERITY_INFO: + return pb.Severity_SEVERITY_LOW + } + case pb.Impact_IMPACT_MEDIUM: + return severity + case pb.Impact_IMPACT_LOW: + switch severity { + case pb.Severity_SEVERITY_CRITICAL: + return pb.Severity_SEVERITY_HIGH + case pb.Severity_SEVERITY_HIGH: + return pb.Severity_SEVERITY_MEDIUM + case pb.Severity_SEVERITY_MEDIUM: + return pb.Severity_SEVERITY_LOW + case pb.Severity_SEVERITY_LOW: + return pb.Severity_SEVERITY_INFO + case pb.Severity_SEVERITY_INFO: + return pb.Severity_SEVERITY_INFO + } + } + return pb.Severity_SEVERITY_UNKNOWN +} + +type impactReason struct { + impact pb.Impact + reason string +} + +func GetEnvironment(tagConfig TagConfig, hierarchy map[string]*pb.RecursiveResource, rgName string) string { + parent := rgName + mergedTags := map[string]string{} + for { + if parent == "" { + break + } + v, ok := hierarchy[parent] + if !ok { + break + } + + for k, v := range v.Tags { + if _, ok := mergedTags[k]; !ok { + // Label not found, adding + mergedTags[k] = v + } + } + parent = v.Parent + } + + if v, ok := mergedTags[tagConfig.Environment]; ok { + tagValue := humanReadableTagValue(v) + return tagValue + } + return "" +} + +// humanReadableTagKey converts a tag name in the format "111111111111/employee_data" to employee_data +func humanReadableTagKey(tagKey string) string { + split := strings.SplitN(tagKey, "/", tagKeyParts) + if len(split) != tagKeyParts { + log.Warnf("unexpected tag key format: %q", tagKey) + return tagKey + } + return split[1] +} + +// humanReadableTagValue converts a tag value from the format "111111111111/environment/prod" to prod +func humanReadableTagValue(tagValue string) string { + split := strings.SplitN(tagValue, "/", tagValueParts) + if len(split) != tagValueParts { + log.Warnf("unexpected tag value format: %q", tagValue) + return tagValue + } + return split[2] +} + +// GetImpact computes the impact of a resource group based on the tags set in its hierarchy. +// The label at the deepest level of the hierarchy is the one that will be considered first. +// If a tags is set at the organization label, it might be overwritten by the project label - because they +// take precedence in our impact analysis. +// +// If we find something that has a high impact, we immediately return since this is the highest possible impact. +// In case of multiple impacts, we return the highest one. +func GetImpact(tagConfig TagConfig, hierarchy map[string]*pb.RecursiveResource, rgName string) (impact pb.Impact, reason string) { + parent := rgName + mergedTags := map[string]string{} + for { + if parent == "" { + break + } + v, ok := hierarchy[parent] + if !ok { + break + } + + for k, v := range v.Tags { + if _, ok := mergedTags[k]; !ok { + // Label not found, adding + mergedTags[k] = v + } + } + parent = v.Parent + } + impactLogger := log.WithField("resource_group", rgName) + + var impacts []impactReason + env := GetEnvironment(tagConfig, hierarchy, rgName) + if env != "" { + impacts = append(impacts, impactReason{ + impact: impactFromEnvironment(tagConfig.ImpactMap, env), + reason: fmt.Sprintf("%s=%s", humanReadableTagKey(tagConfig.Environment), env), + }) + } + + if v, ok := mergedTags[tagConfig.EmployeeData]; ok { + tagValue := humanReadableTagValue(v) + switch strings.ToLower(tagValue) { + case "yes": + impacts = append(impacts, impactReason{ + impact: constants.ImpactEmployeeData, + reason: fmt.Sprintf("%s=%s", constants.ResourceLabelEmployeeData, tagValue), + }) + case "no": + // All good + default: + impactLogger.Warnf("unknown value for label %s: %q", constants.ResourceLabelEmployeeData, tagValue) + } + } + + if v, ok := mergedTags[tagConfig.CustomerData]; ok { + tagValue := humanReadableTagValue(v) + switch strings.ToLower(tagValue) { + case "yes": + impacts = append(impacts, impactReason{ + impact: constants.ImpactCustomerData, + reason: fmt.Sprintf("%s=%s", constants.ResourceLabelCustomerData, tagValue), + }) + case "no": + // All good + default: + impactLogger.Warnf("unknown value for label %s: %q", constants.ResourceLabelCustomerData, tagValue) + } + } + + log.Debugf("mergedTags=%+v, impacts=%+v", mergedTags, impacts) + if len(impacts) == 0 { + log.Debugf("no facts that would change the impact") + return pb.Impact_IMPACT_MEDIUM, reason + } + highestImpact := pb.Impact_IMPACT_UNKNOWN + for _, i := range impacts { + if i.impact > highestImpact { + highestImpact = i.impact + reason = i.reason + } + } + return highestImpact, reason +} + +func impactFromEnvironment(impactMap map[string]pb.Impact, env string) pb.Impact { + v, ok := impactMap[env] + if !ok { + defaultImpact := pb.Impact_IMPACT_MEDIUM + log.Warnf("no impact found for environment %q, using %s", env, defaultImpact.String()) + return defaultImpact + } + return v +} diff --git a/src/server.go b/src/server.go index b013092..68a9150 100644 --- a/src/server.go +++ b/src/server.go @@ -1,373 +1,243 @@ -// Binary modron is a Cloud auditing tool. - -// Modron compares the existing state with a set of predefined rules -// and provides ways to crowd source the resolution of issues -// to resource owners. package main import ( "context" - "errors" + "encoding/json" "fmt" - "os" + "regexp" "strings" "time" - "github.com/golang/glog" - "github.com/google/uuid" "golang.org/x/exp/maps" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/timestamppb" + "golang.org/x/oauth2" + "google.golang.org/api/idtoken" + + "github.com/improbable-eng/grpc-web/go/grpcweb" + _ "github.com/lib/pq" + + "github.com/nianticlabs/modron/src/acl/fakeacl" + "github.com/nianticlabs/modron/src/acl/gcpacl" + "github.com/nianticlabs/modron/src/collector" + "github.com/nianticlabs/modron/src/collector/gcpcollector" "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/engine" "github.com/nianticlabs/modron/src/engine/rules" + "github.com/nianticlabs/modron/src/lognotifier" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + "github.com/nianticlabs/modron/src/nagatha" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/service" + "github.com/nianticlabs/modron/src/statemanager/reqdepstatemanager" + "github.com/nianticlabs/modron/src/storage" + "github.com/nianticlabs/modron/src/storage/gormstorage" + "github.com/nianticlabs/modron/src/storage/memstorage" ) -// TODO: Implement paginated API -type modronService struct { - checker model.Checker - collector model.Collector - notificationSvc model.NotificationService - ruleEngine model.RuleEngine - stateManager model.StateManager - storage model.Storage - // Required - pb.UnimplementedModronServiceServer - pb.UnimplementedNotificationServiceServer -} - -func (modron *modronService) validateResourceGroupNames(ctx context.Context, resourceGroupNames []string) ([]string, error) { - ownedResourceGroups, err := modron.checker.ListResourceGroupNamesOwned(ctx) - if err != nil { - glog.Warningf("validate resource groups: %v", err) - return nil, status.Error(codes.Unauthenticated, "failed authenticating request") - } - if len(resourceGroupNames) == 0 { - for k := range ownedResourceGroups { - resourceGroupNames = append(resourceGroupNames, k) - } - } else { - for _, rsgName := range resourceGroupNames { - if _, ok := ownedResourceGroups[rsgName]; !ok { - return nil, status.Error(codes.PermissionDenied, "resource group(s) is inaccessible") - } - } - } - return resourceGroupNames, nil -} +const ( + defaultCacheTimeout = 20 * time.Second +) -func (modron *modronService) scan(ctx context.Context, resourceGroupNames []string, scanId string) { - glog.V(5).Infof("request scan %s for %+v", scanId, resourceGroupNames) - filteredGroups := modron.stateManager.AddScan(scanId, resourceGroupNames) - glog.V(5).Infof("filtered scan %s for %+v", scanId, filteredGroups) - if len(filteredGroups) < 1 { - glog.Warningf("no groups to scan, aborting %s", scanId) - return - } else { - glog.Infof("starting scan %s for resource groups %v", scanId, filteredGroups) - } - obs, errs := modron.ruleEngine.CheckRules(ctx, scanId, filteredGroups) - if len(errs) > 0 { - glog.Errorf("scanId %s: %v", scanId, errors.Join(errs...)) - } - glog.V(5).Infof("ending scan %s for %+v", scanId, filteredGroups) - modron.stateManager.EndScan(scanId, filteredGroups) - glog.Infof("scan %s completed", scanId) - if err := modron.storage.FlushOpsLog(ctx); err != nil { - glog.Warningf("flush ops log: %v", err) - } - if len(obs) < 1 { - glog.Warningf("scan %s returned no observations.", scanId) - } - for _, o := range obs { - notifications, err := modron.notificationsFromObservation(ctx, o) +func newServer(ctx context.Context) (*service.Modron, error) { + ctx, span := tracer.Start(ctx, "newServer") + defer span.End() + var st model.Storage + var err error + switch storage.Type(strings.ToLower(args.Storage)) { + case storage.Memory: + log.Warnf("using memory storage: this should never be used in production") + st = memstorage.New() + case storage.SQL: + log.Tracef("setting up SQL") + st, err = gormstorage.NewDB( + args.SQLBackendDriver, + args.SQLConnectionString, + gormstorage.Config{ + BatchSize: args.DbBatchSize, + LogAllQueries: args.LogAllSQLQueries, + }, + ) if err != nil { - glog.Warningf("notifications: %v", err) - continue - } - for _, n := range notifications { - _, err := modron.notificationSvc.CreateNotification(ctx, n) - if err != nil { - if s, ok := status.FromError(err); !ok { - glog.Warningf("notification creation: %v", err) - } else { - if s.Code() == codes.AlreadyExists || s.Code() == codes.FailedPrecondition { - // Failed precondition is returned when an exception exist for the notification. - continue - } else { - glog.Warningf("notification %v creation: %s: %s", n, s.Code(), s.Message()) - } - } - } + return nil, fmt.Errorf("sql storage creation: %w", err) } + default: + return nil, fmt.Errorf("invalid storage \"%s\" specified", args.Storage) } -} -func (modron *modronService) collect(ctx context.Context, resourceGroupNames []string, collectId string) { - glog.V(5).Infof("request collection %s for %+v", collectId, resourceGroupNames) - filteredGroups := modron.stateManager.AddCollect(collectId, resourceGroupNames) - glog.V(5).Infof("filtered collection %s for %+v", collectId, filteredGroups) - if len(filteredGroups) > 0 { - glog.Infof("collect start id: %s for %+v", collectId, filteredGroups) - if err := modron.collector.CollectAndStoreAllResourceGroupResources(ctx, collectId, filteredGroups); len(err) != 0 { - glog.Warningf("collectId %s, errs: %v", collectId, errors.Join(err...)) - } else { - glog.Infof("collect done %s", collectId) - } + // Get Impact map + impactMap, err := getImpactMap(args.ImpactMap) + if err != nil { + return nil, fmt.Errorf("getImpactMap: %w", err) } - modron.stateManager.EndCollect(collectId, filteredGroups) -} - -func (modron *modronService) collectAndScan(ctx context.Context, resourceGroupNames []string) *pb.CollectAndScanResponse { - modronCtx := context.Background() - collectId, scanId := uuid.NewString(), uuid.NewString() - go func(resourceGroupNames []string) { - modron.collect(modronCtx, resourceGroupNames, collectId) - modron.scan(modronCtx, resourceGroupNames, scanId) - }(resourceGroupNames) - return &pb.CollectAndScanResponse{ - CollectId: collectId, - ScanId: scanId, + if len(maps.Keys(impactMap)) == 0 { + log.Warn("Impact map is empty") } -} -// rpc exposed CollectAndScan with resource group ownership validation -func (modron *modronService) CollectAndScan(ctx context.Context, in *pb.CollectAndScanRequest) (*pb.CollectAndScanResponse, error) { - resourceGroupNames, err := modron.validateResourceGroupNames(ctx, in.ResourceGroupNames) + // Parse rule configs + ruleConfigs, err := parseRuleConfigs(args.RuleConfigs) if err != nil { - return nil, status.Errorf(codes.FailedPrecondition, "resource group %+v: %v", in.ResourceGroupNames, err) + return nil, fmt.Errorf("parseRuleConfigs: %w", err) } - return modron.collectAndScan(ctx, resourceGroupNames), nil -} - -func (modron *modronService) scheduledRunner(ctx context.Context) { - intervalS := os.Getenv(collectAndScanInterval) - interval, err := time.ParseDuration(intervalS) - if err != nil || interval < time.Hour { // enforce a minimum of 1 hour interval - interval, _ = time.ParseDuration(defaultCollectAndScanInterval) - glog.Warningf("scan scheduler: env interval: %v . Keeping default value: %s", err, interval) + additionalAdminRoles := map[constants.Role]struct{}{} + for _, v := range args.AdditionalAdminRoles { + theRole := constants.ToRole(v) + additionalAdminRoles[theRole] = struct{}{} } - for { - glog.Infof("scan scheduler: starting") - if ctx.Err() != nil { - glog.Errorf("scan scheduler: %v", ctx.Err()) - return - } - rgs, err := modron.collector.ListResourceGroupNames(ctx) - if err != nil { - glog.Errorf("list resource groups: %v", err) - return - } - r := modron.collectAndScan(ctx, rgs) - glog.Infof("scan scheduler done: collectionID: %s, scanID: %s", r.CollectId, r.ScanId) - time.Sleep(interval) - } -} -func (modron *modronService) ListObservations(ctx context.Context, in *pb.ListObservationsRequest) (*pb.ListObservationsResponse, error) { - groups, err := modron.validateResourceGroupNames(ctx, in.ResourceGroupNames) - if err != nil { - return nil, err - } - obsByGroupByRules := map[string]map[string][]*pb.Observation{} - oneWeekAgo := time.Now().Add(-time.Hour * 24 * 7) - obs, err := modron.storage.ListObservations(ctx, model.StorageFilter{ - ResourceGroupNames: groups, - StartTime: oneWeekAgo, - TimeOffset: time.Since(oneWeekAgo), - }) - if err != nil { - glog.Warningf("list observations: %v", err) - return nil, status.Error(codes.Internal, "failed listing observations") - } - for _, group := range groups { - obsByGroupByRules[group] = map[string][]*pb.Observation{} - for _, rule := range rules.GetRules() { - obsByGroupByRules[group][rule.Info().Name] = []*pb.Observation{} - } - } - for _, ob := range obs { - group := ob.Resource.ResourceGroupName - rule := ob.Name - if obsByGroupByRules[group] == nil { - obsByGroupByRules[group] = map[string][]*pb.Observation{} - } - obsByGroupByRules[group][rule] = append( - obsByGroupByRules[group][rule], - ob, - ) - } - res := []*pb.ResourceGroupObservationsPair{} - for name, ruleObs := range obsByGroupByRules { - val := []*pb.RuleObservationPair{} - for rule, obs := range ruleObs { - val = append(val, &pb.RuleObservationPair{ - Rule: rule, - Observations: obs, - }) - } - res = append(res, &pb.ResourceGroupObservationsPair{ - ResourceGroupName: name, - RulesObservations: val, - }) + log.Tracef("Purging incomplete operations") + if err := st.PurgeIncompleteOperations(ctx); err != nil { + log.Errorf("Purging incomplete operations: %v", err) } - return &pb.ListObservationsResponse{ - ResourceGroupsObservations: res, - }, nil -} -func (modron *modronService) CreateObservation(ctx context.Context, in *pb.CreateObservationRequest) (*pb.Observation, error) { - if in.Observation == nil { - return nil, status.Error(codes.InvalidArgument, "observation is nil") - } - if in.Observation.Name == "" { - return nil, status.Error(codes.InvalidArgument, "observation does not have a name") + tagConfig := risk.TagConfig{ + ImpactMap: impactMap, + Environment: args.TagEnvironment, + EmployeeData: args.TagEmployeeData, + CustomerData: args.TagCustomerData, } - if in.Observation.Resource == nil { - return nil, status.Error(codes.InvalidArgument, "resource to link observation with not defined") - } - if in.Observation.Remediation == nil || in.Observation.Remediation.Recommendation == "" { - return nil, status.Error(codes.InvalidArgument, "cannot create an observation without recommendation") - } - in.Observation.Timestamp = timestamppb.Now() - res, err := modron.storage.ListResources(ctx, model.StorageFilter{ResourceNames: []string{in.Observation.Resource.Name}}) + + log.Tracef("Creating rule engine") + ruleEngine, err := engine.New(st, rules.GetRules(), ruleConfigs, args.ExcludedRules, tagConfig) if err != nil { - return nil, status.Errorf(codes.NotFound, "resource to link observation to not found: %v", err) + return nil, fmt.Errorf("creating rule engine: %w", err) } - if len(res) != 1 { - return nil, status.Errorf(codes.FailedPrecondition, "found %d resources matching %+v", len(res), in.Observation.Resource) - } - in.Observation.Resource = res[0] - if obs, err := modron.storage.BatchCreateObservations(ctx, []*pb.Observation{in.Observation}); err != nil { - return nil, status.Error(codes.Internal, err.Error()) - } else { - if len(obs) != 1 { - return nil, status.Errorf(codes.Internal, "creation returned %d items", len(obs)) - } else { - return obs[0], nil - } - } -} -func (modron *modronService) GetStatusCollectAndScan(ctx context.Context, in *pb.GetStatusCollectAndScanRequest) (*pb.GetStatusCollectAndScanResponse, error) { - collectStatus := modron.stateManager.GetCollectState(in.CollectId) - scanStatus := modron.stateManager.GetScanState(in.ScanId) - return &pb.GetStatusCollectAndScanResponse{ - CollectStatus: collectStatus, - ScanStatus: scanStatus, - }, nil -} - -func (modron *modronService) GetNotificationException(ctx context.Context, req *pb.GetNotificationExceptionRequest) (*pb.NotificationException, error) { - ex, err := modron.validateUserAndGetException(ctx, req.Uuid) + log.Tracef("Creating collector and ACL checker") + coll, checker, err := getCollectorAndChecker(ctx, st, tagConfig) if err != nil { - return nil, err + return nil, fmt.Errorf("getCollectorAndChecker: %w", err) } - return ex.ToProto(), err -} -func (modron *modronService) CreateNotificationException(ctx context.Context, req *pb.CreateNotificationExceptionRequest) (*pb.NotificationException, error) { - if userEmail, err := modron.checker.GetValidatedUser(ctx); err != nil { - return nil, status.Error(codes.Unauthenticated, "failed authenticating request") - } else { - req.Exception.UserEmail = userEmail + log.Tracef("Creating reqdepstate manager") + stateManager, err := reqdepstatemanager.New() + if err != nil { + return nil, fmt.Errorf("creating reqdepstatemanager: %w", err) } - ex, err := modron.notificationSvc.CreateException(ctx, model.ExceptionFromProto(req.Exception)) - return ex.ToProto(), err -} -func (modron *modronService) UpdateNotificationException(ctx context.Context, req *pb.UpdateNotificationExceptionRequest) (*pb.NotificationException, error) { - if ex, err := modron.validateUserAndGetException(ctx, req.Exception.Uuid); err != nil { - return nil, err + log.Tracef("Creating Notification Service") + var notificationSvc model.NotificationService + notificationSvcAddr := args.NotificationService + if notificationSvcAddr != "" { + notificationSvc, err = setupNotificationService(ctx, notificationSvcAddr) + if err != nil { + return nil, fmt.Errorf("unable to setup notification service: %w", err) + } } else { - req.Exception.UserEmail = ex.UserEmail + log.Tracef("Using lognotifier as notification service") + log.Infof("NotificationService argument is empty, logging instead") + notificationSvc = lognotifier.New() } - ex, err := modron.notificationSvc.UpdateException(ctx, model.ExceptionFromProto(req.Exception)) - return ex.ToProto(), err -} -func (modron *modronService) DeleteNotificationException(ctx context.Context, req *pb.DeleteNotificationExceptionRequest) (*emptypb.Empty, error) { - if _, err := modron.validateUserAndGetException(ctx, req.Uuid); err != nil { - return nil, err - } - return &emptypb.Empty{}, modron.notificationSvc.DeleteException(ctx, req.Uuid) -} - -func (modron *modronService) ListNotificationExceptions(ctx context.Context, req *pb.ListNotificationExceptionsRequest) (*pb.ListNotificationExceptionsResponse, error) { - if userEmail, err := modron.checker.GetValidatedUser(ctx); err != nil { - return nil, status.Error(codes.Unauthenticated, "failed authenticating request") - } else { - req.UserEmail = userEmail - } - ex, err := modron.notificationSvc.ListExceptions(ctx, req.UserEmail, req.PageSize, req.PageToken) - exList := []*pb.NotificationException{} - for _, e := range ex { - exList = append(exList, e.ToProto()) + labelToEmailRegexp, err := regexp.Compile(args.LabelToEmailRegexp) + if err != nil { + return nil, fmt.Errorf("regexp.Compile failed for contact label regex: %w", err) + } + + return service.New( + checker, + args.CollectAndScanInterval, + coll, + args.NotificationInterval, + notificationSvc, + args.OrgSuffix, + ruleEngine, + args.SelfURL, + stateManager, + st, + additionalAdminRoles, + labelToEmailRegexp, + args.LabelToEmailSubst, + ) +} + +func parseRuleConfigs(configs string) (map[string]json.RawMessage, error) { + var ruleConfigs map[string]json.RawMessage + if err := json.Unmarshal([]byte(configs), &ruleConfigs); err != nil { + return nil, fmt.Errorf("unable to decode rule configs: %w", err) + } + return ruleConfigs, nil +} + +func getImpactMap(impactMapJSON string) (map[string]pb.Impact, error) { + var impactMap map[string]string + if err := json.Unmarshal([]byte(impactMapJSON), &impactMap); err != nil { + return nil, fmt.Errorf("unable to decode impact map: %w", err) + } + finalImpactMap := map[string]pb.Impact{} + for k, v := range impactMap { + impactValue, ok := pb.Impact_value[v] + if !ok { + return nil, fmt.Errorf("invalid impact value: %s", v) + } + finalImpactMap[k] = pb.Impact(impactValue) } - return &pb.ListNotificationExceptionsResponse{Exceptions: exList}, err + return finalImpactMap, nil } -// TODO: Allow security admins to bypass the checks -func (modron *modronService) validateUserAndGetException(ctx context.Context, notificationUuid string) (model.Exception, error) { - userEmail, err := modron.checker.GetValidatedUser(ctx) - if err != nil { - return model.Exception{}, status.Error(codes.Unauthenticated, "failed authenticating request") - } - if ex, err := modron.notificationSvc.GetException(ctx, notificationUuid); err != nil { - return model.Exception{}, err - } else if ex.UserEmail != userEmail { - return model.Exception{}, status.Error(codes.Unauthenticated, "failed authenticating request") +func setupNotificationService(ctx context.Context, notSvcAddr string) (notSvc model.NotificationService, err error) { + log.Tracef("Using Nagatha as notification service") + var tokenSource oauth2.TokenSource + if args.IsE2EGrpcTest { + tokenSource = oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: "e2e-test-token", + }) } else { - return ex, nil + tokenSource, err = idtoken.NewTokenSource(ctx, args.NotificationServiceClientID) + if err != nil { + return nil, fmt.Errorf("idtoken.NewTokenSource: %w", err) + } } -} - -func (modron *modronService) notificationsFromObservation(ctx context.Context, ob *pb.Observation) ([]model.Notification, error) { - rg, err := modron.storage.ListResources(ctx, model.StorageFilter{ResourceNames: []string{ob.Resource.ResourceGroupName}, Limit: 1}) + notSvc, err = nagatha.New(notSvcAddr, args.SelfURL, tokenSource) if err != nil { - return nil, err - } - if len(rg) < 1 { - return nil, fmt.Errorf("no resource found %+v", ob.Resource.Uid) - } - if len(rg) > 1 { - glog.Warningf("multiple resources group found for %+v, using the first one", ob.Resource.Uid) - } - if rg[0].IamPolicy == nil { - glog.Warningf("no iam policy found for %s", rg[0].Name) - } - // We can have the same contacts in owners and labels, de-duplicate. - uniqueContacts := make(map[string]struct{}, 0) - for _, b := range rg[0].IamPolicy.Permissions { - for r := range constants.AdminRoles { - if strings.EqualFold(b.Role, r) { - for _, m := range b.Principals { - if strings.HasSuffix(m, orgSuffix) { - uniqueContacts[strings.TrimPrefix(m, constants.GCPUserAccountPrefix)] = struct{}{} - } - } - } + return nil, fmt.Errorf("nagatha service: %w", err) + } + return notSvc, nil +} + +func getCollectorAndChecker(ctx context.Context, st model.Storage, tagConfig risk.TagConfig) (c model.Collector, aclChecker model.Checker, err error) { + switch collector.Type(strings.ToLower(string(args.Collector))) { + case collector.Fake: + log.Warnf("Using fake collector") + c = gcpcollector.NewFake(ctx, st, tagConfig) + log.Warnf("Using fake ACL") + aclChecker = fakeacl.New() + case collector.Gcp: + log.Tracef("Creating GCP collector") + c, err = gcpcollector.New( + ctx, + st, + args.OrgID, + args.OrgSuffix, + args.AdditionalAdminRoles, + tagConfig, + args.AllowedSccCategories, + ) + if err != nil { + return nil, nil, fmt.Errorf("NewGCPCollector: %w", err) + } + log.Tracef("Creating GCP ACL checker") + if aclChecker, err = gcpacl.New(ctx, c, gcpacl.Config{ + AdminGroups: args.AdminGroups, + CacheTimeout: defaultCacheTimeout, + PersistentCache: args.PersistentCache, + PersistentCacheTimeout: args.PersistentCacheTimeout, + SkipIap: args.SkipIAP, + }); err != nil { + return nil, nil, fmt.Errorf("NewGcpChecker: %w", err) } + default: + return nil, nil, fmt.Errorf("invalid collector \"%s\" specified, use one of: %s", + args.Collector, strings.Join(collector.ValidCollectors(), ", ")) } + return +} - contacts := maps.Keys(uniqueContacts) - if len(contacts) < 1 { - return nil, fmt.Errorf("no contacts found for observation %q, resource group: %q", ob.Uid, ob.Resource.ResourceGroupName) - } - notifications := make([]model.Notification, 0) - for _, c := range contacts { - notifications = append(notifications, - model.Notification{ - SourceSystem: "modron", - Name: ob.Name, - Recipient: c, - Content: ob.Remediation.Recommendation, - Interval: notificationInterval, - }) +func withCors() []grpcweb.Option { + return []grpcweb.Option{ + grpcweb.WithOriginFunc(func(_ string) bool { + return true + }), + grpcweb.WithAllowedRequestHeaders([]string{"*"}), } - return notifications, nil } diff --git a/src/service/mock_notifier_test.go b/src/service/mock_notifier_test.go new file mode 100644 index 0000000..b092469 --- /dev/null +++ b/src/service/mock_notifier_test.go @@ -0,0 +1,70 @@ +package service_test + +import ( + "context" + "errors" + "sync" + + "github.com/nianticlabs/modron/src/model" +) + +type mockNotifier struct { + notificationLock sync.Mutex + notifications []model.Notification +} + +func (m *mockNotifier) BatchCreateNotifications(ctx context.Context, notifications []model.Notification) ([]model.Notification, error) { + var createdNotifications []model.Notification + var errArr []error + for _, n := range notifications { + created, err := m.CreateNotification(ctx, n) + if err != nil { + errArr = append(errArr, err) + continue + } + createdNotifications = append(createdNotifications, created) + } + return createdNotifications, errors.Join(errArr...) +} + +func (m *mockNotifier) CreateNotification(_ context.Context, notification model.Notification) (model.Notification, error) { + m.notificationLock.Lock() + defer m.notificationLock.Unlock() + m.notifications = append(m.notifications, notification) + return notification, nil +} + +func (m *mockNotifier) GetException(context.Context, string) (model.Exception, error) { + panic("implement me") +} + +func (m *mockNotifier) CreateException(context.Context, model.Exception) (model.Exception, error) { + panic("implement me") +} + +func (m *mockNotifier) UpdateException(context.Context, model.Exception) (model.Exception, error) { + panic("implement me") +} + +func (m *mockNotifier) DeleteException(context.Context, string) error { + panic("implement me") +} + +func (m *mockNotifier) ListExceptions(context.Context, string, int32, string) ([]model.Exception, error) { + panic("implement me") +} + +var _ model.NotificationService = (*mockNotifier)(nil) + +func newMockNotifier() *mockNotifier { + return &mockNotifier{} +} + +func (m *mockNotifier) getNotifications() []model.Notification { + // Clone the notifications + m.notificationLock.Lock() + defer m.notificationLock.Unlock() + notifications := make([]model.Notification, len(m.notifications)) + copy(notifications, m.notifications) + return notifications +} diff --git a/src/service/service.go b/src/service/service.go new file mode 100644 index 0000000..8cb2323 --- /dev/null +++ b/src/service/service.go @@ -0,0 +1,694 @@ +package service + +import ( + "context" + "errors" + "fmt" + "regexp" + "sort" + "strings" + "time" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + otelcodes "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + "golang.org/x/exp/maps" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/engine" + "github.com/nianticlabs/modron/src/model" + "github.com/nianticlabs/modron/src/nagatha" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" +) + +var ( + tracer = otel.Tracer("github.com/nianticlabs/modron/src/service") + meter = otel.Meter("github.com/nianticlabs/modron/src/service") +) + +// TODO: Implement paginated API +type Modron struct { + Checker model.Checker + CollectAndScanInterval time.Duration + Collector model.Collector + NotificationInterval time.Duration + NotificationSvc model.NotificationService + OrgSuffix string + RuleEngine model.Engine + SelfURL string + StateManager model.StateManager + Storage model.Storage + + additionalAdminRolesMap map[constants.Role]struct{} + labelToEmailRegexp *regexp.Regexp + labelToEmailSubst string + + metrics metrics + // Required + pb.UnimplementedModronServiceServer + pb.UnimplementedNotificationServiceServer +} + +var ( + log = logrus.StandardLogger().WithField(constants.LogKeyPkg, "service") +) + +type metrics struct { + CollectDuration metric.Float64Histogram + Observations metric.Int64Counter + ScanDuration metric.Float64Histogram +} + +const ( + oneDay = time.Hour * 24 + oneWeek = oneDay * 7 +) + +func (modron *Modron) validateResourceGroupNames(ctx context.Context, resourceGroupNames []string) ([]string, error) { + ownedResourceGroups, err := modron.Checker.ListResourceGroupNamesOwned(ctx) + if err != nil { + log.Warnf("validate resource groups: %v", err) + return nil, status.Error(codes.Unauthenticated, "failed authenticating request") + } + if len(resourceGroupNames) == 0 { + for k := range ownedResourceGroups { + resourceGroupNames = append(resourceGroupNames, k) + } + } else { + for _, rsgName := range resourceGroupNames { + if _, ok := ownedResourceGroups[rsgName]; !ok { + return nil, status.Error(codes.PermissionDenied, "resource group(s) is inaccessible") + } + } + } + return resourceGroupNames, nil +} + +// preCollect retrieves and stores all the Resource Groups (projects, folders, org) _without their IAM policies_. +// We don't collect the IAM policies because we only need those of the RGs that we need to analyze, +// but we need the entire set of RGs to perform cross-environment checks +func (modron *Modron) preCollect(ctx context.Context, resourceGroupNames []string) ([]*pb.Resource, error) { + ctx, span := tracer.Start(ctx, "preCollect") + defer span.End() + collectID, ok := ctx.Value(constants.CollectIDKey).(string) + if !ok { + return nil, fmt.Errorf("collectID not found in context") + } + pcLog := log. + WithField("collect_id", collectID). + WithField("resource_group_names", resourceGroupNames) + pcLog.Info("starting pre-collect") + defer pcLog.Info("pre-collect done") + + rgs, err := modron.Collector.ListResourceGroups(ctx, nil) + if err != nil { + return nil, fmt.Errorf("list resource groups: %w", err) + } + for i := range rgs { + rgs[i].CollectionUid = collectID + } + return rgs, nil +} + +func (modron *Modron) collect(ctx context.Context, resourceGroupNames []string, preCollectedRgs []*pb.Resource) []*pb.Observation { + start := time.Now() + collectID, ok := ctx.Value(constants.CollectIDKey).(string) + if !ok { + log.Errorf("collectID not found in context") + return nil + } + ctx, span := tracer.Start(ctx, "collect", + trace.WithAttributes( + attribute.String(constants.TraceKeyCollectID, collectID), + attribute.StringSlice(constants.TraceKeyResourceGroupNames, resourceGroupNames), + ), + ) + defer span.End() + collectLogger := log. + WithField("collect_id", collectID). + WithField("resource_group_names", resourceGroupNames) + collectLogger.Debug("request collection") + filteredGroups := modron.StateManager.AddCollect(collectID, resourceGroupNames) + collectLogger = collectLogger.WithField("filtered_groups", filteredGroups) + collectLogger.Debugf("filtered collection") + if len(filteredGroups) > 0 { + collectLogger.Infof("collect start") + if err := modron.Collector.CollectAndStoreAll(ctx, collectID, filteredGroups, preCollectedRgs); err != nil { + collectLogger. + WithError(err). + Warnf("some errors during collect: %v", err) + } else { + collectLogger.Info("collect done") + } + } + modron.StateManager.EndCollect(collectID, filteredGroups) + modron.metrics.CollectDuration.Record(ctx, time.Since(start).Seconds()) + + // Create notifications for collected observations + collectedObs, err := modron.getCollectedObservations(ctx, collectID, filteredGroups) + if err != nil { + collectLogger. + WithError(err). + Warnf("get collected observations: %v", err) + return nil + } + return collectedObs +} + +func (modron *Modron) scan(ctx context.Context, resourceGroupNames []string, preCollectedRgs []*pb.Resource) []*pb.Observation { + start := time.Now() + scanID, ok := ctx.Value(constants.ScanIDKey).(string) + if !ok { + log.Errorf("scanID not found in context") + return nil + } + ctx, span := tracer.Start(ctx, "scan", + trace.WithAttributes( + attribute.String(constants.TraceKeyScanID, scanID), + attribute.StringSlice(constants.TraceKeyResourceGroupNames, resourceGroupNames), + ), + ) + defer span.End() + collectID, ok := ctx.Value(constants.CollectIDKey).(string) + if !ok { + log.Errorf("collectID not found in context") + return nil + } + scanLogger := log. + WithField("collect_id", collectID). + WithField("scan_id", scanID). + WithField("resource_group_names", resourceGroupNames) + scanLogger.Debugf("requested scan") + filteredGroups := modron.StateManager.AddScan(scanID, resourceGroupNames) + scanLogger = scanLogger.WithField("filtered_groups", filteredGroups) + scanLogger.Debug("filtered scan") + if len(filteredGroups) < 1 { + scanLogger.Warnf("no groups to scan, aborting") + return nil + } + scanLogger.Info("starting scan") + obs, errs := modron.RuleEngine.CheckRules(ctx, scanID, collectID, filteredGroups, preCollectedRgs) + tookSeconds := time.Since(start).Seconds() + scanLogger = scanLogger. + WithFields(logrus.Fields{ + "observations": len(obs), + "took": tookSeconds, + }) + if len(errs) > 0 { + scanLogger. + WithError(errors.Join(errs...)). + Errorf("scan completed with errors: %v", errors.Join(errs...)) + } + scanLogger.Debug("ending scan") + modron.StateManager.EndScan(scanID, filteredGroups) + scanLogger.Info("scan completed") + modron.metrics.ScanDuration.Record(ctx, tookSeconds) + if err := modron.Storage.FlushOpsLog(ctx); err != nil { + scanLogger.WithError(err). + Warnf("flush ops log: %v", err) + } + modron.metrics.Observations.Add(ctx, int64(len(obs))) + if len(obs) < 1 { + scanLogger.Warnf("scan returned no observations") + } + return obs +} + +func (modron *Modron) createNotifications(ctx context.Context, obs []*pb.Observation) { + ctx, span := tracer.Start(ctx, "createNotifications") + defer span.End() + var allNotifications []model.Notification + for _, o := range obs { + ctx, span := tracer.Start(ctx, "createNotificationFromObservation", + trace.WithAttributes( + attribute.String(constants.TraceKeyObservationUID, o.Uid), + ), + ) + notifications, err := modron.notificationsFromObservation(ctx, o) + if err != nil { + log.Warnf("notifications from observation: %v", err) + continue + } + allNotifications = append(allNotifications, notifications...) + span.End() + } + span.SetAttributes( + attribute.Int(constants.TraceKeyNumNotifications, len(allNotifications)), + ) + log.Infof("Creating %d notifications in batch", len(allNotifications)) + _, err := modron.NotificationSvc.BatchCreateNotifications(ctx, allNotifications) + if err != nil { + log.Warnf("notifications: %v", err) + span.RecordError(err) + } +} + +func (modron *Modron) collectAndScan(ctx context.Context, rgs []string, scanType pb.ScanType) (*pb.CollectAndScanResponse, error) { + ctx = context.WithoutCancel(ctx) + collectID, scanID := uuid.NewString(), uuid.NewString() + ctx = context.WithValue(ctx, constants.CollectIDKey, collectID) + ctx = context.WithValue(ctx, constants.ScanIDKey, scanID) + ctx, span := tracer.Start(ctx, "collectAndScan", + trace.WithAttributes( + attribute.String(constants.TraceKeyCollectID, collectID), + attribute.String(constants.TraceKeyScanID, scanID), + attribute.String(constants.TraceKeyScanType, scanType.String()), + attribute.StringSlice(constants.TraceKeyResourceGroupNames, rgs), + ), + ) + defer span.End() + + switch scanType { + case pb.ScanType_SCAN_TYPE_PARTIAL: + // We use rgs + case pb.ScanType_SCAN_TYPE_FULL: + var err error + if rgs, err = modron.Collector.ListResourceGroupNames(ctx); err != nil { + return nil, fmt.Errorf("list resource groups: %w", err) + } + } + + obsChan := make(chan []*pb.Observation) + go func(resourceGroupNames []string) { + ctx, span := tracer.Start(ctx, "collectAndScanAsync", trace.WithNewRoot()) + defer span.End() + + preCollectedRgs, err := modron.preCollect(ctx, resourceGroupNames) + if err != nil { + log.Errorf("pre-collect failed: %v", err) + modron.StateManager.EndCollect(collectID, resourceGroupNames) + modron.StateManager.EndScan(scanID, resourceGroupNames) + span.SetStatus(otelcodes.Error, err.Error()) + span.End() + return + } + + obsChan <- modron.collect(ctx, resourceGroupNames, preCollectedRgs) + obsChan <- modron.scan(ctx, resourceGroupNames, preCollectedRgs) + close(obsChan) + }(rgs) + go func() { + // Process observations / notifications + for v := range obsChan { + if v == nil { + continue + } + ctx, span := tracer.Start(ctx, "processObservations", + trace.WithNewRoot(), + trace.WithLinks(trace.LinkFromContext(ctx)), + trace.WithAttributes( + attribute.Int(constants.TraceKeyNumObservations, len(v)), + ), + ) + modron.createNotifications(ctx, v) + span.End() + } + }() + return &pb.CollectAndScanResponse{ + CollectId: collectID, + ScanId: scanID, + }, nil +} + +// getCollectedObservations retrieves all the observations collected during the collection phase +// so that we can use them to create notifications +func (modron *Modron) getCollectedObservations(ctx context.Context, collectID string, groups []string) ([]*pb.Observation, error) { + obs, err := modron.Storage.ListObservations(ctx, model.StorageFilter{ + OperationID: collectID, + ResourceGroupNames: groups, + }) + if err != nil { + return nil, fmt.Errorf("list observations: %w", err) + } + return obs, nil +} + +func (modron *Modron) CollectAndScan(ctx context.Context, req *pb.CollectAndScanRequest) (*pb.CollectAndScanResponse, error) { + return modron.collectAndScan(ctx, req.ResourceGroupNames, pb.ScanType_SCAN_TYPE_PARTIAL) +} + +func (modron *Modron) CollectAndScanAll(ctx context.Context, _ *pb.CollectAndScanAllRequest) (*pb.CollectAndScanResponse, error) { + return modron.collectAndScan(ctx, nil, pb.ScanType_SCAN_TYPE_FULL) +} + +func (modron *Modron) ScheduledRunner(ctx context.Context) { + interval := modron.CollectAndScanInterval + log.Tracef("starting scheduler with interval %v", interval) + for { + ctx, span := tracer.Start(ctx, "ScheduledRunner", trace.WithNewRoot()) + log.Infof("scan scheduler: starting") + if ctx.Err() != nil { + log.Errorf("scan scheduler: %v", ctx.Err()) + return + } + r, err := modron.collectAndScan(ctx, nil, pb.ScanType_SCAN_TYPE_FULL) + if err != nil { + log.Errorf("scan scheduler: %v", err) + } + log.Infof("scan scheduler done: collectionID: %s, scanID: %s", r.CollectId, r.ScanId) + span.End() + time.Sleep(interval) + } +} + +func (modron *Modron) ListObservations(ctx context.Context, in *pb.ListObservationsRequest) (*pb.ListObservationsResponse, error) { + groups, err := modron.validateResourceGroupNames(ctx, in.ResourceGroupNames) + if err != nil { + return nil, err + } + obsByGroupByRules := map[string]map[string][]*pb.Observation{} + oneWeekAgo := time.Now().Add(-oneWeek) + obs, err := modron.Storage.ListObservations(ctx, model.StorageFilter{ + ResourceGroupNames: groups, + StartTime: oneWeekAgo, + TimeOffset: time.Since(oneWeekAgo), + }) + if err != nil { + log.Warnf("list observations: %v", err) + return nil, status.Error(codes.Internal, "failed listing observations") + } + for _, group := range groups { + obsByGroupByRules[group] = map[string][]*pb.Observation{} + for _, rule := range modron.RuleEngine.GetRules() { + obsByGroupByRules[group][rule.Info().Name] = []*pb.Observation{} + } + } + for _, ob := range obs { + group := ob.ResourceRef.GroupName + // TODO: Remove in the future when all of our observations do not use this field anymore: + ob.DeprecatedResource = nil + rule := ob.Name + if obsByGroupByRules[group] == nil { + obsByGroupByRules[group] = map[string][]*pb.Observation{} + } + obsByGroupByRules[group][rule] = append( + obsByGroupByRules[group][rule], + ob, + ) + } + var res []*pb.ResourceGroupObservationsPair + keys := maps.Keys(obsByGroupByRules) + sort.Strings(keys) + for _, name := range keys { + ruleObs := obsByGroupByRules[name] + var val []*pb.RuleObservationPair + ruleObsKeys := maps.Keys(ruleObs) + sort.Strings(ruleObsKeys) + for _, key := range ruleObsKeys { + obs := ruleObs[key] + val = append(val, &pb.RuleObservationPair{ + Rule: key, + Observations: obs, + }) + } + res = append(res, &pb.ResourceGroupObservationsPair{ + ResourceGroupName: name, + RulesObservations: val, + }) + } + return &pb.ListObservationsResponse{ + ResourceGroupsObservations: res, + }, nil +} + +func (modron *Modron) CreateObservation(ctx context.Context, in *pb.CreateObservationRequest) (*pb.Observation, error) { + if in.Observation == nil { + return nil, status.Error(codes.InvalidArgument, "observation is nil") + } + if in.Observation.Name == "" { + return nil, status.Error(codes.InvalidArgument, "observation does not have a name") + } + if in.Observation.ResourceRef == nil { + return nil, status.Error(codes.InvalidArgument, "resource to link observation with not defined") + } + if in.Observation.Remediation == nil || in.Observation.Remediation.Recommendation == "" { + return nil, status.Error(codes.InvalidArgument, "cannot create an observation without recommendation") + } + in.Observation.Timestamp = timestamppb.Now() + externalID := "" + if in.Observation.ResourceRef.ExternalId != nil { + externalID = *in.Observation.ResourceRef.ExternalId + } + res, err := modron.Storage.ListResources(ctx, model.StorageFilter{ResourceNames: []string{externalID}}) + if err != nil { + return nil, status.Errorf(codes.NotFound, "resource to link observation to not found: %v", err) + } + if len(res) != 1 { + return nil, status.Errorf(codes.FailedPrecondition, "found %d resources matching %+v", len(res), in.Observation.ResourceRef) + } + in.Observation.ResourceRef = utils.GetResourceRef(res[0]) + in.Observation.Uid = uuid.NewString() + obs, err := modron.Storage.BatchCreateObservations(ctx, []*pb.Observation{in.Observation}) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + if len(obs) != 1 { + return nil, status.Errorf(codes.Internal, "creation returned %d items", len(obs)) + } + return obs[0], nil +} + +func (modron *Modron) GetStatusCollectAndScan(_ context.Context, in *pb.GetStatusCollectAndScanRequest) (*pb.GetStatusCollectAndScanResponse, error) { + collectStatus := modron.StateManager.GetCollectState(in.CollectId) + scanStatus := modron.StateManager.GetScanState(in.ScanId) + return &pb.GetStatusCollectAndScanResponse{ + CollectStatus: collectStatus, + ScanStatus: scanStatus, + }, nil +} + +func (modron *Modron) GetNotificationException(ctx context.Context, req *pb.GetNotificationExceptionRequest) (*pb.NotificationException, error) { + ex, err := modron.validateUserAndGetException(ctx, req.Uuid) + if err != nil { + return nil, err + } + return ex.ToProto(), err +} + +func (modron *Modron) CreateNotificationException(ctx context.Context, req *pb.CreateNotificationExceptionRequest) (*pb.NotificationException, error) { + userEmail, err := modron.Checker.GetValidatedUser(ctx) + if err != nil { + return nil, status.Error(codes.Unauthenticated, "failed authenticating request") + } + req.Exception.UserEmail = userEmail + ex, err := modron.NotificationSvc.CreateException(ctx, model.ExceptionFromProto(req.Exception)) + if err != nil { + return nil, err + } + return ex.ToProto(), err +} + +func (modron *Modron) UpdateNotificationException(ctx context.Context, req *pb.UpdateNotificationExceptionRequest) (*pb.NotificationException, error) { + ex, err := modron.validateUserAndGetException(ctx, req.Exception.Uuid) + if err != nil { + return nil, err + } + req.Exception.UserEmail = ex.UserEmail + ex, err = modron.NotificationSvc.UpdateException(ctx, model.ExceptionFromProto(req.Exception)) + if err != nil { + return nil, err + } + return ex.ToProto(), err +} + +func (modron *Modron) DeleteNotificationException(ctx context.Context, req *pb.DeleteNotificationExceptionRequest) (*emptypb.Empty, error) { + if _, err := modron.validateUserAndGetException(ctx, req.Uuid); err != nil { + return nil, err + } + return &emptypb.Empty{}, modron.NotificationSvc.DeleteException(ctx, req.Uuid) +} + +func (modron *Modron) ListNotificationExceptions(ctx context.Context, req *pb.ListNotificationExceptionsRequest) (*pb.ListNotificationExceptionsResponse, error) { + userEmail, err := modron.Checker.GetValidatedUser(ctx) + if err != nil { + return nil, status.Error(codes.Unauthenticated, "failed authenticating request") + } + req.UserEmail = userEmail + ex, err := modron.NotificationSvc.ListExceptions(ctx, req.UserEmail, req.PageSize, req.PageToken) + var exList []*pb.NotificationException + for _, e := range ex { + exList = append(exList, e.ToProto()) + } + return &pb.ListNotificationExceptionsResponse{Exceptions: exList}, err +} + +// TODO: Allow security admins to bypass the checks +func (modron *Modron) validateUserAndGetException(ctx context.Context, notificationUUID string) (model.Exception, error) { + userEmail, err := modron.Checker.GetValidatedUser(ctx) + if err != nil { + return model.Exception{}, status.Error(codes.Unauthenticated, "failed authenticating request") + } + ex, err := modron.NotificationSvc.GetException(ctx, notificationUUID) + if err != nil { + return model.Exception{}, err + } + if ex.UserEmail != userEmail { + return model.Exception{}, status.Error(codes.Unauthenticated, "failed authenticating request") + } + return ex, nil +} + +func (modron *Modron) notificationsFromObservation(ctx context.Context, ob *pb.Observation) ([]model.Notification, error) { + log := log.WithFields(logrus.Fields{ + constants.LogKeyResourceGroup: ob.ResourceRef.GroupName, + constants.LogKeyObservationUID: ob.Uid, + constants.LogKeyObservationName: ob.Name, + }) + ty, err := utils.TypeFromResource(&pb.Resource{Type: &pb.Resource_ResourceGroup{}}) + if err != nil { + return nil, fmt.Errorf("type from resource: %w", err) + } + collectionID, ok := ctx.Value(constants.CollectIDKey).(string) + if !ok { + return nil, fmt.Errorf("collectID not found in context") + } + rg, err := modron.Storage.ListResources(ctx, model.StorageFilter{ + ResourceNames: []string{ob.ResourceRef.GroupName}, + OperationID: collectionID, + ResourceTypes: []string{ty}, + Limit: 1, + }) + if err != nil { + return nil, err + } + if len(rg) < 1 { + log.Errorf("no resource found") + return nil, fmt.Errorf("no resource found %+v", ob.ResourceRef.Uid) + } + if len(rg) > 1 { + log.Warnf("multiple resources group found for %+v, using the first one", ob.ResourceRef.Uid) + } + // We can have the same contacts in owners and labels, de-duplicate. + contacts := modron.contactsFromRG(rg[0]) + if len(contacts) < 1 { + log.Errorf("no contacts found for observation") + return nil, fmt.Errorf("no contacts found for observation %q, resource group: %q", ob.Uid, ob.ResourceRef.GroupName) + } + notifications := make([]model.Notification, 0) + for _, c := range contacts { + if c == "" { + log.Warnf("empty contact") + continue + } + notifications = append(notifications, + nagatha.NotificationFromObservation(c, modron.NotificationInterval, ob), + ) + } + return notifications, nil +} + +func (modron *Modron) contactsFromRG(rg *pb.Resource) []string { + uniqueContacts := make(map[string]struct{}) + if rg.IamPolicy != nil { + for _, b := range rg.IamPolicy.Permissions { + theRole := constants.ToRole(b.Role) + _, isAdminRole := constants.AdminRoles[theRole] + _, isAdditionalAdminRole := modron.additionalAdminRolesMap[theRole] + if isAdminRole || isAdditionalAdminRole { + for _, m := range b.Principals { + if strings.HasSuffix(m, modron.OrgSuffix) { + uniqueContacts[strings.TrimPrefix(m, constants.GCPUserAccountPrefix)] = struct{}{} + } + } + } + } + } + log.Debugf("contacts from IAM policy: %v", uniqueContacts) + + contact1, ok := rg.Labels[constants.LabelContact1] + if ok && contact1 != "" { + uniqueContacts[modron.LabelToEmail(contact1)] = struct{}{} + } + contact2, ok := rg.Labels[constants.LabelContact2] + if ok && contact2 != "" { + uniqueContacts[modron.LabelToEmail(contact2)] = struct{}{} + } + + contacts := maps.Keys(uniqueContacts) + return contacts +} + +// LabelToEmail converts a contact1,contact2 label into an email address +// these labels are formatted as firstname_lastname_example_com, which is the representation of +// firstname.lastname@example.com. +// Due to the _ replacement, we do not support emails like noreply_test@example.com. +func (modron *Modron) LabelToEmail(contact string) string { + contact = modron.labelToEmailRegexp.ReplaceAllString(contact, modron.labelToEmailSubst) + contact = strings.ReplaceAll(contact, "_", ".") + return contact +} + +func (modron *Modron) initMetrics() error { + collectDurationHist, err := meter.Float64Histogram( + constants.MetricsPrefix+"collections_duration", + metric.WithDescription("Duration of the collection process"), + metric.WithUnit("s"), + ) + if err != nil { + return err + } + observationsCounter, err := meter.Int64Counter( + constants.MetricsPrefix+"observations_total", + metric.WithDescription("Total number of observations created"), + ) + if err != nil { + return err + } + scanDurationHist, err := meter.Float64Histogram( + constants.MetricsPrefix+"scan_duration", + metric.WithDescription("Duration of the scan process"), + metric.WithUnit("s"), + ) + if err != nil { + return err + } + modron.metrics = metrics{ + CollectDuration: collectDurationHist, + Observations: observationsCounter, + ScanDuration: scanDurationHist, + } + return err +} + +func New( + checker model.Checker, + collectAndScanInterval time.Duration, + coll model.Collector, + notificationInterval time.Duration, + svc model.NotificationService, + suffix string, + engine *engine.RuleEngine, + url string, + manager model.StateManager, + st model.Storage, + additionalAdminRoles map[constants.Role]struct{}, + labelToEmailRegexp *regexp.Regexp, + labelToEmailSubst string, +) (*Modron, error) { + s := Modron{ + Checker: checker, + CollectAndScanInterval: collectAndScanInterval, + Collector: coll, + NotificationInterval: notificationInterval, + NotificationSvc: svc, + OrgSuffix: suffix, + RuleEngine: engine, + SelfURL: url, + StateManager: manager, + Storage: st, + additionalAdminRolesMap: additionalAdminRoles, + labelToEmailRegexp: labelToEmailRegexp, + labelToEmailSubst: labelToEmailSubst, + } + err := s.initMetrics() + return &s, err +} diff --git a/src/service/service_test.go b/src/service/service_test.go new file mode 100644 index 0000000..fe7a503 --- /dev/null +++ b/src/service/service_test.go @@ -0,0 +1,527 @@ +package service_test + +import ( + "context" + "encoding/json" + "regexp" + "sort" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/nianticlabs/modron/src/acl/fakeacl" + "github.com/nianticlabs/modron/src/collector/gcpcollector" + "github.com/nianticlabs/modron/src/engine" + "github.com/nianticlabs/modron/src/engine/rules" + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/risk" + "github.com/nianticlabs/modron/src/service" + "github.com/nianticlabs/modron/src/statemanager/reqdepstatemanager" + "github.com/nianticlabs/modron/src/storage/memstorage" +) + +const ( + bucketPublicRemediationDesc = "Bucket [\"bucket-public\"](https://console.cloud.google.com/storage/browser/bucket-public) is publicly accessible" + bucketPublicRemediationRecom = "Unless strictly needed, restrict the IAM policy of bucket [\"bucket-public\"](https://console.cloud.google.com/storage/browser/bucket-public) to prevent unconditional access by anyone. For more details, see [here](https://cloud.google.com/storage/docs/using-public-access-prevention)" + + bucketPublicAllUsersRemediationDesc = "Bucket [\"bucket-public-allusers\"](https://console.cloud.google.com/storage/browser/bucket-public-allusers) is publicly accessible" + bucketPublicAllUsersRemediationRecom string = "Unless strictly needed, restrict the IAM policy of bucket [\"bucket-public-allusers\"](https://console.cloud.google.com/storage/browser/bucket-public-allusers) to prevent unconditional access by anyone. For more details, see [here](https://cloud.google.com/storage/docs/using-public-access-prevention)" + + sqlRemediationDesc = "To lower your attack surface, Cloud SQL databases should not have public IPs. Private IPs provide improved network security and lower latency for your application." + sqlRemediationRecom = "Go to https://console.cloud.google.com/sql/instances/xyz/connections?project=project-id and click the \"Networking\" tab. Uncheck the \"Public IP\" checkbox and click \"SAVE\". If your instance is not configured to use a private IP, you will first have to enable private IP by following the instructions here: https://cloud.google.com/sql/docs/mysql/configure-private-ip#existing-private-instance" +) + +var impactMap = map[string]pb.Impact{ + "prod": pb.Impact_IMPACT_HIGH, + "pre-prod": pb.Impact_IMPACT_MEDIUM, + "dev": pb.Impact_IMPACT_LOW, + "playground": pb.Impact_IMPACT_LOW, +} + +func getService(ctx context.Context, t *testing.T) (*service.Modron, *mockNotifier) { + t.Helper() + st := memstorage.New() + engineRules := []model.Rule{ + rules.NewBucketIsPublicRule(), + } + notifier := newMockNotifier() + sm, err := reqdepstatemanager.New() + tagConfig := risk.TagConfig{ + ImpactMap: impactMap, + Environment: "111111111111/environment", + EmployeeData: "111111111111/employee_data", + CustomerData: "111111111111/customer_data", + } + if err != nil { + t.Fatalf("reqdepstatemanager.New: %v", err) + } + e, err := engine.New(st, engineRules, map[string]json.RawMessage{}, nil, tagConfig) + if err != nil { + t.Fatalf("engine.New: %v", err) + } + svc, err := service.New( + fakeacl.New(), + 10*time.Second, + gcpcollector.NewFake(ctx, st, tagConfig), + 24*time.Hour, + notifier, + "example.com", + e, + "https://modron.example.com", + sm, + st, + nil, + regexp.MustCompile("(.*)_(.*?)_(.*?)$"), + "$1@$2.$3", + ) + if err != nil { + t.Fatalf("service.New: %v", err) + } + return svc, notifier +} + +func TestService_CollectAndScan(t *testing.T) { + logrus.StandardLogger().SetLevel(logrus.DebugLevel) + logrus.StandardLogger().SetFormatter(&logrus.TextFormatter{ForceColors: true}) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + svc, notifier := getService(ctx, t) + res, err := svc.CollectAndScan(ctx, &pb.CollectAndScanRequest{ + ResourceGroupNames: []string{"projects/modron-test"}, + }) + if err != nil { + t.Fatalf("CollectAndScan: %v", err) + } + + scanID := res.ScanId + if scanID == "" { + t.Fatalf("empty scan ID") + } + collectID := res.CollectId + if collectID == "" { + t.Fatalf("empty collect ID") + } + + // Wait for the scan to complete + tick := time.NewTicker(1 * time.Second) + for { + done := false + select { + case <-ctx.Done(): + t.Fatalf("timeout waiting for scan to complete") + case <-tick.C: + status, err := svc.GetStatusCollectAndScan(ctx, &pb.GetStatusCollectAndScanRequest{ + CollectId: collectID, + ScanId: scanID, + }) + if err != nil { + t.Fatalf("GetStatusCollectAndScan: %v", err) + } + if status.CollectStatus == pb.RequestStatus_DONE && status.ScanStatus == pb.RequestStatus_DONE { + t.Log("Collect and scan completed") + done = true + break + } else { + t.Logf("collect=%s, scan=%s", status.CollectStatus.String(), status.ScanStatus.String()) + } + } + if done { + break + } + } + + // Done + obs, err := svc.ListObservations(ctx, &pb.ListObservationsRequest{}) + if err != nil { + t.Fatalf("ListObservations: %v", err) + } + got := obs.ResourceGroupsObservations + want := []*pb.ResourceGroupObservationsPair{ + { + ResourceGroupName: "projects/modron-test", + RulesObservations: []*pb.RuleObservationPair{ + { + Rule: "BUCKET_IS_PUBLIC", + Observations: []*pb.Observation{ + { + Name: "BUCKET_IS_PUBLIC", + Remediation: &pb.Remediation{ + Description: bucketPublicRemediationDesc, + Recommendation: bucketPublicRemediationRecom, + }, + ObservedValue: structpb.NewStringValue("PUBLIC"), + ExpectedValue: structpb.NewStringValue("PRIVATE"), + ResourceRef: &pb.ResourceRef{ + GroupName: "projects/modron-test", + ExternalId: proto.String("bucket-public"), + CloudPlatform: pb.CloudPlatform_GCP, + }, + Source: pb.Observation_SOURCE_MODRON, + Severity: pb.Severity_SEVERITY_MEDIUM, + Impact: pb.Impact_IMPACT_HIGH, + ImpactReason: "environment=prod", + RiskScore: pb.Severity_SEVERITY_HIGH, + ScanUid: proto.String(scanID), + }, + { + Name: "BUCKET_IS_PUBLIC", + Remediation: &pb.Remediation{ + Description: bucketPublicAllUsersRemediationDesc, + Recommendation: bucketPublicAllUsersRemediationRecom, + }, + ResourceRef: &pb.ResourceRef{ + GroupName: "projects/modron-test", + ExternalId: proto.String("bucket-public-allusers"), + CloudPlatform: pb.CloudPlatform_GCP, + }, + ScanUid: proto.String(scanID), + ObservedValue: structpb.NewStringValue("PUBLIC"), + ExpectedValue: structpb.NewStringValue("PRIVATE"), + Source: pb.Observation_SOURCE_MODRON, + Severity: pb.Severity_SEVERITY_MEDIUM, + Impact: pb.Impact_IMPACT_HIGH, + ImpactReason: "environment=prod", + RiskScore: pb.Severity_SEVERITY_HIGH, + }, + }, + }, + { + Rule: "SQL_PUBLIC_IP", + Observations: []*pb.Observation{ + { + Name: "SQL_PUBLIC_IP", + ResourceRef: &pb.ResourceRef{ + GroupName: "projects/modron-test", + ExternalId: proto.String("//cloudsql.googleapis.com/projects/project-id/instances/xyz"), + CloudPlatform: pb.CloudPlatform_GCP, + }, + Remediation: &pb.Remediation{ + Description: sqlRemediationDesc, + Recommendation: sqlRemediationRecom, + }, + CollectionId: proto.String(collectID), + Category: pb.Observation_CATEGORY_MISCONFIGURATION, + ExternalId: proto.String("//securitycenter.googleapis.com/projects/12345/sources/123/findings/48230f1978594ffb9d09a3cb1fe5e1b3"), + Source: pb.Observation_SOURCE_SCC, + Impact: pb.Impact_IMPACT_HIGH, + ImpactReason: "environment=prod", + Severity: pb.Severity_SEVERITY_MEDIUM, + RiskScore: pb.Severity_SEVERITY_HIGH, + }, + }, + }, + }, + }, + } + + if diff := cmp.Diff(want, got, + protocmp.Transform(), + protocmp.IgnoreFields(&pb.Observation{}, "uid", "timestamp"), + protocmp.IgnoreFields(&pb.ResourceRef{}, "uid"), + ); diff != "" { + t.Fatalf("ListObservations: diff (-want +got):\n%s", diff) + } + + user1 := "user-1@example.com" + user2 := "user-2@example.com" + owner1 := "owner1@example.com" + owner2 := "owner2@example.com" + + notificationContent := func(desc, rec string) string { + return desc + "\n\n" + rec + " \n \n" + } + wantNotif := []model.Notification{ + { + SourceSystem: "modron", + Name: "BUCKET_IS_PUBLIC", + Recipient: owner1, + Content: notificationContent(bucketPublicRemediationDesc, bucketPublicRemediationRecom), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "BUCKET_IS_PUBLIC", + Recipient: owner2, + Content: notificationContent(bucketPublicRemediationDesc, bucketPublicRemediationRecom), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "BUCKET_IS_PUBLIC", + Recipient: user1, + Content: notificationContent(bucketPublicRemediationDesc, bucketPublicRemediationRecom), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "BUCKET_IS_PUBLIC", + Recipient: user2, + Content: notificationContent(bucketPublicRemediationDesc, bucketPublicRemediationRecom), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "BUCKET_IS_PUBLIC", + Recipient: owner1, + Content: notificationContent(bucketPublicAllUsersRemediationDesc, bucketPublicAllUsersRemediationRecom), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "BUCKET_IS_PUBLIC", + Recipient: owner2, + Content: notificationContent(bucketPublicAllUsersRemediationDesc, bucketPublicAllUsersRemediationRecom), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "BUCKET_IS_PUBLIC", + Recipient: user1, + Content: notificationContent(bucketPublicAllUsersRemediationDesc, bucketPublicAllUsersRemediationRecom), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "BUCKET_IS_PUBLIC", + Recipient: user2, + Content: notificationContent(bucketPublicAllUsersRemediationDesc, bucketPublicAllUsersRemediationRecom), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "SQL_PUBLIC_IP", + Recipient: owner1, + Content: notificationContent(sqlRemediationDesc, sqlRemediationRecom), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "SQL_PUBLIC_IP", + Recipient: owner2, + Content: notificationContent(sqlRemediationDesc, sqlRemediationRecom), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "SQL_PUBLIC_IP", + Recipient: user1, + Content: notificationContent(sqlRemediationDesc, sqlRemediationRecom), + Interval: 24 * time.Hour, + }, + { + SourceSystem: "modron", + Name: "SQL_PUBLIC_IP", + Recipient: user2, + Content: notificationContent(sqlRemediationDesc, sqlRemediationRecom), + Interval: 24 * time.Hour, + }, + } + + gotNotif := notifier.getNotifications() + sort.Sort(sortNotifications(gotNotif)) + if diff := cmp.Diff(wantNotif, gotNotif); diff != "" { + t.Fatalf("notifications diff (-want +got):\n%s", diff) + } +} + +type sortNotifications []model.Notification + +func (s sortNotifications) Len() int { + return len(s) +} + +func (s sortNotifications) Less(i, j int) bool { + if s[i].Name < s[j].Name { + return true + } else if s[i].Name > s[j].Name { + return false + } + if s[i].Content < s[j].Content { + return true + } else if s[i].Content > s[j].Content { + return false + } + return s[i].Recipient < s[j].Recipient +} + +func (s sortNotifications) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +var _ sort.Interface = (*sortNotifications)(nil) + +func TestCrossEnvRule(t *testing.T) { + ctx := context.Background() + svc, _ := getService(ctx, t) + rgNames := []string{"projects/modron-test"} + svc.RuleEngine, _ = engine.New(svc.Storage, []model.Rule{ + rules.NewCrossEnvironmentPermissionsRule(), + }, + map[string]json.RawMessage{}, + nil, + risk.TagConfig{ + ImpactMap: impactMap, + Environment: "111111111111/environment", + EmployeeData: "111111111111/employee_data", + CustomerData: "111111111111/customer_data", + }) + csRes, err := svc.CollectAndScan(ctx, &pb.CollectAndScanRequest{ + ResourceGroupNames: rgNames, + }) + if err != nil { + t.Fatalf("CollectAndScan: %v", err) + } + collectID := csRes.CollectId + scanID := csRes.ScanId + for { + status, err := svc.GetStatusCollectAndScan(ctx, &pb.GetStatusCollectAndScanRequest{ + CollectId: collectID, + ScanId: scanID, + }) + if err != nil { + t.Fatalf("GetStatusCollectAndScan: %v", err) + } + if status.CollectStatus == pb.RequestStatus_DONE && status.ScanStatus == pb.RequestStatus_DONE { + break + } + time.Sleep(100 * time.Millisecond) + } + t.Logf("Scan done") + + observations, err := svc.ListObservations(ctx, &pb.ListObservationsRequest{ + ResourceGroupNames: rgNames, + }) + if err != nil { + t.Fatalf("ListObservations: %v", err) + } + + want := []*pb.ResourceGroupObservationsPair{ + { + ResourceGroupName: "projects/modron-test", + RulesObservations: []*pb.RuleObservationPair{ + { + Rule: "CROSS_ENVIRONMENT_PERMISSIONS", + Observations: []*pb.Observation{ + { + Name: "CROSS_ENVIRONMENT_PERMISSIONS", + Category: pb.Observation_CATEGORY_MISCONFIGURATION, + ObservedValue: structpb.NewStringValue(""), + ExpectedValue: structpb.NewStringValue("prod"), + Remediation: &pb.Remediation{ + Description: "account-3@modron-other-test.iam.gserviceaccount.com is in a different environment than the resource \"bucket-accessible-from-other-project\"", + Recommendation: "Revoke the access of \"account-3@modron-other-test.iam.gserviceaccount.com\" to the resource \"bucket-accessible-from-other-project\"", + }, + ResourceRef: &pb.ResourceRef{ + GroupName: "projects/modron-test", + CloudPlatform: pb.CloudPlatform_GCP, + ExternalId: proto.String("bucket-accessible-from-other-project"), + }, + Source: pb.Observation_SOURCE_MODRON, + Severity: pb.Severity_SEVERITY_HIGH, + Impact: pb.Impact_IMPACT_HIGH, + RiskScore: pb.Severity_SEVERITY_CRITICAL, + ImpactReason: "environment=prod", + }, + { + Name: "CROSS_ENVIRONMENT_PERMISSIONS", + Category: pb.Observation_CATEGORY_MISCONFIGURATION, + ObservedValue: structpb.NewStringValue(""), + ExpectedValue: structpb.NewStringValue("prod"), + Remediation: &pb.Remediation{ + Description: "account-3@modron-other-test.iam.gserviceaccount.com is in a different environment than the resource \"projects/modron-test\"", + Recommendation: "Revoke the access of \"account-3@modron-other-test.iam.gserviceaccount.com\" to the resource \"projects/modron-test\"", + }, + ResourceRef: &pb.ResourceRef{ + GroupName: "projects/modron-test", + CloudPlatform: pb.CloudPlatform_GCP, + ExternalId: proto.String("projects/modron-test"), + }, + Source: pb.Observation_SOURCE_MODRON, + Severity: pb.Severity_SEVERITY_HIGH, + Impact: pb.Impact_IMPACT_HIGH, + RiskScore: pb.Severity_SEVERITY_CRITICAL, + ImpactReason: "environment=prod", + }, + }, + }, + { + Rule: "SQL_PUBLIC_IP", + Observations: []*pb.Observation{ + { + Name: "SQL_PUBLIC_IP", + ResourceRef: &pb.ResourceRef{ + GroupName: "projects/modron-test", + ExternalId: proto.String("//cloudsql.googleapis.com/projects/project-id/instances/xyz"), + CloudPlatform: pb.CloudPlatform_GCP, + }, + Remediation: &pb.Remediation{ + Description: "To lower your attack surface, Cloud SQL databases should not have public IPs. Private IPs provide improved network security and lower latency for your application.", + Recommendation: "Go to https://console.cloud.google.com/sql/instances/xyz/connections?project=project-id and click the \"Networking\" tab. Uncheck the \"Public IP\" checkbox and click \"SAVE\". If your instance is not configured to use a private IP, you will first have to enable private IP by following the instructions here: https://cloud.google.com/sql/docs/mysql/configure-private-ip#existing-private-instance", + }, + CollectionId: proto.String(collectID), + Category: pb.Observation_CATEGORY_MISCONFIGURATION, + ExternalId: proto.String("//securitycenter.googleapis.com/projects/12345/sources/123/findings/48230f1978594ffb9d09a3cb1fe5e1b3"), + Source: pb.Observation_SOURCE_SCC, + Impact: pb.Impact_IMPACT_HIGH, + ImpactReason: "environment=prod", + Severity: pb.Severity_SEVERITY_MEDIUM, + RiskScore: pb.Severity_SEVERITY_HIGH, + }, + }, + }, + }, + }, + } + if diff := cmp.Diff( + want, + observations.ResourceGroupsObservations, + protocmp.Transform(), + protocmp.IgnoreFields(&pb.Observation{}, "uid", "timestamp", "scan_uid"), + protocmp.IgnoreFields(&pb.ResourceRef{}, "uid"), + ); diff != "" { + t.Fatalf("ListObservations: diff (-want +got):\n%s", diff) + } +} + +func TestLabelToEmail(t *testing.T) { + s, _ := getService(context.Background(), t) + tests := []struct { + labelContent string + want string + }{ + { + labelContent: "user_example_com", + want: "user@example.com", + }, + { + labelContent: "first_last_example_com", + want: "first.last@example.com", + }, + { + labelContent: "first_second_third_example_tokyo", + want: "first.second.third@example.tokyo", + }, + { + labelContent: "user.test_example_com", + want: "user.test@example.com", + }, + { + labelContent: "test", + want: "test", + }, + } + for _, tt := range tests { + t.Run(tt.labelContent, func(t *testing.T) { + got := s.LabelToEmail(tt.labelContent) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Fatalf("LabelToEmail: diff (-want +got):\n%s", diff) + } + }) + } +} diff --git a/src/statemanager/reqdepstatemanager/requestDependenciesStateManager.go b/src/statemanager/reqdepstatemanager/requestDependenciesStateManager.go index ff93f02..52de146 100644 --- a/src/statemanager/reqdepstatemanager/requestDependenciesStateManager.go +++ b/src/statemanager/reqdepstatemanager/requestDependenciesStateManager.go @@ -4,32 +4,32 @@ import ( "sync" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" ) type RequestStateManager struct { - scanIds sync.Map - scanIdsDependencies map[string]map[string]struct{} - collectIds sync.Map - collectIdsDependencies map[string]map[string]struct{} + scanIDs sync.Map + scanIDsDependencies map[string]map[string]struct{} + collectIDs sync.Map + collectIDsDependencies map[string]map[string]struct{} resourceGroupsScanning map[string]string resourceGroupsCollecting map[string]string } func New() (model.StateManager, error) { return &RequestStateManager{ - scanIds: sync.Map{}, - scanIdsDependencies: map[string]map[string]struct{}{}, - collectIds: sync.Map{}, - collectIdsDependencies: map[string]map[string]struct{}{}, + scanIDs: sync.Map{}, + scanIDsDependencies: map[string]map[string]struct{}{}, + collectIDs: sync.Map{}, + collectIDsDependencies: map[string]map[string]struct{}{}, resourceGroupsScanning: map[string]string{}, resourceGroupsCollecting: map[string]string{}, }, nil } -func (manager *RequestStateManager) GetCollectState(collectId string) pb.RequestStatus { +func (manager *RequestStateManager) GetCollectState(collectID string) pb.RequestStatus { status := pb.RequestStatus_UNKNOWN - if v, ok := manager.collectIds.Load(collectId); ok { + if v, ok := manager.collectIDs.Load(collectID); ok { status = v.(pb.RequestStatus) } if status == pb.RequestStatus_CANCELLED || @@ -37,15 +37,15 @@ func (manager *RequestStateManager) GetCollectState(collectId string) pb.Request return status } if status == pb.RequestStatus_ALREADY_RUNNING { - manager.collectIds.Store(collectId, pb.RequestStatus_DONE) + manager.collectIDs.Store(collectID, pb.RequestStatus_DONE) status = pb.RequestStatus_DONE } - if mapDep, ok := manager.collectIdsDependencies[collectId]; ok { + if mapDep, ok := manager.collectIDsDependencies[collectID]; ok { for dep := range mapDep { state := manager.GetCollectState(dep) if state == pb.RequestStatus_UNKNOWN || state == pb.RequestStatus_CANCELLED { - manager.collectIds.Store(collectId, pb.RequestStatus_CANCELLED) + manager.collectIDs.Store(collectID, pb.RequestStatus_CANCELLED) } else if state == pb.RequestStatus_RUNNING { return pb.RequestStatus_RUNNING } @@ -54,9 +54,9 @@ func (manager *RequestStateManager) GetCollectState(collectId string) pb.Request return status } -func (manager *RequestStateManager) GetScanState(scanId string) pb.RequestStatus { +func (manager *RequestStateManager) GetScanState(scanID string) pb.RequestStatus { status := pb.RequestStatus_UNKNOWN - if v, ok := manager.scanIds.Load(scanId); ok { + if v, ok := manager.scanIDs.Load(scanID); ok { status = v.(pb.RequestStatus) } if status == pb.RequestStatus_CANCELLED || @@ -64,15 +64,15 @@ func (manager *RequestStateManager) GetScanState(scanId string) pb.RequestStatus return status } if status == pb.RequestStatus_ALREADY_RUNNING { - manager.scanIds.Store(scanId, pb.RequestStatus_DONE) + manager.scanIDs.Store(scanID, pb.RequestStatus_DONE) status = pb.RequestStatus_DONE } - if mapDep, ok := manager.scanIdsDependencies[scanId]; ok { + if mapDep, ok := manager.scanIDsDependencies[scanID]; ok { for dep := range mapDep { state := manager.GetScanState(dep) if state == pb.RequestStatus_UNKNOWN || state == pb.RequestStatus_CANCELLED { - manager.scanIds.Store(scanId, pb.RequestStatus_CANCELLED) + manager.scanIDs.Store(scanID, pb.RequestStatus_CANCELLED) } else if state == pb.RequestStatus_RUNNING { return pb.RequestStatus_RUNNING } @@ -81,62 +81,62 @@ func (manager *RequestStateManager) GetScanState(scanId string) pb.RequestStatus return status } -func (manager *RequestStateManager) AddScan(scanId string, resourceGroupNames []string) []string { - manager.scanIds.Store(scanId, pb.RequestStatus_RUNNING) +func (manager *RequestStateManager) AddScan(scanID string, resourceGroupNames []string) []string { + manager.scanIDs.Store(scanID, pb.RequestStatus_RUNNING) filteredRG := []string{} for _, rs := range resourceGroupNames { scan, ok := manager.resourceGroupsScanning[rs] if !ok { - manager.resourceGroupsScanning[rs] = scanId + manager.resourceGroupsScanning[rs] = scanID filteredRG = append(filteredRG, rs) } else { - if _, ok := manager.scanIdsDependencies[scanId]; !ok { - manager.scanIdsDependencies[scanId] = map[string]struct{}{} + if _, ok := manager.scanIDsDependencies[scanID]; !ok { + manager.scanIDsDependencies[scanID] = map[string]struct{}{} } - manager.scanIdsDependencies[scanId][scan] = struct{}{} + manager.scanIDsDependencies[scanID][scan] = struct{}{} } } if len(filteredRG) < 1 { - manager.scanIds.Store(scanId, pb.RequestStatus_ALREADY_RUNNING) + manager.scanIDs.Store(scanID, pb.RequestStatus_ALREADY_RUNNING) } return filteredRG } -func (manager *RequestStateManager) EndScan(scanId string, resourceGroupNames []string) { - if _, ok := manager.scanIds.Load(scanId); ok { +func (manager *RequestStateManager) EndScan(scanID string, resourceGroupNames []string) { + if _, ok := manager.scanIDs.Load(scanID); ok { for _, rs := range resourceGroupNames { delete(manager.resourceGroupsScanning, rs) } - manager.scanIds.Store(scanId, pb.RequestStatus_DONE) + manager.scanIDs.Store(scanID, pb.RequestStatus_DONE) } } -func (manager *RequestStateManager) AddCollect(collectId string, resourceGroupNames []string) []string { - manager.collectIds.Store(collectId, pb.RequestStatus_RUNNING) +func (manager *RequestStateManager) AddCollect(collectID string, resourceGroupNames []string) []string { + manager.collectIDs.Store(collectID, pb.RequestStatus_RUNNING) filteredRG := []string{} for _, rs := range resourceGroupNames { collect, ok := manager.resourceGroupsCollecting[rs] if !ok { - manager.resourceGroupsCollecting[rs] = collectId + manager.resourceGroupsCollecting[rs] = collectID filteredRG = append(filteredRG, rs) } else { - if _, ok := manager.collectIdsDependencies[collectId]; !ok { - manager.collectIdsDependencies[collectId] = map[string]struct{}{} + if _, ok := manager.collectIDsDependencies[collectID]; !ok { + manager.collectIDsDependencies[collectID] = map[string]struct{}{} } - manager.collectIdsDependencies[collectId][collect] = struct{}{} + manager.collectIDsDependencies[collectID][collect] = struct{}{} } } if len(filteredRG) < 1 { - manager.collectIds.Store(collectId, pb.RequestStatus_ALREADY_RUNNING) + manager.collectIDs.Store(collectID, pb.RequestStatus_ALREADY_RUNNING) } return filteredRG } -func (manager *RequestStateManager) EndCollect(collectId string, resourceGroupNames []string) { - if _, ok := manager.collectIds.Load(collectId); ok { +func (manager *RequestStateManager) EndCollect(collectID string, resourceGroupNames []string) { + if _, ok := manager.collectIDs.Load(collectID); ok { for _, rs := range resourceGroupNames { delete(manager.resourceGroupsCollecting, rs) } - manager.collectIds.Store(collectId, pb.RequestStatus_DONE) + manager.collectIDs.Store(collectID, pb.RequestStatus_DONE) } } diff --git a/src/statemanager/reqdepstatemanager/requestDependenciesStateManager_test.go b/src/statemanager/reqdepstatemanager/requestDependenciesStateManager_test.go index 11dd34f..aeb6a7d 100644 --- a/src/statemanager/reqdepstatemanager/requestDependenciesStateManager_test.go +++ b/src/statemanager/reqdepstatemanager/requestDependenciesStateManager_test.go @@ -3,7 +3,7 @@ package reqdepstatemanager import ( "testing" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" ) func TestSimpleStateManager(t *testing.T) { @@ -12,55 +12,55 @@ func TestSimpleStateManager(t *testing.T) { t.Fatal(err) } - collectId1 := "collect-id-1" - collectId2 := "collect-id-2" - scanId1 := "scan-id-1" - scanId2 := "scan-id-2" + collectID1 := "collect-id-1" + collectID2 := "collect-id-2" + scanID1 := "scan-id-1" + scanID2 := "scan-id-2" - if state := stateManager.GetCollectState(collectId1); state != pb.RequestStatus_UNKNOWN { - t.Errorf("GetCollectState(%s) got %s, want %s", collectId1, state, pb.RequestStatus_UNKNOWN) + if state := stateManager.GetCollectState(collectID1); state != pb.RequestStatus_UNKNOWN { + t.Errorf("GetCollectState(%s) got %s, want %s", collectID1, state, pb.RequestStatus_UNKNOWN) } - if state := stateManager.GetScanState(scanId1); state != pb.RequestStatus_UNKNOWN { - t.Errorf("GetScanState(%s) got %s, want %s", scanId1, state, pb.RequestStatus_UNKNOWN) + if state := stateManager.GetScanState(scanID1); state != pb.RequestStatus_UNKNOWN { + t.Errorf("GetScanState(%s) got %s, want %s", scanID1, state, pb.RequestStatus_UNKNOWN) } resourceGroups := []string{"projects/p1", "projects/p2", "projects/p3"} - collecting := stateManager.AddCollect(collectId1, resourceGroups) + collecting := stateManager.AddCollect(collectID1, resourceGroups) if len(collecting) != 3 { t.Errorf("AddCollect(%v): got len %d, want %d", resourceGroups, len(collecting), 3) } - scanning := stateManager.AddScan(scanId1, resourceGroups) + scanning := stateManager.AddScan(scanID1, resourceGroups) if len(scanning) != 3 { t.Errorf("AddScan(%v): got len %d, want %d", resourceGroups, len(scanning), 3) } - if state := stateManager.GetCollectState(collectId1); state != pb.RequestStatus_RUNNING { - t.Errorf("GetCollectState(%s): got %s, want %s", collectId1, state, pb.RequestStatus_RUNNING) + if state := stateManager.GetCollectState(collectID1); state != pb.RequestStatus_RUNNING { + t.Errorf("GetCollectState(%s): got %s, want %s", collectID1, state, pb.RequestStatus_RUNNING) } - if state := stateManager.GetCollectState(collectId2); state != pb.RequestStatus_UNKNOWN { - t.Errorf("GetCollectState(%s): got %s, want %s", collectId2, state, pb.RequestStatus_UNKNOWN) + if state := stateManager.GetCollectState(collectID2); state != pb.RequestStatus_UNKNOWN { + t.Errorf("GetCollectState(%s): got %s, want %s", collectID2, state, pb.RequestStatus_UNKNOWN) } - if state := stateManager.GetScanState(scanId1); state != pb.RequestStatus_RUNNING { - t.Errorf("GetScanState(%s): got %s, want %s", scanId1, state, pb.RequestStatus_RUNNING) + if state := stateManager.GetScanState(scanID1); state != pb.RequestStatus_RUNNING { + t.Errorf("GetScanState(%s): got %s, want %s", scanID1, state, pb.RequestStatus_RUNNING) } - if state := stateManager.GetScanState(scanId2); state != pb.RequestStatus_UNKNOWN { - t.Errorf("GetScanState(%s): got %s, want %s", scanId2, state, pb.RequestStatus_UNKNOWN) + if state := stateManager.GetScanState(scanID2); state != pb.RequestStatus_UNKNOWN { + t.Errorf("GetScanState(%s): got %s, want %s", scanID2, state, pb.RequestStatus_UNKNOWN) } - stateManager.EndCollect(collectId2, resourceGroups) - stateManager.EndScan(scanId2, resourceGroups) + stateManager.EndCollect(collectID2, resourceGroups) + stateManager.EndScan(scanID2, resourceGroups) - stateManager.EndCollect(collectId1, resourceGroups) - stateManager.EndScan(scanId1, resourceGroups) + stateManager.EndCollect(collectID1, resourceGroups) + stateManager.EndScan(scanID1, resourceGroups) - if state := stateManager.GetCollectState(collectId1); state != pb.RequestStatus_DONE { - t.Errorf("GetCollectState(%s) got %s, want %s", collectId1, state, pb.RequestStatus_DONE) + if state := stateManager.GetCollectState(collectID1); state != pb.RequestStatus_DONE { + t.Errorf("GetCollectState(%s) got %s, want %s", collectID1, state, pb.RequestStatus_DONE) } - if state := stateManager.GetScanState(scanId1); state != pb.RequestStatus_DONE { - t.Errorf("GetScanState(%s): got %s, want %s", scanId1, state, pb.RequestStatus_DONE) + if state := stateManager.GetScanState(scanID1); state != pb.RequestStatus_DONE { + t.Errorf("GetScanState(%s): got %s, want %s", scanID1, state, pb.RequestStatus_DONE) } } @@ -70,78 +70,78 @@ func TestDepSateManager(t *testing.T) { t.Fatal(err) } - collectId1 := "collect-id-1" - collectId2 := "collect-id-2" - collectId3 := "collect-id-3" - scanId1 := "scan-id-1" - scanId2 := "scan-id-2" - scanId3 := "scan-id-3" + collectID1 := "collect-id-1" + collectID2 := "collect-id-2" + collectID3 := "collect-id-3" + scanID1 := "scan-id-1" + scanID2 := "scan-id-2" + scanID3 := "scan-id-3" resourceGroups := []string{"projects/p1", "projects/p2", "projects/p3"} - if collecting := stateManager.AddCollect(collectId1, resourceGroups); len(collecting) != 3 { + if collecting := stateManager.AddCollect(collectID1, resourceGroups); len(collecting) != 3 { t.Errorf("AddCollect(%v): got len %d, want %d", resourceGroups, len(collecting), 3) } overlappingResourceGroups := []string{"projects/p0", "projects/p1", "projects/p2"} if collecting := stateManager.AddCollect("collect-id-3", overlappingResourceGroups); len(collecting) != 1 { - t.Errorf("AddCollect(%s): got len %d, want %d", collectId1, len(collecting), 1) + t.Errorf("AddCollect(%s): got len %d, want %d", collectID1, len(collecting), 1) } - if scanning := stateManager.AddScan(scanId1, resourceGroups); len(scanning) != 3 { - t.Errorf("AddScan(%s): got len %d, want %d", scanId1, len(scanning), 1) + if scanning := stateManager.AddScan(scanID1, resourceGroups); len(scanning) != 3 { + t.Errorf("AddScan(%s): got len %d, want %d", scanID1, len(scanning), 1) } - if scanning := stateManager.AddScan(scanId3, []string{"projects/p1", "projects/p2"}); len(scanning) != 0 { - t.Errorf("AddScan(%s): got len %d, want %d", scanId3, len(scanning), 0) + if scanning := stateManager.AddScan(scanID3, []string{"projects/p1", "projects/p2"}); len(scanning) != 0 { + t.Errorf("AddScan(%s): got len %d, want %d", scanID3, len(scanning), 0) } - if state := stateManager.GetCollectState(collectId1); state != pb.RequestStatus_RUNNING { - t.Errorf("GetCollectState(%s): got %s, want %s", collectId1, state, pb.RequestStatus_RUNNING) + if state := stateManager.GetCollectState(collectID1); state != pb.RequestStatus_RUNNING { + t.Errorf("GetCollectState(%s): got %s, want %s", collectID1, state, pb.RequestStatus_RUNNING) } - if state := stateManager.GetCollectState(collectId2); state != pb.RequestStatus_UNKNOWN { - t.Errorf("GetCollectState(%s): got %s, want %s", collectId2, state, pb.RequestStatus_UNKNOWN) + if state := stateManager.GetCollectState(collectID2); state != pb.RequestStatus_UNKNOWN { + t.Errorf("GetCollectState(%s): got %s, want %s", collectID2, state, pb.RequestStatus_UNKNOWN) } - if state := stateManager.GetCollectState(collectId3); state != pb.RequestStatus_RUNNING { - t.Errorf("GetCollectState(%s): got %s, want %s", collectId3, state, pb.RequestStatus_RUNNING) + if state := stateManager.GetCollectState(collectID3); state != pb.RequestStatus_RUNNING { + t.Errorf("GetCollectState(%s): got %s, want %s", collectID3, state, pb.RequestStatus_RUNNING) } - if state := stateManager.GetScanState(scanId1); state != pb.RequestStatus_RUNNING { - t.Errorf("GetScanState(%s): got %s, want %s", scanId1, state, pb.RequestStatus_RUNNING) + if state := stateManager.GetScanState(scanID1); state != pb.RequestStatus_RUNNING { + t.Errorf("GetScanState(%s): got %s, want %s", scanID1, state, pb.RequestStatus_RUNNING) } - if state := stateManager.GetScanState(scanId2); state != pb.RequestStatus_UNKNOWN { - t.Errorf("GetScanState(%s): got %s, want %s", scanId2, state, pb.RequestStatus_UNKNOWN) + if state := stateManager.GetScanState(scanID2); state != pb.RequestStatus_UNKNOWN { + t.Errorf("GetScanState(%s): got %s, want %s", scanID2, state, pb.RequestStatus_UNKNOWN) } - if state := stateManager.GetScanState(scanId3); state != pb.RequestStatus_RUNNING { - t.Errorf("GetScanState(%s): got %s, want %s", scanId3, state, pb.RequestStatus_RUNNING) + if state := stateManager.GetScanState(scanID3); state != pb.RequestStatus_RUNNING { + t.Errorf("GetScanState(%s): got %s, want %s", scanID3, state, pb.RequestStatus_RUNNING) } - stateManager.EndCollect(collectId1, resourceGroups) - stateManager.EndCollect(collectId2, resourceGroups) - stateManager.EndScan(scanId1, resourceGroups) - stateManager.EndScan(scanId2, resourceGroups) + stateManager.EndCollect(collectID1, resourceGroups) + stateManager.EndCollect(collectID2, resourceGroups) + stateManager.EndScan(scanID1, resourceGroups) + stateManager.EndScan(scanID2, resourceGroups) - if state := stateManager.GetCollectState(collectId1); state != pb.RequestStatus_DONE { - t.Errorf("GetCollectState(%s): got %s, want %s", collectId1, state, pb.RequestStatus_DONE) + if state := stateManager.GetCollectState(collectID1); state != pb.RequestStatus_DONE { + t.Errorf("GetCollectState(%s): got %s, want %s", collectID1, state, pb.RequestStatus_DONE) } - if state := stateManager.GetCollectState(collectId2); state != pb.RequestStatus_UNKNOWN { - t.Errorf("GetCollectState(%s): got %s, want %s", collectId2, state, pb.RequestStatus_UNKNOWN) + if state := stateManager.GetCollectState(collectID2); state != pb.RequestStatus_UNKNOWN { + t.Errorf("GetCollectState(%s): got %s, want %s", collectID2, state, pb.RequestStatus_UNKNOWN) } - if state := stateManager.GetScanState(scanId1); state != pb.RequestStatus_DONE { - t.Errorf("GetScanState(%s): got %s, want %s", scanId1, state, pb.RequestStatus_DONE) + if state := stateManager.GetScanState(scanID1); state != pb.RequestStatus_DONE { + t.Errorf("GetScanState(%s): got %s, want %s", scanID1, state, pb.RequestStatus_DONE) } - if state := stateManager.GetScanState(scanId2); state != pb.RequestStatus_UNKNOWN { - t.Errorf("GetScanState(%s): got %s, want %s", scanId2, state, pb.RequestStatus_UNKNOWN) + if state := stateManager.GetScanState(scanID2); state != pb.RequestStatus_UNKNOWN { + t.Errorf("GetScanState(%s): got %s, want %s", scanID2, state, pb.RequestStatus_UNKNOWN) } - if state := stateManager.GetScanState(scanId3); state != pb.RequestStatus_DONE { - t.Errorf("GetScanState(%s): got %s, want %s", scanId3, state, pb.RequestStatus_DONE) + if state := stateManager.GetScanState(scanID3); state != pb.RequestStatus_DONE { + t.Errorf("GetScanState(%s): got %s, want %s", scanID3, state, pb.RequestStatus_DONE) } - if state := stateManager.GetCollectState(collectId3); state != pb.RequestStatus_RUNNING { - t.Errorf("GetCollectState(%s): got %s, want %s", collectId3, state, pb.RequestStatus_RUNNING) + if state := stateManager.GetCollectState(collectID3); state != pb.RequestStatus_RUNNING { + t.Errorf("GetCollectState(%s): got %s, want %s", collectID3, state, pb.RequestStatus_RUNNING) } - stateManager.EndCollect(collectId3, overlappingResourceGroups) - if state := stateManager.GetCollectState(collectId3); state != pb.RequestStatus_DONE { - t.Errorf("GetCollectState(%s): got %s, want %s", collectId3, state, pb.RequestStatus_DONE) + stateManager.EndCollect(collectID3, overlappingResourceGroups) + if state := stateManager.GetCollectState(collectID3); state != pb.RequestStatus_DONE { + t.Errorf("GetCollectState(%s): got %s, want %s", collectID3, state, pb.RequestStatus_DONE) } } diff --git a/src/storage/gormstorage/gorm.go b/src/storage/gormstorage/gorm.go new file mode 100644 index 0000000..afc0463 --- /dev/null +++ b/src/storage/gormstorage/gorm.go @@ -0,0 +1,598 @@ +package gormstorage + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "sort" + "strings" + "time" + + "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "google.golang.org/protobuf/proto" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + gormlogger "gorm.io/gorm/logger" + gormtracing "gorm.io/plugin/opentelemetry/tracing" + + "github.com/nianticlabs/modron/src/common" + "github.com/nianticlabs/modron/src/constants" + "github.com/nianticlabs/modron/src/model" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" +) + +const ( + createBatchSize = 500 + listResourcesLimit = 1_000_000 + listObservationsLimit = 1_000_000 + maxUUIDRetries = 3 + sqlDBConnectionTimeout = 3 * time.Second + sqlDBConnectionTimeoutFactor = 3 + dbMaxBootUpWaitTime = 30 * time.Second + + collectionOp opType = "collection" + scanOp opType = "scan" +) + +type opType string + +var ( + log = logrus.StandardLogger().WithField(constants.LogKeyPkg, "gormstorage") + tracer = otel.Tracer("github.com/nianticlabs/modron/src/storage/gormstorage") +) + +// BatchCreateResources creates a batch of resources. +func (svc *Service) BatchCreateResources(ctx context.Context, resources []*pb.Resource) ([]*pb.Resource, error) { + ctx, span := tracer.Start(ctx, "BatchCreateResources") + defer span.End() + span.SetAttributes( + attribute.Int("num_resources", len(resources)), + ) + var toCreateResources []Resource + for _, res := range resources { + if res.Uid == "" { + res.Uid = common.GetUUID(maxUUIDRetries) + } + resPb, err := proto.Marshal(res) + if err != nil { + log.Warnf("proto marshal %q: %v", res.Uid, err) + continue + } + t, err := utils.TypeFromResource(res) + if err != nil { + log.Warnf("type of %q: %v", res.Uid, err) + } + var recordTime *time.Time + ts := res.Timestamp + if ts != nil { + pbTime := ts.AsTime() + recordTime = &pbTime + } + labelsBytes, err := json.Marshal(res.Labels) + if err != nil { + log.Errorf("failed to marshal labels: %v", err) + return nil, err + } + tagsBytes, err := json.Marshal(res.Tags) + if err != nil { + log.Errorf("failed to marshal tags: %v", err) + return nil, err + } + toCreateResources = append(toCreateResources, Resource{ + ID: res.Uid, + Name: res.Name, + DisplayName: res.DisplayName, + ResourceGroupName: res.ResourceGroupName, + CollectionID: res.CollectionUid, + RecordTime: recordTime, + ParentName: res.Parent, + Type: t, + Proto: resPb, + Labels: labelsBytes, + Tags: tagsBytes, + }) + } + if len(toCreateResources) > 0 { + if err := svc.db.WithContext(ctx).CreateInBatches(&toCreateResources, createBatchSize).Error; err != nil { + return nil, fmt.Errorf("insert: %w", err) + } + } + return resources, nil +} + +// ListResources retrieves resources based on the provided filter. +func (svc *Service) ListResources(ctx context.Context, filter model.StorageFilter) (resources []*pb.Resource, err error) { + var sqlResourceRows []Resource + if filter.Limit == 0 { + filter.Limit = listResourcesLimit + } + whereFilter, params := getFilter(filter, collectionOp) + + maxRetries := 3 + for i := 0; i < maxRetries; i++ { + err = + svc.db.WithContext(ctx). + Model(&Resource{}). + Where(whereFilter, params...). + Limit(filter.Limit). + Order("resourcename ASC"). + Find(&sqlResourceRows).Error + if err == nil { + break + } + if errors.Is(err, io.ErrUnexpectedEOF) { + log.Warnf("retrying with exponential backoff (%d/%d): %v", i+1, maxRetries, err) + time.Sleep(time.Duration(math.Pow(2.0, float64(i))) * time.Second) //nolint:mnd + continue + } + break + } + if err != nil { + return nil, fmt.Errorf("select: %w", err) + } + for _, row := range sqlResourceRows { + res, err := row.ToResourceProto() + if err != nil { + log.Warnf("unmarshal: %v", err) + } + resources = append(resources, res) + } + return resources, nil +} + +func getFilter(filter model.StorageFilter, operationName opType) (string, []any) { + var allFilters []string + var params []any + + if filter.OperationID == "" { + // When we don't have the operation ID, we have to get the latest one for the resource group + // This is an expensive operation! + addFilter := "" + if len(filter.ResourceGroupNames) > 0 { + addFilter = "AND resourcegroupname IN ?\n" + } + operationFilter := fmt.Sprintf(`(%s,resourcegroupname) IN ( + SELECT DISTINCT operationid,resourcegroupname FROM operations WHERE (resourcegroupname, endtime) IN ( + SELECT resourcegroupname, timestamp FROM ( + SELECT resourcegroupname, max(endtime) as timestamp + FROM operations AS ts + WHERE + opstype = '%s' AND + status = 'COMPLETED' %s + GROUP BY resourcegroupname + ) + AS ts1 + ) + )`, + operationName+"id", + operationName, + addFilter, + ) + allFilters = append(allFilters, operationFilter) + if addFilter != "" { + params = append(params, filter.ResourceGroupNames) + } + } + + paramFilters, sqlFilterParams := sqlFilterFromModel(filter, operationName) + params = append(params, sqlFilterParams...) + allFilters = append(allFilters, paramFilters...) + finalFilter := strings.Join(allFilters, " AND ") + return finalFilter, params +} + +// BatchCreateObservations creates a batch of observations. +func (svc *Service) BatchCreateObservations(ctx context.Context, observations []*pb.Observation) ([]*pb.Observation, error) { + ctx, span := tracer.Start(ctx, "BatchCreateObservations") + defer span.End() + span.SetAttributes( + attribute.Int("num_observations", len(observations)), + ) + var dbObs []Observation + for _, obs := range observations { + myObs := obs + obsPb, err := proto.Marshal(myObs) + if err != nil { + log.Warnf("proto marshal %q: %v", myObs.Uid, err) + continue + } + rsrcRef := myObs.ResourceRef + var groupName, cloudPlatform string + var externalID, resourceID *string + if rsrcRef != nil { + groupName = rsrcRef.GroupName + resourceID = rsrcRef.Uid + cloudPlatform = rsrcRef.CloudPlatform.String() + externalID = rsrcRef.ExternalId + } + + dbObs = append(dbObs, Observation{ + ID: myObs.Uid, + Name: myObs.Name, + Resource: nil, + ResourceID: resourceID, + ResourceGroupName: groupName, + ResourceCloudPlatform: cloudPlatform, + ResourceExternalID: externalID, + ScanID: myObs.ScanUid, + CollectionID: myObs.CollectionId, + RecordTime: myObs.Timestamp.AsTime(), + Proto: obsPb, + ExternalID: myObs.ExternalId, + Source: ObservationSource(myObs.Source), + Category: ObservationCategory(myObs.Category), + SeverityScore: FromSeverityPb(myObs.Severity), + Impact: Impact(myObs.Impact), + RiskScore: FromSeverityPb(myObs.Severity), + }) + + } + if len(dbObs) > 0 { + if err := svc.db.WithContext(ctx).CreateInBatches(&dbObs, createBatchSize).Error; err != nil { + return nil, fmt.Errorf("insert: %w", err) + } + } + return observations, nil +} + +func validObservation(db *gorm.DB) *gorm.DB { + return db.Where("severity_score >= 0") +} + +// ListObservations retrieves observations based on the provided filter. +func (svc *Service) ListObservations(ctx context.Context, filter model.StorageFilter) (observations []*pb.Observation, err error) { + ctx, span := tracer.Start(ctx, "ListObservations") + defer span.End() + var sqlObservationRows []Observation + if filter.Limit > listObservationsLimit { + return nil, fmt.Errorf("limit too high: %d", filter.Limit) + } + if filter.Limit == 0 { + filter.Limit = listObservationsLimit + } + // Observations generated from a scan (Modron) + scanFilter, scanParams := getFilter(filter, scanOp) + // Observations generated from a collection (SCC, external integrations) + collectFilter, collectParams := getFilter(filter, collectionOp) + err = + svc.db.WithContext(ctx). + Model(&Observation{}). + Scopes(validObservation). + Where( + svc.db.Where(scanFilter, scanParams...).Or(collectFilter, collectParams...), + ). + Order("risk_score DESC, source ASC"). + Limit(filter.Limit). + Find(&sqlObservationRows). + Error + if err != nil { + return nil, fmt.Errorf("select: %w", err) + } + span.SetAttributes( + attribute.Int(constants.TraceKeyNumObservations, len(sqlObservationRows)), + ) + for _, row := range sqlObservationRows { + obs, err := row.ToObservationProto() + if err != nil { + log.Warnf("unmarshal: %v", err) + } + observations = append(observations, obs) + } + return observations, nil +} + +func fromPbOperation(o *pb.Operation) Operation { + var startTime time.Time + var endTime *time.Time + + switch o.Status { + case pb.Operation_STARTED: + startTime = o.StatusTime.AsTime() + case pb.Operation_COMPLETED, pb.Operation_FAILED: + t1 := o.StatusTime.AsTime() + endTime = &t1 + default: + log.Warnf("unknown status: %v", o.Status) + } + + return Operation{ + ID: o.Id, + ResourceGroup: o.ResourceGroup, + OpsType: o.Type, + Status: OperationStatus(o.Status), + StartTime: startTime, + EndTime: endTime, + Reason: o.Reason, + } +} + +// AddOperationLog adds a new operation log entry. +func (svc *Service) AddOperationLog(ctx context.Context, operations []*pb.Operation) error { + var errArr []error + var addOps []Operation + for _, o := range operations { + if o.ResourceGroup == "" { + log.WithField("operation_id", o.Id).Warn("missing resource group for operation") + } + if o.Status != pb.Operation_STARTED { + // We first add new ops + continue + } + addOps = append(addOps, fromPbOperation(o)) + } + if len(addOps) > 0 { + if err := svc.db.WithContext(ctx).Create(&addOps).Error; err != nil { + log.WithError(err).Error("failed to insert operations") + errArr = append(errArr, err) + } + } + + for _, o := range operations { + if o.Status == pb.Operation_STARTED { + // Insert ops were already added + continue + } + opLogger := log.WithFields(logrus.Fields{ + "operation_id": o.Id, + "status": o.Status, + }) + var foundOp Operation + err := svc.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + tx.Find(&foundOp, + "operationid = ? AND opstype = ? AND resourcegroupname = ?", + o.Id, o.Type, o.ResourceGroup, + ) + if errors.Is(tx.Error, gorm.ErrRecordNotFound) { + return fmt.Errorf("operation %q not found", o.Id) + } else if tx.Error != nil { + return fmt.Errorf("unable to find operation: %w", tx.Error) + } + + foundOp.Status = OperationStatus(o.Status) + if o.StatusTime != nil { + t := o.StatusTime.AsTime() + foundOp.EndTime = &t + } + foundOp.Reason = o.Reason + + if err := tx.Save(&foundOp).Error; err != nil { + return fmt.Errorf("failed to update operation: %w", err) + } + return nil + }) + if err != nil { + opLogger.WithError(err).Errorf("failed to perform operation") + errArr = append(errArr, err) + continue + } + } + return errors.Join(errArr...) +} + +// PurgeIncompleteOperations purges incomplete operations from the database. +func (svc *Service) PurgeIncompleteOperations(ctx context.Context) error { + return svc.db.WithContext(ctx). + Model(&Operation{}). + Where("endtime IS NULL"). + Update("endtime", time.Now()). + Update("status", pb.Operation_FAILED).Error +} + +// Deprecated FlushOpsLog flushes the operation log cache to the database. +func (svc *Service) FlushOpsLog(_ context.Context) error { + // NO-OP + return nil +} + +func sqlFilterFromModel(filter model.StorageFilter, operationName opType) (filters []string, params []any) { + if len(filter.ResourceNames) > 0 { + filters = append(filters, "resourceName IN ?") + params = append(params, filter.ResourceNames) + } + if len(filter.ResourceTypes) > 0 { + filters = append(filters, "resourceType IN ?") + params = append(params, filter.ResourceTypes) + } + if len(filter.ResourceGroupNames) > 0 { + filters = append(filters, "resourceGroupName IN ?") + params = append(params, filter.ResourceGroupNames) + } + if len(filter.ResourceIDs) > 0 { + filters = append(filters, "resourceID IN ?") + params = append(params, filter.ResourceIDs) + } + if len(filter.ParentNames) > 0 { + filters = append(filters, "parentName IN ?") + params = append(params, filter.ParentNames) + } + if filter.OperationID != "" { + switch operationName { + case collectionOp: + filters = append(filters, "collectionID = ?") + params = append(params, filter.OperationID) + case scanOp: + filters = append(filters, "scanID = ?") + params = append(params, filter.OperationID) + default: + log.Warnf("unknown operation type: %v", operationName) + } + } + if !filter.StartTime.IsZero() { + filters = append(filters, "recordTime >= ?") + params = append(params, filter.StartTime) + } + return +} + +type Service struct { + db *gorm.DB + cfg Config +} + +// GetChildrenOfResource gets the _children_ of a given resource ID +// when parentResourceName is empty, the whole tree (from the root) is returned +func (svc *Service) GetChildrenOfResource( + ctx context.Context, + collectID string, + parentResourceName string, + resourceType *string, +) (map[string]*pb.RecursiveResource, error) { + var resources []Resource + tx := svc.db.WithContext(ctx) + if resourceType != nil { + tx = tx.Where("resourcetype = ?", *resourceType) + } + if parentResourceName == "" { + tx = tx.Find(&resources, "collectionid = ?", collectID) + } else { + // We use a recursive CTE only in case we have a parentResourceName, otherwise the call is too expensive + tx = tx. + Raw(`WITH RECURSIVE resource_hierarchy(id) AS ( + VALUES(?) + UNION ALL + SELECT resourcename FROM + resources, resource_hierarchy + WHERE + resources.parentname = resource_hierarchy.id AND + resources.collectionid = ? + ) + SELECT resourcename,display_name,parentname,resourcetype,labels + FROM resources + WHERE resourcename IN (SELECT * FROM resource_hierarchy) AND + collectionid = ?`, + parentResourceName, collectID, collectID) + tx = tx.Scan(&resources) + } + + if tx.Error != nil { + return nil, tx.Error + } + + var pbResources []*pb.Resource + for _, r := range resources { + pbRes, err := r.ToResourceProto() + if err != nil { + return nil, err + } + pbResources = append(pbResources, pbRes) + } + return utils.ComputeRgHierarchy(pbResources) +} + +type byTypeAndName []*pb.RecursiveResource + +func (b byTypeAndName) Len() int { + return len(b) +} + +func (b byTypeAndName) Less(i, j int) bool { + // We're lucky that folders/ comes before projects/ (in the alphabet), so it's + // enough to just sort them by their name. Folders will always come before projects. + return b[i].Name < b[j].Name +} + +func (b byTypeAndName) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} + +var _ sort.Interface = byTypeAndName{} + +type Config struct { + BatchSize int32 + LogAllQueries bool +} + +func waitForSQLDatabase(db *gorm.DB) (err error) { + var lastError error + timeOut := sqlDBConnectionTimeout + sqlDb, err := db.DB() + if err != nil { + return err + } + for timeOut < dbMaxBootUpWaitTime { + if err = sqlDb.Ping(); err == nil { + return nil + } + time.Sleep(timeOut) + timeOut = sqlDBConnectionTimeoutFactor * timeOut + } + return lastError +} + +func New(db *gorm.DB, cfg Config) (model.Storage, error) { + if err := db.Use(gormtracing.NewPlugin()); err != nil { + return nil, fmt.Errorf("setup tracing for gorm: %w", err) + } + if cfg.BatchSize <= 0 { + return nil, fmt.Errorf("batch size must be greater than 0") + } + if err := waitForSQLDatabase(db); err != nil { + return nil, err + } + sqlDb, err := db.DB() + if err != nil { + return nil, fmt.Errorf("failed to get database: %w", err) + } + err = sqlDb.Ping() + if err != nil { + return nil, fmt.Errorf("failed to ping database: %w", err) + } + // Automigrate + for _, v := range []any{ + &Resource{}, + &Observation{}, + &Operation{}, + } { + if err := db.AutoMigrate(v); err != nil { + return nil, fmt.Errorf("auto migrate: %w", err) + } + } + + if cfg.LogAllQueries { + db.Config.Logger = gormlogger.Default.LogMode(gormlogger.Info) + } + + return &Service{ + db: db, + cfg: cfg, + }, nil +} + +func NewSQLite(cfg Config, dbPath string) (model.Storage, error) { + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to connect to SQLite: %w", err) + } + phyDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("failed to get DB: %w", err) + } + phyDB.SetMaxOpenConns(1) // https://github.com/mattn/go-sqlite3/issues/274#issuecomment-191597862 + return New(db, cfg) +} + +// NewPostgres creates a new SQL storage service using PostgreSQL. +func NewPostgres(cfg Config, dsn string) (model.Storage, error) { + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to connect to PostgreSQL: %w", err) + } + return New(db, cfg) +} + +func NewDB(driver string, connectionString string, config Config) (model.Storage, error) { + switch driver { + case "sqlite3": + return NewSQLite(config, connectionString) + case "postgres": + return NewPostgres(config, connectionString) + } + return nil, fmt.Errorf("unsupported driver: %s", driver) +} diff --git a/src/storage/gormstorage/gorm_integration_test.go b/src/storage/gormstorage/gorm_integration_test.go new file mode 100644 index 0000000..82480c8 --- /dev/null +++ b/src/storage/gormstorage/gorm_integration_test.go @@ -0,0 +1,65 @@ +//go:build integration + +package gormstorage + +import ( + "context" + "testing" + "time" + + _ "github.com/lib/pq" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" + + "github.com/nianticlabs/modron/src/model" + "github.com/nianticlabs/modron/src/storage/test" +) + +func getPostgresDB(ctx context.Context, t *testing.T) (*postgres.PostgresContainer, error) { + t.Helper() + return postgres.Run(ctx, "postgres:16-alpine", + postgres.WithDatabase("modron"), + testcontainers.WithLogger(testcontainers.TestLogger(t)), + testcontainers.WithWaitStrategy( + wait.ForAll( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(6*time.Second), + wait.ForListeningPort("5432"), + ), + ), + testcontainers.WithHostPortAccess(5432), + ) +} + +func newPostgresTestDb(ctx context.Context, t *testing.T) model.Storage { + t.Helper() + pgDb, err := getPostgresDB(ctx, t) + if err != nil { + t.Fatalf("unable to create postgres container: %v", err) + } + connStr, err := pgDb.ConnectionString(ctx) + if err != nil { + t.Fatalf("unable to get connection string: %v", err) + } + st, err := NewPostgres(Config{ + BatchSize: 10, + }, connStr) + if err != nil { + t.Fatalf("failed to create storage: %v", err) + } + return st +} + +func TestPostgresStorageResource(t *testing.T) { + test.StorageResource(t, newPostgresTestDb(context.Background(), t)) +} + +func TestPostgresStorageObservation(t *testing.T) { + test.StorageObservation(t, newPostgresTestDb(context.Background(), t)) +} + +func TestPostgresStorageListObservationsActive(t *testing.T) { + test.StorageListObservations2(t, newPostgresTestDb(context.Background(), t)) +} diff --git a/src/storage/gormstorage/gorm_test.go b/src/storage/gormstorage/gorm_test.go new file mode 100644 index 0000000..ce3da03 --- /dev/null +++ b/src/storage/gormstorage/gorm_test.go @@ -0,0 +1,32 @@ +package gormstorage + +import ( + "testing" + + "github.com/nianticlabs/modron/src/model" + "github.com/nianticlabs/modron/src/storage/test" + storageutils "github.com/nianticlabs/modron/src/storage/utils" +) + +func newTestDb(t *testing.T) model.Storage { + st, err := NewSQLite(Config{ + BatchSize: 100, + LogAllQueries: true, + }, storageutils.GetSqliteMemoryDbPath()) + if err != nil { + t.Fatalf("failed to create storage: %v", err) + } + return st +} + +func TestStorageResource(t *testing.T) { + test.StorageResource(t, newTestDb(t)) +} + +func TestStorageObservation(t *testing.T) { + test.StorageObservation(t, newTestDb(t)) +} + +func TestStorageListObservationsActive(t *testing.T) { + test.StorageListObservations2(t, newTestDb(t)) +} diff --git a/src/storage/gormstorage/impact.go b/src/storage/gormstorage/impact.go new file mode 100644 index 0000000..31f5d7b --- /dev/null +++ b/src/storage/gormstorage/impact.go @@ -0,0 +1,31 @@ +package gormstorage + +import ( + "database/sql" + "database/sql/driver" + "fmt" + + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +type Impact pb.Impact + +func (i *Impact) Scan(src any) error { + str, ok := src.(string) + if !ok { + return fmt.Errorf("expected string, got %T", src) + } + v, ok := pb.Impact_value[str] + if !ok { + return fmt.Errorf("invalid Impact: %q", str) + } + *i = Impact(v) + return nil +} + +func (i Impact) Value() (driver.Value, error) { + return pb.Impact_name[int32(i)], nil +} + +var _ sql.Scanner = (*Impact)(nil) +var _ driver.Valuer = (*Impact)(nil) diff --git a/src/storage/gormstorage/model.go b/src/storage/gormstorage/model.go new file mode 100644 index 0000000..be6fc2d --- /dev/null +++ b/src/storage/gormstorage/model.go @@ -0,0 +1,133 @@ +package gormstorage + +import ( + "encoding/json" + "fmt" + "time" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +// Resource represents a resource entry in the database. +type Resource struct { + ID string `gorm:"column:resourceid;primaryKey"` + Name string `gorm:"column:resourcename;index:idx_resources_resourcename"` + DisplayName string `gorm:"column:display_name"` + ResourceGroupName string `gorm:"column:resourcegroupname;index:idx_resources_resourcegroupname;index:idx_resources_resourcetype_resourcegroupname;index:idx_collectionid_rgname"` + CollectionID string `gorm:"column:collectionid;index:idx_resources_collectionid;index:idx_collectionid_rgname"` + RecordTime *time.Time `gorm:"column:recordtime;index:idx_resource_recordtime"` + ParentName string `gorm:"column:parentname"` + Type string `gorm:"column:resourcetype;index:idx_resource_resourcetype;index:idx_resources_resourcetype_resourcegroupname"` + Labels json.RawMessage `gorm:"column:labels;type:jsonb"` + Tags json.RawMessage `gorm:"column:tags;type:jsonb"` + Proto []byte `gorm:"column:resourceproto"` +} + +// Observation represents an observation entry in the database. +type Observation struct { + ID string `gorm:"column:observationid;primaryKey"` + Name string `gorm:"column:observationname;not null"` + + // Observations can be associated with either a scan (result of a Modron rule engine execution) or + // a collection (result of fetching them from an external source) + ScanID *string `gorm:"column:scanid;index:idx_observation_scanid;index:idx_observation_resourcegroupname_scan"` + CollectionID *string `gorm:"column:collectionid;index:idx_observation_collectionid"` + + RecordTime time.Time `gorm:"column:recordtime;not null;index:idx_observation_recordtime"` + Proto []byte `gorm:"column:observationproto;not null"` + + // ResourceID is the UUID of the resource, as per the Resource table. + Resource *Resource `gorm:"foreignKey:ResourceID;references:ID"` + ResourceID *string `gorm:"column:resourceid"` + ResourceGroupName string `gorm:"column:resourcegroupname;not null;index:idx_observation_resourcegroupname;index:idx_observation_resourcegroupname_scan"` + ResourceExternalID *string `gorm:"default:null;index:idx_observation_resource_external_id"` + ResourceCloudPlatform string `gorm:"column:resourcecloudplatform;not null;default:'GCP'"` + ExternalID *string `gorm:"index:idx_observation_external_id"` + Source ObservationSource `gorm:"column:source;index:idx_observation_source"` + Category ObservationCategory `gorm:"column:category"` + // SeverityScore represents the original severity (as set by the rule / external observation provider) without taking into account he impact + SeverityScore *SeverityScore `gorm:"column:severity_score;default:null;index:idx_observation_severity_score"` + // Impact is calculated by looking at the Resource Group details (e.g: environment, tags) so that it can be used to calculate the Risk Score + Impact Impact `gorm:"column:impact;default:null"` + // RiskScore represents the final risk score calculated by using the SeverityScore and Impact + RiskScore *SeverityScore `gorm:"column:risk_score;default:null;index:idx_observation_risk_score"` +} + +// ToObservationProto converts an Observation to a pb.Observation. +func (row Observation) ToObservationProto() (*pb.Observation, error) { + obs := &pb.Observation{} + err := proto.Unmarshal(row.Proto, obs) + if err != nil { + return nil, fmt.Errorf("unmarshal observation proto: %w", err) + } + obs.Uid = row.ID + obs.Name = row.Name + obs.ScanUid = row.ScanID + obs.Timestamp = timestamppb.New(row.RecordTime) + + // DB fields + obs.ResourceRef = &pb.ResourceRef{ + Uid: row.ResourceID, + GroupName: row.ResourceGroupName, + CloudPlatform: cloudPlatformFromString(row.ResourceCloudPlatform), + ExternalId: row.ResourceExternalID, + } + obs.Severity = ToSeverity(row.SeverityScore) + obs.Source = pb.Observation_Source(row.Source) + obs.Category = pb.Observation_Category(row.Category) + return obs, nil +} + +func ToSeverity(score *SeverityScore) pb.Severity { + if score == nil { + return pb.Severity_SEVERITY_UNKNOWN + } + return score.ToSeverity() +} + +func cloudPlatformFromString(platform string) pb.CloudPlatform { + switch platform { + case "GCP": + return pb.CloudPlatform_GCP + case "AWS": + return pb.CloudPlatform_AWS + case "AZURE": + return pb.CloudPlatform_AZURE + } + return pb.CloudPlatform_PLATFORM_UNKNOWN +} + +// ToResourceProto converts a Resource to a pb.Resource. +func (row Resource) ToResourceProto() (*pb.Resource, error) { + res := &pb.Resource{} + err := proto.Unmarshal(row.Proto, res) + if err != nil { + return nil, fmt.Errorf("unmarshal resource proto: %w", err) + } + res.Uid = row.ID + res.Name = row.Name + res.ResourceGroupName = row.ResourceGroupName + res.CollectionUid = row.CollectionID + if row.RecordTime != nil { + res.Timestamp = timestamppb.New(*row.RecordTime) + } + res.Parent = row.ParentName + var labels map[string]string + if len(row.Labels) != 0 { + if err := json.Unmarshal(row.Labels, &labels); err != nil { + return nil, fmt.Errorf("unmarshal labels: %w", err) + } + res.Labels = labels + } + var tags map[string]string + if len(row.Tags) != 0 { + if err := json.Unmarshal(row.Tags, &tags); err != nil { + return nil, fmt.Errorf("unmarshal tags: %w", err) + } + res.Tags = tags + } + return res, nil +} diff --git a/src/storage/gormstorage/observation_category.go b/src/storage/gormstorage/observation_category.go new file mode 100644 index 0000000..f5f31f6 --- /dev/null +++ b/src/storage/gormstorage/observation_category.go @@ -0,0 +1,31 @@ +package gormstorage + +import ( + "database/sql" + "database/sql/driver" + "fmt" + + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +type ObservationCategory pb.Observation_Category + +func (o ObservationCategory) Value() (driver.Value, error) { + return pb.Observation_Category_name[int32(o)], nil +} + +func (o *ObservationCategory) Scan(src any) error { + str, ok := src.(string) + if !ok { + return fmt.Errorf("expected string, got %T", src) + } + v, ok := pb.Observation_Category_value[str] + if !ok { + return fmt.Errorf("invalid ObservationCategory: %q", str) + } + *o = ObservationCategory(v) + return nil +} + +var _ sql.Scanner = (*ObservationCategory)(nil) +var _ driver.Valuer = (*ObservationCategory)(nil) diff --git a/src/storage/gormstorage/observation_source.go b/src/storage/gormstorage/observation_source.go new file mode 100644 index 0000000..ecfb7ed --- /dev/null +++ b/src/storage/gormstorage/observation_source.go @@ -0,0 +1,31 @@ +package gormstorage + +import ( + "database/sql" + "database/sql/driver" + "fmt" + + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +type ObservationSource pb.Observation_Source + +func (o ObservationSource) Value() (driver.Value, error) { + return pb.Observation_Source_name[int32(o)], nil +} + +func (o *ObservationSource) Scan(src any) error { + str, ok := src.(string) + if !ok { + return fmt.Errorf("expected string, got %T", src) + } + v, ok := pb.Observation_Source_value[str] + if !ok { + return fmt.Errorf("invalid ObservationSource: %q", str) + } + *o = ObservationSource(v) + return nil +} + +var _ sql.Scanner = (*ObservationSource)(nil) +var _ driver.Valuer = (*ObservationSource)(nil) diff --git a/src/storage/gormstorage/operation.go b/src/storage/gormstorage/operation.go new file mode 100644 index 0000000..81a0c9f --- /dev/null +++ b/src/storage/gormstorage/operation.go @@ -0,0 +1,40 @@ +package gormstorage + +import ( + "database/sql/driver" + "fmt" + "time" + + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +var _ driver.Valuer = (*OperationStatus)(nil) + +type OperationStatus pb.Operation_Status + +func (o OperationStatus) Value() (driver.Value, error) { + return pb.Operation_Status(o).String(), nil +} + +func (o *OperationStatus) Scan(src interface{}) error { + str, ok := src.(string) + if !ok { + return fmt.Errorf("expected string, got %T", src) + } + v, ok := pb.Operation_Status_value[str] + if !ok { + return fmt.Errorf("invalid OperationStatus: %q", str) + } + *o = OperationStatus(v) + return nil +} + +type Operation struct { + ID string `gorm:"column:operationid;primaryKey;index:operations_idx"` + ResourceGroup string `gorm:"column:resourcegroupname;index;primaryKey;index:operations_idx;index:operations_rgname_endtime"` + OpsType string `gorm:"column:opstype;index;primaryKey;index:operations_idx"` + StartTime time.Time `gorm:"column:starttime;index"` + EndTime *time.Time `gorm:"column:endtime;index;index:operations_idx;index:operations_rgname_endtime"` + Status OperationStatus `gorm:"index;index:operations_idx"` + Reason string +} diff --git a/src/storage/gormstorage/severity.go b/src/storage/gormstorage/severity.go new file mode 100644 index 0000000..de953ef --- /dev/null +++ b/src/storage/gormstorage/severity.go @@ -0,0 +1,59 @@ +package gormstorage + +import ( + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +// SeverityScore represents a score for the severity - this number is in the 0.0 - 10.0 range. +type SeverityScore float32 + +const ( + SeverityLowMax = 3.9 + SeverityMediumMax = 6.9 + SeverityHighMax = 8.9 + + SeverityMin = 0.0 + SeverityMax = 10.0 +) + +func (s SeverityScore) ToSeverity() pb.Severity { + if s < 0 { + // This should never happen (SeverityScore should be nil if anything) + return pb.Severity_SEVERITY_UNKNOWN + } + if s == SeverityMin { + return pb.Severity_SEVERITY_INFO + } + if s <= SeverityLowMax { + return pb.Severity_SEVERITY_LOW + } + if s <= SeverityMediumMax { + return pb.Severity_SEVERITY_MEDIUM + } + if s <= SeverityHighMax { + return pb.Severity_SEVERITY_HIGH + } + return pb.Severity_SEVERITY_CRITICAL +} + +func FromSeverityPb(severity pb.Severity) *SeverityScore { + switch severity { + case pb.Severity_SEVERITY_INFO: + info := SeverityScore(0.0) + return &info + case pb.Severity_SEVERITY_LOW: + low := SeverityScore(SeverityLowMax) + return &low + case pb.Severity_SEVERITY_MEDIUM: + medium := SeverityScore(SeverityMediumMax) + return &medium + case pb.Severity_SEVERITY_HIGH: + high := SeverityScore(SeverityHighMax) + return &high + case pb.Severity_SEVERITY_CRITICAL: + critical := SeverityScore(SeverityMax) + return &critical + } + unknownScore := SeverityScore(-1) + return &unknownScore +} diff --git a/src/storage/memstorage/memstorage.go b/src/storage/memstorage/memstorage.go index 7d618f3..aa2c60e 100644 --- a/src/storage/memstorage/memstorage.go +++ b/src/storage/memstorage/memstorage.go @@ -1,300 +1,29 @@ -// Package memstorage provides a storage backend that runs locally in memory. It is supposed to be used primarily for API testing. package memstorage import ( - "context" - "fmt" - "sync" - "time" + "os" + + "github.com/sirupsen/logrus" - "github.com/golang/glog" - "github.com/nianticlabs/modron/src/common" "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + "github.com/nianticlabs/modron/src/storage/gormstorage" + storageutils "github.com/nianticlabs/modron/src/storage/utils" ) -type MemStorage struct { - resources sync.Map - observations sync.Map - operations sync.Map - mostRecentScanID sync.Map -} - -func New() model.Storage { - return &MemStorage{ - resources: sync.Map{}, - observations: sync.Map{}, - operations: sync.Map{}, - mostRecentScanID: sync.Map{}, - } -} - -func (mem *MemStorage) BatchCreateResources(ctx context.Context, resources []*pb.Resource) ([]*pb.Resource, error) { - for _, resource := range resources { - existingRes, ok := mem.resources.Load(resource.ResourceGroupName) - if !ok { - existingRes = []*pb.Resource{} - } - mem.resources.Store(resource.ResourceGroupName, append(existingRes.([]*pb.Resource), resource)) - } - return resources, nil -} - -func (mem *MemStorage) BatchCreateObservations(ctx context.Context, observations []*pb.Observation) ([]*pb.Observation, error) { - for _, o := range observations { - if o.Resource == nil { - glog.Warningf("can't store observation with no attached resource: %+v", o) - continue - } - existingObs, ok := mem.observations.Load(o.Resource.ResourceGroupName) - if !ok { - existingObs = []*pb.Observation{} - } - mem.observations.Store(o.Resource.ResourceGroupName, append(existingObs.([]*pb.Observation), o)) - } - return observations, nil -} - -func (mem *MemStorage) ListResources(ctx context.Context, filter model.StorageFilter) ([]*pb.Resource, error) { - // Resource group filter - latestRes := map[string][]*pb.Resource{} - if filter.ResourceGroupNames == nil { - mem.resources.Range(func(k, v any) bool { - if filteredV, err := filterRes(v.([]*pb.Resource), filter); err != nil { - return false - } else { - latestRes[k.(string)] = filteredV - } - return true - }) - } else { - for _, n := range filter.ResourceGroupNames { - res, ok := mem.resources.Load(n) - if !ok { - continue - } - if filteredV, err := filterRes(res.([]*pb.Resource), filter); err != nil { - return nil, err - } else { - latestRes[n] = filteredV - } - } - } - result := flatValues(latestRes) - if filter.Limit > 0 { - if len(result) > filter.Limit { - return result[:filter.Limit], nil - } - } - return result, nil -} - -func (mem *MemStorage) ListObservations(ctx context.Context, filter model.StorageFilter) ([]*pb.Observation, error) { - // Resource group filter - latestObs := map[string][]*pb.Observation{} - if filter.ResourceGroupNames == nil { - mem.observations.Range(func(k, v any) bool { - if filteredV, err := mem.filterObs(v.([]*pb.Observation), filter); err != nil { - return false - } else { - latestObs[k.(string)] = filteredV - } - return true - }) - } else { - for _, n := range filter.ResourceGroupNames { - ob, ok := mem.observations.Load(n) - if !ok { - continue - } - if filteredV, err := mem.filterObs(ob.([]*pb.Observation), filter); err != nil { - return nil, err - } else { - latestObs[n] = filteredV - } - } - } - - result := flatValues(latestObs) - if filter.Limit > 0 { - if len(result) > filter.Limit { - fmt.Println(filter.Limit) - return result[:filter.Limit], nil - } - } - return result, nil -} - -func (mem *MemStorage) AddOperationLog(ctx context.Context, ops []model.Operation) error { - for _, o := range ops { - mem.operations.Store(o.ID, o) - // Here we assume that operation are always added in chronological order. - if o.OpsType == "scan" && o.Status == model.OperationCompleted { - mem.mostRecentScanID.Store(o.ResourceGroup, o.ID) - } - } - return nil -} - -func (mem *MemStorage) PurgeIncompleteOperations(ctx context.Context) error { - mem.operations = sync.Map{} - return nil -} - -func (mem *MemStorage) FlushOpsLog(ctx context.Context) error { - return nil -} - -func (mem *MemStorage) filterObs(obs []*pb.Observation, filter model.StorageFilter) ([]*pb.Observation, error) { - res := []*pb.Observation{} +const DefaultBatchSize = 100 - if len(obs) == 0 { - return res, nil - } - - // Here we assume that the observations are sorted by date. - for i := len(obs) - 1; i >= 0; i-- { - // TODO: This will fail if we insert observation without a corresponding scan. - mostRecentScanID, ok := mem.mostRecentScanID.Load(obs[i].Resource.ResourceGroupName) - if !ok { - glog.Warningf("no scan found, but observation exist: %v", obs[i]) - continue - } - appendResource := true - if obs[i].ScanUid != mostRecentScanID { - continue - } - if filter.ResourceTypes != nil { - t, err := common.TypeFromResourceAsString(obs[i].Resource) - if err != nil { - return nil, err - } - if _, ok := toSet(filter.ResourceTypes)[t]; !ok { - appendResource = false - } - } - if filter.ResourceIDs != nil { - if _, ok := toSet(filter.ResourceIDs)[obs[i].Resource.Uid]; !ok { - appendResource = false - } - } - if filter.ResourceNames != nil && len(filter.ResourceNames) > 0 { - if _, ok := toSet(filter.ResourceNames)[obs[i].Resource.Name]; !ok { - appendResource = false - } - } - if filter.ParentNames != nil { - if _, ok := toSet(filter.ParentNames)[obs[i].Resource.Parent]; !ok { - appendResource = false - } - } - if filter.ResourceGroupNames != nil { - if _, ok := toSet(filter.ResourceGroupNames)[obs[i].Resource.ResourceGroupName]; !ok { - appendResource = false - } - } - if !filter.StartTime.IsZero() || filter.TimeOffset != 0 { - if !filter.StartTime.IsZero() && filter.TimeOffset != 0 { - timeStamp := obs[i].Timestamp.AsTime() - start, end := extractStartAndEndTimes(filter) - if !timeStamp.After(start) || !timeStamp.Before(end) { - appendResource = false - } - } else { - return nil, fmt.Errorf("StartTime and TimeOffset must both be set") - } - } - if appendResource { - res = append(res, obs[i]) - } - } - return res, nil -} - -func filterRes(resources []*pb.Resource, filter model.StorageFilter) ([]*pb.Resource, error) { - res := []*pb.Resource{} - if len(resources) == 0 { - return res, nil - } +var logger = logrus.StandardLogger() - // Here we assume that the resources are sorted by date. - mostRecentCollectID := resources[len(resources)-1].CollectionUid - for i := len(resources) - 1; i >= 0; i-- { - appendResource := true - if mostRecentCollectID != resources[i].CollectionUid { - break - } - if filter.ResourceTypes != nil { - t, err := common.TypeFromResourceAsString(resources[i]) - if err != nil { - return nil, err - } - if _, ok := toSet(filter.ResourceTypes)[t]; !ok { - appendResource = false - } - } - if filter.ResourceIDs != nil { - if _, ok := toSet(filter.ResourceIDs)[resources[i].Uid]; !ok { - appendResource = false - } - } - if filter.ResourceNames != nil { - if _, ok := toSet(filter.ResourceNames)[resources[i].Name]; !ok { - appendResource = false - } - } - if filter.ParentNames != nil { - if _, ok := toSet(filter.ParentNames)[resources[i].Parent]; !ok { - appendResource = false - } - } - if filter.ResourceGroupNames != nil { - if _, ok := toSet(filter.ResourceGroupNames)[resources[i].ResourceGroupName]; !ok { - appendResource = false - } - } - if !filter.StartTime.IsZero() || filter.TimeOffset != 0 { - if !filter.StartTime.IsZero() && filter.TimeOffset != 0 { - timeStamp := resources[i].GetTimestamp().AsTime() - start, end := extractStartAndEndTimes(filter) - if !timeStamp.After(start) || !timeStamp.Before(end) { - appendResource = false - } - } else { - return nil, fmt.Errorf("StartTime and TimeOffset must both be set") - } - } - if appendResource { - res = append(res, resources[i]) - } - } - return res, nil -} - -func extractStartAndEndTimes(filter model.StorageFilter) (start time.Time, end time.Time) { - startTimeF, offsetTimeF := filter.StartTime, filter.StartTime.Add(filter.TimeOffset) - if startTimeF.Before(offsetTimeF) { - start = startTimeF - end = offsetTimeF - } else { - end = startTimeF - start = offsetTimeF - } - return start, end -} - -func flatValues[T interface{}](m map[string][]T) []T { - res := []T{} - for _, v := range m { - res = append(res, v...) - } - return res -} - -func toSet[T comparable](arr []T) map[T]struct{} { - res := map[T]struct{}{} - for _, e := range arr { - res[e] = struct{}{} - } - return res +func New() model.Storage { + dbPath := storageutils.GetSqliteMemoryDbPath() + logger.Debugf("Using SQLite storage with path: %s", dbPath) + st, err := gormstorage.NewSQLite(gormstorage.Config{ + BatchSize: DefaultBatchSize, + LogAllQueries: os.Getenv("LOG_ALL_SQL_QUERIES") == "true", + }, dbPath) + if err != nil { + // It's fine to panic here, memstorage should only be used in tests + panic(err) + } + return st } diff --git a/src/storage/storage.go b/src/storage/storage.go new file mode 100644 index 0000000..820312b --- /dev/null +++ b/src/storage/storage.go @@ -0,0 +1,8 @@ +package storage + +type Type string + +const ( + SQL Type = "sql" + Memory Type = "memory" +) diff --git a/src/storage/test/test.go b/src/storage/test/test.go index 073d517..5720833 100644 --- a/src/storage/test/test.go +++ b/src/storage/test/test.go @@ -1,4 +1,4 @@ -// Package storage provides a storage backend +// Package test is a collection of test utils for storage tests package test import ( @@ -8,8 +8,11 @@ import ( "testing" "time" + "google.golang.org/protobuf/types/known/structpb" + "github.com/nianticlabs/modron/src/model" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" + "github.com/nianticlabs/modron/src/utils" "github.com/google/go-cmp/cmp" "github.com/google/uuid" @@ -17,39 +20,42 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -// this function checks if the two ResourceEntry objects are equal +const ( + oneDay = 24 * time.Hour +) + func AreEqualResources(t *testing.T, want []*pb.Resource, got []*pb.Resource) { t.Helper() - sort := cmp.Transformer("sort", func(in []*pb.Resource) []*pb.Resource { + sortResources := cmp.Transformer("sort", func(in []*pb.Resource) []*pb.Resource { out := append([]*pb.Resource{}, in...) sort.SliceStable(out, func(i, j int) bool { return out[i].Uid < out[j].Uid }) return out }) - if diff := cmp.Diff(want, got, protocmp.Transform(), sort); diff != "" { + if diff := cmp.Diff(want, got, protocmp.Transform(), sortResources); diff != "" { t.Errorf("unexpected diff (-want, +got): %v", diff) } } func AreEqualObservations(t *testing.T, want []*pb.Observation, got []*pb.Observation) { t.Helper() - sort := cmp.Transformer("sort", func(in []*pb.Observation) []*pb.Observation { + sortObservations := cmp.Transformer("sort", func(in []*pb.Observation) []*pb.Observation { out := append([]*pb.Observation{}, in...) sort.SliceStable(out, func(i, j int) bool { return out[i].Uid < out[j].Uid }) return out }) - if diff := cmp.Diff(want, got, protocmp.Transform(), sort); diff != "" { + if diff := cmp.Diff(want, got, protocmp.Transform(), sortObservations); diff != "" { t.Errorf("unexpected diff (-want, +got): %v", diff) } } // TODO: don't check length of list, but compare the actual returned arrays -func TestStorageResource(t *testing.T, storage model.Storage) { +func StorageResource(t *testing.T, storage model.Storage) { ctx := context.Background() - collectionId := uuid.NewString() + collectionID := uuid.NewString() testResourceName := fmt.Sprintf("test-%s", uuid.NewString()) testResourceName2 := fmt.Sprintf("test2-%s", uuid.NewString()) parentResourceName := fmt.Sprintf("test-parent-%s", uuid.NewString()) @@ -58,8 +64,8 @@ func TestStorageResource(t *testing.T, storage model.Storage) { Name: testResourceName, Parent: parentResourceName, ResourceGroupName: resourceGroupName1, - CollectionUid: collectionId, - Timestamp: timestamppb.New(time.Now().Add(-time.Hour * 24)), + CollectionUid: collectionID, + Timestamp: timestamppb.New(time.Now().Add(-oneDay)), Type: &pb.Resource_ApiKey{ ApiKey: &pb.APIKey{ Scopes: []string{"TEST1"}, @@ -72,8 +78,8 @@ func TestStorageResource(t *testing.T, storage model.Storage) { Name: testResourceName2, Parent: parentResourceName, ResourceGroupName: resourceGroupName2, - CollectionUid: collectionId, - Timestamp: timestamppb.New(time.Now().Add(-time.Hour * 24)), + CollectionUid: collectionID, + Timestamp: timestamppb.New(time.Now().Add(-oneDay)), Type: &pb.Resource_ApiKey{ ApiKey: &pb.APIKey{ Scopes: []string{"TEST2"}, @@ -81,34 +87,35 @@ func TestStorageResource(t *testing.T, storage model.Storage) { }, } - testOps := []model.Operation{ + now := time.Now() + testOps := []*pb.Operation{ { - ID: collectionId, + Id: collectionID, ResourceGroup: resourceGroupName1, - OpsType: "collection", - StatusTime: time.Now(), - Status: model.OperationStarted, + Type: "collection", + StatusTime: timestamppb.New(now), + Status: pb.Operation_STARTED, }, { - ID: collectionId, + Id: collectionID, ResourceGroup: resourceGroupName2, - OpsType: "collection", - StatusTime: time.Now(), - Status: model.OperationStarted, + Type: "collection", + StatusTime: timestamppb.New(now), + Status: pb.Operation_STARTED, }, { - ID: collectionId, + Id: collectionID, ResourceGroup: resourceGroupName1, - OpsType: "collection", - StatusTime: time.Now().Add(time.Second * 60), - Status: model.OperationCompleted, + Type: "collection", + StatusTime: timestamppb.New(now), + Status: pb.Operation_COMPLETED, }, { - ID: collectionId, + Id: collectionID, ResourceGroup: resourceGroupName2, - OpsType: "collection", - StatusTime: time.Now().Add(time.Second * 60), - Status: model.OperationCompleted, + Type: "collection", + StatusTime: timestamppb.New(now), + Status: pb.Operation_COMPLETED, }, } @@ -176,8 +183,8 @@ func TestStorageResource(t *testing.T, storage model.Storage) { if err != nil { t.Errorf("ListResources(ctx, filter) error: %v", err) } - if len(allResources) != 2 { - t.Errorf("len(allResources) got %d, want %d", len(allResources), 2) + if len(allResources) != 2 { //nolint:mnd + t.Errorf("len(allResources) got %d, want %d", len(allResources), 2) //nolint:mnd } // only get one element @@ -218,8 +225,8 @@ func TestStorageResource(t *testing.T, storage model.Storage) { if err != nil { t.Errorf("ListResources(ctx, %v) error: %v", resourceNameFilter, err) } - if len(allResources) != 2 { - t.Errorf("len(allResources) got %d, want %d", len(allResources), 2) + if len(allResources) != 2 { //nolint:mnd + t.Errorf("len(allResources) got %d, want %d", len(allResources), 2) //nolint:mnd } // filter non-existing resourceType @@ -236,8 +243,8 @@ func TestStorageResource(t *testing.T, storage model.Storage) { if err != nil { t.Errorf("ListResources(ctx, %v) error: %v", resourceGroup1Filter, err) } - if len(allResources) != 2 { - t.Errorf("len(allResources) got %d, want %d", len(allResources), 2) + if len(allResources) != 2 { //nolint:mnd + t.Errorf("len(allResources) got %d, want %d", len(allResources), 2) //nolint:mnd } allResources, err = storage.ListResources(ctx, resourceGroup2AndNameFilter) @@ -249,7 +256,7 @@ func TestStorageResource(t *testing.T, storage model.Storage) { } } -func TestStorageObservation(t *testing.T, storage model.Storage) { +func StorageObservation(t *testing.T, storage model.Storage) { ctx := context.Background() parentResourceName := "test-parent" testResourceName := "testResourceName" @@ -284,19 +291,27 @@ func TestStorageObservation(t *testing.T, storage model.Storage) { } testObservation1 := &pb.Observation{ - Uid: "observation1", - Resource: testResource, - Name: "testObservation1", - ScanUid: firstScanUID, - Timestamp: timestamppb.Now(), + Uid: "observation1", + ResourceRef: utils.GetResourceRef(testResource), + Name: "testObservation1", + ScanUid: utils.RefOrNull(firstScanUID), + Timestamp: timestamppb.Now(), + Source: pb.Observation_SOURCE_MODRON, + Severity: pb.Severity_SEVERITY_HIGH, + RiskScore: pb.Severity_SEVERITY_HIGH, + Impact: pb.Impact_IMPACT_MEDIUM, } testObservation2 := &pb.Observation{ - Uid: "observation2", - Resource: testResource2, - Name: "testObservation2", - ScanUid: firstScanUID, - Timestamp: timestamppb.Now(), + Uid: "observation2", + ResourceRef: utils.GetResourceRef(testResource2), + Name: "testObservation2", + ScanUid: utils.RefOrNull(firstScanUID), + Timestamp: timestamppb.Now(), + Source: pb.Observation_SOURCE_MODRON, + Severity: pb.Severity_SEVERITY_HIGH, + RiskScore: pb.Severity_SEVERITY_HIGH, + Impact: pb.Impact_IMPACT_MEDIUM, } // Filters @@ -305,39 +320,44 @@ func TestStorageObservation(t *testing.T, storage model.Storage) { ResourceGroupNames: []string{testResourceGroupName2}, } - testOps := []model.Operation{ + testOps := []*pb.Operation{ { - ID: firstScanUID, + Id: firstScanUID, ResourceGroup: testResourceGroupName1, - OpsType: "scan", - StatusTime: firstScanTime, - Status: model.OperationStarted, + Type: "scan", + StatusTime: timestamppb.New(firstScanTime), + Status: pb.Operation_STARTED, }, { - ID: firstScanUID, + Id: firstScanUID, ResourceGroup: testResourceGroupName2, - OpsType: "scan", - StatusTime: firstScanTime, - Status: model.OperationStarted, + Type: "scan", + StatusTime: timestamppb.New(firstScanTime), + Status: pb.Operation_STARTED, }, { - ID: firstScanUID, + Id: firstScanUID, ResourceGroup: testResourceGroupName1, - OpsType: "scan", - StatusTime: firstScanTime, - Status: model.OperationCompleted, + Type: "scan", + StatusTime: timestamppb.New(firstScanTime), + Status: pb.Operation_COMPLETED, }, { - ID: firstScanUID, + Id: firstScanUID, ResourceGroup: testResourceGroupName2, - OpsType: "scan", - StatusTime: firstScanTime, - Status: model.OperationCompleted, + Type: "scan", + StatusTime: timestamppb.New(firstScanTime), + Status: pb.Operation_COMPLETED, }, } addOps(ctx, t, storage, testOps) + _, err := storage.BatchCreateResources(ctx, []*pb.Resource{testResource, testResource2}) + if err != nil { + t.Fatalf("BatchCreateResources: %v", err) + } + // should not error with empty storage allObservations, err := storage.ListObservations(ctx, model.StorageFilter{ ResourceGroupNames: []string{testResourceGroupName1, testResourceGroupName2}, @@ -392,30 +412,32 @@ func TestStorageObservation(t *testing.T, storage model.Storage) { AreEqualObservations(t, []*pb.Observation{testObservation2}, allObservations) // Run a second scan - secondScanOps := []model.Operation{ + secondScanOps := []*pb.Operation{ { - ID: secondScanUID, + Id: secondScanUID, ResourceGroup: testResourceGroupName1, - OpsType: "scan", - StatusTime: secondScanTime, - Status: model.OperationStarted, + Type: "scan", + StatusTime: timestamppb.New(secondScanTime), + Status: pb.Operation_STARTED, }, { - ID: secondScanUID, + Id: secondScanUID, ResourceGroup: testResourceGroupName1, - OpsType: "scan", - StatusTime: secondScanTime, - Status: model.OperationCompleted, + Type: "scan", + StatusTime: timestamppb.New(secondScanTime), + Status: pb.Operation_COMPLETED, }, } addOps(ctx, t, storage, secondScanOps) testObservationSecondScan := &pb.Observation{ - Uid: "observation3", - Resource: testResource, - Name: "testObservationSecondScan", - ScanUid: secondScanUID, - Timestamp: timestamppb.Now(), + Uid: "observation3", + ResourceRef: utils.GetResourceRef(testResource), + Name: "testObservationSecondScan", + ScanUid: utils.RefOrNull(secondScanUID), + Timestamp: timestamppb.Now(), + Source: pb.Observation_SOURCE_MODRON, + Severity: pb.Severity_SEVERITY_LOW, } _, err = storage.BatchCreateObservations(ctx, []*pb.Observation{testObservationSecondScan}) if err != nil { @@ -434,8 +456,170 @@ func TestStorageObservation(t *testing.T, storage model.Storage) { AreEqualObservations(t, wantObs, gotObs) } -func addOps(ctx context.Context, t *testing.T, storage model.Storage, ops []model.Operation) { - if err := storage.AddOperationLog(context.Background(), ops); err != nil { +func StorageListObservations2(t *testing.T, storage model.Storage) { + ctx := context.Background() + scanUUID := uuid.NewString() + collectionUUID := uuid.NewString() + startTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + endTime := startTime.Add(time.Minute) + + addOps(ctx, t, storage, []*pb.Operation{ + { + Id: collectionUUID, + ResourceGroup: "projects/test-1", + // IMPORTANT: We use "collect" here because the collection of observations from SCC + // is part of the "collection" step - thus we need to make sure that the storage is aware of the fact + // that observations can either come from a "collection" or a "scan" (and they should be merged). + Type: "collection", + StatusTime: timestamppb.New(startTime), + Status: pb.Operation_STARTED, + Reason: "", + }, + { + Id: scanUUID, + ResourceGroup: "projects/test-1", + // IMPORTANT: We use "scan" here because we pretend that we've run a Modron scan that generated + // Modron observations, and these appear only after a "scan" operation. + Type: "scan", + StatusTime: timestamppb.New(startTime), + Status: pb.Operation_STARTED, + Reason: "", + }, + }) + flushOps(ctx, t, storage) + + // Create a resource + resourceUUID := uuid.NewString() + rsrc := &pb.Resource{ + Uid: resourceUUID, + Name: "custom-resource", + Type: &pb.Resource_ApiKey{ApiKey: &pb.APIKey{Scopes: []string{"this", "is", "an", "example"}}}, + } + _, err := storage.BatchCreateResources(ctx, []*pb.Resource{rsrc}) + if err != nil { + t.Fatalf("BatchCreateResources: %v", err) + } + + // Create an observation + scanTs := startTime.Add(10 * time.Second) //nolint:mnd + originalModronObs := &pb.Observation{ + Uid: uuid.NewString(), + ScanUid: utils.RefOrNull(scanUUID), + Timestamp: timestamppb.New(scanTs), + Name: "MY_CUSTOM_OBSERVATION", + ExpectedValue: structpb.NewStringValue("expected"), + ObservedValue: structpb.NewStringValue("observed"), + Remediation: &pb.Remediation{ + Description: "Desc", + Recommendation: "Recommendation", + }, + ResourceRef: &pb.ResourceRef{ + Uid: &resourceUUID, + GroupName: "projects/test-1", + ExternalId: utils.RefOrNull("//cloud.google.com/example"), + CloudPlatform: pb.CloudPlatform_GCP, + }, + + Source: pb.Observation_SOURCE_MODRON, + Severity: pb.Severity_SEVERITY_INFO, + Impact: pb.Impact_IMPACT_MEDIUM, + RiskScore: pb.Severity_SEVERITY_INFO, + } + + unknownSeverityObs := &pb.Observation{ + Uid: uuid.NewString(), + ScanUid: utils.RefOrNull(scanUUID), + Timestamp: timestamppb.New(scanTs), + Name: "UNKNOWN_SEVERITY_OBSERVATIONS_WILL_NEVER_SHOW_UP", + ExpectedValue: structpb.NewStringValue("expected"), + ObservedValue: structpb.NewStringValue("observed"), + Remediation: &pb.Remediation{ + Description: "Desc", + Recommendation: "Recommendation", + }, + ResourceRef: &pb.ResourceRef{ + Uid: &resourceUUID, + GroupName: "projects/test-1", + ExternalId: utils.RefOrNull("//cloud.google.com/example"), + CloudPlatform: pb.CloudPlatform_GCP, + }, + + Source: pb.Observation_SOURCE_MODRON, + Severity: pb.Severity_SEVERITY_UNKNOWN, + Impact: pb.Impact_IMPACT_HIGH, + RiskScore: pb.Severity_SEVERITY_UNKNOWN, + } + + sccObs := &pb.Observation{ + Uid: uuid.NewString(), + CollectionId: utils.RefOrNull(collectionUUID), + Timestamp: timestamppb.New(scanTs), + Name: "GCP_STORAGE_BUCKET_READABLE", + ExpectedValue: nil, + ObservedValue: nil, + Remediation: &pb.Remediation{ + Description: "A world readable GCP Storage bucket was discovered which may contain potentially sensitive data.", + Recommendation: "Investigate whether this bucket should be readable, and if not, adjust the permissions.", + }, + ResourceRef: &pb.ResourceRef{ + Uid: nil, + GroupName: "projects/test-1", + ExternalId: utils.RefOrNull("//storage.googleapis.com/test-dev-example-public"), + CloudPlatform: pb.CloudPlatform_GCP, + }, + ExternalId: utils.RefOrNull("//securitycenter.googleapis.com/projects/12345/sources/123/findings/42000000"), + Source: pb.Observation_SOURCE_SCC, + Category: pb.Observation_CATEGORY_MISCONFIGURATION, + Severity: pb.Severity_SEVERITY_LOW, + Impact: pb.Impact_IMPACT_HIGH, + RiskScore: pb.Severity_SEVERITY_MEDIUM, + } + + listObs := []*pb.Observation{sccObs, originalModronObs, unknownSeverityObs} + _, err = storage.BatchCreateObservations(ctx, listObs) + if err != nil { + t.Fatalf("BatchCreateObservations(ctx, %v) error: %v", listObs, err) + } + addOps(ctx, t, storage, []*pb.Operation{ + { + Id: collectionUUID, + ResourceGroup: "projects/test-1", + Type: "collection", + StatusTime: timestamppb.New(endTime), + Status: pb.Operation_COMPLETED, + Reason: "", + }, + { + Id: scanUUID, + ResourceGroup: "projects/test-1", + Type: "scan", + StatusTime: timestamppb.New(endTime), + Status: pb.Operation_COMPLETED, + Reason: "", + }, + }) + flushOps(ctx, t, storage) + + // Sorted by severity + want := []*pb.Observation{sccObs, originalModronObs} + got, err := storage.ListObservations(ctx, model.StorageFilter{}) + if err != nil { + t.Errorf("ListObservations(ctx, %v) error: %v", model.StorageFilter{}, err) + } + + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("unexpected diff (-want, +got): %v", diff) + } +} + +func flushOps(ctx context.Context, t *testing.T, storage model.Storage) { + if err := storage.FlushOpsLog(ctx); err != nil { + t.Fatalf("Flushops: %v", err) + } +} + +func addOps(ctx context.Context, t *testing.T, storage model.Storage, ops []*pb.Operation) { + if err := storage.AddOperationLog(ctx, ops); err != nil { t.Fatalf("AddOperation unexpected error: %v", err) } if err := storage.FlushOpsLog(ctx); err != nil { diff --git a/src/storage/utils/utils.go b/src/storage/utils/utils.go new file mode 100644 index 0000000..8b11b5b --- /dev/null +++ b/src/storage/utils/utils.go @@ -0,0 +1,19 @@ +package storageutils + +import ( + "fmt" + "os" + + "github.com/google/uuid" +) + +func GetSqliteMemoryDbPath() string { + debugDbPath := os.Getenv("DEBUG_DB_PATH") + if debugDbPath != "" { + return debugDbPath + } + // We use an uniqueID, so that two tests running in parallel do not conflict with each other + uniqueID := uuid.NewString() + // Do not use `:memory:` here! https://github.com/mattn/go-sqlite3/issues/204 + return fmt.Sprintf("file:%s?mode=memory&cache=shared", uniqueID) +} diff --git a/src/test/e2e_test.go b/src/test/e2e_test.go index 97c5f83..7399b1e 100644 --- a/src/test/e2e_test.go +++ b/src/test/e2e_test.go @@ -2,9 +2,14 @@ package e2e import ( "context" + "crypto/ed25519" + "crypto/rand" "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "flag" "fmt" + "math/big" "net" "os" "reflect" @@ -24,7 +29,7 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/nianticlabs/modron/src/pb" + pb "github.com/nianticlabs/modron/src/proto/generated" ) func init() { @@ -46,16 +51,16 @@ var projectListFile string func runFakeNotificationService(t *testing.T, port int64) { t.Helper() - lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port)) + lis, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) if err != nil { t.Fatalf("cannot listen on port %d: %v", port, err) } - srvCert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") + tlsCert, err := getTLSCert() if err != nil { - panic(fmt.Sprintln("load certificate: ", err)) + panic(fmt.Sprintln("generate certificate: ", err)) } grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{ - Certificates: []tls.Certificate{srvCert}, + Certificates: []tls.Certificate{tlsCert}, ClientAuth: tls.NoClientCert, }))) svc := New() @@ -66,12 +71,53 @@ func runFakeNotificationService(t *testing.T, port int64) { } } +func getTLSCert() (tls.Certificate, error) { + public, private, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return tls.Certificate{}, err + } + x509Cert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Country: []string{"US"}, + CommonName: "modron", + }, + } + cert, err := x509.CreateCertificate(rand.Reader, x509Cert, x509Cert, public, private) + if err != nil { + return tls.Certificate{}, err + } + return tls.Certificate{Certificate: [][]byte{cert}, PrivateKey: private}, nil +} + +// TODO: deterministically sort the observations to avoid flaky tests func testModronE2e(t *testing.T, addr string, resourceGroupNames []string, want map[string][]*structpb.Value) { flag.Parse() ctx := context.Background() go func() { runFakeNotificationService(t, extractNotificationServicePortFromEnvironment(t)) }() + + // Wait for backend to be available + var backendAvailable bool + maxTries := 10 + for i := 0; i < maxTries; i++ { + conn, err := net.Dial("tcp", addr) + if err != nil { + fmt.Printf("waiting for backend to be available: %v\n", err) + time.Sleep(time.Second) + continue + } + fmt.Printf("backend is available\n") + backendAvailable = true + conn.Close() + break + } + + if !backendAvailable { + t.Fatalf("backend is not available after %d tries", maxTries) + } + var opts []grpc.DialOption opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) @@ -94,7 +140,7 @@ func testModronE2e(t *testing.T, addr string, resourceGroupNames []string, want } } - allObs := []*pb.Observation{} + var allObs []*pb.Observation for _, el := range listObsResponse.ResourceGroupsObservations { for _, rules := range el.RulesObservations { allObs = append(allObs, rules.Observations...) @@ -103,30 +149,21 @@ func testModronE2e(t *testing.T, addr string, resourceGroupNames []string, want if len(allObs) < 1 { t.Fatalf("no observations returned") } + + // TODO: use a comparer instead of this got := map[string][]*structpb.Value{} for _, ob := range allObs { - if _, ok := got[ob.Resource.Name]; !ok { - got[ob.Resource.Name] = []*structpb.Value{} + if ob.ResourceRef.ExternalId == nil { + t.Errorf("observation %+v has no externalId", ob) + continue } - temp := got[ob.Resource.Name] - got[ob.Resource.Name] = append(temp, ob.ExpectedValue) - } - for k, v := range got { - if diff := cmp.Diff(want[k], v, protocmp.Transform()); diff != "" { - t.Errorf("Resource %v has unexpected observations (-want, +got): %v", k, diff) + if _, ok := got[*ob.ResourceRef.ExternalId]; !ok { + got[*ob.ResourceRef.ExternalId] = []*structpb.Value{} } + temp := got[*ob.ResourceRef.ExternalId] + got[*ob.ResourceRef.ExternalId] = append(temp, ob.ExpectedValue) } - // TODO extract this to its own test - manualObservation := &pb.Observation{ - Resource: allObs[0].GetResource(), - Name: "test-observation", - ObservedValue: structpb.NewStringValue("test observation"), - Remediation: &pb.Remediation{ - Description: "test observation", - Recommendation: "test observation, no recommendation", - }, - } cmpOpts := cmp.Options{ protocmp.Transform(), cmpopts.EquateApproxTime(time.Second), @@ -147,7 +184,26 @@ func testModronE2e(t *testing.T, addr string, resourceGroupNames []string, want return time.Unix(t["seconds"].(int64), 0).UTC() }), ), + cmpopts.SortSlices(func(a, b *structpb.Value) bool { + return a.GetStringValue() < b.GetStringValue() + }), + protocmp.IgnoreFields(&pb.Observation{}, "uid"), + } + if diff := cmp.Diff(want, got, cmpOpts); diff != "" { + t.Errorf("Resources have unexpected observations (-want, +got): %v", diff) } + + // TODO extract this to its own test + manualObservation := &pb.Observation{ + ResourceRef: allObs[0].GetResourceRef(), + Name: "test-observation", + ObservedValue: structpb.NewStringValue("test observation"), + Remediation: &pb.Remediation{ + Description: "test observation", + Recommendation: "test observation, no recommendation", + }, + } + manualObservation.Timestamp = timestamppb.Now() gotManualObs, err := client.CreateObservation(ctx, &pb.CreateObservationRequest{Observation: manualObservation}) if err != nil { @@ -158,7 +214,8 @@ func testModronE2e(t *testing.T, addr string, resourceGroupNames []string, want } } - manualObservation.Resource = &pb.Resource{Name: "non existing"} + nonExisting := "non existing" + manualObservation.ResourceRef = &pb.ResourceRef{ExternalId: &nonExisting} _, err = client.CreateObservation(ctx, &pb.CreateObservationRequest{Observation: manualObservation}) if err == nil { t.Errorf("CreateObservation(ctx, %+v) wanted error, got nil", manualObservation) @@ -232,21 +289,32 @@ func TestModronE2e(t *testing.T) { func TestModronE2eFake(t *testing.T) { want := map[string][]*structpb.Value{ - "account-1": {structpb.NewStringValue(""), structpb.NewStringValue("")}, - "bucket-public": {structpb.NewStringValue("PRIVATE")}, + "//cloudsql.googleapis.com/projects/project-id/instances/xyz": {nil}, + "api-key-unrestricted-0": {structpb.NewStringValue("restricted")}, + "api-key-unrestricted-1": {structpb.NewStringValue("restricted")}, + "api-key-with-overbroad-scope-1": { + structpb.NewStringValue(""), + structpb.NewStringValue(""), + }, + "backend-svc-external-no-modern": {structpb.NewStringValue("TLS 1.2")}, + "backend-svc-no-iap": {structpb.NewBoolValue(true)}, + "bucket-accessible-from-other-project": { + structpb.NewStringValue("prod"), + structpb.NewStringValue("modron-test"), + }, "bucket-public-allusers": {structpb.NewStringValue("PRIVATE")}, - "bucket-accessible-from-other-project": {structpb.NewStringValue("")}, - "api-key-unrestricted-0[]": {structpb.NewStringValue("restricted")}, - "api-key-unrestricted-1[]": {structpb.NewStringValue("restricted")}, - "api-key-with-overbroad-scope-1[]": {structpb.NewStringValue(""), structpb.NewStringValue("")}, - "backend-svc-2[0]": {structpb.NewNumberValue(float64(pb.Certificate_MANAGED))}, - "backend-svc-3[0]": {structpb.NewNumberValue(float64(pb.Certificate_MANAGED))}, - "backend-svc-5[0]": {structpb.NewStringValue("TLS 1.2")}, - "subnetwork-no-private-access-should-be-reported[0]": {structpb.NewStringValue("enabled")}, + "bucket-public": {structpb.NewStringValue("PRIVATE")}, "cloudsql-report-not-enforcing-tls": {structpb.NewBoolValue(true)}, "cloudsql-test-db-public-and-no-authorized-networks": {structpb.NewStringValue("AUTHORIZED_NETWORKS_SET")}, - "instance-1[0]": {structpb.NewStringValue("empty")}, - "projects/modron-test": {structpb.NewStringValue(""), structpb.NewStringValue("")}, + "instance-1": {structpb.NewStringValue("empty")}, + "projects/modron-test": { + structpb.NewStringValue("prod"), + structpb.NewStringValue("modron-test"), + structpb.NewStringValue("modron-test"), + structpb.NewStringValue("No basic roles"), + structpb.NewStringValue("No basic roles"), + }, + "subnetwork-no-private-access-should-be-reported": {structpb.NewStringValue("enabled")}, } testModronE2e(t, fakeServerAddr, []string{"projects/modron-test"}, want) } diff --git a/src/test/fake_notification_service.go b/src/test/fake_notification_service.go index a1425a6..803b3fc 100644 --- a/src/test/fake_notification_service.go +++ b/src/test/fake_notification_service.go @@ -1,6 +1,6 @@ package e2e -import "github.com/nianticlabs/modron/src/pb" +import pb "github.com/nianticlabs/modron/src/proto/generated" func New() pb.NotificationServiceServer { return newFakeServer() diff --git a/src/test/go.mod b/src/test/go.mod index a66c861..a65d275 100644 --- a/src/test/go.mod +++ b/src/test/go.mod @@ -1,20 +1,35 @@ module github.com/nianticlabs/modron/src/e2e_test -go 1.21 +go 1.23.2 -replace github.com/nianticlabs/modron/src/pb => ../proto/ +replace github.com/nianticlabs/modron/src/proto/generated => ../proto/generated require ( - github.com/google/go-cmp v0.5.9 - google.golang.org/grpc v1.57.0 - google.golang.org/protobuf v1.31.0 - github.com/nianticlabs/modron/src/pb v0.0.0-00010101000000-000000000000 + github.com/google/go-cmp v0.6.0 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.35.1 + github.com/nianticlabs/modron/src/proto/generated v0.0.0-00010101000000-000000000000 ) require ( - github.com/golang/protobuf v1.5.3 // indirect - golang.org/x/net v0.14.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/text v0.13.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/api v0.31.2 // indirect + k8s.io/apimachinery v0.31.2 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/src/test/go.sum b/src/test/go.sum new file mode 100644 index 0000000..db82501 --- /dev/null +++ b/src/test/go.sum @@ -0,0 +1,125 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= +k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= +k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= +k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= +k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/src/ui/.gitignore b/src/ui/.gitignore new file mode 100644 index 0000000..61c3bc7 --- /dev/null +++ b/src/ui/.gitignore @@ -0,0 +1 @@ +.yarn diff --git a/src/ui/.yarnrc.yml b/src/ui/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/src/ui/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/src/ui/Dockerfile b/src/ui/Dockerfile index 0a2217a..8496c40 100644 --- a/src/ui/Dockerfile +++ b/src/ui/Dockerfile @@ -1,27 +1,28 @@ -ARG GOVERSION=1.21 +ARG GOVERSION=1.23 +ARG NODE_VERSION=20 -FROM node:18.17.1-alpine AS ui_builder +FROM node:${NODE_VERSION}-alpine AS ui_builder WORKDIR /app -COPY ./client/ . +COPY ./src/ui/client/ . RUN npm install RUN npm run build -FROM golang:${GOVERSION} as server_builder -ENV GOPATH /go +FROM golang:${GOVERSION} AS server_builder +ENV GOPATH=/go WORKDIR /app -COPY go.* ./ +COPY ./src/ui/go.* ./ RUN go mod download -COPY . ./ +COPY ./src/ui/ ./ RUN CGO_ENABLED=0 go build -v -o modron-ui-server -FROM alpine:latest as ca-certificates_builder +FROM alpine:latest AS ca-certificates_builder RUN apk add --no-cache ca-certificates -FROM scratch +# FROM scratch WORKDIR /app -COPY --from=ca-certificates_builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +# COPY --from=ca-certificates_builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=ui_builder /app/dist/ . COPY --from=server_builder /app/modron-ui-server . USER 101:101 EXPOSE 8080 -ENTRYPOINT ["/app/modron-ui-server", "--logtostderr"] +ENTRYPOINT ["/app/modron-ui-server", "-logtostderr"] diff --git a/src/ui/README.md b/src/ui/README.md index 6261859..9244aea 100644 --- a/src/ui/README.md +++ b/src/ui/README.md @@ -2,15 +2,44 @@ User interface for the Modron service. ## Dependencies -- Docker 20.10 (and docker-compose) -- Node 16.16 -- Angular CLI 14.1 +- Docker +- Node.js LTS +- Angular CLI ## How to run ```bash -npm run genproto # Generate gRPC -npm run dev # Run UI with mock gRPC server and envoy proxy +npm run dev # Run UI with mock gRPC server and envoy proxy ``` -Then navigate to `localhost:4200`. \ No newline at end of file +Then navigate to `localhost:4200`. + + +## Developing on MacOS with [Lima](https://github.com/lima-vm/lima) + +### Frontend with `mock-grcp-server` + +When you're developing against the `mock-grpc-server` (`npm run dev`), this will be the setup: + +```mermaid +flowchart LR + subgraph Host + frontend["frontend"] + webpack_proxy["webpack-proxy"] + end + subgraph "Docker Host" + subgraph docker + envoy + mgs["mock-grpc-server"] + end + end + + frontend -- 127.0.0.1:4000 --> webpack_proxy + webpack_proxy -- 127.0.0.1:4201 --> envoy + envoy --> mgs +``` + +### Frontend with Modron as a backend + +1. Start the backend (either with `docker-compose.yml` or via `go run ./src`) and make sure it's listening on `:4201` +1. In this directory (`src/ui`), run `npm run dev:client` diff --git a/src/ui/client/.dockerignore b/src/ui/client/.dockerignore new file mode 100644 index 0000000..bbdb198 --- /dev/null +++ b/src/ui/client/.dockerignore @@ -0,0 +1,6 @@ +/cypress +!/cypress/e2e +!/cypress/components +!/cypress/tsconfig.json +/dist +/results diff --git a/src/ui/client/.eslintignore b/src/ui/client/.eslintignore new file mode 100644 index 0000000..e97b560 --- /dev/null +++ b/src/ui/client/.eslintignore @@ -0,0 +1 @@ +/src/proto/** diff --git a/src/ui/client/Dockerfile b/src/ui/client/Dockerfile index 57f780a..ad502a3 100644 --- a/src/ui/client/Dockerfile +++ b/src/ui/client/Dockerfile @@ -1,5 +1,5 @@ -FROM node:18.17.1-bullseye-slim -RUN apt-get update && apt-get install -y gnupg wget python +FROM node:20-bookworm-slim +RUN apt-get update && apt-get install -y gnupg wget RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' RUN apt-get update && apt-get install -y google-chrome-stable diff --git a/src/ui/client/Dockerfile.e2e b/src/ui/client/Dockerfile.e2e index fd7a2a4..1c7e041 100644 --- a/src/ui/client/Dockerfile.e2e +++ b/src/ui/client/Dockerfile.e2e @@ -1,7 +1,7 @@ -FROM cypress/base:18.16.1 +FROM cypress/base:20.9.0 WORKDIR /app -COPY package.json . -COPY package-lock.json . +COPY ./src/ui/client/package.json . +COPY ./src/ui/client/package-lock.json . ENV CI=1 RUN npm ci RUN npx cypress verify diff --git a/src/ui/client/angular.json b/src/ui/client/angular.json index 7ea7cf9..bd19f3c 100644 --- a/src/ui/client/angular.json +++ b/src/ui/client/angular.json @@ -88,13 +88,13 @@ "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { - "browserTarget": "ui:build:production" + "buildTarget": "ui:build:production" }, "development": { - "browserTarget": "ui:build:development" + "buildTarget": "ui:build:development" }, "developmentLocal": { - "browserTarget": "ui:build:developmentLocal" + "buildTarget": "ui:build:developmentLocal" } }, "defaultConfiguration": "developmentLocal" @@ -102,7 +102,7 @@ "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "ui:build" + "buildTarget": "ui:build" } }, "test": { diff --git a/src/ui/client/cypress/.gitignore b/src/ui/client/cypress/.gitignore new file mode 100644 index 0000000..733b5fd --- /dev/null +++ b/src/ui/client/cypress/.gitignore @@ -0,0 +1,2 @@ +/screenshots +/videos diff --git a/src/ui/client/cypress/e2e/spec.cy.ts b/src/ui/client/cypress/e2e/spec.cy.ts index 6110aa7..6f32edf 100644 --- a/src/ui/client/cypress/e2e/spec.cy.ts +++ b/src/ui/client/cypress/e2e/spec.cy.ts @@ -4,13 +4,31 @@ describe("ModronApp", () => { it("scans, refreshes observations", () => { const group = "modron-test" cy.visit("/") - cy.contains("0 observations").should("be.visible") - cy.contains(group).parents().find(".button").first().click() - cy.contains("Scanning").should("be.visible") + const projectCard = cy.get(".mat-mdc-card").contains(group).parents(".mat-mdc-card") + projectCard.contains("0 observations").should("be.visible") + projectCard.get("mat-progress-bar").should("not.exist") + + const scanButton = cy.get(".scan-all-rgs-button"); + scanButton.should("be.visible") + scanButton.contains("SCAN ALL") + scanButton.click() + projectCard.get("mat-progress-bar").should("be.visible") + cy.get(".scan-all-rgs-button").should("be.disabled") + cy.wait(2000) // Wait for the scan to run - cy.contains("16 observations", { timeout: SCAN_TIMEOUT_MS }).should("be.visible") - cy.contains("Scan").should("be.visible") - cy.contains(group).parents().find(".resourceGroup-info").click() + + projectCard.get(".findings-by-severity", { timeout: SCAN_TIMEOUT_MS }).should("be.visible") + projectCard.get("mat-progress-bar").should("not.exist") + cy.contains("SCAN").should("be.visible") + + // Iterate through the children + projectCard.get(".findings-by-severity > div").then((elements) => { + cy.wrap(elements.eq(0)).contains("5").should("be.visible") + cy.wrap(elements.eq(1)).contains("14").should("be.visible") + cy.wrap(elements.eq(2)).contains("1").should("be.visible") + }) + + projectCard.get(".mat-mdc-card").click() cy.contains("API_KEY_WITH_OVERBROAD_SCOPE").should("be.visible") cy.contains("CROSS_PROJECT_PERMISSIONS").should("be.visible") }) @@ -26,7 +44,7 @@ describe("ModronApp", () => { }) it("creates exceptions", () => { cy.visit("/modron/resourcegroup/projects-modron-test") - cy.get("div.notify-ctn").first().should("be.visible").click() + cy.get("app-notif-bell-button").first().should("be.visible").click() cy.get("textarea[formControlName=\"justification\"]").type("trust me") cy.get("input[formControlName=\"validUntilTime\"]").should(($dateTimePicker: any) => { const date = new Date($dateTimePicker.val()) @@ -35,7 +53,7 @@ describe("ModronApp", () => { }) cy.get("button[type=\"submit\"]").should("be.enabled").click() // Check that the exception is indeed created - cy.get(".notify-ctn>svg").first().should("be.visible").click() + cy.get("app-notif-bell-button").first().should("be.visible").click() cy.contains("trust me").should("be.visible") }) }) diff --git a/src/ui/client/package-lock.json b/src/ui/client/package-lock.json new file mode 100644 index 0000000..b8311df --- /dev/null +++ b/src/ui/client/package-lock.json @@ -0,0 +1,20469 @@ +{ + "name": "ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ui", + "version": "0.0.0", + "dependencies": { + "@angular/animations": "^18.2.1", + "@angular/cdk": "^18.2.1", + "@angular/common": "^18.2.1", + "@angular/compiler": "^18.2.1", + "@angular/core": "^18.2.1", + "@angular/forms": "^18.2.1", + "@angular/material": "^18.2.1", + "@angular/platform-browser": "^18.2.1", + "@angular/platform-browser-dynamic": "^18.2.1", + "@angular/router": "^18.2.1", + "@bufbuild/protobuf": "^2.0.0", + "@grpc/grpc-js": "^1.8.18", + "@material-symbols/font-400": "^0.23.0", + "@types/google-protobuf": "^3.15.6", + "chart.js": "^4.4.4", + "google-protobuf": "^3.21.2", + "grpc-web": "^1.5.0", + "moment": "^2.29.4", + "ng2-charts": "^6.0.1", + "ngx-cookie-service": "^18.0.0", + "ngx-markdown": "^18.0.0", + "rxjs": "~7.8.1", + "tslib": "^2.6.0", + "zone.js": "~0.14.10" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.2.1", + "@angular-eslint/builder": "18.4.0", + "@angular-eslint/eslint-plugin": "18.4.0", + "@angular-eslint/eslint-plugin-template": "18.4.0", + "@angular-eslint/schematics": "18.4.0", + "@angular-eslint/template-parser": "18.4.0", + "@angular/cli": "~18.2.1", + "@angular/compiler-cli": "^18.2.1", + "@cypress/schematic": "^2.5.0", + "@types/jasmine": "~4.3.5", + "@types/jest": "^29.5.12", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "cypress": "^13.14.2", + "eslint": "^8.57.0", + "jasmine-core": "~5.0.1", + "karma": "~6.4.2", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.1", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "karma-junit-reporter": "^2.0.1", + "npm": "^9.8.0", + "ts-protoc-gen": "^0.15.0", + "typescript": "~5.5.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1802.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.11.tgz", + "integrity": "sha512-p+XIc/j51aI83ExNdeZwvkm1F4wkuKMGUUoj0MVUUi5E6NoiMlXYm6uU8+HbRvPBzGy5+3KOiGp3Fks0UmDSAA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "18.2.11", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.11.tgz", + "integrity": "sha512-09Ln3NAdlMw/wMLgnwYU5VgWV5TPBEHolZUIvE9D8b6SFWBCowk3B3RWeAMgg7Peuf9SKwqQHBz2b1C7RTP/8g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1802.11", + "@angular-devkit/build-webpack": "0.1802.11", + "@angular-devkit/core": "18.2.11", + "@angular/build": "18.2.11", + "@babel/core": "7.25.2", + "@babel/generator": "7.25.0", + "@babel/helper-annotate-as-pure": "7.24.7", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.25.0", + "@babel/plugin-transform-async-to-generator": "7.24.7", + "@babel/plugin-transform-runtime": "7.24.7", + "@babel/preset-env": "7.25.3", + "@babel/runtime": "7.25.0", + "@discoveryjs/json-ext": "0.6.1", + "@ngtools/webpack": "18.2.11", + "@vitejs/plugin-basic-ssl": "1.1.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.1.3", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "12.0.2", + "critters": "0.0.24", + "css-loader": "7.1.2", + "esbuild-wasm": "0.23.0", + "fast-glob": "3.3.2", + "http-proxy-middleware": "3.0.3", + "https-proxy-agent": "7.0.5", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "magic-string": "0.30.11", + "mini-css-extract-plugin": "2.9.0", + "mrmime": "2.0.0", + "open": "10.1.0", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "postcss": "8.4.41", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.77.6", + "sass-loader": "16.0.0", + "semver": "7.6.3", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.31.6", + "tree-kill": "1.2.2", + "tslib": "2.6.3", + "vite": "5.4.6", + "watchpack": "2.4.1", + "webpack": "5.94.0", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.0.4", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.23.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "@web/test-runner": "^0.18.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^18.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.6" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1802.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.11.tgz", + "integrity": "sha512-G76rNsyn1iQk7qjyr+K4rnDzfalmEswmwXQorypSDGaHYzIDY1SZXMoP4225WMq5fJNBOJrk82FA0PSfnPE+zQ==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1802.11", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/core": { + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.11.tgz", + "integrity": "sha512-H9P1shRGigORWJHUY2BRa2YurT+DVminrhuaYHsbhXBRsPmgB2Dx/30YLTnC1s5XmR9QIRUCsg/d3kyT1wd5Zg==", + "dev": true, + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.11.tgz", + "integrity": "sha512-efRK3FotTFp4KD5u42jWfXpHUALXB9kJNsWiB4wEImKFH6CN+vjBspJQuLqk2oeBFh/7D2qRMc5P+2tZHM5hdw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "18.2.11", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.11", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-eslint/builder": { + "version": "18.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.4.0.tgz", + "integrity": "sha512-FOzGHX/nHSV1wSduSsabsx3aqC1nfde0opEpEDSOJhxExDxKCwoS1XPy1aERGyKip4ZVA6phC3dLtoBH3QMkVQ==", + "dev": true, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "18.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.4.0.tgz", + "integrity": "sha512-HlFHt2qgdd+jqyVIkCXmrjHauXo/XY3Rp0UNabk83ejGi/raM/6lEFI7iFWzHxLyiAKk4OgGI5W26giSQw991A==", + "dev": true + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "18.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.4.0.tgz", + "integrity": "sha512-Saz9lkWPN3da7ZKW17UsOSN7DeY+TPh+wz/6GCNZCh67Uw2wvMC9agb+4hgpZNXYCP5+u7erqzxQmBoWnS/A+A==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "18.4.0", + "@angular-eslint/utils": "18.4.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "18.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.4.0.tgz", + "integrity": "sha512-n3uZFCy76DnggPqjSVFV3gYD1ik7jCG28o2/HO4kobcMNKnwW8XAlFUagQ4TipNQh7fQiAefsEqvv2quMsYDVw==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "18.4.0", + "@angular-eslint/utils": "18.4.0", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" + }, + "peerDependencies": { + "@typescript-eslint/types": "^7.11.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "18.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-18.4.0.tgz", + "integrity": "sha512-ssqe+0YCfekbWIXNdCrHfoPK/bPZAWybs0Bn/b99dfd8h8uyXkERo9AzIOx4Uyj/08SkP9aPL/0uOOEHDsRGwQ==", + "dev": true, + "dependencies": { + "@angular-eslint/eslint-plugin": "18.4.0", + "@angular-eslint/eslint-plugin-template": "18.4.0", + "ignore": "5.3.2", + "semver": "7.6.3", + "strip-json-comments": "3.1.1" + }, + "peerDependencies": { + "@angular-devkit/core": ">= 18.0.0 < 19.0.0", + "@angular-devkit/schematics": ">= 18.0.0 < 19.0.0" + } + }, + "node_modules/@angular-eslint/template-parser": { + "version": "18.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.4.0.tgz", + "integrity": "sha512-VTep3Xd3IOaRIPL+JN/TV4/2DqUPbjtF3TNY15diD/llnrEhqFnmsvMihexbQyTqzOG+zU554oK44YfvAtHOrw==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "18.4.0", + "eslint-scope": "^8.0.2" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "18.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.4.0.tgz", + "integrity": "sha512-At1yS8GRviGBoaupiQwEOL4/IcZJCE/+2vpXdItMWPGB1HWetxlKAUZTMmIBX/r5Z7CoXxl+LbqpGhrhyzIQAg==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "18.4.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular/animations": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.10.tgz", + "integrity": "sha512-LT5+CocFZJ4t5jXsXLx5w/sBuWSxOEjmNTYga13usRcLOblrAB902pjUdFCHEZyrCUgm4MH8vov9fMS+Ks2GCw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.10" + } + }, + "node_modules/@angular/build": { + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.11.tgz", + "integrity": "sha512-AgirvSCmqUKiDE3C0rl3JA68OkOqQWDKUvjqRHXCkhxldLVOVoeIl87+jBYK/v9gcmk+K+ju+5wbGEfu1FjhiQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1802.11", + "@babel/core": "7.25.2", + "@babel/helper-annotate-as-pure": "7.24.7", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.24.7", + "@inquirer/confirm": "3.1.22", + "@vitejs/plugin-basic-ssl": "1.1.0", + "browserslist": "^4.23.0", + "critters": "0.0.24", + "esbuild": "0.23.0", + "fast-glob": "3.3.2", + "https-proxy-agent": "7.0.5", + "listr2": "8.2.4", + "lmdb": "3.0.13", + "magic-string": "0.30.11", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "rollup": "4.22.4", + "sass": "1.77.6", + "semver": "7.6.3", + "vite": "5.4.6", + "watchpack": "2.4.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "less": "^4.2.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.6" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular/cdk": { + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.11.tgz", + "integrity": "sha512-FuvfhrSz2ch0gyOVHrkWq2C/I2PnOzKYSXlG/VEG+ize/WNrvlYy//5WVrTh/hv+HD9sdoWPr9ULXsfFfgbo7w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^18.0.0 || ^19.0.0", + "@angular/core": "^18.0.0 || ^19.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.11.tgz", + "integrity": "sha512-0JI1xjOLRemBPjdT/yVlabxc3Zkjqa/lhvVxxVC1XhKoW7yGxIGwNrQ4pka4CcQtCuktO6KPMmTGIu8YgC3cpw==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1802.11", + "@angular-devkit/core": "18.2.11", + "@angular-devkit/schematics": "18.2.11", + "@inquirer/prompts": "5.3.8", + "@listr2/prompt-adapter-inquirer": "2.0.15", + "@schematics/angular": "18.2.11", + "@yarnpkg/lockfile": "1.1.0", + "ini": "4.1.3", + "jsonc-parser": "3.3.1", + "listr2": "8.2.4", + "npm-package-arg": "11.0.3", + "npm-pick-manifest": "9.1.0", + "pacote": "18.0.6", + "resolve": "1.22.8", + "semver": "7.6.3", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.10.tgz", + "integrity": "sha512-YzTCmuqLiOuT+Yv07vuKymDWiebOVZ8BuXakJiz4EM7FMoOw5gICHJ04jepZSjDNWpA16e7kJSdt5ucnmvCFDQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.10.tgz", + "integrity": "sha512-cu+Uq1nnyl00Glg0+2uvm+Xpaq5b4YvWpaLGGtit7uGETAJ4L/frlBVeaTRhEoaIAGBI+RRlyuFLae+etQDA0w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.10" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.10.tgz", + "integrity": "sha512-CNFStKWMB89MFKAZZFUOhoQi+fHqRLgNOOrI73LjizXixvngEh3BDZJRtK9hbSGG+giujBrummEA60CWAw69MA==", + "dev": true, + "dependencies": { + "@babel/core": "7.25.2", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "18.2.10", + "typescript": ">=5.4 <5.6" + } + }, + "node_modules/@angular/compiler-cli/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/compiler-cli/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==", + "dev": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/core": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.10.tgz", + "integrity": "sha512-EfxVz0pLaxnOppOYkdhnaUkk8HZT+uxaAGpJD3ppAa7YAWTE9xIGoNJmtS33cZNNOnvriMkdv7yn6pDtV4ct+Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.10" + } + }, + "node_modules/@angular/forms": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.10.tgz", + "integrity": "sha512-2VprGB+enJIeqfz2oALmP/G/UiFzpZW6PHgyZXhk/0J/UMsa26JoYxwDFvfdm/WGTrB+VaQEG7in5xwiFPAFtQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.10", + "@angular/core": "18.2.10", + "@angular/platform-browser": "18.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material": { + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.11.tgz", + "integrity": "sha512-VPfnpwmg6p5DsH1UMfOXjKA+qAbUx6nyinGWpx4+ntr/T1oEhRk5CnoOtVS0Xk0rnRSbEF6ayjDBH2YPR9ol3A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^18.0.0 || ^19.0.0", + "@angular/cdk": "18.2.11", + "@angular/common": "^18.0.0 || ^19.0.0", + "@angular/core": "^18.0.0 || ^19.0.0", + "@angular/forms": "^18.0.0 || ^19.0.0", + "@angular/platform-browser": "^18.0.0 || ^19.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.10.tgz", + "integrity": "sha512-zKyRKFr3AaEA4SE/DEeb5FWHJutT26avHZog6ZGDkMeMN12zMtSqjPuTSgmDXCWleoOkzbb+nhAQ+fK/EyGyPA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "18.2.10", + "@angular/common": "18.2.10", + "@angular/core": "18.2.10" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.10.tgz", + "integrity": "sha512-syKyOTgfQnMxfpDRP1khTSPZ5dsMgA8YQwNF6KsB3eZQl15CKSka7bzjMOUWeZ8M3WShOp1AzTf0MfwNeh0UBA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.10", + "@angular/compiler": "18.2.10", + "@angular/core": "18.2.10", + "@angular/platform-browser": "18.2.10" + } + }, + "node_modules/@angular/router": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.10.tgz", + "integrity": "sha512-ZqJgOGOfvW0epsc7pIo7DffZqYHo3O9aUCVepZAhOxqtjF/sfhX2fy+A0xopTIiR0eM3LrT823V+2hjlBHj+CA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.10", + "@angular/core": "18.2.10", + "@angular/platform-browser": "18.2.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.4.1.tgz", + "integrity": "sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==", + "optional": true, + "dependencies": { + "package-manager-detector": "^0.2.0", + "tinyexec": "^0.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", + "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "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==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz", + "integrity": "sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", + "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", + "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", + "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "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.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@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==", + "dev": true, + "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-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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@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==", + "dev": true, + "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", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "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-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz", + "integrity": "sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-remap-async-to-generator": "^7.25.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/traverse": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", + "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz", + "integrity": "sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", + "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-simple-access": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", + "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz", + "integrity": "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", + "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz", + "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", + "@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-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.0", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.25.0", + "@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-duplicate-named-capturing-groups-regex": "^7.25.0", + "@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.25.1", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.25.2", + "@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.25.0", + "@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/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-regenerator": "^0.6.1", + "core-js-compat": "^3.37.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.0.tgz", + "integrity": "sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==", + "optional": true + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.1.tgz", + "integrity": "sha512-gdWzq7eX017a1kZCU/bP/sbk4e0GZ6idjsXOcMrQwODCb/rx985fHJJ8+hCu79KpuG7PfZh7bo3BBjPH37JuZw==" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "optional": true, + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "optional": true, + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "optional": true + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "optional": true + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "optional": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.6.tgz", + "integrity": "sha512-fi0eVdCOtKu5Ed6+E8mYxUF6ZTFJDZvHogCBelM0xVXmrDEkyM22gRArQzq1YcHPm1V47Vf/iAD+WgVdUlJCGg==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.13.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/schematic": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.5.2.tgz", + "integrity": "sha512-H+V3ZP3KQVOs6b49N66jioXa+rkLzszVi+Bl3jiroVTURUNMOpSa4BOrt10Pn8F57TO0Bamhch2WOk/e9cq98w==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.0.0", + "rxjs": "~6.6.0" + }, + "peerDependencies": { + "@angular/cli": ">=14", + "@angular/core": ">=14" + } + }, + "node_modules/@cypress/schematic/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@cypress/schematic/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz", + "integrity": "sha512-boghen8F0Q8D+0/Q1/1r6DUEieUJ8w2a1gIknExMSHBsJFOr2+0KUfHiVYBvucPwl3+RU5PFBK833FjFCh3BhA==", + "dev": true, + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz", + "integrity": "sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg==", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "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", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "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", + "dev": true + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "optional": true + }, + "node_modules/@iconify/utils": { + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.33.tgz", + "integrity": "sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==", + "optional": true, + "dependencies": { + "@antfu/install-pkg": "^0.4.0", + "@antfu/utils": "^0.7.10", + "@iconify/types": "^2.0.0", + "debug": "^4.3.6", + "kolorist": "^1.8.0", + "local-pkg": "^0.5.0", + "mlly": "^1.7.1" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", + "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.22.tgz", + "integrity": "sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.2.0.tgz", + "integrity": "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.3.0.tgz", + "integrity": "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz", + "integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", + "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.1.0.tgz", + "integrity": "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.2.0.tgz", + "integrity": "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.3.8.tgz", + "integrity": "sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==", + "dev": true, + "dependencies": { + "@inquirer/checkbox": "^2.4.7", + "@inquirer/confirm": "^3.1.22", + "@inquirer/editor": "^2.1.22", + "@inquirer/expand": "^2.1.22", + "@inquirer/input": "^2.2.9", + "@inquirer/number": "^1.0.10", + "@inquirer/password": "^2.1.22", + "@inquirer/rawlist": "^2.2.4", + "@inquirer/search": "^1.0.7", + "@inquirer/select": "^2.4.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.3.0.tgz", + "integrity": "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.1.0.tgz", + "integrity": "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", + "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.0.tgz", + "integrity": "sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg==", + "dev": true, + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.15.tgz", + "integrity": "sha512-MZrGem/Ujjd4cPTLYDfCZK2iKKeiO/8OX13S6jqxldLs0Prf2aGqVlJ77nMBqMv7fzqgXEgjrNHLXcKR8l9lOg==", + "dev": true, + "dependencies": { + "@inquirer/type": "^1.5.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 6" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.0.13.tgz", + "integrity": "sha512-uiKPB0Fv6WEEOZjruu9a6wnW/8jrjzlZbxXscMB8kuCJ1k6kHpcBnuvaAWcqhbI7rqX5GKziwWEdD+wi2gNLfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.0.13.tgz", + "integrity": "sha512-bEVIIfK5mSQoG1R19qA+fJOvCB+0wVGGnXHT3smchBVahYBdlPn2OsZZKzlHWfb1E+PhLBmYfqB5zQXFP7hJig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.0.13.tgz", + "integrity": "sha512-Yml1KlMzOnXj/tnW7yX8U78iAzTk39aILYvCPbqeewAq1kSzl+w59k/fiVkTBfvDi/oW/5YRxL+Fq+Y1Fr1r2Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.0.13.tgz", + "integrity": "sha512-afbVrsMgZ9dUTNUchFpj5VkmJRxvht/u335jUJ7o23YTbNbnpmXif3VKQGCtnjSh+CZaqm6N3CPG8KO3zwyZ1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.0.13.tgz", + "integrity": "sha512-vOtxu0xC0SLdQ2WRXg8Qgd8T32ak4SPqk5zjItRszrJk2BdeXqfGxBJbP7o4aOvSPSmSSv46Lr1EP4HXU8v7Kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.0.13.tgz", + "integrity": "sha512-UCrMJQY/gJnOl3XgbWRZZUvGGBuKy6i0YNSptgMzHBjs+QYDYR1Mt/RLTOPy4fzzves65O1EDmlL//OzEqoLlA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@material-symbols/font-400": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@material-symbols/font-400/-/font-400-0.23.0.tgz", + "integrity": "sha512-sAkBNo8npezd+GDOH/EO5TecwmKx6Ojv2SQ3RpINZTZGtJpNSygfzptlbJ4Ti0xOvET9bmpFVYK44RFBLy8CQg==" + }, + "node_modules/@mermaid-js/parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.3.0.tgz", + "integrity": "sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA==", + "optional": true, + "dependencies": { + "langium": "3.0.0" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@ngtools/webpack": { + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.11.tgz", + "integrity": "sha512-iTdUGJ5O7yMm1DyCzyoMDMxBJ68emUSSXPWbQzEEdcqmtifRebn+VAq4vHN8OmtGM1mtuKeLEsbiZP8ywrw7Ug==", + "dev": true, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "typescript": ">=5.4 <5.6", + "webpack": "^5.54.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", + "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "ini": "^4.1.3", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.1.tgz", + "integrity": "sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==", + "dev": true, + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", + "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "dev": true, + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", + "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", + "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", + "dev": true, + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "proc-log": "^4.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.11.tgz", + "integrity": "sha512-jT54mc9+hPOwie9bji/g2krVuK1kkNh2PNFGwfgCg3Ofmt3hcyOBai1DKuot5uLTX4VCCbvfwiVR/hJniQl2SA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "18.2.11", + "@angular-devkit/schematics": "18.2.11", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", + "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", + "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz", + "integrity": "sha512-c6B0ehIWxMI8wiS/bj6rHMPqeFvngFV7cDU/MY+B16P9Z3Mp9k8L93eYZ7BYzSickzuqAQqAq0V956b3Ju6mLw==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", + "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^13.0.1", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", + "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^2.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", + "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", + "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "dev": true, + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "optional": true, + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "optional": true + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "optional": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "optional": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "optional": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "optional": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "optional": true, + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "optional": true + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "optional": true + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "optional": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "optional": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "optional": true + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "optional": true, + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "optional": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "optional": true + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "optional": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "optional": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "optional": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "optional": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "optional": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "optional": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "optional": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "optional": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", + "optional": true + }, + "node_modules/@types/d3-selection": { + "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==", + "optional": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "optional": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "optional": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "optional": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "optional": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "optional": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "optional": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "optional": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "optional": true + }, + "node_modules/@types/google-protobuf": { + "version": "3.15.12", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz", + "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==" + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.6.tgz", + "integrity": "sha512-3N0FpQTeiWjm+Oo1WUYWguUS7E6JLceiGTriFrG8k5PU7zRLJCzLcWURU3wjMbZGS//a2/LgjsnO3QxIlwxt9g==", + "dev": true + }, + "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/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.8.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz", + "integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", + "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", + "dev": true + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.12.2.tgz", + "integrity": "sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==", + "dev": true, + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.12.2.tgz", + "integrity": "sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==", + "dev": true, + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/typescript-estree": "8.12.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.12.2.tgz", + "integrity": "sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==", + "dev": true, + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.12.2.tgz", + "integrity": "sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==", + "dev": true, + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.12.2.tgz", + "integrity": "sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==", + "dev": true, + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.12.2", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-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==", + "dev": true + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "devOptional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "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==", + "dev": true, + "dependencies": { + "@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" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/bonjour-service": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001676", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001676.tgz", + "integrity": "sha512-Qz6zwGCiPghQXGJvgQAem79esjitvJ+CxSbSQkW9H/UX5hg8XM88d4lp2W+MEQ81j+Hip58Il+jGVdazk1z9cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chart.js": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz", + "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "optional": true, + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "optional": true, + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "optional": true, + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "optional": true + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js-compat": { + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", + "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", + "dev": true, + "dependencies": { + "browserslist": "^4.24.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "optional": true, + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/critters": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.24.tgz", + "integrity": "sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q==", + "deprecated": "Ownership of Critters has moved to the Nuxt team, who will be maintaining the project going forward. If you'd like to keep using Critters, please switch to the actively-maintained fork at https://github.com/danielroe/beasties", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "postcss-media-query-parser": "^0.2.3" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/cypress": { + "version": "13.15.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.1.tgz", + "integrity": "sha512-DwUFiKXo4lef9kA0M4iEhixFqoqp2hw8igr0lTqafRb9qtU3X0XGxKbkSYsUFdkrAkphc7MPDxoNPhk5pj9PVg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.4", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.3", + "tree-kill": "1.2.2", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cypress/node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cypress/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/cypress/node_modules/listr2/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cypress/node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cypress/node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/cypress/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/cytoscape": { + "version": "3.30.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.3.tgz", + "integrity": "sha512-HncJ9gGJbVtw7YXtIs3+6YAFSSiKsom0amWc33Z7QbylbY2JGMrA0yz4EwrdTScZxnwclXeEZHzO5pxoy0ZE4g==", + "optional": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "optional": true, + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "optional": true, + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "optional": true, + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "optional": true + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "optional": true, + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "optional": true, + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "optional": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "optional": true, + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "optional": true, + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "optional": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "optional": true, + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "optional": true, + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "optional": true, + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "optional": true, + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "optional": true + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "optional": true, + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "optional": true + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "optional": true, + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "optional": true, + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "optional": true, + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "optional": true, + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", + "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", + "optional": true, + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "devOptional": true + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "devOptional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/default-gateway/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-gateway/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/default-gateway/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "optional": true, + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==", + "optional": true + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecc-jsbn/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.49", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.49.tgz", + "integrity": "sha512-ZXfs1Of8fDb6z7WEYZjXpgIRF6MEu8JdeGA0A40aZq6OQbS+eJpnnV49epZRna2DU/YsEjSQuGtQPPtvt6J65A==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/emoji-toolkit": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-9.0.1.tgz", + "integrity": "sha512-sMMNqKNLVHXJfIKoPbrRJwtYuysVNC9GlKetr72zE3SSVbHqoeDLWVrxP0uM0AE0qvdl3hbUk+tJhhwXZrDHaw==", + "optional": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "dev": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "devOptional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.23.0.tgz", + "integrity": "sha512-6jP8UmWy6R6TUUV8bMuC3ZyZ6lZKI56x0tkxyCIqWwRRJ/DgeQKneh/Oid5EoGoPFLrGNkz47ZEtWAYuiY/u9g==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "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.", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@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", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "optional": true, + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/google-protobuf": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==" + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/grpc-web": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/grpc-web/-/grpc-web-1.5.0.tgz", + "integrity": "sha512-y1tS3BBIoiVSzKTDF3Hm7E8hV2n7YY7pO0Uo7depfWJqKzWE+SKr0jvHNIJsJJYILQlpYShpi/DRJJMbosgDMQ==" + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "optional": true + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", + "integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.0.1.tgz", + "integrity": "sha512-D4bRej8CplwNtNGyTPD++cafJlZUphzZNV+MSAnbD3er4D0NjL4x9V+mu/SI+5129utnCBen23JwEuBZA9vlpQ==", + "dev": true + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-coverage/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true + }, + "node_modules/karma-junit-reporter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-2.0.1.tgz", + "integrity": "sha512-VtcGfE0JE4OE1wn0LK8xxDKaTP7slN8DO3I+4xg6gAi1IoAHAXOJ1V9G/y45Xg6sxdxPOR3THCFtDlAfBo9Afw==", + "dev": true, + "dependencies": { + "path-is-absolute": "^1.0.0", + "xmlbuilder": "12.0.0" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/katex": { + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", + "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "optional": true, + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "optional": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "optional": true + }, + "node_modules/langium": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.0.0.tgz", + "integrity": "sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg==", + "optional": true, + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/launch-editor": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "optional": true + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/listr2": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.0.13.tgz", + "integrity": "sha512-UGe+BbaSUQtAMZobTb4nHvFMrmvuAQKSeaqAX2meTEQjfsbpl5sxdHD8T72OnwD4GU9uwNhYXIVe4QGs8N9Zyw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "msgpackr": "^1.10.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.0.13", + "@lmdb/lmdb-darwin-x64": "3.0.13", + "@lmdb/lmdb-linux-arm": "3.0.13", + "@lmdb/lmdb-linux-arm64": "3.0.13", + "@lmdb/lmdb-linux-x64": "3.0.13", + "@lmdb/lmdb-win32-x64": "3.0.13" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "optional": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dev": true, + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.14.0.tgz", + "integrity": "sha512-JUeY0F/fQZgIod31Ja1eJgiSxLn7BfQlCnqhwXFBzFHEw63OdLK7VJUJ7bnzNsWgCyoUP5tEp1VRY8rDaYzqOA==", + "dev": true, + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.4.0.tgz", + "integrity": "sha512-mxCfEYvADJqOiHfGpJXLs4/fAjHz448rH0pfY5fAoxiz70rQiDSzUUy4dNET2T08i46IVpjohPd6WWbzmRHiPA==", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^7.0.1", + "@iconify/utils": "^2.1.32", + "@mermaid-js/parser": "^0.3.0", + "@types/d3": "^7.4.3", + "@types/dompurify": "^3.0.5", + "cytoscape": "^3.29.2", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.11", + "dayjs": "^1.11.10", + "dompurify": "^3.0.11 <3.1.7", + "katex": "^0.16.9", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^13.0.2", + "roughjs": "^4.6.6", + "stylis": "^4.3.1", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.1" + } + }, + "node_modules/mermaid/node_modules/marked": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", + "optional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", + "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mlly": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz", + "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", + "optional": true, + "dependencies": { + "acorn": "^8.12.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "devOptional": true + }, + "node_modules/msgpackr": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "dev": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/ng2-charts": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-6.0.1.tgz", + "integrity": "sha512-pO7evbvHqjiKB7zqE12tCKWQI9gmQ8DVOEaWBBLlxJabc4fLGk7o9t4jC4+Q9pJiQrTtQkugm0dIPQ4PFHUaWA==", + "dependencies": { + "lodash-es": "^4.17.15", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=17.0.0", + "@angular/common": ">=17.0.0", + "@angular/core": ">=17.0.0", + "@angular/platform-browser": ">=17.0.0", + "chart.js": "^3.4.0 || ^4.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/ngx-cookie-service": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-18.0.0.tgz", + "integrity": "sha512-hkkUckzZTXXWtFgvVkT2hg6mwYMLXioXDZWBsVCOy9gYkADjsj0N5VViO7eo2izQ0VcMPd/Etog1trf/T4oZMQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@angular/common": "^18.0.0-rc.0", + "@angular/core": "^18.0.0-rc.0" + } + }, + "node_modules/ngx-markdown": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-18.1.0.tgz", + "integrity": "sha512-n4HFSm5oqVMXFuD+WXIVkI6NyxD8Oubr4B3c9U1J7Ptr6t9DVnkNBax3yxWc+8Wli+FXTuGEnDXzB3sp7E9paA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "clipboard": "^2.0.11", + "emoji-toolkit": ">= 8.0.0 < 10.0.0", + "katex": "^0.16.0", + "mermaid": ">= 10.6.0 < 12.0.0", + "prismjs": "^1.28.0" + }, + "peerDependencies": { + "@angular/common": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/platform-browser": "^18.0.0", + "marked": ">= 9.0.0 < 13.0.0", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.0" + } + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/nice-napi/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "optional": true + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", + "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "dev": true, + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm": { + "version": "9.9.3", + "resolved": "https://registry.npmjs.org/npm/-/npm-9.9.3.tgz", + "integrity": "sha512-Z1l+rcQ5kYb17F3hHtO601arEpvdRYnCLtg8xo3AGtyj3IthwaraEOexI9903uANkifFbqHC8hT53KIrozWg8A==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/run-script", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "cli-table3", + "columnify", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "npmlog", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "sigstore", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "dev": true, + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^6.5.0", + "@npmcli/config": "^6.4.0", + "@npmcli/fs": "^3.1.0", + "@npmcli/map-workspaces": "^3.0.4", + "@npmcli/package-json": "^4.0.1", + "@npmcli/promise-spawn": "^6.0.2", + "@npmcli/run-script": "^6.0.2", + "abbrev": "^2.0.0", + "archy": "~1.0.0", + "cacache": "^17.1.4", + "chalk": "^5.3.0", + "ci-info": "^4.0.0", + "cli-columns": "^4.0.0", + "cli-table3": "^0.6.3", + "columnify": "^1.6.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^6.1.1", + "ini": "^4.1.1", + "init-package-json": "^5.0.0", + "is-cidr": "^4.0.2", + "json-parse-even-better-errors": "^3.0.1", + "libnpmaccess": "^7.0.2", + "libnpmdiff": "^5.0.20", + "libnpmexec": "^6.0.4", + "libnpmfund": "^4.2.1", + "libnpmhook": "^9.0.3", + "libnpmorg": "^5.0.4", + "libnpmpack": "^5.0.20", + "libnpmpublish": "^7.5.1", + "libnpmsearch": "^6.0.2", + "libnpmteam": "^5.0.3", + "libnpmversion": "^4.0.2", + "make-fetch-happen": "^11.1.1", + "minimatch": "^9.0.3", + "minipass": "^7.0.4", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^9.4.1", + "nopt": "^7.2.0", + "normalize-package-data": "^5.0.0", + "npm-audit-report": "^5.0.0", + "npm-install-checks": "^6.3.0", + "npm-package-arg": "^10.1.0", + "npm-pick-manifest": "^8.0.2", + "npm-profile": "^7.0.1", + "npm-registry-fetch": "^14.0.5", + "npm-user-validate": "^2.0.0", + "npmlog": "^7.0.1", + "p-map": "^4.0.0", + "pacote": "^15.2.0", + "parse-conflict-json": "^3.0.1", + "proc-log": "^3.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^2.1.0", + "semver": "^7.6.0", + "sigstore": "^1.9.0", + "spdx-expression-parse": "^3.0.1", + "ssri": "^10.0.5", + "supports-color": "^9.4.0", + "tar": "^6.2.0", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^5.0.0", + "which": "^3.0.1", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "dev": true, + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", + "dev": true, + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz", + "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==", + "dev": true, + "dependencies": { + "@npmcli/redact": "^2.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/@colors/colors": { + "version": "1.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/npm/node_modules/@gar/promisify": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "6.5.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^3.1.0", + "@npmcli/installed-package-contents": "^2.0.2", + "@npmcli/map-workspaces": "^3.0.2", + "@npmcli/metavuln-calculator": "^5.0.0", + "@npmcli/name-from-folder": "^2.0.0", + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^4.0.0", + "@npmcli/query": "^3.1.0", + "@npmcli/run-script": "^6.0.0", + "bin-links": "^4.0.1", + "cacache": "^17.0.4", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^6.1.1", + "json-parse-even-better-errors": "^3.0.0", + "json-stringify-nice": "^1.1.4", + "minimatch": "^9.0.0", + "nopt": "^7.0.0", + "npm-install-checks": "^6.2.0", + "npm-package-arg": "^10.1.0", + "npm-pick-manifest": "^8.0.1", + "npm-registry-fetch": "^14.0.3", + "npmlog": "^7.0.1", + "pacote": "^15.0.8", + "parse-conflict-json": "^3.0.0", + "proc-log": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^1.0.2", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^10.0.1", + "treeverse": "^3.0.0", + "walk-up-path": "^3.0.1" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "6.4.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^3.0.2", + "ci-info": "^4.0.0", + "ini": "^4.1.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/disparity-colors": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ansi-styles": "^4.3.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "lib/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^2.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^17.0.0", + "json-parse-even-better-errors": "^3.0.0", + "pacote": "^15.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/move-file": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.1.0", + "glob": "^10.2.2", + "hosted-git-info": "^6.1.1", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.2.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.2.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^1.1.0", + "@sigstore/protobuf-specs": "^0.2.0", + "make-fetch-happen": "^11.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.2.0", + "tuf-js": "^1.1.7" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tootallnate/once": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "1.0.0", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/npm/node_modules/agentkeepalive": { + "version": "4.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/npm/node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/are-we-there-yet": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/builtins": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "17.1.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^7.0.3", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.0.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^4.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cli-table3": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/npm/node_modules/clone": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/color-support": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/npm/node_modules/columnify": { + "version": "1.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/console-control-strings": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/defaults": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/delegates": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/diff": { + "version": "5.2.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/gauge": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^4.0.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "10.3.10", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/has-unicode": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hasown": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "6.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/humanize-ms": { + "version": "1.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "6.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/infer-owner": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/ini": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^10.0.0", + "promzard": "^1.0.0", + "read": "^2.0.0", + "read-package-json": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "9.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^3.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/is-core-module": { + "version": "2.13.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-lambda": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "2.3.6", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/jsbn": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "7.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^10.1.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "5.0.21", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.5.0", + "@npmcli/disparity-colors": "^3.0.0", + "@npmcli/installed-package-contents": "^2.0.2", + "binary-extensions": "^2.2.0", + "diff": "^5.1.0", + "minimatch": "^9.0.0", + "npm-package-arg": "^10.1.0", + "pacote": "^15.0.8", + "tar": "^6.1.13" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "6.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.5.0", + "@npmcli/run-script": "^6.0.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^10.1.0", + "npmlog": "^7.0.1", + "pacote": "^15.0.8", + "proc-log": "^3.0.0", + "read": "^2.0.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "4.2.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.5.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "9.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "5.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "5.0.21", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.5.0", + "@npmcli/run-script": "^6.0.0", + "npm-package-arg": "^10.1.0", + "pacote": "^15.0.8" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "7.5.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ci-info": "^4.0.0", + "normalize-package-data": "^5.0.0", + "npm-package-arg": "^10.1.0", + "npm-registry-fetch": "^14.0.3", + "proc-log": "^3.0.0", + "semver": "^7.3.7", + "sigstore": "^1.4.0", + "ssri": "^10.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "6.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "5.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.0.1", + "@npmcli/run-script": "^6.0.0", + "json-parse-even-better-errors": "^3.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "11.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/make-fetch-happen/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "9.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-json-stream": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/npm/node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "9.4.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/fs": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/abbrev": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { + "version": "16.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { + "version": "10.2.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minipass-fetch": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/ssri": { + "version": "9.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/unique-filename": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/unique-slug": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "7.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "6.3.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "10.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "7.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "14.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npmlog": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^4.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^5.0.0", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/once": { + "version": "1.4.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/pacote": { + "version": "15.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^5.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^1.3.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/pacote/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.10.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "6.0.15", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~1.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json": { + "version": "6.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.6.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "1.9.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^1.1.0", + "@sigstore/protobuf-specs": "^0.2.0", + "@sigstore/sign": "^1.0.0", + "@sigstore/tuf": "^1.0.3", + "make-fetch-happen": "^11.0.1" + }, + "bin": { + "sigstore": "bin/sigstore.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "dev": true, + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.17", + "dev": true, + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "10.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "1.1.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "1.0.4", + "debug": "^4.3.4", + "make-fetch-happen": "^11.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/wcwidth": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/npm/node_modules/which": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/wide-align": { + "version": "1.1.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.2.tgz", + "integrity": "sha512-JTo+4+4Fw7FreyAvlSLjb1BBVaxEQAacmjD3jjuyPZclpbEghTvQZbXBb2qPd2LeIMxiHwXBZUcpmG2Gl/mDEA==", + "dev": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/package-manager-detector": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.2.tgz", + "integrity": "sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==", + "optional": true + }, + "node_modules/pacote": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", + "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", + "dev": true, + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/package-json": "^5.1.0", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^8.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^17.0.0", + "proc-log": "^4.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "devOptional": true, + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "optional": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "optional": true + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/piscina": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz", + "integrity": "sha512-z30AwWGtQE+Apr+2WBZensP2lIvwoaMcOPkQlIEmSGMJNUvaYACylPYrQM6wSdUNJlnDVMSpLv7xTMJqlVshOA==", + "dev": true, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", + "optional": true, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.2", + "pathe": "^1.1.2" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "optional": true + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "optional": true, + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/pretty-format/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/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true + }, + "node_modules/regexpu-core": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "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" + }, + "engines": { + "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==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.2.tgz", + "integrity": "sha512-3OGZZ4HoLJkkAZx/48mTXJNlmqTGOzc0o9OWQPuWpkOlXXPbyN6OafCcoXUnBqE2D3f/T5L+pWc1kdEmnfnRsA==", + "dev": true, + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "optional": true + }, + "node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "optional": true, + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "optional": true + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true + }, + "node_modules/sass": { + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.0.tgz", + "integrity": "sha512-n13Z+3rU9A177dk4888czcVFiC8CL9dii4qpXWUg3YIIgZEvi9TCFKjOQcbK0kJM7DJu9VucrZFddvNfYCPwtw==", + "dev": true, + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "optional": true + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", + "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^2.3.2", + "@sigstore/tuf": "^2.3.4", + "@sigstore/verify": "^1.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", + "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/terser": { + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "optional": true + }, + "node_modules/tinyexec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", + "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "optional": true + }, + "node_modules/tldts": { + "version": "6.1.57", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.57.tgz", + "integrity": "sha512-Oy7yDXK8meJl8vPMOldzA+MtueAJ5BrH4l4HXwZuj2AtfoQbLjmTJmjNWPUcAo+E/ibHn7QlqMS0BOcXJFJyHQ==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.57" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.57", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.57.tgz", + "integrity": "sha512-lXnRhuQpx3zU9EONF9F7HfcRLvN1uRYUBIiKL+C/gehC/77XTU+Jye6ui86GA3rU6FjlJ0triD1Tkjt2F/2lEg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", + "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "optional": true, + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ts-protoc-gen": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/ts-protoc-gen/-/ts-protoc-gen-0.15.0.tgz", + "integrity": "sha512-TycnzEyrdVDlATJ3bWFTtra3SCiEP0W0vySXReAuEygXCUr1j2uaVyL0DhzjwuUdQoW5oXPwk6oZWeA0955V+g==", + "dev": true, + "dependencies": { + "google-protobuf": "^3.15.5" + }, + "bin": { + "protoc-gen-ts": "bin/protoc-gen-ts" + } + }, + "node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" + }, + "node_modules/tuf-js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", + "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "dev": true, + "dependencies": { + "@tufjs/models": "2.0.1", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.39.tgz", + "integrity": "sha512-IZ6acm6RhQHNibSt7+c09hhvsKy9WUr4DVbeq9U8o71qxyYtJpQeDxQnMrVqnIFMLcQjHO0I9wgfO2vIahht4w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "optional": true + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "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==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "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==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vite": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "optional": true, + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "optional": true, + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "optional": true + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "optional": true + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "optional": true + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true + }, + "node_modules/webpack": { + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", + "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.4.0", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "rimraf": "^5.0.5", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.1.0", + "ws": "^8.16.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlbuilder": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-12.0.0.tgz", + "integrity": "sha512-lMo8DJ8u6JRWp0/Y4XLa/atVDr75H9litKlb2E5j3V3MesoL50EBgZDWoLT3F/LztVnG67GjPXLZpqcky/UMnQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz", + "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==" + } + } +} diff --git a/src/ui/client/package.json b/src/ui/client/package.json index 05d83e4..9a5491e 100644 --- a/src/ui/client/package.json +++ b/src/ui/client/package.json @@ -16,41 +16,47 @@ }, "private": true, "dependencies": { - "@angular/animations": "^16.1.5", - "@angular/cdk": "^16.1.5", - "@angular/common": "^16.1.5", - "@angular/compiler": "^16.1.5", - "@angular/core": "^16.1.5", - "@angular/forms": "^16.1.5", - "@angular/material": "^16.1.5", - "@angular/platform-browser": "^16.1.5", - "@angular/platform-browser-dynamic": "^16.1.5", - "@angular/router": "^16.1.5", + "@angular/animations": "^18.2.1", + "@angular/cdk": "^18.2.1", + "@angular/common": "^18.2.1", + "@angular/compiler": "^18.2.1", + "@angular/core": "^18.2.1", + "@angular/forms": "^18.2.1", + "@angular/material": "^18.2.1", + "@angular/platform-browser": "^18.2.1", + "@angular/platform-browser-dynamic": "^18.2.1", + "@angular/router": "^18.2.1", + "@bufbuild/protobuf": "^2.0.0", "@grpc/grpc-js": "^1.8.18", - "@improbable-eng/grpc-web": "^0.15.0", + "@material-symbols/font-400": "^0.23.0", "@types/google-protobuf": "^3.15.6", + "chart.js": "^4.4.4", "google-protobuf": "^3.21.2", - "ngx-cookie-service": "^16.0.0", - "ngx-markdown": "^16.0.0", + "grpc-web": "^1.5.0", + "moment": "^2.29.4", + "ng2-charts": "^6.0.1", + "ngx-cookie-service": "^18.0.0", + "ngx-markdown": "^18.0.0", "rxjs": "~7.8.1", "tslib": "^2.6.0", - "zone.js": "~0.13.1" + "zone.js": "~0.14.10" }, "devDependencies": { - "@angular-devkit/build-angular": "^16.1.4", - "@angular-eslint/builder": "16.1.0", - "@angular-eslint/eslint-plugin": "16.1.0", - "@angular-eslint/eslint-plugin-template": "16.1.0", - "@angular-eslint/schematics": "16.1.0", - "@angular-eslint/template-parser": "16.1.0", - "@angular/cli": "~16.1.4", - "@angular/compiler-cli": "^16.1.5", + "@angular-devkit/build-angular": "^18.2.1", + "@angular-eslint/builder": "18.4.0", + "@angular-eslint/eslint-plugin": "18.4.0", + "@angular-eslint/eslint-plugin-template": "18.4.0", + "@angular-eslint/schematics": "18.4.0", + "@angular-eslint/template-parser": "18.4.0", + "@angular/cli": "~18.2.1", + "@angular/compiler-cli": "^18.2.1", "@cypress/schematic": "^2.5.0", "@types/jasmine": "~4.3.5", - "@typescript-eslint/eslint-plugin": "^6.1.0", - "@typescript-eslint/parser": "^6.1.0", - "cypress": "^12.17.1", - "eslint": "^8.45.0", + "@types/jest": "^29.5.12", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "cypress": "^13.14.2", + "eslint": "^8.57.0", "jasmine-core": "~5.0.1", "karma": "~6.4.2", "karma-chrome-launcher": "~3.2.0", @@ -60,6 +66,6 @@ "karma-junit-reporter": "^2.0.1", "npm": "^9.8.0", "ts-protoc-gen": "^0.15.0", - "typescript": "~5.1.6" + "typescript": "~5.5.4" } } diff --git a/src/ui/client/src/.gitignore b/src/ui/client/src/.gitignore new file mode 100644 index 0000000..32cf1ca --- /dev/null +++ b/src/ui/client/src/.gitignore @@ -0,0 +1,2 @@ +proto/ +!proto/.gitkeep diff --git a/src/ui/client/src/app/app-routing.module.ts b/src/ui/client/src/app/app-routing.module.ts index 5600659..0106fce 100644 --- a/src/ui/client/src/app/app-routing.module.ts +++ b/src/ui/client/src/app/app-routing.module.ts @@ -6,6 +6,7 @@ import { NotificationExceptionsComponent } from "./notification-exceptions/notif import { ResourceGroupDetailsComponent } from "./resource-group-details/resource-group-details.component" import { ResourceGroupsComponent } from "./resource-groups/resource-groups.component" import { StatsComponent } from "./stats/stats.component" +import {UIDemoComponent} from "./ui-demo/ui-demo.component"; const routes: Routes = [ { @@ -24,6 +25,10 @@ const routes: Routes = [ path: "exceptions/new/:notificationName", component: NotificationExceptionFormComponent, }, + { + path: "ui-demo", + component: UIDemoComponent, + } ], }, diff --git a/src/ui/client/src/app/app.module.ts b/src/ui/client/src/app/app.module.ts index 1c1d2cc..dd7e1ea 100644 --- a/src/ui/client/src/app/app.module.ts +++ b/src/ui/client/src/app/app.module.ts @@ -5,23 +5,29 @@ import { AuthenticationStore } from "./state/authentication.store" import { BrowserAnimationsModule } from "@angular/platform-browser/animations" import { BrowserModule } from "@angular/platform-browser" import { CookieService } from "ngx-cookie-service" -import { FilterKeyValuePipe } from "./filter.pipe" +import {FilterKeyValuePipe, ParseExternalIdPipe, ShortenDescriptionPipe, StructValueToStringPipe} from "./filter.pipe" import { FilterNamePipe, MapByTypePipe, MapByObservedValuesPipe, mapFlatRulesPipe } from "./resource-group-details/resource-group-details.pipe" import { FilterNoObservationsPipe, FilterObsPipe, reverseSortPipe } from "./filter.pipe" import { FormsModule, ReactiveFormsModule } from "@angular/forms" -import { HistogramHorizontalComponent } from "./histogram-horizontal/histogram-horizontal.component" +import { ObservationsStatsComponent } from "./observations-stats/observations-stats.component" import { MarkdownModule } from "ngx-markdown" import { MatButtonModule } from "@angular/material/button" import { MatCardModule } from "@angular/material/card" import { MatDatepickerModule } from "@angular/material/datepicker" import { MatDialogModule } from "@angular/material/dialog" +import { MatExpansionModule } from "@angular/material/expansion" import { MatFormFieldModule } from "@angular/material/form-field" import { MatIconModule } from "@angular/material/icon" import { MatInputModule } from "@angular/material/input" -import { MatNativeDateModule } from "@angular/material/core" +import { MatListModule } from "@angular/material/list" +import { MatMenuModule } from "@angular/material/menu"; +import {MatNativeDateModule, MatRippleModule} from "@angular/material/core" import { MatProgressBarModule } from "@angular/material/progress-bar" import { MatSnackBarModule } from "@angular/material/snack-bar" +import { MatSidenavModule } from "@angular/material/sidenav" import { MatTableModule } from "@angular/material/table" +import { MatToolbarModule } from "@angular/material/toolbar"; +import { MatTooltipModule } from "@angular/material/tooltip"; import { ModronAppComponent } from "./modron-app/modron-app.component" import { ModronService } from "./modron.service" import { ModronStore } from "./state/modron.store" @@ -32,40 +38,82 @@ import { NotificationExceptionsFilterPipe } from "./notification-exceptions/noti import { NotificationService } from "./notification.service" import { NotificationStore } from "./state/notification.store" import { ObservationDetailsComponent } from "./observation-details/observation-details.component" -import { ObservationsPipe, MapPerTypeName, ResourceGroupsPipe, InvalidProjectNb, ObsNbPipe } from "./resource-groups/resource-groups.pipe" +import { + ObservationsPipe, + MapPerTypeName, + ResourceGroupsPipe, + InvalidProjectNb, + ObsNbPipe, + MapByRiskScorePipe +} from "./resource-groups/resource-groups.pipe" import { ResourceGroupComponent } from "./resource-group/resource-group.component" import { ResourceGroupDetailsComponent } from "./resource-group-details/resource-group-details.component" import { ResourceGroupsComponent } from "./resource-groups/resource-groups.component" import { SearchObsComponent } from "./search-obs/search-obs.component" +import { SeverityIndicatorComponent } from "./severity-indicator/severity-indicator.component"; import { StatsComponent } from "./stats/stats.component" +import {SidenavComponent} from "./sidenav/sidenav.component"; +import {MatCheckboxModule} from "@angular/material/checkbox"; +import {NgOptimizedImage} from "@angular/common"; +import {MatBadgeModule} from "@angular/material/badge"; +import {FromNowPipe} from "./resource-group/resource-group.pipe"; +import {ImpactNamePipe, SeverityAmountPipe, SeverityNamePipe} from "./severity-indicator/severity-indicator.pipe"; +import {UIDemoComponent} from "./ui-demo/ui-demo.component"; +import {MatSortModule} from "@angular/material/sort"; +import {NotificationBellButtonComponent} from "./notif-bell-button/notif-bell-button.component"; +import {ObservationDetailsDialogComponent} from "./observation-details-dialog/observation-details-dialog.component"; +import { + ObservationDetailsDialogContentComponent +} from "./observation-details-dialog-content/observation-details-dialog-content.component"; +import {ImpactIndicatorComponent} from "./impact-indicator/impact-indicator.component"; +import {CategoryNamePipe} from "./observation-details-dialog-content/observation-details-dialog-content.filter"; +import {ObservationsTableComponent} from "./observations-table/observations-table.component"; +import {BaseChartDirective, provideCharts, withDefaultRegisterables} from "ng2-charts"; +import {MatGridList, MatGridTile} from "@angular/material/grid-list"; @NgModule({ declarations: [ AppComponent, + CategoryNamePipe, FilterKeyValuePipe, FilterNamePipe, FilterNoObservationsPipe, FilterObsPipe, - HistogramHorizontalComponent, + FromNowPipe, + ObservationsStatsComponent, + ImpactIndicatorComponent, + ImpactNamePipe, InvalidProjectNb, MapByObservedValuesPipe, MapByTypePipe, mapFlatRulesPipe, MapPerTypeName, + MapByRiskScorePipe, ModronAppComponent, + NotificationBellButtonComponent, NotificationExceptionFormComponent, NotificationExceptionsComponent, NotificationExceptionsFilterPipe, ObservationDetailsComponent, + ObservationDetailsDialogComponent, + ObservationDetailsDialogContentComponent, ObservationsPipe, + ObservationsTableComponent, ObsNbPipe, + ParseExternalIdPipe, ResourceGroupComponent, ResourceGroupDetailsComponent, ResourceGroupsComponent, ResourceGroupsPipe, reverseSortPipe, SearchObsComponent, + SeverityIndicatorComponent, + SeverityAmountPipe, + SeverityNamePipe, + ShortenDescriptionPipe, StatsComponent, + StructValueToStringPipe, + UIDemoComponent, ], imports: [ AppRoutingModule, @@ -73,18 +121,33 @@ import { StatsComponent } from "./stats/stats.component" BrowserModule, FormsModule, MarkdownModule.forRoot(), + MatBadgeModule, MatButtonModule, MatCardModule, + MatCheckboxModule, MatDatepickerModule, MatDialogModule, + MatExpansionModule, MatFormFieldModule, MatIconModule, MatInputModule, + MatListModule, + MatMenuModule, MatNativeDateModule, MatProgressBarModule, + MatSidenavModule, MatSnackBarModule, MatTableModule, + MatToolbarModule, + MatTooltipModule, + NgOptimizedImage, ReactiveFormsModule, + SidenavComponent, + MatSortModule, + MatRippleModule, + BaseChartDirective, + MatGridList, + MatGridTile, ], providers: [ AuthenticationService, @@ -94,6 +157,7 @@ import { StatsComponent } from "./stats/stats.component" ModronStore, NotificationService, NotificationStore, + provideCharts(withDefaultRegisterables()) ], bootstrap: [AppComponent], }) diff --git a/src/ui/client/src/app/filter.pipe.ts b/src/ui/client/src/app/filter.pipe.ts index da62975..4660197 100644 --- a/src/ui/client/src/app/filter.pipe.ts +++ b/src/ui/client/src/app/filter.pipe.ts @@ -1,7 +1,7 @@ import { KeyValue } from "@angular/common" import { Pipe, PipeTransform } from "@angular/core" import { Value } from "google-protobuf/google/protobuf/struct_pb" -import { Observation, Resource } from "src/proto/modron_pb" +import { Observation, ResourceRef } from "../proto/modron_pb" @Pipe({ name: "filterObs", @@ -26,12 +26,12 @@ export class FilterObsPipe implements PipeTransform { return items.filter((it) => { return ( - (it.getResource() as Resource) - .getName() + (it.getResourceRef() as ResourceRef) + .getExternalId() .toLocaleLowerCase() .includes(resource) && - (it.getResource() as Resource) - .getResourceGroupName().replace("projects/", "") + (it.getResourceRef() as ResourceRef) + .getGroupName().replace("projects/", "") .toLocaleLowerCase() .includes(group) && (it.getObservedValue() @@ -95,3 +95,55 @@ export class reverseSortPipe implements PipeTransform { return items.sort((a, b) => a.value.length - b.value.length).reverse() } } + +@Pipe({ + name: "shortenDescription", +}) +export class ShortenDescriptionPipe implements PipeTransform { + transform(value: string | undefined | null) { + if(value === undefined || value === null) { + return ""; + } + return value.split("\n")[0] + } +} + +@Pipe({ + name: "parseExternalId", +}) +export class ParseExternalIdPipe implements PipeTransform { + transform(value: string | undefined | null) { + if(value === undefined || value === null) { + return ""; + } + + const regex = /^\/\/container\.googleapis\.com\/projects\/[^\\/]+\/locations\/[^\\/]+\/clusters\/[^\\/]+\/k8s\/namespaces\/[^\\/]+\/apps\/((?:deployments|daemonsets)\/[^\\/]+)$/; + const matches = value.match(regex); + if (matches) { + return matches[1]; + } + + return value.split("/", -1).pop() || "" + } +} + +@Pipe({ + name: "structValueToString" +}) +export class StructValueToStringPipe implements PipeTransform { + transform(value: Value | null | undefined): string { + if (value === undefined || value === null) { + return "" + } + if(value.hasBoolValue()) { + return value.getBoolValue().toString() + } + if(value.hasStringValue()) { + return value.getStringValue() + } + if(value.hasNumberValue()) { + return value.getNumberValue().toString() + } + return value.toString() + } +} diff --git a/src/ui/client/src/app/impact-indicator/impact-indicator.component.html b/src/ui/client/src/app/impact-indicator/impact-indicator.component.html new file mode 100644 index 0000000..117d92a --- /dev/null +++ b/src/ui/client/src/app/impact-indicator/impact-indicator.component.html @@ -0,0 +1,3 @@ + + + diff --git a/src/ui/client/src/app/impact-indicator/impact-indicator.component.scss b/src/ui/client/src/app/impact-indicator/impact-indicator.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/client/src/app/impact-indicator/impact-indicator.component.ts b/src/ui/client/src/app/impact-indicator/impact-indicator.component.ts new file mode 100644 index 0000000..8537745 --- /dev/null +++ b/src/ui/client/src/app/impact-indicator/impact-indicator.component.ts @@ -0,0 +1,30 @@ +import {Component, Input} from "@angular/core"; +import {Impact, Severity} from "../../proto/modron_pb"; + +@Component( + { + selector: "app-impact-indicator", + templateUrl: "./impact-indicator.component.html", + styleUrls: ["./impact-indicator.component.scss"], + } +) +export class ImpactIndicatorComponent { + @Input() + public impact: Impact = Impact.IMPACT_UNKNOWN; + constructor() { + } + + // We reuse the severity indicator component to display the impact + public severity(): Severity { + switch (this.impact) { + case Impact.IMPACT_HIGH: + return Severity.SEVERITY_HIGH; + case Impact.IMPACT_MEDIUM: + return Severity.SEVERITY_MEDIUM; + case Impact.IMPACT_LOW: + return Severity.SEVERITY_LOW; + default: + return Severity.SEVERITY_UNKNOWN; + } + } +} diff --git a/src/ui/client/src/app/model/modron.model.ts b/src/ui/client/src/app/model/modron.model.ts index e485704..212aa0c 100644 --- a/src/ui/client/src/app/model/modron.model.ts +++ b/src/ui/client/src/app/model/modron.model.ts @@ -1,4 +1,7 @@ +import {RequestStatus, ScanType} from "../../proto/modron_pb"; + export type StatusInfo = { - state: number - resourceGroups: string[] + state: RequestStatus + resourceGroups: string[] + scanType: ScanType } diff --git a/src/ui/client/src/app/modron-app/modron-app.component.html b/src/ui/client/src/app/modron-app/modron-app.component.html index 70a1e70..3a037ae 100644 --- a/src/ui/client/src/app/modron-app/modron-app.component.html +++ b/src/ui/client/src/app/modron-app/modron-app.component.html @@ -1,40 +1,19 @@ -
-
-
-

{{ organization | uppercase }}

-

MODRON

-
-
- -
- - -
- +
+ +
+ + + Modron + + +
+
diff --git a/src/ui/client/src/app/modron-app/modron-app.component.scss b/src/ui/client/src/app/modron-app/modron-app.component.scss index d79ee23..506218d 100644 --- a/src/ui/client/src/app/modron-app/modron-app.component.scss +++ b/src/ui/client/src/app/modron-app/modron-app.component.scss @@ -1,86 +1,47 @@ -.grey { - color: rgb(220, 220, 220); -} - -svg:focus { - outline: none; -} +@use '../../colors.scss' as colors; -.modron-app-ctn { +.modron-app { + position: absolute; display: flex; - flex-direction: column; - height: 100vh; - - header { - align-items: center; + top: 0; + left: 0; + right: 0; + bottom: 0; + + .sidenav { + top: 0; + bottom: 0; + position: fixed; display: flex; - flex-direction: row; - justify-content: space-between; - padding: 15px 30px; + } - div { - display: flex; - flex-direction: row; - align-items: center; + .app-content { + position: relative; + margin-left: 68px; + width: calc(100% - 68px); - h1 { - margin: 1px 10px; - font-size: 60px; - } + .toolbar { + position: fixed; + display: flex; + height: 60px; + z-index: 300; - svg { - cursor: pointer; - height: 55px; + img.logo { + height: 60%; + margin-right: 4px; } - svg:hover { - fill: rgb(63, 63, 63); + .app-title { + font-weight: 400; + color: colors.$title; + text-transform: uppercase; } } - .logout { - margin: 0px 10px 0px 10px; - cursor: pointer; - } - } - - .modron-app-main { - display: flex; - flex-direction: row; - flex-grow: 1; - - svg { - cursor: pointer; - height: 55px; - } - - .nav-btn { - cursor: pointer; - height: 35px; - padding: 5px 0; - } - - nav { - display: flex; - flex-direction: column; - gap: 40px; - height: 100%; - margin: 10px; - padding: 0 30px; - width: 50px; - } - - .nav-btn>mat-icon { - align-items: center; - display: flex; - height: 55px; - justify-content: center; - font-size: 4em; - } - - .app { - width: 100%; - height: 100%; + .router-container { + margin: 16px; + margin-top: 60px; + overflow: hidden; } } } diff --git a/src/ui/client/src/app/modron-app/modron-app.component.ts b/src/ui/client/src/app/modron-app/modron-app.component.ts index a243794..fc20555 100644 --- a/src/ui/client/src/app/modron-app/modron-app.component.ts +++ b/src/ui/client/src/app/modron-app/modron-app.component.ts @@ -1,5 +1,6 @@ -import { Component } from "@angular/core" -import { environment } from "src/environments/environment" +import { Component, EventEmitter, Input, Output } from "@angular/core" +import { environment } from "../../environments/environment" +import { Router } from "@angular/router"; @Component({ selector: "app-modron-app", @@ -8,12 +9,26 @@ import { environment } from "src/environments/environment" }) export class ModronAppComponent { public organization: string + public href= ""; - constructor() { + @Input() isExpanded: boolean = false; + @Output() toggleMenu = new EventEmitter(); + + constructor(private router: Router) { this.organization = environment.organization } get production(): boolean { return environment.production } + + get currentUrl(): string { + return this.router.url; + } + + public navItems = [ + { link: "/modron/resourcegroups", name: "Resource Groups", icon: "folder" }, + { link: "/modron/stats", name: "Stats", icon: "bar_chart" }, + { link: "/modron/exceptions", name: "Exceptions", icon: "notifications_paused" }, + ]; } diff --git a/src/ui/client/src/app/modron.service.ts b/src/ui/client/src/app/modron.service.ts index da25e23..bd78ab2 100644 --- a/src/ui/client/src/app/modron.service.ts +++ b/src/ui/client/src/app/modron.service.ts @@ -1,10 +1,17 @@ -import { environment } from "src/environments/environment" -import { ModronServiceClient } from "src/proto/modron_pb_service" +import {environment} from "src/environments/environment" +import {ModronServiceClient} from "../proto/ModronServiceClientPb" -import { Injectable } from "@angular/core" -import { concat, EMPTY, from, mergeMap, Observable } from "rxjs" - -import * as pb from "src/proto/modron_pb" +import {Injectable} from "@angular/core" +import {concat, EMPTY, from, mergeMap, Observable} from "rxjs" +import { + CollectAndScanRequest, + CollectAndScanResponse, + GetStatusCollectAndScanRequest, + GetStatusCollectAndScanResponse, + ListObservationsRequest, + ListObservationsResponse, + Observation +} from "../proto/modron_pb"; @Injectable({ providedIn: "root", @@ -22,17 +29,17 @@ export class ModronService { listObservations( resourceGroups: string[] - ): Observable>> { + ): Observable>> { const fetchPage = ( pageToken: string | null - ): Observable => { - const req = new pb.ListObservationsRequest() + ): Observable => { + const req = new ListObservationsRequest() req.setResourceGroupNamesList(resourceGroups) req.setPageSize(ModronService.PAGE_SIZE) req.setPageToken(pageToken ?? "") return new Observable((sub) => { - this._client.listObservations(req, (err, res) => { + this._client.listObservations(req, {}, (err, res) => { if (err !== null) { return sub.error(`listObservations: ${err}`) } @@ -47,13 +54,13 @@ export class ModronService { } const fetchObs = ( pageToken: string | null = null - ): Observable>> => { + ): Observable>> => { return fetchPage(pageToken).pipe( mergeMap((res) => { // deepcode ignore CollectionUpdatedButNeverQueried: Used, false positive. - const obs = new Map>() + const obs = new Map>() res.getResourceGroupsObservationsList().forEach((v) => { - const map = new Map() + const map = new Map() v.getRulesObservationsList().forEach((r) => map.set(r.getRule(), r.getObservationsList()) ) @@ -71,12 +78,12 @@ export class ModronService { return fetchObs() } - collectAndScan(resourceGroups: string[]): Observable { - const fetchPage = (): Observable => { - const req = new pb.CollectAndScanRequest() + collectAndScan(resourceGroups: string[]): Observable { + const fetchPage = (): Observable => { + const req = new CollectAndScanRequest() req.setResourceGroupNamesList(resourceGroups.map( (rg) => { - if (!rg.startsWith("projects/")) { + if (rg.indexOf("/") === -1) { return `projects/${rg}` } return rg @@ -84,7 +91,7 @@ export class ModronService { )) return new Observable((sub) => { - this._client.collectAndScan(req, (err, res) => { + this._client.collectAndScan(req, {}, (err, res) => { if (err !== null) { return sub.error(`collectAndScan: ${err}`) } @@ -98,13 +105,31 @@ export class ModronService { return fetchPage() } - getCollectAndScanStatus(IDs: string): Observable { - const req = new pb.GetStatusCollectAndScanRequest() + collectAndScanAll(): Observable { + const fetchPage = (): Observable => { + const req = new CollectAndScanRequest() + return new Observable((sub) => { + this._client.collectAndScanAll(req, {}, (err, res) => { + if (err !== null) { + return sub.error(`collectAndScanAll: ${err}`) + } + if (res === null) { + return sub.error("collectAndScanAll: unexpected null response") + } + return sub.next(res) + }) + }) + } + return fetchPage() + } + + getCollectAndScanStatus(IDs: string): Observable { + const req = new GetStatusCollectAndScanRequest() req.setCollectId(IDs.split(ModronService.SEPARATOR)[0]) req.setScanId(IDs.split(ModronService.SEPARATOR)[1]) return new Observable((sub) => { - this._client.getStatusCollectAndScan(req, (err, res) => { + this._client.getStatusCollectAndScan(req, {}, (err, res) => { if (err !== null) { return sub.error(`getScanStatus: ${err}`) } diff --git a/src/ui/client/src/app/notif-bell-button/notif-bell-button.component.html b/src/ui/client/src/app/notif-bell-button/notif-bell-button.component.html new file mode 100644 index 0000000..0bdfe5f --- /dev/null +++ b/src/ui/client/src/app/notif-bell-button/notif-bell-button.component.html @@ -0,0 +1,36 @@ +
+
+ + edit_notifications + + + notifications_off + + +
diff --git a/src/ui/client/src/app/notif-bell-button/notif-bell-button.component.scss b/src/ui/client/src/app/notif-bell-button/notif-bell-button.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/client/src/app/notif-bell-button/notif-bell-button.component.ts b/src/ui/client/src/app/notif-bell-button/notif-bell-button.component.ts new file mode 100644 index 0000000..c6e7141 --- /dev/null +++ b/src/ui/client/src/app/notif-bell-button/notif-bell-button.component.ts @@ -0,0 +1,71 @@ +import {Component, Input} from "@angular/core"; +import {Observation} from "../../proto/modron_pb"; +import {NotificationStore} from "../state/notification.store"; +import {NotificationExceptionsFilterPipe} from "../notification-exceptions/notification-exceptions.pipe"; +import {NotificationExceptionFormComponent} from "../notification-exception-form/notification-exception-form.component"; +import {NotificationException} from "../model/notification.model"; +import {MatSnackBar} from "@angular/material/snack-bar"; +import {MatDialog} from "@angular/material/dialog"; +import {Router} from "@angular/router"; + +@Component( + { + selector: "app-notif-bell-button", + templateUrl: "./notif-bell-button.component.html", + styleUrls: ["./notif-bell-button.component.scss"], + } +) +export class NotificationBellButtonComponent { + @Input() + public observation: Observation|undefined; + static readonly SNACKBAR_LINGER_DURATION_MS = 2500; + + constructor( + public notification: NotificationStore, + private _dialog: MatDialog, + private _snackBar: MatSnackBar, + private _router: Router, + ) { + } + + exceptionNameFromObservation(ob: Observation): string { + const resource = ob.getResourceRef() + return `${resource?.getGroupName().replace(new RegExp("/"), "_")}-${resource?.getExternalId()}-${ob.getName()}` + } + + notifyToggle(ob: Observation): void { + const expName = this.exceptionNameFromObservation(ob); + if ( + new NotificationExceptionsFilterPipe().transform( + this.notification.exceptions, + expName + ).length == 0 + ) { + const dialogRef = this._dialog.open(NotificationExceptionFormComponent, { + data: expName, + }); + dialogRef + .afterClosed() + .subscribe((ret: NotificationException | Error | boolean) => { + if(ret === false) { + return + } + if (ret instanceof NotificationException) { + this._snackBar.open( + "Notification exception created successfully", + "", + { + duration: NotificationBellButtonComponent.SNACKBAR_LINGER_DURATION_MS, + } + ); + } else { + this._snackBar.open("Creating notification exception failed", "", { + duration: NotificationBellButtonComponent.SNACKBAR_LINGER_DURATION_MS, + }); + } + }); + } else { + this._router.navigate(["modron", "exceptions", expName]); + } + } +} diff --git a/src/ui/client/src/app/notification-exception-form/notification-exception-form.component.html b/src/ui/client/src/app/notification-exception-form/notification-exception-form.component.html index d7b4feb..b2c6e51 100644 --- a/src/ui/client/src/app/notification-exception-form/notification-exception-form.component.html +++ b/src/ui/client/src/app/notification-exception-form/notification-exception-form.component.html @@ -60,12 +60,11 @@

New exception

- + + + diff --git a/src/ui/client/src/app/observation-details-dialog-content/observation-details-dialog-content.component.scss b/src/ui/client/src/app/observation-details-dialog-content/observation-details-dialog-content.component.scss new file mode 100644 index 0000000..15d0380 --- /dev/null +++ b/src/ui/client/src/app/observation-details-dialog-content/observation-details-dialog-content.component.scss @@ -0,0 +1,69 @@ +.dialog-content { + display: flex; + flex-direction: column; + width: 100%; + + h1, h2, h3, h4, h5, h6 { + margin-bottom: 0.5em; + } + + p { + margin-top: 0; + } +} + +.observation-details { + display: grid; + grid-template-columns: 1fr 2fr; + margin: 16px; + row-gap: 4px; + + p { + margin: 0; + } + + .hdr { + font-weight: bold; + } +} + +.risk-score { + display: grid; + grid-template-columns: 80px 1fr; + column-gap: 8px; + align-items: center; + width: 100%; + + .main-indicator { + justify-self: center; + } + + .severity-impact-container { + display: grid; + align-items: start; + row-gap: 18px; + grid-template-columns: 80px 1fr; + margin-top: 1em; + + .indicator { + justify-self: center; + } + + div.expl { + h1,h2,h3,h4,h5,h6 { + margin: 0 0 0.5em; + } + + code { + background-color: #eeeeee; + padding: 3px; + } + + } + } +} + +.short-risk-score-description { + margin-top: 1em; + margin-bottom: 1em; +} diff --git a/src/ui/client/src/app/observation-details-dialog-content/observation-details-dialog-content.component.ts b/src/ui/client/src/app/observation-details-dialog-content/observation-details-dialog-content.component.ts new file mode 100644 index 0000000..a3ae3d2 --- /dev/null +++ b/src/ui/client/src/app/observation-details-dialog-content/observation-details-dialog-content.component.ts @@ -0,0 +1,15 @@ +import {Component, Input} from "@angular/core"; +import {Impact, Observation} from "../../proto/modron_pb"; + +@Component({ + selector: "app-observation-details-dialog-content", + templateUrl: "./observation-details-dialog-content.component.html", + styleUrls: ["./observation-details-dialog-content.component.scss"], +}) +export class ObservationDetailsDialogContentComponent { + @Input() + observation!: Observation; + constructor() {} + + protected readonly Impact = Impact; +} diff --git a/src/ui/client/src/app/observation-details-dialog-content/observation-details-dialog-content.filter.ts b/src/ui/client/src/app/observation-details-dialog-content/observation-details-dialog-content.filter.ts new file mode 100644 index 0000000..f40caf0 --- /dev/null +++ b/src/ui/client/src/app/observation-details-dialog-content/observation-details-dialog-content.filter.ts @@ -0,0 +1,21 @@ +import {Pipe, PipeTransform} from "@angular/core"; +import {Observation} from "../../proto/modron_pb"; +import Category = Observation.Category; + +@Pipe({ + name: "categoryName" +}) +export class CategoryNamePipe implements PipeTransform { + transform(cat: Category): string { + switch(cat) { + case Category.CATEGORY_VULNERABILITY: + return "Vulnerability"; + case Category.CATEGORY_MISCONFIGURATION: + return "Misconfiguration"; + case Category.CATEGORY_TOXIC_COMBINATION: + return "Toxic Combination"; + default: + return "Unknown"; + } + } +} diff --git a/src/ui/client/src/app/observation-details-dialog/observation-details-dialog.component.html b/src/ui/client/src/app/observation-details-dialog/observation-details-dialog.component.html new file mode 100644 index 0000000..db47510 --- /dev/null +++ b/src/ui/client/src/app/observation-details-dialog/observation-details-dialog.component.html @@ -0,0 +1,5 @@ +
+ +
diff --git a/src/ui/client/src/app/observation-details-dialog/observation-details-dialog.component.scss b/src/ui/client/src/app/observation-details-dialog/observation-details-dialog.component.scss new file mode 100644 index 0000000..de724ad --- /dev/null +++ b/src/ui/client/src/app/observation-details-dialog/observation-details-dialog.component.scss @@ -0,0 +1,3 @@ +.observation-details-dialog { + padding: 20px; +} diff --git a/src/ui/client/src/app/observation-details-dialog/observation-details-dialog.component.ts b/src/ui/client/src/app/observation-details-dialog/observation-details-dialog.component.ts new file mode 100644 index 0000000..76c8c4b --- /dev/null +++ b/src/ui/client/src/app/observation-details-dialog/observation-details-dialog.component.ts @@ -0,0 +1,17 @@ +import {Component, Inject} from "@angular/core"; +import {MAT_DIALOG_DATA} from "@angular/material/dialog"; +import {Observation} from "../../proto/modron_pb"; + +@Component( + { + selector: "app-observation-details-dialog", + templateUrl: "./observation-details-dialog.component.html", + styleUrls: ["./observation-details-dialog.component.scss"], + } +) +export class ObservationDetailsDialogComponent { + constructor( + @Inject(MAT_DIALOG_DATA) public observation: Observation + ) { + } +} diff --git a/src/ui/client/src/app/observation-details/observation-details.component.html b/src/ui/client/src/app/observation-details/observation-details.component.html index 28e5400..67efac4 100644 --- a/src/ui/client/src/app/observation-details/observation-details.component.html +++ b/src/ui/client/src/app/observation-details/observation-details.component.html @@ -1,92 +1,128 @@
-
- v - > - Resource: {{this.parseName(ob.getResource()?.getName()) }} -
+ + + + + {{ + ob.getResourceRef()?.getExternalId() | parseExternalId + }} + + + {{ + ob.getRemediation()?.getDescription() | shortenDescription + }} + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Risk Score + + {{ getSeverity(ob.getRiskScore()).toUpperCase() }} + +
Impact + + {{ ob.getImpact() | impactName | uppercase }} + +
Severity + + {{ getSeverity(ob.getSeverity()).toUpperCase() }} + +
Finding Class{{ getCategoryName(ob.getCategory()) }}
Time of scan{{ ob.getTimestamp()?.toDate()?.toUTCString() }}
Expected{{ this.getExpectedValue(ob) }}
Observed{{ this.getObservedValue(ob) }}
-

-

- Resource Group: - {{ ob.getResource()?.getResourceGroupName() }} - {{ ob.getResource()?.getResourceGroupName() }} - {{ ob.getResource()?.getResourceGroupName() }} -

-

- Resource Time: - {{ ob.getResource()?.getTimestamp()?.toDate()?.toUTCString() }} -

-

-
-
-

Observation:

-

-

Time of scan: {{ ob.getTimestamp()?.toDate()?.toUTCString() }}

-

-
-
-
-
-

Expected:

-
-
- {{ this.getExpectedValue(ob) }} -
-
-

Observed:

-
-
- {{ this.getObservedValue(ob) }} -
-
-
-
-

Finding:

-
- {{ ob.getRemediation()?.getDescription() }}. +

Finding

+
+ {{ ob.getRemediation()?.getDescription() }}
-
+
-

Recommendation:

+

Recommendation

-
- {{ ob.getRemediation()?.getRecommendation() }}. +
+ {{ ob.getRemediation()?.getRecommendation() }}
-
+ -
- - - - - +
+
- - - - + | filterExceptions : this.exceptionNameFromObservation(ob) + ).length > 0; + then has_exceptions; + else has_no_exceptions + " + >
+ + notifications + + + + notifications_off + + +
diff --git a/src/ui/client/src/app/observation-details/observation-details.component.scss b/src/ui/client/src/app/observation-details/observation-details.component.scss index dba8efa..d3d218a 100644 --- a/src/ui/client/src/app/observation-details/observation-details.component.scss +++ b/src/ui/client/src/app/observation-details/observation-details.component.scss @@ -78,13 +78,13 @@ .remediation { background-color: #ffffff; align-self: stretch; - padding: 7px; } -.recommendation { - background-color: #ff832b67; +.recommendation-box { + border-radius: 8px; + background-color: #fff5c0; align-self: stretch; - padding: 7px; + padding: 4px 16px; } .inline { @@ -94,8 +94,26 @@ } .notify-ctn { - svg { - width: 40px; + display: flex; + .notifications-toggle { cursor: pointer; + transform: scale(1.25); + margin-top: 10px; + } +} + + +table.observation-properties { + td { + padding: 0px 8px; + } + + td:first-child { + font-weight: bold; + padding-left: 0; + } + + td > span.severity-value { + text-decoration: underline #333 dotted; } } diff --git a/src/ui/client/src/app/observation-details/observation-details.component.spec.ts b/src/ui/client/src/app/observation-details/observation-details.component.spec.ts index 05ac0a8..fb1dbaa 100644 --- a/src/ui/client/src/app/observation-details/observation-details.component.spec.ts +++ b/src/ui/client/src/app/observation-details/observation-details.component.spec.ts @@ -6,6 +6,8 @@ import { AuthenticationStore } from "../state/authentication.store"; import { NotificationStore } from "../state/notification.store"; import { ObservationDetailsComponent } from "./observation-details.component"; +import { ParseExternalIdPipe, ShortenDescriptionPipe } from "../filter.pipe"; +import {ImpactNamePipe, SeverityNamePipe} from "../severity-indicator/severity-indicator.pipe"; describe("ObservationDetailsComponent", () => { let component: ObservationDetailsComponent; @@ -17,6 +19,10 @@ describe("ObservationDetailsComponent", () => { declarations: [ ObservationDetailsComponent, NotificationExceptionsFilterPipe, + ImpactNamePipe, + ParseExternalIdPipe, + SeverityNamePipe, + ShortenDescriptionPipe, ], providers: [AuthenticationStore, NotificationStore], }).compileComponents(); diff --git a/src/ui/client/src/app/observation-details/observation-details.component.ts b/src/ui/client/src/app/observation-details/observation-details.component.ts index d9a7b8d..7e225c6 100644 --- a/src/ui/client/src/app/observation-details/observation-details.component.ts +++ b/src/ui/client/src/app/observation-details/observation-details.component.ts @@ -1,12 +1,12 @@ -import { ChangeDetectionStrategy, Component, Input } from "@angular/core" -import { MatDialog } from "@angular/material/dialog" -import { MatSnackBar } from "@angular/material/snack-bar" -import { Router } from "@angular/router" -import { Observation } from "src/proto/modron_pb" -import { NotificationException } from "../model/notification.model" -import { NotificationExceptionFormComponent } from "../notification-exception-form/notification-exception-form.component" -import { NotificationExceptionsFilterPipe } from "../notification-exceptions/notification-exceptions.pipe" -import { NotificationStore } from "../state/notification.store" +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { Router } from "@angular/router"; +import { Observation, Severity } from "../../proto/modron_pb"; +import { NotificationException } from "../model/notification.model"; +import { NotificationExceptionFormComponent } from "../notification-exception-form/notification-exception-form.component"; +import { NotificationExceptionsFilterPipe } from "../notification-exceptions/notification-exceptions.pipe"; +import { NotificationStore } from "../state/notification.store"; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -15,15 +15,21 @@ import { NotificationStore } from "../state/notification.store" styleUrls: ["./observation-details.component.scss"], }) export class ObservationDetailsComponent { - private static readonly SNACKBAR_LINGER_DURATION_MS = 2500; + readonly Severity = Severity; + static readonly SNACKBAR_LINGER_DURATION_MS = 2500; - private readonly BASE_GCP_URL = "https://console.cloud.google.com" - readonly FOLDER_URL = `${this.BASE_GCP_URL}/welcome?folder=` - readonly ORGANIZATION_URL = `${this.BASE_GCP_URL}/welcome?organizationId=` - readonly PROJECT_URL = `${this.BASE_GCP_URL}/home/dashboard?project=` + private readonly BASE_GCP_URL = "https://console.cloud.google.com"; + readonly FOLDER_URL = `${this.BASE_GCP_URL}/welcome?folder=`; + readonly ORGANIZATION_URL = `${this.BASE_GCP_URL}/welcome?organizationId=`; + readonly PROJECT_URL = `${this.BASE_GCP_URL}/home/dashboard?project=`; @Input() ob: Observation = new Observation(); + @Input() + public expanded: boolean = true; + @Input() + public showActions: boolean = true; + public notifications: Map = new Map(); constructor( @@ -31,35 +37,100 @@ export class ObservationDetailsComponent { private _dialog: MatDialog, private _snackBar: MatSnackBar, private _router: Router - ) { } - + ) {} display: Map = new Map(); toggle(name: string) { if (this.display.has(name)) { - this.display.set(name, !(this.display.get(name) as boolean)) + this.display.set(name, !(this.display.get(name) as boolean)); } else { - this.display.set(name, true) + this.display.set(name, true); + } + } + + getColor(severity: number): string { + switch (severity) { + case Severity.SEVERITY_CRITICAL: + return "red"; + case Severity.SEVERITY_HIGH: + return "orange"; + case Severity.SEVERITY_MEDIUM: + return "yellow"; + case Severity.SEVERITY_LOW: + return "green"; + default: + return "black"; + } + } + + getSeverity(severity: number): string { + switch (severity) { + case Severity.SEVERITY_CRITICAL: + return "Critical"; + case Severity.SEVERITY_HIGH: + return "High"; + case Severity.SEVERITY_MEDIUM: + return "Medium"; + case Severity.SEVERITY_LOW: + return "Low"; + case Severity.SEVERITY_INFO: + return "Info"; + default: + return "Unknown"; + } + } + + getCategoryName(category: number): string { + switch (category) { + case Observation.Category.CATEGORY_VULNERABILITY: + return "Vulnerability"; + case Observation.Category.CATEGORY_MISCONFIGURATION: + return "Misconfiguration"; + case Observation.Category.CATEGORY_TOXIC_COMBINATION: + return "Toxic Combination"; + } + return "UNKNOWN"; + } + + getRgLink(observation: Observation): string { + const rgName = this.getRgName(observation); + if(rgName.startsWith("folders/")) { + return `${this.FOLDER_URL}${rgName.replace("folders/", "")}`; + } + if(rgName.startsWith("organizations/")) { + return `${this.ORGANIZATION_URL}${rgName.replace("organizations/", "")}`; + } + if(rgName.startsWith("projects/")) { + return `${this.PROJECT_URL}${rgName.replace("projects/", "")}`; + } + return ""; + } + + getRgName(observation: Observation): string { + const resource = observation.getResourceRef(); + if (resource === undefined) { + return ""; } + return resource.getGroupName(); } getObservedValue(ob: Observation): string | undefined { - return ob.getObservedValue()?.toString()?.replace(/,/g, "") + return ob.getObservedValue()?.toString()?.replace(/,/g, ""); } getExpectedValue(ob: Observation): string | undefined { - return ob.getExpectedValue()?.toString()?.replace(/,/g, "") + return ob.getExpectedValue()?.toString()?.replace(/,/g, ""); } parseName(ob: string | undefined): string | undefined { if (!(ob?.includes("[") && ob?.includes("]"))) { - return ob + return ob; } - return ob?.replace(/(\[.*\]$)/g, "") + return ob?.replace(/(\[.*]$)/g, ""); } - notifyToggle(ob: Observation): void { - const expName = this.exceptionNameFromObservation(ob) + async notifyToggle(ob: Observation): Promise { + const expName = this.exceptionNameFromObservation(ob); if ( new NotificationExceptionsFilterPipe().transform( this.notification.exceptions, @@ -68,16 +139,14 @@ export class ObservationDetailsComponent { ) { const dialogRef = this._dialog.open(NotificationExceptionFormComponent, { data: expName, - }) + }); dialogRef .afterClosed() - .subscribe((ret: NotificationException | Error) => { - const isNotificationException = ( - ret: NotificationException | Error - ): ret is NotificationException => { - return ret !== undefined + .subscribe((ret: NotificationException | Error | boolean) => { + if(ret === false) { + return } - if (isNotificationException(ret)) { + if (ret instanceof NotificationException) { this._snackBar.open( "Notification exception created successfully", "", @@ -85,20 +154,20 @@ export class ObservationDetailsComponent { duration: ObservationDetailsComponent.SNACKBAR_LINGER_DURATION_MS, } - ) + ); } else { this._snackBar.open("Creating notification exception failed", "", { duration: ObservationDetailsComponent.SNACKBAR_LINGER_DURATION_MS, - }) + }); } - }) + }); } else { - this._router.navigate(["modron", "exceptions", expName]) + await this._router.navigate(["modron", "exceptions", expName]); } } exceptionNameFromObservation(ob: Observation): string { - const resource = ob.getResource() - return `${resource?.getResourceGroupName().replace(new RegExp("/"), "_")}-${resource?.getName()}-${ob.getName()}` + const resource = ob.getResourceRef() + return `${resource?.getGroupName().replace(new RegExp("/"), "_")}-${resource?.getExternalId()}-${ob.getName()}` } } diff --git a/src/ui/client/src/app/observations-stats/observations-stats.component.html b/src/ui/client/src/app/observations-stats/observations-stats.component.html new file mode 100644 index 0000000..ea112a5 --- /dev/null +++ b/src/ui/client/src/app/observations-stats/observations-stats.component.html @@ -0,0 +1,5 @@ + diff --git a/src/ui/client/src/app/observations-stats/observations-stats.component.scss b/src/ui/client/src/app/observations-stats/observations-stats.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/client/src/app/observations-stats/observations-stats.component.spec.ts b/src/ui/client/src/app/observations-stats/observations-stats.component.spec.ts new file mode 100644 index 0000000..3245491 --- /dev/null +++ b/src/ui/client/src/app/observations-stats/observations-stats.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { ObservationsStatsComponent } from "./observations-stats.component"; + +describe("HistogramHorizontalComponent", () => { + let component: ObservationsStatsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ObservationsStatsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ObservationsStatsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ui/client/src/app/observations-stats/observations-stats.component.ts b/src/ui/client/src/app/observations-stats/observations-stats.component.ts new file mode 100644 index 0000000..d3efb42 --- /dev/null +++ b/src/ui/client/src/app/observations-stats/observations-stats.component.ts @@ -0,0 +1,44 @@ +import { + Component, + OnInit, + Input, + ChangeDetectionStrategy, +} from "@angular/core" +import {ChartData, ChartOptions} from "chart.js"; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: "app-observations-stats", + templateUrl: "./observations-stats.component.html", + styleUrls: ["./observations-stats.component.scss"], +}) +export class ObservationsStatsComponent implements OnInit { + @Input() data: Map = new Map(); + public options: ChartOptions = { + scales: { + + }, + indexAxis: "y", + plugins: { + legend: { + display: false, + }, + } + } + public chartData: ChartData = { + labels: [] as string[], + datasets: [ + { + label: "Observations", + data: [] as number[], + } + ] + }; + max = 1; + + ngOnInit(): void { + this.max = Math.max(...this.data.values()) + this.chartData.labels = Array.from(this.data.keys()); + this.chartData.datasets[0].data = Array.from(this.data.values()); + } +} diff --git a/src/ui/client/src/app/observations-table/observations-table.component.html b/src/ui/client/src/app/observations-table/observations-table.component.html new file mode 100644 index 0000000..85e7045 --- /dev/null +++ b/src/ui/client/src/app/observations-table/observations-table.component.html @@ -0,0 +1,104 @@ + + + Risk + + + + + + + Category + +
{{ r(row).getName() }}
+
+
+ + + Resource Group + + + {{ (r(row).getResourceRef()?.getGroupName() || '') }} + + + + + + Resource + {{ r(row).getResourceRef()?.getExternalId() | parseExternalId }} + + + + + Description + +

+
+
+ + + Observed + +

{{ r(row).getObservedValue() | structValueToString }}

+
+
+ + + Expected + + {{ r(row).getExpectedValue() | structValueToString }} + + + + + Actions + + + open_in_full + + + + + + + + +
diff --git a/src/ui/client/src/app/observations-table/observations-table.component.scss b/src/ui/client/src/app/observations-table/observations-table.component.scss new file mode 100644 index 0000000..6d545f8 --- /dev/null +++ b/src/ui/client/src/app/observations-table/observations-table.component.scss @@ -0,0 +1,49 @@ +.mat-column-riskScore { + max-width: 80px; +} + +.mat-column-category { + max-width: 350px; +} + +.mat-column-shortDesc { + min-width: 30%; +} + +.mat-column-shortDesc, .mat-column-category { + max-height: 1em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + + div { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.mat-cell { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + > p { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.mat-column-actions { + display: grid; + grid-template-columns: repeat(2, 1fr); + justify-items: center; + max-width: 100px; + + > * { + cursor: pointer; + } +} diff --git a/src/ui/client/src/app/observations-table/observations-table.component.ts b/src/ui/client/src/app/observations-table/observations-table.component.ts new file mode 100644 index 0000000..d0c2f43 --- /dev/null +++ b/src/ui/client/src/app/observations-table/observations-table.component.ts @@ -0,0 +1,97 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, Component, Input, OnInit, + ViewChild +} from "@angular/core" +import { ModronService } from "../modron.service" +import { ModronStore } from "../state/modron.store" + +import * as pb from "src/proto/modron_pb" +import {Observation} from "src/proto/modron_pb"; +import {MatSort, Sort} from "@angular/material/sort"; +import {MatTableDataSource} from "@angular/material/table"; +import {MatDialog} from "@angular/material/dialog"; +import {ObservationDetailsDialogComponent} from "../observation-details-dialog/observation-details-dialog.component"; + +type ObsMap = Map +type RgObsMap = Map + +@Component({ + selector: "app-observations-table", + templateUrl: "./observations-table.component.html", + styleUrls: ["./observations-table.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ObservationsTableComponent implements OnInit,AfterViewInit { + public dataSource: MatTableDataSource = new MatTableDataSource(); + public sortedData: MatTableDataSource = new MatTableDataSource(); + + @Input() + public obs: pb.Observation[] = []; + constructor( + public store: ModronStore, + public modron: ModronService, + private dialog: MatDialog, + ){ + + } + + @Input() + public columns = ["riskScore", "category", "shortDesc", "resource", "actions"]; + public resourceGroupName = ""; + + + async ngOnInit(): Promise { + this.dataSource.data = this.obs + this.sortData({active: "riskScore", direction: "desc"}) + } + + @ViewChild(MatSort) sort: MatSort|undefined + + sortData(sort: Sort) { + if (!sort.active || sort.direction === "") { + this.sortedData.data = this.dataSource.data; + return; + } + + this.sortedData.data = this.dataSource.data.slice().sort((a, b) => { + let sortResult = 0; + switch(sort.active) { + case "riskScore": + sortResult = a.getRiskScore() - b.getRiskScore(); + break; + case "category": + sortResult = a.getName().localeCompare(b.getName()); + break; + } + return sort.direction === "asc" ? sortResult : -sortResult; + }); + } + + ngAfterViewInit(): void { + this.dataSource.sort = this.sort as MatSort + } + + getName(obs: RgObsMap): ObsMap | undefined { + return obs.get(this.resourceGroupName) + } + identity(index: number, item: pb.Observation): string { + return item.getUid() + } + + protected readonly JSON = JSON; + protected readonly Object = Object; + protected readonly Observation = Observation; + + r(row: unknown): pb.Observation { + return row as pb.Observation + } + + showObservationDetails(row: Observation) { + this.dialog.open(ObservationDetailsDialogComponent, { + data: row, + width: "50%", + hasBackdrop: true, + }) + } +} diff --git a/src/ui/client/src/app/resource-group-details/resource-group-details.component.html b/src/ui/client/src/app/resource-group-details/resource-group-details.component.html index 65b4eb2..c1fb2d0 100644 --- a/src/ui/client/src/app/resource-group-details/resource-group-details.component.html +++ b/src/ui/client/src/app/resource-group-details/resource-group-details.component.html @@ -1,46 +1,26 @@
-

+

{{ - this.resourceGroupName.replace('projects/', '') }} + href="{{this.PROJECT_URL + this.resourceGroupName.replace('projects/', '') }}">{{ + this.resourceGroupName.replace('projects/', '') + }} {{ - this.resourceGroupName }} + href="{{ this.FOLDER_URL + this.resourceGroupName.replace('folders/', '') }}">{{ + this.resourceGroupName + }} {{ - this.resourceGroupName }} - | -

-

observation details

+ href="{{ this.ORGANIZATION_URL + this.resourceGroupName.replace('organizations/', '') }}">{{ + this.resourceGroupName + }} + +

Observation details

- - +
+
+ +
+ +
-
diff --git a/src/ui/client/src/app/resource-group-details/resource-group-details.component.scss b/src/ui/client/src/app/resource-group-details/resource-group-details.component.scss index ae7877b..c7d8dba 100644 --- a/src/ui/client/src/app/resource-group-details/resource-group-details.component.scss +++ b/src/ui/client/src/app/resource-group-details/resource-group-details.component.scss @@ -40,16 +40,10 @@ } .app-resourcegroup-header { - display: flex; flex-direction: row; align-items: center; - gap: 10px; flex: 1; - background-color: rgb(239, 239, 239); - - h1 { - margin: 5px 40px; - } + margin-top: 24px; } } @@ -114,4 +108,76 @@ svg:hover { fill: rgb(64, 64, 64); } + + .mat-column-riskScore { + max-width: 80px; + } + + .mat-column-category { + max-width: 350px; + } + + .mat-column-shortDesc { + min-width: 30%; + } + + .mat-column-shortDesc, .mat-column-category { + max-height: 1em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + + div { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .mat-column-actions { + display: grid; + grid-template-columns: repeat(2, 1fr); + justify-items: center; + max-width: 100px; + + > * { + cursor: pointer; + } + } +} + +.observation-card { + $margin: 20px; + margin: $margin 0 $margin 0; + padding: 10px; +} + +.observation-card-header { + margin-bottom: 1em; +} + +.observation-card-title { + display: flex; + flex-basis: content; + align-items: center; + + a.more-info { + display: block; + height: 24px; + margin-left: 8px; + } +} + +.observation-details { + display: flex; + flex-direction: column; + gap: 1em; +} + +.observation-subtitle { + &.has-observations { + color: #da1e28; + } } diff --git a/src/ui/client/src/app/resource-group-details/resource-group-details.component.ts b/src/ui/client/src/app/resource-group-details/resource-group-details.component.ts index 509a154..030fadd 100644 --- a/src/ui/client/src/app/resource-group-details/resource-group-details.component.ts +++ b/src/ui/client/src/app/resource-group-details/resource-group-details.component.ts @@ -1,11 +1,16 @@ -import { KeyValue, ViewportScroller } from "@angular/common" -import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core" +import { + ChangeDetectionStrategy, ChangeDetectorRef, + Component, OnDestroy, + OnInit, +} from "@angular/core" import { ActivatedRoute } from "@angular/router" import { ModronService } from "../modron.service" import { ModronStore } from "../state/modron.store" - import * as pb from "src/proto/modron_pb" -import { first } from "rxjs" +import {Subscription, tap} from "rxjs"; + +type ObsMap = Map +type RgObsMap = Map @Component({ selector: "app-resource-group-details", @@ -13,74 +18,62 @@ import { first } from "rxjs" styleUrls: ["./resource-group-details.component.scss"], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ResourceGroupDetailsComponent implements OnInit { +export class ResourceGroupDetailsComponent implements OnInit,OnDestroy { + public loading = true; + private subscription: Subscription | undefined; + public obs: pb.Observation[] = []; constructor( private route: ActivatedRoute, public store: ModronStore, public modron: ModronService, - private viewportScroller: ViewportScroller, - ) { } + private cdr: ChangeDetectorRef, + ){ + } public resourceGroupName = ""; private readonly BASE_GCP_URL = "https://console.cloud.google.com" readonly FOLDER_URL = `${this.BASE_GCP_URL}/welcome?folder=` readonly ORGANIZATION_URL = `${this.BASE_GCP_URL}/welcome?organizationId=` readonly PROJECT_URL = `${this.BASE_GCP_URL}/home/dashboard?project=` - public displayObsDetail: Map = new Map(); - ngOnInit(): void { + async ngOnInit(): Promise { this.resourceGroupName = (this.route.snapshot.paramMap.get("id") as string).replace(new RegExp("-"), "/") - } - - // Wait for https://github.com/angular/angular/issues/30139 to be fixed. - // The bug prevents us from scrolling to a fragment that is dynamically loaded. - ngAfterViewInit(): void { - this.store.observations$.subscribe(() => - this.route.fragment.pipe(first()).subscribe(fragment => { - console.log(fragment) - this.viewportScroller.scrollToAnchor(fragment!) + this.subscription = this.store.observations$.pipe( + tap((obs) => { + if(obs.size > 0) { + this.loading = false + this.cdr.markForCheck() + } + this.obs = this.getObservations(obs) }) - ) + ).subscribe() } - filterName( - obs: Map - ): Map { - const m = new Map() - m.set(this.resourceGroupName, obs.get(this.resourceGroupName as string)) - return m + ngOnDestroy(): void { + this.subscription?.unsubscribe() } - getName( - obs: Map> - ): Map { - return obs.get(this.resourceGroupName) as Map + + getName(obs: RgObsMap): ObsMap | undefined { + return obs.get(this.resourceGroupName) } - mapByType(obs: Map): Map { - const obsByType = new Map() - for (const ob of [...obs.values()].flat()) { - if (ob === undefined) { - continue - } - const type = ob.getName() - if (!obsByType.has(type)) { - obsByType.set(type, []) - } - obsByType.get(type)?.push(ob) + getObservations(obs?: RgObsMap): pb.Observation[] { + if(obs === undefined) { + return [] + } + const obsMap = this.getName(obs) + if(obsMap === undefined) { + return [] } - return obsByType - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return Array.from(obsMap).map(([_, v]) => v).flat() + } identity(index: number, item: pb.Observation): string { return item.getUid() } - - identityKV(index: number, item: KeyValue): string { - return item.key - } - getObservedValue(ob: pb.Observation): string | undefined { return ob.getObservedValue()?.toString()?.replace(/,/g, "") } @@ -89,15 +82,11 @@ export class ResourceGroupDetailsComponent implements OnInit { return ob.getExpectedValue()?.toString()?.replace(/,/g, "") } - toggle(id: string | undefined): void { - id = id as string - if (this.displayObsDetail.has(id)) { - this.displayObsDetail.set( - id, - !(this.displayObsDetail.get(id) as boolean) - ) - } else { - this.displayObsDetail.set(id, true) - } + r(row: unknown): pb.Observation { + return row as pb.Observation + } + + resourceRef(row: pb.Observation) { + return row.getResourceRef()?.getExternalId() } } diff --git a/src/ui/client/src/app/resource-group-details/resource-group-details.pipe.ts b/src/ui/client/src/app/resource-group-details/resource-group-details.pipe.ts index dacc5af..6596d97 100644 --- a/src/ui/client/src/app/resource-group-details/resource-group-details.pipe.ts +++ b/src/ui/client/src/app/resource-group-details/resource-group-details.pipe.ts @@ -1,6 +1,6 @@ import { Pipe, PipeTransform } from "@angular/core" -import { Value } from "google-protobuf/google/protobuf/struct_pb" import * as pb from "src/proto/modron_pb" +import {StructValueToStringPipe} from "../filter.pipe"; @Pipe({ name: "mapByType" }) export class MapByTypePipe implements PipeTransform { @@ -36,7 +36,7 @@ export class MapByObservedValuesPipe implements PipeTransform { const obsByType = new Map() obs.forEach((o) => { const obsValue = o.getObservedValue() - ? (o.getObservedValue() as Value).toString() + ? StructValueToStringPipe.prototype.transform(o.getObservedValue()) : "Observation count" if (!obsByType.has(obsValue)) { obsByType.set(obsValue, 0) diff --git a/src/ui/client/src/app/resource-group/resource-group.component.html b/src/ui/client/src/app/resource-group/resource-group.component.html index 43de7f9..2e79592 100644 --- a/src/ui/client/src/app/resource-group/resource-group.component.html +++ b/src/ui/client/src/app/resource-group/resource-group.component.html @@ -1,40 +1,58 @@ -
-
-

{{ this.name.replace('projects/', '') }}

- - - -
- -
-
-
- - - -

Scan

+ + + {{ this.provider }} + {{ this.name.replace('projects/', '') }} + + +
+
+
+
+ +
+
+ + +
+
0 observations
+
+
+
+
+ Last scanned: {{ this.lastScanDate! | fromNow }}
- -
- Scanning - + + +
+ Never scanned
-
+
+ + + -
-

{{ this.observationCount }} observations

-
-

{{ this.lastScanDate.slice(12) }}

-

{{ this.lastScanDate.slice(0, 12) }}

-
-
-
+ + + + + diff --git a/src/ui/client/src/app/resource-group/resource-group.component.scss b/src/ui/client/src/app/resource-group/resource-group.component.scss index 4880df7..d64ddae 100644 --- a/src/ui/client/src/app/resource-group/resource-group.component.scss +++ b/src/ui/client/src/app/resource-group/resource-group.component.scss @@ -1,3 +1,86 @@ +@use '../../colors.scss' as colors; + +.resource-group-card { + cursor: pointer; + overflow: hidden; + + .resource-group-card-header { + display: block; + } + + .resource-group-title { + height: 36px; + width: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + .content { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + + .observations { + .findings-count, .no-findings { + height: 30px; + padding: 10px; + color: #FFF; + display: flex; + flex-direction: row; + align-items: center; + justify-items: center; + justify-content: center; + } + + .findings-by-severity { + display: flex; + row-gap: 4px; + column-gap: 4px; + justify-content: left; + height: 50px; + align-items: center; + } + + + .no-findings { + background-color: colors.$allGood; + } + + + .findings-count { + background-color: colors.$danger; + + .icon { + margin-right: 4px; + } + + .count { + margin-right: 4px; + } + + .desc { + + } + + } + } + + .last-scan { + color: colors.$secondaryText; + height: 20px; + font-size: 0.8em; + margin-top: 1em; + } + } + + .footer { + height: 4px; + } +} + + .resourceGroup-ctn { width: 300px; height: 220px; diff --git a/src/ui/client/src/app/resource-group/resource-group.component.ts b/src/ui/client/src/app/resource-group/resource-group.component.ts index c645366..a8bd977 100644 --- a/src/ui/client/src/app/resource-group/resource-group.component.ts +++ b/src/ui/client/src/app/resource-group/resource-group.component.ts @@ -3,6 +3,8 @@ import { map, Observable } from "rxjs" import { ModronStore } from "../state/modron.store" import { MatSnackBar } from "@angular/material/snack-bar" import * as pb from "src/proto/modron_pb" +import * as moment from "moment"; +import {Severity} from "src/proto/modron_pb"; @Component({ selector: "app-resource-group", @@ -16,14 +18,17 @@ export class ResourceGroupComponent { name = ""; @Input() - lastScanDate = ""; + lastScanDate: Date | null = null; @Input() - provider = ""; + provider = "GCP"; // TODO: Change when we support other providers @Input() observationCount = -1; + @Input() + observationBySeverity: [number, number][] = []; + constructor(public store: ModronStore, public snackBar: MatSnackBar) { } collectAndScan(resourceGroups: string[]): void { @@ -60,4 +65,10 @@ export class ResourceGroupComponent { }) ) } + + fromNow(date: string): string { + return moment(date).fromNow() + } + + protected readonly Severity = Severity; } diff --git a/src/ui/client/src/app/resource-group/resource-group.pipe.ts b/src/ui/client/src/app/resource-group/resource-group.pipe.ts new file mode 100644 index 0000000..9257f52 --- /dev/null +++ b/src/ui/client/src/app/resource-group/resource-group.pipe.ts @@ -0,0 +1,9 @@ +import {Pipe, PipeTransform} from "@angular/core"; +import * as moment from "moment"; + +@Pipe({name: "fromNow"}) +export class FromNowPipe implements PipeTransform { + transform(value: Date): string { + return moment(value).fromNow() + } +} diff --git a/src/ui/client/src/app/resource-groups/resource-groups.component.html b/src/ui/client/src/app/resource-groups/resource-groups.component.html index 96c8c56..e8973be 100644 --- a/src/ui/client/src/app/resource-groups/resource-groups.component.html +++ b/src/ui/client/src/app/resource-groups/resource-groups.component.html @@ -1,30 +1,28 @@
-

Resource groups |

-
-

Filter

- - -
-

- | - {{ - ( - obs - | mapFlatRules - | keyvalue - | filterKeyValue: searchText - | filterNoObservations: removeNoObs - ).length - }} - matching groups -

-
-
+

Resource Groups

+
+
+ + Filter + + + Only with observations
+ +

+ {{ + ( + obs + | mapFlatRules + | keyvalue + | filterKeyValue: searchText + | filterNoObservations: removeNoObs + ).length + }} + matching groups +

-
-
-
-
-

- {{ obsKvs | invalidProjectNb }} -

-

groups with dangerous observations

-
-

{{ obsKvs | obsNb }} total observations to solve

+ +
+
+
+ error +
{{ obsKvs | invalidProjectNb }}
+
groups with important observations
-
-
-
- - - -

Scan all

-
-
- -
- - - -

Scanning ...

-
-
+ +
+ warning +
{{ obsKvs | obsNb }}
+
total observations to solve
+
+
+ +
+ + + +
+
+
- + + +
diff --git a/src/ui/client/src/app/resource-groups/resource-groups.component.scss b/src/ui/client/src/app/resource-groups/resource-groups.component.scss index f1aa425..28942f7 100644 --- a/src/ui/client/src/app/resource-groups/resource-groups.component.scss +++ b/src/ui/client/src/app/resource-groups/resource-groups.component.scss @@ -1,9 +1,4 @@ -.inline { - display: flex; - flex-direction: row; - gap: 10px; - align-items: baseline; -} +@use '../../colors.scss' as colors; .inline-between { display: flex; @@ -17,9 +12,9 @@ width: 300px; height: 220px; background: linear-gradient( - to right, - rgb(211, 211, 211) 50%, - rgb(236, 236, 236) 50% + to right, + rgb(211, 211, 211) 50%, + rgb(236, 236, 236) 50% ); background-size: 200% 200%; animation: gradient 6s ease infinite; @@ -40,35 +35,86 @@ } .app-resourcegroup { - width: 98%; - // height: 100%; - + display: block; + height: 100%; + width: 100%; .app-resourcegroup-header { display: flex; flex-direction: row; align-items: center; gap: 10px; flex: 1; - background-color: rgb(239, 239, 239); + } + + h3 { + margin: 8px 0; + } + + h4 { + margin: 4px 0; + } + + .app-resourcegroup-filter { + display: flex; + flex-direction: row; + align-items: baseline; + gap: 10px; + } + + .matching-groups-count { + color: colors.$secondaryText; + margin-top: 2px; + } - .app-resourcegroup-header-filter { + .observations-top-bar { + display: grid; + width: 100%; + grid-template-columns: 1fr 1fr; + + .buttons { display: flex; - flex-direction: row; - gap: 10px; + justify-self: end; + } + } - input { - padding: 5px; - margin: 5px; - font-size: 20px; + .observations-result { + display: flex; + flex-direction: column; + row-gap: 4px; + margin: 12px 0px 12px 0px; + + .observation-type { + display: grid; + grid-template-columns: 40px 6em 300px; + justify-items: center; + align-items: center; + + &.warn { + .observation-icon { + color: colors.$warning; + } } - input:focus { - outline: none; + &.danger { + .observation-icon { + color: colors.$danger; + } } - } - h1 { - margin: 5px 40px; + .observation-icon { + align-self: center; + } + + .observation-count { + font-weight: bold; + font-size: 1.5em; + justify-self: right; + margin-right: 8px; + } + + .observation-description { + justify-self: start; + } } } @@ -86,6 +132,9 @@ } .buttons { + display: flex; + flex-direction: row; + .button { background-color: rgb(238, 238, 238); margin-left: 3px; @@ -108,6 +157,9 @@ margin-top: 10px; gap: 20px; display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fit, 300px); + overflow: auto; + padding: 10px; + max-height: calc(100vh - 356px); } } diff --git a/src/ui/client/src/app/resource-groups/resource-groups.component.spec.ts b/src/ui/client/src/app/resource-groups/resource-groups.component.spec.ts index c52f00d..1fb4223 100644 --- a/src/ui/client/src/app/resource-groups/resource-groups.component.spec.ts +++ b/src/ui/client/src/app/resource-groups/resource-groups.component.spec.ts @@ -4,7 +4,7 @@ import { FilterKeyValuePipe, FilterNoObservationsPipe } from "../filter.pipe"; import { mapFlatRulesPipe } from "../resource-group-details/resource-group-details.pipe"; import { ModronStore } from "../state/modron.store"; import { RouterTestingModule } from "@angular/router/testing"; -import { HttpClientTestingModule } from "@angular/common/http/testing"; +import { provideHttpClientTesting } from "@angular/common/http/testing"; import { ResourceGroupsComponent } from "./resource-groups.component"; import { @@ -12,6 +12,7 @@ import { ObsNbPipe, ResourceGroupsPipe, } from "./resource-groups.pipe"; +import { provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; describe("ResourceGroupsComponent", () => { let component: ResourceGroupsComponent; @@ -19,7 +20,7 @@ describe("ResourceGroupsComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ + declarations: [ ResourceGroupsComponent, ResourceGroupsPipe, FilterKeyValuePipe, @@ -27,14 +28,11 @@ describe("ResourceGroupsComponent", () => { InvalidProjectNb, ObsNbPipe, FilterNoObservationsPipe, - ], - imports: [ - MatSnackBarModule, - RouterTestingModule, - HttpClientTestingModule, - ], - providers: [ModronStore], - }).compileComponents(); + ], + imports: [MatSnackBarModule, + RouterTestingModule], + providers: [ModronStore, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] +}).compileComponents(); fixture = TestBed.createComponent(ResourceGroupsComponent); component = fixture.componentInstance; diff --git a/src/ui/client/src/app/resource-groups/resource-groups.component.ts b/src/ui/client/src/app/resource-groups/resource-groups.component.ts index c3c5196..747f272 100644 --- a/src/ui/client/src/app/resource-groups/resource-groups.component.ts +++ b/src/ui/client/src/app/resource-groups/resource-groups.component.ts @@ -36,8 +36,8 @@ export class ResourceGroupsComponent implements OnInit { return balance > 0 ? "#da1e28" : "#24a148" } - collectAndScan(resourceGroups: string[]): void { - this.store.collectAndScan$(resourceGroups).subscribe({ + collectAndScanAll(): void { + this.store.collectAndScanAll$().subscribe({ next: () => this.snackBar.open("Scanning all resource groups ...", "", { duration: ResourceGroupsComponent.SNACKBAR_LINGER_DURATION_MS, @@ -67,12 +67,11 @@ export class ResourceGroupsComponent implements OnInit { ) } - getDate(obs: any[]): string { - obs as Observation[] + getDate(obs: Observation[]): Date | null { if (obs.length > 0) { - return obs[0].getTimestamp()?.toDate().toUTCString().slice(4) + return obs[0].getTimestamp()?.toDate() || null; } - return "" + return null } public updateFilterUrlParam() { diff --git a/src/ui/client/src/app/resource-groups/resource-groups.pipe.ts b/src/ui/client/src/app/resource-groups/resource-groups.pipe.ts index e8a94fd..ff36ab7 100644 --- a/src/ui/client/src/app/resource-groups/resource-groups.pipe.ts +++ b/src/ui/client/src/app/resource-groups/resource-groups.pipe.ts @@ -24,6 +24,23 @@ export class MapPerTypeName implements PipeTransform { } } + +@Pipe({ name: "mapByRiskScore" }) +export class MapByRiskScorePipe implements PipeTransform { + transform(obs: pb.Observation[]): [number, number][] { + const sevMap = new Map(); + for(const o of obs) { + const amountSeverities = sevMap.get(o.getRiskScore()); + if(amountSeverities === undefined){ + sevMap.set(o.getRiskScore(), 1); + continue; + } + sevMap.set(o.getRiskScore(), amountSeverities+1); + } + return Array.from(sevMap.entries()).sort((a, b) => b[0] - a[0]) + } +} + @Pipe({ name: "observations" }) export class ObservationsPipe implements PipeTransform { transform(obs: Map): pb.Observation[] { diff --git a/src/ui/client/src/app/severity-indicator/severity-indicator.component.html b/src/ui/client/src/app/severity-indicator/severity-indicator.component.html new file mode 100644 index 0000000..3480f35 --- /dev/null +++ b/src/ui/client/src/app/severity-indicator/severity-indicator.component.html @@ -0,0 +1,22 @@ +
+ + {{ getIcon() }} + + + {{ count! | severityAmount }} + +
diff --git a/src/ui/client/src/app/severity-indicator/severity-indicator.component.scss b/src/ui/client/src/app/severity-indicator/severity-indicator.component.scss new file mode 100644 index 0000000..6f2feba --- /dev/null +++ b/src/ui/client/src/app/severity-indicator/severity-indicator.component.scss @@ -0,0 +1,47 @@ + +.severity-circle { + color: #FFF; + display: block; + width: 30px; + height: 30px; + line-height: 30px; + font-size: 16px; + overflow: hidden; + text-align: center; + border-radius: 30px; + margin-right: 6px; +} + +.severity-circle-unknown { + background-color: #9E9E9E; +} + +.severity-circle-info { + background-color: #2196F3; +} + +.severity-circle-low { + background-color: #AFB42B; +} + + +.severity-circle-medium { + background-color: #F9A825; +} + +.severity-circle-high { + background-color: #E65100; +} + +.severity-circle-critical { + background-color: #D50000; + + // Make this blink too! + animation: blinker 1s linear infinite; +} + +@keyframes blinker { + 50% { + background-color: #ff0000; + } +} diff --git a/src/ui/client/src/app/severity-indicator/severity-indicator.component.ts b/src/ui/client/src/app/severity-indicator/severity-indicator.component.ts new file mode 100644 index 0000000..62b9a4e --- /dev/null +++ b/src/ui/client/src/app/severity-indicator/severity-indicator.component.ts @@ -0,0 +1,36 @@ +import {Component, Input} from "@angular/core"; +import {Severity} from "../../proto/modron_pb"; + +@Component({ + selector: "app-severity-indicator", + templateUrl: "./severity-indicator.component.html", + styleUrls: ["./severity-indicator.component.scss"], +}) +export class SeverityIndicatorComponent { + @Input() + severity: Severity = Severity.SEVERITY_UNKNOWN; + + @Input() + count: number | undefined; + + getIcon(): string { + switch (this.severity) { + case Severity.SEVERITY_CRITICAL: + return "C"; + case Severity.SEVERITY_HIGH: + return "H"; + case Severity.SEVERITY_MEDIUM: + return "M"; + case Severity.SEVERITY_LOW: + return "L"; + case Severity.SEVERITY_INFO: + return "I"; + default: + return "?"; + } + } + + constructor() {} + + protected readonly Severity = Severity; +} diff --git a/src/ui/client/src/app/severity-indicator/severity-indicator.pipe.ts b/src/ui/client/src/app/severity-indicator/severity-indicator.pipe.ts new file mode 100644 index 0000000..6f41d7e --- /dev/null +++ b/src/ui/client/src/app/severity-indicator/severity-indicator.pipe.ts @@ -0,0 +1,48 @@ +import {Pipe, PipeTransform} from "@angular/core"; +import {Impact, Severity} from "../../proto/modron_pb"; + +@Pipe({name: "severityName"}) +export class SeverityNamePipe implements PipeTransform { + transform(severity: number): string { + switch(severity) { + case Severity.SEVERITY_CRITICAL: + return "Critical"; + case Severity.SEVERITY_HIGH: + return "High"; + case Severity.SEVERITY_MEDIUM: + return "Medium"; + case Severity.SEVERITY_LOW: + return "Low"; + case Severity.SEVERITY_INFO: + return "Info"; + default: + return "Unknown"; + } + } +} + +@Pipe({name: "impactName"}) +export class ImpactNamePipe implements PipeTransform { + transform(impact: number): string { + switch(impact) { + case Impact.IMPACT_HIGH: + return "High"; + case Impact.IMPACT_MEDIUM: + return "Medium"; + case Impact.IMPACT_LOW: + return "Low"; + default: + return "Unknown"; + } + } +} + +@Pipe({name: "severityAmount"}) +export class SeverityAmountPipe implements PipeTransform { + transform(count: number): string { + if(count > 99) { + return "99+"; + } + return count.toString(); + } +} diff --git a/src/ui/client/src/app/sidenav/sidenav.component.html b/src/ui/client/src/app/sidenav/sidenav.component.html new file mode 100644 index 0000000..ec8a9bf --- /dev/null +++ b/src/ui/client/src/app/sidenav/sidenav.component.html @@ -0,0 +1,29 @@ +
+
+ +
+ + + +
diff --git a/src/ui/client/src/app/sidenav/sidenav.component.scss b/src/ui/client/src/app/sidenav/sidenav.component.scss new file mode 100644 index 0000000..25414e1 --- /dev/null +++ b/src/ui/client/src/app/sidenav/sidenav.component.scss @@ -0,0 +1,64 @@ +@use '../../colors.scss' as colors; + +$navbarWidth: 68px; + +.sidenav { + display: flex; + background-color: colors.$sideNavBackground; + flex-direction: column; + width: $navbarWidth; + height: 100%; + + div.top-spacer { + height: 20px; + } + + .menu-icon-container { + align-self: center; + height: 24px; + margin-top: 12px; + margin-bottom: 12px; + } + + div.nav-items { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + gap: 24px; + margin-top: 20px; + + .nav-item { + cursor: pointer; + text-align: center; + width: $navbarWidth; + overflow: hidden; + + &.active { + .nav-item-icon { + background-color: colors.$sideNavButtonActiveBackground; + } + } + + .nav-item-icon { + font-size: 20px; + line-height: 20px; + height: 20px; + width: 30px; + padding: 4px; + border-radius: 40px; + transition: 250ms; + &:hover { + background-color: colors.$sideNavButtonActiveBackground; + } + } + + .nav-item-text { + width: $navbarWidth; + font-weight: bold; + font-size: 12px; + text-align: center; + } + } + } +} diff --git a/src/ui/client/src/app/sidenav/sidenav.component.spec.ts b/src/ui/client/src/app/sidenav/sidenav.component.spec.ts new file mode 100644 index 0000000..40bcb37 --- /dev/null +++ b/src/ui/client/src/app/sidenav/sidenav.component.spec.ts @@ -0,0 +1,47 @@ +import {ComponentFixture, TestBed} from "@angular/core/testing"; +import {SidenavComponent} from "./sidenav.component"; +import {Router} from "@angular/router"; +import {RouterTestingModule} from "@angular/router/testing"; +import {Component} from "@angular/core"; + +@Component({ + template: "" +}) +class DummyComponent { +} + +describe("SidenavComponent", () => { + let component: SidenavComponent; + let fixture: ComponentFixture; + let router: Router; + + beforeEach(async () => { + const testingModule = TestBed.configureTestingModule({ + imports: [SidenavComponent, RouterTestingModule.withRoutes( + [{path: "modron/resourcegroups", component: DummyComponent}] + )], + providers: [] + }) + await testingModule.compileComponents(); + + fixture = TestBed.createComponent(SidenavComponent); + component = fixture.componentInstance; + router = TestBed.inject(Router); + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should mark the icon as active when the route matches", async () => { + await router.navigateByUrl("/modron/resourcegroups"); + fixture.detectChanges(); + const {debugElement} = fixture; + const navItems = debugElement.nativeElement.querySelectorAll("div.nav-items div.nav-item") + expect(navItems.length).toBe(3); + expect(navItems[0].classList).toContain("active"); + expect(navItems[1].classList).not.toContain("active"); + expect(navItems[2].classList).not.toContain("active"); + }); +}); diff --git a/src/ui/client/src/app/sidenav/sidenav.component.ts b/src/ui/client/src/app/sidenav/sidenav.component.ts new file mode 100644 index 0000000..94b6707 --- /dev/null +++ b/src/ui/client/src/app/sidenav/sidenav.component.ts @@ -0,0 +1,23 @@ +import { Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import {MatIconModule} from "@angular/material/icon"; +import {MatListModule} from "@angular/material/list"; +import {Router, RouterLink} from "@angular/router"; +import {MatRippleModule} from "@angular/material/core"; + +@Component({ + selector: "app-sidenav", + standalone: true, + imports: [CommonModule, MatIconModule, MatListModule, RouterLink, MatRippleModule], + templateUrl: "./sidenav.component.html", + styleUrl: "./sidenav.component.scss" +}) +export class SidenavComponent { + constructor(public router: Router) {} + + public navItems = [ + { link: "/modron/resourcegroups", name: "Resource Groups", icon: "folder" }, + { link: "/modron/stats", name: "Stats", icon: "bar_chart" }, + { link: "/modron/exceptions", name: "Exceptions", icon: "notifications_paused" }, + ]; +} diff --git a/src/ui/client/src/app/state/modron.store.ts b/src/ui/client/src/app/state/modron.store.ts index 9d7cecc..0d31041 100644 --- a/src/ui/client/src/app/state/modron.store.ts +++ b/src/ui/client/src/app/state/modron.store.ts @@ -1,9 +1,10 @@ -import { Injectable } from "@angular/core" -import { BehaviorSubject, map, Observable } from "rxjs" -import { ModronService } from "../modron.service" -import { StatusInfo } from "../model/modron.model" +import {Injectable} from "@angular/core" +import {BehaviorSubject, map, Observable} from "rxjs" +import {ModronService} from "../modron.service" +import {StatusInfo} from "../model/modron.model" -import * as pb from "src/proto/modron_pb" +import * as pb from "../../proto/modron_pb" +import {RequestStatus, ScanType} from "../../proto/modron_pb" @Injectable() export class ModronStore { @@ -54,12 +55,35 @@ export class ModronStore { // A shallow copy here is enough const scanInfo = new Map(this.scanInfo) scanInfo.set(res.getCollectId() + ModronService.SEPARATOR + res.getScanId(), { - state: 2, + state: RequestStatus.RUNNING, resourceGroups: resourceGroups, + scanType: ScanType.SCAN_TYPE_PARTIAL }) this._runningScans.set(res.getCollectId() + ModronService.SEPARATOR + res.getScanId(), { - state: 2, + state: RequestStatus.RUNNING, resourceGroups: resourceGroups, + scanType: ScanType.SCAN_TYPE_PARTIAL + }) + this._scanIdsStatus.next(scanInfo) + return res + }) + ) + } + + collectAndScanAll$(): Observable { + this.checkScansStatus() + return this._service.collectAndScanAll().pipe( + map((res) => { + const scanInfo = new Map(this.scanInfo) + scanInfo.set(res.getCollectId() + ModronService.SEPARATOR + res.getScanId(), { + state: RequestStatus.RUNNING, + resourceGroups: [], + scanType: ScanType.SCAN_TYPE_FULL + }) + this._runningScans.set(res.getCollectId() + ModronService.SEPARATOR + res.getScanId(), { + state: RequestStatus.RUNNING, + resourceGroups: [], + scanType: ScanType.SCAN_TYPE_FULL }) this._scanIdsStatus.next(scanInfo) return res @@ -85,7 +109,7 @@ export class ModronStore { s = res.getScanStatus() } const scanInfo = new Map(this.scanInfo) - scanInfo.set(v, { state: s, resourceGroups: k.resourceGroups }) + scanInfo.set(v, { state: s, resourceGroups: k.resourceGroups, scanType: k.scanType }) if (s === pb.RequestStatus.DONE) { this._runningScans.delete(v) this.fetchObservations(k.resourceGroups).subscribe((obs) => { diff --git a/src/ui/client/src/app/stats/stats.component.html b/src/ui/client/src/app/stats/stats.component.html index 5e550ba..c84e415 100644 --- a/src/ui/client/src/app/stats/stats.component.html +++ b/src/ui/client/src/app/stats/stats.component.html @@ -1,118 +1,85 @@
-

Security Statistics |

-

general overview

+

Statistics

-
-
-
-

+
+ + + Compliant Projects + {{ - (obs | mapFlatRules).size - - (obs | mapFlatRules | keyvalue | invalidProjectNb) + (obs | mapFlatRules).size - + (obs | mapFlatRules | keyvalue | invalidProjectNb) }} -

-

compliant projects

-
- -
-

{{ obs | mapFlatRules | keyvalue | invalidProjectNb }}

-

projects with issues

-
- -
-

{{ (obs | mapFlatRules | observations).length }}

-

obser­vations

-
- -
-

{{ (obs | mapFlatRules | mapByType).size }}

-

rule types

-
-
- -
-
-
- - -
- -

- {{ obsType.key }}: {{ obsType.value.length }} total observations -

-
-
-
- - - - - - -
- -
- - - - - - - - -
- -
- - - - - -
+ + + + + + Projects with Issues + {{ obs | mapFlatRules | keyvalue | invalidProjectNb }} + + + + + Observations + {{ (obs | mapFlatRules | observations).length }} + + + + + Rule Types + {{ (obs | mapFlatRules | mapByType).size }} + + +
+
+ + + {{ obsType.key }} + {{ obsType.value.length }} observations + + +
+
+
+ + + + + List of the observations + + + + + + + +
- -
- -
- - -
-
+ +
-
-
diff --git a/src/ui/client/src/app/stats/stats.component.scss b/src/ui/client/src/app/stats/stats.component.scss index ca11492..73baccc 100644 --- a/src/ui/client/src/app/stats/stats.component.scss +++ b/src/ui/client/src/app/stats/stats.component.scss @@ -1,8 +1,3 @@ -.stats-ctn { - overflow: scroll; - max-height: 80vh; -} - .inline { display: flex; flex-direction: row; @@ -10,15 +5,12 @@ } .app-stats { - width: 98%; - .app-stats-header { display: flex; flex-direction: row; align-items: center; gap: 10px; flex: 1; - background-color: rgb(239, 239, 239); h1 { margin: 5px 40px; @@ -29,17 +21,18 @@ } } - h1 { - display: inline; - } + .main-stats { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + column-gap: 16px; - a { - text-decoration: none; - color: rgb(0, 0, 0); - } + .dashboard-card.positive-card { + background-color: rgb(159, 229, 180); + } - a:visited { - text-decoration: none; + .dashboard-card.negative-card { + background-color: rgb(255, 174, 174); + } } div a:hover::after { @@ -51,6 +44,29 @@ content: "\00A0\1F517"; } + .observations-list { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 16px; + + .observation-card { + .card-content { + margin-top: 12px; + } + } + + .observations-histogram { + max-height: 200px; + max-width: 600px; + padding: 10px; + } + + .export-csv-button { + margin-top: 16px; + } + } + .app-stats-main-stats { display: flex; flex-direction: row; diff --git a/src/ui/client/src/app/stats/stats.component.ts b/src/ui/client/src/app/stats/stats.component.ts index d9ba19d..a33cbc3 100644 --- a/src/ui/client/src/app/stats/stats.component.ts +++ b/src/ui/client/src/app/stats/stats.component.ts @@ -1,10 +1,11 @@ -import { KeyValue } from "@angular/common" -import { ChangeDetectionStrategy, Component } from "@angular/core" -import { ActivatedRoute } from "@angular/router" -import { ModronStore } from "../state/modron.store" -import { StatsService } from "../stats.service" +import {KeyValue} from "@angular/common" +import {ChangeDetectionStrategy, Component} from "@angular/core" +import {ActivatedRoute} from "@angular/router" +import {ModronStore} from "../state/modron.store" +import {StatsService} from "../stats.service" -import * as pb from "src/proto/modron_pb" +import * as pb from "../../proto/modron_pb" +import {StructValueToStringPipe} from "../filter.pipe"; @Component({ selector: "app-stats", @@ -13,21 +14,10 @@ import * as pb from "src/proto/modron_pb" changeDetection: ChangeDetectionStrategy.OnPush, }) export class StatsComponent { - constructor(public store: ModronStore, public stats: StatsService, private route: ActivatedRoute) { } - - displaySearchRules: Map = new Map(); - - toggleSearch(rule: string): void { - if (this.displaySearchRules.has(rule)) { - this.displaySearchRules.set( - rule, - !(this.displaySearchRules.get(rule) as boolean) - ) - } else { - this.displaySearchRules.set(rule, true) - } + constructor(public store: ModronStore, public stats: StatsService, private route: ActivatedRoute) { } + displaySearchRules: Map = new Map(); mapByType( obs: Map> ): Map { @@ -43,34 +33,31 @@ export class StatsComponent { return obsByType } - exportCsvMap(data: Map, filename: string) { - const csvData = Array.from( - data, - ([k, v]) => `${k.replace(/,/g, "")},${v}` - ).reduce((prev, curr) => `${prev}\n${curr}`) - this, this.exportCsv(csvData, filename) - } - exportCsvObs(obs: pb.Observation[], filename: string) { - const header = "resource-name,resource-group,observed-value,scan-date\n" - const data = obs.map( - (v) => - `${v.getResource()?.getName()},${v - .getResource() - ?.getResourceGroupName().replace("projects/", "")},${v.getObservedValue()},'${v - .getTimestamp() - ?.toDate() - .toUTCString()}'` - ) - this, + const rows: string[][] = [ + ["resource-name", "resource-group", "expected-value", "observed-value", "scan-date"] + ] + const obsRows: string[][] = obs.map( + (v) => { + return [ + v.getResourceRef()?.getExternalId(), + v.getResourceRef()?.getGroupName().replace("projects/", ""), + StructValueToStringPipe.prototype.transform(v.getExpectedValue()), + StructValueToStringPipe.prototype.transform(v.getObservedValue()), + v.getTimestamp()?.toDate().toUTCString() + ] as string[] + } + ) + rows.push(...obsRows) + obsRows.length = 0 this.exportCsv( - header + data.reduce((prev, curr) => `${prev}\n${curr}`), + rows.map((v)=>v.join(",")).join("\n"), filename ) } exportCsv(data: string, name: string): void { - const blob = new Blob([data], { type: "text/csv" }) + const blob = new Blob([data], {type: "text/csv;charset=utf-8"}) const url = window.URL.createObjectURL(blob) const filename = name + ".csv" diff --git a/src/ui/client/src/app/ui-demo/ui-demo.component.html b/src/ui/client/src/app/ui-demo/ui-demo.component.html new file mode 100644 index 0000000..769333f --- /dev/null +++ b/src/ui/client/src/app/ui-demo/ui-demo.component.html @@ -0,0 +1,48 @@ +
+

UI Demo

+

Severities

+

Empty

+
+ +
+ +

With number

+
+ +
+ +

Card

+
+ + +
+ +

Single Observation (old)

+ + +

Observation Dialog

+
+ +
+ +
diff --git a/src/ui/client/src/app/ui-demo/ui-demo.component.scss b/src/ui/client/src/app/ui-demo/ui-demo.component.scss new file mode 100644 index 0000000..451a2fc --- /dev/null +++ b/src/ui/client/src/app/ui-demo/ui-demo.component.scss @@ -0,0 +1,21 @@ +.severity-container { + display: grid; + grid-template-columns: repeat(8, 50px); + row-gap: 10px; +} + +.rg-container { + display: grid; + padding: 10px; + grid-template-columns: repeat(4, 300px); + column-gap: 10px; + row-gap: 10px; +} + +.observation-dialog { + width: 800px; + border: 1px solid #333; + padding: 10px; + border-radius: 5px; + margin: 16px; +} diff --git a/src/ui/client/src/app/ui-demo/ui-demo.component.ts b/src/ui/client/src/app/ui-demo/ui-demo.component.ts new file mode 100644 index 0000000..5b742cc --- /dev/null +++ b/src/ui/client/src/app/ui-demo/ui-demo.component.ts @@ -0,0 +1,32 @@ +import {ChangeDetectionStrategy, Component} from "@angular/core"; +import {Impact, Observation, Remediation, Severity} from "../../proto/modron_pb"; +import Category = Observation.Category; + +@Component({ + selector: "app-ui-demo", + templateUrl: "./ui-demo.component.html", + styleUrls: ["./ui-demo.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UIDemoComponent { + protected readonly Severity = Severity; + protected readonly Object = Object; + protected readonly severityValues: Severity[] = Object.values(Severity).reverse() as Severity[]; + protected readonly Date = Date; + date: Date | null = new Date(); + + public demoObservation = new Observation(); + constructor() { + this.demoObservation.setName("EXAMPLE_DEMO_OBSERVATION"); + this.demoObservation.setRiskScore(Severity.SEVERITY_CRITICAL); + this.demoObservation.setSeverity(Severity.SEVERITY_HIGH); + this.demoObservation.setImpact(Impact.IMPACT_HIGH); + this.demoObservation.setCategory(Category.CATEGORY_MISCONFIGURATION) + this.demoObservation.setImpactReason("environment=production") + const remediation = new Remediation(); + remediation.setDescription("Example description"); + remediation.setRecommendation("Example recommendation"); + this.demoObservation.setRemediation(remediation); + } + +} diff --git a/src/ui/client/src/assets/modron-white.svg b/src/ui/client/src/assets/modron-white.svg new file mode 100644 index 0000000..0ced35d --- /dev/null +++ b/src/ui/client/src/assets/modron-white.svg @@ -0,0 +1,42 @@ + + + + diff --git a/src/ui/client/src/assets/modron.svg b/src/ui/client/src/assets/modron.svg new file mode 100644 index 0000000..17410ca --- /dev/null +++ b/src/ui/client/src/assets/modron.svg @@ -0,0 +1,42 @@ + + + + diff --git a/src/ui/client/src/colors.scss b/src/ui/client/src/colors.scss new file mode 100644 index 0000000..f390a11 --- /dev/null +++ b/src/ui/client/src/colors.scss @@ -0,0 +1,23 @@ +@use '@angular/material' as mat; + +$my-primary: mat.m2-define-palette(mat.$m2-indigo-palette, 500); +$my-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400); + +$my-theme: mat.m2-define-light-theme(( + color: ( + primary: $my-primary, + accent: $my-accent, + ), + density: 0, +)); + +$sideNavBackground: mat.m2-get-color-from-palette($my-primary, default, 0.1); +$sideNavButtonActiveBackground: mat.m2-get-color-from-palette($my-primary, default, 0.2); + +$title: #FFF; +$secondaryText: #555; + +$danger: #da1e28; +$warning: #f5a623; +$allGood: #24a148; + diff --git a/src/ui/client/src/main.ts b/src/ui/client/src/main.ts index 677f51d..c41b6f5 100644 --- a/src/ui/client/src/main.ts +++ b/src/ui/client/src/main.ts @@ -9,4 +9,4 @@ if (environment.production) { } platformBrowserDynamic().bootstrapModule(AppModule) - .catch(err => console.error(err)); \ No newline at end of file + .catch(err => console.error(err)); diff --git a/src/ui/client/src/styles.scss b/src/ui/client/src/styles.scss index 5026e82..35d9235 100644 --- a/src/ui/client/src/styles.scss +++ b/src/ui/client/src/styles.scss @@ -1,10 +1,14 @@ +@use '@angular/material' as mat; +@import '@material-symbols/font-400'; +@import 'colors.scss'; + /* You can add global styles to this file, and also import other style files */ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans&display=swap'); -@import '@angular/material/prebuilt-themes/deeppurple-amber.css'; + +@include mat.all-component-themes($my-theme); body { font-family: 'IBM Plex Sans', sans-serif; - overflow: hidden; margin: 0; height: 100% } @@ -24,3 +28,19 @@ input { border: none; background-color: rgb(249, 249, 249); } + +/* + Styles for dynamic elements +*/ +.mat-column-shortDesc > p{ + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; +} + +.markdown-content { + p { + margin-top: 0; + margin-bottom: 0; + } +} diff --git a/src/ui/client/tsconfig.app.json b/src/ui/client/tsconfig.app.json index 9b0cbd2..7c7374a 100644 --- a/src/ui/client/tsconfig.app.json +++ b/src/ui/client/tsconfig.app.json @@ -11,5 +11,6 @@ ], "include": [ "src/**/*.d.ts", + "src/proto/**/*.ts" ] } diff --git a/src/ui/docker-compose.yml b/src/ui/docker-compose.yml new file mode 100644 index 0000000..d5fbd30 --- /dev/null +++ b/src/ui/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3' +services: + mock-grpc-server: + build: + context: mock-grpc-server + networks: + - envoy-net + envoy: + image: envoyproxy/envoy:v1.29-latest + volumes: + - ./mock-grpc-server/envoy.yaml:/etc/envoy/envoy.yaml:ro + ports: + - "4201:4201" + networks: + - envoy-net +networks: + envoy-net: diff --git a/src/ui/go.mod b/src/ui/go.mod index c34317f..628cb5a 100644 --- a/src/ui/go.mod +++ b/src/ui/go.mod @@ -1,27 +1,35 @@ module server -go 1.21 +go 1.23.2 require ( - github.com/golang/glog v1.1.1 - google.golang.org/api v0.131.0 + github.com/golang/glog v1.2.2 + google.golang.org/api v0.203.0 ) require ( - cloud.google.com/go/compute v1.21.0 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/auth v0.10.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect + cloud.google.com/go/compute/metadata v0.5.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/s2a-go v0.1.4 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.11.0 // indirect - golang.org/x/net v0.12.0 // indirect - golang.org/x/oauth2 v0.10.0 // indirect - golang.org/x/sys v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230717213848-3f92550aa753 // indirect - google.golang.org/grpc v1.56.2 // indirect - google.golang.org/protobuf v1.31.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect ) diff --git a/src/ui/go.sum b/src/ui/go.sum new file mode 100644 index 0000000..75b455c --- /dev/null +++ b/src/ui/go.sum @@ -0,0 +1,202 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw= +cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s= +cloud.google.com/go/auth v0.10.0 h1:tWlkvFAh+wwTOzXIjrwM64karR1iTBZ/GRr0S/DULYo= +cloud.google.com/go/auth v0.10.0/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk= +cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= +github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= +github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.185.0 h1:ENEKk1k4jW8SmmaT6RE+ZasxmxezCrD5Vw4npvr+pAU= +google.golang.org/api v0.185.0/go.mod h1:HNfvIkJGlgrIlrbYkAm9W9IdkmKZjOTVh33YltygGbg= +google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU= +google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4 h1:CUiCqkPw1nNrNQzCCG4WA65m0nAmQiwXHpub3dNyruU= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/src/ui/mock-grpc-server/.dockerignore b/src/ui/mock-grpc-server/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/src/ui/mock-grpc-server/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/src/ui/mock-grpc-server/Dockerfile b/src/ui/mock-grpc-server/Dockerfile new file mode 100644 index 0000000..339b95f --- /dev/null +++ b/src/ui/mock-grpc-server/Dockerfile @@ -0,0 +1,6 @@ +FROM node:lts +COPY . /app +WORKDIR /app +RUN npm install +ENV NODE_OPTIONS='--loader ts-node/esm' +ENTRYPOINT ["node", "server.ts"] diff --git a/src/ui/mock-grpc-server/copy-proto.sh b/src/ui/mock-grpc-server/copy-proto.sh new file mode 100755 index 0000000..9d735d0 --- /dev/null +++ b/src/ui/mock-grpc-server/copy-proto.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +# Unfortunately Docker doesn't support symlinks, so we need to copy the proto files + +# Make sure we're running in the script directory +pushd "$(dirname "$0")" +cp ../../proto/*.proto proto/ +popd + diff --git a/src/ui/mock-grpc-server/envoy.yaml b/src/ui/mock-grpc-server/envoy.yaml index a9cd615..5d1caf2 100644 --- a/src/ui/mock-grpc-server/envoy.yaml +++ b/src/ui/mock-grpc-server/envoy.yaml @@ -62,7 +62,7 @@ static_resources: - endpoint: address: socket_address: - address: 10.246.6.2 # you may need to replace this with your container/machine IP. + address: mock-grpc-server port_value: 4202 admin: diff --git a/src/ui/mock-grpc-server/package-lock.json b/src/ui/mock-grpc-server/package-lock.json new file mode 100644 index 0000000..bf9b507 --- /dev/null +++ b/src/ui/mock-grpc-server/package-lock.json @@ -0,0 +1,915 @@ +{ + "name": "modron-mock-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "modron-mock-server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@alenon/grpc-mock-server": "^3.0.21", + "@grpc/grpc-js": "^1.8", + "@improbable-eng/grpc-web": "^0.15.0", + "@types/google-protobuf": "^3.15.6", + "google-protobuf": "^3.21.2", + "nodemon": "^3.0.1", + "ts-node": "^10.9.1", + "wait-for-sigint": "^0.1.0" + }, + "devDependencies": { + "ts-protoc-gen": "^0.15.0" + } + }, + "node_modules/@alenon/grpc-mock-server": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/@alenon/grpc-mock-server/-/grpc-mock-server-3.0.21.tgz", + "integrity": "sha512-ZSEPk7BwRm18sGsu4665outrCDBkpPV3sDT/kAS6IdJdsXvwcVcY5EBlnRGGiBW8mkXgFHrO+nJsccmBqD0TvA==", + "dependencies": { + "@grpc/grpc-js": "^1.8.8", + "@types/debug": "^4.1.8", + "@types/google-protobuf": "^3.7.4", + "@types/node": "^20.2.4", + "debug": "^4.3.1", + "google-protobuf": "^3.14.0", + "protobufjs": "^7.2.1" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.8.22", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz", + "integrity": "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.7.tgz", + "integrity": "sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ==", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@improbable-eng/grpc-web": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.15.0.tgz", + "integrity": "sha512-ERft9/0/8CmYalqOVnJnpdDry28q+j+nAlFFARdjyxXDJ+Mhgv9+F600QC8BR9ygOfrXRlAk6CvST2j+JCpQPg==", + "dependencies": { + "browser-headers": "^0.4.1" + }, + "peerDependencies": { + "google-protobuf": "^3.14.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/google-protobuf": { + "version": "3.15.6", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.6.tgz", + "integrity": "sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, + "node_modules/@types/node": { + "version": "20.4.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz", + "integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-headers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", + "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/protobufjs": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", + "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-protoc-gen": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/ts-protoc-gen/-/ts-protoc-gen-0.15.0.tgz", + "integrity": "sha512-TycnzEyrdVDlATJ3bWFTtra3SCiEP0W0vySXReAuEygXCUr1j2uaVyL0DhzjwuUdQoW5oXPwk6oZWeA0955V+g==", + "dev": true, + "dependencies": { + "google-protobuf": "^3.15.5" + }, + "bin": { + "protoc-gen-ts": "bin/protoc-gen-ts" + } + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "node_modules/wait-for-sigint": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wait-for-sigint/-/wait-for-sigint-0.1.0.tgz", + "integrity": "sha512-vrHZMa7tTI/9zD3HR6nSZ6242PpUVw5Tn4Mij8PJObahR1zW2mQZ7mzUjRp+iGViD7a39gysct8/1cEl7LE2ig==" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/src/ui/mock-grpc-server/package.json b/src/ui/mock-grpc-server/package.json index 49fa780..ea116a8 100644 --- a/src/ui/mock-grpc-server/package.json +++ b/src/ui/mock-grpc-server/package.json @@ -15,7 +15,7 @@ "wait-for-sigint": "^0.1.0", "nodemon": "^3.0.1", "ts-node": "^10.9.1", - "@grpc/grpc-js": "^1.8.18", + "@grpc/grpc-js": "^1.8", "@improbable-eng/grpc-web": "^0.15.0", "@types/google-protobuf": "^3.15.6", "google-protobuf": "^3.21.2" diff --git a/src/ui/mock-grpc-server/proto/.gitkeep b/src/ui/mock-grpc-server/proto/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/mock-grpc-server/server.ts b/src/ui/mock-grpc-server/server.ts index 6e8a655..960e944 100644 --- a/src/ui/mock-grpc-server/server.ts +++ b/src/ui/mock-grpc-server/server.ts @@ -9,8 +9,8 @@ import * as proto_loader from '@grpc/proto-loader' const __dirname = dirname(fileURLToPath(import.meta.url)) class ModronMockGrpcServer { - private static readonly MODRON_PROTO_PATH: string = __dirname + '/../../proto/modron.proto' - private static readonly NOTIFICATION_PROTO_PATH: string = __dirname + '/../../proto/notification.proto' + private static readonly MODRON_PROTO_PATH: string = 'proto/modron.proto' + private static readonly NOTIFICATION_PROTO_PATH: string = 'proto/notification.proto' private static readonly PKG_NAME: string = '' private static readonly MODRON_SERVICE_NAME: string = 'ModronService' private static readonly NOTIFICATION_SERVICE_NAME: string = 'NotificationService' @@ -76,6 +76,41 @@ class ModronMockGrpcServer { await this.initMockServer() } + private generateObservations(ruleNr: number, projectName: string, amountObs?: number) { + let observations = []; + if(amountObs == undefined){ + return; + } + for(let i = 0; i < ruleNr; i++){ + observations.push(new this.mpb.Observation.constructor({ + name: `obs-${ruleNr}-rsrc-${i}`, + timestamp: { + seconds: new Date().getTime() / 1000, + nanos: 456, + }, + uid: "5cedca54-a6e0-4de5-8df5-facc533f5903--" + ruleNr, + remediation: this.getRemediation(), + resource: new this.mpb.Resource.constructor({ + name: `resource-${i}` + "[observation" + ruleNr + "]", + resourceGroupName: "project" + projectName, + timestamp: { + seconds: new Date().getTime() / 1000, + nanos: 456, + }, + }), + })); + } + + return observations + } + + private getRemediation() { + return new this.mpb.Remediation.constructor({ + description: "The project \"projects/example\" gives the principal [\"some-account@example.iam.gserviceaccount.com\"](https://example.com) vast permissions through the role `compute.loadBalancerAdmin`. This principal is defined in another project which means that anybody with rights in that project can use it to control the resources in this one.", + recommendation: "Replace the principal [\"some-account@example.iam.gserviceaccount.com\"](https://example.com) controlling the project \"projects/example\" with a principal created in the project \"//cloudresourcemanager.googleapis.com/folders/12345678\" that grants it the smallest set of permissions needed to operate.", + }) + } + private async initMockServer() { const modron_impls = { GetStatusCollectAndScan: (call: any, cb: any) => { @@ -144,29 +179,10 @@ class ModronMockGrpcServer { resourceGroupName: e[2], rulesObservations: [0, 1, 2, 3, 4, 5, 6].map(ruleNb => new this.mpb.RuleObservationPair.constructor({ rule: "observation" + ruleNb, - observations: (e[1] as Array).filter(ele => ele === ruleNb).map(e1 => new this.mpb.Observation.constructor({ - name: "observation" + e1, - timestamp: { - seconds: 123, - nanos: 456, - }, - uid: "5cedca54-a6e0-4de5-8df5-facc533f5903--" + e1, - remediation: new this.mpb.Remediation.constructor({ - description: "some description [title](https://www.example.com)", - recommendation: "do something [title](https://www.example.com)", - }), - resource: new this.mpb.Resource.constructor({ - name: "project" + e[0] + "[observation" + e1 + "]", - resourceGroupName: "project" + e[0], - timestamp: { - seconds: 1273, - nanos: 456, - }, - }), - })), - })) + observations: this.generateObservations(ruleNb, e[2] as string, e[1][ruleNb]), + })), + nextPageToken: '', })), - nextPageToken: '', })) } else { cb(null, new this.mpb.ListObservationsResponse.constructor({ @@ -177,26 +193,7 @@ class ModronMockGrpcServer { resourceGroupName: e[2], rulesObservations: [0, 1, 2, 3, 4, 5, 6].map(ruleNb => new this.mpb.RuleObservationPair.constructor({ rule: "observation" + ruleNb, - observations: (e[1] as Array).filter(ele => ele === ruleNb).map(e1 => new this.mpb.Observation.constructor({ - name: "observation" + e1, - timestamp: { - seconds: new Date().getTime() / 1000, - nanos: 456, - }, - uid: "5cedca54-a6e0-4de5-8df5-facc533f5903--" + e1, - remediation: new this.mpb.Remediation.constructor({ - description: "some description [title](https://www.example.com)", - recommendation: "do something [title](https://www.example.com)", - }), - resource: new this.mpb.Resource.constructor({ - name: "project" + e[0] + "[observation" + e1 + "]", - resourceGroupName: "project" + e[0], - timestamp: { - seconds: new Date().getTime() / 1000, - nanos: 456, - }, - }), - })), + observations: this.generateObservations(ruleNb, e[2] as string, e[1][ruleNb]), })) })), nextPageToken: '', @@ -255,14 +252,12 @@ await server.run() let sigterm = () => { process.stdin.resume() - var p = new Promise(function (resolve, reject) { + return new Promise(function (resolve, reject) { process.on('SIGTERM', function () { process.stdin.pause() resolve() }) - }) - - return p + }); } await sigterm() diff --git a/src/ui/package-lock.json b/src/ui/package-lock.json new file mode 100644 index 0000000..ef4713c --- /dev/null +++ b/src/ui/package-lock.json @@ -0,0 +1,321 @@ +{ + "name": "ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ui", + "version": "0.0.0", + "hasInstallScript": true, + "dependencies": { + "concurrently": "^8.2.2" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/src/ui/package.json b/src/ui/package.json index d116669..7d23058 100644 --- a/src/ui/package.json +++ b/src/ui/package.json @@ -3,25 +3,12 @@ "version": "0.0.0", "scripts": { "postinstall": "(cd client && npm install); (cd mock-grpc-server && npm install)", - "dev": "npm run dev:mock-grpc-server-envoy && concurrently --kill-others \"npm run dev:client\" \"npm run dev:mock-grpc-server\"", - "dev:client": "cd client/ && ng serve --verbose --proxy-config ../proxy.conf.json", + "dev": "concurrently --kill-others \"npm run dev:client\" \"docker-compose up -d\"", + "dev:client": "cd client/ && npm run ng -- serve --verbose --proxy-config ../proxy.conf.json", "dev:mock-grpc-server-envoy": "cd mock-grpc-server/ && docker run --rm -p4201:4201 -p9901:9901 -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml:ro -t envoyproxy/envoy:v1.24-latest", - "dev:mock-grpc-server": "npm run --prefix mock-grpc-server/ dev", - "genproto": "npm run --prefix client/ genproto && npm run --prefix mock-grpc-server/ genproto" - }, - "devDependencies": { - "@angular-eslint/eslint-plugin": "^16.1.0", - "@angular-eslint/eslint-plugin-template": "^16.1.0", - "@angular-eslint/template-parser": "^16.1.0", - "@typescript-eslint/eslint-plugin": "^6.1.0", - "@typescript-eslint/parser": "^6.1.0", - "concurrently": "^8.2.0", - "eslint": "^8.45.0" + "dev:mock-grpc-server": "npm run --prefix mock-grpc-server/ dev" }, "dependencies": { - "@alenon/grpc-mock-server": "^3.0.21", - "@angular/material": "^16.1.5", - "google-protobuf": "^3.21.2", - "ts-node": "^10.9.1" + "concurrently": "^8.2.2" } } diff --git a/src/ui/server.go b/src/ui/server.go index fbcc3db..24b5375 100644 --- a/src/ui/server.go +++ b/src/ui/server.go @@ -40,6 +40,7 @@ func main() { } mux := http.NewServeMux() mux.HandleFunc("/", handle) + glog.Infof("Listening on port %d", port) if err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux); err != nil { glog.Errorf("listenAndServe: %v", err) os.Exit(2) diff --git a/src/utils/gcp.go b/src/utils/gcp.go new file mode 100644 index 0000000..d859a92 --- /dev/null +++ b/src/utils/gcp.go @@ -0,0 +1,101 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/nianticlabs/modron/src/constants" + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +func IsGCPServiceAccountProject(project string) bool { + _, ok := constants.GCPServiceAgentsProjects[project] + return ok +} + +func GetGCPProjectFromSAEmail(saEmail string) string { + if strings.HasSuffix(saEmail, appspotServiceAccountSuffix) { + return strings.TrimSuffix(saEmail, appspotServiceAccountSuffix) + } + + if strings.HasSuffix(saEmail, iamGServiceAccountSuffix) { + noSuffix := strings.TrimSuffix(saEmail, iamGServiceAccountSuffix) + split := strings.Split(noSuffix, "@") + if len(split) != 2 { //nolint:mnd + log.Errorf("failed to split service account email: %s", saEmail) + return "" + } + return split[1] + } + + if strings.HasSuffix(saEmail, developerGserviceAccountSuffix) { + // Can't handle this (we need a project ID, here we only have a project number) + return "" + } + + log.Warnf("unknown service account email format: %s", saEmail) + return "" +} + +type ResourceLink struct { + Name string + URL string + Type string +} + +// TODO: Make sure all the observations use this function +func LinkGCPResource(resource *pb.Resource) ResourceLink { + switch resource.Type.(type) { + case *pb.Resource_Bucket: + return ResourceLink{ + Name: resource.Name, + URL: "https://console.cloud.google.com/storage/browser/" + resource.Name, + Type: "bucket", + } + case *pb.Resource_Database: + return ResourceLink{ + Name: resource.Name, + URL: fmt.Sprintf( + "https://console.cloud.google.com/spanner/instances/%s/details/databases", + resource.Name, + ), + Type: "database", + } + + case *pb.Resource_ServiceAccount: + saEmail := strings.TrimPrefix(resource.Name, constants.GCPServiceAccountPrefix) + return ResourceLink{ + Name: saEmail, + URL: fmt.Sprintf( + "https://console.cloud.google.com/iam-admin/serviceaccounts/details/%s?project=%s", + saEmail, + strings.TrimPrefix(resource.ResourceGroupName, constants.GCPProjectsNamePrefix), + ), + Type: "service account", + } + case *pb.Resource_ResourceGroup: + gcpType := "" + switch { + case strings.HasPrefix(resource.Name, constants.GCPProjectsNamePrefix): + gcpType = "project" + case strings.HasPrefix(resource.Name, constants.GCPFolderIDPrefix): + gcpType = "folder" + case strings.HasPrefix(resource.Name, constants.GCPOrgIDPrefix): + gcpType = "organization" + default: + log.Warnf("LinkGCPResource: unknown resource type: %s", resource.Name) + } + return ResourceLink{ + Name: strings.TrimPrefix(resource.Name, constants.GCPProjectsNamePrefix), + URL: fmt.Sprintf("https://console.cloud.google.com/welcome?project=%s", + strings.TrimPrefix(resource.Name, constants.GCPProjectsNamePrefix), + ), + Type: gcpType, + } + default: + log.Warnf("LinkGCPResource: unknown resource type: %T", resource.Type) + } + return ResourceLink{ + Name: resource.Name, + } +} diff --git a/src/utils/gke.go b/src/utils/gke.go new file mode 100644 index 0000000..7aa2d15 --- /dev/null +++ b/src/utils/gke.go @@ -0,0 +1,33 @@ +package utils + +import ( + "fmt" + "strings" +) + +func GetGKEReference(resourceLink string) (projectID string, location string, clusterName string, namespace string) { + // resourceLink is formatted as follows: + // //container.googleapis.com/projects/project-id/zones/us-central1-b/clusters/gke-cluster-name/k8s/namespaces/kubernetes-ns-name + split := strings.Split(resourceLink, "/") + if len(split) != 12 { //nolint:mnd + return "", "", "", "" + } + projectID = split[4] + location = split[6] + clusterName = split[8] + namespace = split[11] + return +} + +func GetGkePodLink(name string, parent string) string { + // name is like "my-pod-name" + // parent is "//container.googleapis.com/projects/project-id/zones/us-central1-b/clusters/gke-cluster-name/k8s/namespaces/kubernetes-ns-name" + projectID, location, clusterName, namespace := GetGKEReference(parent) + return fmt.Sprintf("https://console.cloud.google.com/kubernetes/pod/%s/%s/%s/%s/details?project=%s", + location, + clusterName, + namespace, + name, + projectID, + ) +} diff --git a/src/utils/gke_test.go b/src/utils/gke_test.go new file mode 100644 index 0000000..6da3d91 --- /dev/null +++ b/src/utils/gke_test.go @@ -0,0 +1,31 @@ +package utils + +import "testing" + +func TestGetGKEReference(t *testing.T) { + tc := [][]string{ + { + "//container.googleapis.com/projects/project-id/zones/us-central1-b/clusters/gke-cluster-name/k8s/namespaces/kubernetes-ns-name", + "project-id", + "us-central1-b", + "gke-cluster-name", + "kubernetes-ns-name", + }, + } + + for _, tt := range tc { + projectID, location, clusterName, namespace := GetGKEReference(tt[0]) + if projectID != tt[1] { + t.Errorf("expected projectID %s, got %s", tt[1], projectID) + } + if location != tt[2] { + t.Errorf("expected location %s, got %s", tt[2], location) + } + if clusterName != tt[3] { + t.Errorf("expected clusterName %s, got %s", tt[3], clusterName) + } + if namespace != tt[4] { + t.Errorf("expected namespace %s, got %s", tt[4], namespace) + } + } +} diff --git a/src/utils/groups.go b/src/utils/groups.go new file mode 100644 index 0000000..f0d72ad --- /dev/null +++ b/src/utils/groups.go @@ -0,0 +1,15 @@ +package utils + +import ( + "golang.org/x/exp/maps" + + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +func GroupsFromResources(resources []*pb.Resource) (allGroups []string) { + resourceGroups := map[string]struct{}{} + for _, r := range resources { + resourceGroups[r.ResourceGroupName] = struct{}{} + } + return maps.Keys(resourceGroups) +} diff --git a/src/utils/hierarchy.go b/src/utils/hierarchy.go new file mode 100644 index 0000000..86c01d6 --- /dev/null +++ b/src/utils/hierarchy.go @@ -0,0 +1,72 @@ +package utils + +import ( + "fmt" + + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +func ComputeRgHierarchy(resources []*pb.Resource) (map[string]*pb.RecursiveResource, error) { + resourceMap := make(map[string]*pb.RecursiveResource) + for _, r := range resources { + if r.GetResourceGroup() == nil { + continue + } + recRes, err := ToRecursiveResource(r) + if err != nil { + return nil, err + } + resourceMap[r.Name] = recRes + } + + for _, r := range resources { + if r.Parent == "" { + var err error + resourceMap[""], err = ToRecursiveResource(r) + if err != nil { + return nil, err + } + continue + } + parent, ok := resourceMap[r.Parent] + if !ok { + log.Warnf("parent %q not found", r.Parent) + if r.ResourceGroupName == "" { + log.Errorf("resource %q has no parent and no resource group", r.Name) + continue + } + if r.ResourceGroupName == r.Name { + log.Errorf("resource %q is its own parent", r.Name) + continue + } + parent, ok = resourceMap[r.ResourceGroupName] + if !ok { + log.Errorf("resource group %q not found, is %q orphan?", r.ResourceGroupName, r.Name) + continue + } + } + recRes, err := ToRecursiveResource(r) + if err != nil { + return nil, fmt.Errorf("toRecursiveResource: %w", err) + } + parent.Children = append(parent.Children, recRes) + resourceMap[r.Parent] = parent + } + return resourceMap, nil +} + +func ToRecursiveResource(r *pb.Resource) (*pb.RecursiveResource, error) { + t, err := TypeFromResource(r) + if err != nil { + return nil, fmt.Errorf("typeFromResourceAsString: %w", err) + } + return &pb.RecursiveResource{ + Uuid: r.Uid, + Name: r.Name, + DisplayName: r.DisplayName, + Parent: r.Parent, + Type: t, + Labels: r.Labels, + Tags: r.Tags, + }, nil +} diff --git a/src/utils/keys.go b/src/utils/keys.go new file mode 100644 index 0000000..345cad2 --- /dev/null +++ b/src/utils/keys.go @@ -0,0 +1,44 @@ +package utils + +import ( + "strings" + + "github.com/sirupsen/logrus" + + "github.com/nianticlabs/modron/src/constants" +) + +var log = logrus.StandardLogger().WithField(constants.LogKeyPkg, "utils") + +const keyParts = 6 + +// GetKeyID converts a key reference (projects/my-project/serviceAccounts/sa-1/keys/abc) to a key ID (abc). +func GetKeyID(keyRef string) string { + if !strings.HasPrefix(keyRef, constants.GCPProjectsNamePrefix) { + log.Errorf("keyRef %s does not start with %s", keyRef, constants.GCPProjectsNamePrefix) + return keyRef + } + + split := strings.Split(keyRef, "/") + if len(split) < keyParts { + log.Errorf("keyRef %s has less than 6 parts", keyRef) + return keyRef + } + + return split[5] +} + +func GetServiceAccountNameFromKeyRef(keyRef string) string { + if !strings.HasPrefix(keyRef, constants.GCPProjectsNamePrefix) { + log.Errorf("keyRef %s does not start with %s", keyRef, constants.GCPProjectsNamePrefix) + return keyRef + } + + split := strings.Split(keyRef, "/") + if len(split) < keyParts { + log.Errorf("keyRef %s has less than 6 parts", keyRef) + return keyRef + } + + return split[3] +} diff --git a/src/utils/keys_test.go b/src/utils/keys_test.go new file mode 100644 index 0000000..99050c8 --- /dev/null +++ b/src/utils/keys_test.go @@ -0,0 +1,69 @@ +package utils_test + +import ( + "testing" + + "github.com/nianticlabs/modron/src/utils" +) + +func TestGetKeyID(t *testing.T) { + tests := []struct { + name string + keyRef string + expected string + }{ + { + name: "valid key reference", + keyRef: "projects/my-project/serviceAccounts/sa-1/keys/abc", + expected: "abc", + }, + { + name: "invalid key reference", + keyRef: "invalid-key-reference", + expected: "invalid-key-reference", + }, + { + name: "key reference with less than 6 parts", + keyRef: "projects/my-project/serviceAccounts/sa-1/keys", + expected: "projects/my-project/serviceAccounts/sa-1/keys", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := utils.GetKeyID(tt.keyRef); got != tt.expected { + t.Errorf("GetKeyID() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestGetServiceAccountNameFromKeyRef(t *testing.T) { + tests := []struct { + name string + keyRef string + expected string + }{ + { + name: "valid key reference", + keyRef: "projects/my-project/serviceAccounts/sa-1/keys/abc", + expected: "sa-1", + }, + { + name: "invalid key reference", + keyRef: "invalid-key-reference", + expected: "invalid-key-reference", + }, + { + name: "key reference with less than 6 parts", + keyRef: "projects/my-project/serviceAccounts/sa-1/keys", + expected: "projects/my-project/serviceAccounts/sa-1/keys", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := utils.GetServiceAccountNameFromKeyRef(tt.keyRef); got != tt.expected { + t.Errorf("GetServiceAccountNameFromKeyRef() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/src/utils/name.go b/src/utils/name.go new file mode 100644 index 0000000..060fd99 --- /dev/null +++ b/src/utils/name.go @@ -0,0 +1,26 @@ +package utils + +import "strings" + +const ( + containerAPI = "//container.googleapis.com/" + iamAPI = "//iam.googleapis.com/" + computeAPI = "//compute.googleapis.com/" +) + +// GetHumanReadableName returns the human-readable name of the resource - we currently use an allow-list of APIs +// to avoid interpreting the resource name incorrectly. +func GetHumanReadableName(resourceLink string) string { + for _, p := range []string{containerAPI, iamAPI, computeAPI} { + if strings.HasPrefix(resourceLink, p) { + r := strings.TrimPrefix(resourceLink, p) + split := strings.Split(r, "/") + return split[len(split)-1] + } + } + return resourceLink +} + +func StripProjectsPrefix(prefixedProject string) string { + return strings.TrimPrefix(prefixedProject, "projects/") +} diff --git a/src/utils/name_test.go b/src/utils/name_test.go new file mode 100644 index 0000000..1b0ff88 --- /dev/null +++ b/src/utils/name_test.go @@ -0,0 +1,62 @@ +package utils_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/nianticlabs/modron/src/utils" +) + +func TestGetHumanReadableName(t *testing.T) { + tc := [][]string{ + { + "//container.googleapis.com/projects/xyz/locations/us-central1/clusters/cluster-name/k8s/namespaces/kube-system/pods/my-pod-1", + "my-pod-1", + }, + { + "//container.googleapis.com/projects/xyz/locations/us-central1/clusters/cluster-name/k8s/namespaces/kube-system", + "kube-system", + }, + { + "//container.googleapis.com/projects/xyz/locations/us-central1/clusters/cluster-name", + "cluster-name", + }, + { + "//container.googleapis.com/projects/xyz/locations/us-central1", + "us-central1", + }, + { + "//container.googleapis.com/projects/xyz", + "xyz", + }, + { + "//iam.googleapis.com/projects/example-project/serviceAccounts/my-service-account@example-project.iam.gserviceaccount.com", + "my-service-account@example-project.iam.gserviceaccount.com", + }, + { + "//iam.googleapis.com/projects/example-project/serviceAccounts/3984989392373/keys/b8ceb3f5d69d4e46acc9e74bf224d4e9", + "b8ceb3f5d69d4e46acc9e74bf224d4e9", + }, + { + "//compute.googleapis.com/projects/example-project/zones/us-central1-f/instances/my-instance-4897322-03032024-cxx1-test-a0b0c0", + "my-instance-4897322-03032024-cxx1-test-a0b0c0", + }, + { + "//container.googleapis.com/projects/example-1/zones/us-central1-b/clusters/security-runners/k8s/namespaces/twistlock", + "twistlock", + }, + { + "//container.googleapis.com/projects/example-2/zones/us-central1-b/clusters/security-runners", + "security-runners", + }, + } + + for _, c := range tc { + want := c[1] + got := utils.GetHumanReadableName(c[0]) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("GetHumanReadableName(%q) mismatch (-want +got):\n%s", c[0], diff) + } + } +} diff --git a/src/utils/protobuf.go b/src/utils/protobuf.go new file mode 100644 index 0000000..4b0f80e --- /dev/null +++ b/src/utils/protobuf.go @@ -0,0 +1,40 @@ +package utils + +import ( + "fmt" + + "google.golang.org/protobuf/proto" + + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +func TypeFromResource(rsrc *pb.Resource) (ty string, err error) { + if rsrc == nil { + return "", fmt.Errorf("resource must not be nil") + } + reflectMsg := rsrc.ProtoReflect() + if reflectMsg == nil { + return "", fmt.Errorf("ProtoReflect() returned nil") + } + typeField := reflectMsg.Descriptor().Oneofs().ByName("type") + if typeField == nil { + return "", fmt.Errorf("cannot find field \"type\"") + } + field := reflectMsg.WhichOneof(typeField) + if field == nil { + return "", fmt.Errorf("cannot find field in oneof") + } + fieldMessage := reflectMsg.Get(field).Message() + if fieldMessage == nil { + return "", fmt.Errorf("field message is nil") + } + ty = string(fieldMessage.Descriptor().FullName()) + return +} + +func ProtoAcceptsTypes(types []proto.Message) (res []string) { + for _, t := range types { + res = append(res, string(t.ProtoReflect().Descriptor().FullName())) + } + return +} diff --git a/src/utils/protobuf_test.go b/src/utils/protobuf_test.go new file mode 100644 index 0000000..5c4c5b7 --- /dev/null +++ b/src/utils/protobuf_test.go @@ -0,0 +1,104 @@ +package utils + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/proto" + + pb "github.com/nianticlabs/modron/src/proto/generated" +) + +func TestTypeFromResource(t *testing.T) { + tests := []struct { + res *pb.Resource + want string + }{ + { + res: &pb.Resource{Type: &pb.Resource_VmInstance{}}, + want: "VmInstance", + }, + { + res: &pb.Resource{Type: &pb.Resource_ApiKey{}}, + want: "APIKey", + }, + { + res: &pb.Resource{Type: &pb.Resource_ServiceAccount{}}, + want: "ServiceAccount", + }, + { + res: &pb.Resource{Type: &pb.Resource_KubernetesCluster{}}, + want: "KubernetesCluster", + }, + } + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + got, err := TypeFromResource(tt.res) + if err != nil { + t.Errorf("TypeFromResource() error = %v", err) + } + if got != tt.want { + t.Errorf("TypeFromResource() gotTy = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTypeFromResourceFail(t *testing.T) { + tests := []struct { + res *pb.Resource + wantErr string + }{ + { + res: &pb.Resource{}, + wantErr: "cannot find field in oneof", + }, + { + res: nil, + wantErr: "resource must not be nil", + }, + } + for _, tt := range tests { + t.Run(tt.wantErr, func(t *testing.T) { + _, err := TypeFromResource(tt.res) + if diff := cmp.Diff(tt.wantErr, err.Error()); diff != "" { + t.Errorf("TypeFromResource() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestToAcceptedTypes(t *testing.T) { + type args struct { + types []proto.Message + } + tests := []struct { + name string + args args + wantRes []string + }{ + { + name: "Three elements", + args: args{ + types: []proto.Message{ + &pb.APIKey{}, + &pb.ServiceAccount{}, + &pb.Bucket{}, + }, + }, + wantRes: []string{ + "APIKey", + "ServiceAccount", + "Bucket", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotRes := ProtoAcceptsTypes(tt.args.types); !reflect.DeepEqual(gotRes, tt.wantRes) { + t.Errorf("ProtoAcceptsTypes() = %v, want %v", gotRes, tt.wantRes) + } + }) + } +} diff --git a/src/utils/ref.go b/src/utils/ref.go new file mode 100644 index 0000000..7af0e03 --- /dev/null +++ b/src/utils/ref.go @@ -0,0 +1,9 @@ +package utils + +func RefOrNull(s string) *string { + if s == "" { + return nil + } + + return &s +} diff --git a/src/utils/resource_ref.go b/src/utils/resource_ref.go new file mode 100644 index 0000000..4c6322f --- /dev/null +++ b/src/utils/resource_ref.go @@ -0,0 +1,15 @@ +package utils + +import pb "github.com/nianticlabs/modron/src/proto/generated" + +func GetResourceRef(rsrc *pb.Resource) *pb.ResourceRef { + if rsrc == nil { + return nil + } + return &pb.ResourceRef{ + Uid: &rsrc.Uid, + GroupName: rsrc.ResourceGroupName, + ExternalId: RefOrNull(rsrc.Name), + CloudPlatform: pb.CloudPlatform_GCP, // TODO: Change when we have more cloud platforms + } +} diff --git a/src/utils/rule.go b/src/utils/rule.go new file mode 100644 index 0000000..d92d437 --- /dev/null +++ b/src/utils/rule.go @@ -0,0 +1,17 @@ +package utils + +import ( + "context" + "encoding/json" + + "github.com/nianticlabs/modron/src/model" +) + +func GetRuleConfig[T any](ctx context.Context, e model.Engine, name string, c *T) error { + v, err := e.GetRuleConfig(ctx, name) + if err != nil { + log.Errorf("no config found for rule %q: %v", name, err) + return err + } + return json.Unmarshal(v, c) +} diff --git a/src/utils/service_account.go b/src/utils/service_account.go new file mode 100644 index 0000000..bc6291c --- /dev/null +++ b/src/utils/service_account.go @@ -0,0 +1,7 @@ +package utils + +const ( + appspotServiceAccountSuffix = "@appspot.gserviceaccount.com" + iamGServiceAccountSuffix = ".iam.gserviceaccount.com" + developerGserviceAccountSuffix = "@developer.gserviceaccount.com" +) diff --git a/src/utils/service_account_test.go b/src/utils/service_account_test.go new file mode 100644 index 0000000..3cfac6b --- /dev/null +++ b/src/utils/service_account_test.go @@ -0,0 +1,35 @@ +package utils_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/nianticlabs/modron/src/utils" +) + +func TestGetProjectFromSAEmail(t *testing.T) { + tc := []struct { + saEmail string + expected string + }{ + { + "gitlab-sa@example-project.iam.gserviceaccount.com", + "example-project", + }, + { + "example-project@appspot.gserviceaccount.com", + "example-project", + }, + { + "123456789012-compute@developer.gserviceaccount.com", + "", + }, + } + + for _, tt := range tc { + if diff := cmp.Diff(tt.expected, utils.GetGCPProjectFromSAEmail(tt.saEmail)); diff != "" { + t.Errorf("unexpected result (-want +got):\n%s", diff) + } + } +} diff --git a/src/validation.go b/src/validation.go new file mode 100644 index 0000000..c7ca1da --- /dev/null +++ b/src/validation.go @@ -0,0 +1,119 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/nianticlabs/modron/src/collector" + "github.com/nianticlabs/modron/src/storage" +) + +func validateArgs() error { + var errArr []error + errArr = append(errArr, validateStorage()...) + errArr = append(errArr, validateProductionArgs()...) + errArr = append(errArr, validateGCPArgs()...) + errArr = append(errArr, validateRiskArgs()) + if err := errors.Join(errArr...); err != nil { + return err + } + warnArgs() + return nil +} + +func validateRiskArgs() error { + var impactMap map[string]string + if err := json.Unmarshal([]byte(args.ImpactMap), &impactMap); err != nil { + return fmt.Errorf("unable to decode impact map: %w", err) + } + + return nil +} + +func validateStorage() (errors []error) { + switch storage.Type(strings.ToLower(args.Storage)) { + case storage.Memory: + break + case storage.SQL: + errors = append(validateSQL(), errors...) + default: + errors = append(errors, fmt.Errorf("invalid storage type: %s", args.Storage)) + } + return +} + +func validateSQL() (errors []error) { + switch strings.ToLower(args.SQLBackendDriver) { + case "postgres": + break + default: + errors = append(errors, fmt.Errorf("invalid SQL backend driver: %s", args.SQLBackendDriver)) + return + } + + if args.SQLConnectionString == "" { + errors = append(errors, fmt.Errorf("SQL connection string is required")) + } + + if args.DbBatchSize < 1 { + errors = append(errors, fmt.Errorf("DB batch size must be greater than 0")) + } + + if args.DbMaxConnections < 1 { + errors = append(errors, fmt.Errorf("DB max connections must be greater than 0")) + } + + if args.DbMaxIdleConnections < 1 { + errors = append(errors, fmt.Errorf("DB max idle connections must be greater than 0")) + } + return +} + +func validateProductionArgs() (errors []error) { + if strings.EqualFold(args.Environment, "production") { + if args.NotificationService == "" { + errors = append(errors, fmt.Errorf("notification service is required in production")) + } + + if args.Collector == collector.Fake { + errors = append(errors, fmt.Errorf("fake collector cannot be used in production")) + } + + if args.SkipIAP { + errors = append(errors, fmt.Errorf("IAP cannot be skipped in production")) + } + + if args.PersistentCache { + errors = append(errors, fmt.Errorf("persistent cache cannot be used in production")) + } + } + return +} + +func validateGCPArgs() (errors []error) { + if args.OrgID == "" { + errors = append(errors, fmt.Errorf("organization ID is required")) + } + + if args.OrgSuffix == "" { + errors = append(errors, fmt.Errorf("organization suffix is required")) + } + return +} + +func warnArgs() { + if args.Collector == collector.Fake { + log.Warnf("Using fake collector") + } + if args.SkipIAP { + log.Warnf("Skipping IAP, if you see this in production, reach out to security.") + } + if args.NotificationService == "" { + log.Warnf("Notification service address is empty, logging instead") + } + if len(args.AdditionalAdminRoles) == 0 { + log.Warnf("No additional admin roles specified") + } +} diff --git a/terraform/dev/main.tf.example b/terraform/dev/main.tf.example index e618eec..724803c 100644 --- a/terraform/dev/main.tf.example +++ b/terraform/dev/main.tf.example @@ -3,10 +3,10 @@ module "modron" { source = "../modron" - domain = "hosted.at.example.com" + domain = "modron-dev.example.com" env = "dev" org_id = "GCP_ORGID" - project = "GCP_PROJECT_NAME-dev" + project = "my-modron-dev" zone = "GCP_ZONE" modron_admins = [ @@ -18,4 +18,8 @@ module "modron" { project_admins = [ "group:modron-project-admins@example.com" ] + docker_registry = "mirror.gcr.io" + notification_system = "https://notification-system.example.com" + notification_system_client_id = "client-id" + org_suffix = "@example.com" } diff --git a/terraform/modron/artifact_registry.tf b/terraform/modron/artifact_registry.tf new file mode 100644 index 0000000..8e290b8 --- /dev/null +++ b/terraform/modron/artifact_registry.tf @@ -0,0 +1,23 @@ +resource "google_artifact_registry_repository" "registry" { + location = local.region + repository_id = "modron" + description = "Modron Docker images" + format = "DOCKER" +} + +# writer is not enough: GitLab needs to be able to delete tags +# otherwise the pipeline will fail with IAM_PERMISSION_DENIED when trying to replace the :dev / :prod tags +data "google_iam_policy" "modron_repository_editor_policy" { + binding { + role = "organizations/0123456789/roles/ArtifactRegistryDockerEditor" + members = [ + "serviceAccount:${google_service_account.deployer_SA.email}" + ] + } +} + +resource "google_artifact_registry_repository_iam_policy" "modron_repository_write_policy" { + location = google_artifact_registry_repository.registry.location + repository = google_artifact_registry_repository.registry.name + policy_data = data.google_iam_policy.modron_repository_editor_policy.policy_data +} diff --git a/terraform/modron/cloud_run.tf b/terraform/modron/cloud_run.tf index 8729e0a..38a4d8e 100644 --- a/terraform/modron/cloud_run.tf +++ b/terraform/modron/cloud_run.tf @@ -1,132 +1,194 @@ -resource "google_cloud_run_service" "grpc_web" { +resource "google_cloud_run_v2_service" "grpc_web" { name = "modron-grpc-web-${var.env}" - location = substr(var.zone, 0, length(var.zone) - 2) + location = local.region - # We need this to avoid naming collision with CI/CD deployments. - autogenerate_revision_name = true template { - spec { - service_account_name = google_service_account.modron_runner.email - timeout_seconds = 1800 - containers { - image = "gcr.io/${var.project}/modron:${var.env}" - ports { - container_port = 8080 - name = "http1" - } - resources { - limits = { - cpu = "4000m" - memory = "4Gi" - } - } - env { - name = "ADMIN_GROUPS" - value = join(",", [for g in var.modron_admins : split(":", g)[1]]) - } - env { - name = "DB_MAX_CONNECTIONS" - # Max is 100 for Cloud SQL, but we may need some connections for other purposes. - value = 90 - } - env { - name = "ENVIRONMENT" - value = "PRODUCTION" - } - env { - name = "GCP_PROJECT_ID" - value = var.project - } - env { - name = "GLOG_logtostderr" - value = 1 - } - env { - name = "GLOG_v" - value = var.env == "dev" ? 10 : 1 - } - env { - name = "NOTIFICATION_INTERVAL_DURATION" - value = "720h" // 30d - } - env { - name = "NOTIFICATION_SERVICE" - value = "https://nagatha.example.com:443" - } - env { - name = "OBSERVATION_TABLE_ID" - value = "observations" - } - env { - name = "OPERATION_TABLE_ID" - value = "operations" - } - env { - name = "ORG_ID" - value = var.org_id - } - env { - name = "ORG_SUFFIX" - value = var.org_suffix - } - env { - name = "RESOURCE_TABLE_ID" - value = "resources" + scaling { + max_instance_count = 1 + min_instance_count = 1 + } + + service_account = google_service_account.modron_runner.email + timeout = "300s" + containers { + name = "modron" + depends_on = ["collector"] + image = "${local.region}-docker.pkg.dev/${var.project}/modron/modron:${var.env}" + ports { + container_port = 8080 + name = "http1" + } + startup_probe { + http_get { + path = "/healthz" + port = 8080 } - env { - name = "SQL_BACKEND_DRIVER" - value = "postgres" + initial_delay_seconds = 10 + period_seconds = 3 + failure_threshold = 5 * 20 # 5 minutes (20 times 3s intervals = 1 minute) + } + resources { + cpu_idle = false + limits = { + cpu = "4000m" + memory = "4Gi" } - env { - name = "SQL_CONNECT_STRING" - value_from { - secret_key_ref { - key = "latest" - name = split("/", resource.google_secret_manager_secret.sql_connect_string_config.name)[3] - } + } + env { + name = "ADDITIONAL_ADMIN_ROLES" + value = join(",", var.additional_admin_roles) + } + env { + name = "ADMIN_GROUPS" + value = join(",", [for g in var.modron_admins : split(":", g)[1]]) + } + env { + name = "ALLOWED_SCC_CATEGORIES" + value = join(",", var.allowed_scc_categories) + } + env { + name = "DB_MAX_CONNECTIONS" + # Max is 100 for Cloud SQL, but we may need some connections for other purposes. + value = 90 + } + env { + name = "ENVIRONMENT" + value = "production" + } + env { + name = "IMPACT_MAP" + value = jsonencode(var.impact_map) + } + env { + name = "LABEL_TO_EMAIL_REGEXP" + value = var.label_to_email_regexp + } + env { + name = "LABEL_TO_EMAIL_SUBSTITUTION" + value = var.label_to_email_substitution + } + env { + name = "LISTEN_ADDR" + value = "0.0.0.0" + } + env { + name = "LOG_LEVEL" + value = var.env == "dev" ? "debug" : "warning" + } + env { + name = "NOTIFICATION_INTERVAL_DURATION" + value = "720h" // 30d + } + env { + name = "NOTIFICATION_SERVICE" + value = var.notification_system + } + env { + name = "NOTIFICATION_SERVICE_CLIENT_ID" + value = var.notification_system_client_id + } + env { + name = "ORG_ID" + value = var.org_id + } + env { + name = "ORG_SUFFIX" + value = var.org_suffix + } + env { + name = "RULE_CONFIGS" + value = var.rule_configs + } + env { + name = "SELF_URL" + value = "https://${var.domain}" + } + env { + name = "SQL_BACKEND_DRIVER" + value = "postgres" + } + env { + name = "SQL_CONNECT_STRING" + value_source { + secret_key_ref { + secret = split("/", resource.google_secret_manager_secret.sql_connect_string_config.name)[3] + version = "latest" } } - env { - name = "STORAGE" - value = "SQL" + } + env { + name = "STORAGE" + value = "sql" + } + env { + name = "TAG_CUSTOMER_DATA" + value = "${var.org_id}/customer_data" + } + env { + name = "TAG_EMPLOYEE_DATA" + value = "${var.org_id}/employee_data" + } + env { + name = "TAG_ENVIRONMENT" + value = "${var.org_id}/environment" + } + + volume_mounts { + mount_path = "/cloudsql" + name = "cloudsql" + } + } + + containers { + name = "collector" + image = "${var.docker_registry}/otel/opentelemetry-collector-contrib:0.111.0" + startup_probe { + http_get { + path = "/" + port = 13133 } } + + volume_mounts { + mount_path = "/etc/otelcol-contrib" + name = "otel-config" + } + } + + vpc_access { + connector = google_vpc_access_connector.connector.id + egress = "PRIVATE_RANGES_ONLY" } - metadata { - annotations = { - "autoscaling.knative.dev/maxScale" = "1" - "autoscaling.knative.dev/minScale" = "1" - "client.knative.dev/user-image" = "gcr.io/${var.project}/modron:${var.env}" - "run.googleapis.com/cloudsql-instances" = google_sql_database_instance.instance.connection_name - "run.googleapis.com/cpu-throttling" = "false" - "run.googleapis.com/vpc-access-connector" = google_vpc_access_connector.connector.id - "run.googleapis.com/vpc-access-egress" = "private-ranges-only" - } - labels = { - "run.googleapis.com/startupProbeType" = "Default" + + volumes { + name = "cloudsql" + cloud_sql_instance { + instances = [ + google_sql_database_instance.instance.connection_name + ] } } - } - metadata { - annotations = { - "client.knative.dev/user-image" = "gcr.io/${var.project}/modron:${var.env}" - "run.googleapis.com/ingress" = "internal-and-cloud-load-balancing" + volumes { + name = "otel-config" + gcs { + bucket = google_storage_bucket.otel_config.name + } } } + + ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" + traffic { - percent = 100 - latest_revision = true + percent = 100 + type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" } lifecycle { ignore_changes = [ - metadata[0].annotations["run.googleapis.com/operation-id"], - metadata[0].annotations["run.googleapis.com/client-name"], - metadata[0].annotations["run.googleapis.com/client-version"], - template[0].metadata[0].annotations["run.googleapis.com/operation-id"], - template[0].metadata[0].annotations["run.googleapis.com/client-name"], - template[0].metadata[0].annotations["run.googleapis.com/client-version"], + annotations["run.googleapis.com/operation-id"], + annotations["run.googleapis.com/client-name"], + annotations["run.googleapis.com/client-version"], ] } depends_on = [ @@ -134,65 +196,45 @@ resource "google_cloud_run_service" "grpc_web" { ] } -resource "google_cloud_run_service" "ui" { +resource "google_cloud_run_v2_service" "ui" { name = "modron-ui" - location = substr(var.zone, 0, length(var.zone) - 2) - - # We need this to avoid naming collision with CI/CD deployments. - autogenerate_revision_name = true + location = local.region template { - spec { - service_account_name = google_service_account.modron_runner.email - timeout_seconds = 300 - containers { - image = "gcr.io/${var.project}/modron-ui:${var.env}" - ports { - container_port = 8080 - name = "http1" - } - resources { - limits = { - cpu = "4000m" - memory = "4Gi" - } - } - env { - name = "DIST_PATH" - value = "./ui" - } - } + service_account = google_service_account.modron_runner.email + timeout = "300s" + scaling { + max_instance_count = 1 + min_instance_count = 1 } - metadata { - annotations = { - "autoscaling.knative.dev/maxScale" = "1" - "autoscaling.knative.dev/minScale" = "1" - "client.knative.dev/user-image" = "gcr.io/${var.project}/modron-ui:${var.env}" + containers { + image = "${local.region}-docker.pkg.dev/${var.project}/modron/modron-ui:${var.env}" + ports { + container_port = 8080 + name = "http1" } - labels = { - "run.googleapis.com/startupProbeType" = "Default" + resources { + limits = { + cpu = "4000m" + memory = "4Gi" + } + } + env { + name = "DIST_PATH" + value = "./ui" } } } - - metadata { - annotations = { - "client.knative.dev/user-image" = "gcr.io/${var.project}/modron-ui:${var.env}" - "run.googleapis.com/ingress" = "internal-and-cloud-load-balancing" - } - } + ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" traffic { - percent = 100 - latest_revision = true + percent = 100 + type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" } lifecycle { ignore_changes = [ - metadata[0].annotations["run.googleapis.com/operation-id"], - metadata[0].annotations["run.googleapis.com/client-name"], - metadata[0].annotations["run.googleapis.com/client-version"], - template[0].metadata[0].annotations["run.googleapis.com/operation-id"], - template[0].metadata[0].annotations["run.googleapis.com/client-name"], - template[0].metadata[0].annotations["run.googleapis.com/client-version"], + annotations["run.googleapis.com/operation-id"], + annotations["run.googleapis.com/client-name"], + annotations["run.googleapis.com/client-version"], ] } depends_on = [ @@ -200,11 +242,6 @@ resource "google_cloud_run_service" "ui" { ] } -resource "google_project_iam_member" "runner_log_writer" { - project = var.project - role = "roles/logging.logWriter" - member = "serviceAccount:${google_service_account.modron_runner.email}" -} data "google_iam_policy" "cloud_run_invokers" { binding { @@ -214,13 +251,13 @@ data "google_iam_policy" "cloud_run_invokers" { } resource "google_cloud_run_service_iam_policy" "cloud_run_ui_invokers" { - service = google_cloud_run_service.ui.name - location = google_cloud_run_service.ui.location + service = google_cloud_run_v2_service.ui.name + location = google_cloud_run_v2_service.ui.location policy_data = data.google_iam_policy.cloud_run_invokers.policy_data } resource "google_cloud_run_service_iam_policy" "cloud_run_backend_invokers" { - service = google_cloud_run_service.grpc_web.name - location = google_cloud_run_service.grpc_web.location + service = google_cloud_run_v2_service.grpc_web.name + location = google_cloud_run_v2_service.grpc_web.location policy_data = data.google_iam_policy.cloud_run_invokers.policy_data } diff --git a/terraform/modron/cloud_sql.tf b/terraform/modron/cloud_sql.tf new file mode 100644 index 0000000..ef0a8b2 --- /dev/null +++ b/terraform/modron/cloud_sql.tf @@ -0,0 +1,128 @@ +resource "google_compute_global_address" "private_ip_address" { + name = "modron-${var.env}-db-address" + purpose = "VPC_PEERING" + address_type = "INTERNAL" + prefix_length = 16 + network = google_compute_network.cloud_run_network.id +} + +resource "google_service_networking_connection" "private_vpc_connection" { + network = google_compute_network.cloud_run_network.id + service = "servicenetworking.googleapis.com" + reserved_peering_ranges = [google_compute_global_address.private_ip_address.name] +} + +resource "random_id" "db_name_suffix" { + byte_length = 4 +} + +resource "google_sql_database_instance" "instance" { + name = "modron-${var.env}-${random_id.db_name_suffix.hex}" + database_version = "POSTGRES_14" + + depends_on = [google_service_networking_connection.private_vpc_connection] + + settings { + tier = "db-custom-8-30720" + availability_type = "ZONAL" + ip_configuration { + # Set this to true if you need to connect to the database via the Cloud SQL Proxy. + ipv4_enabled = false + private_network = google_compute_network.cloud_run_network.id + } + maintenance_window { + day = 7 + hour = 1 + } + database_flags { + name = "cloudsql.iam_authentication" + value = "on" + } + database_flags { + name = "max_connections" + // 100 is the maximum we can do from cloud run. + value = "100" + } + database_flags { + name = "log_temp_files" + value = "0" + } + backup_configuration { + enabled = true + location = "us" + } + insights_config { + query_insights_enabled = true + query_plans_per_minute = 5 + query_string_length = var.env == "dev" ? 4000 : 1024 + record_application_tags = false + record_client_address = false + } + } + + deletion_protection = "true" +} + +resource "google_sql_database" "modron_database" { + name = "modron${var.env}" + instance = google_sql_database_instance.instance.name +} + +resource "google_sql_user" "iam_user" { + name = "modron${var.env}runner" + instance = google_sql_database_instance.instance.name + # TODO: Move to cloud IAM (soon) + # https://github.com/GoogleCloudPlatform/cloud-sql-proxy#-enable_iam_login + type = "BUILT_IN" + password = random_password.sql_user_password.result +} + +resource "random_password" "sql_user_password" { + length = 16 + special = true + min_lower = 2 + min_numeric = 2 + min_special = 2 + min_upper = 2 + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "google_compute_instance" "jump_host_sql" { + name = "jump-host-sql" + machine_type = "e2-standard-2" + zone = var.zone + + boot_disk { + initialize_params { + image = "ubuntu-os-cloud/ubuntu-2204-lts" + } + } + + network_interface { + network = google_compute_network.cloud_run_network.id + } + + metadata_startup_script = "sudo apt -y install postgresql-client-14 && gcloud -q components install cloud_sql_proxy" + + service_account { + # Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles. + email = google_service_account.jump_host_runner.email + scopes = ["cloud-platform"] + } + + shielded_instance_config { + enable_secure_boot = true + } +} + +data "google_iam_policy" "jump_host_accessors" { + binding { + role = "roles/compute.instanceAdmin.v1" + members = var.modron_admins + } +} + +resource "google_compute_instance_iam_policy" "jump_host_policy" { + instance_name = google_compute_instance.jump_host_sql.id + policy_data = data.google_iam_policy.jump_host_accessors.policy_data +} diff --git a/terraform/modron/gitlab.tf b/terraform/modron/gitlab.tf new file mode 100644 index 0000000..5e5c616 --- /dev/null +++ b/terraform/modron/gitlab.tf @@ -0,0 +1,42 @@ +resource "google_service_account" "deployer_SA" { + account_id = "gitlab-deployer" + description = "Used by Gitlab to deploy on Cloud Run." + display_name = "gitlab-deployer" +} + +data "google_iam_policy" "gitlab_deployer" { + binding { + role = "roles/iam.serviceAccountTokenCreator" + members = [ + "serviceAccount:${var.gitlab_impersonator_service_account}", + ] + } + count = var.gitlab_impersonator_service_account != "" ? 1 : 0 +} + +resource "google_service_account_iam_policy" "gitlab_deployer_iam_policy" { + policy_data = data.google_iam_policy.gitlab_deployer[0].policy_data + service_account_id = google_service_account.deployer_SA.name + count = var.gitlab_impersonator_service_account != "" ? 1 : 0 +} + +resource "google_project_iam_member" "gitlab_cloud_build" { + project = var.project + role = "roles/cloudbuild.builds.editor" + member = "serviceAccount:${google_service_account.deployer_SA.email}" +} + +resource "google_project_iam_member" "gitlab_run_developer" { + project = var.project + role = "roles/run.developer" + member = "serviceAccount:${google_service_account.deployer_SA.email}" +} + +# This is required to build, according to Google it is compatible with the concept of least privilege -_- +# https://cloud.google.com/build/docs/securing-builds/store-manage-build-logs#viewing_build_logs +# TODO: Update to a custom log bucket and remove this permission. +resource "google_project_iam_member" "gitlab_cloud_build_storage" { + project = var.project + role = "roles/viewer" + member = "serviceAccount:${google_service_account.deployer_SA.email}" +} diff --git a/terraform/modron/load_balancer.tf b/terraform/modron/load_balancer.tf index e39b82c..67b875e 100644 --- a/terraform/modron/load_balancer.tf +++ b/terraform/modron/load_balancer.tf @@ -23,6 +23,7 @@ resource "google_compute_backend_service" "modron_grpc_web" { group = google_compute_region_network_endpoint_group.grpc_web_neg.self_link } iap { + enabled = true oauth2_client_id = google_iap_client.project_client.client_id oauth2_client_secret = google_iap_client.project_client.secret } @@ -44,6 +45,7 @@ resource "google_compute_backend_service" "modron_ui" { group = google_compute_region_network_endpoint_group.ui_neg.self_link } iap { + enabled = true oauth2_client_id = google_iap_client.project_client.client_id oauth2_client_secret = google_iap_client.project_client.secret } diff --git a/terraform/modron/main.tf b/terraform/modron/main.tf index 5761ba7..f20319e 100644 --- a/terraform/modron/main.tf +++ b/terraform/modron/main.tf @@ -1,10 +1,14 @@ provider "google" { project = var.project - region = substr(var.zone, 0, length(var.zone) - 2) + region = local.region access_token = data.google_service_account_access_token.sa.access_token zone = var.zone } +locals { + region = substr(var.zone, 0, length(var.zone) - 2) +} + resource "google_compute_ssl_policy" "modern_TLS_policy" { min_tls_version = "TLS_1_2" name = "modern-ssl-policy" diff --git a/terraform/modron/network.tf b/terraform/modron/network.tf index 1689e68..e7c0974 100644 --- a/terraform/modron/network.tf +++ b/terraform/modron/network.tf @@ -11,18 +11,18 @@ resource "google_compute_network" "cloud_run_network" { resource "google_compute_region_network_endpoint_group" "grpc_web_neg" { name = "modron-grpc-web-${var.env}-endpoint" network_endpoint_type = "SERVERLESS" - region = substr(var.zone, 0, length(var.zone) - 2) + region = local.region cloud_run { - service = google_cloud_run_service.grpc_web.name + service = google_cloud_run_v2_service.grpc_web.name } } resource "google_compute_region_network_endpoint_group" "ui_neg" { name = "modron-ui-${var.env}-endpoint" network_endpoint_type = "SERVERLESS" - region = substr(var.zone, 0, length(var.zone) - 2) + region = local.region cloud_run { - service = google_cloud_run_service.ui.name + service = google_cloud_run_v2_service.ui.name } } @@ -36,7 +36,7 @@ resource "google_vpc_access_connector" "connector" { # This is required to install packages on the SQL jump host resource "google_compute_router" "router" { name = "sql-jump-host" - region = substr(var.zone, 0, length(var.zone) - 2) + region = local.region network = google_compute_network.cloud_run_network.id bgp { diff --git a/terraform/modron/otel/README.md b/terraform/modron/otel/README.md new file mode 100644 index 0000000..90002f0 --- /dev/null +++ b/terraform/modron/otel/README.md @@ -0,0 +1,3 @@ +# otel-collector + +Configuration adapted from [GoogleCloudRun/opentelemetry-cloud-run](https://github.com/GoogleCloudPlatform/opentelemetry-cloud-run) diff --git a/terraform/modron/otel/config.yaml b/terraform/modron/otel/config.yaml new file mode 100644 index 0000000..53f7fd7 --- /dev/null +++ b/terraform/modron/otel/config.yaml @@ -0,0 +1,55 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + # batch metrics before sending to reduce API usage + send_batch_max_size: 200 + send_batch_size: 200 + timeout: 5s + + memory_limiter: + # drop metrics if memory usage gets too high + check_interval: 1s + limit_percentage: 65 + spike_limit_percentage: 20 + + resourcedetection: + detectors: [env, gcp] + timeout: 2s + override: false + +exporters: + googlecloud: + log: + default_log_name: "otel-collector" + otlphttp: + endpoint: "http://10.43.0.2:80" + tls: + insecure: true + googlemanagedprometheus: + +extensions: + health_check: + endpoint: "0.0.0.0:13133" + +service: + extensions: [health_check] + pipelines: + traces: + receivers: [otlp] + processors: [resourcedetection] + exporters: [googlecloud,otlphttp] + logs: + receivers: [otlp] + processors: [resourcedetection] + exporters: [googlecloud] + metrics: + receivers: [otlp] + processors: [resourcedetection] + exporters: [googlemanagedprometheus] diff --git a/terraform/modron/project.tf b/terraform/modron/project.tf new file mode 100644 index 0000000..91a0ebb --- /dev/null +++ b/terraform/modron/project.tf @@ -0,0 +1,21 @@ +resource "google_project_iam_binding" "cloud_sql_admins" { + project = var.project + role = "roles/cloudsql.admin" + members = var.project_admins +} + +resource "google_project_iam_binding" "cloud_trace_agent" { + project = var.project + role = "roles/cloudtrace.agent" + members = [ + "serviceAccount:${google_service_account.modron_runner.email}", + ] +} + +resource "google_project_iam_binding" "monitoring_writer" { + project = var.project + role = "roles/monitoring.metricWriter" + members = [ + "serviceAccount:${google_service_account.modron_runner.email}", + ] +} \ No newline at end of file diff --git a/terraform/modron/secret.tf b/terraform/modron/secret.tf index 3394dfb..b354a3a 100644 --- a/terraform/modron/secret.tf +++ b/terraform/modron/secret.tf @@ -4,7 +4,7 @@ resource "google_secret_manager_secret" "sql_connect_string_config" { replication { user_managed { replicas { - location = substr(var.zone, 0, length(var.zone) - 2) + location = local.region } } } diff --git a/terraform/modron/service_account.tf b/terraform/modron/service_account.tf index c204476..c995b45 100644 --- a/terraform/modron/service_account.tf +++ b/terraform/modron/service_account.tf @@ -4,11 +4,49 @@ resource "google_service_account" "modron_runner" { display_name = "modron-${var.env}-runner" } +locals { + service_account_sa_users = [for v in compact([ + google_service_account.deployer_SA.email, + var.gitlab_impersonator_service_account, + ]) : "serviceAccount:${v}"] +} + +resource "google_service_account_iam_binding" "modron_runner_user" { + service_account_id = google_service_account.modron_runner.name + role = "roles/iam.serviceAccountUser" + members = concat(local.service_account_sa_users, var.project_admins) +} + +resource "google_project_iam_member" "runner_log_writer" { + project = var.project + role = "roles/logging.logWriter" + member = "serviceAccount:${google_service_account.modron_runner.email}" +} + +resource "google_project_iam_member" "project_monitoring" { + project = var.project + role = "roles/monitoring.metricWriter" + member = "serviceAccount:${google_service_account.modron_runner.email}" +} + +resource "google_project_iam_member" "sql_client_iam" { + project = var.project + role = "roles/cloudsql.client" + member = "serviceAccount:${google_service_account.modron_runner.email}" +} +############ + resource "google_service_account" "jump_host_runner" { account_id = "modron-${var.env}-sql-jumphost" display_name = "modron-${var.env}-sql-jumphost" } +resource "google_service_account_iam_binding" "jump_host_runner_user" { + service_account_id = google_service_account.jump_host_runner.name + role = "roles/iam.serviceAccountUser" + members = var.project_admins +} + resource "google_project_iam_member" "jump_host_log_writer" { project = var.project role = "roles/logging.logWriter" diff --git a/terraform/modron/tracing.tf b/terraform/modron/tracing.tf new file mode 100644 index 0000000..786ce2d --- /dev/null +++ b/terraform/modron/tracing.tf @@ -0,0 +1,35 @@ +resource "google_storage_bucket" "otel_config" { + name = "${var.project}-otel-config" + location = local.region + uniform_bucket_level_access = true +} + +data "google_iam_policy" "otel_config" { + binding { + role = "roles/storage.objectViewer" + members = [ + "serviceAccount:${google_service_account.modron_runner.email}", + ] + } + + binding { + role = "roles/storage.admin" + members = concat( + var.project_admins, + [ + "serviceAccount:${data.google_service_account.terraform_sa.email}", + ] + ) + } +} + +resource "google_storage_bucket_iam_policy" "otel_config" { + bucket = google_storage_bucket.otel_config.name + policy_data = data.google_iam_policy.otel_config.policy_data +} + +resource "google_storage_bucket_object" "otel_config" { + bucket = google_storage_bucket.otel_config.name + name = "config.yaml" + content = file("${path.module}/otel/config.yaml") +} \ No newline at end of file diff --git a/terraform/modron/variables.tf b/terraform/modron/variables.tf index d3d371c..00dc900 100644 --- a/terraform/modron/variables.tf +++ b/terraform/modron/variables.tf @@ -34,12 +34,6 @@ variable "env" { description = "Environment type of the database." } -variable "dataset_id" { - description = "(optional) Name of the dataset to be created. Will default to modron" - type = string - default = "modron" -} - variable "project_admins" { description = "People that can impersonate the terraform account and manage the project." type = list(string) @@ -58,3 +52,81 @@ variable "modron_users" { description = "List of group or users that will have access to the modron UI. The content will still be showed depending on the users' access inside the organisation." type = list(string) } + +variable "notification_system" { + description = "Notification system to use for modron." + type = string + validation { + condition = length(var.notification_system) > 0 + error_message = "The notification_system URL is required" + } +} + +variable "notification_system_client_id" { + description = "Notification system client id." + type = string + validation { + condition = length(var.notification_system_client_id) > 0 + error_message = "The notification_system_client_id is required" + } +} + +variable "gitlab_impersonator_service_account" { + description = "The service account email that will impersonate the GitLab service account" + type = string + default = "" + validation { + error_message = "This must be a valid GCP service account email." + condition = can(regex(".*@.*\\.iam\\.gserviceaccount\\.com", var.gitlab_impersonator_service_account)) || length(var.gitlab_impersonator_service_account) == 0 + } +} + +variable "docker_registry" { + description = "Docker registry to use for the public images" + validation { + error_message = "The docker registry must be a valid URL" + condition = length(var.docker_registry) > 0 + } +} + +variable "impact_map" { + type = map(string) + description = "A map of environments to impact (e.g: prod -> IMPACT_HIGH) that will be used for calculating the risk score" + default = { + "prod" = "IMPACT_HIGH" + "staging" = "IMPACT_MEDIUM" + "dev" = "IMPACT_LOW" + } +} + +variable "additional_admin_roles" { + type = list(string) + default = [] + description = "A list of additional roles that are considered admin in GCP, for example ['organizations/11111/roles/MyOrgOwner']" +} + +variable "label_to_email_regexp" { + description = "Regexp to be used to convert the label contents of contact1,contact2 to an email" + type = string + default = "(.*)_(.*?)_(.*?)$" +} + +variable "label_to_email_substitution" { + description = "Substitution to be used to convert the label contents of contact1,contact2 to an email" + type = string + default = "$1@$2.$3" +} + +variable "allowed_scc_categories" { + description = "List of allowed Security Command Center categories to create observations.\nThese categories are the Finding.category (https://cloud.google.com/security-command-center/docs/reference/rest/v1/organizations.sources.findings#Finding), often refered to as \"API equivalent\" in the GCP console.\nFor example, if you want to allow the category \"GKE_RUNTIME_OS_VULNERABILITY\" you should add it to this list.\n\nA list with some possible findings can be found on https://cloud.google.com/chronicle/docs/ingestion/default-parsers/collect-security-command-center-findings." + type = list(string) + default = [] +} + +variable "rule_configs" { + description = "A JSON map of the rules to their configuration" + validation { + error_message = "The rule_configs must be a valid JSON map" + condition = can(jsondecode(var.rule_configs)) + } +} diff --git a/terraform/prod/main.tf.example b/terraform/prod/main.tf.example index 771679b..8dc1047 100644 --- a/terraform/prod/main.tf.example +++ b/terraform/prod/main.tf.example @@ -3,10 +3,10 @@ module "modron" { source = "../modron" - domain = "hosted.at.example.com" + domain = "modron-prod.example.com" env = "prod" org_id = "GCP_ORGID" - project = "GCP_PROJECT_NAME" + project = "my-modron-prod" zone = "GCP_ZONE" modron_admins = [ @@ -18,4 +18,8 @@ module "modron" { project_admins = [ "group:modron-project-admins@example.com" ] + docker_registry = "mirror.gcr.io" + notification_system = "https://notification-system.example.com" + notification_system_client_id = "client-id" + org_suffix = "@example.com" } diff --git a/utils/gcp_service_agents/.gitignore b/utils/gcp_service_agents/.gitignore new file mode 100644 index 0000000..94a2dd1 --- /dev/null +++ b/utils/gcp_service_agents/.gitignore @@ -0,0 +1 @@ +*.json \ No newline at end of file diff --git a/utils/gcp_service_agents/README.md b/utils/gcp_service_agents/README.md new file mode 100644 index 0000000..3278828 --- /dev/null +++ b/utils/gcp_service_agents/README.md @@ -0,0 +1,15 @@ +# gcp_service_agents + +GCP publishes a list of "Service Agents" on their [documentation pages](https://cloud.google.com/iam/docs/service-agents). +Unfortunately this list is not in a machine readable format. This little helper scrapes that page and provides a list +of project IDs (e.g: `service-PROJECT_NUMBER@gcp-sa-aiplatform-cc.iam.gserviceaccount.com` -> `gcp-sa-aiplatform-cc`) +that Google provides, and thus that are considered "secure" to be used in IAM policies. + +## Usage + +```bash +go run ./ -o out.json +jq -r '.projects[] | "\"" + . + "\"" + ": {},"' out.json | clipcopy +``` + +Then paste the content of your clipboard into the `constants/gcp_sa_projects.go` file. \ No newline at end of file diff --git a/utils/gcp_service_agents/go.mod b/utils/gcp_service_agents/go.mod new file mode 100644 index 0000000..458965b --- /dev/null +++ b/utils/gcp_service_agents/go.mod @@ -0,0 +1,17 @@ +module github.com/nianticlabs/modron/utils/gcp_service_agents + +go 1.23.2 + +require ( + github.com/alexflint/go-arg v1.5.1 + github.com/sirupsen/logrus v1.9.3 + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 +) + +require ( + github.com/PuerkitoBio/goquery v1.9.0 // indirect + github.com/alexflint/go-scalar v1.2.0 // indirect + github.com/andybalholm/cascadia v1.3.2 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect +) diff --git a/utils/gcp_service_agents/go.sum b/utils/gcp_service_agents/go.sum new file mode 100644 index 0000000..bb3eef3 --- /dev/null +++ b/utils/gcp_service_agents/go.sum @@ -0,0 +1,72 @@ +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/PuerkitoBio/goquery v1.9.0 h1:zgjKkdpRY9T97Q5DCtcXwfqkcylSFIVCocZmn2huTp8= +github.com/PuerkitoBio/goquery v1.9.0/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= +github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= +github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= +github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= +github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= +github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= +github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/utils/gcp_service_agents/main.go b/utils/gcp_service_agents/main.go new file mode 100644 index 0000000..ec573f2 --- /dev/null +++ b/utils/gcp_service_agents/main.go @@ -0,0 +1,166 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "regexp" + "sort" + "strings" + + "github.com/PuerkitoBio/goquery" + "github.com/alexflint/go-arg" + "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" +) + +const ( + serviceAgentsUrl = "https://cloud.google.com/iam/docs/service-agents" + firebaseServiceAccountsUrl = "https://firebase.google.com/support/guides/service-accounts" +) + +var ( + log = logrus.StandardLogger() + undocumentedProjects = map[string]struct{}{ + "appsheet-prod-service-accounts": {}, + "cloud-ml.google.com": {}, + "cloud-cdn-fill": {}, + "gae-api-prod.google.com": {}, + } +) + +var args struct { + OutputFile string `arg:"-o,required" help:"Output file to write the JSON to"` +} + +func main() { + arg.MustParse(&args) + + f, err := os.Create(args.OutputFile) + if err != nil { + log.Fatalf("failed to create output file: %v", err) + } + defer f.Close() + + if err := generateServiceAgents(f); err != nil { + log.Fatalf("failed to generate service agents: %v", err) + } +} + +type ServiceAgentProjects struct { + Projects []string `json:"projects"` +} + +func generateServiceAgents(f io.Writer) error { + projects := make(map[string]struct{}) + if err := scrapeServiceAgentsPage(projects); err != nil { + return err + } + + if err := scrapeFirebaseServiceAccountsPage(projects); err != nil { + return err + } + + for _, undocumentedProjects := range maps.Keys(undocumentedProjects) { + projects[undocumentedProjects] = struct{}{} + } + + projectIDs := maps.Keys(projects) + sort.Strings(projectIDs) + toWrite := ServiceAgentProjects{Projects: projectIDs} + if err := json.NewEncoder(f).Encode(toWrite); err != nil { + return fmt.Errorf("failed to encode JSON: %v", err) + } + return nil +} + +func scrapeFirebaseServiceAccountsPage(projects map[string]struct{}) error { + doc, err := getGoQueryDocument(firebaseServiceAccountsUrl) + if err != nil { + return err + } + doc.Find("table tbody tr").Each(func(i int, s *goquery.Selection) { + svcAccount := strings.TrimSpace(s.Find("td:nth-child(1)").Text()) + svcAccountProject := getServiceAccountProject(svcAccount) + if len(svcAccountProject) == 0 { + log.Warnf("Service account project not found for %s", strings.ReplaceAll(svcAccount, "\n", " ")) + return + } + for _, saProjectID := range svcAccountProject { + projects[saProjectID] = struct{}{} + } + }) + return nil +} + +func getGoQueryDocument(pageURL string) (*goquery.Document, error) { + page, err := scrapePage(pageURL) + if err != nil { + return nil, fmt.Errorf("failed to scrape page: %v", err) + } + doc, err := goquery.NewDocumentFromReader(bytes.NewBufferString(page)) + if err != nil { + return nil, fmt.Errorf("failed to parse HTML: %v", err) + } + return doc, nil +} + +func scrapeServiceAgentsPage(projects map[string]struct{}) error { + doc, err := getGoQueryDocument(serviceAgentsUrl) + if err != nil { + return err + } + doc.Find("#service-agents tbody tr").Each(func(i int, s *goquery.Selection) { + serviceAgent := strings.TrimSpace(s.Find("td:nth-child(1)").Text()) + svcAccountProject := getServiceAccountProject(serviceAgent) + if len(svcAccountProject) == 0 { + log.Warnf("Service agent project not found for %s", strings.ReplaceAll(serviceAgent, "\n", " ")) + return + } + log.Infof("Service agent: %s", svcAccountProject) + for _, saProjectID := range svcAccountProject { + projects[saProjectID] = struct{}{} + } + }) + return nil +} + +var saAccountProjectRegex = regexp.MustCompile("\\S+@([A-z0-9-]+)\\.iam\\.gserviceaccount\\.com") + +func getServiceAccountProject(agent string) []string { + projectsMap := make(map[string]struct{}) + matches := saAccountProjectRegex.FindAllStringSubmatch(agent, -1) + for _, match := range matches { + if len(match) != 2 { + log.Warnf("failed to extract project from service agent: %s", agent) + } else { + if match[1] == "project-id" || match[1] == "project-name" { + continue + } + projectsMap[match[1]] = struct{}{} + } + } + return maps.Keys(projectsMap) +} + +func scrapePage(pageUrl string) (string, error) { + req, err := http.NewRequest("GET", pageUrl, nil) + if err != nil { + return "", err + } + res, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + if res.StatusCode != http.StatusOK { + return "", fmt.Errorf("got status code %d", res.StatusCode) + } + buffer := bytes.NewBuffer(nil) + if _, err := io.Copy(buffer, res.Body); err != nil { + return "", fmt.Errorf("failed to read response body: %v", err) + } + return buffer.String(), nil +}