diff --git a/.environment/chatops/help.txt b/.environment/chatops/help.txt index 21a0356e852..5d95387fdf6 100644 --- a/.environment/chatops/help.txt +++ b/.environment/chatops/help.txt @@ -3,7 +3,7 @@ ACTION USAGE [<@bot>] gh-deploy [] to [] [OPTIONAL: for ] EXAMPLES - @DevBot gh-deploy master to trialfrontend1 + @DevBot gh-deploy main to trialfrontend1 ========================================================================== ACTION Lock branch to prevent deployments @@ -26,4 +26,4 @@ USAGE [<@bot>] gh-run [] [OPTIONAL: ] [OPTIONAL: --inputs ] EXAMPLES @DevBot gh-run destroy_demo_environment.yml --inputs env_name:demo1 - @DevBot gh-run destroy_demo_environment.yml CDCgov/prime-reportstream master --inputs env_name:demo1 + @DevBot gh-run destroy_demo_environment.yml CDCgov/prime-reportstream main --inputs env_name:demo1 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1e716fcb574..700d5ad089c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,6 +6,8 @@ /operations/ @cdcgov/PRIME-ReportStream-DevOps /CODEOWNERS @cdcgov/PRIME-ReportStream-DevOps /prime-router/ @cdcgov/PRIME-ReportStream-CODEOWNERS-backend +/prime-router/src/main/resources/metadata/fhir_transforms/senders/Flexion @cdcgov/trusted-intermediary +/prime-router/settings/STLTs/Flexion @cdcgov/trusted-intermediary # The CODEOWNERS file takes the last matching line into account. You can make definitions with empty owners to specify paths/files without an owner. /prime-router/settings/prod/ diff --git a/.github/ISSUE_TEMPLATE/platform-epic-template.md b/.github/ISSUE_TEMPLATE/platform-epic-template.md index f6bd412152e..12c7548c4bb 100644 --- a/.github/ISSUE_TEMPLATE/platform-epic-template.md +++ b/.github/ISSUE_TEMPLATE/platform-epic-template.md @@ -8,22 +8,28 @@ assignees: '' --- ## Outcome/Objective + ## Description + ## Product Requirement(s) + ## Use Case(s) + ## Dependencies + ## Acceptance criteria + ## Technical Requirement(s) \ No newline at end of file +--> diff --git a/.github/ISSUE_TEMPLATE/up-receiver-migration-set-up-receiver-settings.md b/.github/ISSUE_TEMPLATE/up-receiver-migration-set-up-receiver-settings.md index 0e99ffcfe1a..2f3bba47aa9 100644 --- a/.github/ISSUE_TEMPLATE/up-receiver-migration-set-up-receiver-settings.md +++ b/.github/ISSUE_TEMPLATE/up-receiver-migration-set-up-receiver-settings.md @@ -19,9 +19,9 @@ As a developer, I want to compare the messages generated from the Covid and Univ ### Dev Notes: - [ ] Fetch [STLT] organization settings from production and load them locally -- [ ] Use the attached SimpleReport covid postman collection and make sure the message gets routed to [STLT] locally. Modify the message to meet [STLT] filter if needed [Simple Report Covid.postman_collection](https://github.com/CDCgov/prime-reportstream/blob/master/prime-router/docs/onboarding-users/samples/SimpleReport/Simple%20Report%20Covid.postman_collection.json) -- [ ] Make a copy of the [STLT] organization settings to onboard them to the UP. See How to Migrate an existing receiver to the UP documentation for more details: https://github.com/CDCgov/prime-reportstream/blob/master/prime-router/docs/onboarding-users/migrating-receivers.md -- [ ] Use this Postman collection to send a FHIR bundle the UP and make sure the message gets routed to the new UP [STLT] receiver. You may need to update the Simple Report sender to use the simple-report-sender-transform.yml if it's not using it. [Simple Report UP.postman_collection](https://github.com/CDCgov/prime-reportstream/blob/master/prime-router/docs/onboarding-users/samples/SimpleReport/Simple%20Report%20UP.postman_collection.json) +- [ ] Use the attached SimpleReport covid postman collection and make sure the message gets routed to [STLT] locally. Modify the message to meet [STLT] filter if needed [Simple Report Covid.postman_collection](https://github.com/CDCgov/prime-reportstream/blob/main/prime-router/docs/onboarding-users/samples/SimpleReport/Simple%20Report%20Covid.postman_collection.json) +- [ ] Make a copy of the [STLT] organization settings to onboard them to the UP. See How to Migrate an existing receiver to the UP documentation for more details: https://github.com/CDCgov/prime-reportstream/blob/main/prime-router/docs/onboarding-users/migrating-receivers.md +- [ ] Use this Postman collection to send a FHIR bundle the UP and make sure the message gets routed to the new UP [STLT] receiver. You may need to update the Simple Report sender to use the simple-report-sender-transform.yml if it's not using it. [Simple Report UP.postman_collection](https://github.com/CDCgov/prime-reportstream/blob/main/prime-router/docs/onboarding-users/samples/SimpleReport/Simple%20Report%20UP.postman_collection.json) - To migrate the Covid translation settings start by looking at their current translation settings. If the receiver uses any of the following settings you will need to create a receiver schema: - receivingApplicationName - receivingApplicationOID @@ -45,7 +45,7 @@ As a developer, I want to compare the messages generated from the Covid and Univ - useOrderingFacilityName not STANDARD - receivingOrganization - stripInvalidCharsRegex - + - More documentation on how to set-up these transforms in the UP will be provided, but for now you can look for examples on how to set this up in either the NY-receiver-transforms or CA-receiver-transforms - If the receiver uses any of those transforms you will need to create a receiver transform under `metadata/hl7_mapping/receivers/STLTs/` and update the receiver settings to point to this schema. @@ -60,11 +60,10 @@ As a developer, I want to compare the messages generated from the Covid and Univ - If there are no major differences we can move on to sending test messages to the STLTs staging environment. -### Acceptance Criteria +### Acceptance Criteria - [ ] Created and sent data to [STLT] through the covid pipeline locally - [ ] Created and sent data to [STLTS] through the universal pipeline locally - [ ] Migrated Covid receiver translation settings to the UP receiver settings - [ ] Successfully generated a message with migrated UP receiver settings - [ ] Review transforms settings with the team - [ ] Compared messages from the covid and universal pipelines and documented differences and review with team - diff --git a/.github/actions/azviz/README.md b/.github/actions/azviz/README.md new file mode 100644 index 00000000000..662bc2ade45 --- /dev/null +++ b/.github/actions/azviz/README.md @@ -0,0 +1,90 @@ +# AzViz (Azure Visualizer) action +Note: This GitHub Action is imported from: https://github.com/josiahsiegel/azviz-action + +## ☕ Please donate to [AzViz Developer](https://github.com/PrateekKumarSingh/AzViz#readme) + +![](https://github.com/PrateekKumarSingh/AzViz/blob/master/img/themeneon.jpg) + +## Synopsis + +[AzViz](https://github.com/PrateekKumarSingh/AzViz) for [GitHub actions](https://github.com/marketplace?type=actions)! + +## Inputs + +### Required + +```yml +inputs: + resource-group: + description: Comma-seperated resource group list + required: true + out-file: + description: Graph export path + required: true + default: output/viz.svg + sub-name: + description: Azure subscription name + required: true + default: Pay-As-You-Go +``` + +### Optional + +```yml + theme: + description: Graph theme (dark, light, neon) + required: false + default: neon + depth: + description: Level of Azure Resource Sub-category to be included in vizualization (1 or 2) + required: false + default: '1' + verbosity: + description: Level of information to included in vizualization (1 or 2) + required: false + default: '1' + format: + description: Graph format (png or svg) + required: false + default: svg + direction: + description: Direction in which resource groups are plotted on the visualization (left-to-right or top-to-bottom) + required: false + default: top-to-bottom + exclude-types: + description: Exclude resources via string search + required: false + default: '*excludethisthing1,excludethisthing2*' + splines: + description: Controls how edges appear in visualization. ('spline', 'polyline', 'curved', 'ortho', 'line') + required: false + default: spline +``` + +## Quick start + +`sample_min_workflow.yml` +```yml +jobs: + generate-viz: + runs-on: ubuntu-latest + steps: + - name: Login to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + enable-AzPSSession: true + - uses: CDCgov/prime-reportstream/.github/actions/azviz@663e24299a6336f1ff8dbddadfac1ba5d462f731aaa + with: + resource-group: ${{ github.event.inputs.resource-group }} + out-file: ${{ github.event.inputs.out-file }} + sub-name: ${{ github.event.inputs.sub-name }} + - uses: actions/upload-artifact@v2 + with: + name: viz + path: output/* +``` + +## Dependencies + + * [azure/login](https://github.com/marketplace/actions/azure-login) with `enable-AzPSSession: true` diff --git a/.github/actions/azviz/action.yml b/.github/actions/azviz/action.yml new file mode 100644 index 00000000000..0178f26fcaf --- /dev/null +++ b/.github/actions/azviz/action.yml @@ -0,0 +1,83 @@ +# action.yml +name: 'Generate Azure resource topology diagrams with AzViz (Azure Visualizer)' +description: 'Run AzViz against one or more Azure Resource Groups' +branding: + icon: 'download-cloud' + color: 'blue' +inputs: + resource-group: + description: Comma-seperated resource group list + required: true + out-file: + description: Graph export path + required: true + default: viz.svg + sub-name: + description: Azure subscription name + required: true + default: Pay-As-You-Go + theme: + description: Graph theme (dark, light, neon) + required: false + default: neon + depth: + description: Level of Azure Resource Sub-category to be included in vizualization (1 or 2) + required: false + default: '1' + verbosity: + description: Level of information to included in vizualization (1 or 2) + required: false + default: '1' + format: + description: Graph format (png or svg) + required: true + default: svg + direction: + description: Direction in which resource groups are plotted on the visualization (left-to-right or top-to-bottom) + required: false + default: top-to-bottom + exclude-types: + description: Exclude resources via string search + required: false + default: '*excludethisthing1,excludethisthing2*' + splines: + description: Controls how edges appear in visualization. ('spline', 'polyline', 'curved', 'ortho', 'line') + required: false + default: spline + +runs: + using: "composite" + steps: + - name: Choco install graphviz + if: runner.os == 'Windows' + uses: crazy-max/ghaction-chocolatey@v1 + with: + args: install graphviz + - name: Apt-get install graphviz + if: runner.os != 'Windows' + run: | + sudo apt-get update; + sudo apt-get install graphviz -y; + shell: bash + - name: 'Install AzViz module' + shell: pwsh + run: | + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; + Install-Module -Name AzViz -AllowClobber -Confirm:$False -Force; + Import-Module AzViz; + - name: Run AzViz + uses: azure/powershell@v1 + with: + azPSVersion: 'latest' + inlineScript: | + ${{ github.action_path }}/viz_run.ps1 ` + -RESOURCE_GROUP '${{ inputs.resource-group }}' ` + -OUT_FILE '${{ inputs.out-file }}' ` + -SUB_NAME '${{ inputs.sub-name }}' ` + -THEME '${{ inputs.theme }}' ` + -DEPTH ${{ inputs.depth }} ` + -VERBOSITY ${{ inputs.verbosity }} ` + -FORMAT '${{ inputs.format }}' ` + -DIRECTION '${{ inputs.direction }}' ` + -EXCLUDE_TYPES '${{ inputs.exclude-types }}' ` + -SPLINES '${{ inputs.splines }}' diff --git a/.github/actions/azviz/viz_run.ps1 b/.github/actions/azviz/viz_run.ps1 new file mode 100644 index 00000000000..87071186628 --- /dev/null +++ b/.github/actions/azviz/viz_run.ps1 @@ -0,0 +1,57 @@ +Param( + [Parameter(Mandatory)] + [String]$RESOURCE_GROUP, + [Parameter(Mandatory)] + [String]$OUT_FILE = 'viz.svg', + [Parameter(Mandatory)] + [String]$SUB_NAME = 'Pay-As-You-Go', + [Parameter(Mandatory)] + [String]$THEME = 'neon', + [Parameter(Mandatory)] + [String]$DEPTH = '1', + [Parameter(Mandatory)] + [String]$VERBOSITY = '1', + [Parameter(Mandatory)] + [String]$FORMAT = 'svg', + [Parameter(Mandatory)] + [String]$DIRECTION = 'top-to-bottom', + [String]$EXCLUDE_TYPES = '*excludethisthing1,excludethisthing2*', + [Parameter(Mandatory)] + [String]$SPLINES = 'spline' +) + +# Create missing directory paths for output +New-Item -ItemType File -Force -Path ${OUT_FILE} + +# Get current Azure context +$currentAzureContext = Get-AzContext; + +# Check If Azure context exists +if ($currentAzureContext.Tenant.TenantId) { + + # Set Azure subscription to match SUB_NAME + Set-AzContext -SubscriptionName ${SUB_NAME}; +}; + +# Run AzViz and export Azure diagram to location OUT_FILE +Export-AzViz ` + -ResourceGroup ${RESOURCE_GROUP}.Split(",") ` + -Theme ${THEME} ` + -OutputFormat ${FORMAT} ` + -CategoryDepth ${DEPTH} ` + -LabelVerbosity ${VERBOSITY} ` + -ExcludeTypes ${EXCLUDE_TYPES}.Split(",") ` + -Splines ${SPLINES} ` + -Direction ${DIRECTION} ` + -OutputFilePath ${OUT_FILE}; + +if (${FORMAT} -eq 'svg') { + + # Move svg embedded png to output directory + ((Get-Content -path ${OUT_FILE} -Raw) -replace '(?<=xlink:href\=").+?(?=icons)','') | Set-Content -Path ${OUT_FILE} + $ICON_PATH=$(Split-Path -Path ${OUT_FILE})+'/icons/' + Write-Host "Moving ${HOME}/*/AzViz/* icons to ${ICON_PATH}" + New-Item -ItemType Directory -Force -Path ${ICON_PATH} + Get-Childitem -Path ${HOME} -Force -recurse -include *.png -ErrorAction SilentlyContinue | Move-Item -dest ${ICON_PATH} -Force + +}; \ No newline at end of file diff --git a/.github/actions/build-vars/action.yml b/.github/actions/build-vars/action.yml index 663d29f06dd..35bf7e7914e 100644 --- a/.github/actions/build-vars/action.yml +++ b/.github/actions/build-vars/action.yml @@ -103,8 +103,8 @@ runs: - name: Set Build Environment - STAGING id: build_staging if: | - (github.event_name != 'pull_request' && github.ref_name == 'master') || - (github.event_name == 'pull_request' && github.base_ref == 'master') + (github.event_name != 'pull_request' && github.ref_name == 'main') || + (github.event_name == 'pull_request' && github.base_ref == 'main') shell: bash run: | echo "env_name=staging" >> $GITHUB_OUTPUT @@ -170,7 +170,7 @@ runs: - 'frontend-react/**/!(*.md)' - '.github/actions/build-vars/action.yml' - '.github/actions/build-frontend/action.yml' - - '.github/workflows/frontend_ci.yml' + - '.github/workflows/frontend_ci.yml' terraform: - 'operations/app/terraform/**/!(*.md)' - '.github/workflows/validate_terraform.yml' @@ -215,7 +215,7 @@ runs: else echo "has_router_change=${{ steps.filter.outputs.router }}" >> $GITHUB_OUTPUT fi - + - name: Determine if frontend changed if: github.event_name != 'schedule' id: frontend_change_result @@ -234,7 +234,7 @@ runs: echo "has_frontend_change=${{ steps.filter.outputs.frontend_react }}" >> $GITHUB_OUTPUT fi - - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 + - uses: azure/login@v2 if: inputs.sp-creds != 'false' with: creds: ${{ inputs.sp-creds }} diff --git a/.github/actions/checksum-validate/README.md b/.github/actions/checksum-validate/README.md new file mode 100644 index 00000000000..b228bfcab77 --- /dev/null +++ b/.github/actions/checksum-validate/README.md @@ -0,0 +1,94 @@ +# Checksum Validate Action + +[![Test Action](https://github.com/JosiahSiegel/checksum-validate-action/actions/workflows/test_action.yml/badge.svg)](https://github.com/JosiahSiegel/checksum-validate-action/actions/workflows/test_action.yml) + +## Synopsis + +1. Generate a checksum from either a string or shell command (use command substitution: `$()`). +2. Validate if checksum is identical to input (even across multiple jobs), using a `key` to link the validation attempt with the correct generated checksum. + * Validation is possible across jobs since the checksum is uploaded as a workflow artifact + +## Usage + +```yml +jobs: + generate-checksums: + name: Generate checksum + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + + - name: Generate checksum of string + uses: ./.github/actions/checksum-validate@ebdf8c12c00912d18de93c483b935d51582f9236 + with: + key: test string + input: hello world + + - name: Generate checksum of command output + uses: ./.github/actions/checksum-validate@ebdf8c12c00912d18de93c483b935d51582f9236 + with: + key: test command + input: $(cat action.yml) + + validate-checksums: + name: Validate checksum + needs: + - generate-checksums + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + + - name: Validate checksum of valid string + id: valid-string + uses: ./.github/actions/checksum-validate@ebdf8c12c00912d18de93c483b935d51582f9236 + with: + key: test string + validate: true + fail-invalid: true + input: hello world + + - name: Validate checksum of valid command output + id: valid-command + uses: ./.github/actions/checksum-validate@ebdf8c12c00912d18de93c483b935d51582f9236 + with: + key: test command + validate: true + fail-invalid: true + input: $(cat action.yml) + + - name: Get outputs + run: | + echo ${{ steps.valid-string.outputs.valid }} + echo ${{ steps.valid-command.outputs.valid }} +``` + +## Workflow summary + +### ✅ test string checksum valid ✅ + +### ❌ test string checksum INVALID ❌ + +## Inputs + +```yml +inputs: + validate: + description: Check if checksums match + default: false + key: + description: String to keep unique checksums separate + required: true + fail-invalid: + description: Fail step if invalid checksum + default: false + input: + description: String or command for checksum + required: true +``` + +## Outputs +```yml +outputs: + valid: + description: True if checksums match +``` diff --git a/.github/actions/checksum-validate/action.yml b/.github/actions/checksum-validate/action.yml new file mode 100644 index 00000000000..1ad3023476e --- /dev/null +++ b/.github/actions/checksum-validate/action.yml @@ -0,0 +1,111 @@ +# action.yml +name: Checksum Validate Action +description: Generate and validate checksums +branding: + icon: 'lock' + color: 'orange' +inputs: + validate: + description: Check if checksums match + default: false + key: + description: String to keep unique checksums separate + required: true + fail-invalid: + description: Fail step if invalid checksum + default: false + input: + description: String or command for checksum + required: true +outputs: + valid: + description: True if checksums match + value: ${{ steps.validate_checksum.outputs.valid }} + +runs: + using: "composite" + steps: + + # CHECKSUM START + - name: Generate SHA + uses: nick-fields/retry@v3.0.0 + with: + max_attempts: 5 + retry_on: any + timeout_seconds: 10 + retry_wait_seconds: 15 + command: | + function fail { + printf '%s\n' "$1" >&2 + exit "${2-1}" + } + input_cmd="${{ inputs.input }}" || fail + sha="$(echo "$input_cmd" | sha256sum)" + echo "sha=$sha" >> $GITHUB_ENV + echo "success=true" >> $GITHUB_ENV + + - name: Get input SHA + if: env.success + id: input_sha + shell: bash + run: echo "sha=${{ env.sha }}" >> $GITHUB_OUTPUT + + - name: Get input SHA + if: env.success != 'true' + shell: bash + run: | + echo "failed to generate sha" + exit 1 + # CHECKSUM END + + # UPLOAD FILE START + - name: Create checksum file + if: inputs.validate != 'true' + shell: bash + run: | + echo "${{ steps.input_sha.outputs.sha }}" > "${{ github.sha }}-${{ inputs.key }}.txt" + + - name: Upload checksum file + if: inputs.validate != 'true' + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 + with: + name: "${{ github.sha }}-${{ inputs.key }}.txt" + path: "${{ github.sha }}-${{ inputs.key }}.txt" + retention-days: 5 + # UPLOAD FILE END + + # VALIDATE FILE START + - name: Download checksum file + if: inputs.validate == 'true' + uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe + with: + name: "${{ github.sha }}-${{ inputs.key }}.txt" + + - name: Validate pre and post checksums + if: inputs.validate == 'true' + id: validate_checksum + shell: bash + run: | + echo "${{ steps.input_sha.outputs.sha }}" > "${{ github.sha }}-${{ inputs.key }}-2.txt" + DIFF=$(diff -q "${{ github.sha }}-${{ inputs.key }}-2.txt" "${{ github.sha }}-${{ inputs.key }}.txt") || true + codevalid=true + if [ "$DIFF" != "" ] + then + codevalid=false + fi + echo "valid=$codevalid" >> $GITHUB_OUTPUT + + - name: Create summary + if: inputs.validate == 'true' + run: | + # Use ternary operator to assign emoji based on validity + emoji=${{ steps.validate_checksum.outputs.valid == 'true' && '✅' || '❌' }} + valid=${{ steps.validate_checksum.outputs.valid == 'true' && 'valid' || 'INVALID' }} + echo "### $emoji ${{ inputs.key }} checksum $valid $emoji" >> $GITHUB_STEP_SUMMARY + shell: bash + # VALIDATE FILE END + + - name: Fail if invalid checksum + if: inputs.validate == 'true' && steps.validate_checksum.outputs.valid == 'false' && inputs.fail-invalid == 'true' + run: exit 1 + shell: bash diff --git a/.github/actions/connect-ovpn/action.yml b/.github/actions/connect-ovpn/action.yml index 220338866d6..140c478c0af 100644 --- a/.github/actions/connect-ovpn/action.yml +++ b/.github/actions/connect-ovpn/action.yml @@ -34,14 +34,10 @@ runs: sudo apt-get install openvpn-systemd-resolved shell: bash - - name: Validate OpenVPN + - name: Validate OpenVPN Version run: | echo -e "\nOpenVPN Version:" ; openvpn --version ; - echo -e "\nPinging URL: ${{ inputs.PING_URL }}" ; - ping -c 3 ${{ inputs.PING_URL }} ; - echo -e "\nIP Route:" ; - ip route ; shell: bash - name: Connect VPN @@ -70,7 +66,14 @@ runs: run: | sleep 5 if ping -c 2 $PING_URL > /dev/null 2>&1; then + + # echo -e "\nPinging URL: ${{ inputs.PING_URL }}" ; + # ping -c 3 ${{ inputs.PING_URL }} ; + echo "vpn-status=true" >> $GITHUB_OUTPUT + + echo -e "\nIP Route:" ; + ip route ; else echo "vpn-status=false" >> $GITHUB_OUTPUT fi diff --git a/.github/vpn/config.ovpn b/.github/actions/connect-ovpn/config.ovpn similarity index 100% rename from .github/vpn/config.ovpn rename to .github/actions/connect-ovpn/config.ovpn diff --git a/.github/actions/connect-ovpn/example.ovpn b/.github/actions/connect-ovpn/example.ovpn deleted file mode 100644 index ee61f15a507..00000000000 --- a/.github/actions/connect-ovpn/example.ovpn +++ /dev/null @@ -1,38 +0,0 @@ -// FULL FILE OVPN - -client -dev tun -proto udp -resolv-retry infinite -nobind -persist-key -persist-tun -remote-cert-tls server -auth-nocache -verb 3 - -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - b1:b0:0b:1a:ad:05:54:0f ------BEGIN CERTIFICATE----- -MIIBtjCCAVygAwIBAgIUbPYCDoO+XmScoS84AhQsbnKvd84wCgYIKoZIzj0EAwIw -u1MjifHr6jMxwQ== ------END CERTIFICATE----- - - -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - b1:b0:0b:1a:ad:05:54:0f ------BEGIN CERTIFICATE----- -MIIBtjCCAVygAwIBAgIUbPYCDoO+XmScoS ------END CERTIFICATE----- - - ------BEGIN CERTIFICATE----- -MIIBtjCCAVygAwIBAgIUbPYCDoO+XmScoS84AhQsbn ------END CERTIFICATE----- - diff --git a/.github/actions/demo-env/action.yml b/.github/actions/demo-env/action.yml index 3ef5847af33..fb0a4e9ecc0 100644 --- a/.github/actions/demo-env/action.yml +++ b/.github/actions/demo-env/action.yml @@ -174,7 +174,7 @@ runs: with: owner: CDCgov repo: prime-reportstream - ref: master + ref: main github_token: ${{ inputs.github-token }} workflow_file_name: restore_databases.yml wait_interval: 120 diff --git a/.github/actions/deploy-backend/action.yml b/.github/actions/deploy-backend/action.yml index 60c558eae6d..1d9beecaa4f 100644 --- a/.github/actions/deploy-backend/action.yml +++ b/.github/actions/deploy-backend/action.yml @@ -332,10 +332,7 @@ runs: - name: Validate function app checksum if: inputs.checksum-validation == 'true' - - uses: JosiahSiegel/checksum-validate-action@ebdf8c12c00912d18de93c483b935d51582f9236 - ## DevSecOps - Aquia (Replace) uses: ./.github/actions/checksum-validate-action - + uses: ./.github/actions/checksum-validate with: key: backend validate: true diff --git a/.github/actions/git-secrets/.gitattributes b/.github/actions/git-secrets/.gitattributes new file mode 100644 index 00000000000..107aad84b10 --- /dev/null +++ b/.github/actions/git-secrets/.gitattributes @@ -0,0 +1,10 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Force the bash scripts to be checked out with LF line endings. +git-secrets text eol=lf +git-secrets.1 text eol=lf +test/bats/bin/* text eol=lf +test/bats/libexec/* text eol=lf +*.bats text eol=lf +*.bash text eol=lf \ No newline at end of file diff --git a/.github/actions/git-secrets/.travis.yml b/.github/actions/git-secrets/.travis.yml new file mode 100644 index 00000000000..259379892db --- /dev/null +++ b/.github/actions/git-secrets/.travis.yml @@ -0,0 +1,8 @@ +language: bash + +before_install: +- git config --global user.email "you@example.com" +- git config --global user.name "Your Name" + +script: +- make test diff --git a/.github/actions/git-secrets/CHANGELOG.md b/.github/actions/git-secrets/CHANGELOG.md new file mode 100644 index 00000000000..cfcae4e818c --- /dev/null +++ b/.github/actions/git-secrets/CHANGELOG.md @@ -0,0 +1,49 @@ +# CHANGELOG + +## 1.3.0 - 2019-02-10 + +* Empty provider output is now excluded + (https://github.com/awslabs/git-secrets/issues/34) +* Spaces are now supported in git exec path, making more Windows + paths execute properly. +* Patterns with newlines and carriage returns are now loaded properly. +* Patterns that contain only "\n" are now ignored. +* Various Bash 4 fixes (https://github.com/awslabs/git-secrets/issues/66). +* Make IAM key scanning much more targeted. + +## 1.2.1 - 2016-06-27 + +* Fixed an issue where secret provider commands were causing "command not + found" errors due to a previously set IFS variable. + https://github.com/awslabs/git-secrets/pull/30 + +## 1.2.0 - 2016-05-23 + +* Fixed an issue where spaces files with spaces in their names were not being + properly scanned in the pre-commit hook. +* Now ignoring empty lines and comments (e.g., `#`) in the .gitallowed file. +* Fixed an issue where numbers were being compared to strings causing failures + on some platforms. + +## 1.1.0 - 2016-04-06 + +* Bug fix: the pre-commit hook previously only scanned the working directory + rather than staged files. This release updates the pre-commit hook to instead + scan staged files so that git-secrets will detect violations if the working + directory drifts from the staging directory. +* Added the `--scan-history` subcommand so that you can scan your entire + git history for violations. +* Added the ability to filter false positives by using a .gitallowed file. +* Added support for `--cached`, `--no-index`, and `--untracked` to the `--scan` + subcommand. + +## 1.0.1 - 2016-01-11 + +* Now works correctly with filenames in a repository that contain spaces when + executing `git secrets --scan` with no provided filename (via `git grep`). +* Now works with git repositories with hundreds of thousands of files when + using `git secrets --scan` with no provided filename (via `git grep`). + +## 1.0.0 - 2015-12-10 + +* Initial release of ``git-secrets``. diff --git a/.github/actions/git-secrets/CODE_OF_CONDUCT.md b/.github/actions/git-secrets/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..3b64466870c --- /dev/null +++ b/.github/actions/git-secrets/CODE_OF_CONDUCT.md @@ -0,0 +1,4 @@ +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/.github/actions/git-secrets/CONTRIBUTING.md b/.github/actions/git-secrets/CONTRIBUTING.md new file mode 100644 index 00000000000..de6d3d38075 --- /dev/null +++ b/.github/actions/git-secrets/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional +documentation, we greatly value feedback and contributions from our community. + +Please read through this document before submitting any issues or pull requests to ensure we have all the necessary +information to effectively respond to your bug report or contribution. + + +## Reporting Bugs/Feature Requests + +We welcome you to use the GitHub issue tracker to report bugs or suggest features. + +When filing an issue, please check [existing open](https://github.com/awslabs/git-secrets/issues), or [recently closed](https://github.com/awslabs/git-secrets/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already +reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +* A reproducible test case or series of steps +* The version of our code being used +* Any modifications you've made relevant to the bug +* Anything unusual about your environment or deployment + + +## Contributing via Pull Requests +Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: + +1. You are working against the latest source on the *master* branch. +2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. +3. You open an issue to discuss any significant work - we would hate for your time to be wasted. + +To send us a pull request, please: + +1. Fork the repository. +2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. +3. Ensure local tests pass. +4. Commit to your fork using clear commit messages. +5. Send us a pull request, answering any default questions in the pull request interface. +6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. + +GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and +[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + + +## Finding contributions to work on +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/git-secrets/labels/help%20wanted) issues is a great place to start. + + +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. + + +## Security issue notifications +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. + + +## Licensing + +See the [LICENSE](https://github.com/awslabs/git-secrets/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. + +We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/.github/actions/git-secrets/LICENSE.txt b/.github/actions/git-secrets/LICENSE.txt new file mode 100644 index 00000000000..de96b9473c9 --- /dev/null +++ b/.github/actions/git-secrets/LICENSE.txt @@ -0,0 +1,208 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Note: Other license terms may apply to certain, identified software files +contained within or distributed with the accompanying software if such terms +are included in the directory containing the accompanying software. Such other +license terms will then apply in lieu of the terms of the software license +above. diff --git a/.github/actions/git-secrets/Makefile b/.github/actions/git-secrets/Makefile new file mode 100644 index 00000000000..a67eee2dd23 --- /dev/null +++ b/.github/actions/git-secrets/Makefile @@ -0,0 +1,25 @@ +PREFIX ?= /usr/local +MANPREFIX ?= "${PREFIX}/share/man/man1" + +help: + @echo "Please use \`make ' where is one of" + @echo " test to perform unit tests." + @echo " man to build the man file from README.rst" + @echo " install to install. Use PREFIX and MANPREFIX to customize." + +# We use bats for testing: https://github.com/sstephenson/bats +test: + LANG=C test/bats/bin/bats test/ + +# The man page is completely derived from README.rst. Edits to +# README.rst require a rebuild of the man page. +man: + rst2man.py README.rst > git-secrets.1 + +install: + @mkdir -p ${DESTDIR}${MANPREFIX} + @mkdir -p ${DESTDIR}${PREFIX}/bin + @cp -f git-secrets ${DESTDIR}${PREFIX}/bin + @cp -f git-secrets.1 ${DESTDIR}${MANPREFIX} + +.PHONY: help test man diff --git a/.github/actions/git-secrets/NOTICE.txt b/.github/actions/git-secrets/NOTICE.txt new file mode 100644 index 00000000000..a5e5da9ba01 --- /dev/null +++ b/.github/actions/git-secrets/NOTICE.txt @@ -0,0 +1,6 @@ +git-secrets +Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +bats +This product bundles bats, which is available under a "MIT" license. +For details, see test/bats. diff --git a/.github/actions/git-secrets/README.rst b/.github/actions/git-secrets/README.rst new file mode 100644 index 00000000000..20a3fa5feb2 --- /dev/null +++ b/.github/actions/git-secrets/README.rst @@ -0,0 +1,543 @@ +=========== +git-secrets +=========== + +------------------------------------------------------------------------------------------- +Prevents you from committing passwords and other sensitive information to a git repository. +------------------------------------------------------------------------------------------- + +.. contents:: :depth: 2 + +Synopsis +-------- + +:: + + git secrets --scan [-r|--recursive] [--cached] [--no-index] [--untracked] [...] + git secrets --scan-history + git secrets --install [-f|--force] [] + git secrets --list [--global] + git secrets --add [-a|--allowed] [-l|--literal] [--global] + git secrets --add-provider [--global] [arguments...] + git secrets --register-aws [--global] + git secrets --aws-provider [] + + +Description +----------- + +``git-secrets`` scans commits, commit messages, and ``--no-ff`` merges to +prevent adding secrets into your git repositories. If a commit, +commit message, or any commit in a ``--no-ff`` merge history matches one of +your configured prohibited regular expression patterns, then the commit is +rejected. + + +Installing git-secrets +---------------------- + +``git-secrets`` must be placed somewhere in your PATH so that it is picked up +by ``git`` when running ``git secrets``. + +\*nix (Linux/macOS) +~~~~~~~~~~~~~~~~~ + +You can use the ``install`` target of the provided Makefile to install ``git secrets`` and the man page. +You can customize the install path using the PREFIX and MANPREFIX variables. + +:: + + make install + +Windows +~~~~~~~ + +Run the provided ``install.ps1`` powershell script. This will copy the needed files +to an installation directory (``%USERPROFILE%/.git-secrets`` by default) and add +the directory to the current user ``PATH``. + +:: + + PS > ./install.ps1 + +Homebrew (for macOS users) +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + brew install git-secrets + +.. warning:: + + You're not done yet! You MUST install the git hooks for every repo that + you wish to use with ``git secrets --install``. + +Here's a quick example of how to ensure a git repository is scanned for secrets +on each commit:: + + cd /path/to/my/repo + git secrets --install + git secrets --register-aws + + +Advanced configuration +---------------------- + +Add a configuration template if you want to add hooks to all repositories you +initialize or clone in the future. + +:: + + git secrets --register-aws --global + + +Add hooks to all your local repositories. + +:: + + git secrets --install ~/.git-templates/git-secrets + git config --global init.templateDir ~/.git-templates/git-secrets + + +Add custom providers to scan for security credentials. + +:: + + git secrets --add-provider -- cat /path/to/secret/file/patterns + + +Before making public a repository +--------------------------------- + +With git-secrets is also possible to scan a repository including all revisions: + +:: + + git secrets --scan-history + + +Options +------- + +Operation Modes +~~~~~~~~~~~~~~~ + +Each of these options must appear first on the command line. + +``--install`` + Installs git hooks for a repository. Once the hooks are installed for a git + repository, commits and non-fast-forward merges for that repository will be prevented + from committing secrets. + +``--scan`` + Scans one or more files for secrets. When a file contains a secret, the + matched text from the file being scanned will be written to stdout and the + script will exit with a non-zero status. Each matched line will be written with + the name of the file that matched, a colon, the line number that matched, + a colon, and then the line of text that matched. If no files are provided, + all files returned by ``git ls-files`` are scanned. + +``--scan-history`` + Scans repository including all revisions. When a file contains a secret, the + matched text from the file being scanned will be written to stdout and the + script will exit with a non-zero status. Each matched line will be written with + the name of the file that matched, a colon, the line number that matched, + a colon, and then the line of text that matched. + +``--list`` + Lists the ``git-secrets`` configuration for the current repo or in the global + git config. + +``--add`` + Adds a prohibited or allowed pattern. + +``--add-provider`` + Registers a secret provider. Secret providers are executables that when + invoked output prohibited patterns that ``git-secrets`` should treat as + prohibited. + +``--register-aws`` + Adds common AWS patterns to the git config and ensures that keys present + in ``~/.aws/credentials`` are not found in any commit. The following + checks are added: + + - AWS Access Key IDs via ``(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}`` + - AWS Secret Access Key assignments via ":" or "=" surrounded by optional + quotes + - AWS account ID assignments via ":" or "=" surrounded by optional quotes + - Allowed patterns for example AWS keys (``AKIAIOSFODNN7EXAMPLE`` and + ``wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY``) + - Known credentials from ``~/.aws/credentials`` + + .. note:: + + While the patterns registered by this command should catch most + instances of AWS credentials, these patterns are **not** guaranteed to + catch them **all**. ``git-secrets`` should be used as an extra means of + insurance -- you still need to do your due diligence to ensure that you + do not commit credentials to a repository. + +``--aws-provider`` + Secret provider that outputs credentials found in an INI file. You can + optionally provide the path to an INI file. + + +Options for ``--install`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +``-f, --force`` + Overwrites existing hooks if present. + +```` + When provided, installs git hooks to the given directory. The current + directory is assumed if ```` is not provided. + + If the provided ```` is not in a git repository, the + directory will be created and hooks will be placed in + ``/hooks``. This can be useful for creating git template + directories using with ``git init --template ``. + + You can run ``git init`` on a repository that has already been initialized. + From the `git init documentation `_: + + From the git documentation: Running ``git init`` in an existing repository + is safe. It will not overwrite things that are already there. The + primary reason for rerunning ``git init`` is to pick up newly added + templates (or to move the repository to another place if + ``--separate-git-dir`` is given). + + The following git hooks are installed: + + 1. ``pre-commit``: Used to check if any of the files changed in the commit + use prohibited patterns. + 2. ``commit-msg``: Used to determine if a commit message contains a + prohibited patterns. + 3. ``prepare-commit-msg``: Used to determine if a merge commit will + introduce a history that contains a prohibited pattern at any point. + Please note that this hook is only invoked for non fast-forward merges. + + .. note:: + + Git only allows a single script to be executed per hook. If the + repository contains Debian-style subdirectories like ``pre-commit.d`` + and ``commit-msg.d``, then the git hooks will be installed into these + directories, which assumes that you've configured the corresponding + hooks to execute all of the scripts found in these directories. If + these git subdirectories are not present, then the git hooks will be + installed to the git repo's ``.git/hooks`` directory. + + +Examples +^^^^^^^^ + +Install git hooks to the current directory:: + + cd /path/to/my/repository + git secrets --install + +Install git hooks to a repository other than the current directory:: + + git secrets --install /path/to/my/repository + +Create a git template that has ``git-secrets`` installed, and then copy that +template into a git repository:: + + git secrets --install ~/.git-templates/git-secrets + git init --template ~/.git-templates/git-secrets + +Overwrite existing hooks if present:: + + git secrets --install -f + + +Options for ``--scan`` +~~~~~~~~~~~~~~~~~~~~~~ + +``-r, --recursive`` + Scans the given files recursively. If a directory is encountered, the + directory will be scanned. If ``-r`` is not provided, directories will be + ignored. + + ``-r`` cannot be used alongside ``--cached``, ``--no-index``, or + ``--untracked``. + +``--cached`` + Searches blobs registered in the index file. + +``--no-index`` + Searches files in the current directory that is not managed by git. + +``--untracked`` + In addition to searching in the tracked files in the working tree, + ``--scan`` also in untracked files. + +``...`` + The path to one or more files on disk to scan for secrets. + + If no files are provided, all files returned by ``git ls-files`` are + scanned. + + +Examples +^^^^^^^^ + +Scan all files in the repo:: + + git secrets --scan + +Scans a single file for secrets:: + + git secrets --scan /path/to/file + +Scans a directory recursively for secrets:: + + git secrets --scan -r /path/to/directory + +Scans multiple files for secrets:: + + git secrets --scan /path/to/file /path/to/other/file + +You can scan by globbing:: + + git secrets --scan /path/to/directory/* + +Scan from stdin:: + + echo 'hello!' | git secrets --scan - + + +Options for ``--list`` +~~~~~~~~~~~~~~~~~~~~~~ + +``--global`` + Lists only git-secrets configuration in the global git config. + + +Options for ``--add`` +~~~~~~~~~~~~~~~~~~~~~ + +``--global`` + Adds patterns to the global git config + +``-l, --literal`` + Escapes special regular expression characters in the provided pattern so + that the pattern is searched for literally. + +``-a, --allowed`` + Mark the pattern as allowed instead of prohibited. Allowed patterns are + used to filter our false positives. + +```` + The regex pattern to search. + + +Examples +^^^^^^^^ + +Adds a prohibited pattern to the current repo:: + + git secrets --add '[A-Z0-9]{20}' + +Adds a prohibited pattern to the global git config:: + + git secrets --add --global '[A-Z0-9]{20}' + +Adds a string that is scanned for literally (``+`` is escaped):: + + git secrets --add --literal 'foo+bar' + +Add an allowed pattern:: + + git secrets --add -a 'allowed pattern' + + +Options for ``--register-aws`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``--global`` + Adds AWS specific configuration variables to the global git config. + + +Options for ``--aws-provider`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``[]`` + If provided, specifies the custom path to an INI file to scan. If not + provided, ``~/.aws/credentials`` is assumed. + + +Options for ``--add-provider`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``--global`` + Adds the provider to the global git config. + +```` + Provider command to invoke. When invoked the command is expected to write + prohibited patterns separated by new lines to stdout. Any extra arguments + provided are passed on to the command. + + +Examples +^^^^^^^^ + +Registers a secret provider with arguments:: + + git secrets --add-provider -- git secrets --aws-provider + +Cats secrets out of a file:: + + git secrets --add-provider -- cat /path/to/secret/file/patterns + + +Defining prohibited patterns +---------------------------- + +``egrep``-compatible regular expressions are used to determine if a commit or +commit message contains any prohibited patterns. These regular expressions are +defined using the ``git config`` command. It is important to note that +different systems use different versions of egrep. For example, when running on +macOS, you will use a different version of ``egrep`` than when running on something +like Ubuntu (BSD vs GNU). + +You can add prohibited regular expression patterns to your git config using +``git secrets --add ``. + + +Ignoring false positives +------------------------ + +Sometimes a regular expression might match false positives. For example, git +commit SHAs look a lot like AWS access keys. You can specify many different +regular expression patterns as false positives using the following command: + +:: + + git secrets --add --allowed 'my regex pattern' + +You can also add regular expressions patterns to filter false positives to a +``.gitallowed`` file located in the repository's root directory. Lines starting +with ``#`` are skipped (comment line) and empty lines are also skipped. + +First, git-secrets will extract all lines from a file that contain a prohibited +match. Included in the matched results will be the full path to the name of +the file that was matched, followed by ':', followed by the line number that was +matched, followed by the entire line from the file that was matched by a secret +pattern. Then, if you've defined allowed regular expressions, git-secrets will +check to see if all of the matched lines match at least one of your registered +allowed regular expressions. If all of the lines that were flagged as secret +are canceled out by an allowed match, then the subject text does not contain +any secrets. If any of the matched lines are not matched by an allowed regular +expression, then git-secrets will fail the commit/merge/message. + +.. important:: + + Just as it is a bad practice to add prohibited patterns that are too + greedy, it is also a bad practice to add allowed patterns that are too + forgiving. Be sure to test out your patterns using ad-hoc calls to + ``git secrets --scan $filename`` to ensure they are working as intended. + + +Secret providers +---------------- + +Sometimes you want to check for an exact pattern match against a set of known +secrets. For example, you might want to ensure that no credentials present in +``~/.aws/credentials`` ever show up in a commit. In these cases, it's better to +leave these secrets in one location rather than spread them out across git +repositories in git configs. You can use "secret providers" to fetch these +types of credentials. A secret provider is an executable that when invoked +outputs prohibited patterns separated by new lines. + +You can add secret providers using the ``--add-provider`` command:: + + git secrets --add-provider -- git secrets --aws-provider + +Notice the use of ``--``. This ensures that any arguments associated with the +provider are passed to the provider each time it is invoked when scanning +for secrets. + + +Example walkthrough +------------------- + +Let's take a look at an example. Given the following subject text (stored in +``/tmp/example``):: + + This is a test! + password=ex@mplepassword + password=****** + More test... + +And the following registered patterns: + +:: + + git secrets --add 'password\s*=\s*.+' + git secrets --add --allowed --literal 'ex@mplepassword' + +Running ``git secrets --scan /tmp/example``, the result will +result in the following error output:: + + /tmp/example:3:password=****** + + [ERROR] Matched prohibited pattern + + Possible mitigations: + - Mark false positives as allowed using: git config --add secrets.allowed ... + - List your configured patterns: git config --get-all secrets.patterns + - List your configured allowed patterns: git config --get-all secrets.allowed + - Use --no-verify if this is a one-time false positive + +Breaking this down, the prohibited pattern value of ``password\s*=\s*.+`` will +match the following lines:: + + /tmp/example:2:password=ex@mplepassword + /tmp/example:3:password=****** + +...But the first match will be filtered out due to the fact that it matches the +allowed regular expression of ``ex@mplepassword``. Because there is still a +remaining line that did not match, it is considered a secret. + +Because that matching lines are placed on lines that start with the filename +and line number (e.g., ``/tmp/example:3:...``), you can create allowed +patterns that take filenames and line numbers into account in the regular +expression. For example, you could whitelist an entire file using something +like:: + + git secrets --add --allowed '/tmp/example:.*' + git secrets --scan /tmp/example && echo $? + # Outputs: 0 + +Alternatively, you could allow a specific line number of a file if that +line is unlikely to change using something like the following: + +:: + + git secrets --add --allowed '/tmp/example:3:.*' + git secrets --scan /tmp/example && echo $? + # Outputs: 0 + +Keep this in mind when creating allowed patterns to ensure that your allowed +patterns are not inadvertently matched due to the fact that the filename is +included in the subject text that allowed patterns are matched against. + + +Skipping validation +------------------- + +Use the ``--no-verify`` option in the event of a false positive match in a +commit, merge, or commit message. This will skip the execution of the +git hook and allow you to make the commit or merge. + + +About +------ + +- Author: `Michael Dowling `_ +- Issue tracker: This project's source code and issue tracker can be found at + `https://github.com/awslabs/git-secrets `_ +- Special thanks to Adrian Vatchinsky and Ari Juels of Cornell University for + providing suggestions and feedback. + +Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/.github/actions/git-secrets/git-secrets b/.github/actions/git-secrets/git-secrets new file mode 100755 index 00000000000..11be1537e74 --- /dev/null +++ b/.github/actions/git-secrets/git-secrets @@ -0,0 +1,357 @@ +#!/usr/bin/env bash +# Copyright 2010-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://aws.amazon.com/apache2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +NONGIT_OK=1 OPTIONS_SPEC="\ +git secrets --scan [-r|--recursive] [--cached] [--no-index] [--untracked] [...] +git secrets --scan-history +git secrets --install [-f|--force] [] +git secrets --list [--global] +git secrets --add [-a|--allowed] [-l|--literal] [--global] +git secrets --add-provider [--global] [arguments...] +git secrets --register-aws [--global] +git secrets --aws-provider [] +-- +scan Scans for prohibited patterns +scan-history Scans repo for prohibited patterns +install Installs git hooks for Git repository or Git template directory +list Lists secret patterns +add Adds a prohibited or allowed pattern, ensuring to de-dupe with existing patterns +add-provider Adds a secret provider that when called outputs secret patterns on new lines +aws-provider Secret provider that outputs credentials found in an ini file +register-aws Adds common AWS patterns to the git config and scans for ~/.aws/credentials +r,recursive --scan scans directories recursively +cached --scan scans searches blobs registered in the index file +no-index --scan searches files in the current directory that is not managed by Git +untracked In addition to searching in the tracked files in the working tree, --scan also in untracked files +f,force --install overwrites hooks if the hook already exists +l,literal --add and --add-allowed patterns are escaped so that they are literal +a,allowed --add adds an allowed pattern instead of a prohibited pattern +global Uses the --global git config +commit_msg_hook* commit-msg hook (internal only) +pre_commit_hook* pre-commit hook (internal only) +prepare_commit_msg_hook* prepare-commit-msg hook (internal only)" + +# Include the git setup script. This parses and normalized CLI arguments. +. "$(git --exec-path)"/git-sh-setup + +load_patterns() { + git config --get-all secrets.patterns + # Execute each provider and use their output to build up patterns + git config --get-all secrets.providers | while read -r cmd; do + # Only split words on '\n\t ' and strip "\r" from the output to account + # for carriage returns being added on Windows systems. Note that this + # trimming is done before the test to ensure that the string is not empty. + local result="$(export IFS=$'\n\t '; $cmd | tr -d $'\r')" + # Do not add empty lines from providers as that would match everything. + if [ -n "${result}" ]; then + echo "${result}" + fi + done +} + +load_allowed() { + git config --get-all secrets.allowed + local gitallowed="$(git rev-parse --show-toplevel)/.gitallowed" + if [ -e "$gitallowed" ]; then + cat $gitallowed | awk 'NF && $1!~/^#/' + fi +} + +# load patterns and combine them with | +load_combined_patterns() { + local patterns=$(load_patterns) + local combined_patterns='' + for pattern in $patterns; do + combined_patterns=${combined_patterns}${pattern}"|" + done + combined_patterns=${combined_patterns%?} + echo $combined_patterns +} + +# Scans files or a repo using patterns. +scan() { + local files=("${@}") options="" + [ "${SCAN_CACHED}" == 1 ] && options+="--cached" + [ "${SCAN_UNTRACKED}" == 1 ] && options+=" --untracked" + [ "${SCAN_NO_INDEX}" == 1 ] && options+=" --no-index" + # Scan using git-grep if there are no files or if git options are applied. + if [ ${#files[@]} -eq 0 ] || [ ! -z "${options}" ]; then + output=$(git_grep $options "${files[@]}") + else + output=$(regular_grep "${files[@]}") + fi + process_output $? "${output}" +} + +# Scans through history using patterns +scan_history() { + # git log does not support multiple patterns, so we need to combine them + local combined_patterns=$(load_combined_patterns) + [ -z "${combined_patterns}" ] && return 0 + # Looks for differences matching the patterns, reduces the number of revisions to scan + local to_scan=$(git log --all -G"${combined_patterns}" --pretty=%H) + # Scan through revisions with findings to normalize output + output=$(GREP_OPTIONS= LC_ALL=C git grep -nwHEI "${combined_patterns}" $to_scan) + process_output $? "${output}" +} + +# Performs a git-grep, taking into account patterns and options. +# Note: this function returns 1 on success, 0 on error. +git_grep() { + local options="$1"; shift + local files=("${@}") combined_patterns=$(load_combined_patterns) + + [ -z "${combined_patterns}" ] && return 1 + GREP_OPTIONS= LC_ALL=C git grep -nwHEI ${options} "${combined_patterns}" -- "${files[@]}" +} + +# Performs a regular grep, taking into account patterns and recursion. +# Note: this function returns 1 on success, 0 on error. +regular_grep() { + local files=("${@}") patterns=$(load_patterns) action='skip' + [ -z "${patterns}" ] && return 1 + [ ${RECURSIVE} -eq 1 ] && action="recurse" + GREP_OPTIONS= LC_ALL=C grep -d "${action}" -nwHEI "${patterns}" "${files[@]}" +} + +# Process the given status ($1) and output variables ($2). +# Takes into account allowed patterns, and if a bad match is found, +# prints an error message and exits 1. +process_output() { + local status="$1" output="$2" + local allowed=$(load_allowed) + case "$status" in + 0) + [ -z "${allowed}" ] && echo "${output}" >&2 && return 1 + # Determine with a negative grep if the found matches are allowed + echo "${output}" | GREP_OPTIONS= LC_ALL=C grep -Ev "${allowed}" >&2 \ + && return 1 || return 0 + ;; + 1) return 0 ;; + *) exit $status + esac +} + +# Calls the given scanning function at $1, shifts, and passes to it $@. +# Exit 0 if success, otherwise exit 1 with error message. +scan_with_fn_or_die() { + local fn="$1"; shift + $fn "$@" && exit 0 + echo >&2 + echo "[ERROR] Matched one or more prohibited patterns" >&2 + echo >&2 + echo "Possible mitigations:" >&2 + echo "- Mark false positives as allowed using: git config --add secrets.allowed ..." >&2 + echo "- Mark false positives as allowed by adding regular expressions to .gitallowed at repository's root directory" >&2 + echo "- List your configured patterns: git config --get-all secrets.patterns" >&2 + echo "- List your configured allowed patterns: git config --get-all secrets.allowed" >&2 + echo "- List your configured allowed patterns in .gitallowed at repository's root directory" >&2 + echo "- Use --no-verify if this is a one-time false positive" >&2 + exit 1 +} + +# Scans a commit message, passed in the path to a file. +commit_msg_hook() { + scan_with_fn_or_die "scan" "$1" +} + +# Scans all files that are about to be committed. +pre_commit_hook() { + SCAN_CACHED=1 + local files=() file found_match=0 rev="4b825dc642cb6eb9a060e54bf8d69288fbee4904" + # Diff against HEAD if this is not the first commit in the repo. + git rev-parse --verify HEAD >/dev/null 2>&1 && rev="HEAD" + # Filter out deleted files using --diff-filter + while IFS= read -r file; do + [ -n "$file" ] && files+=("$file") + done <<< "$(git diff-index --diff-filter 'ACMU' --name-only --cached $rev --)" + scan_with_fn_or_die "scan" "${files[@]}" +} + +# Determines if merging in a commit will introduce tainted history. +prepare_commit_msg_hook() { + case "$2,$3" in + merge,) + local git_head=$(env | grep GITHEAD) # e.g. GITHEAD_=release/1.43 + local sha="${git_head##*=}" # Get just the SHA + local branch=$(git symbolic-ref HEAD) # e.g. refs/heads/master + local dest="${branch#refs/heads/}" # cut out "refs/heads" + git log "${dest}".."${sha}" -p | scan_with_fn_or_die "scan" - + ;; + esac +} + +install_hook() { + local path="$1" hook="$2" cmd="$3" dest + # Determines the approriate path for a hook to be installed + if [ -d "${path}/hooks/${hook}.d" ]; then + dest="${path}/hooks/${hook}.d/git-secrets" + else + dest="${path}/hooks/${hook}" + fi + [ -f "${dest}" ] && [ "${FORCE}" -ne 1 ] \ + && die "${dest} already exists. Use -f to force" + echo "#!/usr/bin/env bash" > "${dest}" + echo "git secrets --${cmd} -- \"\$@\"" >> "${dest}" + chmod +x "${dest}" + say "$(tput setaf 2)✓$(tput sgr 0) Installed ${hook} hook to ${dest}" +} + +install_all_hooks() { + install_hook "$1" "commit-msg" "commit_msg_hook" + install_hook "$1" "pre-commit" "pre_commit_hook" + install_hook "$1" "prepare-commit-msg" "prepare_commit_msg_hook" +} + +# Adds a git config pattern, ensuring to de-dupe +add_config() { + local key="$1"; shift + local value="$@" + if [ ${LITERAL} -eq 1 ]; then + value=$(sed 's/[\.|$(){}?+*^]/\\&/g' <<< "${value}") + fi + if [ ${GLOBAL} -eq 1 ]; then + git config --global --get-all $key | grep -Fq "${value}" && return 1 + git config --global --add "${key}" "${value}" + else + git config --get-all $key | grep -Fq "${value}" && return 1 + git config --add "${key}" "${value}" + fi +} + +register_aws() { + # Reusable regex patterns + local aws="(AWS|aws|Aws)?_?" quote="(\"|')" connect="\s*(:|=>|=)\s*" + local opt_quote="${quote}?" + add_config 'secrets.providers' 'git secrets --aws-provider' + add_config 'secrets.patterns' '(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}' + add_config 'secrets.patterns' "${opt_quote}${aws}(SECRET|secret|Secret)?_?(ACCESS|access|Access)?_?(KEY|key|Key)${opt_quote}${connect}${opt_quote}[A-Za-z0-9/\+=]{40}${opt_quote}" + add_config 'secrets.patterns' "${opt_quote}${aws}(ACCOUNT|account|Account)_?(ID|id|Id)?${opt_quote}${connect}${opt_quote}[0-9]{4}\-?[0-9]{4}\-?[0-9]{4}${opt_quote}" + add_config 'secrets.allowed' 'AKIAIOSFODNN7EXAMPLE' + add_config 'secrets.allowed' "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + + if [[ $? == 0 ]]; then + echo 'OK' + fi + + exit $? +} + +aws_provider() { + local fi="$1" + [ -z "$fi" ] && fi=~/.aws/credentials + # Find keys and ensure that special characters are escaped. + if [ -f $fi ]; then + awk -F "=" '/aws_access_key_id|aws_secret_access_key/ {print $2}' $fi \ + | tr -d ' "' \ + | sed 's/[]\.|$(){}?+*^]/\\&/g' + fi +} + +# Ensures that the command is what was expected for an option. +assert_option_for_command() { + local expected_command="$1" + local option_name="$2" + if [ "${COMMAND}" != "${expected_command}" ]; then + die "${option_name} can only be supplied with the ${expected_command} subcommand" + fi +} + +declare COMMAND="$1" FORCE=0 RECURSIVE=0 LITERAL=0 GLOBAL=0 ALLOWED=0 +declare SCAN_CACHED=0 SCAN_NO_INDEX=0 SCAN_UNTRACKED=0 + +# Shift off the command name +shift 1 +while [ "$#" -ne 0 ]; do + case "$1" in + -f) + assert_option_for_command "--install" "-f|--force" + FORCE=1 + ;; + -r) + assert_option_for_command "--scan" "-r|--recursive" + RECURSIVE=1 + ;; + -a) + assert_option_for_command "--add" "-a|--allowed" + ALLOWED=1 + ;; + -l) + assert_option_for_command "--add" "-l|--literal" + LITERAL=1 + ;; + --cached) + assert_option_for_command "--scan" "--cached" + SCAN_CACHED=1 + ;; + --no-index) + assert_option_for_command "--scan" "--no-index" + SCAN_NO_INDEX=1 + ;; + --untracked) + assert_option_for_command "--scan" "--untracked" + SCAN_UNTRACKED=1 + ;; + --global) GLOBAL=1 ;; + --) shift; break ;; + esac + shift +done + +# Ensure that recursive is not applied with mutually exclusive options. +if [ ${RECURSIVE} -eq 1 ]; then + if [ ${SCAN_CACHED} -eq 1 ] \ + || [ ${SCAN_NO_INDEX} -eq 1 ] \ + || [ ${SCAN_UNTRACKED} -eq 1 ]; + then + die "-r|--recursive cannot be supplied with --cached, --no-index, or --untracked" + fi +fi + +case "${COMMAND}" in + -h|--help|--) "$0" -h; exit 0 ;; + --add-provider) add_config "secrets.providers" "$@" ;; + --register-aws) register_aws ;; + --aws-provider) aws_provider "$1" ;; + --commit_msg_hook|--pre_commit_hook|--prepare_commit_msg_hook) + ${COMMAND:2} "$@" + ;; + --add) + if [ ${ALLOWED} -eq 1 ]; then + add_config "secrets.allowed" "$1" + else + add_config "secrets.patterns" "$1" + fi + ;; + --scan) scan_with_fn_or_die "scan" "$@" ;; + --scan-history) scan_with_fn_or_die "scan_history" "$@" ;; + --list) + if [ ${GLOBAL} -eq 1 ]; then + git config --global --get-regex secrets.* + else + git config --get-regex secrets.* + fi + ;; + --install) + DIRECTORY="$1" + if [ -z "${DIRECTORY}" ]; then + DIRECTORY=$(git rev-parse --git-dir) || die "Not in a Git repository" + elif [ -d "${DIRECTORY}"/.git ]; then + DIRECTORY="${DIRECTORY}/.git" + fi + mkdir -p "${DIRECTORY}/hooks" || die "Could not create dir: ${DIRECTORY}" + install_all_hooks "${DIRECTORY}" + ;; + *) echo "Unknown option: ${COMMAND}" && "$0" -h ;; +esac diff --git a/.github/actions/git-secrets/git-secrets.1 b/.github/actions/git-secrets/git-secrets.1 new file mode 100644 index 00000000000..1c6d25c3413 --- /dev/null +++ b/.github/actions/git-secrets/git-secrets.1 @@ -0,0 +1,843 @@ +.\" Man page generated from reStructuredText. +. +.TH GIT-SECRETS "" "" "" +.SH NAME +git-secrets \- Prevents you from committing passwords and other sensitive information to a git repository. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SS Contents +.INDENT 0.0 +.IP \(bu 2 +\fI\%Synopsis\fP +.IP \(bu 2 +\fI\%Description\fP +.IP \(bu 2 +\fI\%Installing git\-secrets\fP +.INDENT 2.0 +.IP \(bu 2 +\fI\%*nix (Linux/macOS)\fP +.IP \(bu 2 +\fI\%Windows\fP +.IP \(bu 2 +\fI\%Homebrew (for macOS users)\fP +.UNINDENT +.IP \(bu 2 +\fI\%Advanced configuration\fP +.IP \(bu 2 +\fI\%Before making public a repository\fP +.IP \(bu 2 +\fI\%Options\fP +.INDENT 2.0 +.IP \(bu 2 +\fI\%Operation Modes\fP +.IP \(bu 2 +\fI\%Options for \fB\-\-install\fP\fP +.IP \(bu 2 +\fI\%Options for \fB\-\-scan\fP\fP +.IP \(bu 2 +\fI\%Options for \fB\-\-list\fP\fP +.IP \(bu 2 +\fI\%Options for \fB\-\-add\fP\fP +.IP \(bu 2 +\fI\%Options for \fB\-\-register\-aws\fP\fP +.IP \(bu 2 +\fI\%Options for \fB\-\-aws\-provider\fP\fP +.IP \(bu 2 +\fI\%Options for \fB\-\-add\-provider\fP\fP +.UNINDENT +.IP \(bu 2 +\fI\%Defining prohibited patterns\fP +.IP \(bu 2 +\fI\%Ignoring false positives\fP +.IP \(bu 2 +\fI\%Secret providers\fP +.IP \(bu 2 +\fI\%Example walkthrough\fP +.IP \(bu 2 +\fI\%Skipping validation\fP +.IP \(bu 2 +\fI\%About\fP +.UNINDENT +.SH SYNOPSIS +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-scan [\-r|\-\-recursive] [\-\-cached] [\-\-no\-index] [\-\-untracked] [...] +git secrets \-\-scan\-history +git secrets \-\-install [\-f|\-\-force] [] +git secrets \-\-list [\-\-global] +git secrets \-\-add [\-a|\-\-allowed] [\-l|\-\-literal] [\-\-global] +git secrets \-\-add\-provider [\-\-global] [arguments...] +git secrets \-\-register\-aws [\-\-global] +git secrets \-\-aws\-provider [] +.ft P +.fi +.UNINDENT +.UNINDENT +.SH DESCRIPTION +.sp +\fBgit\-secrets\fP scans commits, commit messages, and \fB\-\-no\-ff\fP merges to +prevent adding secrets into your git repositories. If a commit, +commit message, or any commit in a \fB\-\-no\-ff\fP merge history matches one of +your configured prohibited regular expression patterns, then the commit is +rejected. +.SH INSTALLING GIT-SECRETS +.sp +\fBgit\-secrets\fP must be placed somewhere in your PATH so that it is picked up +by \fBgit\fP when running \fBgit secrets\fP\&. +.SS *nix (Linux/macOS) +.IP "System Message: WARNING/2 (README.rst:, line 43)" +Title underline too short. +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +\e*nix (Linux/macOS) +~~~~~~~~~~~~~~~~~ +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +You can use the \fBinstall\fP target of the provided Makefile to install \fBgit secrets\fP and the man page. +You can customize the install path using the PREFIX and MANPREFIX variables. +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +make install +.ft P +.fi +.UNINDENT +.UNINDENT +.SS Windows +.sp +Run the provided \fBinstall.ps1\fP powershell script. This will copy the needed files +to an installation directory (\fB%USERPROFILE%/.git\-secrets\fP by default) and add +the directory to the current user \fBPATH\fP\&. +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +PS > ./install.ps1 +.ft P +.fi +.UNINDENT +.UNINDENT +.SS Homebrew (for macOS users) +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +brew install git\-secrets +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +\fBWARNING:\fP +.INDENT 0.0 +.INDENT 3.5 +You\(aqre not done yet! You MUST install the git hooks for every repo that +you wish to use with \fBgit secrets \-\-install\fP\&. +.UNINDENT +.UNINDENT +.sp +Here\(aqs a quick example of how to ensure a git repository is scanned for secrets +on each commit: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +cd /path/to/my/repo +git secrets \-\-install +git secrets \-\-register\-aws +.ft P +.fi +.UNINDENT +.UNINDENT +.SH ADVANCED CONFIGURATION +.sp +Add a configuration template if you want to add hooks to all repositories you +initialize or clone in the future. +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-register\-aws \-\-global +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Add hooks to all your local repositories. +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-install ~/.git\-templates/git\-secrets +git config \-\-global init.templateDir ~/.git\-templates/git\-secrets +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Add custom providers to scan for security credentials. +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add\-provider \-\- cat /path/to/secret/file/patterns +.ft P +.fi +.UNINDENT +.UNINDENT +.SH BEFORE MAKING PUBLIC A REPOSITORY +.sp +With git\-secrets is also possible to scan a repository including all revisions: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-scan\-history +.ft P +.fi +.UNINDENT +.UNINDENT +.SH OPTIONS +.SS Operation Modes +.sp +Each of these options must appear first on the command line. +.INDENT 0.0 +.TP +.B \fB\-\-install\fP +Installs git hooks for a repository. Once the hooks are installed for a git +repository, commits and non\-fast\-forward merges for that repository will be prevented +from committing secrets. +.TP +.B \fB\-\-scan\fP +Scans one or more files for secrets. When a file contains a secret, the +matched text from the file being scanned will be written to stdout and the +script will exit with a non\-zero status. Each matched line will be written with +the name of the file that matched, a colon, the line number that matched, +a colon, and then the line of text that matched. If no files are provided, +all files returned by \fBgit ls\-files\fP are scanned. +.TP +.B \fB\-\-scan\-history\fP +Scans repository including all revisions. When a file contains a secret, the +matched text from the file being scanned will be written to stdout and the +script will exit with a non\-zero status. Each matched line will be written with +the name of the file that matched, a colon, the line number that matched, +a colon, and then the line of text that matched. +.TP +.B \fB\-\-list\fP +Lists the \fBgit\-secrets\fP configuration for the current repo or in the global +git config. +.TP +.B \fB\-\-add\fP +Adds a prohibited or allowed pattern. +.TP +.B \fB\-\-add\-provider\fP +Registers a secret provider. Secret providers are executables that when +invoked output prohibited patterns that \fBgit\-secrets\fP should treat as +prohibited. +.TP +.B \fB\-\-register\-aws\fP +Adds common AWS patterns to the git config and ensures that keys present +in \fB~/.aws/credentials\fP are not found in any commit. The following +checks are added: +.INDENT 7.0 +.IP \(bu 2 +AWS Access Key IDs via \fB(A3T[A\-Z0\-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A\-Z0\-9]{16}\fP +.IP \(bu 2 +AWS Secret Access Key assignments via ":" or "=" surrounded by optional +quotes +.IP \(bu 2 +AWS account ID assignments via ":" or "=" surrounded by optional quotes +.IP \(bu 2 +Allowed patterns for example AWS keys (\fBAKIAIOSFODNN7EXAMPLE\fP and +\fBwJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\fP) +.IP \(bu 2 +Known credentials from \fB~/.aws/credentials\fP +.UNINDENT +.sp +\fBNOTE:\fP +.INDENT 7.0 +.INDENT 3.5 +While the patterns registered by this command should catch most +instances of AWS credentials, these patterns are \fBnot\fP guaranteed to +catch them \fBall\fP\&. \fBgit\-secrets\fP should be used as an extra means of +insurance \-\- you still need to do your due diligence to ensure that you +do not commit credentials to a repository. +.UNINDENT +.UNINDENT +.TP +.B \fB\-\-aws\-provider\fP +Secret provider that outputs credentials found in an INI file. You can +optionally provide the path to an INI file. +.UNINDENT +.SS Options for \fB\-\-install\fP +.INDENT 0.0 +.TP +.B \fB\-f, \-\-force\fP +Overwrites existing hooks if present. +.TP +.B \fB\fP +When provided, installs git hooks to the given directory. The current +directory is assumed if \fB\fP is not provided. +.sp +If the provided \fB\fP is not in a git repository, the +directory will be created and hooks will be placed in +\fB/hooks\fP\&. This can be useful for creating git template +directories using with \fBgit init \-\-template \fP\&. +.sp +You can run \fBgit init\fP on a repository that has already been initialized. +From the \fI\%git init documentation\fP: +.INDENT 7.0 +.INDENT 3.5 +From the git documentation: Running \fBgit init\fP in an existing repository +is safe. It will not overwrite things that are already there. The +primary reason for rerunning \fBgit init\fP is to pick up newly added +templates (or to move the repository to another place if +\fB\-\-separate\-git\-dir\fP is given). +.UNINDENT +.UNINDENT +.sp +The following git hooks are installed: +.INDENT 7.0 +.IP 1. 3 +\fBpre\-commit\fP: Used to check if any of the files changed in the commit +use prohibited patterns. +.IP 2. 3 +\fBcommit\-msg\fP: Used to determine if a commit message contains a +prohibited patterns. +.IP 3. 3 +\fBprepare\-commit\-msg\fP: Used to determine if a merge commit will +introduce a history that contains a prohibited pattern at any point. +Please note that this hook is only invoked for non fast\-forward merges. +.UNINDENT +.sp +\fBNOTE:\fP +.INDENT 7.0 +.INDENT 3.5 +Git only allows a single script to be executed per hook. If the +repository contains Debian\-style subdirectories like \fBpre\-commit.d\fP +and \fBcommit\-msg.d\fP, then the git hooks will be installed into these +directories, which assumes that you\(aqve configured the corresponding +hooks to execute all of the scripts found in these directories. If +these git subdirectories are not present, then the git hooks will be +installed to the git repo\(aqs \fB\&.git/hooks\fP directory. +.UNINDENT +.UNINDENT +.UNINDENT +.SS Examples +.sp +Install git hooks to the current directory: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +cd /path/to/my/repository +git secrets \-\-install +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Install git hooks to a repository other than the current directory: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-install /path/to/my/repository +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Create a git template that has \fBgit\-secrets\fP installed, and then copy that +template into a git repository: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-install ~/.git\-templates/git\-secrets +git init \-\-template ~/.git\-templates/git\-secrets +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Overwrite existing hooks if present: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-install \-f +.ft P +.fi +.UNINDENT +.UNINDENT +.SS Options for \fB\-\-scan\fP +.INDENT 0.0 +.TP +.B \fB\-r, \-\-recursive\fP +Scans the given files recursively. If a directory is encountered, the +directory will be scanned. If \fB\-r\fP is not provided, directories will be +ignored. +.sp +\fB\-r\fP cannot be used alongside \fB\-\-cached\fP, \fB\-\-no\-index\fP, or +\fB\-\-untracked\fP\&. +.TP +.B \fB\-\-cached\fP +Searches blobs registered in the index file. +.TP +.B \fB\-\-no\-index\fP +Searches files in the current directory that is not managed by git. +.TP +.B \fB\-\-untracked\fP +In addition to searching in the tracked files in the working tree, +\fB\-\-scan\fP also in untracked files. +.TP +.B \fB...\fP +The path to one or more files on disk to scan for secrets. +.sp +If no files are provided, all files returned by \fBgit ls\-files\fP are +scanned. +.UNINDENT +.SS Examples +.sp +Scan all files in the repo: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-scan +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Scans a single file for secrets: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-scan /path/to/file +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Scans a directory recursively for secrets: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-scan \-r /path/to/directory +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Scans multiple files for secrets: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-scan /path/to/file /path/to/other/file +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +You can scan by globbing: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-scan /path/to/directory/* +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Scan from stdin: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +echo \(aqhello!\(aq | git secrets \-\-scan \- +.ft P +.fi +.UNINDENT +.UNINDENT +.SS Options for \fB\-\-list\fP +.INDENT 0.0 +.TP +.B \fB\-\-global\fP +Lists only git\-secrets configuration in the global git config. +.UNINDENT +.SS Options for \fB\-\-add\fP +.INDENT 0.0 +.TP +.B \fB\-\-global\fP +Adds patterns to the global git config +.TP +.B \fB\-l, \-\-literal\fP +Escapes special regular expression characters in the provided pattern so +that the pattern is searched for literally. +.TP +.B \fB\-a, \-\-allowed\fP +Mark the pattern as allowed instead of prohibited. Allowed patterns are +used to filter our false positives. +.TP +.B \fB\fP +The regex pattern to search. +.UNINDENT +.SS Examples +.sp +Adds a prohibited pattern to the current repo: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add \(aq[A\-Z0\-9]{20}\(aq +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Adds a prohibited pattern to the global git config: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add \-\-global \(aq[A\-Z0\-9]{20}\(aq +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Adds a string that is scanned for literally (\fB+\fP is escaped): +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add \-\-literal \(aqfoo+bar\(aq +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Add an allowed pattern: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add \-a \(aqallowed pattern\(aq +.ft P +.fi +.UNINDENT +.UNINDENT +.SS Options for \fB\-\-register\-aws\fP +.INDENT 0.0 +.TP +.B \fB\-\-global\fP +Adds AWS specific configuration variables to the global git config. +.UNINDENT +.SS Options for \fB\-\-aws\-provider\fP +.INDENT 0.0 +.TP +.B \fB[]\fP +If provided, specifies the custom path to an INI file to scan. If not +provided, \fB~/.aws/credentials\fP is assumed. +.UNINDENT +.SS Options for \fB\-\-add\-provider\fP +.INDENT 0.0 +.TP +.B \fB\-\-global\fP +Adds the provider to the global git config. +.TP +.B \fB\fP +Provider command to invoke. When invoked the command is expected to write +prohibited patterns separated by new lines to stdout. Any extra arguments +provided are passed on to the command. +.UNINDENT +.SS Examples +.sp +Registers a secret provider with arguments: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add\-provider \-\- git secrets \-\-aws\-provider +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Cats secrets out of a file: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add\-provider \-\- cat /path/to/secret/file/patterns +.ft P +.fi +.UNINDENT +.UNINDENT +.SH DEFINING PROHIBITED PATTERNS +.sp +\fBegrep\fP\-compatible regular expressions are used to determine if a commit or +commit message contains any prohibited patterns. These regular expressions are +defined using the \fBgit config\fP command. It is important to note that +different systems use different versions of egrep. For example, when running on +macOS, you will use a different version of \fBegrep\fP than when running on something +like Ubuntu (BSD vs GNU). +.sp +You can add prohibited regular expression patterns to your git config using +\fBgit secrets \-\-add \fP\&. +.SH IGNORING FALSE POSITIVES +.sp +Sometimes a regular expression might match false positives. For example, git +commit SHAs look a lot like AWS access keys. You can specify many different +regular expression patterns as false positives using the following command: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add \-\-allowed \(aqmy regex pattern\(aq +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +You can also add regular expressions patterns to filter false positives to a +\fB\&.gitallowed\fP file located in the repository\(aqs root directory. Lines starting +with \fB#\fP are skipped (comment line) and empty lines are also skipped. +.sp +First, git\-secrets will extract all lines from a file that contain a prohibited +match. Included in the matched results will be the full path to the name of +the file that was matched, followed by \(aq:\(aq, followed by the line number that was +matched, followed by the entire line from the file that was matched by a secret +pattern. Then, if you\(aqve defined allowed regular expressions, git\-secrets will +check to see if all of the matched lines match at least one of your registered +allowed regular expressions. If all of the lines that were flagged as secret +are canceled out by an allowed match, then the subject text does not contain +any secrets. If any of the matched lines are not matched by an allowed regular +expression, then git\-secrets will fail the commit/merge/message. +.sp +\fBIMPORTANT:\fP +.INDENT 0.0 +.INDENT 3.5 +Just as it is a bad practice to add prohibited patterns that are too +greedy, it is also a bad practice to add allowed patterns that are too +forgiving. Be sure to test out your patterns using ad\-hoc calls to +\fBgit secrets \-\-scan $filename\fP to ensure they are working as intended. +.UNINDENT +.UNINDENT +.SH SECRET PROVIDERS +.sp +Sometimes you want to check for an exact pattern match against a set of known +secrets. For example, you might want to ensure that no credentials present in +\fB~/.aws/credentials\fP ever show up in a commit. In these cases, it\(aqs better to +leave these secrets in one location rather than spread them out across git +repositories in git configs. You can use "secret providers" to fetch these +types of credentials. A secret provider is an executable that when invoked +outputs prohibited patterns separated by new lines. +.sp +You can add secret providers using the \fB\-\-add\-provider\fP command: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add\-provider \-\- git secrets \-\-aws\-provider +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Notice the use of \fB\-\-\fP\&. This ensures that any arguments associated with the +provider are passed to the provider each time it is invoked when scanning +for secrets. +.SH EXAMPLE WALKTHROUGH +.sp +Let\(aqs take a look at an example. Given the following subject text (stored in +\fB/tmp/example\fP): +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +This is a test! +password=ex@mplepassword +password=****** +More test... +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +And the following registered patterns: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add \(aqpassword\es*=\es*.+\(aq +git secrets \-\-add \-\-allowed \-\-literal \(aqex@mplepassword\(aq +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Running \fBgit secrets \-\-scan /tmp/example\fP, the result will +result in the following error output: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +/tmp/example:3:password=****** + +[ERROR] Matched prohibited pattern + +Possible mitigations: +\- Mark false positives as allowed using: git config \-\-add secrets.allowed ... +\- List your configured patterns: git config \-\-get\-all secrets.patterns +\- List your configured allowed patterns: git config \-\-get\-all secrets.allowed +\- Use \-\-no\-verify if this is a one\-time false positive +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Breaking this down, the prohibited pattern value of \fBpassword\es*=\es*.+\fP will +match the following lines: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +/tmp/example:2:password=ex@mplepassword +/tmp/example:3:password=****** +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +\&...But the first match will be filtered out due to the fact that it matches the +allowed regular expression of \fBex@mplepassword\fP\&. Because there is still a +remaining line that did not match, it is considered a secret. +.sp +Because that matching lines are placed on lines that start with the filename +and line number (e.g., \fB/tmp/example:3:...\fP), you can create allowed +patterns that take filenames and line numbers into account in the regular +expression. For example, you could whitelist an entire file using something +like: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add \-\-allowed \(aq/tmp/example:.*\(aq +git secrets \-\-scan /tmp/example && echo $? +# Outputs: 0 +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Alternatively, you could allow a specific line number of a file if that +line is unlikely to change using something like the following: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +git secrets \-\-add \-\-allowed \(aq/tmp/example:3:.*\(aq +git secrets \-\-scan /tmp/example && echo $? +# Outputs: 0 +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Keep this in mind when creating allowed patterns to ensure that your allowed +patterns are not inadvertently matched due to the fact that the filename is +included in the subject text that allowed patterns are matched against. +.SH SKIPPING VALIDATION +.sp +Use the \fB\-\-no\-verify\fP option in the event of a false positive match in a +commit, merge, or commit message. This will skip the execution of the +git hook and allow you to make the commit or merge. +.SH ABOUT +.INDENT 0.0 +.IP \(bu 2 +Author: \fI\%Michael Dowling\fP +.IP \(bu 2 +Issue tracker: This project\(aqs source code and issue tracker can be found at +\fI\%https://github.com/awslabs/git\-secrets\fP +.IP \(bu 2 +Special thanks to Adrian Vatchinsky and Ari Juels of Cornell University for +providing suggestions and feedback. +.UNINDENT +.sp +Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. +.\" Generated by docutils manpage writer. +. diff --git a/.github/actions/git-secrets/install.ps1 b/.github/actions/git-secrets/install.ps1 new file mode 100644 index 00000000000..fbffbfa9906 --- /dev/null +++ b/.github/actions/git-secrets/install.ps1 @@ -0,0 +1,48 @@ +Param([string]$InstallationDirectory = $($Env:USERPROFILE + "\.git-secrets")) + +Write-Host "Checking to see if installation directory already exists..." +if (-not (Test-Path $InstallationDirectory)) +{ + Write-Host "Creating installation directory." + New-Item -ItemType Directory -Path $InstallationDirectory | Out-Null +} +else +{ + Write-Host "Installation directory already exists." +} + +Write-Host "Copying files." +Copy-Item ./git-secrets -Destination $InstallationDirectory -Force +Copy-Item ./git-secrets.1 -Destination $InstallationDirectory -Force + +Write-Host "Checking if directory already exists in Path..." +$currentPath = [Environment]::GetEnvironmentVariable("PATH", "User") +if ($currentPath -notlike "*$InstallationDirectory*") +{ + Write-Host "Adding to path." + $newPath = $currentPath + if(-not ($newPath.EndsWith(";"))) + { + $newPath = $newPath + ";" + } + $newPath = $newPath + $InstallationDirectory + [Environment]::SetEnvironmentVariable("PATH", $newPath, "User") +} +else +{ + Write-Host "Already in Path." +} + +# Adding to Session +Write-Host "Adding to user session." +$currentSessionPath = $Env:Path +if ($currentSessionPath -notlike "*$InstallationDirectory*") +{ + if(-not ($currentSessionPath.EndsWith(";"))) + { + $currentSessionPath = $currentSessionPath + ";" + } + $Env:Path = $currentSessionPath + $InstallationDirectory +} + +Write-Host "Done." \ No newline at end of file diff --git a/.github/actions/git-secrets/test/bats/LICENSE b/.github/actions/git-secrets/test/bats/LICENSE new file mode 100644 index 00000000000..bac4eb29ccf --- /dev/null +++ b/.github/actions/git-secrets/test/bats/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014 Sam Stephenson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/.github/actions/git-secrets/test/bats/bin/bats b/.github/actions/git-secrets/test/bats/bin/bats new file mode 120000 index 00000000000..a50a884e581 --- /dev/null +++ b/.github/actions/git-secrets/test/bats/bin/bats @@ -0,0 +1 @@ +../libexec/bats \ No newline at end of file diff --git a/.github/actions/git-secrets/test/bats/libexec/bats b/.github/actions/git-secrets/test/bats/libexec/bats new file mode 100755 index 00000000000..71f392f757e --- /dev/null +++ b/.github/actions/git-secrets/test/bats/libexec/bats @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +set -e + +version() { + echo "Bats 0.4.0" +} + +usage() { + version + echo "Usage: bats [-c] [-p | -t] [ ...]" +} + +help() { + usage + echo + echo " is the path to a Bats test file, or the path to a directory" + echo " containing Bats test files." + echo + echo " -c, --count Count the number of test cases without running any tests" + echo " -h, --help Display this help message" + echo " -p, --pretty Show results in pretty format (default for terminals)" + echo " -t, --tap Show results in TAP format" + echo " -v, --version Display the version number" + echo + echo " For more information, see https://github.com/sstephenson/bats" + echo +} + +resolve_link() { + $(type -p greadlink readlink | head -1) "$1" +} + +abs_dirname() { + local cwd="$(pwd)" + local path="$1" + + while [ -n "$path" ]; do + cd "${path%/*}" + local name="${path##*/}" + path="$(resolve_link "$name" || true)" + done + + pwd + cd "$cwd" +} + +expand_path() { + { cd "$(dirname "$1")" 2>/dev/null + local dirname="$PWD" + cd "$OLDPWD" + echo "$dirname/$(basename "$1")" + } || echo "$1" +} + +BATS_LIBEXEC="$(abs_dirname "$0")" +export BATS_PREFIX="$(abs_dirname "$BATS_LIBEXEC")" +export BATS_CWD="$(abs_dirname .)" +export PATH="$BATS_LIBEXEC:$PATH" + +options=() +arguments=() +for arg in "$@"; do + if [ "${arg:0:1}" = "-" ]; then + if [ "${arg:1:1}" = "-" ]; then + options[${#options[*]}]="${arg:2}" + else + index=1 + while option="${arg:$index:1}"; do + [ -n "$option" ] || break + options[${#options[*]}]="$option" + let index+=1 + done + fi + else + arguments[${#arguments[*]}]="$arg" + fi +done + +unset count_flag pretty +[ -t 0 ] && [ -t 1 ] && pretty="1" +[ -n "$CI" ] && pretty="" + +for option in "${options[@]}"; do + case "$option" in + "h" | "help" ) + help + exit 0 + ;; + "v" | "version" ) + version + exit 0 + ;; + "c" | "count" ) + count_flag="-c" + ;; + "t" | "tap" ) + pretty="" + ;; + "p" | "pretty" ) + pretty="1" + ;; + * ) + usage >&2 + exit 1 + ;; + esac +done + +if [ "${#arguments[@]}" -eq 0 ]; then + usage >&2 + exit 1 +fi + +filenames=() +for filename in "${arguments[@]}"; do + if [ -d "$filename" ]; then + shopt -s nullglob + for suite_filename in "$(expand_path "$filename")"/*.bats; do + filenames["${#filenames[@]}"]="$suite_filename" + done + shopt -u nullglob + else + filenames["${#filenames[@]}"]="$(expand_path "$filename")" + fi +done + +if [ "${#filenames[@]}" -eq 1 ]; then + command="bats-exec-test" +else + command="bats-exec-suite" +fi + +if [ -n "$pretty" ]; then + extended_syntax_flag="-x" + formatter="bats-format-tap-stream" +else + extended_syntax_flag="" + formatter="cat" +fi + +set -o pipefail execfail +exec "$command" $count_flag $extended_syntax_flag "${filenames[@]}" | "$formatter" diff --git a/.github/actions/git-secrets/test/bats/libexec/bats-exec-suite b/.github/actions/git-secrets/test/bats/libexec/bats-exec-suite new file mode 100755 index 00000000000..29ab255d062 --- /dev/null +++ b/.github/actions/git-secrets/test/bats/libexec/bats-exec-suite @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -e + +count_only_flag="" +if [ "$1" = "-c" ]; then + count_only_flag=1 + shift +fi + +extended_syntax_flag="" +if [ "$1" = "-x" ]; then + extended_syntax_flag="-x" + shift +fi + +trap "kill 0; exit 1" int + +count=0 +for filename in "$@"; do + let count+="$(bats-exec-test -c "$filename")" +done + +if [ -n "$count_only_flag" ]; then + echo "$count" + exit +fi + +echo "1..$count" +status=0 +offset=0 +for filename in "$@"; do + index=0 + { + IFS= read -r # 1..n + while IFS= read -r line; do + case "$line" in + "begin "* ) + let index+=1 + echo "${line/ $index / $(($offset + $index)) }" + ;; + "ok "* | "not ok "* ) + [ -n "$extended_syntax_flag" ] || let index+=1 + echo "${line/ $index / $(($offset + $index)) }" + [ "${line:0:6}" != "not ok" ] || status=1 + ;; + * ) + echo "$line" + ;; + esac + done + } < <( bats-exec-test $extended_syntax_flag "$filename" ) + offset=$(($offset + $index)) +done + +exit "$status" diff --git a/.github/actions/git-secrets/test/bats/libexec/bats-exec-test b/.github/actions/git-secrets/test/bats/libexec/bats-exec-test new file mode 100755 index 00000000000..8f3bd5102e4 --- /dev/null +++ b/.github/actions/git-secrets/test/bats/libexec/bats-exec-test @@ -0,0 +1,346 @@ +#!/usr/bin/env bash +set -e +set -E +set -T + +BATS_COUNT_ONLY="" +if [ "$1" = "-c" ]; then + BATS_COUNT_ONLY=1 + shift +fi + +BATS_EXTENDED_SYNTAX="" +if [ "$1" = "-x" ]; then + BATS_EXTENDED_SYNTAX="$1" + shift +fi + +BATS_TEST_FILENAME="$1" +if [ -z "$BATS_TEST_FILENAME" ]; then + echo "usage: bats-exec " >&2 + exit 1 +elif [ ! -f "$BATS_TEST_FILENAME" ]; then + echo "bats: $BATS_TEST_FILENAME does not exist" >&2 + exit 1 +else + shift +fi + +BATS_TEST_DIRNAME="$(dirname "$BATS_TEST_FILENAME")" +BATS_TEST_NAMES=() + +load() { + local name="$1" + local filename + + if [ "${name:0:1}" = "/" ]; then + filename="${name}" + else + filename="$BATS_TEST_DIRNAME/${name}.bash" + fi + + [ -f "$filename" ] || { + echo "bats: $filename does not exist" >&2 + exit 1 + } + + source "${filename}" +} + +run() { + local e E T oldIFS + [[ ! "$-" =~ e ]] || e=1 + [[ ! "$-" =~ E ]] || E=1 + [[ ! "$-" =~ T ]] || T=1 + set +e + set +E + set +T + output="$("$@" 2>&1)" + status="$?" + oldIFS=$IFS + IFS=$'\n' lines=($output) + [ -z "$e" ] || set -e + [ -z "$E" ] || set -E + [ -z "$T" ] || set -T + IFS=$oldIFS +} + +setup() { + true +} + +teardown() { + true +} + +skip() { + BATS_TEST_SKIPPED=${1:-1} + BATS_TEST_COMPLETED=1 + exit 0 +} + +bats_test_begin() { + BATS_TEST_DESCRIPTION="$1" + if [ -n "$BATS_EXTENDED_SYNTAX" ]; then + echo "begin $BATS_TEST_NUMBER $BATS_TEST_DESCRIPTION" >&3 + fi + setup +} + +bats_test_function() { + local test_name="$1" + BATS_TEST_NAMES["${#BATS_TEST_NAMES[@]}"]="$test_name" +} + +bats_capture_stack_trace() { + BATS_PREVIOUS_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) + BATS_CURRENT_STACK_TRACE=() + + local test_pattern=" $BATS_TEST_NAME $BATS_TEST_SOURCE" + local setup_pattern=" setup $BATS_TEST_SOURCE" + local teardown_pattern=" teardown $BATS_TEST_SOURCE" + + local frame + local index=1 + + while frame="$(caller "$index")"; do + BATS_CURRENT_STACK_TRACE["${#BATS_CURRENT_STACK_TRACE[@]}"]="$frame" + if [[ "$frame" = *"$test_pattern" || \ + "$frame" = *"$setup_pattern" || \ + "$frame" = *"$teardown_pattern" ]]; then + break + else + let index+=1 + fi + done + + BATS_SOURCE="$(bats_frame_filename "${BATS_CURRENT_STACK_TRACE[0]}")" + BATS_LINENO="$(bats_frame_lineno "${BATS_CURRENT_STACK_TRACE[0]}")" +} + +bats_print_stack_trace() { + local frame + local index=1 + local count="${#@}" + + for frame in "$@"; do + local filename="$(bats_trim_filename "$(bats_frame_filename "$frame")")" + local lineno="$(bats_frame_lineno "$frame")" + + if [ $index -eq 1 ]; then + echo -n "# (" + else + echo -n "# " + fi + + local fn="$(bats_frame_function "$frame")" + if [ "$fn" != "$BATS_TEST_NAME" ]; then + echo -n "from function \`$fn' " + fi + + if [ $index -eq $count ]; then + echo "in test file $filename, line $lineno)" + else + echo "in file $filename, line $lineno," + fi + + let index+=1 + done +} + +bats_print_failed_command() { + local frame="$1" + local status="$2" + local filename="$(bats_frame_filename "$frame")" + local lineno="$(bats_frame_lineno "$frame")" + + local failed_line="$(bats_extract_line "$filename" "$lineno")" + local failed_command="$(bats_strip_string "$failed_line")" + echo -n "# \`${failed_command}' " + + if [ $status -eq 1 ]; then + echo "failed" + else + echo "failed with status $status" + fi +} + +bats_frame_lineno() { + local frame="$1" + local lineno="${frame%% *}" + echo "$lineno" +} + +bats_frame_function() { + local frame="$1" + local rest="${frame#* }" + local fn="${rest%% *}" + echo "$fn" +} + +bats_frame_filename() { + local frame="$1" + local rest="${frame#* }" + local filename="${rest#* }" + + if [ "$filename" = "$BATS_TEST_SOURCE" ]; then + echo "$BATS_TEST_FILENAME" + else + echo "$filename" + fi +} + +bats_extract_line() { + local filename="$1" + local lineno="$2" + sed -n "${lineno}p" "$filename" +} + +bats_strip_string() { + local string="$1" + printf "%s" "$string" | sed -e "s/^[ "$'\t'"]*//" -e "s/[ "$'\t'"]*$//" +} + +bats_trim_filename() { + local filename="$1" + local length="${#BATS_CWD}" + + if [ "${filename:0:length+1}" = "${BATS_CWD}/" ]; then + echo "${filename:length+1}" + else + echo "$filename" + fi +} + +bats_debug_trap() { + if [ "$BASH_SOURCE" != "$1" ]; then + bats_capture_stack_trace + fi +} + +bats_error_trap() { + BATS_ERROR_STATUS="$?" + BATS_ERROR_STACK_TRACE=( "${BATS_PREVIOUS_STACK_TRACE[@]}" ) + trap - debug +} + +bats_teardown_trap() { + trap "bats_exit_trap" exit + local status=0 + teardown >>"$BATS_OUT" 2>&1 || status="$?" + + if [ $status -eq 0 ]; then + BATS_TEARDOWN_COMPLETED=1 + elif [ -n "$BATS_TEST_COMPLETED" ]; then + BATS_ERROR_STATUS="$status" + BATS_ERROR_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) + fi + + bats_exit_trap +} + +bats_exit_trap() { + local status + local skipped + trap - err exit + + skipped="" + if [ -n "$BATS_TEST_SKIPPED" ]; then + skipped=" # skip" + if [ "1" != "$BATS_TEST_SKIPPED" ]; then + skipped+=" ($BATS_TEST_SKIPPED)" + fi + fi + + if [ -z "$BATS_TEST_COMPLETED" ] || [ -z "$BATS_TEARDOWN_COMPLETED" ]; then + echo "not ok $BATS_TEST_NUMBER $BATS_TEST_DESCRIPTION" >&3 + bats_print_stack_trace "${BATS_ERROR_STACK_TRACE[@]}" >&3 + bats_print_failed_command "${BATS_ERROR_STACK_TRACE[${#BATS_ERROR_STACK_TRACE[@]}-1]}" "$BATS_ERROR_STATUS" >&3 + sed -e "s/^/# /" < "$BATS_OUT" >&3 + status=1 + else + echo "ok ${BATS_TEST_NUMBER}${skipped} ${BATS_TEST_DESCRIPTION}" >&3 + status=0 + fi + + rm -f "$BATS_OUT" + exit "$status" +} + +bats_perform_tests() { + echo "1..$#" + test_number=1 + status=0 + for test_name in "$@"; do + "$0" $BATS_EXTENDED_SYNTAX "$BATS_TEST_FILENAME" "$test_name" "$test_number" || status=1 + let test_number+=1 + done + exit "$status" +} + +bats_perform_test() { + BATS_TEST_NAME="$1" + if [ "$(type -t "$BATS_TEST_NAME" || true)" = "function" ]; then + BATS_TEST_NUMBER="$2" + if [ -z "$BATS_TEST_NUMBER" ]; then + echo "1..1" + BATS_TEST_NUMBER="1" + fi + + BATS_TEST_COMPLETED="" + BATS_TEARDOWN_COMPLETED="" + trap "bats_debug_trap \"\$BASH_SOURCE\"" debug + trap "bats_error_trap" err + trap "bats_teardown_trap" exit + "$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1 + BATS_TEST_COMPLETED=1 + + else + echo "bats: unknown test name \`$BATS_TEST_NAME'" >&2 + exit 1 + fi +} + +if [ -z "$TMPDIR" ]; then + BATS_TMPDIR="/tmp" +else + BATS_TMPDIR="${TMPDIR%/}" +fi + +BATS_TMPNAME="$BATS_TMPDIR/bats.$$" +BATS_PARENT_TMPNAME="$BATS_TMPDIR/bats.$PPID" +BATS_OUT="${BATS_TMPNAME}.out" + +bats_preprocess_source() { + BATS_TEST_SOURCE="${BATS_TMPNAME}.src" + { tr -d '\r' < "$BATS_TEST_FILENAME"; echo; } | bats-preprocess > "$BATS_TEST_SOURCE" + trap "bats_cleanup_preprocessed_source" err exit + trap "bats_cleanup_preprocessed_source; exit 1" int +} + +bats_cleanup_preprocessed_source() { + rm -f "$BATS_TEST_SOURCE" +} + +bats_evaluate_preprocessed_source() { + if [ -z "$BATS_TEST_SOURCE" ]; then + BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src" + fi + source "$BATS_TEST_SOURCE" +} + +exec 3<&1 + +if [ "$#" -eq 0 ]; then + bats_preprocess_source + bats_evaluate_preprocessed_source + + if [ -n "$BATS_COUNT_ONLY" ]; then + echo "${#BATS_TEST_NAMES[@]}" + else + bats_perform_tests "${BATS_TEST_NAMES[@]}" + fi +else + bats_evaluate_preprocessed_source + bats_perform_test "$@" +fi diff --git a/.github/actions/git-secrets/test/bats/libexec/bats-format-tap-stream b/.github/actions/git-secrets/test/bats/libexec/bats-format-tap-stream new file mode 100755 index 00000000000..614768f4d9e --- /dev/null +++ b/.github/actions/git-secrets/test/bats/libexec/bats-format-tap-stream @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -e + +# Just stream the TAP output (sans extended syntax) if tput is missing +command -v tput >/dev/null || exec grep -v "^begin " + +header_pattern='[0-9]+\.\.[0-9]+' +IFS= read -r header + +if [[ "$header" =~ $header_pattern ]]; then + count="${header:3}" + index=0 + failures=0 + skipped=0 + name="" + count_column_width=$(( ${#count} * 2 + 2 )) +else + # If the first line isn't a TAP plan, print it and pass the rest through + printf "%s\n" "$header" + exec cat +fi + +update_screen_width() { + screen_width="$(tput cols)" + count_column_left=$(( $screen_width - $count_column_width )) +} + +trap update_screen_width WINCH +update_screen_width + +begin() { + go_to_column 0 + printf_with_truncation $(( $count_column_left - 1 )) " %s" "$name" + clear_to_end_of_line + go_to_column $count_column_left + printf "%${#count}s/${count}" "$index" + go_to_column 1 +} + +pass() { + go_to_column 0 + printf " ✓ %s" "$name" + advance +} + +skip() { + local reason="$1" + [ -z "$reason" ] || reason=": $reason" + go_to_column 0 + printf " - %s (skipped%s)" "$name" "$reason" + advance +} + +fail() { + go_to_column 0 + set_color 1 bold + printf " ✗ %s" "$name" + advance +} + +log() { + set_color 1 + printf " %s\n" "$1" + clear_color +} + +summary() { + printf "\n%d test%s" "$count" "$(plural "$count")" + + printf ", %d failure%s" "$failures" "$(plural "$failures")" + + if [ "$skipped" -gt 0 ]; then + printf ", %d skipped" "$skipped" + fi + + printf "\n" +} + +printf_with_truncation() { + local width="$1" + shift + local string="$(printf "$@")" + + if [ "${#string}" -gt "$width" ]; then + printf "%s..." "${string:0:$(( $width - 4 ))}" + else + printf "%s" "$string" + fi +} + +go_to_column() { + local column="$1" + printf "\x1B[%dG" $(( $column + 1 )) +} + +clear_to_end_of_line() { + printf "\x1B[K" +} + +advance() { + clear_to_end_of_line + echo + clear_color +} + +set_color() { + local color="$1" + local weight="$2" + printf "\x1B[%d;%dm" $(( 30 + $color )) "$( [ "$weight" = "bold" ] && echo 1 || echo 22 )" +} + +clear_color() { + printf "\x1B[0m" +} + +plural() { + [ "$1" -eq 1 ] || echo "s" +} + +_buffer="" + +buffer() { + _buffer="${_buffer}$("$@")" +} + +flush() { + printf "%s" "$_buffer" + _buffer="" +} + +finish() { + flush + printf "\n" +} + +trap finish EXIT + +while IFS= read -r line; do + case "$line" in + "begin "* ) + let index+=1 + name="${line#* $index }" + buffer begin + flush + ;; + "ok "* ) + skip_expr="ok $index # skip (\(([^)]*)\))?" + if [[ "$line" =~ $skip_expr ]]; then + let skipped+=1 + buffer skip "${BASH_REMATCH[2]}" + else + buffer pass + fi + ;; + "not ok "* ) + let failures+=1 + buffer fail + ;; + "# "* ) + buffer log "${line:2}" + ;; + esac +done + +buffer summary diff --git a/.github/actions/git-secrets/test/bats/libexec/bats-preprocess b/.github/actions/git-secrets/test/bats/libexec/bats-preprocess new file mode 100755 index 00000000000..04297ed019b --- /dev/null +++ b/.github/actions/git-secrets/test/bats/libexec/bats-preprocess @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -e + +encode_name() { + local name="$1" + local result="test_" + + if [[ ! "$name" =~ [^[:alnum:]\ _-] ]]; then + name="${name//_/-5f}" + name="${name//-/-2d}" + name="${name// /_}" + result+="$name" + else + local length="${#name}" + local char i + + for ((i=0; i "$BATS_TMPDIR/test.txt" + repo_run git-secrets --scan "$BATS_TMPDIR/test.txt" + [ $status -eq 0 ] +} + +@test "Scans all files when no file provided" { + setup_bad_repo + repo_run git-secrets --scan + [ $status -eq 1 ] +} + +@test "Scans all files including history" { + setup_bad_repo + repo_run git-secrets --scan-history + [ $status -eq 1 ] +} + +@test "Scans all files when no file provided with secret in history" { + setup_bad_repo_history + repo_run git-secrets --scan + [ $status -eq 0 ] +} + +@test "Scans all files including history with secret in history" { + setup_bad_repo_history + repo_run git-secrets --scan-history + [ $status -eq 1 ] +} + +@test "Scans history with secrets distributed among branches in history" { + cd $TEST_REPO + echo '@todo' > $TEST_REPO/history_failure.txt + git add -A + git commit -m "Testing history" + echo 'todo' > $TEST_REPO/history_failure.txt + git add -A + git commit -m "Testing history" + git checkout -b testbranch + echo '@todo' > $TEST_REPO/history_failure.txt + git add -A + git commit -m "Testing history" + git checkout master + cd - + repo_run git-secrets --scan-history + [ $status -eq 1 ] +} + +@test "Scans recursively" { + setup_bad_repo + mkdir -p $TEST_REPO/foo/bar/baz + echo '@todo more stuff' > $TEST_REPO/foo/bar/baz/data.txt + repo_run git-secrets --scan -r $TEST_REPO/foo + [ $status -eq 1 ] +} + +@test "Scans recursively only if -r is given" { + setup_bad_repo + mkdir -p $TEST_REPO/foo/bar/baz + echo '@todo more stuff' > $TEST_REPO/foo/bar/baz/data.txt + repo_run git-secrets --scan $TEST_REPO/foo + [ $status -eq 0 ] +} + +@test "Excludes allowed patterns from failures" { + git config --add secrets.patterns 'foo="baz{1,5}"' + git config --add secrets.allowed 'foo="bazzz"' + echo 'foo="bazzz" is ok because 3 "z"s' > "$BATS_TMPDIR/test.txt" + repo_run git-secrets --scan "$BATS_TMPDIR/test.txt" + [ $status -eq 0 ] + echo 'This is NOT: ok foo="bazzzz"' > "$BATS_TMPDIR/test.txt" + repo_run git-secrets --scan "$BATS_TMPDIR/test.txt" + [ $status -eq 1 ] +} + +@test "Prohibited matches exits 1" { + file="$TEST_REPO/test.txt" + echo '@todo stuff' > $file + echo 'this is forbidden right?' >> $file + repo_run git-secrets --scan $file + [ $status -eq 1 ] + [ "${lines[0]}" == "$file:1:@todo stuff" ] + [ "${lines[1]}" == "$file:2:this is forbidden right?" ] +} + +@test "Only matches on word boundaries" { + file="$TEST_REPO/test.txt" + # Note that the following does not match as it is not a word. + echo 'mesa Jar Jar Binks' > $file + # The following do match because they are in word boundaries. + echo 'foo.me' >> $file + echo '"me"' >> $file + repo_run git-secrets --scan $file + [ $status -eq 1 ] + [ "${lines[0]}" == "$file:2:foo.me" ] + [ "${lines[1]}" == "$file:3:\"me\"" ] +} + +@test "Can scan from stdin using -" { + echo "foo" | "${BATS_TEST_DIRNAME}/../git-secrets" --scan - + echo "me" | "${BATS_TEST_DIRNAME}/../git-secrets" --scan - && exit 1 || true +} + +@test "installs hooks for repo" { + setup_bad_repo + repo_run git-secrets --install $TEST_REPO + [ -f $TEST_REPO/.git/hooks/pre-commit ] + [ -f $TEST_REPO/.git/hooks/prepare-commit-msg ] + [ -f $TEST_REPO/.git/hooks/commit-msg ] +} + +@test "fails if hook exists and no -f" { + repo_run git-secrets --install $TEST_REPO + repo_run git-secrets --install $TEST_REPO + [ $status -eq 1 ] +} + +@test "Overwrites hooks if -f is given" { + repo_run git-secrets --install $TEST_REPO + repo_run git-secrets --install -f $TEST_REPO + [ $status -eq 0 ] +} + +@test "installs hooks for repo with Debian style directories" { + setup_bad_repo + mkdir $TEST_REPO/.git/hooks/pre-commit.d + mkdir $TEST_REPO/.git/hooks/prepare-commit-msg.d + mkdir $TEST_REPO/.git/hooks/commit-msg.d + run git-secrets --install $TEST_REPO + [ -f $TEST_REPO/.git/hooks/pre-commit.d/git-secrets ] + [ -f $TEST_REPO/.git/hooks/prepare-commit-msg.d/git-secrets ] + [ -f $TEST_REPO/.git/hooks/commit-msg.d/git-secrets ] +} + +@test "installs hooks to template directory" { + setup_bad_repo + run git-secrets --install $TEMPLATE_DIR + [ $status -eq 0 ] + run git init --template $TEMPLATE_DIR + [ $status -eq 0 ] + [ -f "${TEST_REPO}/.git/hooks/pre-commit" ] + [ -f "${TEST_REPO}/.git/hooks/prepare-commit-msg" ] + [ -f "${TEST_REPO}/.git/hooks/commit-msg" ] +} + +@test "Scans using keys from credentials file" { + echo 'aws_access_key_id = abc123' > $BATS_TMPDIR/test.ini + echo 'aws_secret_access_key=foobaz' >> $BATS_TMPDIR/test.ini + echo 'aws_access_key_id = "Bernard"' >> $BATS_TMPDIR/test.ini + echo 'aws_secret_access_key= "Laverne"' >> $BATS_TMPDIR/test.ini + echo 'aws_access_key_id= Hoagie+man' >> $BATS_TMPDIR/test.ini + cd $TEST_REPO + run git secrets --aws-provider $BATS_TMPDIR/test.ini + [ $status -eq 0 ] + echo "$output" | grep -F "foobaz" + echo "$output" | grep -F "abc123" + echo "$output" | grep -F "Bernard" + echo "$output" | grep -F "Laverne" + echo "$output" | grep -F 'Hoagie\+man' + run git secrets --add-provider -- git secrets --aws-provider $BATS_TMPDIR/test.ini + [ $status -eq 0 ] + echo '(foobaz) test' > $TEST_REPO/bad_file + echo "abc123 test" >> $TEST_REPO/bad_file + echo 'Bernard test' >> $TEST_REPO/bad_file + echo 'Laverne test' >> $TEST_REPO/bad_file + echo 'Hoagie+man test' >> $TEST_REPO/bad_file + repo_run git-secrets --scan $TEST_REPO/bad_file + [ $status -eq 1 ] + echo "$output" | grep "foobaz" + echo "$output" | grep "abc123" + echo "$output" | grep "Bernard" + echo "$output" | grep "Laverne" + echo "$output" | grep -F 'Hoagie+man' +} + +@test "Lists secrets for a repo" { + repo_run git-secrets --list + [ $status -eq 0 ] + echo "$output" | grep -F 'secrets.patterns @todo' + echo "$output" | grep -F 'secrets.patterns forbidden|me' +} + +@test "Adds secrets to a repo and de-dedupes" { + repo_run git-secrets --add 'testing+123' + [ $status -eq 0 ] + repo_run git-secrets --add 'testing+123' + [ $status -eq 1 ] + repo_run git-secrets --add --literal 'testing+abc' + [ $status -eq 0 ] + repo_run git-secrets --add -l 'testing+abc' + [ $status -eq 1 ] + repo_run git-secrets --list + echo "$output" | grep -F 'secrets.patterns @todo' + echo "$output" | grep -F 'secrets.patterns forbidden|me' + echo "$output" | grep -F 'secrets.patterns testing+123' + echo "$output" | grep -F 'secrets.patterns testing\+abc' +} + +@test "Adds allowed patterns to a repo and de-dedupes" { + repo_run git-secrets --add -a 'testing+123' + [ $status -eq 0 ] + repo_run git-secrets --add --allowed 'testing+123' + [ $status -eq 1 ] + repo_run git-secrets --add -a -l 'testing+abc' + [ $status -eq 0 ] + repo_run git-secrets --add -a -l 'testing+abc' + [ $status -eq 1 ] + repo_run git-secrets --list + echo "$output" | grep -F 'secrets.patterns @todo' + echo "$output" | grep -F 'secrets.patterns forbidden|me' + echo "$output" | grep -F 'secrets.allowed testing+123' + echo "$output" | grep -F 'secrets.allowed testing\+abc' +} + +@test "Empty lines must be ignored in .gitallowed files" { + setup_bad_repo + echo '' >> $TEST_REPO/.gitallowed + repo_run git-secrets --scan + [ $status -eq 1 ] +} + +@test "Comment lines must be ignored in .gitallowed files" { + setup_bad_repo_with_hash + repo_run git-secrets --scan + [ $status -eq 1 ] + echo '#hash' > $TEST_REPO/.gitallowed + repo_run git-secrets --scan + [ $status -eq 1 ] + echo 'hash' > $TEST_REPO/.gitallowed + repo_run git-secrets --scan + [ $status -eq 0 ] +} + +@test "Scans all files and allowing none of the bad patterns in .gitallowed" { + setup_bad_repo + echo 'hello' > $TEST_REPO/.gitallowed + repo_run git-secrets --scan + [ $status -eq 1 ] +} + +@test "Scans all files and allowing all bad patterns in .gitallowed" { + setup_bad_repo + echo '@todo' > $TEST_REPO/.gitallowed + echo 'forbidden' >> $TEST_REPO/.gitallowed + echo 'me' >> $TEST_REPO/.gitallowed + repo_run git-secrets --scan + [ $status -eq 0 ] +} + +@test "Adds common AWS patterns" { + repo_run git config --unset-all secrets + repo_run git-secrets --register-aws + git config --local --get secrets.providers + repo_run git-secrets --list + echo "$output" | grep -F '(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}' + echo "$output" | grep "AKIAIOSFODNN7EXAMPLE" + echo "$output" | grep "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" +} + +@test "Adds providers" { + repo_run git-secrets --add-provider -- echo foo baz bar + [ $status -eq 0 ] + repo_run git-secrets --add-provider -- echo bam + [ $status -eq 0 ] + repo_run git-secrets --list + echo "$output" | grep -F 'echo foo baz bar' + echo "$output" | grep -F 'echo bam' + echo 'foo baz bar' > $TEST_REPO/bad_file + echo 'bam' >> $TEST_REPO/bad_file + repo_run git-secrets --scan $TEST_REPO/bad_file + [ $status -eq 1 ] + echo "$output" | grep -F 'foo baz bar' + echo "$output" | grep -F 'bam' +} + +@test "Strips providers that return nothing" { + repo_run git-secrets --add-provider -- 'echo' + [ $status -eq 0 ] + repo_run git-secrets --add-provider -- 'echo 123' + [ $status -eq 0 ] + repo_run git-secrets --list + echo "$output" | grep -F 'echo 123' + echo 'foo' > $TEST_REPO/bad_file + repo_run git-secrets --scan $TEST_REPO/bad_file + [ $status -eq 0 ] +} + +@test "--recursive cannot be used with SCAN_*" { + repo_run git-secrets --scan -r --cached + [ $status -eq 1 ] + repo_run git-secrets --scan -r --no-index + [ $status -eq 1 ] + repo_run git-secrets --scan -r --untracked + [ $status -eq 1 ] +} + +@test "--recursive can be used with --scan" { + repo_run git-secrets --scan -r + [ $status -eq 0 ] +} + +@test "--recursive can't be used with --list" { + repo_run git-secrets --list -r + [ $status -eq 1 ] +} + +@test "-f can only be used with --install" { + repo_run git-secrets --scan -f + [ $status -eq 1 ] +} + +@test "-a can only be used with --add" { + repo_run git-secrets --scan -a + [ $status -eq 1 ] +} + +@test "-l can only be used with --add" { + repo_run git-secrets --scan -l + [ $status -eq 1 ] +} + +@test "--cached can only be used with --scan" { + repo_run git-secrets --list --cached + [ $status -eq 1 ] +} + +@test "--no-index can only be used with --scan" { + repo_run git-secrets --list --no-index + [ $status -eq 1 ] +} + +@test "--untracked can only be used with --scan" { + repo_run git-secrets --list --untracked + [ $status -eq 1 ] +} diff --git a/.github/actions/git-secrets/test/pre-commit.bats b/.github/actions/git-secrets/test/pre-commit.bats new file mode 100644 index 00000000000..5ace267cbb3 --- /dev/null +++ b/.github/actions/git-secrets/test/pre-commit.bats @@ -0,0 +1,62 @@ +#!/usr/bin/env bats +load test_helper + +@test "Rejects commits with prohibited patterns in changeset" { + setup_bad_repo + repo_run git-secrets --install $TEST_REPO + cd $TEST_REPO + run git commit -m 'Contents are bad not the message' + [ $status -eq 1 ] + [ "${lines[0]}" == "data.txt:1:@todo more stuff" ] + [ "${lines[1]}" == "failure1.txt:1:another line... forbidden" ] + [ "${lines[2]}" == "failure2.txt:1:me" ] +} + +@test "Rejects commits with prohibited patterns in changeset with filename that contain spaces" { + setup_bad_repo_with_spaces + repo_run git-secrets --install $TEST_REPO + cd $TEST_REPO + run git commit -m 'Contents are bad not the message' + [ $status -eq 1 ] + [ "${lines[0]}" == "da ta.txt:1:@todo more stuff" ] +} + +@test "Scans staged files" { + cd $TEST_REPO + repo_run git-secrets --install $TEST_REPO + echo '@todo more stuff' > $TEST_REPO/data.txt + echo 'hi there' > $TEST_REPO/ok.txt + git add -A + echo 'fixed the working directory, but not staged' > $TEST_REPO/data.txt + run git commit -m 'Contents are bad not the message' + [ $status -eq 1 ] + [ "${lines[0]}" == "data.txt:1:@todo more stuff" ] +} + +@test "Allows commits that do not match prohibited patterns" { + setup_good_repo + repo_run git-secrets --install $TEST_REPO + cd $TEST_REPO + run git commit -m 'This is fine' + [ $status -eq 0 ] + # Ensure deleted files are filtered out of the grep + rm $TEST_REPO/data.txt + echo 'aaa' > $TEST_REPO/data_2.txt + run git add -A + run git commit -m 'This is also fine' + [ $status -eq 0 ] +} + +@test "Rejects commits with prohibited patterns in changeset when AWS provider is enabled" { + setup_bad_repo + repo_run git-secrets --install $TEST_REPO + repo_run git-secrets --register-aws $TEST_REPO + cd $TEST_REPO + run git commit -m 'Contents are bad not the message' + [ $status -eq 1 ] + echo "${lines}" | grep -vq 'git secrets --aws-provider: command not found' + + [ "${lines[0]}" == "data.txt:1:@todo more stuff" ] + [ "${lines[1]}" == "failure1.txt:1:another line... forbidden" ] + [ "${lines[2]}" == "failure2.txt:1:me" ] +} diff --git a/.github/actions/git-secrets/test/prepare-commit-msg.bats b/.github/actions/git-secrets/test/prepare-commit-msg.bats new file mode 100644 index 00000000000..a211c1318a2 --- /dev/null +++ b/.github/actions/git-secrets/test/prepare-commit-msg.bats @@ -0,0 +1,33 @@ +#!/usr/bin/env bats +load test_helper + +@test "Rejects merges with prohibited patterns in history" { + setup_good_repo + repo_run git-secrets --install $TEST_REPO + cd $TEST_REPO + git commit -m 'OK' + git checkout -b feature + echo '@todo' > data.txt + git add -A + git commit -m 'Bad commit' --no-verify + echo 'Fixing!' > data.txt + git add -A + git commit -m 'Fixing commit' + git checkout master + run git merge --no-ff feature + [ $status -eq 1 ] +} + +@test "Allows merges that do not match prohibited patterns" { + setup_good_repo + cd $TEST_REPO + repo_run git-secrets --install + git commit -m 'OK' + git checkout -b feature + echo 'Not bad' > data.txt + git add -A + git commit -m 'Good commit' + git checkout master + run git merge --no-ff feature + [ $status -eq 0 ] +} diff --git a/.github/actions/git-secrets/test/test_helper.bash b/.github/actions/git-secrets/test/test_helper.bash new file mode 100644 index 00000000000..9133e5162ec --- /dev/null +++ b/.github/actions/git-secrets/test/test_helper.bash @@ -0,0 +1,94 @@ +#!/bin/bash +export TEST_REPO="$BATS_TMPDIR/test-repo" +export TEMP_HOME="$BATS_TMPDIR/home" +export TEMPLATE_DIR="${BATS_TMPDIR}/template" +INITIAL_PATH="${PATH}" +INITIAL_HOME=${HOME} + +setup() { + setup_repo + [ -d "${TEMPLATE_DIR}" ] && rm -rf "${TEMPLATE_DIR}" + [ -d "${TEMP_HOME}" ] && rm -rf "${TEMP_HOME}" + mkdir -p $TEMP_HOME + export HOME=$TEMP_HOME + export PATH="${BATS_TEST_DIRNAME}/..:${INITIAL_PATH}" + cd $TEST_REPO +} + +teardown() { + delete_repo + export PATH="${INITIAL_PATH}" + export HOME="${INITIAL_HOME}" + [ -d "${TEMP_HOME}" ] && rm -rf "${TEMP_HOME}" +} + +delete_repo() { + [ -d $TEST_REPO ] && rm -rf $TEST_REPO || true +} + +setup_repo() { + delete_repo + mkdir -p $TEST_REPO + cd $TEST_REPO + git init + git config --local --add secrets.patterns '@todo' + git config --local --add secrets.patterns 'forbidden|me' + git config --local --add secrets.patterns '#hash' + git config --local user.email "you@example.com" + git config --local user.name "Your Name" + cd - +} + +repo_run() { + cmd="$1" + shift + cd "${TEST_REPO}" + run "${BATS_TEST_DIRNAME}/../${cmd}" $@ + cd - +} + +# Creates a repo that should fail +setup_bad_repo() { + cd $TEST_REPO + echo '@todo more stuff' > $TEST_REPO/data.txt + echo 'hi there' > $TEST_REPO/ok.txt + echo 'another line... forbidden' > $TEST_REPO/failure1.txt + echo 'me' > $TEST_REPO/failure2.txt + git add -A + cd - +} + +# Creates a repo that should fail +setup_bad_repo_with_spaces() { + cd $TEST_REPO + echo '@todo more stuff' > "$TEST_REPO/da ta.txt" + git add -A + cd - +} + +# Creates a repo that should fail +setup_bad_repo_with_hash() { + cd $TEST_REPO + echo '#hash' > "$TEST_REPO/data.txt" + git add -A + cd - +} + +# Creates a repo that should fail +setup_bad_repo_history() { + cd $TEST_REPO + echo '@todo' > $TEST_REPO/history_failure.txt + git add -A + git commit -m "Testing history" + echo 'todo' > $TEST_REPO/history_failure.txt + git add -A + cd - +} + +# Creates a repo that does not fail +setup_good_repo() { + cd $TEST_REPO + echo 'hello!' > $TEST_REPO/data.txt + git add -A + cd - +} diff --git a/.github/actions/reliable-pull-request/LICENSE b/.github/actions/reliable-pull-request/LICENSE new file mode 100644 index 00000000000..ade79f7960a --- /dev/null +++ b/.github/actions/reliable-pull-request/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Josiah Siegel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.github/actions/reliable-pull-request/README.md b/.github/actions/reliable-pull-request/README.md new file mode 100644 index 00000000000..52650c0c493 --- /dev/null +++ b/.github/actions/reliable-pull-request/README.md @@ -0,0 +1,83 @@ +# Reliable* Pull Request Action + +> *Only uses built-in GitHub runner commands + +[![Test Action](https://github.com/CDCgov/prime-reportstream/.github/workflows/reliable-pull-request--test-action.yml/badge.svg)](https://github.com/CDCgov/prime-reportstream/.github/workflows/reliable-pull-request--test-action.yml) + +## Synopsis + +1. Create a pull request on a GitHub repository using existing branches. +2. [actions/checkout](https://github.com/actions/checkout) determins the active repo. + +## Usage + +```yml +jobs: + create-pr: + name: Test create PR on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + steps: + - name: Checkout the repo + uses: actions/checkout@v4.1.1 + + - name: Create Pull Request + id: create_pr + uses: CDCgov/prime-reportstream/.github/actions/reliable-pull-request@ae8d0c88126329ee363a35392793d0bc94cb82e7 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + title: 'Automated Pull Request' + sourceBranch: ${{ github.ref_name }} + targetBranch: 'main' + body: 'This is an automated pull request.' + labels: 'automated,pr' + assignees: 'octocat' + + - name: Output PR URL + run: echo "The PR URL is ${{ steps.create_pr.outputs.PRURL }}" +``` + +## Inputs + +```yml +inputs: + title: + description: 'Pull Request Title' + required: true + sourceBranch: + description: 'Source Branch Name' + required: true + targetBranch: + description: 'Target Branch Name' + required: true + body: + description: 'Pull Request Body' + required: false + labels: + description: 'Labels (comma-separated)' + required: false + assignees: + description: 'Assignees (comma-separated)' + required: false +``` + +## Outputs +```yml +outputs: + PRURL: + description: 'The URL of the created pull request' +``` + +## Requirements + +The following permissions must be set for the repository: + * `Settings > Actions > General` + * Workflow permissions + 1. Read and write permissions + 2. Allow GitHub Actions to create and approve pull requests + 3. Save + +>*Alternative is to set [jobs..permissions](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idpermissions)* diff --git a/.github/actions/reliable-pull-request/action.yml b/.github/actions/reliable-pull-request/action.yml new file mode 100644 index 00000000000..4fcda57b318 --- /dev/null +++ b/.github/actions/reliable-pull-request/action.yml @@ -0,0 +1,42 @@ +name: Reliable Pull Request Action +description: Creates a pull request on a GitHub repository using existing branches +branding: + icon: 'git-pull-request' + color: 'blue' +inputs: + title: + description: 'Pull Request Title' + required: true + sourceBranch: + description: 'Source Branch Name' + required: true + targetBranch: + description: 'Target Branch Name' + required: true + body: + description: 'Pull Request Body' + required: false + labels: + description: 'Labels (comma-separated)' + required: false + assignees: + description: 'Assignees (comma-separated)' + required: false +outputs: + PRURL: + description: 'The URL of the created pull request' + value: ${{ steps.create_pr.outputs.PR_URL }} +runs: + using: 'composite' + steps: + - name: Create Pull Request + id: create_pr + shell: bash + run: bash ${{github.action_path}}/create-pr.sh + env: + INPUT_TITLE: ${{ inputs.title }} + INPUT_SOURCEBRANCH: ${{ inputs.sourceBranch }} + INPUT_TARGETBRANCH: ${{ inputs.targetBranch }} + INPUT_BODY: ${{ inputs.body }} + INPUT_LABELS: ${{ inputs.labels }} + INPUT_ASSIGNEES: ${{ inputs.assignees }} diff --git a/.github/actions/reliable-pull-request/create-pr.sh b/.github/actions/reliable-pull-request/create-pr.sh new file mode 100644 index 00000000000..c6046c0582d --- /dev/null +++ b/.github/actions/reliable-pull-request/create-pr.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Create Pull Request and capture the output +PR_OUTPUT=$(gh pr create \ + --title "$INPUT_TITLE" \ + --body "$INPUT_BODY" \ + --base "$INPUT_TARGETBRANCH" \ + --head "$INPUT_SOURCEBRANCH" \ + --label "$INPUT_LABELS" \ + --assignee "$INPUT_ASSIGNEES" 2>&1) + +# Extract PR URL from the output +PR_URL=$(echo "$PR_OUTPUT" | grep -o 'https://github.com/[^ ]*') + +# Set the PR URL as the output +echo "PR_URL=$PR_URL" >> $GITHUB_OUTPUT diff --git a/.github/actions/slack-boltjs-app/.dockerignore b/.github/actions/slack-boltjs-app/.dockerignore new file mode 100644 index 00000000000..2eea525d885 --- /dev/null +++ b/.github/actions/slack-boltjs-app/.dockerignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/.github/actions/slack-boltjs-app/.env.example b/.github/actions/slack-boltjs-app/.env.example new file mode 100644 index 00000000000..6f435a07baf --- /dev/null +++ b/.github/actions/slack-boltjs-app/.env.example @@ -0,0 +1,10 @@ +# App-Level Token w/connections:write +SLACK_APP_TOKEN= +# Bot User OAuth Token under the OAuth & Permissions sidebar +SLACK_BOT_TOKEN= +# https://github.com/settings/tokens +GITHUB_TOKEN= +# Only pushes to this repo if specified +GITHUB_REPO= +# Only pushes are permitted to these branches +GITHUB_TARGET_BRANCHES= \ No newline at end of file diff --git a/.github/actions/slack-boltjs-app/.eslintignore b/.github/actions/slack-boltjs-app/.eslintignore new file mode 100644 index 00000000000..3c3629e647f --- /dev/null +++ b/.github/actions/slack-boltjs-app/.eslintignore @@ -0,0 +1 @@ +node_modules diff --git a/.github/actions/slack-boltjs-app/.eslintrc.cjs b/.github/actions/slack-boltjs-app/.eslintrc.cjs new file mode 100644 index 00000000000..7a28658ef87 --- /dev/null +++ b/.github/actions/slack-boltjs-app/.eslintrc.cjs @@ -0,0 +1,15 @@ +module.exports = { + env: { + browser: false, + es2021: true, + }, + extends: ["eslint:recommended", "plugin:node/recommended"], + rules: { + "node/no-missing-import": [ + "error", + { + allowModules: ["@slack/bolt"], + }, + ], + }, +}; diff --git a/.github/actions/slack-boltjs-app/.gitignore b/.github/actions/slack-boltjs-app/.gitignore new file mode 100644 index 00000000000..1cda3b10143 --- /dev/null +++ b/.github/actions/slack-boltjs-app/.gitignore @@ -0,0 +1,7 @@ +.env +.help +node_modules +.git +_git +.github +_github diff --git a/.github/actions/slack-boltjs-app/.glitch-assets b/.github/actions/slack-boltjs-app/.glitch-assets new file mode 100644 index 00000000000..954626f61ff --- /dev/null +++ b/.github/actions/slack-boltjs-app/.glitch-assets @@ -0,0 +1,6 @@ +{"name":"drag-in-files.svg","date":"2016-10-22T16:17:49.954Z","url":"https://cdn.hyperdev.com/drag-in-files.svg","type":"image/svg","size":7646,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/drag-in-files.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(102, 153, 205)","uuid":"adSBq97hhhpFNUna"} +{"name":"click-me.svg","date":"2016-10-23T16:17:49.954Z","url":"https://cdn.hyperdev.com/click-me.svg","type":"image/svg","size":7116,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/click-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(243, 185, 186)","uuid":"adSBq97hhhpFNUnb"} +{"name":"paste-me.svg","date":"2016-10-24T16:17:49.954Z","url":"https://cdn.hyperdev.com/paste-me.svg","type":"image/svg","size":7242,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/paste-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(42, 179, 185)","uuid":"adSBq97hhhpFNUnc"} +{"uuid":"adSBq97hhhpFNUna","deleted":true} +{"uuid":"adSBq97hhhpFNUnb","deleted":true} +{"uuid":"adSBq97hhhpFNUnc","deleted":true} diff --git a/.github/actions/slack-boltjs-app/.help.example b/.github/actions/slack-boltjs-app/.help.example new file mode 100644 index 00000000000..b24e6e72302 --- /dev/null +++ b/.github/actions/slack-boltjs-app/.help.example @@ -0,0 +1,32 @@ +USAGE + [<@bot>] gh-deploy [] to [] [OPTIONAL: for ] + +EXAMPLES + @MyBot gh-deploy master to feature3 + @MyBot gh-deploy feature1 to feature3 for my/awesome_repo + +========================================================================== + +USAGE + [<@bot>] gh-lock [add|remove|show] [] + +EXAMPLES + @MyBot gh-lock add feature3 + @MyBot gh-lock remove feature1 + +========================================================================== + +USAGE + [<@bot>] gh-run [] [OPTIONAL: ] [OPTIONAL: --inputs ] + +EXAMPLES + @MyBot gh-run log_management.yml --inputs name:alpha,desc:uat env + @MyBot gh-run log_management.yml my/awesome_repo main --inputs name:alpha,desc:uat env + +========================================================================== + +USAGE + [<@bot>] gh-targets + +EXAMPLES + @bot gh-targets diff --git a/.github/actions/slack-boltjs-app/Dockerfile.example b/.github/actions/slack-boltjs-app/Dockerfile.example new file mode 100644 index 00000000000..0bc2f1257f9 --- /dev/null +++ b/.github/actions/slack-boltjs-app/Dockerfile.example @@ -0,0 +1,11 @@ +FROM node:20-bookworm-slim + +WORKDIR /usr/app +COPY --chown=node:node ./ ./ +RUN mkdir -p ./src/.locks && chown node ./src/.locks +VOLUME /usr/app/src/.locks + +RUN npm install +USER node + +CMD ["npm", "start"] diff --git a/.github/actions/slack-boltjs-app/Makefile b/.github/actions/slack-boltjs-app/Makefile new file mode 100644 index 00000000000..032201f6d6d --- /dev/null +++ b/.github/actions/slack-boltjs-app/Makefile @@ -0,0 +1,10 @@ +default: start + +start: build + docker stop slack-boltjs-app || true + docker rm slack-boltjs-app || true + docker run --name=slack-boltjs-app --env-file .env \ + --volume gh_locks:/usr/app/src/.locks -d slack_boltjs_app + +build: + docker build -t slack_boltjs_app -f Dockerfile.example . \ No newline at end of file diff --git a/.github/actions/slack-boltjs-app/README.md b/.github/actions/slack-boltjs-app/README.md new file mode 100644 index 00000000000..8eda2990df4 --- /dev/null +++ b/.github/actions/slack-boltjs-app/README.md @@ -0,0 +1,83 @@ +# Slack-Boltjs-App + +[![CodeQL](https://github.com/CDCgov/prime-reportstream/.github/workflows/slack-boltjs-app--codeql.yml/badge.svg)](https://github.com/JosiahSiegel/slack-bolt/actions/workflows/codeql.yml) +[![njsscan sarif](https://github.com/CDCgov/prime-reportstream/.github/workflows/slack-boltjs-app--njsscan.yml/badge.svg)](https://github.com/JosiahSiegel/slack-bolt/actions/workflows/njsscan.yml) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/3f3ee15eaf3f4c81915d658008e01c3f)](https://app.codacy.com/gh/CDCgov/prime-reportstream/.github/actions/slack-boltjs-app/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) + +![](https://avatars.slack-edge.com/2023-04-24/5159910288243_7af56ae264408f296381_128.png) + +Remix on Glitch + +## A secure and simple Boltjs app for Slack ChatOps + +> App includes basic GitHub push functionality to get you started + +### Quickstart +* `cp .env.example .env` + * Update `.env` ⚠️(required)⚠️ +* `cp .help.example .help` + * Update `.help` (optional) +* `npm install` +* `npm start` + +### Helpful links + +* [Bolt getting started guide](https://api.slack.com/start/building/bolt) +* [Bolt documentation](https://slack.dev/bolt) +* [Slack app home](https://api.slack.com/apps) + +### Example Slack app manifest + +```yml +display_information: + name: Bolt DevBot +features: + app_home: + home_tab_enabled: true + messages_tab_enabled: false + messages_tab_read_only_enabled: false + bot_user: + display_name: DevBot + always_online: true +oauth_config: + scopes: + bot: + - app_mentions:read + - calls:write + - channels:read + - chat:write + - commands + - channels:history + - reactions:read +settings: + event_subscriptions: + bot_events: + - app_home_opened + - app_mention + - message.channels + - reaction_added + interactivity: + is_enabled: true + org_deploy_enabled: false + socket_mode_enabled: true + token_rotation_enabled: false +``` + +Tips: + +* For [external workspace users][1], add an `app.message` per `app.command`. +* Check vulnerabilities: `npm audit` +* Fix Glitch out of sync with repo: + * `git pull` + * `refresh` +* Hard refresh Glitch from repo: + * `git fetch --all` + * `git reset --hard origin/main` + * `refresh` + +--- + +[Glitch](https://glitch.com/~slack-boltjs-app) +[GitHub](https://github.com/CDCgov/prime-reportstream/.github/actions/slack-boltjs-app) + +[1]: https://slack.com/help/articles/115004151203-Slack-Connect-guide--work-with-external-organizations diff --git a/.github/actions/slack-boltjs-app/npm-shrinkwrap.json b/.github/actions/slack-boltjs-app/npm-shrinkwrap.json new file mode 100644 index 00000000000..aa05bfa7ddf --- /dev/null +++ b/.github/actions/slack-boltjs-app/npm-shrinkwrap.json @@ -0,0 +1,4263 @@ +{ + "name": "slack-bolt", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "slack-bolt", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@slack/bolt": "^3.18.0", + "dotenv": "^16", + "node-localstorage": "^3.0.5" + }, + "devDependencies": { + "eslint": "9.3.0", + "eslint-config-eslint": "^10.0.0" + }, + "engines": { + "node": "^20", + "npm": "^10" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/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/@babel/highlight/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==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/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==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.42.0.tgz", + "integrity": "sha512-R1w57YlVA6+YE01wch3GPYn6bCsrOV3YW/5oGGE2tmX6JcL9Nr+b5IikrjMPF+v9CV3ay+obImEdsDhovhJrzw==", + "dev": true, + "dependencies": { + "comment-parser": "1.4.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@eslint-community/eslint-plugin-eslint-comments": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.3.0.tgz", + "integrity": "sha512-6e93KtgsndNkvwCCa07LOQJSwzzLLxwrFll3+huyFoiiQXWG0KBcmo0Q1bVgYQQDLfWOOZl2VPBsXqZL6vHIBQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^4.0.0", + "ignore": "^5.2.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.3.0.tgz", + "integrity": "sha512-niBqk8iwv96+yuTwjM6bWg8ovzAPF9qkICsGtcoa5/dmqcEMfdwNAX7+/OHcJHc7wj7XqPxH98oAHytFYlw6Sw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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==", + "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/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==", + "dev": true + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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/@slack/bolt": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/@slack/bolt/-/bolt-3.18.0.tgz", + "integrity": "sha512-A7bDi5kY50fS6/nsmURkQdO3iMxD8aX/rA+m1UXEM2ue2z4KijeQtx2sOZ4YkJQ/h7BsgTQM0CYh3qqmo+m5sQ==", + "dependencies": { + "@slack/logger": "^4.0.0", + "@slack/oauth": "^2.6.2", + "@slack/socket-mode": "^1.3.3", + "@slack/types": "^2.11.0", + "@slack/web-api": "^6.11.2", + "@types/express": "^4.16.1", + "@types/promise.allsettled": "^1.0.3", + "@types/tsscmp": "^1.0.0", + "axios": "^1.6.0", + "express": "^4.16.4", + "path-to-regexp": "^6.2.1", + "please-upgrade-node": "^3.2.0", + "promise.allsettled": "^1.0.2", + "raw-body": "^2.3.3", + "tsscmp": "^1.0.6" + }, + "engines": { + "node": ">=12.13.0", + "npm": ">=6.12.0" + } + }, + "node_modules/@slack/logger": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz", + "integrity": "sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==", + "dependencies": { + "@types/node": ">=18.0.0" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/oauth": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@slack/oauth/-/oauth-2.6.2.tgz", + "integrity": "sha512-2R3MyB/R63hTRXzk5J6wcui59TBxXzhk+Uh2/Xu3Wp3O4pXg/BNucQhP/DQbL/ScVhLvFtMXirLrKi0Yo5gIVw==", + "dependencies": { + "@slack/logger": "^3.0.0", + "@slack/web-api": "^6.11.2", + "@types/jsonwebtoken": "^8.3.7", + "@types/node": ">=12", + "jsonwebtoken": "^9.0.0", + "lodash.isstring": "^4.0.1" + }, + "engines": { + "node": ">=12.13.0", + "npm": ">=6.12.0" + } + }, + "node_modules/@slack/oauth/node_modules/@slack/logger": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-3.0.0.tgz", + "integrity": "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA==", + "dependencies": { + "@types/node": ">=12.0.0" + }, + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/socket-mode": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@slack/socket-mode/-/socket-mode-1.3.3.tgz", + "integrity": "sha512-vN3zG4woRtf2Ut6rZgRW6G/Oe56uLMlnz39I08Q7DOvVfB+1MmDbNv0PNOiFgujdKXJR+bXF41/F/VvryXcqlw==", + "dependencies": { + "@slack/logger": "^3.0.0", + "@slack/web-api": "^6.11.2", + "@types/node": ">=12.0.0", + "@types/p-queue": "^2.3.2", + "@types/ws": "^7.4.7", + "eventemitter3": "^3.1.0", + "finity": "^0.5.4", + "p-cancelable": "^1.1.0", + "p-queue": "^2.4.2", + "ws": "^7.5.3" + }, + "engines": { + "node": ">=12.13.0", + "npm": ">=6.12.0" + } + }, + "node_modules/@slack/socket-mode/node_modules/@slack/logger": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-3.0.0.tgz", + "integrity": "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA==", + "dependencies": { + "@types/node": ">=12.0.0" + }, + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/types": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.11.0.tgz", + "integrity": "sha512-UlIrDWvuLaDly3QZhCPnwUSI/KYmV1N9LyhuH6EDKCRS1HWZhyTG3Ja46T3D0rYfqdltKYFXbJSSRPwZpwO0cQ==", + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-6.12.0.tgz", + "integrity": "sha512-RPw6F8rWfGveGkZEJ4+4jUin5iazxRK2q3FpQDz/FvdgzC3nZmPyLx8WRzc6nh0w3MBjEbphNnp2VZksfhpBIQ==", + "dependencies": { + "@slack/logger": "^3.0.0", + "@slack/types": "^2.11.0", + "@types/is-stream": "^1.1.0", + "@types/node": ">=12.0.0", + "axios": "^1.6.5", + "eventemitter3": "^3.1.0", + "form-data": "^2.5.0", + "is-electron": "2.2.2", + "is-stream": "^1.1.0", + "p-queue": "^6.6.1", + "p-retry": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api/node_modules/@slack/logger": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-3.0.0.tgz", + "integrity": "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA==", + "dependencies": { + "@types/node": ">=12.0.0" + }, + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api/node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@slack/web-api/node_modules/p-queue/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "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==", + "dependencies": { + "@types/connect": "*", + "@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==", + "dependencies": { + "@types/node": "*" + } + }, + "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==", + "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": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "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==" + }, + "node_modules/@types/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", + "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==", + "dependencies": { + "@types/node": "*" + } + }, + "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==" + }, + "node_modules/@types/node": { + "version": "20.11.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.23.tgz", + "integrity": "sha512-ZUarKKfQuRILSNYt32FuPL20HS7XwNT7/uRwSV8tiHWfyyVwDLYZNF6DZKc2bove++pgfsXn9sUwII/OsQ82cQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/@types/p-queue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/p-queue/-/p-queue-2.3.2.tgz", + "integrity": "sha512-eKAv5Ql6k78dh3ULCsSBxX6bFNuGjTmof5Q/T6PiECDq0Yf8IIn46jCyp3RJvCi8owaEmm3DZH1PEImjBMd/vQ==" + }, + "node_modules/@types/promise.allsettled": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/promise.allsettled/-/promise.allsettled-1.0.6.tgz", + "integrity": "sha512-wA0UT0HeT2fGHzIFV9kWpYz5mdoyLxKrTgMdZQM++5h6pYAFH73HXcQhefg24nD1yivUFEn5KU+EF4b+CXJ4Wg==" + }, + "node_modules/@types/qs": { + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.12.tgz", + "integrity": "sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==" + }, + "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==" + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/tsscmp": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/tsscmp/-/tsscmp-1.0.2.tgz", + "integrity": "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g==" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": { + "@types/node": "*" + } + }, + "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==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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/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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "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/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==" + }, + "node_modules/array.prototype.map": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.6.tgz", + "integrity": "sha512-nK1psgF2cXqP3wSyCSq0Hc7zwNq3sfljQqaG27r/7a7ooNUnn5nGq6yYWyks9jMO5EoFQ0ax80hSg6oXSRNXaw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "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.11.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==", + "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==" + }, + "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/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "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.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "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.30001614", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001614.tgz", + "integrity": "sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog==", + "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/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/ci-info": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/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/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==", + "dev": true, + "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==", + "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==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, + "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/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==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "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==" + }, + "node_modules/core-js-compat": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.0.tgz", + "integrity": "sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "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/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==", + "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-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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==" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.751", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.751.tgz", + "integrity": "sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw==", + "dev": true + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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-abstract": { + "version": "1.22.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz", + "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.1", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.0", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.5", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "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==", + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "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==" + }, + "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": "9.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.3.0.tgz", + "integrity": "sha512-5Iv4CsZW030lpUqHBapdPo3MJetAPtejVW8B84GIcIIv8+ohFaddXsrn1Gn8uD9ijDb+kcYKFUVmC8qG8B2ORQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.3.0", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.1", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.0.tgz", + "integrity": "sha512-dc6Y8tzEcSYZMHa+CMPLi/hyo1FzNeonbhJL7Ol0ccuKQkwopJcJBA9YL/xmMTLU1eKigXo9vj9nALElWYSowg==", + "dev": true, + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-eslint": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-eslint/-/eslint-config-eslint-10.0.0.tgz", + "integrity": "sha512-ejGeXkQLyAEqUBr6UE246sZBRqscQVuJOTuYLYIt+GaCrfRodLsoJ6/oXRHZumcLDbZYnZoXRpgYy8+NJ/UOOw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.3.0", + "@eslint/js": "^9.0.0", + "eslint-plugin-jsdoc": "^48.2.3", + "eslint-plugin-n": "^17.2.0", + "eslint-plugin-unicorn": "^52.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint-plugin-es-x": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.6.0.tgz", + "integrity": "sha512-I0AmeNgevgaTR7y2lrVCJmGYF0rjoznpDvqV/kIkZSZbZ8Rw3eu4cGlvBBULScfkSOCzqKbff5LR4CNrV7mZHA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.6.0", + "eslint-compat-utils": "^0.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "48.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.2.3.tgz", + "integrity": "sha512-r9DMAmFs66VNvNqRLLjHejdnJtILrt3xGi+Qx0op0oRfFGVpOR1Hb3BC++MacseHx93d8SKYPhyrC9BS7Os2QA==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.42.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.6.0", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-n": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.3.1.tgz", + "integrity": "sha512-25+HTtKe1F8U/M4ERmdzbz/xkm/gaY0OYC8Fcv1z/WvpLJ8Xfh9LzJ13JV5uj4QyCUD8kOPJrNjn/3y+tc57Vw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "enhanced-resolve": "^5.15.0", + "eslint-plugin-es-x": "^7.5.0", + "get-tsconfig": "^4.7.0", + "globals": "^15.0.0", + "ignore": "^5.2.4", + "minimatch": "^9.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": ">=8.23.0" + } + }, + "node_modules/eslint-plugin-n/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/eslint-plugin-n/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "52.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz", + "integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "@eslint-community/eslint-utils": "^4.4.0", + "@eslint/eslintrc": "^2.1.4", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.34.0", + "esquery": "^1.5.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.5.4", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=8.56.0" + } + }, + "node_modules/eslint-plugin-unicorn/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-plugin-unicorn/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/eslint-plugin-unicorn/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-plugin-unicorn/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-scope": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", + "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/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", + "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "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==" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "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-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/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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "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==" + }, + "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/finity": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/finity/-/finity-0.5.4.tgz", + "integrity": "sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA==" + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz", + "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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/globals": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.1.0.tgz", + "integrity": "sha512-926gJqg+4mkxwYKiFvoomM4J0kWESfk3qfTvRL2/oc/tK/eTDBbrfcKnSa2KtfdxB5onoL7D3A3qIHQFpd4+UA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "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/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "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==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "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==", + "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/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "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==", + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==" + }, + "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-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-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "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/iterate-iterator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.2.tgz", + "integrity": "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", + "dependencies": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "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/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": "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/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/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/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "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/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/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/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.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "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==" + }, + "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/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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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/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/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/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-localstorage": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-3.0.5.tgz", + "integrity": "sha512-GCwtK33iwVXboZWYcqQHu3aRvXEBwmPkAMRBLeaX86ufhqslyUkLGsi4aW3INEfdQYpUB5M9qtYf3eHvAk2VBg==", + "dependencies": { + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/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, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, + "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-queue": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-2.4.2.tgz", + "integrity": "sha512-n8/y+yDJwBjoLQe1GSJbbaYQLTI7QHNZI2+rpmCDbe++WLf9HC3gf6iqj5yfPAV71W4UF3ql5W1+UBPXoXTxng==", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "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-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-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, + "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/promise.allsettled": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.7.tgz", + "integrity": "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA==", + "dependencies": { + "array.prototype.map": "^1.0.5", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "iterate-value": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "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/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "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/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==", + "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==", + "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/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regjsparser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", + "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "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-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "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/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/safe-array-concat": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", + "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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==", + "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==" + }, + "node_modules/send/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==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "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==" + }, + "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/side-channel": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "dependencies": { + "call-bind": "^1.0.6", + "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==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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-correct/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-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": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/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/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/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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, + "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.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz", + "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "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.1.1", + "picocolors": "^1.0.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/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==", + "engines": { + "node": ">= 0.4.0" + } + }, + "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-license/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/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "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/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", + "dependencies": { + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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/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" + } + } + } +} diff --git a/.github/actions/slack-boltjs-app/package.json b/.github/actions/slack-boltjs-app/package.json new file mode 100644 index 00000000000..1017bc2de5b --- /dev/null +++ b/.github/actions/slack-boltjs-app/package.json @@ -0,0 +1,27 @@ +{ + "name": "slack-bolt", + "version": "1.0.0", + "description": "A secure and simple Bolt app for Slack ChatOps", + "main": "src/index.js", + "type": "module", + "scripts": { + "start": "node -r dotenv/config src/index.js" + }, + "dependencies": { + "dotenv": "^16", + "@slack/bolt": "^3.18.0", + "node-localstorage": "^3.0.5" + }, + "engines": { + "node": "^20", + "npm": "^10" + }, + "repository": { + "url": "https://github.com/CDCgov/prime-reportstream/.github/actions/slack-boltjs-app" + }, + "license": "MIT", + "devDependencies": { + "eslint": "9.3.0", + "eslint-config-eslint": "^10.0.0" + } +} diff --git a/.github/actions/slack-boltjs-app/src/index.js b/.github/actions/slack-boltjs-app/src/index.js new file mode 100644 index 00000000000..a40aabce36a --- /dev/null +++ b/.github/actions/slack-boltjs-app/src/index.js @@ -0,0 +1,47 @@ +import boltjs from "@slack/bolt"; +const { App, directMention } = boltjs; +import fs from "fs"; +import ghRouter from "./utils/github/router.js"; +import appHome from "./views/app_home.js"; + +const app = new App({ + token: process.env.SLACK_BOT_TOKEN, + appToken: process.env.SLACK_APP_TOKEN, + socketMode: true, +}); + +// Use a single message listener with a switch statement to handle different commands +app.message(directMention(), async ({ message, say }) => { + const command = message.text.split(" ")[1].split("-")[0]; + switch (command) { + case ":wave:": + await say(`Hello, <@${message.user}>`); + break; + case "gh": + await ghRouter({ message, say }); + break; + case "help": + const filename = ".help"; + const data = fs.readFileSync(filename, "utf8"); + say(data); + break; + default: + // Handle unknown commands + say( + `Sorry, I don't recognize that command. Try typing \`@bot help\` for more information.` + ); + } +}); + +// Listen for users opening App Home +app.event("app_home_opened", async ({ event, client }) => { + appHome({ event, client }); +}); + +(async () => { + const port = process.env.PORT || 3000; + + // Start your app + await app.start(port); + console.log(`⚡️ Slack Bolt app is running on port ${port}!`); +})(); diff --git a/.github/actions/slack-boltjs-app/src/utils/github/default_branch.js b/.github/actions/slack-boltjs-app/src/utils/github/default_branch.js new file mode 100644 index 00000000000..7ba49a0a476 --- /dev/null +++ b/.github/actions/slack-boltjs-app/src/utils/github/default_branch.js @@ -0,0 +1,11 @@ +import request from "./request.js"; + +const default_branch = async function ({ api, app, token, say, msg }) { + const path = `/repos/${app}`; + const method = "GET"; + const data = null; + const out = await request({ api, path, method, token, data, say, msg }); + return JSON.parse(out)?.default_branch; +}; + +export default default_branch; diff --git a/.github/actions/slack-boltjs-app/src/utils/github/list_targets.js b/.github/actions/slack-boltjs-app/src/utils/github/list_targets.js new file mode 100644 index 00000000000..1cf459027c9 --- /dev/null +++ b/.github/actions/slack-boltjs-app/src/utils/github/list_targets.js @@ -0,0 +1,13 @@ +const list_targets = async ({ say }) => { + const deployTargets = process.env.GITHUB_TARGET_BRANCHES.split(","); + if (deployTargets.length === 0) { + return say( + "No targets branches defined. Set GITHUB_TARGET_BRANCHES first." + ); + } else { + var targets = "- " + deployTargets.join("\n- "); + return say(targets); + } +}; + +export default list_targets; diff --git a/.github/actions/slack-boltjs-app/src/utils/github/lock_target.js b/.github/actions/slack-boltjs-app/src/utils/github/lock_target.js new file mode 100644 index 00000000000..c7332647dc6 --- /dev/null +++ b/.github/actions/slack-boltjs-app/src/utils/github/lock_target.js @@ -0,0 +1,61 @@ +const lock_target = async ({ localStorage, args, say, user }) => { + const action = args[2]; + const branch = args[3]; + + const deployTargets = process.env.GITHUB_TARGET_BRANCHES.split(","); + if (!deployTargets.includes(branch)) { + say( + `\"${branch}\" is not in available target branches. Use: <@bot name> gh-targets` + ); + return false; + } + + switch (action) { + case "add": + // Check if the branch already has a lock + if (localStorage.getItem(branch)) { + // If yes, say that the branch is already locked + await say( + `Branch ${branch} is already locked by <@${localStorage.getItem( + branch + )}>` + ); + } else { + // If no, add the lock and say that the branch is locked + localStorage.setItem(branch, user); + await say(`Locked branch ${branch}`); + } + break; + case "remove": + // Check if the branch has a lock + if (localStorage.getItem(branch)) { + // If yes, remove the lock and say that the lock is removed + await say( + `Removed <@${localStorage.getItem( + branch + )}>'s lock on branch ${branch}` + ); + localStorage.removeItem(branch); + } else { + // If no, say that the branch is not locked + await say(`Branch ${branch} is not locked`); + } + break; + case "show": + // Check if the branch has a lock + if (localStorage.getItem(branch)) { + // If yes, say who locked the branch + await say( + `Branch ${branch} locked by <@${localStorage.getItem(branch)}>` + ); + } else { + // If no, say that the branch is not locked + await say(`Branch ${branch} is not locked`); + } + break; + default: + await say("Invalid lock command"); + } +}; + +export default lock_target; diff --git a/.github/actions/slack-boltjs-app/src/utils/github/push.js b/.github/actions/slack-boltjs-app/src/utils/github/push.js new file mode 100644 index 00000000000..3124d2aed65 --- /dev/null +++ b/.github/actions/slack-boltjs-app/src/utils/github/push.js @@ -0,0 +1,182 @@ +import request from "./request.js"; +import https from "https"; + +const push = async ({ localStorage, args, api, respond, say, force, isCommand }) => { + branchPush(localStorage, args, api, force, respond, say, isCommand); +}; + +export default push; + +// Define a function to check the configuration for branch push +const branchPushCheckConfiguration = function ( + localStorage, + sourceBranch, + targetBranch, + app, + token, + respond +) { + // Get the deploy targets from the environment variable + const deployTargets = process.env.GITHUB_TARGET_BRANCHES.split(","); + + // Define an array of error messages and conditions + const errors = [ + { + message: "Missing configuration: GITHUB_TOKEN", + condition: !token, + }, + { + message: + "Missing configuration: [for :owner/:repo] or GITHUB_OWNER/GITHUB_REPO", + condition: !app, + }, + { + message: + "Missing : gh-deploy to ", + condition: !sourceBranch, + }, + { + message: + "Missing : gh-deploy to ", + condition: !targetBranch, + }, + { + message: "Missing configuration: GITHUB_TARGET_BRANCHES", + condition: !process.env.GITHUB_TARGET_BRANCHES, + }, + { + message: `\"${targetBranch}\" is not in available target branches. Use: <@bot name> gh-targets`, + condition: !deployTargets.includes(targetBranch), + }, + ]; + + // Loop through the errors and respond if any condition is true + for (const error of errors) { + if (error.condition) { + respond(error.message); + return false; + } + } + + if (localStorage.getItem(targetBranch)) { + respond(`Branch ${targetBranch} is locked by <@${localStorage.getItem(targetBranch)}>`); + return false; + } + + // Return true if no errors are found + return true; +}; + +// Define constants +const token = process.env.GITHUB_TOKEN; +const app = process.env.GITHUB_REPO; +const port = 443; +const method = "PATCH"; +const headers = { + "User-Agent": "request", + "X-GitHub-Api-Version": "2022-11-28", +}; + +// Define a function to get the SHA of a branch +const getSHA = (api, branch) => { + return new Promise((resolve, reject) => { + // Set the options for the HTTPS request + const options = { + hostname: api, + port, + path: `/repos/${app}/git/refs/heads/${branch}`, + method: "GET", + headers, + }; + + // Make the HTTPS request + const req = https.request(options, (res) => { + let data = ""; + + // Concatenate the data chunks + res.on("data", (chunk) => { + data += chunk; + }); + + // Parse the JSON response and resolve the promise with the SHA + res.on("end", () => { + try { + const sha = JSON.parse(data).object.sha; + resolve(sha); + } catch (error) { + reject(error); + } + }); + }); + + // Handle errors and end the request + req.on("error", (error) => { + reject(error); + }); + req.end(); + }); +}; + +// Define a function to push a branch to another branch +const branchPush = async (localStorage, args, api, force, respond, say, isCommand) => { + // Get the source and target branches from the arguments + const sourceBranch = isCommand ? args[0] : args[2]; + const targetBranch = isCommand ? args[2] : args[4]; + + if ( + !branchPushCheckConfiguration( + localStorage, + sourceBranch, + targetBranch, + app, + token, + respond + ) + ) { + return; + } + + // Get the SHA of the source branch + try { + const sha = await getSHA(api, sourceBranch); + + // Prepare the data for the push request + const data = JSON.stringify({ + sha, + force, + }); + + // Set the path for the push request + const path = `/repos/${app}/git/refs/heads/${targetBranch}`; + + // Make the push request using the request module + const out = await request({ + api, + path, + method, + token, + data, + say, + msg: "", + }); + + // If the push request is successful, respond and say accordingly + if (out) { + const json = JSON.parse(out); + console.log(json); + respond( + `${ + force ? "Force pushed" : "Pushed" + } commit \"${sha}\" to branch \"${targetBranch}\"` + ); + say( + `\`deploy ${sourceBranch} to ${targetBranch} for ${app}\` triggered! :rocket:` + ); + } + } catch (error) { + // If there is an error, say that the branch was not found + say(`I failed to find branch \"${sourceBranch}\"!`); + } + + return true; +}; diff --git a/.github/actions/slack-boltjs-app/src/utils/github/request.js b/.github/actions/slack-boltjs-app/src/utils/github/request.js new file mode 100644 index 00000000000..faf1e373bb8 --- /dev/null +++ b/.github/actions/slack-boltjs-app/src/utils/github/request.js @@ -0,0 +1,44 @@ +import https from "https"; + +const request = async ({ api, path, method, token, data, say, msg }) => { + const json = await httpsRequest(api, path, method, token, data, say, msg); + return json; +}; + +export default request; + +const httpsRequest = (api, path, method, token, data, say, msg) => { + return new Promise((resolve, reject) => { + const options = { + hostname: api, + port: 443, + path, + method, + headers: { + "User-Agent": "request", + Authorization: `token ${token}`, + "Content-Type": "application/json", + "X-GitHub-Api-Version": "2022-11-28", + }, + }; + const req = https.request(options, async (res) => { + let body = ""; + res.on("data", (chunk) => { + body += chunk; + }); + res.on("end", async () => { + resolve(body); + if (msg) { + await say(msg); + } + }); + }); + req.on("error", (err) => { + reject(err); + }); + if (data) { + req.write(data); + } + req.end(); + }); +}; diff --git a/.github/actions/slack-boltjs-app/src/utils/github/router.js b/.github/actions/slack-boltjs-app/src/utils/github/router.js new file mode 100644 index 00000000000..da71834e9ef --- /dev/null +++ b/.github/actions/slack-boltjs-app/src/utils/github/router.js @@ -0,0 +1,65 @@ +import push from "../../utils/github/push.js"; +import listTargets from "./list_targets.js"; +import lockTarget from "./lock_target.js"; +import runWorkflow from "./run_workflow.js"; +import nodeLs from "node-localstorage"; + +const { LocalStorage } = nodeLs; +const { GITHUB_API } = process.env; +const localStorage = new LocalStorage("src/.locks"); +const api = GITHUB_API || "api.github.com"; +const force = true; + +// Define a function to route the commands +const router = async ({ message, say }) => { + const { text, user } = message; + // Split the message text by spaces + const args = text.split(" "); + + // Check if there are inputs after --inputs flag + const inputsIndex = args.indexOf("--inputs"); + let inputs; + if (inputsIndex > -1) { + // Get the inputs from the message text + inputs = text.split("--inputs")[1].trim(); + // Remove the inputs from the args array + args.splice(inputsIndex); + } + + // Get the command from the second argument + const ghCommand = args[1].split("-")[1]; + + try { + // Execute the command based on a switch statement + switch (ghCommand) { + case "deploy": + await push({ + localStorage, + args, + api, + respond: say, + say, + force, + isCommand: false, + }); + break; + case "targets": + await listTargets({ say }); + break; + case "run": + await runWorkflow({ args, api, say, inputs }); + break; + case "lock": + await lockTarget({ localStorage, args, say, user }); + break; + default: + await say(`Invalid command :(: ${ghCommand}`); + } + } catch (error) { + // Handle errors and log them + await say(`gh ${ghCommand} failed with error: ${error}`); + console.error(error); + } +}; + +export default router; diff --git a/.github/actions/slack-boltjs-app/src/utils/github/run_workflow.js b/.github/actions/slack-boltjs-app/src/utils/github/run_workflow.js new file mode 100644 index 00000000000..09881a97c92 --- /dev/null +++ b/.github/actions/slack-boltjs-app/src/utils/github/run_workflow.js @@ -0,0 +1,43 @@ +import defaultBranch from "../../utils/github/default_branch.js"; +import request from "../../utils/github/request.js"; + +const run_workflow = async ({ args, api, say, inputs }) => { + const [workflowFile, app = process.env.GITHUB_REPO, branch] = args.slice(2); + const token = process.env.GITHUB_TOKEN; + // Use an object to store the request data + let data = { ref: branch || (await defaultBranch({ api, app, token, say })) }; + + if (inputs) { + // Use JSON.parse and JSON.stringify to convert the inputs to a valid JSON object + data.inputs = JSON.parse( + `{${inputs}}` + .replace(/([,\{] *)(\w+):/g, '$1"$2":') + .replace( + /([,\{] *"\w+":)(?! *-?[0-9\.]+[,\}])(?! *[\{\[])( *)([^,\}]*)/g, + '$1$2"$3"' + ) + ); + } + + const stringData = JSON.stringify(data); + const path = `/repos/${app}/actions/workflows/${workflowFile}/dispatches`; + const method = "POST"; + + const out = await request({ + api, + path, + method, + token, + data: stringData, + say, + }); + if (out) { + say(JSON.parse(out).message); + } else { + say( + `Triggered workflow \`${workflowFile}\` with \`${stringData}\` for \`${app}\`! :rocket:` + ); + } +}; + +export default run_workflow; diff --git a/.github/actions/slack-boltjs-app/src/views/app_home.js b/.github/actions/slack-boltjs-app/src/views/app_home.js new file mode 100644 index 00000000000..11202a820d9 --- /dev/null +++ b/.github/actions/slack-boltjs-app/src/views/app_home.js @@ -0,0 +1,33 @@ +const appHome = async ({ event, client }) => { + try { + /* view.publish is the method that your app uses to push a view to the Home tab */ + await client.views.publish({ + /* the user that opened your app's app home */ + user_id: event.user, + + /* the view object that appears in the app home*/ + view: { + type: "home", + callback_id: "home_view", + + /* body of the view */ + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "Welcome :tada:", + }, + }, + { + type: "divider", + }, + ], + }, + }); + } catch (error) { + console.error(error); + } +}; + +export default appHome; diff --git a/.github/actions/vpn-azure/action.yml b/.github/actions/vpn-azure/action.yml index faba23787a2..f52373efb58 100644 --- a/.github/actions/vpn-azure/action.yml +++ b/.github/actions/vpn-azure/action.yml @@ -63,7 +63,7 @@ runs: fi shell: bash - - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 + - uses: azure/login@v2 if: inputs.sp-creds with: creds: ${{ inputs.sp-creds }} diff --git a/.github/changelog_config.json b/.github/changelog_config.json index 7d9a2ad4266..b9a84bf1d4e 100644 --- a/.github/changelog_config.json +++ b/.github/changelog_config.json @@ -113,6 +113,6 @@ } }, "base_branches": [ - "master" + "main" ] } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 498f787b167..6365ac26954 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -125,6 +125,11 @@ updates: schedule: interval: "daily" + - package-ecosystem: "github-actions" + directory: "/.github/actions/checksum-validate" + schedule: + interval: "daily" + # Frontend - package-ecosystem: "npm" directory: "/frontend-react" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8408c228617..ef040b9b7b3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,6 @@ This PR ... -**If you are suggesting a fix for a currently exploitable issue, please disclose the issue to the prime-reportstream team directly outside of GitHub instead of filing a PR, so we may immediately patch the affected systems before a disclosure. See [SECURITY.md/Reporting a Vulnerability](https://github.com/CDCgov/prime-reportstream/blob/master/SECURITY.md#reporting-a-vulnerability) for more information.** +**If you are suggesting a fix for a currently exploitable issue, please disclose the issue to the prime-reportstream team directly outside of GitHub instead of filing a PR, so we may immediately patch the affected systems before a disclosure. See [SECURITY.md/Reporting a Vulnerability](https://github.com/CDCgov/prime-reportstream/blob/main/SECURITY.md#reporting-a-vulnerability) for more information.** Test Steps: 1. *Include steps to test these changes* @@ -14,7 +14,7 @@ Test Steps: ### Testing - [ ] Tested locally? - [ ] Ran `./prime test` or `./gradlew testSmoke` against local Docker ReportStream container? -- [ ] (For Changes to /frontend-react/...) Ran `npm run lint:write`? +- [ ] (For Changes to /frontend-react/...) Ran `npm run lint:write`? - [ ] Added tests? ### Process diff --git a/.github/scripts/alert_stale_branches/stale_branch_check.sh b/.github/scripts/alert_stale_branches/stale_branch_check.sh index e38d7cb2853..3479151e7e5 100755 --- a/.github/scripts/alert_stale_branches/stale_branch_check.sh +++ b/.github/scripts/alert_stale_branches/stale_branch_check.sh @@ -23,7 +23,7 @@ get_branches () { count=0 for k in $branches do - if [[ "$k" != *"HEAD"* ]] && [[ "$k" != *"->"* ]] && [[ "$k" != *"master"* ]] + if [[ "$k" != *"HEAD"* ]] && [[ "$k" != *"->"* ]] && [[ "$k" != *"main"* ]] then if [ -z "$(git log -1 --since='6 months ago' -s $k)" ] then diff --git a/.github/workflows/alert_PD_schedule_Slack.yml b/.github/workflows/alert_PD_schedule_Slack.yml index e9593ec4d9c..19b054ee274 100644 --- a/.github/workflows/alert_PD_schedule_Slack.yml +++ b/.github/workflows/alert_PD_schedule_Slack.yml @@ -5,7 +5,8 @@ on: - cron: "7 13 * * Mon" #UTC-5 env: - channel: prime-reportstream-engineering + channel: temp-cdc-rs-notifications + # prime-reportstream-engineering jobs: pre_job: @@ -82,7 +83,7 @@ jobs: 🔸"Next": *"${{ env.Schedules_oncallSchedule_3_NextPersonName }}"* : \`"${{ env.Schedules_oncallSchedule_3_NextFrom }}"\` _until_ \`"${{ env.Schedules_oncallSchedule_3_NextUntil }}"\` icon-emoji: ':alarm_clock:' channel: ${{ env.channel }} - webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-url: ${{ secrets.SLACK_NOTIFICATIONS_WEBHOOK_URL }} color: good PDAlert_WeekDays: @@ -133,5 +134,5 @@ jobs: ❗"Now": *"${{ env.Schedules_oncallSchedule_3_OnCallPersonName }}"* icon-emoji: ':alarm_clock:' channel: ${{ env.channel }} - webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-url: ${{ secrets.SLACK_NOTIFICATIONS_WEBHOOK_URL }} color: good diff --git a/.github/workflows/alert_cert_expire.yml b/.github/workflows/alert_cert_expire.yml index 430ee3353d4..304a8e37a75 100644 --- a/.github/workflows/alert_cert_expire.yml +++ b/.github/workflows/alert_cert_expire.yml @@ -5,6 +5,9 @@ on: # The workflow runs every day at 8:07am - cron: "7 13 * * *" #UTC-5 +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: check-certificates: runs-on: ubuntu-latest @@ -23,7 +26,7 @@ jobs: ca-cert: ${{ secrets.CA_CRT}} user-crt: ${{ secrets.USER_CRT }} user-key: ${{ secrets.USER_KEY }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} - name: Add Runner IP to Key Vault Firewall run: | @@ -48,7 +51,7 @@ jobs: echo "LIST<<$EOF" >> $GITHUB_OUTPUT cat certificates.json >> $GITHUB_OUTPUT echo "$EOF" >> $GITHUB_OUTPUT - + - name: Slack Notification if: ${{ steps.format_out.outputs.LIST != '' }} uses: ./.github/actions/notifications diff --git a/.github/workflows/alert_resource_costs.yml b/.github/workflows/alert_resource_costs.yml index 0b16f6491b6..a085af0132e 100644 --- a/.github/workflows/alert_resource_costs.yml +++ b/.github/workflows/alert_resource_costs.yml @@ -7,6 +7,7 @@ on: env: ALERT_THRESHOLD: 60 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' jobs: alert_costs: @@ -23,7 +24,7 @@ jobs: - name: Login into Azure uses: ./.github/actions/vpn-azure with: - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} - name: Run Az Cost CLI id: az-cost @@ -59,7 +60,7 @@ jobs: message: | Resource Group \`${{ matrix.rg }}\` is exceeding the cost threshold and is ${{ steps.env-age.outputs.age_in_days }} days old. If still running and no longer needed, please [destroy](https://github.com/CDCgov/prime-reportstream/actions/workflows/destroy_demo_environment.yml). - + * **Cost per day: 💲${{ steps.az-cost.outputs.result }}** * **Provisioned by: \`${{ steps.last-pusher.outputs.username }}\`** * **Last Change Date: \`${{ steps.last-pusher.outputs.last_change_date }}\`** diff --git a/.github/workflows/alert_terraform_changes.yml b/.github/workflows/alert_terraform_changes.yml index c7730e3a01a..0e0bb7392a8 100644 --- a/.github/workflows/alert_terraform_changes.yml +++ b/.github/workflows/alert_terraform_changes.yml @@ -4,6 +4,10 @@ on: schedule: # The workflow runs every day at 8:20am - cron: "7 13 * * *" #UTC-5 + +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: alert_tf_changes: name: Check Terraform plan for ${{ matrix.env }} @@ -24,7 +28,7 @@ jobs: ca-cert: ${{ secrets.CA_CRT}} user-crt: ${{ secrets.USER_CRT }} user-key: ${{ secrets.USER_KEY }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} tf-auth: true - name: Collect Terraform stats diff --git a/.github/workflows/build_frontend.yaml b/.github/workflows/build_frontend.yaml index 49be6ce187a..83c85d0d9da 100644 --- a/.github/workflows/build_frontend.yaml +++ b/.github/workflows/build_frontend.yaml @@ -3,16 +3,16 @@ name: Build Frontend on: pull_request: branches: - - master + - main - production push: branches: - - master + - main merge_group: types: - checks_requested branches: - - master + - main concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/build_hub.yml b/.github/workflows/build_hub.yml index 33f4587a15f..5727ec498aa 100644 --- a/.github/workflows/build_hub.yml +++ b/.github/workflows/build_hub.yml @@ -3,15 +3,15 @@ name: Build Hub on: pull_request: branches: - - master + - main push: branches: - - master + - main merge_group: types: - checks_requested branches: - - master + - main concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -21,6 +21,7 @@ env: # These are for CI and not credentials of any system DB_USER: prime DB_PASSWORD: changeIT! + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' jobs: pre_job: @@ -64,7 +65,7 @@ jobs: version: ${{ github.run_id }} upload-build: false run-integration-tests: true - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} - name: Generate New Schema Docs working-directory: ./ diff --git a/.github/workflows/cleanup_acr_images.yml b/.github/workflows/cleanup_acr_images.yml index abf1185097a..fe1bb66d412 100644 --- a/.github/workflows/cleanup_acr_images.yml +++ b/.github/workflows/cleanup_acr_images.yml @@ -4,6 +4,10 @@ on: schedule: - cron: "0 0 * * *" # Runs daily at midnight UTC + +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: cleanup_images: runs-on: ubuntu-latest @@ -22,7 +26,7 @@ jobs: ca-cert: ${{ secrets.CA_CRT}} user-crt: ${{ secrets.USER_CRT }} user-key: ${{ secrets.USER_KEY }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} - name: List ${{ matrix.env }} repository images run: | @@ -35,7 +39,7 @@ jobs: - name: Delete old images in ${{ matrix.env }} env env: - IMAGE_FILE: ${{ matrix.env }}-images.txt + IMAGE_FILE: ${{ matrix.env }}-images.txt run: | if [ -e "$IMAGE_FILE" ]; then while IFS= read -r image_id; do @@ -49,9 +53,9 @@ jobs: else echo "File not found: $IMAGE_FILE" fi - - # Pushing a modified image using an existing tag untags the previously pushed image, - # resulting in an orphaned (or "dangling") image. + + # Pushing a modified image using an existing tag untags the previously pushed image, + # resulting in an orphaned (or "dangling") image. # The previously pushed image's manifest--and its layer data--remains in the registry. # They still need to be removed @@ -65,7 +69,7 @@ jobs: - name: Delete image manifest in ${{ matrix.env }} env env: - UNTAGED_FILE: ${{ matrix.env }}-untaged-images.txt + UNTAGED_FILE: ${{ matrix.env }}-untaged-images.txt run: | if [ -e "$UNTAGED_FILE" ]; then while IFS= read -r manifest_id; do diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index cda3706bbdd..8a1a1bab180 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,10 +14,10 @@ name: "CodeQL" on: push: branches: - - master + - main pull_request: branches: - - master + - main paths: - "frontend-react/**" - "prime-router/src/scripts/**" diff --git a/.github/workflows/dependency_review.yml b/.github/workflows/dependency_review.yml index 0bbf231149f..3535cef2b91 100644 --- a/.github/workflows/dependency_review.yml +++ b/.github/workflows/dependency_review.yml @@ -8,7 +8,7 @@ name: 'Dependency Review' on: pull_request: branches: - - master + - main paths: - 'frontend-react/**' - 'prime-router/**' diff --git a/.github/workflows/deploy_terraform.yml b/.github/workflows/deploy_terraform.yml index a5ce59ce698..97f5cc3b0d6 100644 --- a/.github/workflows/deploy_terraform.yml +++ b/.github/workflows/deploy_terraform.yml @@ -3,11 +3,14 @@ name: Deploy Terraform on: push: branches: - - master + - main - production paths: - '**.tf' +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: pre_job: name: Set Build Environment @@ -48,7 +51,7 @@ jobs: ca-cert: ${{ secrets.CA_CRT}} user-crt: ${{ secrets.USER_CRT }} user-key: ${{ secrets.USER_KEY }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} tf-auth: true - name: Collect Terraform stats @@ -101,7 +104,7 @@ jobs: ca-cert: ${{ secrets.CA_CRT}} user-crt: ${{ secrets.USER_CRT }} user-key: ${{ secrets.USER_KEY }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} tf-auth: true - name: Use specific version of Terraform uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd @@ -114,4 +117,4 @@ jobs: terraform validate terraform fmt -recursive terraform plan -out ${{ needs.pre_job.outputs.env_name }}-tf.plan - # terraform apply -input=false -no-color -lock-timeout=600s -auto-approve ${{ needs.pre_job.outputs.env_name }}-tf.plan + terraform apply -input=false -no-color -lock-timeout=600s -auto-approve ${{ needs.pre_job.outputs.env_name }}-tf.plan diff --git a/.github/workflows/deployment_rollback.yml b/.github/workflows/deployment_rollback.yml index 1b35fdd641b..4fb20811984 100644 --- a/.github/workflows/deployment_rollback.yml +++ b/.github/workflows/deployment_rollback.yml @@ -11,6 +11,10 @@ on: - staging - test type: choice + +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: DeployToCandidateSlot: runs-on: ubuntu-latest @@ -45,7 +49,7 @@ jobs: ca-cert: ${{ secrets.CA_CRT}} user-crt: ${{ secrets.USER_CRT }} user-key: ${{ secrets.USER_KEY }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} - name: Add runner IP to Function App firewall run: | diff --git a/.github/workflows/destroy_demo_environment.yml b/.github/workflows/destroy_demo_environment.yml index a64a2e21f81..b74c11ae406 100644 --- a/.github/workflows/destroy_demo_environment.yml +++ b/.github/workflows/destroy_demo_environment.yml @@ -12,6 +12,10 @@ on: - demo3 type: choice + +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: destroy_demo_environment: name: "Destroy Environment: ${{ github.event.inputs.env_name }}" @@ -36,7 +40,7 @@ jobs: uses: ./.github/actions/vpn-azure with: env-name: ${{ github.event.inputs.env_name }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} tf-auth: true - name: Destroy demo environment diff --git a/.github/workflows/export_cost_data.yml b/.github/workflows/export_cost_data.yml index 05969238c83..434fbcf6c96 100644 --- a/.github/workflows/export_cost_data.yml +++ b/.github/workflows/export_cost_data.yml @@ -4,6 +4,10 @@ on: schedule: # The workflow runs every day at 3PM - cron: "0 19 * * *" #UTC-5 + +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: export_cost_data: name: Export Azure Cost Data to Storage @@ -27,7 +31,7 @@ jobs: uses: ./.github/actions/vpn-azure with: env-name: ${{ matrix.env }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} tf-auth: false - name: Run Az Cost CLI id: az-cost @@ -76,7 +80,7 @@ jobs: user: prime database: prime_data_hub output-file: result.txt - query: | + query: | CREATE TABLE IF NOT EXISTS Azure_Costs ( Date date, Cost NUMERIC(13, 8), diff --git a/.github/workflows/frontend_chromatic_main.yml b/.github/workflows/frontend_chromatic_main.yml index 7ca6151a5df..3be72354c75 100644 --- a/.github/workflows/frontend_chromatic_main.yml +++ b/.github/workflows/frontend_chromatic_main.yml @@ -2,7 +2,7 @@ name: Chromatic Main Branch on: push: - branches: [master] + branches: [main] paths: - "frontend-react/**" diff --git a/.github/workflows/frontend_ci.yml b/.github/workflows/frontend_ci.yml index 55aacb3d022..1ca0e2e4c9d 100644 --- a/.github/workflows/frontend_ci.yml +++ b/.github/workflows/frontend_ci.yml @@ -18,6 +18,7 @@ env: TEST_SENDER_PASSWORD: ${{ secrets.TEST_SENDER_PASSWORD }} TEST_RECEIVER_USERNAME: ${{ secrets.TEST_RECEIVER_USERNAME }} TEST_RECEIVER_PASSWORD: ${{ secrets.TEST_RECEIVER_PASSWORD }} + LOCAL_SITEMAP_URL: "http://localhost:3000/sitemap.xml" jobs: pre-job: @@ -54,6 +55,108 @@ jobs: - name: Run linting run: yarn run lint + pa11y: + name: Run Pa11y 508 Compliance Check + needs: lint + runs-on: ubuntu-20.04 # pinned to this version due to: https://github.com/pa11y/pa11y-ci/issues/198 + + steps: + - name: Check out changes + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + - name: Use Node.js with yarn + uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 + with: + node-version-file: frontend-react/.nvmrc + cache: yarn + cache-dependency-path: frontend-react/yarn.lock + - name: Yarn install + run: yarn install --immutable + + - name: Run Frontend Server + run: | + yarn run dev --host 0.0.0.0 --port 3000 & + echo $! > server.pid + sleep 5 # Wait for the server to start up + echo + echo "Checking for sitemap on localhost:3000" + if ! curl -s ${{ env.LOCAL_SITEMAP_URL }}; then + echo "❌ Server failed to start or sitemap not present" + exit 1 + else + echo "✅ Server started successfully" + fi + + - name: Install Pa11y + run: | + echo "Installing google-chrome-stable" + sudo apt-get update && sudo apt-get install -y google-chrome-stable jq + which google-chrome-stable || echo "google-chrome-stable not found" + echo "Installing Pa11y" + npm install -g \ + pa11y@8.0.0 \ + puppeteer@22.8.2 \ + --no-save + + - name: Run Pa11y Check # Pa11y is configured in frontend-react/.pa11yci + run: | + SITEMAP_URL="${{ env.LOCAL_SITEMAP_URL }}" + echo "Local Sitemap URL: $SITEMAP_URL" + + echo "Checking Pa11y is installed and accessible" + pa11y --version + + if [ $? -ne 0 ]; then + echo "❌ Pa11y installation failed" + exit 1 + else + echo "Running sitemap check - curl on localhost:3000" + + if ! curl -s "$SITEMAP_URL"; then + echo "❌ Server failed to start or sitemap not accessible" + exit 1 + else + echo "✅ Server appears to be running" + echo "Extracting URLs from sitemap" + + # Download the sitemap.xml content + curl -s "$SITEMAP_URL" -o sitemap.xml + + # Check if download was successful + if [ ! -s sitemap.xml ]; then + echo "❌ Failed to download sitemap or file is empty." + exit 1 + fi + + # Extract URLs and save them to sitemap_urls.txt + #Linux grep flags: -P for Perl-compatible regex, -o for only matching part, -P for non-greedy matching + grep -oP '(?<=).*?(?=)' sitemap.xml | sed "s|https://reportstream.cdc.gov|http://localhost:3000|g" > sitemap_urls.txt + # OSX + # grep "" sitemap.xml | sed -E 's/.*(.*)<\/loc>.*/\1/'| sed "s|https://reportstream.cdc.gov|http://localhost:3000|g" > sitemap_urls.txt + + + # Check if URLs were extracted successfully + if [ -s sitemap_urls.txt ]; then + echo "✅ URLs extracted successfully:" + cat sitemap_urls.txt + echo "Running Pa11y WCAG2AA checks on sitemap URLs" + for f in `cat sitemap_urls.txt`;do pa11y -c ./pa11y.json -e htmlcs -e axe -d -s WCAG2AA $f ;done + echo "Pa11y WCAG2AA checks complete" + else + echo "❌ No URLs found in sitemap.xml." + exit 1 + fi + + # Clean up + rm sitemap.xml sitemap_urls.txt + + fi + fi + + - name: Stop Frontend Server + run: | + echo "Stopping Frontend Server" + kill $(cat server.pid) + unit_tests: name: Unit tests needs: lint diff --git a/.github/workflows/publish_docker.yaml b/.github/workflows/publish_docker.yaml index 2c58a5c8b0c..03e7e4bae4d 100644 --- a/.github/workflows/publish_docker.yaml +++ b/.github/workflows/publish_docker.yaml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - master + - main schedule: - cron: "0 0 1 * *" diff --git a/.github/workflows/release_chatops_app.yml b/.github/workflows/release_chatops_app.yml index 2cf114cb640..89543decad6 100644 --- a/.github/workflows/release_chatops_app.yml +++ b/.github/workflows/release_chatops_app.yml @@ -8,10 +8,13 @@ on: - "operations/slack-boltjs-app" pull_request: branches: - - master + - main paths: - "operations/slack-boltjs-app" +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: pre_job: name: Set Build Environment @@ -40,9 +43,9 @@ jobs: with: submodules: true - - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 + - uses: azure/login@v2 with: - creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + creds: ${{ env.AZURE_CREDENTIALS }} - name: Set environment variables shell: bash diff --git a/.github/workflows/release_to_azure.yml b/.github/workflows/release_to_azure.yml index 0c22420eb31..6e01c192131 100644 --- a/.github/workflows/release_to_azure.yml +++ b/.github/workflows/release_to_azure.yml @@ -4,7 +4,7 @@ name: Release to Azure on: push: branches: - - master + - main - production - test - demo1 @@ -15,6 +15,9 @@ defaults: run: working-directory: prime-router +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: pre_job: name: "Set Build Environment" @@ -33,7 +36,7 @@ jobs: id: build_vars uses: ./.github/actions/build-vars with: - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} build_router_release: name: "Release: Build Router" @@ -102,7 +105,7 @@ jobs: uses: ./.github/actions/vpn-azure with: env-name: ${{ needs.pre_job.outputs.env_name }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} tf-auth: true - name: Provision demo environment @@ -138,16 +141,13 @@ jobs: uses: ./.github/actions/vpn-azure with: env-name: ${{ needs.pre_job.outputs.env_name }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} - name: Get function app checksum env: checksum_validation: ${{ vars.CHECKSUM_VALIDATION }} if: needs.pre_job.outputs.has_router_change == 'true' && env.checksum_validation == 'true' - - uses: JosiahSiegel/checksum-validate-action@ebdf8c12c00912d18de93c483b935d51582f9236 - ## DevSecOps - Aquia (Replace) - uses: ./.github/actions/checksum-validate-action - + uses: ./.github/actions/checksum-validate with: key: backend input: $(az functionapp config appsettings list -g prime-data-hub-${{ needs.pre_job.outputs.env_name }} -n pdh${{ needs.pre_job.outputs.env_name }}-functionapp -o tsv | sort) @@ -194,7 +194,7 @@ jobs: ca-cert: ${{ secrets.CA_CRT}} user-crt: ${{ secrets.USER_CRT }} user-key: ${{ secrets.USER_KEY }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} dns-ip: ${{ needs.pre_job.outputs.dns_ip }} - name: Deploy backend @@ -233,7 +233,7 @@ jobs: ca-cert: ${{ secrets.CA_CRT}} user-crt: ${{ secrets.USER_CRT }} user-key: ${{ secrets.USER_KEY }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} dns-ip: ${{ needs.pre_job.outputs.dns_ip }} - name: Deploy frontend diff --git a/.github/workflows/release_to_github.yml b/.github/workflows/release_to_github.yml index a1b804a342c..0170f97d62e 100644 --- a/.github/workflows/release_to_github.yml +++ b/.github/workflows/release_to_github.yml @@ -3,7 +3,7 @@ name: Release to GitHub on: push: branches: - - master + - main - production concurrency: ${{ github.ref }}-gh-release diff --git a/.github/workflows/release_trial_frontend.yml b/.github/workflows/release_trial_frontend.yml index 9c8df9192c7..10bddf055f9 100644 --- a/.github/workflows/release_trial_frontend.yml +++ b/.github/workflows/release_trial_frontend.yml @@ -12,6 +12,9 @@ defaults: run: working-directory: prime-router +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: pre_job: name: "Set Build Environment" @@ -76,7 +79,7 @@ jobs: ca-cert: ${{ secrets.CA_CRT}} user-crt: ${{ secrets.USER_CRT }} user-key: ${{ secrets.USER_KEY }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} - name: Deploy frontend uses: ./.github/actions/deploy-frontend diff --git a/.github/workflows/restore_databases.yml b/.github/workflows/restore_databases.yml index 628f82c377e..1d159a31d51 100644 --- a/.github/workflows/restore_databases.yml +++ b/.github/workflows/restore_databases.yml @@ -43,6 +43,9 @@ defaults: run: working-directory: prime-router +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: pre_job: name: "Set Build Environment" @@ -66,7 +69,7 @@ jobs: shell: bash run: | echo "event_name: ${{ github.event_name }}" - if [[ "${{ github.event_name }}" == 'workflow_dispatch' ]]; then + if [[ "${{ github.event_name }}" == 'workflow_dispatch' ]]; then echo "ENV_NAME=${{ github.event.inputs.backup_from }}" >> $GITHUB_ENV echo "SINK_ENV_NAME=${{ github.event.inputs.restore_to }}" >> $GITHUB_ENV echo "RESTORE_IGNORE_BACKUP_AGE=${{ github.event.inputs.restore_ignore_backup_age }}" >> $GITHUB_ENV @@ -79,7 +82,7 @@ jobs: echo "BACKUP_AGE_LIMIT_MINS=1440" >> $GITHUB_ENV echo "DATABASES=[\"prime_data_hub_candidate\"]" >> $GITHUB_ENV fi - + - name: Set backup and restore variables shell: bash run: | @@ -90,9 +93,9 @@ jobs: echo "SINK_BACKUP_STORAGE=pdh${{ env.SINK_ENV_NAME }}terraform" >> $GITHUB_ENV # Login to Azure - - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 + - uses: azure/login@v2 with: - creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + creds: ${{ env.AZURE_CREDENTIALS }} # Restores will fail if sink server is not pre-expanded - name: Increase sink server size if necessary @@ -139,9 +142,9 @@ jobs: - name: Check out changes uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 - - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 + - uses: azure/login@v2 with: - creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + creds: ${{ env.AZURE_CREDENTIALS }} - name: Get public ip shell: bash @@ -155,7 +158,7 @@ jobs: --share-name dbbackups --query 'exists' -o tsv --only-show-errors) if [ $backup_exists == "false" ]; then lastModified="1970-01-01" - else + else lastModified=$(az storage directory show --name ${{ env.env_name }}_${{ matrix.db }} --share-name dbbackups \ --account-name ${{ env.sink_backup_storage }} --query 'properties.lastModified' --only-show-errors -o tsv) fi @@ -230,9 +233,9 @@ jobs: - name: Check out changes uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 - - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 + - uses: azure/login@v2 with: - creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + creds: ${{ env.AZURE_CREDENTIALS }} - name: Get public ip shell: bash @@ -293,15 +296,15 @@ jobs: \echo

setting table changes:

\echo UPDATE - setting - SET + setting + SET values = values || '{"transport": {"host": "10.0.1.100", "port": "22", "type": "SFTP", "filePath": "./staging", "credentialName": null}}'::jsonb - WHERE + WHERE type = 'RECEIVER' AND values -> 'transport' ->> 'type' = 'SFTP'; \echo
- SELECT - relname, n_tup_ins - n_tup_del as rowcount + SELECT + relname, n_tup_ins - n_tup_del as rowcount FROM pg_stat_all_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema') AND schemaname !~ '^pg_toast' diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index 2704fa9ba6f..fa532920c3d 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -1,10 +1,10 @@ name: Snyk Java/Kotlin scan -# refresh Snyk console results for Java/Kotlin scans when merging with master +# refresh Snyk console results for Java/Kotlin scans when merging with main on: push: branches: - - master + - main paths: - "prime-router/**" - "submissions/**" diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 6f996603649..fa9c8b9f2be 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -4,10 +4,10 @@ on: workflow_dispatch: push: branches: - - master + - main pull_request: branches: - - master + - main paths: - "prime-router/**" - "frontend-react/**" @@ -128,7 +128,7 @@ jobs: yarn run test:ci shell: bash - - name: Run SonarCloud Scan master or full + - name: Run SonarCloud Scan main or full if: ( steps.changed-files-yaml.outputs.frontend_any_changed == 'true' && steps.changed-files-yaml.outputs.backend_any_changed == 'true' ) || steps.branch-name.outputs.is_default == 'true' uses: ./.github/actions/sonarcloud with: diff --git a/.github/workflows/start_test_servers.yml b/.github/workflows/start_test_servers.yml index 292ef517956..45fcb8279ed 100644 --- a/.github/workflows/start_test_servers.yml +++ b/.github/workflows/start_test_servers.yml @@ -2,12 +2,16 @@ name: Start apps in Test server on: workflow_dispatch: - # push: + # push: # branches: # - feature/9537-schedulemaintenance_DemoandTest schedule: - cron: "0 5 * * MON" #UTC-5 - cron: '0 11 * * 1-5' + +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: alert_stale_branches: name: Start Test Subscription apps @@ -15,11 +19,11 @@ jobs: steps: - name: Check Out Changes uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 - + # Login to Azure - - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 + - uses: azure/login@v2 with: - creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + creds: ${{ env.AZURE_CREDENTIALS }} - name: Start postgres uses: ./.github/actions/Start-PostgresDB @@ -27,7 +31,7 @@ jobs: name: "pdhtest-pgsql-flex" resource-group: prime-data-hub-test subscription: OCIO-DMZ-C1 - + - name: Start function app uses: ./.github/actions/Start-AzFunction with: diff --git a/.github/workflows/stop_test_servers.yml b/.github/workflows/stop_test_servers.yml index 5f16822a7f9..ba8b476ad28 100644 --- a/.github/workflows/stop_test_servers.yml +++ b/.github/workflows/stop_test_servers.yml @@ -2,12 +2,15 @@ name: Stop Apps in Test server on: workflow_dispatch: - # push: + # push: # branches: # - feature/9537-schedulemaintenance_DemoandTest schedule: - cron: "0 5 * * SAT" #UTC-5 - - cron: '0 2 * * 1-5' + - cron: '0 2 * * 1-5' + +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' jobs: alert_stale_branches: @@ -16,7 +19,7 @@ jobs: steps: - name: Check Out Changes uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 - + - name: Connect to VPN and login to Azure uses: ./.github/actions/vpn-azure with: @@ -25,12 +28,12 @@ jobs: ca-cert: ${{ secrets.CA_CRT}} user-crt: ${{ secrets.USER_CRT }} user-key: ${{ secrets.USER_KEY }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} tf-auth: true # Login to Azure - - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 + - uses: azure/login@v2 with: - creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + creds: ${{ env.AZURE_CREDENTIALS }} - name: Stop postgres uses: ./.github/actions/Stop-PostgresDB @@ -39,7 +42,7 @@ jobs: resource-group: prime-data-hub-test subscription: OCIO-DMZ-C1 pass: ${{ secrets.POSTGRESQL_Test_PWD }} - creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + creds: ${{ env.AZURE_CREDENTIALS }} - name: Stop function app uses: ./.github/actions/Stop-AzFunction diff --git a/.github/workflows/sync-translation-schemas.yml b/.github/workflows/sync-translation-schemas.yml index 45ea3cd7f0c..031ba19bbdc 100644 --- a/.github/workflows/sync-translation-schemas.yml +++ b/.github/workflows/sync-translation-schemas.yml @@ -11,6 +11,9 @@ on: - HL7 required: true +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: sync-schemas: runs-on: ubuntu-latest @@ -21,7 +24,7 @@ jobs: - name: Login into Azure uses: ./.github/actions/vpn-azure with: - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} - name: Build backend uses: ./.github/actions/build-backend diff --git a/.github/workflows/validate_resources.yml b/.github/workflows/validate_resources.yml index 02af76df69d..01c961b0985 100644 --- a/.github/workflows/validate_resources.yml +++ b/.github/workflows/validate_resources.yml @@ -4,16 +4,19 @@ on: pull_request: branches: - production - - master + - main - test push: branches: - production - - master + - main - test schedule: - cron: "36 3 * * *" +env: + AZURE_CREDENTIALS: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' + jobs: pre_job: name: Set Build Environment @@ -57,7 +60,7 @@ jobs: ca-cert: ${{ secrets.CA_CRT}} user-crt: ${{ secrets.USER_CRT }} user-key: ${{ secrets.USER_KEY }} - sp-creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + sp-creds: ${{ env.AZURE_CREDENTIALS }} - name: Restart DNS if failure uses: ./.github/actions/retry @@ -103,9 +106,9 @@ jobs: - name: Check Out Changes uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 - - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 + - uses: azure/login@v2 with: - creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + creds: ${{ env.AZURE_CREDENTIALS }} - name: Reduce database replica sizes uses: ./.github/actions/retry @@ -136,9 +139,9 @@ jobs: - name: Check Out Changes uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 - - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 + - uses: azure/login@v2 with: - creds: ${{ secrets.SERVICE_PRINCIPAL_CREDS }} + creds: ${{ env.AZURE_CREDENTIALS }} - name: Fetch VPN DNS IP id: fetch_vpn_ip diff --git a/.github/workflows/validate_terraform.yml b/.github/workflows/validate_terraform.yml index bd3cc505cca..5924b2a9f7f 100644 --- a/.github/workflows/validate_terraform.yml +++ b/.github/workflows/validate_terraform.yml @@ -4,7 +4,7 @@ on: pull_request: branches: - production - - master + - main - test paths: - '**.tf' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef57fd25fa4..9d2732642a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,7 +69,7 @@ Once your changes and tests are ready to submit for review: 2. **Rebase your changes** - Update your local repository with the most recent code from the principal repository, and rebase your branch on top of the latest master branch. We prefer your initial changes to be squashed into a single commit. Later, if we ask you to make changes, add the changes as separate commits. This makes the changes easier to review. + Update your local repository with the most recent code from the principal repository, and rebase your branch on top of the latest main branch. We prefer your initial changes to be squashed into a single commit. Later, if we ask you to make changes, add the changes as separate commits. This makes the changes easier to review. 3. **Submit a pull request** diff --git a/DEPLOYMENTS.md b/DEPLOYMENTS.md index 3ecdd5ca31c..c974938ed2a 100644 --- a/DEPLOYMENTS.md +++ b/DEPLOYMENTS.md @@ -2,16 +2,16 @@ ReportStream production deployments take place on Tuesdays and Thursday around 10am EST. -The testing and staging environments are automatically deployed on every merge into `master` through our Continuous Deployment pipeline. +The testing and staging environments are automatically deployed on every merge into `main` through our Continuous Deployment pipeline. ## How is ReportStream deployed? We automatically deploy changes through [GitHub Actions](.github/workflows/release.yml) based on changes in target branches as described in the table below. These changes can only enter these branches through successfully reviewed Pull Requests. | Changes are merged into branch | Changes get deployed into environment(s) | Release Builds | -|:--|:--|:--| -| `master` | test and staging | *[pre-release](https://github.com/CDCgov/prime-reportstream/releases/tag/v-pre-release) (staging) | -| `production` | production | [release](https://github.com/CDCgov/prime-reportstream/releases/latest) | +|:-------------------------------|:--|:--| +| `main` | test and staging | *[pre-release](https://github.com/CDCgov/prime-reportstream/releases/tag/v-pre-release) (staging) | +| `production` | production | [release](https://github.com/CDCgov/prime-reportstream/releases/latest) | \* ⚠️*Returns "404 not found" if no changes have been merged since the last release.* @@ -39,11 +39,11 @@ In preparation for our Tuesday and Thursday deployments, a cutoff time has been | Tuesday, 10am EST | Monday, 12pm EST | | Thursday, 10am EST | Wednesday, 12pm EST | -The cutoff time is automatically enforced via automatic branching from `master` into a dedicated deployment branch targeting the `production` branch through a [GitHub Action](.github/workflows/prepare_deployment_branch.yaml). +The cutoff time is automatically enforced via automatic branching from `main` into a dedicated deployment branch targeting the `production` branch through a [GitHub Action](.github/workflows/prepare_deployment_branch.yaml). -1. At the specified cut-off time (Mondays and Wednesdays at noon ET), the GitHub action creates a new branch named `deployment/YYYY-MM-DD` (where the YYYY-MM-DD is `today + 1day`, i.e. the date of the deployment, not the date of the branching) which branches from `master`. This branch now contains everything that was present in `master` at that cut-off time. This is the content that is/will be part of the production deployment. +1. At the specified cut-off time (Mondays and Wednesdays at noon ET), the GitHub action creates a new branch named `deployment/YYYY-MM-DD` (where the YYYY-MM-DD is `today + 1day`, i.e. the date of the deployment, not the date of the branching) which branches from `main`. This branch now contains everything that was present in `main` at that cut-off time. This is the content that is/will be part of the production deployment. 1. A new PR from the deployment branch is filed to merge `deployment/YYYY-MM-DD` into `production`. The PR has title `"Deployment of YYYY-MM-DD"` and is tagged with the [`deployment` tag](https://github.com/CDCgov/prime-reportstream/issues?q=label%3Adeployment). -1. The contents of `master` is deployed to the staging environment for verification +1. The contents of `main` is deployed to the staging environment for verification * Manual testing takes place 1. The PR is reviewed by the team 1. The PR is merged during the specified deployment window by an approved team member. diff --git a/README.md b/README.md index 7c46f256654..5b2e9ef5a11 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ unless pursuant to an existing contract or agreement. ## Privacy Standard Notice This repository contains only non-sensitive, publicly available data and information. All material and community participation is covered by the -[Disclaimer](https://github.com/CDCgov/template/blob/master/DISCLAIMER.md) -and [Code of Conduct](https://github.com/CDCgov/template/blob/master/code-of-conduct.md). +[Disclaimer](https://github.com/CDCgov/template/blob/main/DISCLAIMER.md) +and [Code of Conduct](https://github.com/CDCgov/template/blob/main/code-of-conduct.md). For more information about CDC's privacy policy, please visit [http://www.cdc.gov/other/privacy.html](https://www.cdc.gov/other/privacy.html). ## Contributing Standard Notice @@ -80,6 +80,6 @@ published through the [CDC web site](http://www.cdc.gov). ## Additional Standard Notices Please refer to [CDC's Template Repository](https://github.com/CDCgov/template) -for more information about [contributing to this repository](https://github.com/CDCgov/template/blob/master/CONTRIBUTING.md), -[public domain notices and disclaimers](https://github.com/CDCgov/template/blob/master/DISCLAIMER.md), -and [code of conduct](https://github.com/CDCgov/template/blob/master/code-of-conduct.md). +for more information about [contributing to this repository](https://github.com/CDCgov/template/blob/main/CONTRIBUTING.md), +[public domain notices and disclaimers](https://github.com/CDCgov/template/blob/main/DISCLAIMER.md), +and [code of conduct](https://github.com/CDCgov/template/blob/main/code-of-conduct.md). diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index e6d82c00810..043426387a5 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -1,10 +1,10 @@ apply(from = rootProject.file("buildSrc/shared.gradle.kts")) plugins { - id("org.springframework.boot") version "3.3.4" - id("io.spring.dependency-management") version "1.1.6" + id("org.springframework.boot") version "3.4.1" + id("io.spring.dependency-management") version "1.1.7" id("reportstream.project-conventions") - kotlin("plugin.spring") version "2.0.0" + kotlin("plugin.spring") version "2.1.0" } group = "gov.cdc.prime" @@ -24,18 +24,18 @@ dependencies { implementation("org.springframework.cloud:spring-cloud-starter-gateway") implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") - runtimeOnly("com.nimbusds:oauth2-oidc-sdk:11.19.1") + runtimeOnly("com.nimbusds:oauth2-oidc-sdk:11.20.1") + + // okta + implementation("com.okta.sdk:okta-sdk-api:20.0.0") + runtimeOnly("com.okta.sdk:okta-sdk-impl:20.0.0") // Swagger - implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.6.0") + implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.7.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") - testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") - testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0") - - testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner") compileOnly("org.springframework.boot:spring-boot-devtools") } @@ -48,8 +48,8 @@ configurations.all { dependencyManagement { imports { - mavenBom("com.azure.spring:spring-cloud-azure-dependencies:5.16.0") - mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.3") + mavenBom("com.azure.spring:spring-cloud-azure-dependencies:5.19.0") + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2024.0.0") } } diff --git a/auth/docs/setup.md b/auth/docs/setup.md new file mode 100644 index 00000000000..4b421abd7d8 --- /dev/null +++ b/auth/docs/setup.md @@ -0,0 +1,56 @@ +# Running the Auth Microservice + +## Prerequisites + +A few secrets are required to run the Auth which are not committed to source. These values are +configured in Okta. + +| Environment variable | Value | +|----------------------|---------------------------------| +| OKTA_ADMIN_CLIENT_API_ENCODED_PRIVATE_KEY | Base 64 encoded private key pem | +| SPRING_SECURITY_OAUTH2_RESOURCESERVER_OPAQUETOKEN_CLIENT_SECRET | Base 64 encoded secret | + +## How to run application locally + +```bash +# from project root +# start ReportStream and all dependent docker containers +./gradlew quickRun +# start submissions service +./ gradlew submissions:bootRun +# start auth service +./gradlew auth:bootRun +``` + +## Setup a Sender + +- Sign in to Admin Okta +- Applications -> Application tab +- Click "Create App Integration" +- Select "API Services" and click next +- Name your sender +- Copy your client ID and client secret or private key locally to be used while calling the /token endpoint +- Add the user to the appropriate sender group + - You can find this option on the small gear next to your newly created application + - Ensure the group has the prefix DHSender_ + +## Submitting reports locally + +- Retrieve an access token directly from Okta and ensure the JWT contains the "sender" scope + - Make a well-formed request to https://reportstream.oktapreview.com/oauth2/default/v1/token to retrieve your access token + - [See Okta documentation on that endpoint here](https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/main/#get-an-access-token) +- Submit your report to http://localhost:9000/api/v1/reports + - Note the it's port 9000 which is auth rather than directly to 8880 which is submissions + - See endpoint definition in [SubmissionController](../../submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/controllers/SubmissionController.kt) + - Add the access token you retrieved from Okta as a `Bearer` token in the `Authorization` header +- Inspect the logs if you received a 401 or a 403. This indicates there is an issue with your access token. + +## Notes on secrets + +The Okta-Groups JWT signing key pair has a local dev value already set up appropriately in auth and +downstream in submissions. New values _must_ be generated for deployed environments. You can look +at [KeyGenerationUtils](../src/test/kotlin/gov/cdc/prime/reportstream/auth/util/KeyGenerationUtils.kt) +for scripts to run to generate new keys. + +By Default, we are connecting to the Staging Okta. We cannot post connection secrets directly in this document so +you will have to ask someone for those values. diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplicationConstants.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplicationConstants.kt index ce68c2e0efc..8822627d6f0 100644 --- a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplicationConstants.kt +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/AuthApplicationConstants.kt @@ -12,10 +12,8 @@ object AuthApplicationConstants { const val HEALTHCHECK_ENDPOINT_V1 = "/api/v1/healthcheck" } - /** - * All Submissions service endpoints defined here - */ - object SubmissionsEndpoints { - const val REPORTS_ENDPOINT_V1 = "/api/v1/reports" + object Scopes { + const val ORGANIZATION_SCOPE = "organization" + const val SUBJECT_SCOPE = "sub" } } \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/client/OktaGroupsClient.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/client/OktaGroupsClient.kt new file mode 100644 index 00000000000..84f9b1530db --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/client/OktaGroupsClient.kt @@ -0,0 +1,37 @@ +package gov.cdc.prime.reportstream.auth.client + +import com.okta.sdk.resource.api.ApplicationGroupsApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.apache.logging.log4j.kotlin.Logging +import org.springframework.stereotype.Service + +@Service +class OktaGroupsClient( + private val applicationGroupsApi: ApplicationGroupsApi, +) : Logging { + + /** + * Get all application groups from the Okta Admin API + * + * Group names are found at json path "_embedded.group.profile.name" + * + * @see https://developer.okta.com/docs/api/openapi/okta-management/management/tag/ApplicationGroups/#tag/ApplicationGroups/operation/listApplicationGroupAssignments + */ + suspend fun getApplicationGroups(appId: String): List { + return withContext(Dispatchers.IO) { + try { + val groups = applicationGroupsApi + .listApplicationGroupAssignments(appId, null, null, null, "group") + .map { it.embedded?.get("group") as Map<*, *> } + .map { it["profile"] as Map<*, *> } + .map { it["name"] as String } + logger.info("$appId is a member of ${groups.joinToString()}") + groups + } catch (ex: Exception) { + logger.error("Error retrieving application groups from Okta API", ex) + throw ex + } + } + } +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/ApplicationConfig.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/ApplicationConfig.kt index 28db94952fc..a55c87d6184 100644 --- a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/ApplicationConfig.kt +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/ApplicationConfig.kt @@ -2,15 +2,15 @@ package gov.cdc.prime.reportstream.auth.config import gov.cdc.prime.reportstream.auth.model.Environment import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.boot.context.properties.bind.ConstructorBinding import org.springframework.context.annotation.Bean +import java.time.Clock import kotlin.time.TimeSource /** * Simple class to automatically read configuration from application.yml (or environment variable overrides) */ @ConfigurationProperties(prefix = "app") -data class ApplicationConfig @ConstructorBinding constructor( +data class ApplicationConfig( val environment: Environment, ) { @@ -18,4 +18,9 @@ data class ApplicationConfig @ConstructorBinding constructor( fun timeSource(): TimeSource { return TimeSource.Monotonic } + + @Bean + fun clock(): Clock { + return Clock.systemUTC() + } } \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/OktaClientConfig.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/OktaClientConfig.kt new file mode 100644 index 00000000000..66085dbb9c7 --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/OktaClientConfig.kt @@ -0,0 +1,46 @@ +package gov.cdc.prime.reportstream.auth.config + +import com.okta.sdk.client.AuthorizationMode +import com.okta.sdk.client.Clients +import com.okta.sdk.resource.api.ApplicationGroupsApi +import com.okta.sdk.resource.client.ApiClient +import gov.cdc.prime.reportstream.shared.StringUtilities.base64Decode +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile + +@Configuration +@Profile("!test") +class OktaClientConfig( + private val oktaClientProperties: OktaClientProperties, +) { + + @Bean + fun apiClient(): ApiClient { + return Clients.builder() + .setOrgUrl(oktaClientProperties.orgUrl) + .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) + .setClientId(oktaClientProperties.clientId) + .setScopes(oktaClientProperties.requiredScopes) + .setPrivateKey(oktaClientProperties.apiPrivateKey) + // .setCacheManager(...) TODO: investigate caching since groups don't often change + .build() + } + + @Bean + fun applicationGroupsApi(): ApplicationGroupsApi { + return ApplicationGroupsApi(apiClient()) + } + + @ConfigurationProperties(prefix = "okta.admin-client") + data class OktaClientProperties( + val orgUrl: String, + val clientId: String, + val requiredScopes: Set, + private val apiEncodedPrivateKey: String, + ) { + // PEM encoded format + val apiPrivateKey = apiEncodedPrivateKey.base64Decode() + } +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/RouteConfig.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/RouteConfig.kt deleted file mode 100644 index d1aac8c15e6..00000000000 --- a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/RouteConfig.kt +++ /dev/null @@ -1,27 +0,0 @@ -package gov.cdc.prime.reportstream.auth.config - -import gov.cdc.prime.reportstream.auth.AuthApplicationConstants -import org.springframework.cloud.gateway.route.RouteLocator -import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration - -/** - * Configuration class to set up route forwarding - */ -@Configuration -class RouteConfig( - private val submissionsConfig: SubmissionsConfig, -) { - - @Bean - fun routes(builder: RouteLocatorBuilder): RouteLocator { - return builder.routes() - .route { - it - .path(AuthApplicationConstants.SubmissionsEndpoints.REPORTS_ENDPOINT_V1) - .uri(submissionsConfig.baseUrl) - } - .build() - } -} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SubmissionsConfig.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SubmissionsConfig.kt deleted file mode 100644 index 703b9a8bccd..00000000000 --- a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/config/SubmissionsConfig.kt +++ /dev/null @@ -1,12 +0,0 @@ -package gov.cdc.prime.reportstream.auth.config - -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.boot.context.properties.bind.ConstructorBinding - -/** - * Configuration for Submissions microservice - */ -@ConfigurationProperties(prefix = "submissions") -data class SubmissionsConfig @ConstructorBinding constructor( - val baseUrl: String, -) \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthController.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthController.kt index f90ee051982..98979e5f801 100644 --- a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthController.kt +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthController.kt @@ -14,6 +14,9 @@ class HealthController( private val applicationStart = timeSource.markNow() + /** + * Simple endpoint that returns a healthcheck with uptime + */ @GetMapping( AuthApplicationConstants.Endpoints.HEALTHCHECK_ENDPOINT_V1, produces = [MediaType.APPLICATION_JSON_VALUE] diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/filter/AppendOktaGroupsGatewayFilterFactory.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/filter/AppendOktaGroupsGatewayFilterFactory.kt new file mode 100644 index 00000000000..e618da63f76 --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/filter/AppendOktaGroupsGatewayFilterFactory.kt @@ -0,0 +1,61 @@ +package gov.cdc.prime.reportstream.auth.filter + +import gov.cdc.prime.reportstream.auth.AuthApplicationConstants +import gov.cdc.prime.reportstream.auth.service.OktaGroupsService +import gov.cdc.prime.reportstream.shared.auth.jwt.OktaGroupsJWTConstants +import kotlinx.coroutines.reactor.mono +import org.springframework.cloud.gateway.filter.GatewayFilter +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication +import org.springframework.stereotype.Component +import reactor.core.publisher.Mono + +/** + * This filter defines how the Okta-Groups header is added to requests. It follows the conventions + * defined in spring-cloud-gateway and is instantiated via configuration under a route's filters. + */ +@Component +class AppendOktaGroupsGatewayFilterFactory( + private val oktaGroupsService: OktaGroupsService, +) : AbstractGatewayFilterFactory() { + + /** + * function used only in testing to create our filter without any configuration + */ + fun apply(): GatewayFilter { + return apply { _: Any? -> } + } + + override fun apply(config: Any?): GatewayFilter { + return GatewayFilter { exchange, chain -> + exchange + .getPrincipal() + .flatMap { oktaAccessTokenJWT -> + val appId = oktaAccessTokenJWT + .tokenAttributes[AuthApplicationConstants.Scopes.SUBJECT_SCOPE] as String + val organizations = oktaAccessTokenJWT + .tokenAttributes[AuthApplicationConstants.Scopes.ORGANIZATION_SCOPE] as List<*>? + + // If there is no organization claim present, then we have an application user and + // require appending our custom header + if (organizations == null) { + mono { oktaGroupsService.generateOktaGroupsJWT(appId) } + } else { + Mono.empty() + } + } + .map { oktaGroupsJWT: String -> + exchange.request + .mutate() + .headers { + it.add(OktaGroupsJWTConstants.OKTA_GROUPS_HEADER, oktaGroupsJWT) + } + .build() + } + .switchIfEmpty(Mono.just(exchange.request)) // drop back in original unmodified request if not an app + .flatMap { request -> + chain.filter(exchange.mutate().request(request).build()) + } + } + } +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsJWTWriter.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsJWTWriter.kt new file mode 100644 index 00000000000..0533882ed21 --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsJWTWriter.kt @@ -0,0 +1,64 @@ +package gov.cdc.prime.reportstream.auth.service + +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.crypto.RSASSASigner +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT +import gov.cdc.prime.reportstream.shared.StringUtilities.base64Decode +import gov.cdc.prime.reportstream.shared.auth.jwt.OktaGroupsJWT +import gov.cdc.prime.reportstream.shared.auth.jwt.OktaGroupsJWTConstants +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.stereotype.Service +import java.time.Clock +import java.time.Duration +import java.util.Date +import java.util.UUID + +@Service +class OktaGroupsJWTWriter( + private val jwtConfig: OktaGroupsJWTConfig, + private val clock: Clock, +) { + + /** + * generate and sign our custom JWT containing Okta group information for a particular application + */ + fun write(model: OktaGroupsJWT): String { + val now = clock.instant() + val expires = now.plus(jwtConfig.ttl) + val nbf = now.minus(jwtConfig.nbf) + val claimsSetBuilder = JWTClaimsSet.Builder() + .subject(model.appId) + .issuer(jwtConfig.issuer) + .jwtID(UUID.randomUUID().toString()) + .issueTime(Date.from(now)) + .notBeforeTime(Date.from(nbf)) + .expirationTime(Date.from(expires)) + .claim(OktaGroupsJWTConstants.OKTA_GROUPS_JWT_GROUP_CLAIM, model.groups) + + val signedJWT = SignedJWT( + JWSHeader.Builder(JWSAlgorithm.RS256).build(), + claimsSetBuilder.build() + ) + + signedJWT.sign(RSASSASigner(jwtConfig.jwtPrivateKeyJWK.toRSAKey())) + + return signedJWT.serialize() + } + + /** + * Configuration for Submissions microservice + */ + @ConfigurationProperties(prefix = "okta.jwt") + data class OktaGroupsJWTConfig( + private val jwtEncodedPrivateKeyJWK: String, + val ttl: Duration, + val nbf: Duration, + val issuer: String, + ) { + // JWK json format + val jwtPrivateKeyJWK: JWK = JWK.parse(jwtEncodedPrivateKeyJWK.base64Decode()) + } +} \ No newline at end of file diff --git a/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsService.kt b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsService.kt new file mode 100644 index 00000000000..d25edf1c5da --- /dev/null +++ b/auth/src/main/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsService.kt @@ -0,0 +1,20 @@ +package gov.cdc.prime.reportstream.auth.service + +import gov.cdc.prime.reportstream.auth.client.OktaGroupsClient +import gov.cdc.prime.reportstream.shared.auth.jwt.OktaGroupsJWT +import org.springframework.stereotype.Service + +@Service +class OktaGroupsService( + private val oktaGroupsClient: OktaGroupsClient, + private val oktaGroupsJWTWriter: OktaGroupsJWTWriter, +) { + + /** + * Grab Okta groups from the Okta API and write the JWT + */ + suspend fun generateOktaGroupsJWT(appId: String): String { + val groups = oktaGroupsClient.getApplicationGroups(appId) + return oktaGroupsJWTWriter.write(OktaGroupsJWT(appId, groups)) + } +} \ No newline at end of file diff --git a/auth/src/main/resources/application.yml b/auth/src/main/resources/application.yml index eea5a2c8ec6..e60d2ed073c 100644 --- a/auth/src/main/resources/application.yml +++ b/auth/src/main/resources/application.yml @@ -1,21 +1,41 @@ spring: application: name: "auth" + profiles: + active: "local" security: oauth2: resourceserver: opaquetoken: # Set client secret in SPRING_SECURITY_OAUTH2_RESOURCESERVER_OPAQUETOKEN_CLIENT_SECRET env variable client-id: 0oaek8tip2lhrhHce1d7 introspection-uri: https://reportstream.oktapreview.com/oauth2/ausekaai7gUuUtHda1d7/v1/introspect + cloud: + gateway: + routes: + - id: reports_route + uri: http://localhost:8880 + predicates: + - Path=/api/v1/reports + filters: + - AppendOktaGroups server.port: 9000 app: environment: local -# submissions microservice configuration -submissions: - baseUrl: http://localhost:8080 +okta: + adminClient: + orgUrl: https://reportstream.oktapreview.com + clientId: 0oahfz3wazBEHJLEL1d7 + requiredScopes: + - okta.apps.read + # set base 64 encoded pem string in OKTA_ADMIN_CLIENT_API_ENCODED_PRIVATE_KEY env variable + jwt: + nbf: 5s + ttl: 5m + issuer: http://localhost:9000 + jwtEncodedPrivateKeyJWK: eyJwIjoieC1yaFlLVVM2TnpRd0FCYjVmakNEMGl1NlM0MFAySUtQd3pnaUJqU3VjTDZ3VFpoYU4tbTBfZHBEdGhwMHZVQjQyR01LQ2ZHNklWNlFHSV91eF80c3I1QTBRSF9sY09jNC1DbTBWal9Udy1DcFRYbnRaVlJSSDlSN00tdDJ1WXNvT1ZyaXJsUUwxdW1ybThPdjkzVnBUWnJ4QlB6UXBlejZvVGJxZUQ0WF9jIiwia3R5IjoiUlNBIiwicSI6InlYYlhQamJHRmo0cUN3RWJqSUpMQ3hQS1FkTnduX2MtQ0M0ZVBma3UyNTR2VW1jeE1IQ1Q0UGtmQ3ZiTXlKU2plWUxzRFZwY0M1eThqeGJwLXpVMUF3WDJvZk15ZEhJa0RfSW51cjVXM1J2U1ZGUFFqR01YVk5Qc09LU1hxRUUzb3BfSU9QZEtUcXhBR3NZMkhYVEZDYVJSU25SdHMwQnV4ZS10SlZjbnpHcyIsImQiOiJQdmk3YnpOZy1ZekZQcWxsRlNHdXk0UnV5cGE0SDctN1lIZnZEb1pKVktqZFZIcHBqVHNsbjRmZWxicEYxQWJ1c2R2blJnU2QzSHVWQWVTTi1rT2dRbDR6SlNJLThvdDhSd29EQU4wSXNadGg0UDhSbTMwa0JFcFdMR0xPb3Q3ZkFWRDRMSzJFTjVEUlU5dWVVWmNoaFpmcDJwaGl2cHJ4TVluTnlhMzNXSm1oTHdSUFRRVjBIY3JmYk9kVG82a2FHUllfZkpCdHRjd2QzWlQ0dzljOXd6ZzFJT0t6Ul96S2g5VkR1b2Y3SWF0ekNab0ZzeUZ5ZHgwbHZxalZjRXAxSllQMTFBY0VOaUdYOHBSTXpzVktmaUhuNlBET2ZoSXBTU1lBd21wYWI3WGF6V2ZWbEZyam5VQXlpN2wyZV90U1lwMGc1RnpZcjB6NDZrTWo1YnBsIiwiZSI6IkFRQUIiLCJxaSI6IkdWbTR6WllDeTNVNEF6TmhSdE5FWFhzVl9ZYm9CZXR5SS1FRjhhcHNsUGc4YTBhMElzXzFZTTZndTQ4ZlNndVE1WFRpN2RVMURJLTZaR2JSbmVvMXVGb0R1M2RsVGR1dXdrcWx6XzJIREphY1dWZjFWQkRSNkdBSjJ3eWNXckdMM0VkM0k4eGtBeHUzMnlYUGJ0YjlpUy1pRF9WQmo1LVJoSktIUXhHbGNSTSIsImRwIjoid0p5V1JHMEd5UUJteDNZUkZJTVZSWEI3eFFIVktQUW1keFRMQjVVVEFoTFBVWFE1YWJlQm5sdVRCdENQTk1jRjZMTkZQRE1HdTJST290V0dIWjN5R1JTZ2tqN2dwc1J1MWtiTnNvbVNnZk9wcGM5SHpYVnRkUmRPTVdEdVdpYkZfTWJOVkR5eS1zM015LWNJU09kTVBmOHUyUjEzbEVOZ19xUy1sdS1fbllVIiwiZHEiOiJyRXd2MzJ4VzB4VU5QZVlQbW9hZ0NYUS1hVGVjdmFKazhmZ0hNemRXUk1zdmE0a0hmNGI0WWRLTkl3Slp0ejJ2NWE3N2xKdnYxcHFRaE11ekJuM0Z2YlV1N2VpaEFRZlJJYllYRmxYTTBrTUdDY3E0dENmV18xeFRUVW91emQ0ZzU3dEJNTDhGVk8xcDBid3M4ZG80M1hzamJzck9PeHhpNEhPUG9EeS1zOHMiLCJuIjoiblZRNVQ1MFk3Q3NBZkhfWllzQjBhRVFHYnlYcklQdDU1dG5OTGZNSWQwbWJYM1ljdVB6cVVQbVJzYlhiNmNPYmlXT1k1Znk2UGxfaEZDeUs2em9qU1JxaFdCd2dMcHo2NUg3NVJ2bVk5Y2FQbndLSXdPOWhpS2ZHTW5DMkdvS3U2S1otcFFLNXlHaUUwRGhVRGE0Q3gxa0h4NlJUUzVoSXlHZHRFOXk5eGlCU0RJODI0eXVwZ1Z5SkYtU1cteHBINVFYdy1saUxGdVBwSFpnWnQyNEhVRlBPM1JqcXJXTmNkWUZ0S00tZGhzWkxMQXp0cEZSMGlqY3M1SEdjbUVlWnRpaVBJYzFpUF9NWlZGSV9SbnVQdzBxbENFRDF4UGdqeXhzT3ZScDlIdU85NDVfenlib2tyYzlVbTljZnpTTEd3WmdJSW9HZVdtc2VRYWR0Zy1ud1BRIn0= # Ensure these are disabled in production springdoc: @@ -29,3 +49,4 @@ springdoc: # level: # web: debug # org.springframework.web: debug +# com.okta: debug diff --git a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/client/OktaGroupsClientTest.kt b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/client/OktaGroupsClientTest.kt new file mode 100644 index 00000000000..40fcd31459a --- /dev/null +++ b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/client/OktaGroupsClientTest.kt @@ -0,0 +1,86 @@ +package gov.cdc.prime.reportstream.auth.client + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.okta.sdk.resource.api.ApplicationGroupsApi +import com.okta.sdk.resource.model.ApplicationGroupAssignment +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals + +class OktaGroupsClientTest { + + class Fixture { + val appId = "appId" + + // truncated response from staging + val apiResponse = """ + [ + { + "id":"00gek8f3iuksaVp1e1d7", + "_embedded":{ + "group":{ + "id":"00gek8f3iuksaVp1e1d7", + "objectClass":[ + "okta:user_group" + ], + "type":"OKTA_GROUP", + "profile":{ + "name":"ArnejTestGroup", + "description":"Using this group to play around with scopes" + } + } + } + }, + { + "id":"00g9fxoz8jR9JEbhp1d7", + "priority":2, + "_embedded":{ + "group":{ + "id":"00g9fxoz8jR9JEbhp1d7", + "objectClass":[ + "okta:user_group" + ], + "type":"OKTA_GROUP", + "profile":{ + "name":"DHflexion", + "description":"Flexion org receiver group" + } + } + } + } + ] + """.trimIndent() + + val mapper = jacksonObjectMapper() + var parsedResponse: List = mapper + .readValue( + apiResponse, + mapper.typeFactory.constructCollectionType(List::class.java, ApplicationGroupAssignment::class.java) + ) + + val applicationGroupsApi: ApplicationGroupsApi = mockk() + val client = OktaGroupsClient(applicationGroupsApi) + } + + @Test + fun `fetch groups returns group names`() { + val f = Fixture() + + every { + f.applicationGroupsApi.listApplicationGroupAssignments( + f.appId, + null, + null, + null, + "group" + ) + }.returns(f.parsedResponse) + + assertEquals( + runBlocking { f.client.getApplicationGroups(f.appId) }, + listOf("ArnejTestGroup", "DHflexion") + ) + } +} \ No newline at end of file diff --git a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/config/TestOktaClientConfig.kt b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/config/TestOktaClientConfig.kt new file mode 100644 index 00000000000..f5446568763 --- /dev/null +++ b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/config/TestOktaClientConfig.kt @@ -0,0 +1,26 @@ +package gov.cdc.prime.reportstream.auth.config + +import com.okta.sdk.resource.api.ApplicationGroupsApi +import com.okta.sdk.resource.client.ApiClient +import io.mockk.mockk +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile + +/** + * We don't want the Okta client to actually attempt to connect to staging Okta during tests + */ +@TestConfiguration +@Profile("test") +class TestOktaClientConfig { + + @Bean + fun apiClient(): ApiClient { + return mockk() + } + + @Bean + fun applicationGroupsApi(): ApplicationGroupsApi { + return mockk() + } +} \ No newline at end of file diff --git a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthControllerTest.kt b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthControllerTest.kt index 9a643e15c11..46fef5e71b0 100644 --- a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthControllerTest.kt +++ b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/controller/HealthControllerTest.kt @@ -1,11 +1,13 @@ package gov.cdc.prime.reportstream.auth.controller import gov.cdc.prime.reportstream.auth.AuthApplicationConstants +import gov.cdc.prime.reportstream.auth.config.TestOktaClientConfig import gov.cdc.prime.reportstream.auth.model.ApplicationStatus import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Import import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.reactive.server.WebTestClient import kotlin.test.Test @@ -13,6 +15,7 @@ import kotlin.test.Test @ExtendWith(SpringExtension::class) @SpringBootTest @AutoConfigureWebTestClient +@Import(TestOktaClientConfig::class) class HealthControllerTest @Autowired constructor( private val webTestClient: WebTestClient, ) { diff --git a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/filter/AppendOktaGroupsGatewayFilterFactoryTest.kt b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/filter/AppendOktaGroupsGatewayFilterFactoryTest.kt new file mode 100644 index 00000000000..384ed4852ae --- /dev/null +++ b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/filter/AppendOktaGroupsGatewayFilterFactoryTest.kt @@ -0,0 +1,128 @@ +package gov.cdc.prime.reportstream.auth.filter + +import com.github.tomakehurst.wiremock.client.WireMock.aResponse +import com.github.tomakehurst.wiremock.client.WireMock.get +import com.github.tomakehurst.wiremock.client.WireMock.stubFor +import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo +import gov.cdc.prime.reportstream.auth.config.TestOktaClientConfig +import gov.cdc.prime.reportstream.auth.service.OktaGroupsService +import io.mockk.coEvery +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock +import org.springframework.cloud.gateway.route.RouteLocator +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Import +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOpaqueToken +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.test.web.reactive.server.WebTestClient + +/** + * The testing strategy here was modeled off how Spring Cloud Gateway tested their own add request filter. + * + * A Wiremock endpoint is set up to take the request header and return it as a part of the body. We then check + * that the body matches what we expect. + */ +@ExtendWith(SpringExtension::class) +@AutoConfigureWebTestClient +@SpringBootTest +@AutoConfigureWireMock(port = 0) +@Import(TestOktaClientConfig::class) +class AppendOktaGroupsGatewayFilterFactoryTest @Autowired constructor( + private val client: WebTestClient, + private val oktaGroupsService: OktaGroupsService, +) { + + @TestConfiguration + class Config( + @Value("\${wiremock.server.port}") val port: Int, + ) { + + @Bean + fun oktaGroupsService(): OktaGroupsService { + return mockk() + } + + @Bean + fun appendOktaGroupsGatewayFilterFactory(): AppendOktaGroupsGatewayFilterFactory { + return AppendOktaGroupsGatewayFilterFactory(oktaGroupsService()) + } + + @Bean + fun testRouteLocator(builder: RouteLocatorBuilder): RouteLocator { + val filterFactory = appendOktaGroupsGatewayFilterFactory() + return builder.routes() + .route("wiremock_route") { + it + .path("/get") + .filters { filter -> + filter.filter(filterFactory.apply()) + } + .uri("http://localhost:$port") + } + .build() + } + } + + @BeforeEach + fun setUp() { + stubFor( + get( + urlEqualTo("/get") + ) + .willReturn( + aResponse() + .withTransformers("response-template") + .withBody("{{ request.headers.Okta-Groups }}") // reflect the request header back into the body + ) + ) + } + + @Test + fun `Successfully pass Okta-Groups header when sender scope is present`() { + val expectedJwt = "okta-groups-jwt" + + coEvery { oktaGroupsService.generateOktaGroupsJWT(any()) } + .returns(expectedJwt) + + client + .mutateWith( + mockOpaqueToken() + .attributes { map -> + map["sub"] = "appId" + map["scope"] = listOf("sender") + } + ) + .get() + .uri("/get") + .exchange() + .expectBody(String::class.java) + .isEqualTo(expectedJwt) + } + + @Test + fun `Do not pass Okta-Groups header when organization scope is present`() { + client + .mutateWith( + mockOpaqueToken() + .attributes { map -> + map["sub"] = "email@cdc.gov" + map["scope"] = listOf("openid", "email") + map["organization"] = listOf("org") + } + ) + .get() + .uri("/get") + .exchange() + .expectBody() + .isEmpty + } +} \ No newline at end of file diff --git a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsJWTWriterTest.kt b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsJWTWriterTest.kt new file mode 100644 index 00000000000..ba395c37461 --- /dev/null +++ b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsJWTWriterTest.kt @@ -0,0 +1,63 @@ +package gov.cdc.prime.reportstream.auth.service + +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.crypto.RSASSAVerifier +import com.nimbusds.jwt.SignedJWT +import gov.cdc.prime.reportstream.auth.util.KeyGenerationUtils +import gov.cdc.prime.reportstream.shared.StringUtilities.base64Encode +import gov.cdc.prime.reportstream.shared.auth.jwt.OktaGroupsJWT +import java.time.Clock +import java.time.Duration +import java.time.Instant +import java.time.ZoneId +import kotlin.test.Test +import kotlin.test.assertEquals + +class OktaGroupsJWTWriterTest { + + inner class Fixture { + val now = Instant.now() + private val fixedClock = Clock.fixed(now, ZoneId.systemDefault()) + + val pair = KeyGenerationUtils.generateRSAKeyPair() + val privateKey = pair.first + val publicKey = pair.second + + val verifier = RSASSAVerifier(publicKey) + + val config = OktaGroupsJWTWriter.OktaGroupsJWTConfig( + jwtEncodedPrivateKeyJWK = privateKey.toJSONString().base64Encode(), + ttl = Duration.ofMinutes(5), + nbf = Duration.ofSeconds(5), + issuer = "I'm the issuer" + ) + + val oktaGroups = OktaGroupsJWT("appId", listOf("DHSender_org")) + + val jwtWriter = OktaGroupsJWTWriter(config, fixedClock) + } + + @Test + fun `successfully write JWT`() { + val f = Fixture() + + val jwt = f.jwtWriter.write(f.oktaGroups) + + val parsed = SignedJWT.parse(jwt) + val claims = parsed.jwtClaimsSet + + // expected signing algorithm + assertEquals(parsed.header.algorithm, JWSAlgorithm.RS256) + + // all expected claims there + assertEquals(claims.subject, f.oktaGroups.appId) + assertEquals(claims.getStringListClaim("groups"), f.oktaGroups.groups) + assertEquals(claims.issuer, "I'm the issuer") + assertEquals(claims.issueTime.toInstant().epochSecond, f.now.epochSecond) + assertEquals(claims.notBeforeTime.toInstant().epochSecond, f.now.minusSeconds(5).epochSecond) + assertEquals(claims.expirationTime.toInstant().epochSecond, f.now.plusSeconds(5 * 60).epochSecond) + + // correctly signed + assertEquals(parsed.verify(f.verifier), true) + } +} \ No newline at end of file diff --git a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsServiceTest.kt b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsServiceTest.kt new file mode 100644 index 00000000000..31957da20d4 --- /dev/null +++ b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/service/OktaGroupsServiceTest.kt @@ -0,0 +1,40 @@ +package gov.cdc.prime.reportstream.auth.service + +import gov.cdc.prime.reportstream.auth.client.OktaGroupsClient +import gov.cdc.prime.reportstream.shared.auth.jwt.OktaGroupsJWT +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals + +class OktaGroupsServiceTest { + + inner class Fixture { + val appId = "appId" + val groups = listOf("group1", "group2") + val oktaGroupsJWT = OktaGroupsJWT(appId, groups) + val jwt = "jwt" + + val oktaGroupsClient: OktaGroupsClient = mockk() + val oktaGroupsJWTWriter: OktaGroupsJWTWriter = mockk() + + val service = OktaGroupsService(oktaGroupsClient, oktaGroupsJWTWriter) + } + + @Test + fun `write JWT given the groups`() { + val f = Fixture() + + coEvery { f.oktaGroupsClient.getApplicationGroups(f.appId) } + .returns(f.groups) + every { f.oktaGroupsJWTWriter.write(f.oktaGroupsJWT) } + .returns(f.jwt) + + assertEquals( + runBlocking { f.service.generateOktaGroupsJWT(f.appId) }, + f.jwt + ) + } +} \ No newline at end of file diff --git a/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/util/KeyGenerationUtils.kt b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/util/KeyGenerationUtils.kt new file mode 100644 index 00000000000..5fd6c30a6b5 --- /dev/null +++ b/auth/src/test/kotlin/gov/cdc/prime/reportstream/auth/util/KeyGenerationUtils.kt @@ -0,0 +1,45 @@ +package gov.cdc.prime.reportstream.auth.util + +import com.nimbusds.jose.jwk.RSAKey +import gov.cdc.prime.reportstream.shared.StringUtilities.base64Encode +import java.security.KeyPairGenerator +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import kotlin.test.Ignore +import kotlin.test.Test + +/** + * Handy RSA key generation scripts in JUnit test form for easy running + */ +class KeyGenerationUtils { + + /** + * If you remove the @Ignore annotation below and run this test you can create + * a random 2048-bit RSA key pair. This will be useful for key rotations. + */ + @Ignore + @Test + fun generateAndPrintRSAKeyPair() { + val (privateJWK, publicJWK) = generateRSAKeyPair() + + println("private JWK: $privateJWK") + println("public JWK: $publicJWK") + + println("Encoded private JWK: ${privateJWK.toJSONString().base64Encode()}") + println("Encoded public JWK: ${publicJWK.toJSONString().base64Encode()}") + } + + companion object { + fun generateRSAKeyPair(): Pair { + val keyGen = KeyPairGenerator.getInstance("RSA") + keyGen.initialize(2048) + val keyPair = keyGen.generateKeyPair() + + val privateJWK = RSAKey.Builder(keyPair.public as RSAPublicKey) + .privateKey(keyPair.private as RSAPrivateKey) + .build() + val publicJWK = privateJWK.toPublicJWK() + return Pair(privateJWK, publicJWK) + } + } +} \ No newline at end of file diff --git a/auth/src/test/resources/application.yml b/auth/src/test/resources/application.yml index 0ec6f98f25f..68b84ac2249 100644 --- a/auth/src/test/resources/application.yml +++ b/auth/src/test/resources/application.yml @@ -2,7 +2,7 @@ spring: application: name: "auth" profiles: - active: test + active: "test" security: oauth2: resourceserver: @@ -10,8 +10,43 @@ spring: client-id: mockClient client-secret: mockSecret introspection-uri: https://localhost:9999/oauth2/default/v1/introspect # should never be hit + cloud: + gateway: + routes: + - id: test_route + uri: http://fakeurl.io + predicates: + - Path=/get + filters: + - AppendOktaGroups +server.port: 9000 +app: + environment: local -server.port: 9000 -app.environment: local -submissions.baseUrl: http://localhost:8080 \ No newline at end of file +okta: + adminClient: + orgUrl: https://reportstream.oktapreview.com + clientId: 0oahfz3wazBEHJLEL1d7 + requiredScopes: + - okta.apps.read + apiEncodedPrivateKey: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQ0Njd0dBN0JLUG9oZlQKYmZwdW9vZGFIU3o4VSs2cmVrUEFMdzdNbVJMSHovb3JkZ2pmaWl3RFIzaCtjSkpkVEZBd0NiSFJlVUFpZ0VIYwovZWtMTEpKak1LQUlpM2tJOTJJV0M2bUNRckErRVUweTZsRGpxRGNtZG1Gak9YZUJVUVRtYklzQVROdnU4KzZiCkY0bndWZnE2YTFuUmRuRGJOemNYaE9TbTRlV2JDcmMvUTVIYUZGY2F4S3E5NVRobjBPdXJFbG82TVgwTjJtNUMKT28zTjQvMjArZ21Rc2cyamtiMjdqdGNSVjZIUHBmRFhRZHMwNmZRVzhGZVMxZlIrN1pDOEdzbEIwMjlhL1ZXTAp4QWNIL240cE5FSldSM04vL3JNYys1R2VtOHM2MlJNVEVCbmZBK1BEL01DMWJtN1I0ZVVHTXNKOUgwRHBIY1pMCmtlaWdRZnlaQWdNQkFBRUNnZ0VBQlRGdzRPSUZCeVRMMEFYenowMjNGN1pMb1N2eU4yOWhuUmdDZmRDbU44QW0KMUc0WmdsU3MxZUNWZ21zVzJKSCtLenRua2RCQzVseTJ3b1oxTFpXenFqRTRYYjYzcmN5elllOUs2ejJlYUlvbAp5WjRjWkVQQkZrM21LSjRVRE5qZDJoSitJaC90TFlFV2dhUVpRTEVianlwUTVBN1VCVllZWWw1Ty8vbkVPenpPCmE5cDBhckdIeDRHelBTMnAxeVZhSThQUkMyUnFsNUdnTUxnRlhxTmNZaXpsbmYrNzhLaWtRSzExYkhCTXRCRlUKbzdDWWd5Z0NOQWMxa3lGSXJOSVNxYldFaWJ0OGJYMElVQ2Fpem1RTUtuRG83dXRVbnA0ZTZwU1lQbkU5SVkvdApVdTNKNXVqaktEUDdxUk1wdUNEMDU3WjNHbXI5YWRkcmw0dnFWS2szZFFLQmdRQzMzWVB5YlgzTXRQalRoeXM0CmpWSk5LL2VOTFJ2RjgxTUplS0xIa3VIbkUzSHZzL0ovU2ZVL1U2S2JHT3pOQlBzR3ZwUzZsczMrWUd1TTd4WHYKd3ZZODc0eVBZTS9oRkUrTWhEbVNDZWNmdm1qZTRDU2RBYjFPRWQ5ZHRjYTJHVU5FNEhBRHlVWVRPeWt0c0hndwp4UnZiWDlHZk1ERHdURVhudXVjSDhLcFZ6d0tCZ1FDMW9LZ2xKbGFFelJKUk1QSnE3RlY2WUV4NEo3cVB3aG1GClZJTkNHQUZOSWxhUjRDWG5OYjFma0htM1VkVmdyV3lKby94OCtZSDRweXZWZjNWM0NUWjRhMzdMOVRtQjBTNksKYU9IOFplc295Mi9tOWlqRk9wT3FwQ0ZTaFJmWTUyU29WanVISnFPY0I4aE52Vm1vK1dDVGl5blFzWDhveXE2Mgp1eHEvcVJBSkZ3S0JnUUN4MjlKYm5KYm9ndGVBcDJ5ajAvRWRQYjdHRGpDamwvRm5aQTd5eDU5SERJUld2OWVBClVtYXV6NVNvTzhBMXd1K2hZcEkwdk5TZmtWMzRndjdSWStNV3B4TnRUdFZJZ1lGQ0NGWTRjdVBrelNoZEVLM2EKUTJpQU1NSEZ3S1ZzV1p1ODhPN3FlclVTdlZQa0lxVGhhSXE5OXo2cm9zNTBaUlBxU2Q1YXkrKzUrUUtCZ0d5QgpQZkp6cE54UlpzLzZYZGhpdCs0VCtac09vUFdoRDM0SHJ5S2RGS253Q2FlOE1PaWZ3aktGTFRISFFhSXYrTmVCCmtDVlpLYnhTb20wNWFBTmxEWldESW96V1F6UzZzd01kQldTenZuandrRGw2ZFlEZUxibVR0QlNJVG1iV1ZkdjcKS0RUbGNIaVdiYU9EcXp5M1Btcm1pR1NVcFZMSlF2Y0hjRU52ekpTaEFvR0JBSUxIa2U3aG1Md0JWRDU1SEwzUQpBMDFHUGVWTk9DeCthTlNMMHdTbVlXc3F5MDcvRHAzc3hVVUN0Q1YvN25JY2hnVjFDQTZ4dCs0MkF5VWhzVW9kCmh2MnNQTUw2OFZBSGVSQ3lwTDh5bW5seWZjNmpMS2FqaHBWSmxQcWduSUVNTGZvNXM0UXB1VG9RVUkrZXI3dGIKSTlGRVBGYzlKa21hYWlzcjkxOHNnT0IyCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= + jwt: + nbf: 5s + ttl: 5m + issuer: http://localhost:9000 + jwtEncodedPrivateKeyJWK: eyJwIjoieC1yaFlLVVM2TnpRd0FCYjVmakNEMGl1NlM0MFAySUtQd3pnaUJqU3VjTDZ3VFpoYU4tbTBfZHBEdGhwMHZVQjQyR01LQ2ZHNklWNlFHSV91eF80c3I1QTBRSF9sY09jNC1DbTBWal9Udy1DcFRYbnRaVlJSSDlSN00tdDJ1WXNvT1ZyaXJsUUwxdW1ybThPdjkzVnBUWnJ4QlB6UXBlejZvVGJxZUQ0WF9jIiwia3R5IjoiUlNBIiwicSI6InlYYlhQamJHRmo0cUN3RWJqSUpMQ3hQS1FkTnduX2MtQ0M0ZVBma3UyNTR2VW1jeE1IQ1Q0UGtmQ3ZiTXlKU2plWUxzRFZwY0M1eThqeGJwLXpVMUF3WDJvZk15ZEhJa0RfSW51cjVXM1J2U1ZGUFFqR01YVk5Qc09LU1hxRUUzb3BfSU9QZEtUcXhBR3NZMkhYVEZDYVJSU25SdHMwQnV4ZS10SlZjbnpHcyIsImQiOiJQdmk3YnpOZy1ZekZQcWxsRlNHdXk0UnV5cGE0SDctN1lIZnZEb1pKVktqZFZIcHBqVHNsbjRmZWxicEYxQWJ1c2R2blJnU2QzSHVWQWVTTi1rT2dRbDR6SlNJLThvdDhSd29EQU4wSXNadGg0UDhSbTMwa0JFcFdMR0xPb3Q3ZkFWRDRMSzJFTjVEUlU5dWVVWmNoaFpmcDJwaGl2cHJ4TVluTnlhMzNXSm1oTHdSUFRRVjBIY3JmYk9kVG82a2FHUllfZkpCdHRjd2QzWlQ0dzljOXd6ZzFJT0t6Ul96S2g5VkR1b2Y3SWF0ekNab0ZzeUZ5ZHgwbHZxalZjRXAxSllQMTFBY0VOaUdYOHBSTXpzVktmaUhuNlBET2ZoSXBTU1lBd21wYWI3WGF6V2ZWbEZyam5VQXlpN2wyZV90U1lwMGc1RnpZcjB6NDZrTWo1YnBsIiwiZSI6IkFRQUIiLCJxaSI6IkdWbTR6WllDeTNVNEF6TmhSdE5FWFhzVl9ZYm9CZXR5SS1FRjhhcHNsUGc4YTBhMElzXzFZTTZndTQ4ZlNndVE1WFRpN2RVMURJLTZaR2JSbmVvMXVGb0R1M2RsVGR1dXdrcWx6XzJIREphY1dWZjFWQkRSNkdBSjJ3eWNXckdMM0VkM0k4eGtBeHUzMnlYUGJ0YjlpUy1pRF9WQmo1LVJoSktIUXhHbGNSTSIsImRwIjoid0p5V1JHMEd5UUJteDNZUkZJTVZSWEI3eFFIVktQUW1keFRMQjVVVEFoTFBVWFE1YWJlQm5sdVRCdENQTk1jRjZMTkZQRE1HdTJST290V0dIWjN5R1JTZ2tqN2dwc1J1MWtiTnNvbVNnZk9wcGM5SHpYVnRkUmRPTVdEdVdpYkZfTWJOVkR5eS1zM015LWNJU09kTVBmOHUyUjEzbEVOZ19xUy1sdS1fbllVIiwiZHEiOiJyRXd2MzJ4VzB4VU5QZVlQbW9hZ0NYUS1hVGVjdmFKazhmZ0hNemRXUk1zdmE0a0hmNGI0WWRLTkl3Slp0ejJ2NWE3N2xKdnYxcHFRaE11ekJuM0Z2YlV1N2VpaEFRZlJJYllYRmxYTTBrTUdDY3E0dENmV18xeFRUVW91emQ0ZzU3dEJNTDhGVk8xcDBid3M4ZG80M1hzamJzck9PeHhpNEhPUG9EeS1zOHMiLCJuIjoiblZRNVQ1MFk3Q3NBZkhfWllzQjBhRVFHYnlYcklQdDU1dG5OTGZNSWQwbWJYM1ljdVB6cVVQbVJzYlhiNmNPYmlXT1k1Znk2UGxfaEZDeUs2em9qU1JxaFdCd2dMcHo2NUg3NVJ2bVk5Y2FQbndLSXdPOWhpS2ZHTW5DMkdvS3U2S1otcFFLNXlHaUUwRGhVRGE0Q3gxa0h4NlJUUzVoSXlHZHRFOXk5eGlCU0RJODI0eXVwZ1Z5SkYtU1cteHBINVFYdy1saUxGdVBwSFpnWnQyNEhVRlBPM1JqcXJXTmNkWUZ0S00tZGhzWkxMQXp0cEZSMGlqY3M1SEdjbUVlWnRpaVBJYzFpUF9NWlZGSV9SbnVQdzBxbENFRDF4UGdqeXhzT3ZScDlIdU85NDVfenlib2tyYzlVbTljZnpTTEd3WmdJSW9HZVdtc2VRYWR0Zy1ud1BRIn0= + +# Ensure these are disabled in production +springdoc: + swagger-ui: + path: /swagger/ui.html + api-docs: + path: /swagger/api-docs + +#Uncomment for verbose logging +logging: + level: + web: debug + org.springframework.web: debug + com.okta: debug diff --git a/buildSrc/src/main/kotlin/reportstream.project-conventions.gradle.kts b/buildSrc/src/main/kotlin/reportstream.project-conventions.gradle.kts index 34c9d7b3d99..f8f9a6bd43d 100644 --- a/buildSrc/src/main/kotlin/reportstream.project-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/reportstream.project-conventions.gradle.kts @@ -69,10 +69,10 @@ dependencies { // Common test dependencies testImplementation(kotlin("test-junit5")) testImplementation("io.mockk:mockk:1.13.11") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.3") testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") - testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.3") + testImplementation("org.junit.jupiter:junit-jupiter:5.11.3") testImplementation("org.testcontainers:testcontainers:1.19.8") testImplementation("org.testcontainers:junit-jupiter:1.19.8") testImplementation("org.testcontainers:postgresql:1.19.8") diff --git a/frontend-react/.pa11yci b/frontend-react/.pa11yci new file mode 100644 index 00000000000..8f03097fb80 --- /dev/null +++ b/frontend-react/.pa11yci @@ -0,0 +1,28 @@ +{ + "defaults": { + "timeout": 120000, + "viewport": { + "width": 320, + "height": 480 + }, + "chromeLaunchConfig": { + "executablePath": "/usr/bin/google-chrome-stable", + "args": ["--no-sandbox"] + }, + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" + }, + "reporters": [ + "cli", + "pa11y-ci-reporter-cli-summary" + ], + "standard": "WCAG2AAA", + "runners": ["htmlcs", "axe"] + }, + "urls": [ + { + "url": "https://reportstream.cdc.gov/*", + "ignore": [] + } + ] +} diff --git a/frontend-react/README.md b/frontend-react/README.md index 1b4ca5897f6..ff940173e9d 100644 --- a/frontend-react/README.md +++ b/frontend-react/README.md @@ -5,8 +5,8 @@ Our new React front-end is easy to get up and running on your machine. First, ensure the following dependencies installed: -- `node` (see .nvmrc for version specification) via `nvm` -- `yarn` package manager +- `node` (see .nvmrc for version specification) via `nvm` +- `yarn` package manager Use the directions here to install nvm: https://github.com/nvm-sh/nvm#install--update-script Then: @@ -276,19 +276,19 @@ These overwrites will ONLY be scoped to your particular component. ### General -- [Best Practices](docs/best-practices.md) -- [Content](docs/content.md) -- [Data fetching patterns](docs/data-fetching-patterns.md) -- [Feature flags](docs/feature-flags.md) -- [RS Auth Element](docs/rs-auth-element.md) -- [RS Error Boundary and Suspense](docs/rs-error-boundary-and-suspense.md) -- [RS IA Content System](docs/rs-ia-content-system.md) -- [RS IA Template System](docs/rs-ia-template-system.md) -- [RS React Testing Network Calls](docs/rs-react-testing-network-calls.md) -- [Test Conventions](docs/test-conventions.md) +- [Best Practices](docs/best-practices.md) +- [Content](docs/content.md) +- [Data fetching patterns](docs/data-fetching-patterns.md) +- [Feature flags](docs/feature-flags.md) +- [RS Auth Element](docs/rs-auth-element.md) +- [RS Error Boundary and Suspense](docs/rs-error-boundary-and-suspense.md) +- [RS IA Content System](docs/rs-ia-content-system.md) +- [RS IA Template System](docs/rs-ia-template-system.md) +- [RS React Testing Network Calls](docs/rs-react-testing-network-calls.md) +- [Test Conventions](docs/test-conventions.md) ### Proposals -- [Permissions Layer](docs/proposals/0001-permissions-layer-proposal.md) -- [Domain Driven Directory Structure](docs/proposals/0002-domain-driven-directory-structure.md) -- [USWDS React Components](docs/proposals/0003-uswds-react-components.md) +- [Permissions Layer](docs/proposals/0001-permissions-layer-proposal.md) +- [Domain Driven Directory Structure](docs/proposals/0002-domain-driven-directory-structure.md) +- [USWDS React Components](docs/proposals/0003-uswds-react-components.md) diff --git a/frontend-react/e2e/pages/authenticated/admin/receiver-status.ts b/frontend-react/e2e/pages/authenticated/admin/receiver-status.ts index 2f0673d9892..baa3808c4c4 100644 --- a/frontend-react/e2e/pages/authenticated/admin/receiver-status.ts +++ b/frontend-react/e2e/pages/authenticated/admin/receiver-status.ts @@ -4,6 +4,9 @@ import type { RSOrganizationSettings } from "../../../../src/config/endpoints/se import { RSReceiverStatus } from "../../../../src/hooks/api/UseReceiversConnectionStatus/UseReceiversConnectionStatus"; import { createStatusTimePeriodData, + isConnectionResultMatch, + MATCHING_FILTER_CLASSNAME_MAP, + MatchingFilter, SUCCESS_RATE_CLASSNAME_MAP, SuccessRate, } from "../../../../src/pages/admin/receiver-dashboard/utils"; @@ -403,9 +406,13 @@ export class AdminReceiverStatusPage extends BasePage { } async testReceiverStatusDisplay(isSmoke = false) { + const selectedSuccessRate = this.filterFormInputs.successType.value as SuccessRate; const [startDate, endDate] = this.filterFormInputs.dateRange.value; const statusRows = this.receiverStatusRowsLocator; - await expect(statusRows).toHaveCount(new Set(this.receiverStatus?.map((r) => r.receiverId)).size); + const timePeriodData = this.timePeriodData.filter( + (d) => selectedSuccessRate === SuccessRate.UNDEFINED || d.successRateType === selectedSuccessRate, + ); + await expect(statusRows).toHaveCount(timePeriodData.length); const expectedDaysText = [ dateShortFormat(startDate), @@ -415,7 +422,7 @@ export class AdminReceiverStatusPage extends BasePage { for (const [ i, { days, successRate, organizationName, receiverName, successRateType }, - ] of this.timePeriodData.entries()) { + ] of timePeriodData.entries()) { const { title, display, days: daysLoc } = statusRows.nthCustom(i); const expectedTitleText = this.getExpectedReceiverStatusRowTitle( @@ -438,9 +445,31 @@ export class AdminReceiverStatusPage extends BasePage { const daySlices = daysLoc.nthCustom(i).timePeriods; await expect(daySlices).toHaveCount(timePeriods.length); - for (const [i, { successRateType }] of timePeriods.entries()) { + for (const [i, { successRateType, entries }] of timePeriods.entries()) { const sliceEle = daySlices.nth(i); - const expectedClass = new RegExp(SUCCESS_RATE_CLASSNAME_MAP[successRateType]); + // filtering status isn't recalculated automatically, so do it here + const isResultFilterMatch = + this.filterFormInputs.resultMessage.value === "" + ? undefined + : entries.some((e) => + isConnectionResultMatch( + e.connectionCheckResult, + this.filterFormInputs.resultMessage.value, + ), + ); + const classStr = [ + SUCCESS_RATE_CLASSNAME_MAP[successRateType], + MATCHING_FILTER_CLASSNAME_MAP[ + isResultFilterMatch == null + ? MatchingFilter.NO_FILTER + : isResultFilterMatch + ? MatchingFilter.FILTER_IS_MATCHED + : MatchingFilter.FILTER_NOT_MATCHED + ], + ] + .filter(Boolean) + .join(" "); + const expectedClass = new RegExp(classStr); await expect(sliceEle).toBeVisible(); await expect(sliceEle).toHaveClass(expectedClass); @@ -456,17 +485,15 @@ export class AdminReceiverStatusPage extends BasePage { } async testReceiverName() { - const { organizationName, receiverName, successRate } = - this.timePeriodData[1]; + const { organizationName, receiverName, successRate } = this.timePeriodData[1]; const receiversStatusRows = this.receiverStatusRowsLocator; const expectedReceiverStatusRow = receiversStatusRows.nthCustom(0); - const expectedReceiverStatusRowTitle = - this.getExpectedReceiverStatusRowTitle( - organizationName, - receiverName, - successRate, - ); + const expectedReceiverStatusRowTitle = this.getExpectedReceiverStatusRowTitle( + organizationName, + receiverName, + successRate, + ); await expect(receiversStatusRows).toHaveCount(this.timePeriodData.length); @@ -491,8 +518,8 @@ export class AdminReceiverStatusPage extends BasePage { const dayI = 0; const timePeriodI = 2; const entryI = 0; - const {days} = this.timePeriodData[receiverI]; - const {connectionCheckResult} = days[dayI].timePeriods[timePeriodI].entries[entryI]; + const { days } = this.timePeriodData[receiverI]; + const { connectionCheckResult } = days[dayI].timePeriods[timePeriodI].entries[entryI]; const receiversStatusRows = this.receiverStatusRowsLocator; @@ -500,11 +527,11 @@ export class AdminReceiverStatusPage extends BasePage { resultMessage: connectionCheckResult, }); - for (const [i, {days}] of this.timePeriodData.entries()) { + for (const [i, { days }] of this.timePeriodData.entries()) { const isRowExpected = i === receiverI; const row = receiversStatusRows.nthCustom(i); - for (const [i, {timePeriods}] of days.entries()) { + for (const [i, { timePeriods }] of days.entries()) { const isDayExpected = isRowExpected && i === dayI; const rowDay = row.days.nthCustom(i); @@ -537,20 +564,18 @@ export class AdminReceiverStatusPage extends BasePage { const link = row.title.getByRole("link", { name: organizationName, exact: true }).first(); const expectedUrl = this.getExpectedStatusOrganizationUrl(i); await expect(link).toBeVisible(); - const p = this.page.route( - `/api/settings/organizations/${organizationName}`, - (route) => - route.fulfill({ - json: { - description: "fake", - filters: [], - name: organizationName, - jurisdiction: "fake", - version: 0, - createdAt: "", - createdBy: "", - } satisfies RSOrganizationSettings, - }), + const p = this.page.route(`/api/settings/organizations/${organizationName}`, (route) => + route.fulfill({ + json: { + description: "fake", + filters: [], + name: organizationName, + jurisdiction: "fake", + version: 0, + createdAt: "", + createdBy: "", + } satisfies RSOrganizationSettings, + }), ); await link.click(); await expect(this.page).toHaveURL(expectedUrl); @@ -624,9 +649,7 @@ export class AdminReceiverStatusPage extends BasePage { }); await expect(link).toBeVisible(); await link.click(); - await expect(this.page).toHaveURL( - this.getExpectedStatusReceiverUrl(i), - ); + await expect(this.page).toHaveURL(this.getExpectedStatusReceiverUrl(i)); await this.page.goBack(); if (isSmoke && i === 0) { diff --git a/frontend-react/e2e/spec/all/public/developer-resources/api/api.spec.ts b/frontend-react/e2e/spec/all/public/developer-resources/api/api.spec.ts index 16f850ce267..03a5cf40943 100644 --- a/frontend-react/e2e/spec/all/public/developer-resources/api/api.spec.ts +++ b/frontend-react/e2e/spec/all/public/developer-resources/api/api.spec.ts @@ -1,6 +1,6 @@ import { developerResourcesApiSideNav } from "../../../../../helpers/internal-links"; import { DeveloperResourcesApiPage } from "../../../../../pages/public/developer-resources/api/api"; -import { test as baseTest, expect } from "../../../../../test.js"; +import { test as baseTest } from "../../../../../test.js"; export interface Fixtures { developerResourcesApiPage: DeveloperResourcesApiPage; @@ -45,14 +45,6 @@ test.describe( await developerResourcesApiPage.testSidenav(developerResourcesApiSideNav); }); - test("pdf file download works", async ({ developerResourcesApiPage }) => { - const downloadPromise = developerResourcesApiPage.page.waitForEvent("download"); - await developerResourcesApiPage.page.getByRole("link", { name: "downloadable PDF" }).click(); - const download = await downloadPromise; - expect(download.suggestedFilename()).toMatch(/^.+\.pdf$/); - await download.cancel(); - }); - test("has correct title + heading", async ({ developerResourcesApiPage }) => { await developerResourcesApiPage.testHeader(); }); diff --git a/frontend-react/e2e/spec/chromium-only/authenticated/receiver-status-page-user-flow.spec.ts b/frontend-react/e2e/spec/chromium-only/authenticated/receiver-status-page-user-flow.spec.ts index 8cb59f03528..152996d902e 100644 --- a/frontend-react/e2e/spec/chromium-only/authenticated/receiver-status-page-user-flow.spec.ts +++ b/frontend-react/e2e/spec/chromium-only/authenticated/receiver-status-page-user-flow.spec.ts @@ -1,4 +1,5 @@ import { addDays, endOfDay, startOfDay, subDays } from "date-fns"; +import { SuccessRate } from "../../../../src/pages/admin/receiver-dashboard/utils"; import { AdminReceiverStatusPage } from "../../../pages/authenticated/admin/receiver-status"; import { test as baseTest, expect, logins } from "../../../test"; @@ -36,74 +37,70 @@ const test = baseTest.extend({ }, }); -test.describe("Admin Receiver Status Page", +test.describe( + "Admin Receiver Status Page", { // TODO: Investigate Admin Receiver Status Page › functions correctly › receiver statuses › time period modals - // tag: "@smoke", - }, () => { + tag: "@smoke", + }, + () => { test.use({ storageState: logins.admin.path }); test.describe("displays correctly", () => { test.describe("header", () => { - test( - "has correct title + heading", - async ({ adminReceiverStatusPage }) => { - await adminReceiverStatusPage.testHeader(); - }, - ); + test("has correct title + heading", async ({ adminReceiverStatusPage }) => { + await adminReceiverStatusPage.testHeader(); + }); }); - test.describe( - "filters", - () => { - test("date range", async ({ adminReceiverStatusPage }) => { - const { button, label, modalOverlay, valueDisplay } = - adminReceiverStatusPage.filterFormInputs.dateRange; - await expect(label).toBeVisible(); - await expect(button).toBeVisible(); - await expect(valueDisplay).toHaveText(adminReceiverStatusPage.expectedDateRangeLabelText); - await expect(modalOverlay).toBeHidden(); - }); + test.describe("filters", () => { + test("date range", async ({ adminReceiverStatusPage }) => { + const { button, label, modalOverlay, valueDisplay } = + adminReceiverStatusPage.filterFormInputs.dateRange; + await expect(label).toBeVisible(); + await expect(button).toBeVisible(); + await expect(valueDisplay).toHaveText(adminReceiverStatusPage.expectedDateRangeLabelText); + await expect(modalOverlay).toBeHidden(); + }); - test("receiver name", async ({ adminReceiverStatusPage }) => { - const { input, expectedTooltipText, label, tooltip, expectedDefaultValue } = - adminReceiverStatusPage.filterFormInputs.receiverName; - await expect(label).toBeVisible(); - await expect(input).toBeVisible(); - await expect(input).toHaveValue(expectedDefaultValue); - - await expect(tooltip).toBeHidden(); - await input.hover(); - await expect(tooltip).toBeVisible(); - await expect(tooltip).toHaveText(expectedTooltipText); - }); + test("receiver name", async ({ adminReceiverStatusPage }) => { + const { input, expectedTooltipText, label, tooltip, expectedDefaultValue } = + adminReceiverStatusPage.filterFormInputs.receiverName; + await expect(label).toBeVisible(); + await expect(input).toBeVisible(); + await expect(input).toHaveValue(expectedDefaultValue); + + await expect(tooltip).toBeHidden(); + await input.hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText(expectedTooltipText); + }); - test("results message", async ({ adminReceiverStatusPage }) => { - const { input, expectedTooltipText, label, tooltip, expectedDefaultValue } = - adminReceiverStatusPage.filterFormInputs.resultMessage; - await expect(label).toBeVisible(); - await expect(input).toBeVisible(); - await expect(input).toHaveValue(expectedDefaultValue); - - await expect(tooltip).toBeHidden(); - await input.hover(); - await expect(tooltip).toBeVisible(); - await expect(tooltip).toHaveText(expectedTooltipText); - }); + test("results message", async ({ adminReceiverStatusPage }) => { + const { input, expectedTooltipText, label, tooltip, expectedDefaultValue } = + adminReceiverStatusPage.filterFormInputs.resultMessage; + await expect(label).toBeVisible(); + await expect(input).toBeVisible(); + await expect(input).toHaveValue(expectedDefaultValue); + + await expect(tooltip).toBeHidden(); + await input.hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText(expectedTooltipText); + }); - test("success type", async ({ adminReceiverStatusPage }) => { - const { input, expectedTooltipText, label, tooltip, expectedDefaultValue } = - adminReceiverStatusPage.filterFormInputs.successType; - await expect(label).toBeVisible(); - await expect(input).toBeVisible(); - await expect(input).toHaveValue(expectedDefaultValue); - - await expect(tooltip).toBeHidden(); - await input.hover(); - await expect(tooltip).toBeVisible(); - await expect(tooltip).toHaveText(expectedTooltipText); - }); - }, - ); + test("success type", async ({ adminReceiverStatusPage }) => { + const { input, expectedTooltipText, label, tooltip, expectedDefaultValue } = + adminReceiverStatusPage.filterFormInputs.successType; + await expect(label).toBeVisible(); + await expect(input).toBeVisible(); + await expect(input).toHaveValue(expectedDefaultValue); + + await expect(tooltip).toBeHidden(); + await input.hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText(expectedTooltipText); + }); + }); // Failures here indicate potential misalignment of playwright/browser timezone test.describe("receiver statuses", () => { @@ -114,85 +111,72 @@ test.describe("Admin Receiver Status Page", }); test.describe("has footer", () => { - test("has footer and explicit scroll to footer and scroll to top", - async ({ - adminReceiverStatusPage, - }) => { - await adminReceiverStatusPage.testFooter(); - }); + test("has footer and explicit scroll to footer and scroll to top", async ({ + adminReceiverStatusPage, + }) => { + await adminReceiverStatusPage.testFooter(); + }); }); }); test.describe("functions correctly", () => { test.describe("filters", () => { - test.describe( - "date range", - () => { - test("works through calendar", async ({ adminReceiverStatusPage }) => { - const { valueDisplay } = adminReceiverStatusPage.filterFormInputs.dateRange; - const now = new Date(); - const targetFrom = startOfDay(subDays(now, 3)); - const targetTo = addDays(endOfDay(now), 1); - - const reqUrl = await adminReceiverStatusPage.updateFilters({ - dateRange: { - value: [targetFrom, targetTo], - }, - }); - expect(reqUrl).toBeDefined(); - - await expect(valueDisplay).toHaveText( - adminReceiverStatusPage.expectedDateRangeLabelText, - ); - expect(Object.fromEntries(reqUrl!.searchParams.entries())).toMatchObject({ - start_date: targetFrom.toISOString(), - end_date: targetTo.toISOString(), - }); + test.describe("date range", () => { + test("works through calendar", async ({ adminReceiverStatusPage }) => { + const { valueDisplay } = adminReceiverStatusPage.filterFormInputs.dateRange; + const now = new Date(); + const targetFrom = startOfDay(subDays(now, 3)); + const targetTo = addDays(endOfDay(now), 1); + + const reqUrl = await adminReceiverStatusPage.updateFilters({ + dateRange: { + value: [targetFrom, targetTo], + }, + }); + expect(reqUrl).toBeDefined(); + + await expect(valueDisplay).toHaveText(adminReceiverStatusPage.expectedDateRangeLabelText); + expect(Object.fromEntries(reqUrl!.searchParams.entries())).toMatchObject({ + start_date: targetFrom.toISOString(), + end_date: targetTo.toISOString(), + }); + }); + + test("works through textboxes", async ({ adminReceiverStatusPage }) => { + const { valueDisplay } = adminReceiverStatusPage.filterFormInputs.dateRange; + await expect(adminReceiverStatusPage.receiverStatusRowsLocator).not.toHaveCount(0); + const now = new Date(); + const targetFrom = startOfDay(subDays(now, 3)); + const targetTo = addDays(endOfDay(now), 1); + + const reqUrl = await adminReceiverStatusPage.updateFilters({ + dateRange: { + value: [targetFrom, targetTo], + }, }); - test("works through textboxes", async ({ adminReceiverStatusPage }) => { - const { valueDisplay } = adminReceiverStatusPage.filterFormInputs.dateRange; - await expect(adminReceiverStatusPage.receiverStatusRowsLocator).not.toHaveCount(0); - const now = new Date(); - const targetFrom = startOfDay(subDays(now, 3)); - const targetTo = addDays(endOfDay(now), 1); - - const reqUrl = await adminReceiverStatusPage.updateFilters({ - dateRange: { - value: [targetFrom, targetTo], - }, - }); - - expect(reqUrl).toBeDefined(); - - await expect(valueDisplay).toHaveText( - adminReceiverStatusPage.expectedDateRangeLabelText, - ); - expect(Object.fromEntries(reqUrl!.searchParams.entries())).toMatchObject({ - start_date: targetFrom.toISOString(), - end_date: targetTo.toISOString(), - }); + expect(reqUrl).toBeDefined(); + + await expect(valueDisplay).toHaveText(adminReceiverStatusPage.expectedDateRangeLabelText); + expect(Object.fromEntries(reqUrl!.searchParams.entries())).toMatchObject({ + start_date: targetFrom.toISOString(), + end_date: targetTo.toISOString(), }); - }, - ); + }); + }); - // TODO: revisit after filters have been fixed per ticket #15737 - test.skip("receiver name", async ({adminReceiverStatusPage, isMockDisabled}) => { - test.skip(!isMockDisabled, "Mocks are ENABLED, skipping 'receiver name' test"); - const {organizationName, receiverName, successRate} = - adminReceiverStatusPage.timePeriodData[1]; + test("receiver name", async ({ adminReceiverStatusPage }) => { + const { organizationName, receiverName, successRate } = adminReceiverStatusPage.timePeriodData[1]; const receiversStatusRows = adminReceiverStatusPage.receiverStatusRowsLocator; + await expect(receiversStatusRows).toHaveCount(adminReceiverStatusPage.timePeriodData.length); const defaultReceiversStatusRowsCount = await receiversStatusRows.count(); const expectedReceiverStatusRow = receiversStatusRows.nthCustom(0); - const expectedReceiverStatusRowTitle = - adminReceiverStatusPage.getExpectedReceiverStatusRowTitle( - organizationName, - receiverName, - successRate, - ); - - expect(defaultReceiversStatusRowsCount).toBe(adminReceiverStatusPage.timePeriodData.length); + const expectedReceiverStatusRowTitle = adminReceiverStatusPage.getExpectedReceiverStatusRowTitle( + organizationName, + receiverName, + successRate, + ); await adminReceiverStatusPage.updateFilters({ receiverName, @@ -208,82 +192,33 @@ test.describe("Admin Receiver Status Page", expect(defaultReceiversStatusRowsCount).toBe(adminReceiverStatusPage.timePeriodData.length); }); - test.skip("result message", async ({adminReceiverStatusPage, isMockDisabled}) => { - test.skip(!isMockDisabled, "Mocks are ENABLED, skipping 'result message' test"); + test("result message", async ({ adminReceiverStatusPage }) => { // get first entry's result from all-fail receiver's first day -> third time period const receiverI = 0; const dayI = 0; const timePeriodI = 2; const entryI = 0; - const {days} = adminReceiverStatusPage.timePeriodData[receiverI]; - const {connectionCheckResult} = days[dayI].timePeriods[timePeriodI].entries[entryI]; - - const receiversStatusRows = adminReceiverStatusPage.receiverStatusRowsLocator; + const { days } = adminReceiverStatusPage.timePeriodData[receiverI]; + const { connectionCheckResult } = days[dayI].timePeriods[timePeriodI].entries[entryI]; await adminReceiverStatusPage.updateFilters({ resultMessage: connectionCheckResult, }); - for (const [i, {days}] of adminReceiverStatusPage.timePeriodData.entries()) { - const row = receiversStatusRows.nthCustom(i); - - for (const [i, {timePeriods}] of days.entries()) { - const rowDay = row.days.nthCustom(i); - - for (const [i] of timePeriods.entries()) { - const rowDayTimePeriod = rowDay.timePeriods.nth(i); - - await expect(rowDayTimePeriod).toHaveClass(/success-result-hidden/); - } - } - } + await adminReceiverStatusPage.testReceiverStatusDisplay(); await adminReceiverStatusPage.resetFilters(); - // TODO: revisit after filters have been fixed per ticket #15737 - // await adminReceiverStatusPage.testReceiverStatusDisplay(); + await adminReceiverStatusPage.testReceiverStatusDisplay(); }); - test.skip("success type", async ({ adminReceiverStatusPage, isMockDisabled }) => { - test.skip(!isMockDisabled, "Mocks are ENABLED, skipping 'success type' test"); - const [failRow, ,] = adminReceiverStatusPage.timePeriodData; - const failRowTitle = adminReceiverStatusPage.getExpectedReceiverStatusRowTitle( - failRow.organizationName, - failRow.receiverName, - failRow.successRate, - ); - // const mixedRowTitle = adminReceiverStatusPage.getExpectedReceiverStatusRowTitle( - // mixedRow.organizationName, - // mixedRow.receiverName, - // mixedRow.successRate, - // ); - - const receiversStatusRows = adminReceiverStatusPage.receiverStatusRowsLocator; - const defaultReceiversStatusRowsCount = await receiversStatusRows.count(); - const expectedRow = receiversStatusRows.nthCustom(0); - - expect(defaultReceiversStatusRowsCount).toBe(adminReceiverStatusPage.timePeriodData.length); - - await adminReceiverStatusPage.updateFilters({ - successType: "ALL_FAILURE", - }); - let receiversStatusRowsCount = await receiversStatusRows.count(); - - expect(receiversStatusRowsCount).toBeGreaterThanOrEqual(1); - await expect(expectedRow.title).toHaveText(failRowTitle); - - await adminReceiverStatusPage.updateFilters({ - successType: "MIXED_SUCCESS", - }); - receiversStatusRowsCount = await receiversStatusRows.count(); - expect(receiversStatusRowsCount).toBeGreaterThanOrEqual(1); - // TODO: revisit after filters have been fixed per ticket #15737 - // await expect(expectedRow.title).toHaveText(mixedRowTitle); + test("success type", async ({ adminReceiverStatusPage }) => { + const successTypes = [SuccessRate.ALL_FAILURE, SuccessRate.MIXED_SUCCESS, SuccessRate.UNDEFINED]; - // await adminReceiverStatusPage.resetFilters(); - // receiversStatusRowsCount = await receiversStatusRows.count(); - // - // expect(receiversStatusRowsCount).toBe(defaultReceiversStatusRowsCount); + for (const successType of successTypes) { + await adminReceiverStatusPage.updateFilterSuccessType(successType); + await adminReceiverStatusPage.testReceiverStatusDisplay(); + } }); }); @@ -320,7 +255,7 @@ test.describe("Admin Receiver Status Page", }); }); - test.skip("time period modals", async ({ adminReceiverStatusPage }) => { + test("time period modals", async ({ adminReceiverStatusPage }) => { const result = await adminReceiverStatusPage.testReceiverTimePeriodModals(true); expect(result).toBe(true); }); @@ -336,4 +271,5 @@ test.describe("Admin Receiver Status Page", }); }); }); - }); + }, +); diff --git a/frontend-react/pa11y-net-logger.js b/frontend-react/pa11y-net-logger.js new file mode 100644 index 00000000000..f2d34eabb34 --- /dev/null +++ b/frontend-react/pa11y-net-logger.js @@ -0,0 +1,14 @@ +/* eslint-env node */ +/* eslint-disable no-undef */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/require-await */ + +module.exports = async (page) => { + page.on("request", (request) => { + console.log(`Request: ${request.method()} ${request.url()}`); + }); + + page.on("response", (response) => { + console.log(`Response: ${response.status()} ${response.url()}`); + }); +}; diff --git a/frontend-react/pa11y.json b/frontend-react/pa11y.json new file mode 100644 index 00000000000..16498d44c54 --- /dev/null +++ b/frontend-react/pa11y.json @@ -0,0 +1,20 @@ +{ + "defaults": { + "timeout": 120000, + "wait": "[data-loaded='true']", + "chromeLaunchConfig": { + "executablePath": "/usr/bin/google-chrome-stable", + "args": ["--no-sandbox", "--disable-setuid-sandbox"] + }, + "headers": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" + }, + "actions": "./pa11y-net-logger.js" + }, + "urls": [ + { + "url": "https://reportstream.cdc.gov/", + "ignore": [] + } + ] +} diff --git a/frontend-react/package.json b/frontend-react/package.json index fad46aa994e..29afe0ce1a4 100644 --- a/frontend-react/package.json +++ b/frontend-react/package.json @@ -5,23 +5,22 @@ "type": "module", "npmClient": "yarn", "dependencies": { - "@microsoft/applicationinsights-react-js": "^17.3.3", - "@microsoft/applicationinsights-web": "^3.3.3", + "@microsoft/applicationinsights-react-js": "^17.3.4", + "@microsoft/applicationinsights-web": "^3.3.4", "@okta/okta-react": "^6.9.0", - "@okta/okta-signin-widget": "^7.24.2", + "@okta/okta-signin-widget": "^7.26.1", "@rest-hooks/rest": "^3.0.3", - "@tanstack/react-query": "^5.59.15", - "@tanstack/react-query-devtools": "^5.59.15", + "@tanstack/react-query": "^5.62.2", + "@tanstack/react-query-devtools": "^5.62.2", "@trussworks/react-uswds": "^9.1.0", "@uswds/uswds": "3.7.1", - "axios": "^1.7.7", + "axios": "^1.7.8", "classnames": "^2.5.1", - "date-fns": "^3.6.0", + "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", - "dompurify": "^3.1.7", - "downloadjs": "^1.4.7", + "dompurify": "^3.2.3", "export-to-csv-fix-source-map": "^0.2.1", - "focus-trap-react": "^10.3.0", + "focus-trap-react": "^10.3.1", "history": "^5.3.0", "html-to-text": "^9.0.5", "lodash": "^4.17.21", @@ -32,18 +31,18 @@ "react-idle-timer": "^5.7.2", "react-loader-spinner": "^6.1.6", "react-markdown": "^9.0.1", - "react-query-kit": "^3.3.0", - "react-router": "^6.27.0", - "react-router-dom": "^6.27.0", + "react-query-kit": "^3.3.1", + "react-router": "~6.28.0", + "react-router-dom": "~6.28.0", "react-scroll-sync": "^0.11.2", "react-toastify": "^10.0.6", "rehype-raw": "^7.0.0", "rehype-slug": "^5.1.0", "rest-hooks": "^6.1.7", "sanitize-html": "^2.13.1", - "tsx": "^4.19.1", + "tsx": "^4.19.2", "use-deep-compare-effect": "^1.8.1", - "uuid": "^10.0.0", + "uuid": "^11.0.3", "web-vitals": "^3.4.0" }, "scripts": { @@ -110,36 +109,35 @@ ] }, "devDependencies": { - "@eslint/compat": "^1.2.2", - "@eslint/js": "^9.13.0", + "@eslint/compat": "^1.2.4", + "@eslint/js": "^9.17.0", "@mdx-js/react": "^3.1.0", "@mdx-js/rollup": "^3.1.0", - "@playwright/test": "^1.48.1", + "@playwright/test": "^1.49.1", "@rest-hooks/test": "^7.3.1", - "@storybook/addon-a11y": "^8.3.6", - "@storybook/addon-actions": "^8.3.6", - "@storybook/addon-essentials": "^8.3.6", - "@storybook/addon-interactions": "^8.3.6", - "@storybook/addon-links": "^8.3.6", - "@storybook/blocks": "^8.3.6", - "@storybook/components": "^8.3.6", - "@storybook/core-events": "^8.3.6", + "@storybook/addon-a11y": "^8.4.7", + "@storybook/addon-actions": "^8.4.7", + "@storybook/addon-essentials": "^8.4.7", + "@storybook/addon-interactions": "^8.4.7", + "@storybook/addon-links": "^8.4.7", + "@storybook/blocks": "^8.4.7", + "@storybook/components": "^8.4.7", + "@storybook/core-events": "^8.4.7", "@storybook/mdx2-csf": "1.1.0", - "@storybook/react": "^8.3.6", - "@storybook/react-vite": "^8.3.6", + "@storybook/react": "^8.4.7", + "@storybook/react-vite": "^8.4.7", "@storybook/testing-library": "^0.2.2", - "@storybook/theming": "^8.3.6", + "@storybook/theming": "^8.4.7", "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.6.2", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", - "@types/dompurify": "^3.0.5", + "@types/dompurify": "^3.2.0", "@types/dotenv-flow": "^3.3.3", - "@types/downloadjs": "^1.4.6", "@types/eslint__js": "^8.42.3", - "@types/github-slugger": "^1.3.0", + "@types/github-slugger": "^2.0.0", "@types/html-to-text": "^9.0.4", - "@types/lodash": "^4.17.12", + "@types/lodash": "^4.17.13", "@types/mdx": "^2.0.13", "@types/node": "^20.12.5", "@types/react": "18.3.11", @@ -147,56 +145,56 @@ "@types/react-router-dom": "^5.3.3", "@types/react-scroll-sync": "^0.9.0", "@types/sanitize-html": "^2.13.0", - "@vitejs/plugin-react": "^4.3.3", - "@vitest/coverage-istanbul": "^2.1.3", - "@vitest/ui": "^2.1.3", + "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-istanbul": "^2.1.8", + "@vitest/ui": "^2.1.8", "autoprefixer": "^10.4.20", - "browserslist": "^4.24.2", + "browserslist": "^4.24.3", "browserslist-useragent-regexp": "^4.1.3", - "chromatic": "^11.12.6", + "chromatic": "^11.20.2", "cross-env": "^7.0.3", "dotenv-flow": "^4.1.0", - "eslint": "9.13.0", + "eslint": "9.17.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest-dom": "^5.4.0", + "eslint-plugin-jest-dom": "^5.5.0", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-playwright": "^2.0.0", + "eslint-plugin-playwright": "^2.1.0", "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", - "eslint-plugin-storybook": "^0.10.1", - "eslint-plugin-testing-library": "^6.4.0", + "eslint-plugin-storybook": "^0.11.1", + "eslint-plugin-testing-library": "^7.1.1", "eslint-plugin-vitest": "^0.5.4", - "globals": "^15.11.0", - "husky": "^9.1.6", + "globals": "^15.13.0", + "husky": "^9.1.7", "jsdom": "^25.0.1", - "lint-staged": "^15.2.10", + "lint-staged": "^15.2.11", "mockdate": "^3.0.5", - "msw": "^2.4.11", - "msw-storybook-addon": "^2.0.3", + "msw": "^2.6.8", + "msw-storybook-addon": "^2.0.4", "npm-run-all": "^4.1.5", - "otpauth": "^9.3.4", + "otpauth": "^9.3.6", "patch-package": "^8.0.0", - "postcss": "^8.4.47", - "prettier": "^3.3.3", + "postcss": "^8.4.49", + "prettier": "^3.4.2", "react-error-boundary": "^4.1.2", "remark-frontmatter": "^5.0.0", "remark-mdx-frontmatter": "^5.0.0", "remark-mdx-toc": "^0.3.1", - "sass": "^1.80.3", - "storybook": "^8.3.6", - "storybook-addon-remix-react-router": "^3.0.1", + "sass": "^1.81.0", + "storybook": "^8.4.7", + "storybook-addon-remix-react-router": "^3.0.2", "ts-node": "^10.9.2", - "tslib": "^2.8.0", - "typescript": "^5.6.3", - "typescript-eslint": "^8.12.2", - "undici": "^6.20.1", - "vite": "^5.4.10", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "typescript-eslint": "^8.16.0", + "undici": "~6.20.1", + "vite": "^6.0.3", "vite-plugin-checker": "^0.8.0", - "vite-plugin-svgr": "^4.2.0", - "vitest": "^2.1.3" + "vite-plugin-svgr": "^4.3.0", + "vitest": "^2.1.8" }, "resolutions": { "@types/react": "18.3.11", diff --git a/frontend-react/public/assets/xlsm/20241204_ReportStream-Mapping-Template.xlsm b/frontend-react/public/assets/xlsm/20241204_ReportStream-Mapping-Template.xlsm new file mode 100644 index 00000000000..a6563f7d9e4 Binary files /dev/null and b/frontend-react/public/assets/xlsm/20241204_ReportStream-Mapping-Template.xlsm differ diff --git a/frontend-react/src/AppRouter.tsx b/frontend-react/src/AppRouter.tsx index 9cbbf55e334..b2c78dc8a94 100644 --- a/frontend-react/src/AppRouter.tsx +++ b/frontend-react/src/AppRouter.tsx @@ -7,6 +7,7 @@ import { SenderType } from "./utils/DataDashboardUtils"; import { lazyRouteMarkdown } from "./utils/LazyRouteMarkdown"; import { PERMISSIONS } from "./utils/UsefulTypes"; +const ReportTestingPage = lazy(() => import("./components/Admin/MessageTesting/MessageTesting")); /* Content Pages */ const Home = lazy(lazyRouteMarkdown(() => import("./content/home/index.mdx"))); const About = lazy(lazyRouteMarkdown(() => import("./content/about/index.mdx"))); @@ -437,6 +438,10 @@ export const appRoutes: RouteObject[] = [ path: "orgreceiversettings/org/:orgname/receiver/:receivername/action/:action", element: , }, + { + path: "orgreceiversettings/org/:orgname/receiver/:receivername/action/:action/message-testing", + element: , + }, { path: "orgsendersettings/org/:orgname/sender/:sendername/action/:action", element: , diff --git a/frontend-react/src/components/Admin/EditReceiverSettings.tsx b/frontend-react/src/components/Admin/EditReceiverSettings.tsx index efae9af94d4..cbc9e724721 100644 --- a/frontend-react/src/components/Admin/EditReceiverSettings.tsx +++ b/frontend-react/src/components/Admin/EditReceiverSettings.tsx @@ -3,17 +3,9 @@ import { FC, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { useController, useResource } from "rest-hooks"; -import { - CheckboxComponent, - DropdownComponent, - TextAreaComponent, - TextInputComponent, -} from "./AdminFormEdit"; +import { CheckboxComponent, DropdownComponent, TextAreaComponent, TextInputComponent } from "./AdminFormEdit"; import { AdminFormWrapper } from "./AdminFormWrapper"; -import { - ConfirmSaveSettingModal, - ConfirmSaveSettingModalRef, -} from "./CompareJsonModal"; +import { ConfirmSaveSettingModal, ConfirmSaveSettingModalRef } from "./CompareJsonModal"; import Title from "../../components/Title"; import config from "../../config"; import useSessionContext from "../../contexts/Session/useSessionContext"; @@ -21,11 +13,7 @@ import { showToast } from "../../contexts/Toast"; import useAppInsightsContext from "../../hooks/UseAppInsightsContext/UseAppInsightsContext"; import OrgReceiverSettingsResource from "../../resources/OrgReceiverSettingsResource"; import { jsonSortReplacer } from "../../utils/JsonSortReplacer"; -import { - getErrorDetailFromResponse, - getVersionWarning, - VersionWarningType, -} from "../../utils/misc"; +import { getErrorDetailFromResponse, getVersionWarning, VersionWarningType } from "../../utils/misc"; import { getListOfEnumValues, SampleTimingObj, @@ -43,35 +31,29 @@ interface EditReceiverSettingsFormProps { action: "edit" | "clone"; } -const EditReceiverSettingsForm: FC = ({ - orgname, - receivername, - action, -}) => { +const EditReceiverSettingsForm: FC = ({ orgname, receivername, action }) => { const { properties } = useAppInsightsContext(); const [loading, setLoading] = useState(false); const navigate = useNavigate(); const { activeMembership, authState } = useSessionContext(); const confirmModalRef = useRef(null); - const orgReceiverSettings: OrgReceiverSettingsResource = useResource( - OrgReceiverSettingsResource.detail(), - { orgname, receivername, action }, - ); + const orgReceiverSettings: OrgReceiverSettingsResource = useResource(OrgReceiverSettingsResource.detail(), { + orgname, + receivername, + action, + }); const { fetch: fetchController } = useController(); - const [orgReceiverSettingsOldJson, setOrgReceiverSettingsOldJson] = - useState(""); - const [orgReceiverSettingsNewJson, setOrgReceiverSettingsNewJson] = - useState(""); + const [orgReceiverSettingsOldJson, setOrgReceiverSettingsOldJson] = useState(""); + const [orgReceiverSettingsNewJson, setOrgReceiverSettingsNewJson] = useState(""); const { invalidate } = useController(); const modalRef = useRef(null); const ShowDeleteConfirm = (deleteItemId: string) => { modalRef?.current?.showModal({ title: "Confirm Delete", - message: - "Deleting a setting will only mark it deleted. It can be accessed via the revision history", + message: "Deleting a setting will only mark it deleted. It can be accessed via the revision history", okButtonText: "Delete", itemId: deleteItemId, }); @@ -89,10 +71,7 @@ const EditReceiverSettingsForm: FC = ({ navigate(-1); return true; } catch (e: any) { - showToast( - `Deleting item '${deleteItemId}' failed. ${e.toString()}`, - "error", - ); + showToast(`Deleting item '${deleteItemId}' failed. ${e.toString()}`, "error"); return false; } }; @@ -101,16 +80,13 @@ const EditReceiverSettingsForm: FC = ({ const accessToken = authState.accessToken?.accessToken; const organization = activeMembership?.parsedName; - const response = await fetch( - `${RS_API_URL}/api/settings/organizations/${orgname}/receivers/${receivername}`, - { - headers: { - "x-ms-session-id": properties.context.getSessionId(), - Authorization: `Bearer ${accessToken}`, - Organization: organization!, - }, + const response = await fetch(`${RS_API_URL}/api/settings/organizations/${orgname}/receivers/${receivername}`, { + headers: { + "x-ms-session-id": properties.context.getSessionId(), + Authorization: `Bearer ${accessToken}`, + Organization: organization!, }, - ); + }); return await response.json(); } @@ -120,20 +96,11 @@ const EditReceiverSettingsForm: FC = ({ // fetch original version setLoading(true); const latestResponse = await getLatestReceiverResponse(); - setOrgReceiverSettingsOldJson( - JSON.stringify(latestResponse, jsonSortReplacer, 2), - ); - setOrgReceiverSettingsNewJson( - JSON.stringify(orgReceiverSettings, jsonSortReplacer, 2), - ); - if ( - action === "edit" && - latestResponse?.version !== orgReceiverSettings?.version - ) { + setOrgReceiverSettingsOldJson(JSON.stringify(latestResponse, jsonSortReplacer, 2)); + setOrgReceiverSettingsNewJson(JSON.stringify(orgReceiverSettings, jsonSortReplacer, 2)); + if (action === "edit" && latestResponse?.version !== orgReceiverSettings?.version) { showToast(getVersionWarning(VersionWarningType.POPUP), "error"); - confirmModalRef?.current?.setWarning( - getVersionWarning(VersionWarningType.FULL, latestResponse), - ); + confirmModalRef?.current?.setWarning(getVersionWarning(VersionWarningType.FULL, latestResponse)); confirmModalRef?.current?.disableSave(); } @@ -142,10 +109,7 @@ const EditReceiverSettingsForm: FC = ({ } catch (e: any) { setLoading(false); const errorDetail = await getErrorDetailFromResponse(e); - showToast( - `Reloading receiver '${receivername}' failed with: ${errorDetail}`, - "error", - ); + showToast(`Reloading receiver '${receivername}' failed with: ${errorDetail}`, "error"); return false; } }; @@ -166,21 +130,16 @@ const EditReceiverSettingsForm: FC = ({ const latestResponse = await getLatestReceiverResponse(); if (latestResponse.version !== orgReceiverSettings?.version) { // refresh left-side panel in compare modal to make it obvious what has changed - setOrgReceiverSettingsOldJson( - JSON.stringify(latestResponse, jsonSortReplacer, 2), - ); + setOrgReceiverSettingsOldJson(JSON.stringify(latestResponse, jsonSortReplacer, 2)); showToast(getVersionWarning(VersionWarningType.POPUP), "error"); - confirmModalRef?.current?.setWarning( - getVersionWarning(VersionWarningType.FULL, latestResponse), - ); + confirmModalRef?.current?.setWarning(getVersionWarning(VersionWarningType.FULL, latestResponse)); confirmModalRef?.current?.disableSave(); return false; } const data = confirmModalRef?.current?.getEditedText(); - const receivernamelocal = - action === "clone" ? orgReceiverSettings.name : receivername; + const receivernamelocal = action === "clone" ? orgReceiverSettings.name : receivername; await fetchController( OrgReceiverSettingsResource.update(), @@ -196,10 +155,7 @@ const EditReceiverSettingsForm: FC = ({ } catch (e: any) { setLoading(false); const errorDetail = await getErrorDetailFromResponse(e); - showToast( - `Updating receiver '${receivername}' failed with: ${errorDetail}`, - "error", - ); + showToast(`Updating receiver '${receivername}' failed with: ${errorDetail}`, "error"); return false; } @@ -212,9 +168,7 @@ const EditReceiverSettingsForm: FC = ({ (orgReceiverSettings.name = v)} disabled={action === "edit"} /> @@ -262,29 +216,15 @@ const EditReceiverSettingsForm: FC = ({ - } + toolTip={} defaultvalue={orgReceiverSettings.jurisdictionalFilter} defaultnullvalue="[]" - savefunc={(v) => - (orgReceiverSettings.jurisdictionalFilter = v) - } + savefunc={(v) => (orgReceiverSettings.jurisdictionalFilter = v)} /> - } + toolTip={} defaultvalue={orgReceiverSettings.qualityFilter} defaultnullvalue="[]" savefunc={(v) => (orgReceiverSettings.qualityFilter = v)} @@ -293,20 +233,12 @@ const EditReceiverSettingsForm: FC = ({ fieldname={"reverseTheQualityFilter"} label={"Reverse the Quality Filter"} defaultvalue={orgReceiverSettings.reverseTheQualityFilter} - savefunc={(v) => - (orgReceiverSettings.reverseTheQualityFilter = v) - } + savefunc={(v) => (orgReceiverSettings.reverseTheQualityFilter = v)} /> - } + toolTip={} defaultvalue={orgReceiverSettings.routingFilter} defaultnullvalue="[]" savefunc={(v) => (orgReceiverSettings.routingFilter = v)} @@ -314,18 +246,10 @@ const EditReceiverSettingsForm: FC = ({ - } + toolTip={} defaultvalue={orgReceiverSettings.processingModeFilter} defaultnullvalue="[]" - savefunc={(v) => - (orgReceiverSettings.processingModeFilter = v) - } + savefunc={(v) => (orgReceiverSettings.processingModeFilter = v)} /> = ({ - } + toolTip={} defaultvalue={orgReceiverSettings.transport} defaultnullvalue={null} savefunc={(v) => (orgReceiverSettings.transport = v)} @@ -374,11 +296,7 @@ const EditReceiverSettingsForm: FC = ({ @@ -394,11 +312,7 @@ const EditReceiverSettingsForm: FC = ({ void saveReceiverData()} oldjson={orgReceiverSettingsOldJson} @@ -414,25 +328,17 @@ const EditReceiverSettingsForm: FC = ({ ); }; -interface EditReceiverSettingsParams extends Record { +export interface EditReceiverSettingsParams extends Record { orgname: string; receivername: string; action: "edit" | "clone"; } export function EditReceiverSettingsPage() { - const { orgname, receivername, action } = - useParams(); + const { orgname, receivername, action } = useParams(); return ( - - } - > + }> void; + setCurrentTestMessages: Dispatch>; + setOpenCustomMessage: (value: boolean) => void; +}) => { + const [text, setText] = useState(""); + const handleTextareaChange = (event: ChangeEvent) => { + setText(event.target.value); + }; + const handleAddCustomMessage = () => { + const dateCreated = new Date(); + setCurrentTestMessages([ + ...currentTestMessages, + { + dateCreated: dateCreated.toString(), + fileName: `Custom message ${customMessageNumber}`, + reportBody: text, + }, + ]); + setCustomMessageNumber(customMessageNumber + 1); + setText(""); + setOpenCustomMessage(false); + }; + + return ( +
+

Enter custom message

+

Custom messages do not save to the bank after you log out.

+