diff --git a/.env.example b/.env.example
index 05a888db0..059ec124f 100644
--- a/.env.example
+++ b/.env.example
@@ -123,8 +123,17 @@ INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET=
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
# azure app connection
-INF_APP_CONNECTION_AZURE_CLIENT_ID=
-INF_APP_CONNECTION_AZURE_CLIENT_SECRET=
+INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID=
+INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET=
+
+INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID=
+INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET=
+
+INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID=
+INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET=
+
+INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID=
+INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET=
# datadog
SHOULD_USE_DATADOG_TRACER=
diff --git a/.github/workflows/release_build_infisical_cli.yml b/.github/workflows/release_build_infisical_cli.yml
deleted file mode 100644
index 1c16b00b3..000000000
--- a/.github/workflows/release_build_infisical_cli.yml
+++ /dev/null
@@ -1,153 +0,0 @@
-name: Build and release CLI
-
-on:
- workflow_dispatch:
-
- push:
- # run only against tags
- tags:
- - "infisical-cli/v*.*.*"
-
-permissions:
- contents: write
-
-jobs:
- cli-integration-tests:
- name: Run tests before deployment
- uses: ./.github/workflows/run-cli-tests.yml
- secrets:
- CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
- CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
- CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
- CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
- CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
- CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
- CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
- CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
-
- npm-release:
- runs-on: ubuntu-latest
- env:
- working-directory: ./npm
- needs:
- - cli-integration-tests
- - goreleaser
- steps:
- - uses: actions/checkout@v3
- with:
- fetch-depth: 0
-
- - name: Extract version
- run: |
- VERSION=$(echo ${{ github.ref_name }} | sed 's/infisical-cli\/v//')
- echo "Version extracted: $VERSION"
- echo "CLI_VERSION=$VERSION" >> $GITHUB_ENV
-
- - name: Print version
- run: echo ${{ env.CLI_VERSION }}
-
- - name: Setup Node
- uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
- with:
- node-version: 20
- cache: "npm"
- cache-dependency-path: ./npm/package-lock.json
- - name: Install dependencies
- working-directory: ${{ env.working-directory }}
- run: npm install --ignore-scripts
-
- - name: Set NPM version
- working-directory: ${{ env.working-directory }}
- run: npm version ${{ env.CLI_VERSION }} --allow-same-version --no-git-tag-version
-
- - name: Setup NPM
- working-directory: ${{ env.working-directory }}
- run: |
- echo 'registry="https://registry.npmjs.org/"' > ./.npmrc
- echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ./.npmrc
-
- echo 'registry="https://registry.npmjs.org/"' > ~/.npmrc
- echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
- env:
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
-
- - name: Pack NPM
- working-directory: ${{ env.working-directory }}
- run: npm pack
-
- - name: Publish NPM
- working-directory: ${{ env.working-directory }}
- run: npm publish --tarball=./infisical-sdk-${{github.ref_name}} --access public --registry=https://registry.npmjs.org/
- env:
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
-
- goreleaser:
- runs-on: ubuntu-latest-8-cores
- needs: [cli-integration-tests]
- steps:
- - uses: actions/checkout@v3
- with:
- fetch-depth: 0
- - name: 🐋 Login to Docker Hub
- uses: docker/login-action@v2
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
- - name: 🔧 Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
- - run: git fetch --force --tags
- - run: echo "Ref name ${{github.ref_name}}"
- - uses: actions/setup-go@v3
- with:
- go-version: ">=1.19.3"
- cache: true
- cache-dependency-path: cli/go.sum
- - name: Setup for libssl1.0-dev
- run: |
- echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
- sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
- sudo apt update
- sudo apt-get install -y libssl1.0-dev
- - name: OSXCross for CGO Support
- run: |
- mkdir ../../osxcross
- git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
- - uses: goreleaser/goreleaser-action@v4
- with:
- distribution: goreleaser-pro
- version: v1.26.2-pro
- args: release --clean
- env:
- GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
- POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
- FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
- AUR_KEY: ${{ secrets.AUR_KEY }}
- GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- - uses: actions/setup-python@v4
- - run: pip install --upgrade cloudsmith-cli
- - uses: ruby/setup-ruby@354a1ad156761f5ee2b7b13fa8e09943a5e8d252
- with:
- ruby-version: "3.3" # Not needed with a .ruby-version, .tool-versions or mise.toml
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- - name: Install deb-s3
- run: gem install deb-s3
- - name: Configure GPG Key
- run: echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --batch --import
- env:
- GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
- GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }}
- - name: Publish to CloudSmith
- run: sh cli/upload_to_cloudsmith.sh
- env:
- CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
- INFISICAL_CLI_S3_BUCKET: ${{ secrets.INFISICAL_CLI_S3_BUCKET }}
- INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }}
- AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
- AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
- - name: Invalidate Cloudfront cache
- run: aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths '/deb/dists/stable/*'
- env:
- AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
- AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
- CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID }}
diff --git a/.github/workflows/run-cli-tests.yml b/.github/workflows/run-cli-tests.yml
deleted file mode 100644
index da6f507a7..000000000
--- a/.github/workflows/run-cli-tests.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-name: Go CLI Tests
-
-on:
- pull_request:
- types: [opened, synchronize]
- paths:
- - "cli/**"
-
- workflow_dispatch:
-
- workflow_call:
- secrets:
- CLI_TESTS_UA_CLIENT_ID:
- required: true
- CLI_TESTS_UA_CLIENT_SECRET:
- required: true
- CLI_TESTS_SERVICE_TOKEN:
- required: true
- CLI_TESTS_PROJECT_ID:
- required: true
- CLI_TESTS_ENV_SLUG:
- required: true
- CLI_TESTS_USER_EMAIL:
- required: true
- CLI_TESTS_USER_PASSWORD:
- required: true
- CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE:
- required: true
-jobs:
- test:
- defaults:
- run:
- working-directory: ./cli
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
- - name: Setup Go
- uses: actions/setup-go@v4
- with:
- go-version: "1.21.x"
- - name: Install dependencies
- run: go get .
- - name: Test with the Go CLI
- env:
- CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
- CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
- CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
- CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
- CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
- CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
- CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
- # INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
-
- run: go test -v -count=1 ./test
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
deleted file mode 100644
index e3147d650..000000000
--- a/.goreleaser.yaml
+++ /dev/null
@@ -1,241 +0,0 @@
-# This is an example .goreleaser.yml file with some sensible defaults.
-# Make sure to check the documentation at https://goreleaser.com
-# before:
-# hooks:
-# # You may remove this if you don't use go modules.
-# - cd cli && go mod tidy
-# # you may remove this if you don't need go generate
-# - cd cli && go generate ./...
-before:
- hooks:
- - ./cli/scripts/completions.sh
- - ./cli/scripts/manpages.sh
-
-monorepo:
- tag_prefix: infisical-cli/
- dir: cli
-
-builds:
- - id: darwin-build
- binary: infisical
- ldflags:
- - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
- flags:
- - -trimpath
- env:
- - CGO_ENABLED=1
- - CC=/home/runner/work/osxcross/target/bin/o64-clang
- - CXX=/home/runner/work/osxcross/target/bin/o64-clang++
- goos:
- - darwin
- ignore:
- - goos: darwin
- goarch: "386"
- dir: ./cli
-
- - id: all-other-builds
- env:
- - CGO_ENABLED=0
- binary: infisical
- ldflags:
- - -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- - -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
- flags:
- - -trimpath
- goos:
- - freebsd
- - linux
- - netbsd
- - openbsd
- - windows
- goarch:
- - "386"
- - amd64
- - arm
- - arm64
- goarm:
- - "6"
- - "7"
- ignore:
- - goos: windows
- goarch: "386"
- - goos: freebsd
- goarch: "386"
- dir: ./cli
-
-archives:
- - format_overrides:
- - goos: windows
- format: zip
- files:
- - ../README*
- - ../LICENSE*
- - ../manpages/*
- - ../completions/*
-
-release:
- replace_existing_draft: true
- mode: "replace"
-
-checksum:
- name_template: "checksums.txt"
-
-snapshot:
- name_template: "{{ .Version }}-devel"
-
-# publishers:
-# - name: fury.io
-# ids:
-# - infisical
-# dir: "{{ dir .ArtifactPath }}"
-# cmd: curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/infisical/
-
-brews:
- - name: infisical
- tap:
- owner: Infisical
- name: homebrew-get-cli
- commit_author:
- name: "Infisical"
- email: ai@infisical.com
- folder: Formula
- homepage: "https://infisical.com"
- description: "The official Infisical CLI"
- install: |-
- bin.install "infisical"
- bash_completion.install "completions/infisical.bash" => "infisical"
- zsh_completion.install "completions/infisical.zsh" => "_infisical"
- fish_completion.install "completions/infisical.fish"
- man1.install "manpages/infisical.1.gz"
- - name: "infisical@{{.Version}}"
- tap:
- owner: Infisical
- name: homebrew-get-cli
- commit_author:
- name: "Infisical"
- email: ai@infisical.com
- folder: Formula
- homepage: "https://infisical.com"
- description: "The official Infisical CLI"
- install: |-
- bin.install "infisical"
- bash_completion.install "completions/infisical.bash" => "infisical"
- zsh_completion.install "completions/infisical.zsh" => "_infisical"
- fish_completion.install "completions/infisical.fish"
- man1.install "manpages/infisical.1.gz"
-
-nfpms:
- - id: infisical
- package_name: infisical
- builds:
- - all-other-builds
- vendor: Infisical, Inc
- homepage: https://infisical.com/
- maintainer: Infisical, Inc
- description: The offical Infisical CLI
- license: MIT
- formats:
- - rpm
- - deb
- - apk
- - archlinux
- bindir: /usr/bin
- contents:
- - src: ./completions/infisical.bash
- dst: /etc/bash_completion.d/infisical
- - src: ./completions/infisical.fish
- dst: /usr/share/fish/vendor_completions.d/infisical.fish
- - src: ./completions/infisical.zsh
- dst: /usr/share/zsh/site-functions/_infisical
- - src: ./manpages/infisical.1.gz
- dst: /usr/share/man/man1/infisical.1.gz
-
-scoop:
- bucket:
- owner: Infisical
- name: scoop-infisical
- commit_author:
- name: "Infisical"
- email: ai@infisical.com
- homepage: "https://infisical.com"
- description: "The official Infisical CLI"
- license: MIT
-
-winget:
- - name: infisical
- publisher: infisical
- license: MIT
- homepage: https://infisical.com
- short_description: "The official Infisical CLI"
- repository:
- owner: infisical
- name: winget-pkgs
- branch: "infisical-{{.Version}}"
- pull_request:
- enabled: true
- draft: false
- base:
- owner: microsoft
- name: winget-pkgs
- branch: master
-
-aurs:
- - name: infisical-bin
- homepage: "https://infisical.com"
- description: "The official Infisical CLI"
- maintainers:
- - Infisical, Inc
- license: MIT
- private_key: "{{ .Env.AUR_KEY }}"
- git_url: "ssh://aur@aur.archlinux.org/infisical-bin.git"
- package: |-
- # bin
- install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
- # license
- install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE"
- # completions
- mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
- mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
- mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
- install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical"
- install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/_infisical"
- install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
- # man pages
- install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
-
-dockers:
- - dockerfile: docker/alpine
- goos: linux
- goarch: amd64
- use: buildx
- ids:
- - all-other-builds
- image_templates:
- - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64"
- - "infisical/cli:latest-amd64"
- build_flag_templates:
- - "--pull"
- - "--platform=linux/amd64"
- - dockerfile: docker/alpine
- goos: linux
- goarch: amd64
- use: buildx
- ids:
- - all-other-builds
- image_templates:
- - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64"
- - "infisical/cli:latest-arm64"
- build_flag_templates:
- - "--pull"
- - "--platform=linux/arm64"
-
-docker_manifests:
- - name_template: "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
- image_templates:
- - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-amd64"
- - "infisical/cli:{{ .Major }}.{{ .Minor }}.{{ .Patch }}-arm64"
- - name_template: "infisical/cli:latest"
- image_templates:
- - "infisical/cli:latest-amd64"
- - "infisical/cli:latest-arm64"
diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical
index d2b2a2d87..974ce4a42 100644
--- a/Dockerfile.fips.standalone-infisical
+++ b/Dockerfile.fips.standalone-infisical
@@ -34,6 +34,8 @@ ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG CAPTCHA_SITE_KEY
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
+ENV NODE_OPTIONS="--max-old-space-size=8192"
+
# Build
RUN npm run build
@@ -145,7 +147,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
&& cd openssl-3.1.2 \
&& ./Configure enable-fips \
&& make \
- && make install_fips
+ && make install_fips \
+ && cd / \
+ && rm -rf /openssl-build \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install Infisical CLI
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
@@ -186,12 +192,11 @@ ENV NODE_ENV production
ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
-ENV NODE_OPTIONS="--max-old-space-size=1024"
+ENV NODE_OPTIONS="--max-old-space-size=8192 --force-fips"
# FIPS mode of operation:
ENV OPENSSL_CONF=/backend/nodejs.fips.cnf
ENV OPENSSL_MODULES=/usr/local/lib/ossl-modules
-ENV NODE_OPTIONS=--force-fips
ENV FIPS_ENABLED=true
@@ -206,6 +211,11 @@ EXPOSE 443
RUN grep -v 'import "./lib/telemetry/instrumentation.mjs";' dist/main.mjs > dist/main.mjs.tmp && \
mv dist/main.mjs.tmp dist/main.mjs
+# The OpenSSL library is installed in different locations in different architectures (x86_64 and arm64).
+# This is a workaround to avoid errors when the library is not found.
+RUN ln -sf /usr/local/lib64/ossl-modules /usr/local/lib/ossl-modules || \
+ ln -sf /usr/local/lib/ossl-modules /usr/local/lib64/ossl-modules
+
USER non-root-user
CMD ["./standalone-entrypoint.sh"]
\ No newline at end of file
diff --git a/backend/Dockerfile.dev.fips b/backend/Dockerfile.dev.fips
index 977362e03..b954ccd50 100644
--- a/backend/Dockerfile.dev.fips
+++ b/backend/Dockerfile.dev.fips
@@ -59,7 +59,11 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
&& cd openssl-3.1.2 \
&& ./Configure enable-fips \
&& make \
- && make install_fips
+ && make install_fips \
+ && cd / \
+ && rm -rf /openssl-build \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# ? App setup
diff --git a/backend/package-lock.json b/backend/package-lock.json
index a5c106540..cb9efa148 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -7,6 +7,7 @@
"": {
"name": "backend",
"version": "1.0.0",
+ "hasInstallScript": true,
"license": "ISC",
"dependencies": {
"@aws-sdk/client-elasticache": "^3.637.0",
@@ -61,7 +62,7 @@
"ajv": "^8.12.0",
"argon2": "^0.31.2",
"aws-sdk": "^2.1553.0",
- "axios": "^1.6.7",
+ "axios": "^1.11.0",
"axios-retry": "^4.0.0",
"bcrypt": "^5.1.1",
"botbuilder": "^4.23.2",
@@ -13699,14 +13700,16 @@
}
},
"node_modules/@types/request/node_modules/form-data": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz",
- "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
+ "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
- "combined-stream": "^1.0.6",
- "mime-types": "^2.1.12",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.35",
"safe-buffer": "^5.2.1"
},
"engines": {
@@ -15230,13 +15233,13 @@
}
},
"node_modules/axios": {
- "version": "1.7.9",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
- "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
+ "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -18761,13 +18764,15 @@
}
},
"node_modules/form-data": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
- "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
diff --git a/backend/package.json b/backend/package.json
index bd355dd22..01bb0c42a 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -181,7 +181,7 @@
"ajv": "^8.12.0",
"argon2": "^0.31.2",
"aws-sdk": "^2.1553.0",
- "axios": "^1.6.7",
+ "axios": "^1.11.0",
"axios-retry": "^4.0.0",
"bcrypt": "^5.1.1",
"botbuilder": "^4.23.2",
diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts
index 1d2461acf..bcc50ee6c 100644
--- a/backend/src/@types/fastify.d.ts
+++ b/backend/src/@types/fastify.d.ts
@@ -126,6 +126,15 @@ declare module "@fastify/request-context" {
namespace: string;
name: string;
};
+ aws?: {
+ accountId: string;
+ arn: string;
+ userId: string;
+ partition: string;
+ service: string;
+ resourceType: string;
+ resourceName: string;
+ };
};
identityPermissionMetadata?: Record; // filled by permission service
assumedPrivilegeDetails?: { requesterId: string; actorId: string; actorType: ActorType; projectId: string };
diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts
index 7fac20c0e..185a32356 100644
--- a/backend/src/@types/knex.d.ts
+++ b/backend/src/@types/knex.d.ts
@@ -489,6 +489,11 @@ import {
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
} from "@app/db/schemas";
+import {
+ TAccessApprovalPoliciesEnvironments,
+ TAccessApprovalPoliciesEnvironmentsInsert,
+ TAccessApprovalPoliciesEnvironmentsUpdate
+} from "@app/db/schemas/access-approval-policies-environments";
import {
TIdentityLdapAuths,
TIdentityLdapAuthsInsert,
@@ -510,6 +515,11 @@ import {
TRemindersRecipientsInsert,
TRemindersRecipientsUpdate
} from "@app/db/schemas/reminders-recipients";
+import {
+ TSecretApprovalPoliciesEnvironments,
+ TSecretApprovalPoliciesEnvironmentsInsert,
+ TSecretApprovalPoliciesEnvironmentsUpdate
+} from "@app/db/schemas/secret-approval-policies-environments";
import {
TSecretReminderRecipients,
TSecretReminderRecipientsInsert,
@@ -887,6 +897,12 @@ declare module "knex/types/tables" {
TAccessApprovalPoliciesBypassersUpdate
>;
+ [TableName.AccessApprovalPolicyEnvironment]: KnexOriginal.CompositeTableType<
+ TAccessApprovalPoliciesEnvironments,
+ TAccessApprovalPoliciesEnvironmentsInsert,
+ TAccessApprovalPoliciesEnvironmentsUpdate
+ >;
+
[TableName.AccessApprovalRequest]: KnexOriginal.CompositeTableType<
TAccessApprovalRequests,
TAccessApprovalRequestsInsert,
@@ -935,6 +951,11 @@ declare module "knex/types/tables" {
TSecretApprovalRequestSecretTagsInsert,
TSecretApprovalRequestSecretTagsUpdate
>;
+ [TableName.SecretApprovalPolicyEnvironment]: KnexOriginal.CompositeTableType<
+ TSecretApprovalPoliciesEnvironments,
+ TSecretApprovalPoliciesEnvironmentsInsert,
+ TSecretApprovalPoliciesEnvironmentsUpdate
+ >;
[TableName.SecretRotation]: KnexOriginal.CompositeTableType<
TSecretRotations,
TSecretRotationsInsert,
diff --git a/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts b/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts
new file mode 100644
index 000000000..57ec13203
--- /dev/null
+++ b/backend/src/db/migrations/20250722152841_add-policies-environments-table.ts
@@ -0,0 +1,96 @@
+import { Knex } from "knex";
+
+import { selectAllTableCols } from "@app/lib/knex";
+
+import { TableName } from "../schemas";
+import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
+
+export async function up(knex: Knex): Promise {
+ if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyEnvironment))) {
+ await knex.schema.createTable(TableName.AccessApprovalPolicyEnvironment, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.uuid("policyId").notNullable();
+ t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE");
+ t.uuid("envId").notNullable();
+ t.foreign("envId").references("id").inTable(TableName.Environment);
+ t.timestamps(true, true, true);
+ t.unique(["policyId", "envId"]);
+ });
+
+ await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyEnvironment);
+
+ const existingAccessApprovalPolicies = await knex(TableName.AccessApprovalPolicy)
+ .select(selectAllTableCols(TableName.AccessApprovalPolicy))
+ .whereNotNull(`${TableName.AccessApprovalPolicy}.envId`);
+
+ const accessApprovalPolicies = existingAccessApprovalPolicies.map(async (policy) => {
+ await knex(TableName.AccessApprovalPolicyEnvironment).insert({
+ policyId: policy.id,
+ envId: policy.envId
+ });
+ });
+
+ await Promise.all(accessApprovalPolicies);
+ }
+ if (!(await knex.schema.hasTable(TableName.SecretApprovalPolicyEnvironment))) {
+ await knex.schema.createTable(TableName.SecretApprovalPolicyEnvironment, (t) => {
+ t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
+ t.uuid("policyId").notNullable();
+ t.foreign("policyId").references("id").inTable(TableName.SecretApprovalPolicy).onDelete("CASCADE");
+ t.uuid("envId").notNullable();
+ t.foreign("envId").references("id").inTable(TableName.Environment);
+ t.timestamps(true, true, true);
+ t.unique(["policyId", "envId"]);
+ });
+
+ await createOnUpdateTrigger(knex, TableName.SecretApprovalPolicyEnvironment);
+
+ const existingSecretApprovalPolicies = await knex(TableName.SecretApprovalPolicy)
+ .select(selectAllTableCols(TableName.SecretApprovalPolicy))
+ .whereNotNull(`${TableName.SecretApprovalPolicy}.envId`);
+
+ const secretApprovalPolicies = existingSecretApprovalPolicies.map(async (policy) => {
+ await knex(TableName.SecretApprovalPolicyEnvironment).insert({
+ policyId: policy.id,
+ envId: policy.envId
+ });
+ });
+
+ await Promise.all(secretApprovalPolicies);
+ }
+
+ await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
+ t.dropForeign(["envId"]);
+
+ // Add the new foreign key constraint with ON DELETE SET NULL
+ t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL");
+ });
+
+ await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
+ t.dropForeign(["envId"]);
+
+ // Add the new foreign key constraint with ON DELETE SET NULL
+ t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL");
+ });
+}
+
+export async function down(knex: Knex): Promise {
+ if (await knex.schema.hasTable(TableName.AccessApprovalPolicyEnvironment)) {
+ await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyEnvironment);
+ await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyEnvironment);
+ }
+ if (await knex.schema.hasTable(TableName.SecretApprovalPolicyEnvironment)) {
+ await knex.schema.dropTableIfExists(TableName.SecretApprovalPolicyEnvironment);
+ await dropOnUpdateTrigger(knex, TableName.SecretApprovalPolicyEnvironment);
+ }
+
+ await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
+ t.dropForeign(["envId"]);
+ t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
+ });
+
+ await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
+ t.dropForeign(["envId"]);
+ t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
+ });
+}
diff --git a/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts
new file mode 100644
index 000000000..b720c97ae
--- /dev/null
+++ b/backend/src/db/migrations/20250725144940_fix-secret-reminders-migration.ts
@@ -0,0 +1,111 @@
+/* eslint-disable no-await-in-loop */
+import { Knex } from "knex";
+
+import { chunkArray } from "@app/lib/fn";
+import { logger } from "@app/lib/logger";
+
+import { TableName } from "../schemas";
+import { TReminders, TRemindersInsert } from "../schemas/reminders";
+
+export async function up(knex: Knex): Promise {
+ logger.info("Initializing secret reminders migration");
+ const hasReminderTable = await knex.schema.hasTable(TableName.Reminder);
+
+ if (hasReminderTable) {
+ const secretsWithLatestVersions = await knex(TableName.SecretV2)
+ .whereNotNull(`${TableName.SecretV2}.reminderRepeatDays`)
+ .whereRaw(`"${TableName.SecretV2}"."reminderRepeatDays" > 0`)
+ .innerJoin(TableName.SecretVersionV2, (qb) => {
+ void qb
+ .on(`${TableName.SecretVersionV2}.secretId`, "=", `${TableName.SecretV2}.id`)
+ .andOn(`${TableName.SecretVersionV2}.reminderRepeatDays`, "=", `${TableName.SecretV2}.reminderRepeatDays`);
+ })
+ .whereIn([`${TableName.SecretVersionV2}.secretId`, `${TableName.SecretVersionV2}.version`], (qb) => {
+ void qb
+ .select(["v2.secretId", knex.raw("MIN(v2.version) as version")])
+ .from(`${TableName.SecretVersionV2} as v2`)
+ .innerJoin(`${TableName.SecretV2} as s2`, "v2.secretId", "s2.id")
+ .whereRaw(`v2."reminderRepeatDays" = s2."reminderRepeatDays"`)
+ .whereNotNull("v2.reminderRepeatDays")
+ .whereRaw(`v2."reminderRepeatDays" > 0`)
+ .groupBy("v2.secretId");
+ })
+ // Add LEFT JOIN with Reminder table to check for existing reminders
+ .leftJoin(TableName.Reminder, `${TableName.Reminder}.secretId`, `${TableName.SecretV2}.id`)
+ // Only include secrets that don't already have reminders
+ .whereNull(`${TableName.Reminder}.secretId`)
+ .select(
+ knex.ref("id").withSchema(TableName.SecretV2).as("secretId"),
+ knex.ref("reminderRepeatDays").withSchema(TableName.SecretV2).as("reminderRepeatDays"),
+ knex.ref("reminderNote").withSchema(TableName.SecretV2).as("reminderNote"),
+ knex.ref("createdAt").withSchema(TableName.SecretVersionV2).as("createdAt")
+ );
+
+ logger.info(`Found ${secretsWithLatestVersions.length} reminders to migrate`);
+
+ const reminderInserts: TRemindersInsert[] = [];
+ if (secretsWithLatestVersions.length > 0) {
+ secretsWithLatestVersions.forEach((secret) => {
+ if (!secret.reminderRepeatDays) return;
+
+ const now = new Date();
+ const createdAt = new Date(secret.createdAt);
+ let nextReminderDate = new Date(createdAt);
+ nextReminderDate.setDate(nextReminderDate.getDate() + secret.reminderRepeatDays);
+
+ // If the next reminder date is in the past, calculate the proper next occurrence
+ if (nextReminderDate < now) {
+ const daysSinceCreation = Math.floor((now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24));
+ const daysIntoCurrentCycle = daysSinceCreation % secret.reminderRepeatDays;
+ const daysUntilNextReminder = secret.reminderRepeatDays - daysIntoCurrentCycle;
+
+ nextReminderDate = new Date(now);
+ nextReminderDate.setDate(now.getDate() + daysUntilNextReminder);
+ }
+
+ reminderInserts.push({
+ secretId: secret.secretId,
+ message: secret.reminderNote,
+ repeatDays: secret.reminderRepeatDays,
+ nextReminderDate
+ });
+ });
+
+ const commitBatches = chunkArray(reminderInserts, 2000);
+ for (const commitBatch of commitBatches) {
+ const insertedReminders = (await knex
+ .batchInsert(TableName.Reminder, commitBatch)
+ .returning("*")) as TReminders[];
+
+ const insertedReminderSecretIds = insertedReminders.map((reminder) => reminder.secretId).filter(Boolean);
+
+ const recipients = await knex(TableName.SecretReminderRecipients)
+ .whereRaw(`??.?? IN (${insertedReminderSecretIds.map(() => "?").join(",")})`, [
+ TableName.SecretReminderRecipients,
+ "secretId",
+ ...insertedReminderSecretIds
+ ])
+ .select(
+ knex.ref("userId").withSchema(TableName.SecretReminderRecipients).as("userId"),
+ knex.ref("secretId").withSchema(TableName.SecretReminderRecipients).as("secretId")
+ );
+ const reminderRecipients = recipients.map((recipient) => ({
+ reminderId: insertedReminders.find((reminder) => reminder.secretId === recipient.secretId)?.id,
+ userId: recipient.userId
+ }));
+
+ const filteredRecipients = reminderRecipients.filter((recipient) => Boolean(recipient.reminderId));
+ await knex.batchInsert(TableName.ReminderRecipient, filteredRecipients);
+ }
+ logger.info(`Successfully migrated ${reminderInserts.length} secret reminders`);
+ }
+
+ logger.info("Secret reminders migration completed");
+ } else {
+ logger.warn("Reminder table does not exist, skipping migration");
+ }
+}
+
+export async function down(): Promise {
+ logger.info("Rollback not implemented for secret reminders fix migration");
+}
diff --git a/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts
new file mode 100644
index 000000000..c8257b771
--- /dev/null
+++ b/backend/src/db/migrations/20250725171821_add-secret-detection-ignore-values.ts
@@ -0,0 +1,19 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ if (!(await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues"))) {
+ await knex.schema.alterTable(TableName.Project, (t) => {
+ t.specificType("secretDetectionIgnoreValues", "text[]");
+ });
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ if (await knex.schema.hasColumn(TableName.Project, "secretDetectionIgnoreValues")) {
+ await knex.schema.alterTable(TableName.Project, (t) => {
+ t.dropColumn("secretDetectionIgnoreValues");
+ });
+ }
+}
diff --git a/backend/src/db/migrations/utils/env-config.ts b/backend/src/db/migrations/utils/env-config.ts
index debaea03f..de32f4db9 100644
--- a/backend/src/db/migrations/utils/env-config.ts
+++ b/backend/src/db/migrations/utils/env-config.ts
@@ -53,7 +53,7 @@ export const getMigrationEnvConfig = async (superAdminDAL: TSuperAdminDALFactory
let envCfg = Object.freeze(parsedEnv.data);
- const fipsEnabled = await crypto.initialize(superAdminDAL);
+ const fipsEnabled = await crypto.initialize(superAdminDAL, envCfg);
// Fix for 128-bit entropy encryption key expansion issue:
// In FIPS it is not ideal to expand a 128-bit key into 256-bit. We solved this issue in the past by creating the ROOT_ENCRYPTION_KEY.
diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts
index 2e71486bf..55ec12faa 100644
--- a/backend/src/db/schemas/models.ts
+++ b/backend/src/db/schemas/models.ts
@@ -100,6 +100,7 @@ export enum TableName {
AccessApprovalPolicyBypasser = "access_approval_policies_bypassers",
AccessApprovalRequest = "access_approval_requests",
AccessApprovalRequestReviewer = "access_approval_requests_reviewers",
+ AccessApprovalPolicyEnvironment = "access_approval_policies_environments",
SecretApprovalPolicy = "secret_approval_policies",
SecretApprovalPolicyApprover = "secret_approval_policies_approvers",
SecretApprovalPolicyBypasser = "secret_approval_policies_bypassers",
@@ -107,6 +108,7 @@ export enum TableName {
SecretApprovalRequestReviewer = "secret_approval_requests_reviewers",
SecretApprovalRequestSecret = "secret_approval_requests_secrets",
SecretApprovalRequestSecretTag = "secret_approval_request_secret_tags",
+ SecretApprovalPolicyEnvironment = "secret_approval_policies_environments",
SecretRotation = "secret_rotations",
SecretRotationOutput = "secret_rotation_outputs",
SamlConfig = "saml_configs",
diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts
index 177f5e1fd..ef44344de 100644
--- a/backend/src/ee/routes/v1/access-approval-policy-router.ts
+++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts
@@ -17,52 +17,66 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
rateLimit: writeLimit
},
schema: {
- body: z.object({
- projectSlug: z.string().trim(),
- name: z.string().optional(),
- secretPath: z.string().trim().min(1, { message: "Secret path cannot be empty" }).transform(removeTrailingSlash),
- environment: z.string(),
- approvers: z
- .discriminatedUnion("type", [
- z.object({
- type: z.literal(ApproverType.Group),
- id: z.string(),
- sequence: z.number().int().default(1)
- }),
- z.object({
- type: z.literal(ApproverType.User),
- id: z.string().optional(),
- username: z.string().optional(),
- sequence: z.number().int().default(1)
+ body: z
+ .object({
+ projectSlug: z.string().trim(),
+ name: z.string().optional(),
+ secretPath: z
+ .string()
+ .trim()
+ .min(1, { message: "Secret path cannot be empty" })
+ .transform(removeTrailingSlash),
+ environment: z.string().optional(),
+ environments: z.string().array().optional(),
+ approvers: z
+ .discriminatedUnion("type", [
+ z.object({
+ type: z.literal(ApproverType.Group),
+ id: z.string(),
+ sequence: z.number().int().default(1)
+ }),
+ z.object({
+ type: z.literal(ApproverType.User),
+ id: z.string().optional(),
+ username: z.string().optional(),
+ sequence: z.number().int().default(1)
+ })
+ ])
+ .array()
+ .max(100, "Cannot have more than 100 approvers")
+ .min(1, { message: "At least one approver should be provided" })
+ .refine(
+ // @ts-expect-error this is ok
+ (el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)),
+ "Must provide either username or id"
+ ),
+ bypassers: z
+ .discriminatedUnion("type", [
+ z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
+ z.object({
+ type: z.literal(BypasserType.User),
+ id: z.string().optional(),
+ username: z.string().optional()
+ })
+ ])
+ .array()
+ .max(100, "Cannot have more than 100 bypassers")
+ .optional(),
+ approvalsRequired: z
+ .object({
+ numberOfApprovals: z.number().int(),
+ stepNumber: z.number().int()
})
- ])
- .array()
- .max(100, "Cannot have more than 100 approvers")
- .min(1, { message: "At least one approver should be provided" })
- .refine(
- // @ts-expect-error this is ok
- (el) => el.every((i) => Boolean(i?.id) || Boolean(i?.username)),
- "Must provide either username or id"
- ),
- bypassers: z
- .discriminatedUnion("type", [
- z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
- z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() })
- ])
- .array()
- .max(100, "Cannot have more than 100 bypassers")
- .optional(),
- approvalsRequired: z
- .object({
- numberOfApprovals: z.number().int(),
- stepNumber: z.number().int()
- })
- .array()
- .optional(),
- approvals: z.number().min(1).default(1),
- enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
- allowedSelfApprovals: z.boolean().default(true)
- }),
+ .array()
+ .optional(),
+ approvals: z.number().min(1).default(1),
+ enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
+ allowedSelfApprovals: z.boolean().default(true)
+ })
+ .refine(
+ (val) => Boolean(val.environment) || Boolean(val.environments),
+ "Must provide either environment or environments"
+ ),
response: {
200: z.object({
approval: sapPubSchema
@@ -78,7 +92,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId,
...req.body,
projectSlug: req.body.projectSlug,
- name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
+ name:
+ req.body.name ?? `${req.body.environment || req.body.environments?.join("-").substring(0, 250)}-${nanoid(3)}`,
enforcementLevel: req.body.enforcementLevel
});
return { approval };
@@ -211,6 +226,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
approvals: z.number().min(1).optional(),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
allowedSelfApprovals: z.boolean().default(true),
+ environments: z.array(z.string()).optional(),
approvalsRequired: z
.object({
numberOfApprovals: z.number().int(),
diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
index 46b2544b2..dc87b83f2 100644
--- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts
+++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts
@@ -17,34 +17,45 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
rateLimit: writeLimit
},
schema: {
- body: z.object({
- workspaceId: z.string(),
- name: z.string().optional(),
- environment: z.string(),
- secretPath: z
- .string()
- .min(1, { message: "Secret path cannot be empty" })
- .transform((val) => removeTrailingSlash(val)),
- approvers: z
- .discriminatedUnion("type", [
- z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
- z.object({ type: z.literal(ApproverType.User), id: z.string().optional(), username: z.string().optional() })
- ])
- .array()
- .min(1, { message: "At least one approver should be provided" })
- .max(100, "Cannot have more than 100 approvers"),
- bypassers: z
- .discriminatedUnion("type", [
- z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
- z.object({ type: z.literal(BypasserType.User), id: z.string().optional(), username: z.string().optional() })
- ])
- .array()
- .max(100, "Cannot have more than 100 bypassers")
- .optional(),
- approvals: z.number().min(1).default(1),
- enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
- allowedSelfApprovals: z.boolean().default(true)
- }),
+ body: z
+ .object({
+ workspaceId: z.string(),
+ name: z.string().optional(),
+ environment: z.string().optional(),
+ environments: z.string().array().optional(),
+ secretPath: z
+ .string()
+ .min(1, { message: "Secret path cannot be empty" })
+ .transform((val) => removeTrailingSlash(val)),
+ approvers: z
+ .discriminatedUnion("type", [
+ z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
+ z.object({
+ type: z.literal(ApproverType.User),
+ id: z.string().optional(),
+ username: z.string().optional()
+ })
+ ])
+ .array()
+ .min(1, { message: "At least one approver should be provided" })
+ .max(100, "Cannot have more than 100 approvers"),
+ bypassers: z
+ .discriminatedUnion("type", [
+ z.object({ type: z.literal(BypasserType.Group), id: z.string() }),
+ z.object({
+ type: z.literal(BypasserType.User),
+ id: z.string().optional(),
+ username: z.string().optional()
+ })
+ ])
+ .array()
+ .max(100, "Cannot have more than 100 bypassers")
+ .optional(),
+ approvals: z.number().min(1).default(1),
+ enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard),
+ allowedSelfApprovals: z.boolean().default(true)
+ })
+ .refine((data) => data.environment || data.environments, "At least one environment should be provided"),
response: {
200: z.object({
approval: sapPubSchema
@@ -60,7 +71,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId,
projectId: req.body.workspaceId,
...req.body,
- name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
+ name: req.body.name ?? `${req.body.environment || req.body.environments?.join(",")}-${nanoid(3)}`,
enforcementLevel: req.body.enforcementLevel
});
return { approval };
@@ -103,7 +114,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
.optional()
.transform((val) => (val ? removeTrailingSlash(val) : undefined)),
enforcementLevel: z.nativeEnum(EnforcementLevel).optional(),
- allowedSelfApprovals: z.boolean().default(true)
+ allowedSelfApprovals: z.boolean().default(true),
+ environments: z.array(z.string()).optional()
}),
response: {
200: z.object({
diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts
index 995534f8f..9baf762d6 100644
--- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts
+++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts
@@ -26,6 +26,7 @@ export interface TAccessApprovalPolicyDALFactory
>,
customFilter?: {
policyId?: string;
+ envId?: string;
},
tx?: Knex
) => Promise<
@@ -55,11 +56,6 @@ export interface TAccessApprovalPolicyDALFactory
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
- environment: {
- id: string;
- name: string;
- slug: string;
- };
projectId: string;
bypassers: (
| {
@@ -72,6 +68,11 @@ export interface TAccessApprovalPolicyDALFactory
type: BypasserType.Group;
}
)[];
+ environments: {
+ id: string;
+ name: string;
+ slug: string;
+ }[];
}[]
>;
findById: (
@@ -95,11 +96,11 @@ export interface TAccessApprovalPolicyDALFactory
allowedSelfApprovals: boolean;
secretPath: string;
deletedAt?: Date | null | undefined;
- environment: {
+ environments: {
id: string;
name: string;
slug: string;
- };
+ }[];
projectId: string;
}
| undefined
@@ -143,6 +144,26 @@ export interface TAccessApprovalPolicyDALFactory
}
| undefined
>;
+ findPolicyByEnvIdAndSecretPath: (
+ { envIds, secretPath }: { envIds: string[]; secretPath: string },
+ tx?: Knex
+ ) => Promise<{
+ name: string;
+ id: string;
+ createdAt: Date;
+ updatedAt: Date;
+ approvals: number;
+ enforcementLevel: string;
+ allowedSelfApprovals: boolean;
+ secretPath: string;
+ deletedAt?: Date | null | undefined;
+ environments: {
+ id: string;
+ name: string;
+ slug: string;
+ }[];
+ projectId: string;
+ }>;
}
export interface TAccessApprovalPolicyServiceFactory {
@@ -367,6 +388,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
filter: TFindFilter,
customFilter?: {
policyId?: string;
+ envId?: string;
}
) => {
const result = await tx(TableName.AccessApprovalPolicy)
@@ -377,7 +399,17 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
}
})
- .join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
+ .join(
+ TableName.AccessApprovalPolicyEnvironment,
+ `${TableName.AccessApprovalPolicy}.id`,
+ `${TableName.AccessApprovalPolicyEnvironment}.policyId`
+ )
+ .join(TableName.Environment, `${TableName.AccessApprovalPolicyEnvironment}.envId`, `${TableName.Environment}.id`)
+ .where((qb) => {
+ if (customFilter?.envId) {
+ void qb.where(`${TableName.AccessApprovalPolicyEnvironment}.envId`, "=", customFilter.envId);
+ }
+ })
.leftJoin(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
@@ -404,7 +436,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
.select(tx.ref("bypasserGroupId").withSchema(TableName.AccessApprovalPolicyBypasser))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
- .select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
+ .select(tx.ref("id").withSchema(TableName.Environment).as("environmentId"))
.select(tx.ref("projectId").withSchema(TableName.Environment))
.select(selectAllTableCols(TableName.AccessApprovalPolicy));
@@ -448,6 +480,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
sequence: approverSequence,
approvalsRequired
})
+ },
+ {
+ key: "environmentId",
+ label: "environments" as const,
+ mapper: ({ environmentId: id, envName, envSlug }) => ({
+ id,
+ name: envName,
+ slug: envSlug
+ })
}
]
});
@@ -470,11 +511,6 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
data: docs,
key: "id",
parentMapper: (data) => ({
- environment: {
- id: data.envId,
- name: data.envName,
- slug: data.envSlug
- },
projectId: data.projectId,
...AccessApprovalPoliciesSchema.parse(data)
// secretPath: data.secretPath || undefined,
@@ -517,6 +553,15 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
id,
type: BypasserType.Group as const
})
+ },
+ {
+ key: "environmentId",
+ label: "environments" as const,
+ mapper: ({ environmentId: id, envName, envSlug }) => ({
+ id,
+ name: envName,
+ slug: envSlug
+ })
}
]
});
@@ -545,14 +590,20 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
// eslint-disable-next-line @typescript-eslint/no-misused-promises
buildFindFilter(
{
- envId,
secretPath
},
TableName.AccessApprovalPolicy
)
)
+ .join(
+ TableName.AccessApprovalPolicyEnvironment,
+ `${TableName.AccessApprovalPolicyEnvironment}.policyId`,
+ `${TableName.AccessApprovalPolicy}.id`
+ )
+ .where(`${TableName.AccessApprovalPolicyEnvironment}.envId`, "=", envId)
.orderBy("deletedAt", "desc")
.orderByRaw(`"deletedAt" IS NULL`)
+ .select(selectAllTableCols(TableName.AccessApprovalPolicy))
.first();
return result;
@@ -561,5 +612,81 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient): TAccessApprovalPo
}
};
- return { ...accessApprovalPolicyOrm, find, findById, softDeleteById, findLastValidPolicy };
+ const findPolicyByEnvIdAndSecretPath: TAccessApprovalPolicyDALFactory["findPolicyByEnvIdAndSecretPath"] = async (
+ { envIds, secretPath },
+ tx
+ ) => {
+ try {
+ const docs = await (tx || db.replicaNode())(TableName.AccessApprovalPolicy)
+ .join(
+ TableName.AccessApprovalPolicyEnvironment,
+ `${TableName.AccessApprovalPolicyEnvironment}.policyId`,
+ `${TableName.AccessApprovalPolicy}.id`
+ )
+ .join(
+ TableName.Environment,
+ `${TableName.AccessApprovalPolicyEnvironment}.envId`,
+ `${TableName.Environment}.id`
+ )
+ .where(
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ buildFindFilter(
+ {
+ $in: {
+ envId: envIds
+ }
+ },
+ TableName.AccessApprovalPolicyEnvironment
+ )
+ )
+ .where(
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ buildFindFilter(
+ {
+ secretPath
+ },
+ TableName.AccessApprovalPolicy
+ )
+ )
+ .whereNull(`${TableName.AccessApprovalPolicy}.deletedAt`)
+ .orderBy("deletedAt", "desc")
+ .orderByRaw(`"deletedAt" IS NULL`)
+ .select(selectAllTableCols(TableName.AccessApprovalPolicy))
+ .select(db.ref("name").withSchema(TableName.Environment).as("envName"))
+ .select(db.ref("slug").withSchema(TableName.Environment).as("envSlug"))
+ .select(db.ref("id").withSchema(TableName.Environment).as("environmentId"))
+ .select(db.ref("projectId").withSchema(TableName.Environment));
+ const formattedDocs = sqlNestRelationships({
+ data: docs,
+ key: "id",
+ parentMapper: (data) => ({
+ projectId: data.projectId,
+ ...AccessApprovalPoliciesSchema.parse(data)
+ }),
+ childrenMapper: [
+ {
+ key: "environmentId",
+ label: "environments" as const,
+ mapper: ({ environmentId: id, envName, envSlug }) => ({
+ id,
+ name: envName,
+ slug: envSlug
+ })
+ }
+ ]
+ });
+ return formattedDocs?.[0];
+ } catch (error) {
+ throw new DatabaseError({ error, name: "findPolicyByEnvIdAndSecretPath" });
+ }
+ };
+
+ return {
+ ...accessApprovalPolicyOrm,
+ find,
+ findById,
+ softDeleteById,
+ findLastValidPolicy,
+ findPolicyByEnvIdAndSecretPath
+ };
};
diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts
new file mode 100644
index 000000000..f0d8079cf
--- /dev/null
+++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-environment-dal.ts
@@ -0,0 +1,32 @@
+import { Knex } from "knex";
+
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { DatabaseError } from "@app/lib/errors";
+import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
+
+export type TAccessApprovalPolicyEnvironmentDALFactory = ReturnType;
+
+export const accessApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => {
+ const accessApprovalPolicyEnvironmentOrm = ormify(db, TableName.AccessApprovalPolicyEnvironment);
+
+ const findAvailablePoliciesByEnvId = async (envId: string, tx?: Knex) => {
+ try {
+ const docs = await (tx || db.replicaNode())(TableName.AccessApprovalPolicyEnvironment)
+ .join(
+ TableName.AccessApprovalPolicy,
+ `${TableName.AccessApprovalPolicyEnvironment}.policyId`,
+ `${TableName.AccessApprovalPolicy}.id`
+ )
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ .where(buildFindFilter({ envId }, TableName.AccessApprovalPolicyEnvironment))
+ .whereNull(`${TableName.AccessApprovalPolicy}.deletedAt`)
+ .select(selectAllTableCols(TableName.AccessApprovalPolicyEnvironment));
+ return docs;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "findAvailablePoliciesByEnvId" });
+ }
+ };
+
+ return { ...accessApprovalPolicyEnvironmentOrm, findAvailablePoliciesByEnvId };
+};
diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts
index 6d656bafa..0b3c4e128 100644
--- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts
+++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts
@@ -21,6 +21,7 @@ import {
TAccessApprovalPolicyBypasserDALFactory
} from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
+import { TAccessApprovalPolicyEnvironmentDALFactory } from "./access-approval-policy-environment-dal";
import {
ApproverType,
BypasserType,
@@ -45,12 +46,14 @@ type TAccessApprovalPolicyServiceFactoryDep = {
additionalPrivilegeDAL: Pick;
accessApprovalRequestReviewerDAL: Pick;
orgMembershipDAL: Pick;
+ accessApprovalPolicyEnvironmentDAL: TAccessApprovalPolicyEnvironmentDALFactory;
};
export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
accessApprovalPolicyBypasserDAL,
+ accessApprovalPolicyEnvironmentDAL,
groupDAL,
permissionService,
projectEnvDAL,
@@ -63,21 +66,22 @@ export const accessApprovalPolicyServiceFactory = ({
}: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => {
const $policyExists = async ({
envId,
+ envIds,
secretPath,
policyId
}: {
- envId: string;
+ envId?: string;
+ envIds?: string[];
secretPath: string;
policyId?: string;
}) => {
- const policy = await accessApprovalPolicyDAL
- .findOne({
- envId,
- secretPath,
- deletedAt: null
- })
- .catch(() => null);
-
+ if (!envId && !envIds) {
+ throw new BadRequestError({ message: "Must provide either envId or envIds" });
+ }
+ const policy = await accessApprovalPolicyDAL.findPolicyByEnvIdAndSecretPath({
+ secretPath,
+ envIds: envId ? [envId] : (envIds as string[])
+ });
return policyId ? policy && policy.id !== policyId : Boolean(policy);
};
@@ -93,6 +97,7 @@ export const accessApprovalPolicyServiceFactory = ({
bypassers,
projectSlug,
environment,
+ environments,
enforcementLevel,
allowedSelfApprovals,
approvalsRequired
@@ -125,13 +130,23 @@ export const accessApprovalPolicyServiceFactory = ({
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
);
- const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
- if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` });
+ const mergedEnvs = (environment ? [environment] : environments) || [];
+ if (mergedEnvs.length === 0) {
+ throw new BadRequestError({ message: "Must provide either environment or environments" });
+ }
+ const envs = await projectEnvDAL.find({ $in: { slug: mergedEnvs }, projectId: project.id });
+ if (!envs.length || envs.length !== mergedEnvs.length) {
+ const notFoundEnvs = mergedEnvs.filter((env) => !envs.find((el) => el.slug === env));
+ throw new NotFoundError({ message: `One or more environments not found: ${notFoundEnvs.join(", ")}` });
+ }
- if (await $policyExists({ envId: env.id, secretPath })) {
- throw new BadRequestError({
- message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'`
- });
+ for (const env of envs) {
+ // eslint-disable-next-line no-await-in-loop
+ if (await $policyExists({ envId: env.id, secretPath })) {
+ throw new BadRequestError({
+ message: `A policy for secret path '${secretPath}' already exists in environment '${env.slug}'`
+ });
+ }
}
let approverUserIds = userApprovers;
@@ -199,7 +214,7 @@ export const accessApprovalPolicyServiceFactory = ({
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.create(
{
- envId: env.id,
+ envId: envs[0].id,
approvals,
secretPath,
name,
@@ -208,6 +223,10 @@ export const accessApprovalPolicyServiceFactory = ({
},
tx
);
+ await accessApprovalPolicyEnvironmentDAL.insertMany(
+ envs.map((el) => ({ policyId: doc.id, envId: el.id })),
+ tx
+ );
if (approverUserIds.length) {
await accessApprovalPolicyApproverDAL.insertMany(
@@ -260,7 +279,7 @@ export const accessApprovalPolicyServiceFactory = ({
return doc;
});
- return { ...accessApproval, environment: env, projectId: project.id };
+ return { ...accessApproval, environments: envs, projectId: project.id, environment: envs[0] };
};
const getAccessApprovalPolicyByProjectSlug: TAccessApprovalPolicyServiceFactory["getAccessApprovalPolicyByProjectSlug"] =
@@ -279,7 +298,10 @@ export const accessApprovalPolicyServiceFactory = ({
});
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null });
- return accessApprovalPolicies;
+ return accessApprovalPolicies.map((policy) => ({
+ ...policy,
+ environment: policy.environments[0]
+ }));
};
const updateAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["updateAccessApprovalPolicy"] = async ({
@@ -295,7 +317,8 @@ export const accessApprovalPolicyServiceFactory = ({
approvals,
enforcementLevel,
allowedSelfApprovals,
- approvalsRequired
+ approvalsRequired,
+ environments
}: TUpdateAccessApprovalPolicy) => {
const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group);
@@ -323,16 +346,27 @@ export const accessApprovalPolicyServiceFactory = ({
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
}
+ let envs = accessApprovalPolicy.environments;
if (
- await $policyExists({
- envId: accessApprovalPolicy.envId,
- secretPath: secretPath || accessApprovalPolicy.secretPath,
- policyId: accessApprovalPolicy.id
- })
+ environments &&
+ (environments.length !== envs.length || environments.some((env) => !envs.find((el) => el.slug === env)))
) {
- throw new BadRequestError({
- message: `A policy for secret path '${secretPath}' already exists in environment '${accessApprovalPolicy.environment.slug}'`
- });
+ envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: accessApprovalPolicy.projectId });
+ }
+
+ for (const env of envs) {
+ if (
+ // eslint-disable-next-line no-await-in-loop
+ await $policyExists({
+ envId: env.id,
+ secretPath: secretPath || accessApprovalPolicy.secretPath,
+ policyId: accessApprovalPolicy.id
+ })
+ ) {
+ throw new BadRequestError({
+ message: `A policy for secret path '${secretPath || accessApprovalPolicy.secretPath}' already exists in environment '${env.slug}'`
+ });
+ }
}
const { permission } = await permissionService.getProjectPermission({
@@ -488,6 +522,14 @@ export const accessApprovalPolicyServiceFactory = ({
);
}
+ if (environments) {
+ await accessApprovalPolicyEnvironmentDAL.delete({ policyId: doc.id }, tx);
+ await accessApprovalPolicyEnvironmentDAL.insertMany(
+ envs.map((env) => ({ policyId: doc.id, envId: env.id })),
+ tx
+ );
+ }
+
await accessApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx);
if (bypasserUserIds.length) {
@@ -517,7 +559,8 @@ export const accessApprovalPolicyServiceFactory = ({
return {
...updatedPolicy,
- environment: accessApprovalPolicy.environment,
+ environments: accessApprovalPolicy.environments,
+ environment: accessApprovalPolicy.environments[0],
projectId: accessApprovalPolicy.projectId
};
};
@@ -568,7 +611,10 @@ export const accessApprovalPolicyServiceFactory = ({
}
});
- return policy;
+ return {
+ ...policy,
+ environment: policy.environments[0]
+ };
};
const getAccessPolicyCountByEnvSlug: TAccessApprovalPolicyServiceFactory["getAccessPolicyCountByEnvSlug"] = async ({
@@ -598,11 +644,13 @@ export const accessApprovalPolicyServiceFactory = ({
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` });
- const policies = await accessApprovalPolicyDAL.find({
- envId: environment.id,
- projectId: project.id,
- deletedAt: null
- });
+ const policies = await accessApprovalPolicyDAL.find(
+ {
+ projectId: project.id,
+ deletedAt: null
+ },
+ { envId: environment.id }
+ );
if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` });
return { count: policies.length };
@@ -634,7 +682,10 @@ export const accessApprovalPolicyServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
- return policy;
+ return {
+ ...policy,
+ environment: policy.environments[0]
+ };
};
return {
diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts
index f3f195914..27ec228f7 100644
--- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts
+++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts
@@ -26,7 +26,8 @@ export enum BypasserType {
export type TCreateAccessApprovalPolicy = {
approvals: number;
secretPath: string;
- environment: string;
+ environment?: string;
+ environments?: string[];
approvers: (
| { type: ApproverType.Group; id: string; sequence?: number }
| { type: ApproverType.User; id?: string; username?: string; sequence?: number }
@@ -58,6 +59,7 @@ export type TUpdateAccessApprovalPolicy = {
enforcementLevel?: EnforcementLevel;
allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
+ environments?: string[];
} & Omit;
export type TDeleteAccessApprovalPolicy = {
@@ -113,6 +115,15 @@ export interface TAccessApprovalPolicyServiceFactory {
slug: string;
position: number;
};
+ environments: {
+ name: string;
+ id: string;
+ createdAt: Date;
+ updatedAt: Date;
+ projectId: string;
+ slug: string;
+ position: number;
+ }[];
projectId: string;
name: string;
id: string;
@@ -153,6 +164,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string;
slug: string;
};
+ environments: {
+ id: string;
+ name: string;
+ slug: string;
+ }[];
projectId: string;
}>;
updateAccessApprovalPolicy: ({
@@ -168,13 +184,19 @@ export interface TAccessApprovalPolicyServiceFactory {
approvals,
enforcementLevel,
allowedSelfApprovals,
- approvalsRequired
+ approvalsRequired,
+ environments
}: TUpdateAccessApprovalPolicy) => Promise<{
environment: {
id: string;
name: string;
slug: string;
};
+ environments: {
+ id: string;
+ name: string;
+ slug: string;
+ }[];
projectId: string;
name: string;
id: string;
@@ -225,6 +247,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string;
slug: string;
};
+ environments: {
+ id: string;
+ name: string;
+ slug: string;
+ }[];
projectId: string;
bypassers: (
| {
@@ -276,6 +303,11 @@ export interface TAccessApprovalPolicyServiceFactory {
name: string;
slug: string;
};
+ environments: {
+ id: string;
+ name: string;
+ slug: string;
+ }[];
projectId: string;
bypassers: (
| {
diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts
index 671d2c1de..9872df067 100644
--- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts
+++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts
@@ -65,7 +65,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit environment
}
]
});
diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts
index bdf579616..dcbe717da 100644
--- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts
+++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts
@@ -86,6 +86,25 @@ export const accessApprovalRequestServiceFactory = ({
projectMicrosoftTeamsConfigDAL,
projectSlackConfigDAL
}: TSecretApprovalRequestServiceFactoryDep): TAccessApprovalRequestServiceFactory => {
+ const $getEnvironmentFromPermissions = (permissions: unknown): string | null => {
+ if (!Array.isArray(permissions) || permissions.length === 0) {
+ return null;
+ }
+
+ const firstPermission = permissions[0] as unknown[];
+ if (!Array.isArray(firstPermission) || firstPermission.length < 3) {
+ return null;
+ }
+
+ const metadata = firstPermission[2] as Record;
+ if (typeof metadata === "object" && metadata !== null && "environment" in metadata) {
+ const env = metadata.environment;
+ return typeof env === "string" ? env : null;
+ }
+
+ return null;
+ };
+
const createAccessApprovalRequest: TAccessApprovalRequestServiceFactory["createAccessApprovalRequest"] = async ({
isTemporary,
temporaryRange,
@@ -308,6 +327,15 @@ export const accessApprovalRequestServiceFactory = ({
requests = requests.filter((request) => request.environment === envSlug);
}
+ requests = requests.map((request) => {
+ const permissionEnvironment = $getEnvironmentFromPermissions(request.permissions);
+
+ if (permissionEnvironment) {
+ request.environmentName = permissionEnvironment;
+ }
+ return request;
+ });
+
return { requests };
};
@@ -325,13 +353,27 @@ export const accessApprovalRequestServiceFactory = ({
throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` });
}
- const { policy, environment } = accessApprovalRequest;
+ const { policy, environments, permissions } = accessApprovalRequest;
if (policy.deletedAt) {
throw new BadRequestError({
message: "The policy associated with this access request has been deleted."
});
}
+ const permissionEnvironment = $getEnvironmentFromPermissions(permissions);
+ if (
+ !permissionEnvironment ||
+ (!environments.includes(permissionEnvironment) && status === ApprovalStatus.APPROVED)
+ ) {
+ throw new BadRequestError({
+ message: `The original policy ${policy.name} is not attached to environment '${permissionEnvironment}'.`
+ });
+ }
+ const environment = await projectEnvDAL.findOne({
+ projectId: accessApprovalRequest.projectId,
+ slug: permissionEnvironment
+ });
+
const { membership, hasRole } = await permissionService.getProjectPermission({
actor,
actorId,
@@ -553,7 +595,7 @@ export const accessApprovalRequestServiceFactory = ({
requesterEmail: actingUser.email,
bypassReason: bypassReason || "No reason provided",
secretPath: policy.secretPath || "/",
- environment,
+ environment: environment?.name || permissionEnvironment,
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`,
requestType: "access"
},
diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts
index 0a1b9db40..9cc6e134d 100644
--- a/backend/src/ee/services/scim/scim-service.ts
+++ b/backend/src/ee/services/scim/scim-service.ts
@@ -579,6 +579,9 @@ export const scimServiceFactory = ({
});
const serverCfg = await getServerCfg();
+ const hasEmailChanged = email?.toLowerCase() !== membership.email;
+ const defaultEmailVerified =
+ org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails;
await userDAL.transaction(async (tx) => {
await userAliasDAL.update(
{
@@ -605,8 +608,7 @@ export const scimServiceFactory = ({
firstName,
email: email?.toLowerCase(),
lastName,
- isEmailVerified:
- org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails
+ isEmailVerified: hasEmailChanged ? defaultEmailVerified : undefined
},
tx
);
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts
index fd8be93cf..3212fb902 100644
--- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-dal.ts
@@ -23,6 +23,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
filter: TFindFilter,
customFilter?: {
sapId?: string;
+ envId?: string;
}
) =>
tx(TableName.SecretApprovalPolicy)
@@ -33,7 +34,17 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
void qb.where(`${TableName.SecretApprovalPolicy}.id`, "=", customFilter.sapId);
}
})
- .join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
+ .join(
+ TableName.SecretApprovalPolicyEnvironment,
+ `${TableName.SecretApprovalPolicyEnvironment}.policyId`,
+ `${TableName.SecretApprovalPolicy}.id`
+ )
+ .join(TableName.Environment, `${TableName.SecretApprovalPolicyEnvironment}.envId`, `${TableName.Environment}.id`)
+ .where((qb) => {
+ if (customFilter?.envId) {
+ void qb.where(`${TableName.SecretApprovalPolicyEnvironment}.envId`, "=", customFilter.envId);
+ }
+ })
.leftJoin(
TableName.SecretApprovalPolicyApprover,
`${TableName.SecretApprovalPolicy}.id`,
@@ -97,7 +108,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
.select(
tx.ref("name").withSchema(TableName.Environment).as("envName"),
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
- tx.ref("id").withSchema(TableName.Environment).as("envId"),
+ tx.ref("id").withSchema(TableName.Environment).as("environmentId"),
tx.ref("projectId").withSchema(TableName.Environment)
)
.select(selectAllTableCols(TableName.SecretApprovalPolicy))
@@ -146,6 +157,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
firstName,
lastName
})
+ },
+ {
+ key: "environmentId",
+ label: "environments" as const,
+ mapper: ({ environmentId, envName, envSlug }) => ({
+ id: environmentId,
+ name: envName,
+ slug: envSlug
+ })
}
]
});
@@ -160,6 +180,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
filter: TFindFilter,
customFilter?: {
sapId?: string;
+ envId?: string;
},
tx?: Knex
) => {
@@ -221,6 +242,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
mapper: ({ approverGroupUserId: userId }) => ({
userId
})
+ },
+ {
+ key: "environmentId",
+ label: "environments" as const,
+ mapper: ({ environmentId, envName, envSlug }) => ({
+ id: environmentId,
+ name: envName,
+ slug: envSlug
+ })
}
]
});
@@ -235,5 +265,74 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
return softDeletedPolicy;
};
- return { ...secretApprovalPolicyOrm, findById, find, softDeleteById };
+ const findPolicyByEnvIdAndSecretPath = async (
+ { envIds, secretPath }: { envIds: string[]; secretPath: string },
+ tx?: Knex
+ ) => {
+ try {
+ const docs = await (tx || db.replicaNode())(TableName.SecretApprovalPolicy)
+ .join(
+ TableName.SecretApprovalPolicyEnvironment,
+ `${TableName.SecretApprovalPolicyEnvironment}.policyId`,
+ `${TableName.SecretApprovalPolicy}.id`
+ )
+ .join(
+ TableName.Environment,
+ `${TableName.SecretApprovalPolicyEnvironment}.envId`,
+ `${TableName.Environment}.id`
+ )
+ .where(
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ buildFindFilter(
+ {
+ $in: {
+ envId: envIds
+ }
+ },
+ TableName.SecretApprovalPolicyEnvironment
+ )
+ )
+ .where(
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ buildFindFilter(
+ {
+ secretPath
+ },
+ TableName.SecretApprovalPolicy
+ )
+ )
+ .whereNull(`${TableName.SecretApprovalPolicy}.deletedAt`)
+ .orderBy("deletedAt", "desc")
+ .orderByRaw(`"deletedAt" IS NULL`)
+ .select(selectAllTableCols(TableName.SecretApprovalPolicy))
+ .select(db.ref("name").withSchema(TableName.Environment).as("envName"))
+ .select(db.ref("slug").withSchema(TableName.Environment).as("envSlug"))
+ .select(db.ref("id").withSchema(TableName.Environment).as("environmentId"))
+ .select(db.ref("projectId").withSchema(TableName.Environment));
+ const formattedDocs = sqlNestRelationships({
+ data: docs,
+ key: "id",
+ parentMapper: (data) => ({
+ projectId: data.projectId,
+ ...SecretApprovalPoliciesSchema.parse(data)
+ }),
+ childrenMapper: [
+ {
+ key: "environmentId",
+ label: "environments" as const,
+ mapper: ({ environmentId: id, envName, envSlug }) => ({
+ id,
+ name: envName,
+ slug: envSlug
+ })
+ }
+ ]
+ });
+ return formattedDocs?.[0];
+ } catch (error) {
+ throw new DatabaseError({ error, name: "findPolicyByEnvIdAndSecretPath" });
+ }
+ };
+
+ return { ...secretApprovalPolicyOrm, findById, find, softDeleteById, findPolicyByEnvIdAndSecretPath };
};
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts
new file mode 100644
index 000000000..d12ace04c
--- /dev/null
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-environment-dal.ts
@@ -0,0 +1,32 @@
+import { Knex } from "knex";
+
+import { TDbClient } from "@app/db";
+import { TableName } from "@app/db/schemas";
+import { DatabaseError } from "@app/lib/errors";
+import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
+
+export type TSecretApprovalPolicyEnvironmentDALFactory = ReturnType;
+
+export const secretApprovalPolicyEnvironmentDALFactory = (db: TDbClient) => {
+ const secretApprovalPolicyEnvironmentOrm = ormify(db, TableName.SecretApprovalPolicyEnvironment);
+
+ const findAvailablePoliciesByEnvId = async (envId: string, tx?: Knex) => {
+ try {
+ const docs = await (tx || db.replicaNode())(TableName.SecretApprovalPolicyEnvironment)
+ .join(
+ TableName.SecretApprovalPolicy,
+ `${TableName.SecretApprovalPolicyEnvironment}.policyId`,
+ `${TableName.SecretApprovalPolicy}.id`
+ )
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ .where(buildFindFilter({ envId }, TableName.SecretApprovalPolicyEnvironment))
+ .whereNull(`${TableName.SecretApprovalPolicy}.deletedAt`)
+ .select(selectAllTableCols(TableName.SecretApprovalPolicyEnvironment));
+ return docs;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "findAvailablePoliciesByEnvId" });
+ }
+ };
+
+ return { ...secretApprovalPolicyEnvironmentOrm, findAvailablePoliciesByEnvId };
+};
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
index 41a635e2f..96757dc22 100644
--- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts
@@ -19,6 +19,7 @@ import {
TSecretApprovalPolicyBypasserDALFactory
} from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
+import { TSecretApprovalPolicyEnvironmentDALFactory } from "./secret-approval-policy-environment-dal";
import {
TCreateSapDTO,
TDeleteSapDTO,
@@ -36,12 +37,13 @@ const getPolicyScore = (policy: { secretPath?: string | null }) =>
type TSecretApprovalPolicyServiceFactoryDep = {
permissionService: Pick;
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
- projectEnvDAL: Pick;
+ projectEnvDAL: Pick;
userDAL: Pick;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
secretApprovalPolicyBypasserDAL: TSecretApprovalPolicyBypasserDALFactory;
licenseService: Pick;
secretApprovalRequestDAL: Pick;
+ secretApprovalPolicyEnvironmentDAL: TSecretApprovalPolicyEnvironmentDALFactory;
};
export type TSecretApprovalPolicyServiceFactory = ReturnType;
@@ -51,27 +53,30 @@ export const secretApprovalPolicyServiceFactory = ({
permissionService,
secretApprovalPolicyApproverDAL,
secretApprovalPolicyBypasserDAL,
+ secretApprovalPolicyEnvironmentDAL,
projectEnvDAL,
userDAL,
licenseService,
secretApprovalRequestDAL
}: TSecretApprovalPolicyServiceFactoryDep) => {
const $policyExists = async ({
+ envIds,
envId,
secretPath,
policyId
}: {
- envId: string;
+ envIds?: string[];
+ envId?: string;
secretPath: string;
policyId?: string;
}) => {
- const policy = await secretApprovalPolicyDAL
- .findOne({
- envId,
- secretPath,
- deletedAt: null
- })
- .catch(() => null);
+ if (!envIds && !envId) {
+ throw new BadRequestError({ message: "At least one environment should be provided" });
+ }
+ const policy = await secretApprovalPolicyDAL.findPolicyByEnvIdAndSecretPath({
+ envIds: envId ? [envId] : envIds || [],
+ secretPath
+ });
return policyId ? policy && policy.id !== policyId : Boolean(policy);
};
@@ -88,6 +93,7 @@ export const secretApprovalPolicyServiceFactory = ({
projectId,
secretPath,
environment,
+ environments,
enforcementLevel,
allowedSelfApprovals
}: TCreateSapDTO) => {
@@ -127,17 +133,23 @@ export const secretApprovalPolicyServiceFactory = ({
});
}
- const env = await projectEnvDAL.findOne({ slug: environment, projectId });
- if (!env) {
- throw new NotFoundError({
- message: `Environment with slug '${environment}' not found in project with ID ${projectId}`
- });
+ const mergedEnvs = (environment ? [environment] : environments) || [];
+ if (mergedEnvs.length === 0) {
+ throw new BadRequestError({ message: "Must provide either environment or environments" });
+ }
+ const envs = await projectEnvDAL.find({ $in: { slug: mergedEnvs }, projectId });
+ if (!envs.length || envs.length !== mergedEnvs.length) {
+ const notFoundEnvs = mergedEnvs.filter((env) => !envs.find((el) => el.slug === env));
+ throw new NotFoundError({ message: `One or more environments not found: ${notFoundEnvs.join(", ")}` });
}
- if (await $policyExists({ envId: env.id, secretPath })) {
- throw new BadRequestError({
- message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'`
- });
+ for (const env of envs) {
+ // eslint-disable-next-line no-await-in-loop
+ if (await $policyExists({ envId: env.id, secretPath })) {
+ throw new BadRequestError({
+ message: `A policy for secret path '${secretPath}' already exists in environment '${env.slug}'`
+ });
+ }
}
let groupBypassers: string[] = [];
@@ -181,7 +193,7 @@ export const secretApprovalPolicyServiceFactory = ({
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
const doc = await secretApprovalPolicyDAL.create(
{
- envId: env.id,
+ envId: envs[0].id,
approvals,
secretPath,
name,
@@ -190,6 +202,13 @@ export const secretApprovalPolicyServiceFactory = ({
},
tx
);
+ await secretApprovalPolicyEnvironmentDAL.insertMany(
+ envs.map((env) => ({
+ envId: env.id,
+ policyId: doc.id
+ })),
+ tx
+ );
let userApproverIds = userApprovers;
if (userApproverNames.length) {
@@ -253,12 +272,13 @@ export const secretApprovalPolicyServiceFactory = ({
return doc;
});
- return { ...secretApproval, environment: env, projectId };
+ return { ...secretApproval, environments: envs, projectId, environment: envs[0] };
};
const updateSecretApprovalPolicy = async ({
approvers,
bypassers,
+ environments,
secretPath,
name,
actorId,
@@ -288,17 +308,26 @@ export const secretApprovalPolicyServiceFactory = ({
message: `Secret approval policy with ID '${secretPolicyId}' not found`
});
}
-
+ let envs = secretApprovalPolicy.environments;
if (
- await $policyExists({
- envId: secretApprovalPolicy.envId,
- secretPath: secretPath || secretApprovalPolicy.secretPath,
- policyId: secretApprovalPolicy.id
- })
+ environments &&
+ (environments.length !== envs.length || environments.some((env) => !envs.find((el) => el.slug === env)))
) {
- throw new BadRequestError({
- message: `A policy for secret path '${secretPath}' already exists in environment '${secretApprovalPolicy.environment.slug}'`
- });
+ envs = await projectEnvDAL.find({ $in: { slug: environments }, projectId: secretApprovalPolicy.projectId });
+ }
+ for (const env of envs) {
+ if (
+ // eslint-disable-next-line no-await-in-loop
+ await $policyExists({
+ envId: env.id,
+ secretPath: secretPath || secretApprovalPolicy.secretPath,
+ policyId: secretApprovalPolicy.id
+ })
+ ) {
+ throw new BadRequestError({
+ message: `A policy for secret path '${secretPath || secretApprovalPolicy.secretPath}' already exists in environment '${env.slug}'`
+ });
+ }
}
const { permission } = await permissionService.getProjectPermission({
@@ -415,6 +444,17 @@ export const secretApprovalPolicyServiceFactory = ({
);
}
+ if (environments) {
+ await secretApprovalPolicyEnvironmentDAL.delete({ policyId: doc.id }, tx);
+ await secretApprovalPolicyEnvironmentDAL.insertMany(
+ envs.map((env) => ({
+ envId: env.id,
+ policyId: doc.id
+ })),
+ tx
+ );
+ }
+
await secretApprovalPolicyBypasserDAL.delete({ policyId: doc.id }, tx);
if (bypasserUserIds.length) {
@@ -441,7 +481,8 @@ export const secretApprovalPolicyServiceFactory = ({
});
return {
...updatedSap,
- environment: secretApprovalPolicy.environment,
+ environments: secretApprovalPolicy.environments,
+ environment: secretApprovalPolicy.environments[0],
projectId: secretApprovalPolicy.projectId
};
};
@@ -487,7 +528,12 @@ export const secretApprovalPolicyServiceFactory = ({
const updatedPolicy = await secretApprovalPolicyDAL.softDeleteById(secretPolicyId, tx);
return updatedPolicy;
});
- return { ...deletedPolicy, projectId: sapPolicy.projectId, environment: sapPolicy.environment };
+ return {
+ ...deletedPolicy,
+ projectId: sapPolicy.projectId,
+ environments: sapPolicy.environments,
+ environment: sapPolicy.environments[0]
+ };
};
const getSecretApprovalPolicyByProjectId = async ({
@@ -520,7 +566,7 @@ export const secretApprovalPolicyServiceFactory = ({
});
}
- const policies = await secretApprovalPolicyDAL.find({ envId: env.id, deletedAt: null });
+ const policies = await secretApprovalPolicyDAL.find({ deletedAt: null }, { envId: env.id });
if (!policies.length) return;
// this will filter policies either without scoped to secret path or the one that matches with secret path
const policiesFilteredByPath = policies.filter(
diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
index ba5334e5c..80369e638 100644
--- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
+++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts
@@ -5,7 +5,8 @@ import { ApproverType, BypasserType } from "../access-approval-policy/access-app
export type TCreateSapDTO = {
approvals: number;
secretPath: string;
- environment: string;
+ environment?: string;
+ environments?: string[];
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
bypassers?: (
| { type: BypasserType.Group; id: string }
@@ -29,6 +30,7 @@ export type TUpdateSapDTO = {
name?: string;
enforcementLevel?: EnforcementLevel;
allowedSelfApprovals?: boolean;
+ environments?: string[];
} & Omit;
export type TDeleteSapDTO = {
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts
index c098d9b31..49f31bdf6 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts
@@ -40,6 +40,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
+ .leftJoin(TableName.SecretApprovalPolicyEnvironment, (bd) => {
+ bd.on(
+ `${TableName.SecretApprovalPolicy}.id`,
+ "=",
+ `${TableName.SecretApprovalPolicyEnvironment}.policyId`
+ ).andOn(`${TableName.SecretApprovalPolicyEnvironment}.envId`, "=", `${TableName.SecretFolder}.envId`);
+ })
.leftJoin(
db(TableName.Users).as("statusChangedByUser"),
`${TableName.SecretApprovalRequest}.statusChangedByUserId`,
@@ -146,7 +153,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
- tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"),
+ tx.ref("envId").withSchema(TableName.SecretApprovalPolicyEnvironment).as("policyEnvId"),
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"),
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
index 2be0361e0..d1eefe7e3 100644
--- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
+++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts
@@ -69,6 +69,7 @@ import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission
import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
+import { scanSecretPolicyViolations } from "../secret-scanning-v2/secret-scanning-v2-fns";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
import { sendApprovalEmailsFn } from "./secret-approval-request-fns";
@@ -537,6 +538,11 @@ export const secretApprovalRequestServiceFactory = ({
message: "The policy associated with this secret approval request has been deleted."
});
}
+ if (!policy.envId) {
+ throw new BadRequestError({
+ message: "The policy associated with this secret approval request is not linked to the environment."
+ });
+ }
const { hasRole } = await permissionService.getProjectPermission({
actor: ActorType.USER,
@@ -1407,6 +1413,20 @@ export const secretApprovalRequestServiceFactory = ({
projectId
});
+ const project = await projectDAL.findById(projectId);
+ await scanSecretPolicyViolations(
+ projectId,
+ secretPath,
+ [
+ ...(data[SecretOperations.Create] || []),
+ ...(data[SecretOperations.Update] || []).filter((el) => el.secretValue)
+ ].map((el) => ({
+ secretKey: el.secretKey,
+ secretValue: el.secretValue as string
+ })),
+ project.secretDetectionIgnoreValues || []
+ );
+
// for created secret approval change
const createdSecrets = data[SecretOperations.Create];
if (createdSecrets && createdSecrets?.length) {
diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts
index 3e6e5d265..1da1db376 100644
--- a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts
+++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts
@@ -7,12 +7,13 @@ import {
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
+import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
executeWithPotentialGateway,
SQL_CONNECTION_ALTER_LOGIN_STATEMENT
} from "@app/services/app-connection/shared/sql";
-import { generatePassword } from "../utils";
+import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../utils";
import {
TSqlCredentialsRotationGeneratedCredentials,
TSqlCredentialsRotationWithConnection
@@ -32,6 +33,11 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
return redactedMessage;
};
+const ORACLE_PASSWORD_REQUIREMENTS = {
+ ...DEFAULT_PASSWORD_REQUIREMENTS,
+ length: 30
+};
+
export const sqlCredentialsRotationFactory: TRotationFactory<
TSqlCredentialsRotationWithConnection,
TSqlCredentialsRotationGeneratedCredentials
@@ -43,6 +49,9 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
secretsMapping
} = secretRotation;
+ const passwordRequirement =
+ connection.app === AppConnection.OracleDB ? ORACLE_PASSWORD_REQUIREMENTS : DEFAULT_PASSWORD_REQUIREMENTS;
+
const executeOperation = (
operation: (client: Knex) => Promise,
credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number]
@@ -65,7 +74,7 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
try {
await executeOperation(async (client) => {
- await client.raw("SELECT 1");
+ await client.raw(connection.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`);
}, credentials);
} catch (error) {
throw new Error(redactPasswords(error, [credentials]));
@@ -75,11 +84,13 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
const issueCredentials: TRotationFactoryIssueCredentials = async (
callback
) => {
+ // For SQL, since we get existing users, we change both their passwords
+ // on issue to invalidate their existing passwords
// For SQL, since we get existing users, we change both their passwords
// on issue to invalidate their existing passwords
const credentialsSet = [
- { username: username1, password: generatePassword() },
- { username: username2, password: generatePassword() }
+ { username: username1, password: generatePassword(passwordRequirement) },
+ { username: username2, password: generatePassword(passwordRequirement) }
];
try {
@@ -105,7 +116,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
credentialsToRevoke,
callback
) => {
- const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
+ const revokedCredentials = credentialsToRevoke.map(({ username }) => ({
+ username,
+ password: generatePassword(passwordRequirement)
+ }));
try {
await executeOperation(async (client) => {
@@ -128,7 +142,10 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
callback
) => {
// generate new password for the next active user
- const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() };
+ const credentials = {
+ username: activeIndex === 0 ? username2 : username1,
+ password: generatePassword(passwordRequirement)
+ };
try {
await executeOperation(async (client) => {
diff --git a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts
index ef58687a1..4122abfda 100644
--- a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts
+++ b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts
@@ -11,7 +11,7 @@ type TPasswordRequirements = {
allowedSymbols?: string;
};
-const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
+export const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
length: 48,
required: {
lowercase: 1,
diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts
index 9489f4658..bdda63f7a 100644
--- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts
+++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-fns.ts
@@ -1,11 +1,21 @@
import { AxiosError } from "axios";
import { exec } from "child_process";
+import { join } from "path";
+import picomatch from "picomatch";
import RE2 from "re2";
-import { readFindingsFile } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
+import {
+ createTempFolder,
+ deleteTempFolder,
+ readFindingsFile,
+ writeTextToFile
+} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-fns";
import { SecretMatch } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import { BITBUCKET_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/bitbucket";
import { GITHUB_SECRET_SCANNING_DATA_SOURCE_LIST_OPTION } from "@app/ee/services/secret-scanning-v2/github";
+import { getConfig } from "@app/lib/config/env";
+import { crypto } from "@app/lib/crypto";
+import { BadRequestError } from "@app/lib/errors";
import { titleCaseToCamelCase } from "@app/lib/fn";
import { SecretScanningDataSource, SecretScanningFindingSeverity } from "./secret-scanning-v2-enums";
@@ -46,6 +56,19 @@ export function scanDirectory(inputPath: string, outputPath: string, configPath?
});
}
+export function scanFile(inputPath: string): Promise {
+ return new Promise((resolve, reject) => {
+ const command = `infisical scan --exit-code=77 --source "${inputPath}" --no-git`;
+ exec(command, (error) => {
+ if (error && error.code === 77) {
+ reject(error);
+ } else {
+ resolve();
+ }
+ });
+ });
+}
+
export const scanGitRepositoryAndGetFindings = async (
scanPath: string,
findingsPath: string,
@@ -140,3 +163,47 @@ export const parseScanErrorMessage = (err: unknown): string => {
? errorMessage
: `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
};
+
+export const scanSecretPolicyViolations = async (
+ projectId: string,
+ secretPath: string,
+ secrets: { secretKey: string; secretValue: string }[],
+ ignoreValues: string[]
+) => {
+ const appCfg = getConfig();
+
+ if (!appCfg.PARAMS_FOLDER_SECRET_DETECTION_ENABLED) {
+ return;
+ }
+
+ const match = appCfg.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.find(
+ (el) => el.projectId === projectId && picomatch.isMatch(secretPath, el.secretPath, { strictSlashes: false })
+ );
+
+ if (!match) {
+ return;
+ }
+
+ const tempFolder = await createTempFolder();
+ try {
+ const scanPromises = secrets
+ .filter((secret) => !ignoreValues.includes(secret.secretValue))
+ .map(async (secret) => {
+ const secretFilePath = join(tempFolder, `${crypto.nativeCrypto.randomUUID()}.txt`);
+ await writeTextToFile(secretFilePath, `${secret.secretKey}=${secret.secretValue}`);
+
+ try {
+ await scanFile(secretFilePath);
+ } catch (error) {
+ throw new BadRequestError({
+ message: `Secret value detected in ${secret.secretKey}. Please add this instead to the designated secrets path in the project.`,
+ name: "SecretPolicyViolation"
+ });
+ }
+ });
+
+ await Promise.all(scanPromises);
+ } finally {
+ await deleteTempFolder(tempFolder);
+ }
+};
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index b6c00985a..71b6afb6b 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -704,7 +704,8 @@ export const PROJECTS = {
hasDeleteProtection: "Enable or disable delete protection for the project.",
secretSharing: "Enable or disable secret sharing for the project.",
showSnapshotsLegacy: "Enable or disable legacy snapshots for the project.",
- defaultProduct: "The default product in which the project will open"
+ defaultProduct: "The default product in which the project will open",
+ secretDetectionIgnoreValues: "The list of secret values to ignore for secret detection."
},
GET_KEY: {
workspaceId: "The ID of the project to get the key from."
@@ -2245,7 +2246,9 @@ export const AppConnections = {
},
AZURE_CLIENT_SECRETS: {
code: "The OAuth code to use to connect with Azure Client Secrets.",
- tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
+ tenantId: "The Tenant ID to use to connect with Azure Client Secrets.",
+ clientId: "The Client ID to use to connect with Azure Client Secrets.",
+ clientSecret: "The Client Secret to use to connect with Azure Client Secrets."
},
AZURE_DEVOPS: {
code: "The OAuth code to use to connect with Azure DevOps.",
@@ -2373,6 +2376,10 @@ export const SecretSyncs = {
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
tags: "Optional tags to add to secrets synced by Infisical.",
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.`
+ },
+ RENDER: {
+ autoRedeployServices:
+ "Whether Infisical should automatically redeploy the configured Render service upon secret changes."
}
},
DESTINATION_CONFIG: {
diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts
index 986963e47..3fa2c0f82 100644
--- a/backend/src/lib/config/env.ts
+++ b/backend/src/lib/config/env.ts
@@ -204,6 +204,17 @@ const envSchema = z
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
+ // Special Detection Feature
+ PARAMS_FOLDER_SECRET_DETECTION_PATHS: zpStr(
+ z
+ .string()
+ .optional()
+ .transform((val) => {
+ if (!val) return undefined;
+ return JSON.parse(val) as { secretPath: string; projectId: string }[];
+ })
+ ),
+
// HSM
HSM_LIB_PATH: zpStr(z.string().optional()),
HSM_PIN: zpStr(z.string().optional()),
@@ -261,10 +272,26 @@ const envSchema = z
// gcp app
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()),
- // azure app
+ // Legacy Single Multi Purpose Azure App Connection
INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()),
INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()),
+ // Azure App Configuration App Connection
+ INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID: zpStr(z.string().optional()),
+ INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET: zpStr(z.string().optional()),
+
+ // Azure Key Vault App Connection
+ INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID: zpStr(z.string().optional()),
+ INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET: zpStr(z.string().optional()),
+
+ // Azure Client Secrets App Connection
+ INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID: zpStr(z.string().optional()),
+ INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET: zpStr(z.string().optional()),
+
+ // Azure DevOps App Connection
+ INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID: zpStr(z.string().optional()),
+ INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET: zpStr(z.string().optional()),
+
// datadog
SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"),
DATADOG_PROFILING_ENABLED: zodStrBool.default("false"),
@@ -341,7 +368,24 @@ const envSchema = z
isHsmConfigured:
Boolean(data.HSM_LIB_PATH) && Boolean(data.HSM_PIN) && Boolean(data.HSM_KEY_LABEL) && data.HSM_SLOT !== undefined,
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG,
- SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(",")
+ SECRET_SCANNING_ORG_WHITELIST: data.SECRET_SCANNING_ORG_WHITELIST?.split(","),
+ PARAMS_FOLDER_SECRET_DETECTION_ENABLED: (data.PARAMS_FOLDER_SECRET_DETECTION_PATHS?.length ?? 0) > 0,
+ INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID:
+ data.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
+ INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET:
+ data.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID:
+ data.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
+ INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET:
+ data.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID:
+ data.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
+ INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET:
+ data.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID:
+ data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
+ INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET:
+ data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET
}));
export type TEnvConfig = Readonly>;
@@ -451,15 +495,54 @@ export const overwriteSchema: {
}
]
},
- azure: {
- name: "Azure",
+ azureAppConfiguration: {
+ name: "Azure App Configuration",
fields: [
{
- key: "INF_APP_CONNECTION_AZURE_CLIENT_ID",
+ key: "INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID",
description: "The Application (Client) ID of your Azure application."
},
{
- key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRET",
+ key: "INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET",
+ description: "The Client Secret of your Azure application."
+ }
+ ]
+ },
+ azureKeyVault: {
+ name: "Azure Key Vault",
+ fields: [
+ {
+ key: "INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID",
+ description: "The Application (Client) ID of your Azure application."
+ },
+ {
+ key: "INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET",
+ description: "The Client Secret of your Azure application."
+ }
+ ]
+ },
+ azureClientSecrets: {
+ name: "Azure Client Secrets",
+ fields: [
+ {
+ key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID",
+ description: "The Application (Client) ID of your Azure application."
+ },
+ {
+ key: "INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET",
+ description: "The Client Secret of your Azure application."
+ }
+ ]
+ },
+ azureDevOps: {
+ name: "Azure DevOps",
+ fields: [
+ {
+ key: "INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID",
+ description: "The Application (Client) ID of your Azure application."
+ },
+ {
+ key: "INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET",
description: "The Client Secret of your Azure application."
}
]
diff --git a/backend/src/lib/crypto/cryptography/crypto.ts b/backend/src/lib/crypto/cryptography/crypto.ts
index 967e7e007..b8fc45645 100644
--- a/backend/src/lib/crypto/cryptography/crypto.ts
+++ b/backend/src/lib/crypto/cryptography/crypto.ts
@@ -14,7 +14,7 @@ import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal
import { ADMIN_CONFIG_DB_UUID } from "@app/services/super-admin/super-admin-service";
import { isBase64 } from "../../base64";
-import { getConfig } from "../../config/env";
+import { getConfig, TEnvConfig } from "../../config/env";
import { CryptographyError } from "../../errors";
import { logger } from "../../logger";
import { asymmetricFipsValidated } from "./asymmetric-fips";
@@ -106,12 +106,12 @@ const cryptographyFactory = () => {
}
};
- const $setFipsModeEnabled = (enabled: boolean) => {
+ const $setFipsModeEnabled = (enabled: boolean, envCfg?: Pick) => {
// If FIPS is enabled, we need to validate that the ENCRYPTION_KEY is in a base64 format, and is a 256-bit key.
if (enabled) {
crypto.setFips(true);
- const appCfg = getConfig();
+ const appCfg = envCfg || getConfig();
if (appCfg.ENCRYPTION_KEY) {
// we need to validate that the ENCRYPTION_KEY is a base64 encoded 256-bit key
@@ -141,14 +141,14 @@ const cryptographyFactory = () => {
$isInitialized = true;
};
- const initialize = async (superAdminDAL: TSuperAdminDALFactory) => {
+ const initialize = async (superAdminDAL: TSuperAdminDALFactory, envCfg?: Pick) => {
if ($isInitialized) {
return isFipsModeEnabled();
}
if (process.env.FIPS_ENABLED !== "true") {
logger.info("Cryptography module initialized in normal operation mode.");
- $setFipsModeEnabled(false);
+ $setFipsModeEnabled(false, envCfg);
return false;
}
@@ -158,11 +158,11 @@ const cryptographyFactory = () => {
if (serverCfg) {
if (serverCfg.fipsEnabled) {
logger.info("[FIPS]: Instance is configured for FIPS mode of operation. Continuing startup with FIPS enabled.");
- $setFipsModeEnabled(true);
+ $setFipsModeEnabled(true, envCfg);
return true;
}
logger.info("[FIPS]: Instance age predates FIPS mode inception date. Continuing without FIPS.");
- $setFipsModeEnabled(false);
+ $setFipsModeEnabled(false, envCfg);
return false;
}
@@ -171,7 +171,7 @@ const cryptographyFactory = () => {
// TODO(daniel): check if it's an enterprise deployment
// if there is no server cfg, and FIPS_MODE is `true`, its a fresh FIPS deployment. We need to set the fipsEnabled to true.
- $setFipsModeEnabled(true);
+ $setFipsModeEnabled(true, envCfg);
return true;
};
diff --git a/backend/src/lib/validator/validate-url.ts b/backend/src/lib/validator/validate-url.ts
index 8f195e0b5..a4c07b37d 100644
--- a/backend/src/lib/validator/validate-url.ts
+++ b/backend/src/lib/validator/validate-url.ts
@@ -14,6 +14,11 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
if (appCfg.isDevelopmentMode) return;
const validUrl = new URL(url);
+
+ if (validUrl.username || validUrl.password) {
+ throw new BadRequestError({ message: "URLs with user credentials (e.g., user:pass@) are not allowed" });
+ }
+
const inputHostIps: string[] = [];
if (isIPv4(validUrl.hostname)) {
inputHostIps.push(validUrl.hostname);
diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts
index 7b4f5d90c..4fe639510 100644
--- a/backend/src/server/plugins/auth/inject-identity.ts
+++ b/backend/src/server/plugins/auth/inject-identity.ts
@@ -162,6 +162,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
kubernetes: token?.identityAuth?.kubernetes
});
}
+ if (token?.identityAuth?.aws) {
+ requestContext.set("identityAuthInfo", {
+ identityId: identity.identityId,
+ aws: token?.identityAuth?.aws
+ });
+ }
break;
}
case AuthMode.SERVICE_TOKEN: {
diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts
index 65f306f1c..d6d627576 100644
--- a/backend/src/server/routes/index.ts
+++ b/backend/src/server/routes/index.ts
@@ -11,6 +11,7 @@ import {
accessApprovalPolicyBypasserDALFactory
} from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
+import { accessApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-environment-dal";
import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal";
import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal";
@@ -76,6 +77,7 @@ import {
secretApprovalPolicyBypasserDALFactory
} from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal";
import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
+import { secretApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-environment-dal";
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { secretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { secretApprovalRequestReviewerDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-reviewer-dal";
@@ -425,9 +427,11 @@ export const registerRoutes = async (
const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db);
const accessApprovalPolicyBypasserDAL = accessApprovalPolicyBypasserDALFactory(db);
const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db);
+ const accessApprovalPolicyEnvironmentDAL = accessApprovalPolicyEnvironmentDALFactory(db);
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
const sapBypasserDAL = secretApprovalPolicyBypasserDALFactory(db);
+ const sapEnvironmentDAL = secretApprovalPolicyEnvironmentDALFactory(db);
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
const secretApprovalRequestReviewerDAL = secretApprovalRequestReviewerDALFactory(db);
@@ -561,6 +565,7 @@ export const registerRoutes = async (
projectEnvDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL,
secretApprovalPolicyBypasserDAL: sapBypasserDAL,
+ secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL,
permissionService,
secretApprovalPolicyDAL,
licenseService,
@@ -1037,6 +1042,15 @@ export const registerRoutes = async (
kmsService
});
+ const gatewayService = gatewayServiceFactory({
+ permissionService,
+ gatewayDAL,
+ kmsService,
+ licenseService,
+ orgGatewayConfigDAL,
+ keyStore
+ });
+
const secretSyncQueue = secretSyncQueueFactory({
queueService,
secretSyncDAL,
@@ -1060,7 +1074,8 @@ export const registerRoutes = async (
secretVersionTagV2BridgeDAL,
resourceMetadataDAL,
appConnectionDAL,
- licenseService
+ licenseService,
+ gatewayService
});
const secretQueueService = secretQueueFactory({
@@ -1151,7 +1166,9 @@ export const registerRoutes = async (
keyStore,
licenseService,
projectDAL,
- folderDAL
+ folderDAL,
+ accessApprovalPolicyEnvironmentDAL,
+ secretApprovalPolicyEnvironmentDAL: sapEnvironmentDAL
});
const projectRoleService = projectRoleServiceFactory({
@@ -1226,6 +1243,7 @@ export const registerRoutes = async (
const secretV2BridgeService = secretV2BridgeServiceFactory({
folderDAL,
+ projectDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
folderCommitService,
secretQueueService,
@@ -1312,6 +1330,7 @@ export const registerRoutes = async (
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
accessApprovalPolicyBypasserDAL,
+ accessApprovalPolicyEnvironmentDAL,
groupDAL,
permissionService,
projectEnvDAL,
@@ -1476,15 +1495,6 @@ export const registerRoutes = async (
licenseService
});
- const gatewayService = gatewayServiceFactory({
- permissionService,
- gatewayDAL,
- kmsService,
- licenseService,
- orgGatewayConfigDAL,
- keyStore
- });
-
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
identityKubernetesAuthDAL,
identityOrgMembershipDAL,
diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts
index beef663b9..8af4baa8b 100644
--- a/backend/src/server/routes/sanitizedSchemas.ts
+++ b/backend/src/server/routes/sanitizedSchemas.ts
@@ -93,6 +93,13 @@ export const sapPubSchema = SecretApprovalPoliciesSchema.merge(
name: z.string(),
slug: z.string()
}),
+ environments: z.array(
+ z.object({
+ id: z.string(),
+ name: z.string(),
+ slug: z.string()
+ })
+ ),
projectId: z.string()
})
);
@@ -264,7 +271,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
auditLogsRetentionDays: true,
hasDeleteProtection: true,
secretSharing: true,
- showSnapshotsLegacy: true
+ showSnapshotsLegacy: true,
+ secretDetectionIgnoreValues: true
});
export const SanitizedTagSchema = SecretTagsSchema.pick({
diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts
index 6cc50dc5c..6ad368816 100644
--- a/backend/src/server/routes/v1/admin-router.ts
+++ b/backend/src/server/routes/v1/admin-router.ts
@@ -52,7 +52,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
defaultAuthOrgAuthEnforced: z.boolean().nullish(),
defaultAuthOrgAuthMethod: z.string().nullish(),
isSecretScanningDisabled: z.boolean(),
- kubernetesAutoFetchServiceAccountToken: z.boolean()
+ kubernetesAutoFetchServiceAccountToken: z.boolean(),
+ paramsFolderSecretDetectionEnabled: z.boolean()
})
})
}
@@ -67,7 +68,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
fipsEnabled: crypto.isFipsModeEnabled(),
isMigrationModeOn: serverEnvs.MAINTENANCE_MODE,
isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING,
- kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN
+ kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN,
+ paramsFolderSecretDetectionEnabled: serverEnvs.PARAMS_FOLDER_SECRET_DETECTION_ENABLED
}
};
}
@@ -685,6 +687,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit
},
schema: {
+ hide: false,
body: z.object({
email: z.string().email().trim().min(1),
password: z.string().trim().min(1),
diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts
index 135effc4c..029d4a848 100644
--- a/backend/src/server/routes/v1/dashboard-router.ts
+++ b/backend/src/server/routes/v1/dashboard-router.ts
@@ -270,11 +270,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
});
-
- remainingLimit -= imports.length;
- adjustedOffset = 0;
- } else {
- adjustedOffset = Math.max(0, adjustedOffset - totalImportCount);
}
}
@@ -317,7 +312,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
- if (!includeDynamicSecrets && !includeSecrets)
+ if (!includeDynamicSecrets && !includeSecrets && !includeSecretRotations)
return {
folders,
totalFolderCount,
@@ -547,7 +542,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
(totalFolderCount ?? 0) +
(totalDynamicSecretCount ?? 0) +
(totalSecretCount ?? 0) +
- (totalImportCount ?? 0) +
(totalSecretRotationCount ?? 0)
};
}
diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts
index ab27ce753..7a5a9341f 100644
--- a/backend/src/server/routes/v1/project-router.ts
+++ b/backend/src/server/routes/v1/project-router.ts
@@ -369,7 +369,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.describe(PROJECTS.UPDATE.slug),
secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing),
showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy),
- defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct)
+ defaultProduct: z.nativeEnum(ProjectType).optional().describe(PROJECTS.UPDATE.defaultProduct),
+ secretDetectionIgnoreValues: z
+ .array(z.string())
+ .optional()
+ .describe(PROJECTS.UPDATE.secretDetectionIgnoreValues)
}),
response: {
200: z.object({
@@ -392,7 +396,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
hasDeleteProtection: req.body.hasDeleteProtection,
slug: req.body.slug,
secretSharing: req.body.secretSharing,
- showSnapshotsLegacy: req.body.showSnapshotsLegacy
+ showSnapshotsLegacy: req.body.showSnapshotsLegacy,
+ secretDetectionIgnoreValues: req.body.secretDetectionIgnoreValues
},
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
diff --git a/backend/src/server/routes/v3/external-migration-router.ts b/backend/src/server/routes/v3/external-migration-router.ts
index 865287157..89621811d 100644
--- a/backend/src/server/routes/v3/external-migration-router.ts
+++ b/backend/src/server/routes/v3/external-migration-router.ts
@@ -1,9 +1,11 @@
import fastifyMultipart from "@fastify/multipart";
+import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
-import { readLimit } from "@app/server/config/rateLimiter";
+import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
+import { VaultMappingType } from "@app/services/external-migration/external-migration-types";
const MB25_IN_BYTES = 26214400;
@@ -15,7 +17,7 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
bodyLimit: MB25_IN_BYTES,
url: "/env-key",
config: {
- rateLimit: readLimit
+ rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
@@ -52,4 +54,30 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
});
}
});
+
+ server.route({
+ method: "POST",
+ url: "/vault",
+ config: {
+ rateLimit: writeLimit
+ },
+ schema: {
+ body: z.object({
+ vaultAccessToken: z.string(),
+ vaultNamespace: z.string().trim().optional(),
+ vaultUrl: z.string(),
+ mappingType: z.nativeEnum(VaultMappingType)
+ })
+ },
+ onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
+ handler: async (req) => {
+ await server.services.migration.importVaultData({
+ actorId: req.permission.id,
+ actor: req.permission.type,
+ actorOrgId: req.permission.orgId,
+ actorAuthMethod: req.permission.authMethod,
+ ...req.body
+ });
+ }
+ });
};
diff --git a/backend/src/server/routes/v3/index.ts b/backend/src/server/routes/v3/index.ts
index ed8401560..1c31741a1 100644
--- a/backend/src/server/routes/v3/index.ts
+++ b/backend/src/server/routes/v3/index.ts
@@ -11,5 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerSecretRouter, { prefix: "/secrets" });
await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" });
- await server.register(registerExternalMigrationRouter, { prefix: "/migrate" });
+ await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" });
};
diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts
index 98c9e5f4d..976d154b3 100644
--- a/backend/src/services/app-connection/app-connection-service.ts
+++ b/backend/src/services/app-connection/app-connection-service.ts
@@ -583,7 +583,7 @@ export const appConnectionServiceFactory = ({
deleteAppConnection,
connectAppConnectionById,
listAvailableAppConnectionsForUser,
- github: githubConnectionService(connectAppConnectionById),
+ github: githubConnectionService(connectAppConnectionById, gatewayService),
githubRadar: githubRadarConnectionService(connectAppConnectionById),
gcp: gcpConnectionService(connectAppConnectionById),
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
diff --git a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts
index 937a8a84f..114794dc5 100644
--- a/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts
+++ b/backend/src/services/app-connection/azure-app-configuration/azure-app-configuration-connection-fns.ts
@@ -14,13 +14,13 @@ import {
} from "./azure-app-configuration-connection-types";
export const getAzureAppConfigurationConnectionListItem = () => {
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
+ const { INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID } = getConfig();
return {
name: "Azure App Configuration" as const,
app: AppConnection.AzureAppConfiguration as const,
methods: Object.values(AzureAppConfigurationConnectionMethod) as [AzureAppConfigurationConnectionMethod.OAuth],
- oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
+ oauthClientId: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID
};
};
@@ -29,9 +29,16 @@ export const validateAzureAppConfigurationConnectionCredentials = async (
) => {
const { credentials: inputCredentials, method } = config;
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
+ const {
+ INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID,
+ INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET,
+ SITE_URL
+ } = getConfig();
- if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
+ if (
+ !INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID ||
+ !INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET
+ ) {
throw new InternalServerError({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
@@ -47,8 +54,8 @@ export const validateAzureAppConfigurationConnectionCredentials = async (
grant_type: "authorization_code",
code: inputCredentials.code,
scope: `openid offline_access https://azconfig.io/.default`,
- client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ client_id: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID,
+ client_secret: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
})
);
diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts
index 338126c1e..eb0521c64 100644
--- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts
+++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-enums.ts
@@ -1,3 +1,4 @@
export enum AzureClientSecretsConnectionMethod {
- OAuth = "oauth"
+ OAuth = "oauth",
+ ClientSecret = "client-secret"
}
diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts
index 463e40e7c..41dbb4392 100644
--- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts
+++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-case-declarations */
import { AxiosError, AxiosResponse } from "axios";
import { getConfig } from "@app/lib/config/env";
@@ -16,18 +17,22 @@ import { AppConnection } from "../app-connection-enums";
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
import {
ExchangeCodeAzureResponse,
+ TAzureClientSecretsConnectionClientSecretCredentials,
TAzureClientSecretsConnectionConfig,
TAzureClientSecretsConnectionCredentials
} from "./azure-client-secrets-connection-types";
export const getAzureClientSecretsConnectionListItem = () => {
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
+ const { INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID } = getConfig();
return {
name: "Azure Client Secrets" as const,
app: AppConnection.AzureClientSecrets as const,
- methods: Object.values(AzureClientSecretsConnectionMethod) as [AzureClientSecretsConnectionMethod.OAuth],
- oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
+ methods: Object.values(AzureClientSecretsConnectionMethod) as [
+ AzureClientSecretsConnectionMethod.OAuth,
+ AzureClientSecretsConnectionMethod.ClientSecret
+ ],
+ oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID
};
};
@@ -37,12 +42,6 @@ export const getAzureConnectionAccessToken = async (
kmsService: Pick
) => {
const appCfg = getConfig();
- if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
- throw new BadRequestError({
- message: `Azure environment variables have not been configured`
- });
- }
-
const appConnection = await appConnectionDAL.findById(connectionId);
if (!appConnection) {
@@ -63,104 +62,195 @@ export const getAzureConnectionAccessToken = async (
const { refreshToken } = credentials;
const currentTime = Date.now();
+ switch (appConnection.method) {
+ case AzureClientSecretsConnectionMethod.OAuth:
+ if (
+ !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID ||
+ !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET
+ ) {
+ throw new BadRequestError({
+ message: `Azure OAuth environment variables have not been configured`
+ });
+ }
+ const { data } = await request.post(
+ IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
+ new URLSearchParams({
+ grant_type: "refresh_token",
+ scope: `openid offline_access https://graph.microsoft.com/.default`,
+ client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID,
+ client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET,
+ refresh_token: refreshToken
+ })
+ );
- const { data } = await request.post(
- IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
- new URLSearchParams({
- grant_type: "refresh_token",
- scope: `openid offline_access https://graph.microsoft.com/.default`,
- client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
- refresh_token: refreshToken
- })
- );
+ const updatedCredentials = {
+ ...credentials,
+ accessToken: data.access_token,
+ expiresAt: currentTime + data.expires_in * 1000,
+ refreshToken: data.refresh_token
+ };
- const updatedCredentials = {
- ...credentials,
- accessToken: data.access_token,
- expiresAt: currentTime + data.expires_in * 1000,
- refreshToken: data.refresh_token
- };
+ const encryptedCredentials = await encryptAppConnectionCredentials({
+ credentials: updatedCredentials,
+ orgId: appConnection.orgId,
+ kmsService
+ });
- const encryptedCredentials = await encryptAppConnectionCredentials({
- credentials: updatedCredentials,
- orgId: appConnection.orgId,
- kmsService
- });
+ await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
- await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
+ return data.access_token;
+ case AzureClientSecretsConnectionMethod.ClientSecret:
+ const accessTokenCredentials = (await decryptAppConnectionCredentials({
+ orgId: appConnection.orgId,
+ kmsService,
+ encryptedCredentials: appConnection.encryptedCredentials
+ })) as TAzureClientSecretsConnectionClientSecretCredentials;
+ const { accessToken, expiresAt, clientId, clientSecret, tenantId } = accessTokenCredentials;
+ if (accessToken && expiresAt && expiresAt > currentTime + 300000) {
+ return accessToken;
+ }
- return data.access_token;
+ const { data: clientData } = await request.post(
+ IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
+ new URLSearchParams({
+ grant_type: "client_credentials",
+ scope: `https://graph.microsoft.com/.default`,
+ client_id: clientId,
+ client_secret: clientSecret
+ })
+ );
+
+ const updatedClientCredentials = {
+ ...accessTokenCredentials,
+ accessToken: clientData.access_token,
+ expiresAt: currentTime + clientData.expires_in * 1000
+ };
+
+ const encryptedClientCredentials = await encryptAppConnectionCredentials({
+ credentials: updatedClientCredentials,
+ orgId: appConnection.orgId,
+ kmsService
+ });
+
+ await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials: encryptedClientCredentials });
+
+ return clientData.access_token;
+ default:
+ throw new InternalServerError({
+ message: `Unhandled Azure connection method: ${appConnection.method as AzureClientSecretsConnectionMethod}`
+ });
+ }
};
export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => {
const { credentials: inputCredentials, method } = config;
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
-
- if (!SITE_URL) {
- throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
- }
-
- if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
- throw new InternalServerError({
- message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
- });
- }
-
- let tokenResp: AxiosResponse | null = null;
- let tokenError: AxiosError | null = null;
-
- try {
- tokenResp = await request.post(
- IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
- new URLSearchParams({
- grant_type: "authorization_code",
- code: inputCredentials.code,
- scope: `openid offline_access https://graph.microsoft.com/.default`,
- client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
- redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
- })
- );
- } catch (e: unknown) {
- if (e instanceof AxiosError) {
- tokenError = e;
- } else {
- throw new BadRequestError({
- message: `Unable to validate connection: verify credentials`
- });
- }
- }
-
- if (tokenError) {
- if (tokenError instanceof AxiosError) {
- throw new BadRequestError({
- message: `Failed to get access token: ${
- (tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
- }`
- });
- } else {
- throw new InternalServerError({
- message: "Failed to get access token"
- });
- }
- }
-
- if (!tokenResp) {
- throw new InternalServerError({
- message: `Failed to get access token: Token was empty with no error`
- });
- }
+ const {
+ INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID,
+ INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET,
+ SITE_URL
+ } = getConfig();
switch (method) {
case AzureClientSecretsConnectionMethod.OAuth:
+ if (!SITE_URL) {
+ throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
+ }
+
+ if (
+ !INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID ||
+ !INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET
+ ) {
+ throw new InternalServerError({
+ message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
+ });
+ }
+
+ let tokenResp: AxiosResponse | null = null;
+ let tokenError: AxiosError | null = null;
+
+ try {
+ tokenResp = await request.post(
+ IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
+ new URLSearchParams({
+ grant_type: "authorization_code",
+ code: inputCredentials.code,
+ scope: `openid offline_access https://graph.microsoft.com/.default`,
+ client_id: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_ID,
+ client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET,
+ redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
+ })
+ );
+ } catch (e: unknown) {
+ if (e instanceof AxiosError) {
+ tokenError = e;
+ } else {
+ throw new BadRequestError({
+ message: `Unable to validate connection: verify credentials`
+ });
+ }
+ }
+
+ if (tokenError) {
+ if (tokenError instanceof AxiosError) {
+ throw new BadRequestError({
+ message: `Failed to get access token: ${
+ (tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
+ }`
+ });
+ } else {
+ throw new InternalServerError({
+ message: "Failed to get access token"
+ });
+ }
+ }
+
+ if (!tokenResp) {
+ throw new InternalServerError({
+ message: `Failed to get access token: Token was empty with no error`
+ });
+ }
+
return {
tenantId: inputCredentials.tenantId,
accessToken: tokenResp.data.access_token,
refreshToken: tokenResp.data.refresh_token,
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
};
+
+ case AzureClientSecretsConnectionMethod.ClientSecret:
+ const { tenantId, clientId, clientSecret } = inputCredentials;
+ try {
+ const { data: clientData } = await request.post(
+ IntegrationUrls.AZURE_TOKEN_URL.replace("common", tenantId || "common"),
+ new URLSearchParams({
+ grant_type: "client_credentials",
+ scope: `https://graph.microsoft.com/.default`,
+ client_id: clientId,
+ client_secret: clientSecret
+ })
+ );
+
+ return {
+ tenantId,
+ accessToken: clientData.access_token,
+ expiresAt: Date.now() + clientData.expires_in * 1000,
+ clientId,
+ clientSecret
+ };
+ } catch (e: unknown) {
+ if (e instanceof AxiosError) {
+ throw new BadRequestError({
+ message: `Failed to get access token: ${
+ (e?.response?.data as { error_description?: string })?.error_description || "Unknown error"
+ }`
+ });
+ } else {
+ throw new InternalServerError({
+ message: "Failed to get access token"
+ });
+ }
+ }
default:
throw new InternalServerError({
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts
index 2b4e65a13..d9f178a06 100644
--- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts
+++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-schemas.ts
@@ -26,6 +26,36 @@ export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object
expiresAt: z.number()
});
+export const AzureClientSecretsConnectionClientSecretInputCredentialsSchema = z.object({
+ clientId: z
+ .string()
+ .uuid()
+ .trim()
+ .min(1, "Client ID required")
+ .max(50, "Client ID must be at most 50 characters long")
+ .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientId),
+ clientSecret: z
+ .string()
+ .trim()
+ .min(1, "Client Secret required")
+ .max(50, "Client Secret must be at most 50 characters long")
+ .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.clientSecret),
+ tenantId: z
+ .string()
+ .uuid()
+ .trim()
+ .min(1, "Tenant ID required")
+ .describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId)
+});
+
+export const AzureClientSecretsConnectionClientSecretOutputCredentialsSchema = z.object({
+ clientId: z.string(),
+ clientSecret: z.string(),
+ tenantId: z.string(),
+ accessToken: z.string(),
+ expiresAt: z.number()
+});
+
export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
@@ -34,6 +64,14 @@ export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discrimin
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
)
+ }),
+ z.object({
+ method: z
+ .literal(AzureClientSecretsConnectionMethod.ClientSecret)
+ .describe(AppConnections.CREATE(AppConnection.AzureClientSecrets).method),
+ credentials: AzureClientSecretsConnectionClientSecretInputCredentialsSchema.describe(
+ AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
+ )
})
]);
@@ -43,9 +81,13 @@ export const CreateAzureClientSecretsConnectionSchema = ValidateAzureClientSecre
export const UpdateAzureClientSecretsConnectionSchema = z
.object({
- credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.optional().describe(
- AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials
- )
+ credentials: z
+ .union([
+ AzureClientSecretsConnectionOAuthInputCredentialsSchema,
+ AzureClientSecretsConnectionClientSecretInputCredentialsSchema
+ ])
+ .optional()
+ .describe(AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
@@ -59,6 +101,10 @@ export const AzureClientSecretsConnectionSchema = z.intersection(
z.object({
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
+ }),
+ z.object({
+ method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
+ credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
})
])
);
@@ -69,6 +115,13 @@ export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion(
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
tenantId: true
})
+ }),
+ BaseAzureClientSecretsConnectionSchema.extend({
+ method: z.literal(AzureClientSecretsConnectionMethod.ClientSecret),
+ credentials: AzureClientSecretsConnectionClientSecretOutputCredentialsSchema.pick({
+ clientId: true,
+ tenantId: true
+ })
})
]);
diff --git a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts
index fb20fbadd..1ad5a3411 100644
--- a/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts
+++ b/backend/src/services/app-connection/azure-client-secrets/azure-client-secrets-connection-types.ts
@@ -4,6 +4,7 @@ import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
+ AzureClientSecretsConnectionClientSecretOutputCredentialsSchema,
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
AzureClientSecretsConnectionSchema,
CreateAzureClientSecretsConnectionSchema,
@@ -30,6 +31,10 @@ export type TAzureClientSecretsConnectionCredentials = z.infer<
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
>;
+export type TAzureClientSecretsConnectionClientSecretCredentials = z.infer<
+ typeof AzureClientSecretsConnectionClientSecretOutputCredentialsSchema
+>;
+
export interface ExchangeCodeAzureResponse {
token_type: string;
scope: string;
diff --git a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts
index 644747353..e9bb1f6bd 100644
--- a/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts
+++ b/backend/src/services/app-connection/azure-devops/azure-devops-fns.ts
@@ -23,7 +23,7 @@ import {
} from "./azure-devops-types";
export const getAzureDevopsConnectionListItem = () => {
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
+ const { INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID } = getConfig();
return {
name: "Azure DevOps" as const,
@@ -32,7 +32,7 @@ export const getAzureDevopsConnectionListItem = () => {
AzureDevOpsConnectionMethod.OAuth,
AzureDevOpsConnectionMethod.AccessToken
],
- oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
+ oauthClientId: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID
};
};
@@ -63,7 +63,7 @@ export const getAzureDevopsConnection = async (
switch (appConnection.method) {
case AzureDevOpsConnectionMethod.OAuth:
const appCfg = getConfig();
- if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
+ if (!appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET) {
throw new BadRequestError({
message: `Azure environment variables have not been configured`
});
@@ -81,8 +81,8 @@ export const getAzureDevopsConnection = async (
new URLSearchParams({
grant_type: "refresh_token",
scope: `https://app.vssps.visualstudio.com/.default`,
- client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ client_id: appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID,
+ client_secret: appCfg.INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET,
refresh_token: refreshToken
})
);
@@ -119,7 +119,8 @@ export const getAzureDevopsConnection = async (
export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDevOpsConnectionConfig) => {
const { credentials: inputCredentials, method } = config;
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
+ const { INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID, INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET, SITE_URL } =
+ getConfig();
switch (method) {
case AzureDevOpsConnectionMethod.OAuth:
@@ -127,7 +128,7 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
}
- if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
+ if (!INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID || !INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET) {
throw new InternalServerError({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
@@ -144,8 +145,8 @@ export const validateAzureDevOpsConnectionCredentials = async (config: TAzureDev
grant_type: "authorization_code",
code: oauthCredentials.code,
scope: `https://app.vssps.visualstudio.com/.default`,
- client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ client_id: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID,
+ client_secret: INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
})
);
diff --git a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts
index 116597ec4..95102c5d1 100644
--- a/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts
+++ b/backend/src/services/app-connection/azure-key-vault/azure-key-vault-connection-fns.ts
@@ -26,7 +26,10 @@ export const getAzureConnectionAccessToken = async (
kmsService: Pick
) => {
const appCfg = getConfig();
- if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
+ if (
+ !appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID ||
+ !appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET
+ ) {
throw new BadRequestError({
message: `Azure environment variables have not been configured`
});
@@ -57,8 +60,8 @@ export const getAzureConnectionAccessToken = async (
new URLSearchParams({
grant_type: "refresh_token",
scope: `openid offline_access`,
- client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ client_id: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
+ client_secret: appCfg.INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
refresh_token: credentials.refreshToken
})
);
@@ -92,22 +95,23 @@ export const getAzureConnectionAccessToken = async (
};
export const getAzureKeyVaultConnectionListItem = () => {
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
+ const { INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID } = getConfig();
return {
name: "Azure Key Vault" as const,
app: AppConnection.AzureKeyVault as const,
methods: Object.values(AzureKeyVaultConnectionMethod) as [AzureKeyVaultConnectionMethod.OAuth],
- oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
+ oauthClientId: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID
};
};
export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureKeyVaultConnectionConfig) => {
const { credentials: inputCredentials, method } = config;
- const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
+ const { INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID, INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET, SITE_URL } =
+ getConfig();
- if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
+ if (!INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID || !INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET) {
throw new InternalServerError({
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
});
@@ -123,8 +127,8 @@ export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureK
grant_type: "authorization_code",
code: inputCredentials.code,
scope: `openid offline_access https://vault.azure.net/.default`,
- client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
- client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
+ client_id: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_ID,
+ client_secret: INF_APP_CONNECTION_AZURE_KEY_VAULT_CLIENT_SECRET,
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
})
);
diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts
index 360923e19..57d01be29 100644
--- a/backend/src/services/app-connection/github/github-connection-fns.ts
+++ b/backend/src/services/app-connection/github/github-connection-fns.ts
@@ -1,10 +1,16 @@
import { createAppAuth } from "@octokit/auth-app";
-import { Octokit } from "@octokit/rest";
-import { AxiosResponse } from "axios";
+import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
+import https from "https";
+import RE2 from "re2";
+import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
+import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { getConfig } from "@app/lib/config/env";
-import { request } from "@app/lib/config/request";
+import { request as httpRequest } from "@app/lib/config/request";
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
+import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
+import { logger } from "@app/lib/logger";
+import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
@@ -24,123 +30,224 @@ export const getGitHubConnectionListItem = () => {
};
};
-export const getGitHubClient = (appConnection: TGitHubConnection) => {
+export const requestWithGitHubGateway = async (
+ appConnection: { gatewayId?: string | null },
+ gatewayService: Pick,
+ requestConfig: AxiosRequestConfig
+): Promise> => {
+ const { gatewayId } = appConnection;
+
+ // If gateway isn't set up, don't proxy request
+ if (!gatewayId) {
+ return httpRequest.request(requestConfig);
+ }
+
+ const url = new URL(requestConfig.url as string);
+
+ await blockLocalAndPrivateIpAddresses(url.toString());
+
+ const [targetHost] = await verifyHostInputValidity(url.host, true);
+ const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId);
+ const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
+
+ return withGatewayProxy(
+ async (proxyPort) => {
+ const httpsAgent = new https.Agent({
+ servername: targetHost
+ });
+
+ url.protocol = "https:";
+ url.host = `localhost:${proxyPort}`;
+
+ const finalRequestConfig: AxiosRequestConfig = {
+ ...requestConfig,
+ url: url.toString(),
+ httpsAgent,
+ headers: {
+ ...requestConfig.headers,
+ Host: targetHost
+ }
+ };
+
+ try {
+ return await httpRequest.request(finalRequestConfig);
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ logger.error("Error during GitHub gateway request:", axiosError.message, axiosError.response?.data);
+ throw error;
+ }
+ },
+ {
+ protocol: GatewayProxyProtocol.Tcp,
+ targetHost,
+ targetPort: 443,
+ relayHost,
+ relayPort: Number(relayPort),
+ identityId: relayDetails.identityId,
+ orgId: relayDetails.orgId,
+ tlsOptions: {
+ ca: relayDetails.certChain,
+ cert: relayDetails.certificate,
+ key: relayDetails.privateKey.toString()
+ }
+ }
+ );
+};
+
+export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) => {
const appCfg = getConfig();
-
- const { method, credentials } = appConnection;
-
- let client: Octokit;
-
const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
- switch (method) {
- case GitHubConnectionMethod.App:
- if (!appId || !appPrivateKey) {
- throw new InternalServerError({
- message: `GitHub ${getAppConnectionMethodName(method).replace("GitHub", "")} has not been configured`
- });
- }
-
- client = new Octokit({
- authStrategy: createAppAuth,
- auth: {
- appId,
- privateKey: appPrivateKey,
- installationId: credentials.installationId
- }
- });
- break;
- case GitHubConnectionMethod.OAuth:
- client = new Octokit({
- auth: credentials.accessToken
- });
- break;
- default:
- throw new InternalServerError({
- message: `Unhandled GitHub connection method: ${method as GitHubConnectionMethod}`
- });
+ if (!appId || !appPrivateKey) {
+ throw new InternalServerError({
+ message: `GitHub App keys are not configured.`
+ });
}
- return client;
+ if (appConnection.method !== GitHubConnectionMethod.App) {
+ throw new InternalServerError({ message: "Cannot generate GitHub App token for non-app connection" });
+ }
+
+ const appAuth = createAppAuth({
+ appId,
+ privateKey: appPrivateKey,
+ installationId: appConnection.credentials.installationId
+ });
+
+ const { token } = await appAuth({ type: "installation" });
+ return token;
+};
+
+function extractNextPageUrl(linkHeader: string | undefined): string | null {
+ if (!linkHeader) return null;
+
+ const links = linkHeader.split(",");
+ const nextLink = links.find((link) => link.includes('rel="next"'));
+
+ if (!nextLink) return null;
+
+ const match = new RE2(/<([^>]+)>/).exec(nextLink);
+ return match ? match[1] : null;
+}
+
+export const makePaginatedGitHubRequest = async (
+ appConnection: TGitHubConnection,
+ gatewayService: Pick,
+ path: string,
+ dataMapper?: (data: R) => T[]
+): Promise => {
+ const { credentials, method } = appConnection;
+
+ const token =
+ method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection);
+ let url: string | null = `https://api.${credentials.host || "github.com"}${path}`;
+ let results: T[] = [];
+ let i = 0;
+
+ while (url && i < 1000) {
+ // eslint-disable-next-line no-await-in-loop
+ const response: AxiosResponse = await requestWithGitHubGateway(appConnection, gatewayService, {
+ url,
+ method: "GET",
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `Bearer ${token}`,
+ "X-GitHub-Api-Version": "2022-11-28"
+ }
+ });
+
+ const items = dataMapper ? dataMapper(response.data) : (response.data as unknown as T[]);
+ results = results.concat(items);
+
+ url = extractNextPageUrl(response.headers.link as string | undefined);
+ i += 1;
+ }
+
+ return results;
};
type GitHubOrganization = {
login: string;
id: number;
+ type: string;
};
type GitHubRepository = {
id: number;
name: string;
owner: GitHubOrganization;
+ permissions?: {
+ admin: boolean;
+ maintain: boolean;
+ push: boolean;
+ triage: boolean;
+ pull: boolean;
+ };
};
-export const getGitHubRepositories = async (appConnection: TGitHubConnection) => {
- const client = getGitHubClient(appConnection);
+type GitHubEnvironment = {
+ id: number;
+ name: string;
+};
- let repositories: GitHubRepository[];
-
- switch (appConnection.method) {
- case GitHubConnectionMethod.App:
- repositories = await client.paginate("GET /installation/repositories");
- break;
- case GitHubConnectionMethod.OAuth:
- default:
- repositories = (await client.paginate("GET /user/repos")).filter((repo) => repo.permissions?.admin);
- break;
+export const getGitHubRepositories = async (
+ appConnection: TGitHubConnection,
+ gatewayService: Pick
+) => {
+ if (appConnection.method === GitHubConnectionMethod.App) {
+ return makePaginatedGitHubRequest(
+ appConnection,
+ gatewayService,
+ "/installation/repositories",
+ (data) => data.repositories
+ );
}
- return repositories;
+ const repos = await makePaginatedGitHubRequest(appConnection, gatewayService, "/user/repos");
+ return repos.filter((repo) => repo.permissions?.admin);
};
-export const getGitHubOrganizations = async (appConnection: TGitHubConnection) => {
- const client = getGitHubClient(appConnection);
+export const getGitHubOrganizations = async (
+ appConnection: TGitHubConnection,
+ gatewayService: Pick
+) => {
+ if (appConnection.method === GitHubConnectionMethod.App) {
+ const installationRepositories = await makePaginatedGitHubRequest<
+ GitHubRepository,
+ { repositories: GitHubRepository[] }
+ >(appConnection, gatewayService, "/installation/repositories", (data) => data.repositories);
- let organizations: GitHubOrganization[];
-
- switch (appConnection.method) {
- case GitHubConnectionMethod.App: {
- const installationRepositories = await client.paginate("GET /installation/repositories");
-
- const organizationMap: Record = {};
-
- installationRepositories.forEach((repo) => {
- if (repo.owner.type === "Organization") {
- organizationMap[repo.owner.id] = repo.owner;
- }
- });
-
- organizations = Object.values(organizationMap);
-
- break;
- }
- case GitHubConnectionMethod.OAuth:
- default:
- organizations = await client.paginate("GET /user/orgs");
- break;
- }
-
- return organizations;
-};
-
-export const getGitHubEnvironments = async (appConnection: TGitHubConnection, owner: string, repo: string) => {
- const client = getGitHubClient(appConnection);
-
- try {
- const environments = await client.paginate("GET /repos/{owner}/{repo}/environments", {
- owner,
- repo
+ const organizationMap: Record = {};
+ installationRepositories.forEach((repo) => {
+ if (repo.owner.type === "Organization") {
+ organizationMap[repo.owner.id] = repo.owner;
+ }
});
- return environments;
- } catch (e) {
- // repo doesn't have envs
- if ((e as { status: number }).status === 404) {
- return [];
- }
+ return Object.values(organizationMap);
+ }
- throw e;
+ return makePaginatedGitHubRequest(appConnection, gatewayService, "/user/orgs");
+};
+
+export const getGitHubEnvironments = async (
+ appConnection: TGitHubConnection,
+ gatewayService: Pick,
+ owner: string,
+ repo: string
+) => {
+ try {
+ return await makePaginatedGitHubRequest(
+ appConnection,
+ gatewayService,
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/environments`,
+ (data) => data.environments
+ );
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ if (axiosError.response?.status === 404) return [];
+ throw error;
}
};
@@ -159,9 +266,11 @@ export function isGithubErrorResponse(data: GithubTokenRespData): data is Github
return "error" in data;
}
-export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
+export const validateGitHubConnectionCredentials = async (
+ config: TGitHubConnectionConfig,
+ gatewayService: Pick
+) => {
const { credentials, method } = config;
-
const {
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
@@ -192,10 +301,13 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
}
let tokenResp: AxiosResponse;
+ const host = credentials.host || "github.com";
try {
- tokenResp = await request.get("https://github.com/login/oauth/access_token", {
- params: {
+ tokenResp = await requestWithGitHubGateway(config, gatewayService, {
+ url: `https://${host}/login/oauth/access_token`,
+ method: "POST",
+ data: {
client_id: clientId,
client_secret: clientSecret,
code: credentials.code,
@@ -203,7 +315,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
},
headers: {
Accept: "application/json",
- "Accept-Encoding": "application/json"
+ "Content-Type": "application/json"
}
});
@@ -233,7 +345,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
- const installationsResp = await request.get<{
+ const installationsResp = await requestWithGitHubGateway<{
installations: {
id: number;
account: {
@@ -242,7 +354,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
id: number;
};
}[];
- }>(IntegrationUrls.GITHUB_USER_INSTALLATIONS, {
+ }>(config, gatewayService, {
+ url: IntegrationUrls.GITHUB_USER_INSTALLATIONS.replace("api.github.com", `api.${host}`),
headers: {
Accept: "application/json",
Authorization: `Bearer ${tokenResp.data.access_token}`,
diff --git a/backend/src/services/app-connection/github/github-connection-schemas.ts b/backend/src/services/app-connection/github/github-connection-schemas.ts
index e98b9169d..bf92ec155 100644
--- a/backend/src/services/app-connection/github/github-connection-schemas.ts
+++ b/backend/src/services/app-connection/github/github-connection-schemas.ts
@@ -11,20 +11,24 @@ import {
import { GitHubConnectionMethod } from "./github-connection-enums";
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
- code: z.string().trim().min(1, "OAuth code required")
+ code: z.string().trim().min(1, "OAuth code required"),
+ host: z.string().trim().optional()
});
export const GitHubConnectionAppInputCredentialsSchema = z.object({
code: z.string().trim().min(1, "GitHub App code required"),
- installationId: z.string().min(1, "GitHub App Installation ID required")
+ installationId: z.string().min(1, "GitHub App Installation ID required"),
+ host: z.string().trim().optional()
});
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({
- accessToken: z.string()
+ accessToken: z.string(),
+ host: z.string().trim().optional()
});
export const GitHubConnectionAppOutputCredentialsSchema = z.object({
- installationId: z.string()
+ installationId: z.string(),
+ host: z.string().trim().optional()
});
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
@@ -43,7 +47,9 @@ export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("m
]);
export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsSchema.and(
- GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub)
+ GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub, {
+ supportsGateways: true
+ })
);
export const UpdateGitHubConnectionSchema = z
@@ -53,7 +59,11 @@ export const UpdateGitHubConnectionSchema = z
.optional()
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
})
- .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub));
+ .and(
+ GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub, {
+ supportsGateways: true
+ })
+ );
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
diff --git a/backend/src/services/app-connection/github/github-connection-service.ts b/backend/src/services/app-connection/github/github-connection-service.ts
index b4e95c5a7..f1198ddfa 100644
--- a/backend/src/services/app-connection/github/github-connection-service.ts
+++ b/backend/src/services/app-connection/github/github-connection-service.ts
@@ -1,3 +1,4 @@
+import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
@@ -19,11 +20,14 @@ type TListGitHubEnvironmentsDTO = {
owner: string;
};
-export const githubConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
+export const githubConnectionService = (
+ getAppConnection: TGetAppConnectionFunc,
+ gatewayService: Pick
+) => {
const listRepositories = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
- const repositories = await getGitHubRepositories(appConnection);
+ const repositories = await getGitHubRepositories(appConnection, gatewayService);
return repositories;
};
@@ -31,7 +35,7 @@ export const githubConnectionService = (getAppConnection: TGetAppConnectionFunc)
const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
- const organizations = await getGitHubOrganizations(appConnection);
+ const organizations = await getGitHubOrganizations(appConnection, gatewayService);
return organizations;
};
@@ -42,7 +46,7 @@ export const githubConnectionService = (getAppConnection: TGetAppConnectionFunc)
) => {
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
- const environments = await getGitHubEnvironments(appConnection, owner, repo);
+ const environments = await getGitHubEnvironments(appConnection, gatewayService, owner, repo);
return environments;
};
diff --git a/backend/src/services/app-connection/github/github-connection-types.ts b/backend/src/services/app-connection/github/github-connection-types.ts
index 600506277..c4aed54b5 100644
--- a/backend/src/services/app-connection/github/github-connection-types.ts
+++ b/backend/src/services/app-connection/github/github-connection-types.ts
@@ -17,4 +17,7 @@ export type TGitHubConnectionInput = z.infer;
+export type TGitHubConnectionConfig = DiscriminativePick<
+ TGitHubConnectionInput,
+ "method" | "app" | "credentials" | "gatewayId"
+>;
diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts
index d9adc91dd..4c671986f 100644
--- a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts
+++ b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts
@@ -164,7 +164,7 @@ export const validateSqlConnectionCredentials = async (
) => {
try {
await executeWithPotentialGateway(config, gatewayService, async (client) => {
- await client.raw(`Select 1`);
+ await client.raw(config.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`);
});
return config.credentials;
} catch (error) {
diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns/envkey.ts
similarity index 61%
rename from backend/src/services/external-migration/external-migration-fns.ts
rename to backend/src/services/external-migration/external-migration-fns/envkey.ts
index 8af22d858..ffdf80e9c 100644
--- a/backend/src/services/external-migration/external-migration-fns.ts
+++ b/backend/src/services/external-migration/external-migration-fns/envkey.ts
@@ -1,32 +1,26 @@
-import slugify from "@sindresorhus/slugify";
import sjcl from "sjcl";
import tweetnacl from "tweetnacl";
import tweetnaclUtil from "tweetnacl-util";
-import { SecretType, TSecretFolders } from "@app/db/schemas";
import { crypto } from "@app/lib/crypto/cryptography";
-import { BadRequestError, NotFoundError } from "@app/lib/errors";
-import { chunkArray } from "@app/lib/fn";
+import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
-import { alphaNumericNanoId } from "@app/lib/nanoid";
-import { CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
-import { TKmsServiceFactory } from "../kms/kms-service";
-import { KmsDataKey } from "../kms/kms-types";
-import { TProjectDALFactory } from "../project/project-dal";
-import { TProjectServiceFactory } from "../project/project-service";
-import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
-import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
-import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
-import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
-import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
-import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
-import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
-import { fnSecretBulkInsert, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
-import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
-import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
-import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
-import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "./external-migration-types";
+import { TFolderCommitServiceFactory } from "../../folder-commit/folder-commit-service";
+import { TKmsServiceFactory } from "../../kms/kms-service";
+import { TProjectDALFactory } from "../../project/project-dal";
+import { TProjectServiceFactory } from "../../project/project-service";
+import { TProjectEnvDALFactory } from "../../project-env/project-env-dal";
+import { TProjectEnvServiceFactory } from "../../project-env/project-env-service";
+import { TResourceMetadataDALFactory } from "../../resource-metadata/resource-metadata-dal";
+import { TSecretFolderDALFactory } from "../../secret-folder/secret-folder-dal";
+import { TSecretFolderVersionDALFactory } from "../../secret-folder/secret-folder-version-dal";
+import { TSecretTagDALFactory } from "../../secret-tag/secret-tag-dal";
+import { TSecretV2BridgeDALFactory } from "../../secret-v2-bridge/secret-v2-bridge-dal";
+import type { TSecretV2BridgeServiceFactory } from "../../secret-v2-bridge/secret-v2-bridge-service";
+import { TSecretVersionV2DALFactory } from "../../secret-v2-bridge/secret-version-dal";
+import { TSecretVersionV2TagDALFactory } from "../../secret-v2-bridge/secret-version-tag-dal";
+import { InfisicalImportData, TEnvKeyExportJSON, TImportInfisicalDataCreate } from "../external-migration-types";
export type TImportDataIntoInfisicalDTO = {
projectDAL: Pick;
@@ -499,326 +493,3 @@ export const parseEnvKeyDataFn = async (decryptedJson: string): Promise {
- // Import data to infisical
- if (!data || !data.projects) {
- throw new BadRequestError({ message: "No projects found in data" });
- }
-
- const originalToNewProjectId = new Map();
- const originalToNewEnvironmentId = new Map<
- string,
- { envId: string; envSlug: string; rootFolderId: string; projectId: string }
- >();
- const originalToNewFolderId = new Map<
- string,
- {
- folderId: string;
- projectId: string;
- }
- >();
- const projectsNotImported: string[] = [];
-
- await projectDAL.transaction(async (tx) => {
- for await (const project of data.projects) {
- const newProject = await projectService
- .createProject({
- actor,
- actorId,
- actorOrgId,
- actorAuthMethod,
- workspaceName: project.name,
- createDefaultEnvs: false,
- tx
- })
- .catch((e) => {
- logger.error(e, `Failed to import to project [name:${project.name}]`);
- throw new BadRequestError({ message: `Failed to import to project [name:${project.name}]` });
- });
- originalToNewProjectId.set(project.id, newProject.id);
- }
-
- // Import environments
- if (data.environments) {
- for await (const environment of data.environments) {
- const projectId = originalToNewProjectId.get(environment.projectId);
- const slug = slugify(`${environment.name}-${alphaNumericNanoId(4)}`);
-
- if (!projectId) {
- projectsNotImported.push(environment.projectId);
- // eslint-disable-next-line no-continue
- continue;
- }
-
- const existingEnv = await projectEnvDAL.findOne({ projectId, slug }, tx);
-
- if (existingEnv) {
- throw new BadRequestError({
- message: `Environment with slug '${slug}' already exist`,
- name: "CreateEnvironment"
- });
- }
-
- const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
- const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx);
- const folder = await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
-
- originalToNewEnvironmentId.set(environment.id, {
- envSlug: doc.slug,
- envId: doc.id,
- rootFolderId: folder.id,
- projectId
- });
- }
- }
-
- if (data.folders) {
- for await (const folder of data.folders) {
- const parentEnv = originalToNewEnvironmentId.get(folder.parentFolderId as string);
-
- if (!parentEnv) {
- // eslint-disable-next-line no-continue
- continue;
- }
-
- const newFolder = await folderDAL.create(
- {
- name: folder.name,
- envId: parentEnv.envId,
- parentId: parentEnv.rootFolderId
- },
- tx
- );
-
- const newFolderVersion = await folderVersionDAL.create(
- {
- name: newFolder.name,
- envId: newFolder.envId,
- version: newFolder.version,
- folderId: newFolder.id
- },
- tx
- );
-
- await folderCommitService.createCommit(
- {
- actor: {
- type: actor,
- metadata: {
- id: actorId
- }
- },
- message: "Changed by external migration",
- folderId: parentEnv.rootFolderId,
- changes: [
- {
- type: CommitType.ADD,
- folderVersionId: newFolderVersion.id
- }
- ]
- },
- tx
- );
-
- originalToNewFolderId.set(folder.id, {
- folderId: newFolder.id,
- projectId: parentEnv.projectId
- });
- }
- }
-
- // Useful for debugging:
- // console.log("data.secrets", data.secrets);
- // console.log("data.folders", data.folders);
- // console.log("data.environment", data.environments);
-
- if (data.secrets && data.secrets.length > 0) {
- const mappedToEnvironmentId = new Map<
- string,
- {
- secretKey: string;
- secretValue: string;
- folderId?: string;
- isFromBlock?: boolean;
- }[]
- >();
-
- for (const secret of data.secrets) {
- const targetId = secret.folderId || secret.environmentId;
-
- // Skip if we can't find either an environment or folder mapping for this secret
- if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) {
- logger.info({ secret }, "[importDataIntoInfisicalFn]: Could not find environment or folder for secret");
-
- // eslint-disable-next-line no-continue
- continue;
- }
-
- if (!mappedToEnvironmentId.has(targetId)) {
- mappedToEnvironmentId.set(targetId, []);
- }
-
- const alreadyHasSecret = mappedToEnvironmentId
- .get(targetId)!
- .find((el) => el.secretKey === secret.name && el.folderId === secret.folderId);
-
- if (alreadyHasSecret && alreadyHasSecret.isFromBlock) {
- // remove the existing secret if any
- mappedToEnvironmentId
- .get(targetId)!
- .splice(mappedToEnvironmentId.get(targetId)!.indexOf(alreadyHasSecret), 1);
- }
- mappedToEnvironmentId.get(targetId)!.push({
- secretKey: secret.name,
- secretValue: secret.value || "",
- folderId: secret.folderId,
- isFromBlock: secret.appBlockOrderIndex !== undefined
- });
- }
-
- // for each of the mappedEnvironmentId
- for await (const [targetId, secrets] of mappedToEnvironmentId) {
- logger.info("[importDataIntoInfisicalFn]: Processing secrets for targetId", targetId);
-
- let selectedFolder: TSecretFolders | undefined;
- let selectedProjectId: string | undefined;
-
- // Case 1: Secret belongs to a folder / branch / branch of a block
- const foundFolder = originalToNewFolderId.get(targetId);
- if (foundFolder) {
- logger.info("[importDataIntoInfisicalFn]: Processing secrets for folder");
- selectedFolder = await folderDAL.findById(foundFolder.folderId, tx);
- selectedProjectId = foundFolder.projectId;
- } else {
- logger.info("[importDataIntoInfisicalFn]: Processing secrets for normal environment");
- const environment = data.environments.find((env) => env.id === targetId);
- if (!environment) {
- logger.info(
- {
- targetId
- },
- "[importDataIntoInfisicalFn]: Could not find environment for secret"
- );
- // eslint-disable-next-line no-continue
- continue;
- }
-
- const projectId = originalToNewProjectId.get(environment.projectId)!;
-
- if (!projectId) {
- throw new BadRequestError({ message: `Failed to import secret, project not found` });
- }
-
- const env = originalToNewEnvironmentId.get(targetId);
- if (!env) {
- logger.info(
- {
- targetId
- },
- "[importDataIntoInfisicalFn]: Could not find environment for secret"
- );
-
- // eslint-disable-next-line no-continue
- continue;
- }
-
- const folder = await folderDAL.findBySecretPath(projectId, env.envSlug, "/", tx);
-
- if (!folder) {
- throw new NotFoundError({
- message: `Folder not found for the given environment slug (${env.envSlug}) & secret path (/)`,
- name: "Create secret"
- });
- }
-
- selectedFolder = folder;
- selectedProjectId = projectId;
- }
-
- if (!selectedFolder) {
- throw new NotFoundError({
- message: `Folder not found for the given environment slug & secret path`,
- name: "CreateSecret"
- });
- }
-
- if (!selectedProjectId) {
- throw new NotFoundError({
- message: `Project not found for the given environment slug & secret path`,
- name: "CreateSecret"
- });
- }
-
- const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey(
- {
- type: KmsDataKey.SecretManager,
- projectId: selectedProjectId
- },
- tx
- );
-
- const secretBatches = chunkArray(secrets, 2500);
- for await (const secretBatch of secretBatches) {
- const secretsByKeys = await secretDAL.findBySecretKeys(
- selectedFolder.id,
- secretBatch.map((el) => ({
- key: el.secretKey,
- type: SecretType.Shared
- })),
- tx
- );
- if (secretsByKeys.length) {
- throw new BadRequestError({
- message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}`
- });
- }
- await fnSecretBulkInsert({
- inputSecrets: secretBatch.map((el) => {
- const references = getAllSecretReferences(el.secretValue).nestedReferences;
-
- return {
- version: 1,
- encryptedValue: el.secretValue
- ? secretManagerEncrypt({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob
- : undefined,
- key: el.secretKey,
- references,
- type: SecretType.Shared
- };
- }),
- folderId: selectedFolder.id,
- orgId: actorOrgId,
- resourceMetadataDAL,
- secretDAL,
- secretVersionDAL,
- secretTagDAL,
- secretVersionTagDAL,
- folderCommitService,
- actor: {
- type: actor,
- actorId
- },
- tx
- });
- }
- }
- }
- });
-
- return { projectsNotImported };
-};
diff --git a/backend/src/services/external-migration/external-migration-fns/import.ts b/backend/src/services/external-migration/external-migration-fns/import.ts
new file mode 100644
index 000000000..5728bf1c0
--- /dev/null
+++ b/backend/src/services/external-migration/external-migration-fns/import.ts
@@ -0,0 +1,352 @@
+import slugify from "@sindresorhus/slugify";
+
+import { SecretType, TSecretFolders } from "@app/db/schemas";
+import { BadRequestError, NotFoundError } from "@app/lib/errors";
+import { chunkArray } from "@app/lib/fn";
+import { logger } from "@app/lib/logger";
+import { alphaNumericNanoId } from "@app/lib/nanoid";
+import { CommitType } from "@app/services/folder-commit/folder-commit-service";
+import { KmsDataKey } from "@app/services/kms/kms-types";
+import { fnSecretBulkInsert, getAllSecretReferences } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
+
+import { TImportDataIntoInfisicalDTO } from "./envkey";
+
+export const importDataIntoInfisicalFn = async ({
+ projectService,
+ projectEnvDAL,
+ projectDAL,
+ secretDAL,
+ kmsService,
+ secretVersionDAL,
+ secretTagDAL,
+ secretVersionTagDAL,
+ folderDAL,
+ resourceMetadataDAL,
+ folderVersionDAL,
+ folderCommitService,
+ input: { data, actor, actorId, actorOrgId, actorAuthMethod }
+}: TImportDataIntoInfisicalDTO) => {
+ // Import data to infisical
+ if (!data || !data.projects) {
+ throw new BadRequestError({ message: "No projects found in data" });
+ }
+
+ const originalToNewProjectId = new Map();
+ const originalToNewEnvironmentId = new Map<
+ string,
+ { envId: string; envSlug: string; rootFolderId?: string; projectId: string }
+ >();
+ const originalToNewFolderId = new Map<
+ string,
+ {
+ envId: string;
+ envSlug: string;
+ folderId: string;
+ projectId: string;
+ }
+ >();
+ const projectsNotImported: string[] = [];
+
+ await projectDAL.transaction(async (tx) => {
+ for await (const project of data.projects) {
+ const newProject = await projectService
+ .createProject({
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ workspaceName: project.name,
+ createDefaultEnvs: false,
+ tx
+ })
+ .catch((e) => {
+ logger.error(e, `Failed to import to project [name:${project.name}]`);
+ throw new BadRequestError({ message: `Failed to import to project [name:${project.name}]` });
+ });
+ originalToNewProjectId.set(project.id, newProject.id);
+ }
+
+ // Import environments
+ if (data.environments) {
+ for await (const environment of data.environments) {
+ const projectId = originalToNewProjectId.get(environment.projectId);
+ const slug = slugify(`${environment.name}-${alphaNumericNanoId(4)}`);
+
+ if (!projectId) {
+ projectsNotImported.push(environment.projectId);
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ const existingEnv = await projectEnvDAL.findOne({ projectId, slug }, tx);
+
+ if (existingEnv) {
+ throw new BadRequestError({
+ message: `Environment with slug '${slug}' already exist`,
+ name: "CreateEnvironment"
+ });
+ }
+
+ const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
+ const doc = await projectEnvDAL.create({ slug, name: environment.name, projectId, position: lastPos + 1 }, tx);
+ const folder = await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
+
+ originalToNewEnvironmentId.set(environment.id, {
+ envSlug: doc.slug,
+ envId: doc.id,
+ rootFolderId: folder.id,
+ projectId
+ });
+ }
+ }
+
+ if (data.folders) {
+ for await (const folder of data.folders) {
+ const parentEnv = originalToNewEnvironmentId.get(folder.parentFolderId as string);
+ const parentFolder = originalToNewFolderId.get(folder.parentFolderId as string);
+
+ let newFolder: TSecretFolders;
+
+ if (parentEnv?.rootFolderId) {
+ newFolder = await folderDAL.create(
+ {
+ name: folder.name,
+ envId: parentEnv.envId,
+ parentId: parentEnv.rootFolderId
+ },
+ tx
+ );
+ } else if (parentFolder) {
+ newFolder = await folderDAL.create(
+ {
+ name: folder.name,
+ envId: parentFolder.envId,
+ parentId: parentFolder.folderId
+ },
+ tx
+ );
+ } else {
+ logger.info({ folder }, "No parent environment found for folder");
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ const newFolderVersion = await folderVersionDAL.create(
+ {
+ name: newFolder.name,
+ envId: newFolder.envId,
+ version: newFolder.version,
+ folderId: newFolder.id
+ },
+ tx
+ );
+
+ await folderCommitService.createCommit(
+ {
+ actor: {
+ type: actor,
+ metadata: {
+ id: actorId
+ }
+ },
+ message: "Changed by external migration",
+ folderId: parentEnv?.rootFolderId || parentFolder?.folderId || "",
+ changes: [
+ {
+ type: CommitType.ADD,
+ folderVersionId: newFolderVersion.id
+ }
+ ]
+ },
+ tx
+ );
+
+ originalToNewFolderId.set(folder.id, {
+ folderId: newFolder.id,
+ envId: parentEnv?.envId || parentFolder?.envId || "",
+ envSlug: parentEnv?.envSlug || parentFolder?.envSlug || "",
+ projectId: parentEnv?.projectId || parentFolder?.projectId || ""
+ });
+ }
+ }
+
+ // Useful for debugging:
+ // console.log("data.secrets", data.secrets);
+ // console.log("data.folders", data.folders);
+ // console.log("data.environment", data.environments);
+
+ if (data.secrets && data.secrets.length > 0) {
+ const mappedToEnvironmentId = new Map<
+ string,
+ {
+ secretKey: string;
+ secretValue: string;
+ folderId?: string;
+ isFromBlock?: boolean;
+ }[]
+ >();
+
+ for (const secret of data.secrets) {
+ const targetId = secret.folderId || secret.environmentId;
+
+ // Skip if we can't find either an environment or folder mapping for this secret
+ if (!originalToNewEnvironmentId.get(secret.environmentId) && !originalToNewFolderId.get(targetId)) {
+ logger.info({ secret }, "[importDataIntoInfisicalFn]: Could not find environment or folder for secret");
+
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ if (!mappedToEnvironmentId.has(targetId)) {
+ mappedToEnvironmentId.set(targetId, []);
+ }
+
+ const alreadyHasSecret = mappedToEnvironmentId
+ .get(targetId)!
+ .find((el) => el.secretKey === secret.name && el.folderId === secret.folderId);
+
+ if (alreadyHasSecret && alreadyHasSecret.isFromBlock) {
+ // remove the existing secret if any
+ mappedToEnvironmentId
+ .get(targetId)!
+ .splice(mappedToEnvironmentId.get(targetId)!.indexOf(alreadyHasSecret), 1);
+ }
+ mappedToEnvironmentId.get(targetId)!.push({
+ secretKey: secret.name,
+ secretValue: secret.value || "",
+ folderId: secret.folderId,
+ isFromBlock: secret.appBlockOrderIndex !== undefined
+ });
+ }
+
+ // for each of the mappedEnvironmentId
+ for await (const [targetId, secrets] of mappedToEnvironmentId) {
+ logger.info("[importDataIntoInfisicalFn]: Processing secrets for targetId", targetId);
+
+ let selectedFolder: TSecretFolders | undefined;
+ let selectedProjectId: string | undefined;
+
+ // Case 1: Secret belongs to a folder / branch / branch of a block
+ const foundFolder = originalToNewFolderId.get(targetId);
+ if (foundFolder) {
+ logger.info("[importDataIntoInfisicalFn]: Processing secrets for folder");
+ selectedFolder = await folderDAL.findById(foundFolder.folderId, tx);
+ selectedProjectId = foundFolder.projectId;
+ } else {
+ logger.info("[importDataIntoInfisicalFn]: Processing secrets for normal environment");
+ const environment = data.environments.find((env) => env.id === targetId);
+ if (!environment) {
+ logger.info(
+ {
+ targetId
+ },
+ "[importDataIntoInfisicalFn]: Could not find environment for secret"
+ );
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ const projectId = originalToNewProjectId.get(environment.projectId)!;
+
+ if (!projectId) {
+ throw new BadRequestError({ message: `Failed to import secret, project not found` });
+ }
+
+ const env = originalToNewEnvironmentId.get(targetId);
+ if (!env) {
+ logger.info(
+ {
+ targetId
+ },
+ "[importDataIntoInfisicalFn]: Could not find environment for secret"
+ );
+
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ const folder = await folderDAL.findBySecretPath(projectId, env.envSlug, "/", tx);
+
+ if (!folder) {
+ throw new NotFoundError({
+ message: `Folder not found for the given environment slug (${env.envSlug}) & secret path (/)`,
+ name: "Create secret"
+ });
+ }
+
+ selectedFolder = folder;
+ selectedProjectId = projectId;
+ }
+
+ if (!selectedFolder) {
+ throw new NotFoundError({
+ message: `Folder not found for the given environment slug & secret path`,
+ name: "CreateSecret"
+ });
+ }
+
+ if (!selectedProjectId) {
+ throw new NotFoundError({
+ message: `Project not found for the given environment slug & secret path`,
+ name: "CreateSecret"
+ });
+ }
+
+ const { encryptor: secretManagerEncrypt } = await kmsService.createCipherPairWithDataKey(
+ {
+ type: KmsDataKey.SecretManager,
+ projectId: selectedProjectId
+ },
+ tx
+ );
+
+ const secretBatches = chunkArray(secrets, 2500);
+ for await (const secretBatch of secretBatches) {
+ const secretsByKeys = await secretDAL.findBySecretKeys(
+ selectedFolder.id,
+ secretBatch.map((el) => ({
+ key: el.secretKey,
+ type: SecretType.Shared
+ })),
+ tx
+ );
+ if (secretsByKeys.length) {
+ throw new BadRequestError({
+ message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}`
+ });
+ }
+ await fnSecretBulkInsert({
+ inputSecrets: secretBatch.map((el) => {
+ const references = getAllSecretReferences(el.secretValue).nestedReferences;
+
+ return {
+ version: 1,
+ encryptedValue: el.secretValue
+ ? secretManagerEncrypt({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob
+ : undefined,
+ key: el.secretKey,
+ references,
+ type: SecretType.Shared
+ };
+ }),
+ folderId: selectedFolder.id,
+ orgId: actorOrgId,
+ resourceMetadataDAL,
+ secretDAL,
+ secretVersionDAL,
+ secretTagDAL,
+ secretVersionTagDAL,
+ folderCommitService,
+ actor: {
+ type: actor,
+ actorId
+ },
+ tx
+ });
+ }
+ }
+ }
+ });
+
+ return { projectsNotImported };
+};
diff --git a/backend/src/services/external-migration/external-migration-fns/index.ts b/backend/src/services/external-migration/external-migration-fns/index.ts
new file mode 100644
index 000000000..4af82bf22
--- /dev/null
+++ b/backend/src/services/external-migration/external-migration-fns/index.ts
@@ -0,0 +1,3 @@
+export * from "./envkey";
+export * from "./import";
+export * from "./vault";
diff --git a/backend/src/services/external-migration/external-migration-fns/vault.ts b/backend/src/services/external-migration/external-migration-fns/vault.ts
new file mode 100644
index 000000000..197102f82
--- /dev/null
+++ b/backend/src/services/external-migration/external-migration-fns/vault.ts
@@ -0,0 +1,341 @@
+import axios, { AxiosInstance } from "axios";
+import { v4 as uuidv4 } from "uuid";
+
+import { BadRequestError } from "@app/lib/errors";
+import { logger } from "@app/lib/logger";
+import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
+
+import { InfisicalImportData, VaultMappingType } from "../external-migration-types";
+
+type VaultData = {
+ namespace: string;
+ mount: string;
+ path: string;
+ secretData: Record;
+};
+
+const vaultFactory = () => {
+ const getMounts = async (request: AxiosInstance) => {
+ const response = await request
+ .get<
+ Record<
+ string,
+ {
+ accessor: string;
+ options: {
+ version?: string;
+ } | null;
+ type: string;
+ }
+ >
+ >("/v1/sys/mounts")
+ .catch((err) => {
+ if (axios.isAxiosError(err)) {
+ logger.error(err.response?.data, "External migration: Failed to get Vault mounts");
+ }
+ throw err;
+ });
+ return response.data;
+ };
+
+ const getPaths = async (
+ request: AxiosInstance,
+ { mountPath, secretPath = "" }: { mountPath: string; secretPath?: string }
+ ) => {
+ try {
+ // For KV v2: /v1/{mount}/metadata/{path}?list=true
+ const path = secretPath ? `${mountPath}/metadata/${secretPath}` : `${mountPath}/metadata`;
+ const response = await request.get<{
+ data: {
+ keys: string[];
+ };
+ }>(`/v1/${path}?list=true`);
+
+ return response.data.data.keys;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ logger.error(err.response?.data, "External migration: Failed to get Vault paths");
+ if (err.response?.status === 404) {
+ return null;
+ }
+ }
+ throw err;
+ }
+ };
+
+ const getSecrets = async (
+ request: AxiosInstance,
+ { mountPath, secretPath }: { mountPath: string; secretPath: string }
+ ) => {
+ // For KV v2: /v1/{mount}/data/{path}
+ const response = await request
+ .get<{
+ data: {
+ data: Record; // KV v2 has nested data structure
+ metadata: {
+ created_time: string;
+ deletion_time: string;
+ destroyed: boolean;
+ version: number;
+ };
+ };
+ }>(`/v1/${mountPath}/data/${secretPath}`)
+ .catch((err) => {
+ if (axios.isAxiosError(err)) {
+ logger.error(err.response?.data, "External migration: Failed to get Vault secret");
+ }
+ throw err;
+ });
+
+ return response.data.data.data;
+ };
+
+ // helper function to check if a mount is KV v2 (will be useful if we add support for Vault KV v1)
+ // const isKvV2Mount = (mountInfo: { type: string; options?: { version?: string } | null }) => {
+ // return mountInfo.type === "kv" && mountInfo.options?.version === "2";
+ // };
+
+ const recursivelyGetAllPaths = async (
+ request: AxiosInstance,
+ mountPath: string,
+ currentPath: string = ""
+ ): Promise => {
+ const paths = await getPaths(request, { mountPath, secretPath: currentPath });
+
+ if (paths === null || paths.length === 0) {
+ return [];
+ }
+
+ const allSecrets: string[] = [];
+
+ for await (const path of paths) {
+ const cleanPath = path.endsWith("/") ? path.slice(0, -1) : path;
+ const fullItemPath = currentPath ? `${currentPath}/${cleanPath}` : cleanPath;
+
+ if (path.endsWith("/")) {
+ // it's a folder so we recurse into it
+ const subSecrets = await recursivelyGetAllPaths(request, mountPath, fullItemPath);
+ allSecrets.push(...subSecrets);
+ } else {
+ // it's a secret so we add it to our results
+ allSecrets.push(`${mountPath}/${fullItemPath}`);
+ }
+ }
+
+ return allSecrets;
+ };
+
+ async function collectVaultData({
+ baseUrl,
+ namespace,
+ accessToken
+ }: {
+ baseUrl: string;
+ namespace?: string;
+ accessToken: string;
+ }): Promise {
+ const request = axios.create({
+ baseURL: baseUrl,
+ headers: {
+ "X-Vault-Token": accessToken,
+ ...(namespace ? { "X-Vault-Namespace": namespace } : {})
+ }
+ });
+
+ const allData: VaultData[] = [];
+
+ // Get all mounts in this namespace
+ const mounts = await getMounts(request);
+
+ for (const mount of Object.keys(mounts)) {
+ if (!mount.endsWith("/")) {
+ delete mounts[mount];
+ }
+ }
+
+ for await (const [mountPath, mountInfo] of Object.entries(mounts)) {
+ // skip non-KV mounts
+ if (!mountInfo.type.startsWith("kv")) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ // get all paths in this mount
+ const paths = await recursivelyGetAllPaths(request, `${mountPath.replace(/\/$/, "")}`);
+
+ const cleanMountPath = mountPath.replace(/\/$/, "");
+
+ for await (const secretPath of paths) {
+ // get the actual secret data
+ const secretData = await getSecrets(request, {
+ mountPath: cleanMountPath,
+ secretPath: secretPath.replace(`${cleanMountPath}/`, "")
+ });
+
+ allData.push({
+ namespace: namespace || "",
+ mount: mountPath.replace(/\/$/, ""),
+ path: secretPath.replace(`${cleanMountPath}/`, ""),
+ secretData
+ });
+ }
+ }
+
+ return allData;
+ }
+
+ return {
+ collectVaultData,
+ getMounts,
+ getPaths,
+ getSecrets,
+ recursivelyGetAllPaths
+ };
+};
+
+export const transformToInfisicalFormatNamespaceToProjects = (
+ vaultData: VaultData[],
+ mappingType: VaultMappingType
+): InfisicalImportData => {
+ const projects: Array<{ name: string; id: string }> = [];
+ const environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }> = [];
+ const folders: Array<{ id: string; name: string; environmentId: string; parentFolderId?: string }> = [];
+ const secrets: Array<{ id: string; name: string; environmentId: string; value: string; folderId?: string }> = [];
+
+ // track created entities to avoid duplicates
+ const projectMap = new Map(); // namespace -> projectId
+ const environmentMap = new Map(); // namespace:mount -> environmentId
+ const folderMap = new Map(); // namespace:mount:folderPath -> folderId
+
+ let environmentId: string = "";
+ for (const data of vaultData) {
+ const { namespace, mount, path, secretData } = data;
+
+ if (mappingType === VaultMappingType.Namespace) {
+ // create project (namespace)
+ if (!projectMap.has(namespace)) {
+ const projectId = uuidv4();
+ projectMap.set(namespace, projectId);
+ projects.push({
+ name: namespace,
+ id: projectId
+ });
+ }
+ const projectId = projectMap.get(namespace)!;
+
+ // create environment (mount)
+ const envKey = `${namespace}:${mount}`;
+ if (!environmentMap.has(envKey)) {
+ environmentId = uuidv4();
+ environmentMap.set(envKey, environmentId);
+ environments.push({
+ name: mount,
+ id: environmentId,
+ projectId
+ });
+ }
+ environmentId = environmentMap.get(envKey)!;
+ } else if (mappingType === VaultMappingType.KeyVault) {
+ if (!projectMap.has(mount)) {
+ const projectId = uuidv4();
+ projectMap.set(mount, projectId);
+ projects.push({
+ name: mount,
+ id: projectId
+ });
+ }
+ const projectId = projectMap.get(mount)!;
+
+ // create single "Production" environment per project, because we have no good way of determining environments from vault
+ if (!environmentMap.has(mount)) {
+ environmentId = uuidv4();
+ environmentMap.set(mount, environmentId);
+ environments.push({
+ name: "Production",
+ id: environmentId,
+ projectId
+ });
+ }
+ environmentId = environmentMap.get(mount)!;
+ }
+
+ // create folder structure
+ let currentFolderId: string | undefined;
+ let currentPath = "";
+
+ if (path.includes("/")) {
+ const pathParts = path.split("/").filter(Boolean);
+
+ const folderParts = pathParts;
+
+ // create nested folder structure for the entire path
+ for (const folderName of folderParts) {
+ currentPath = currentPath ? `${currentPath}/${folderName}` : folderName;
+ const folderKey = `${namespace}:${mount}:${currentPath}`;
+
+ if (!folderMap.has(folderKey)) {
+ const folderId = uuidv4();
+ folderMap.set(folderKey, folderId);
+ folders.push({
+ id: folderId,
+ name: folderName,
+ environmentId,
+ parentFolderId: currentFolderId || environmentId
+ });
+ currentFolderId = folderId;
+ } else {
+ currentFolderId = folderMap.get(folderKey)!;
+ }
+ }
+ }
+
+ for (const [key, value] of Object.entries(secretData)) {
+ secrets.push({
+ id: uuidv4(),
+ name: key,
+ environmentId,
+ value: String(value),
+ folderId: currentFolderId
+ });
+ }
+ }
+
+ return {
+ projects,
+ environments,
+ folders,
+ secrets
+ };
+};
+
+export const importVaultDataFn = async ({
+ vaultAccessToken,
+ vaultNamespace,
+ vaultUrl,
+ mappingType
+}: {
+ vaultAccessToken: string;
+ vaultNamespace?: string;
+ vaultUrl: string;
+ mappingType: VaultMappingType;
+}) => {
+ await blockLocalAndPrivateIpAddresses(vaultUrl);
+
+ if (mappingType === VaultMappingType.Namespace && !vaultNamespace) {
+ throw new BadRequestError({
+ message: "Vault namespace is required when project mapping type is set to namespace."
+ });
+ }
+
+ const vaultApi = vaultFactory();
+
+ const vaultData = await vaultApi.collectVaultData({
+ accessToken: vaultAccessToken,
+ baseUrl: vaultUrl,
+ namespace: vaultNamespace
+ });
+
+ const infisicalData = transformToInfisicalFormatNamespaceToProjects(vaultData, mappingType);
+
+ return infisicalData;
+};
diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts
index c4e6b43c5..8bde91aa5 100644
--- a/backend/src/services/external-migration/external-migration-queue.ts
+++ b/backend/src/services/external-migration/external-migration-queue.ts
@@ -19,7 +19,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { importDataIntoInfisicalFn } from "./external-migration-fns";
-import { ExternalPlatforms, TImportInfisicalDataCreate } from "./external-migration-types";
+import { ExternalPlatforms, ImportType, TImportInfisicalDataCreate } from "./external-migration-types";
export type TExternalMigrationQueueFactoryDep = {
smtpService: TSmtpService;
@@ -67,6 +67,7 @@ export const externalMigrationQueueFactory = ({
const startImport = async (dto: {
actorEmail: string;
data: {
+ importType: ImportType;
iv: string;
tag: string;
ciphertext: string;
diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts
index c310fd273..766b6cef4 100644
--- a/backend/src/services/external-migration/external-migration-service.ts
+++ b/backend/src/services/external-migration/external-migration-service.ts
@@ -4,9 +4,9 @@ import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { TUserDALFactory } from "../user/user-dal";
-import { decryptEnvKeyDataFn, parseEnvKeyDataFn } from "./external-migration-fns";
+import { decryptEnvKeyDataFn, importVaultDataFn, parseEnvKeyDataFn } from "./external-migration-fns";
import { TExternalMigrationQueueFactory } from "./external-migration-queue";
-import { TImportEnvKeyDataCreate } from "./external-migration-types";
+import { ImportType, TImportEnvKeyDataDTO, TImportVaultDataDTO } from "./external-migration-types";
type TExternalMigrationServiceFactoryDep = {
permissionService: TPermissionServiceFactory;
@@ -28,7 +28,7 @@ export const externalMigrationServiceFactory = ({
actorId,
actorOrgId,
actorAuthMethod
- }: TImportEnvKeyDataCreate) => {
+ }: TImportEnvKeyDataDTO) => {
if (crypto.isFipsModeEnabled()) {
throw new BadRequestError({ message: "EnvKey migration is not supported when running in FIPS mode." });
}
@@ -60,11 +60,65 @@ export const externalMigrationServiceFactory = ({
await externalMigrationQueue.startImport({
actorEmail: user.email!,
- data: encrypted
+ data: {
+ importType: ImportType.EnvKey,
+ ...encrypted
+ }
+ });
+ };
+
+ const importVaultData = async ({
+ vaultAccessToken,
+ vaultNamespace,
+ mappingType,
+ vaultUrl,
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod
+ }: TImportVaultDataDTO) => {
+ const { membership } = await permissionService.getOrgPermission(
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod,
+ actorOrgId
+ );
+
+ if (membership.role !== OrgMembershipRole.Admin) {
+ throw new ForbiddenRequestError({ message: "Only admins can import data" });
+ }
+
+ const user = await userDAL.findById(actorId);
+
+ const vaultData = await importVaultDataFn({
+ vaultAccessToken,
+ vaultNamespace,
+ vaultUrl,
+ mappingType
+ });
+
+ const stringifiedJson = JSON.stringify({
+ data: vaultData,
+ actor,
+ actorId,
+ actorOrgId,
+ actorAuthMethod
+ });
+
+ const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
+
+ await externalMigrationQueue.startImport({
+ actorEmail: user.email!,
+ data: {
+ importType: ImportType.Vault,
+ ...encrypted
+ }
});
};
return {
- importEnvKeyData
+ importEnvKeyData,
+ importVaultData
};
};
diff --git a/backend/src/services/external-migration/external-migration-types.ts b/backend/src/services/external-migration/external-migration-types.ts
index 32c70a688..89589d674 100644
--- a/backend/src/services/external-migration/external-migration-types.ts
+++ b/backend/src/services/external-migration/external-migration-types.ts
@@ -1,5 +1,17 @@
+import { TOrgPermission } from "@app/lib/types";
+
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
+export enum ImportType {
+ EnvKey = "envkey",
+ Vault = "vault"
+}
+
+export enum VaultMappingType {
+ Namespace = "namespace",
+ KeyVault = "key-vault"
+}
+
export type InfisicalImportData = {
projects: Array<{ name: string; id: string }>;
environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }>;
@@ -14,14 +26,17 @@ export type InfisicalImportData = {
}>;
};
-export type TImportEnvKeyDataCreate = {
+export type TImportEnvKeyDataDTO = {
decryptionKey: string;
encryptedJson: { nonce: string; data: string };
- actor: ActorType;
- actorId: string;
- actorOrgId: string;
- actorAuthMethod: ActorAuthMethod;
-};
+} & Omit;
+
+export type TImportVaultDataDTO = {
+ vaultAccessToken: string;
+ vaultNamespace?: string;
+ mappingType: VaultMappingType;
+ vaultUrl: string;
+} & Omit;
export type TImportInfisicalDataCreate = {
data: InfisicalImportData;
diff --git a/backend/src/services/identity-access-token/identity-access-token-types.ts b/backend/src/services/identity-access-token/identity-access-token-types.ts
index 87adfa5dc..e7b73a8c1 100644
--- a/backend/src/services/identity-access-token/identity-access-token-types.ts
+++ b/backend/src/services/identity-access-token/identity-access-token-types.ts
@@ -15,5 +15,16 @@ export type TIdentityAccessTokenJwtPayload = {
namespace: string;
name: string;
};
+ aws?: {
+ accountId: string;
+ arn: string;
+ userId: string;
+
+ // Derived from ARN
+ partition: string; // "aws", "aws-gov", "aws-cn"
+ service: string; // "iam", "sts"
+ resourceType: string; // "user" or "role"
+ resourceName: string;
+ };
};
};
diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts
index 517e9f613..d0fb4d323 100644
--- a/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts
+++ b/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts
@@ -1,67 +1,91 @@
+interface PrincipalArnEntity {
+ Partition: string;
+ Service: "iam" | "sts";
+ AccountNumber: string;
+ Type: "user" | "role" | "instance-profile";
+ Path: string;
+ FriendlyName: string;
+ SessionInfo: string; // Only populated for assumed-role
+}
+
+export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => {
+ // split the ARN into parts using ":" as the delimiter
+ const fullParts = arn.split(":");
+ if (fullParts.length !== 6) {
+ throw new Error(`Unrecognized ARN: "${arn}" contains ${fullParts.length} colon-separated parts, expected 6`);
+ }
+ const [prefix, partition, service, , accountNumber, resource] = fullParts;
+ if (prefix !== "arn") {
+ throw new Error(`Unrecognized ARN: "${arn}" does not begin with "arn:"`);
+ }
+
+ // validate the service is either 'iam' or 'sts'
+ if (service !== "iam" && service !== "sts") {
+ throw new Error(`Unrecognized service: "${service}" in ARN "${arn}", expected "iam" or "sts"`);
+ }
+
+ // parse the last part of the ARN which describes the resource
+ const parts = resource.split("/");
+ if (parts.length < 2) {
+ throw new Error(
+ `Unrecognized ARN: "${resource}" in ARN "${arn}" contains fewer than 2 slash-separated parts (expected type/name)`
+ );
+ }
+
+ const [rawType, ...rest] = parts;
+
+ let finalType: PrincipalArnEntity["Type"];
+ let friendlyName: string = parts[parts.length - 1];
+ let path: string = "";
+ let sessionInfo: string = "";
+
+ // handle different types of resources
+ switch (rawType) {
+ case "assumed-role": {
+ if (rest.length < 2) {
+ throw new Error(
+ `Unrecognized ARN: "${resource}" for assumed-role in ARN "${arn}" contains fewer than 3 slash-separated parts (type/roleName/sessionId)`
+ );
+ }
+ // assumed roles use a special format where the friendly name is the role name
+ const [roleName, sessionId] = rest;
+ finalType = "role"; // treat assumed role case as role
+ friendlyName = roleName;
+ sessionInfo = sessionId;
+ break;
+ }
+ case "user":
+ case "role":
+ case "instance-profile":
+ finalType = rawType;
+ path = rest.slice(0, -1).join("/");
+ break;
+ default:
+ throw new Error(
+ `Unrecognized principal type: "${rawType}" in ARN "${arn}". Expected "user", "role", "instance-profile", or "assumed-role".`
+ );
+ }
+
+ const entity: PrincipalArnEntity = {
+ Partition: partition,
+ Service: service,
+ AccountNumber: accountNumber,
+ Type: finalType,
+ Path: path,
+ FriendlyName: friendlyName,
+ SessionInfo: sessionInfo
+ };
+
+ return entity;
+};
+
/**
* Extracts the identity ARN from the GetCallerIdentity response to one of the following formats:
* - arn:aws:iam::123456789012:user/MyUserName
* - arn:aws:iam::123456789012:role/MyRoleName
*/
export const extractPrincipalArn = (arn: string) => {
- // split the ARN into parts using ":" as the delimiter
- const fullParts = arn.split(":");
- if (fullParts.length !== 6) {
- throw new Error(`Unrecognized ARN: contains ${fullParts.length} colon-separated parts, expected 6`);
- }
- const [prefix, partition, service, , accountNumber, resource] = fullParts;
- if (prefix !== "arn") {
- throw new Error('Unrecognized ARN: does not begin with "arn:"');
- }
-
- // structure to hold the parsed data
- const entity = {
- Partition: partition,
- Service: service,
- AccountNumber: accountNumber,
- Type: "",
- Path: "",
- FriendlyName: "",
- SessionInfo: ""
- };
-
- // validate the service is either 'iam' or 'sts'
- if (entity.Service !== "iam" && entity.Service !== "sts") {
- throw new Error(`Unrecognized service: ${entity.Service}, not one of iam or sts`);
- }
-
- // parse the last part of the ARN which describes the resource
- const parts = resource.split("/");
- if (parts.length < 2) {
- throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 2 slash-separated parts`);
- }
-
- const [type, ...rest] = parts;
- entity.Type = type;
- entity.FriendlyName = parts[parts.length - 1];
-
- // handle different types of resources
- switch (entity.Type) {
- case "assumed-role": {
- if (rest.length < 2) {
- throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 3 slash-separated parts`);
- }
- // assumed roles use a special format where the friendly name is the role name
- const [roleName, sessionId] = rest;
- entity.Type = "role"; // treat assumed role case as role
- entity.FriendlyName = roleName;
- entity.SessionInfo = sessionId;
- break;
- }
- case "user":
- case "role":
- case "instance-profile":
- // standard cases: just join back the path if there's any
- entity.Path = rest.slice(0, -1).join("/");
- break;
- default:
- throw new Error(`Unrecognized principal type: "${entity.Type}"`);
- }
+ const entity = extractPrincipalArnEntity(arn);
return `arn:aws:iam::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`;
};
diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts
index 7c339f15e..b5035946e 100644
--- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts
+++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts
@@ -22,7 +22,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityAwsAuthDALFactory } from "./identity-aws-auth-dal";
-import { extractPrincipalArn } from "./identity-aws-auth-fns";
+import { extractPrincipalArn, extractPrincipalArnEntity } from "./identity-aws-auth-fns";
import {
TAttachAwsAuthDTO,
TAwsGetCallerIdentityHeaders,
@@ -107,7 +107,7 @@ export const identityAwsAuthServiceFactory = ({
const {
data: {
GetCallerIdentityResponse: {
- GetCallerIdentityResult: { Account, Arn }
+ GetCallerIdentityResult: { Account, Arn, UserId }
}
}
}: { data: TGetCallerIdentityResponse } = await axios({
@@ -168,11 +168,25 @@ export const identityAwsAuthServiceFactory = ({
});
const appCfg = getConfig();
+ const splitArn = extractPrincipalArnEntity(Arn);
const accessToken = crypto.jwt().sign(
{
identityId: identityAwsAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
- authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
+ authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
+ identityAuth: {
+ aws: {
+ accountId: Account,
+ arn: Arn,
+ userId: UserId,
+
+ // Derived from ARN
+ partition: splitArn.Partition,
+ service: splitArn.Service,
+ resourceType: splitArn.Type,
+ resourceName: splitArn.FriendlyName
+ }
+ }
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
diff --git a/backend/src/services/project-env/project-env-service.ts b/backend/src/services/project-env/project-env-service.ts
index 9a82a6bbe..7fbe7343e 100644
--- a/backend/src/services/project-env/project-env-service.ts
+++ b/backend/src/services/project-env/project-env-service.ts
@@ -1,9 +1,11 @@
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
+import { TAccessApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-environment-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
+import { TSecretApprovalPolicyEnvironmentDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-environment-dal";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
@@ -20,6 +22,8 @@ type TProjectEnvServiceFactoryDep = {
permissionService: Pick;
licenseService: Pick;
keyStore: Pick;
+ accessApprovalPolicyEnvironmentDAL: Pick;
+ secretApprovalPolicyEnvironmentDAL: Pick;
};
export type TProjectEnvServiceFactory = ReturnType;
@@ -30,7 +34,9 @@ export const projectEnvServiceFactory = ({
licenseService,
keyStore,
projectDAL,
- folderDAL
+ folderDAL,
+ accessApprovalPolicyEnvironmentDAL,
+ secretApprovalPolicyEnvironmentDAL
}: TProjectEnvServiceFactoryDep) => {
const createEnvironment = async ({
projectId,
@@ -220,6 +226,20 @@ export const projectEnvServiceFactory = ({
}
const env = await projectEnvDAL.transaction(async (tx) => {
+ const secretApprovalPolicies = await secretApprovalPolicyEnvironmentDAL.findAvailablePoliciesByEnvId(id, tx);
+ if (secretApprovalPolicies.length > 0) {
+ throw new BadRequestError({
+ message: "Environment is in use by a secret approval policy",
+ name: "DeleteEnvironment"
+ });
+ }
+ const accessApprovalPolicies = await accessApprovalPolicyEnvironmentDAL.findAvailablePoliciesByEnvId(id, tx);
+ if (accessApprovalPolicies.length > 0) {
+ throw new BadRequestError({
+ message: "Environment is in use by an access approval policy",
+ name: "DeleteEnvironment"
+ });
+ }
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx);
if (!doc)
throw new NotFoundError({
diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts
index 4ab6dbfdb..4261870b1 100644
--- a/backend/src/services/project/project-service.ts
+++ b/backend/src/services/project/project-service.ts
@@ -550,7 +550,7 @@ export const projectServiceFactory = ({
const updateProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, update, filter }: TUpdateProjectDTO) => {
const project = await projectDAL.findProjectByFilter(filter);
- const { permission } = await permissionService.getProjectPermission({
+ const { permission, hasRole } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: project.id,
@@ -572,6 +572,12 @@ export const projectServiceFactory = ({
}
}
+ if (update.secretDetectionIgnoreValues && !hasRole(ProjectMembershipRole.Admin)) {
+ throw new ForbiddenRequestError({
+ message: "Only admins can update secret detection ignore values"
+ });
+ }
+
const updatedProject = await projectDAL.updateById(project.id, {
name: update.name,
description: update.description,
@@ -581,7 +587,8 @@ export const projectServiceFactory = ({
slug: update.slug,
secretSharing: update.secretSharing,
defaultProduct: update.defaultProduct,
- showSnapshotsLegacy: update.showSnapshotsLegacy
+ showSnapshotsLegacy: update.showSnapshotsLegacy,
+ secretDetectionIgnoreValues: update.secretDetectionIgnoreValues
});
return updatedProject;
diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts
index b7c785db5..ceef78f6a 100644
--- a/backend/src/services/project/project-types.ts
+++ b/backend/src/services/project/project-types.ts
@@ -96,6 +96,7 @@ export type TUpdateProjectDTO = {
slug?: string;
secretSharing?: boolean;
showSnapshotsLegacy?: boolean;
+ secretDetectionIgnoreValues?: string[];
};
} & Omit;
diff --git a/backend/src/services/reminder/reminder-queue.ts b/backend/src/services/reminder/reminder-queue.ts
index 4e31c8a6d..c038734bd 100644
--- a/backend/src/services/reminder/reminder-queue.ts
+++ b/backend/src/services/reminder/reminder-queue.ts
@@ -11,7 +11,7 @@ import { TReminderServiceFactory } from "./reminder-types";
type TDailyReminderQueueServiceFactoryDep = {
reminderService: TReminderServiceFactory;
queueService: TQueueServiceFactory;
- secretDAL: Pick;
+ secretDAL: Pick;
secretReminderRecipientsDAL: Pick;
};
@@ -69,7 +69,7 @@ export const dailyReminderQueueServiceFactory = ({
// Find existing secrets with pagination
// eslint-disable-next-line no-await-in-loop
- const secrets = await secretDAL.findSecretsWithReminderRecipients(batchIds, REMINDER_PRUNE_BATCH_SIZE);
+ const secrets = await secretDAL.findSecretsWithReminderRecipientsOld(batchIds, REMINDER_PRUNE_BATCH_SIZE);
const secretsWithReminder = secrets.filter((secret) => secret.reminderRepeatDays);
const foundSecretIds = new Set(secretsWithReminder.map((secret) => secret.id));
@@ -173,12 +173,6 @@ export const dailyReminderQueueServiceFactory = ({
{ pattern: "0 */1 * * *", utc: true },
QueueName.SecretReminderMigration // just a job id
);
-
- await queueService.queue(QueueName.SecretReminderMigration, QueueJobs.SecretReminderMigration, undefined, {
- delay: 5000,
- jobId: QueueName.SecretReminderMigration,
- repeat: { pattern: "0 */1 * * *", utc: true }
- });
};
queueService.listen(QueueName.DailyReminders, "failed", (_, err) => {
diff --git a/backend/src/services/reminder/reminder-service.ts b/backend/src/services/reminder/reminder-service.ts
index ddccbbf62..a03e9cddd 100644
--- a/backend/src/services/reminder/reminder-service.ts
+++ b/backend/src/services/reminder/reminder-service.ts
@@ -308,12 +308,11 @@ export const reminderServiceFactory = ({
);
const newReminders = await reminderDAL.insertMany(
- processedReminders.map(({ secretId, message, repeatDays, nextReminderDate, projectId }) => ({
+ processedReminders.map(({ secretId, message, repeatDays, nextReminderDate }) => ({
secretId,
message,
repeatDays,
- nextReminderDate,
- projectId
+ nextReminderDate
})),
tx
);
diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts
index f06f0cfc2..b37d5e90e 100644
--- a/backend/src/services/secret-sync/github/github-sync-fns.ts
+++ b/backend/src/services/secret-sync/github/github-sync-fns.ts
@@ -1,7 +1,12 @@
-import { Octokit } from "@octokit/rest";
import sodium from "libsodium-wrappers";
-import { getGitHubClient } from "@app/services/app-connection/github";
+import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
+import {
+ getGitHubAppAuthToken,
+ GitHubConnectionMethod,
+ makePaginatedGitHubRequest,
+ requestWithGitHubGateway
+} from "@app/services/app-connection/github";
import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
@@ -12,155 +17,165 @@ import { TGitHubPublicKey, TGitHubSecret, TGitHubSecretPayload, TGitHubSyncWithC
// TODO: rate limit handling
-const getEncryptedSecrets = async (client: Octokit, secretSync: TGitHubSyncWithCredentials) => {
- let encryptedSecrets: TGitHubSecret[];
-
- const { destinationConfig } = secretSync;
+const getEncryptedSecrets = async (
+ secretSync: TGitHubSyncWithCredentials,
+ gatewayService: Pick
+) => {
+ const { destinationConfig, connection } = secretSync;
+ let path: string;
switch (destinationConfig.scope) {
case GitHubSyncScope.Organization: {
- encryptedSecrets = await client.paginate("GET /orgs/{org}/actions/secrets", {
- org: destinationConfig.org
- });
+ path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets`;
break;
}
case GitHubSyncScope.Repository: {
- encryptedSecrets = await client.paginate("GET /repos/{owner}/{repo}/actions/secrets", {
- owner: destinationConfig.owner,
- repo: destinationConfig.repo
- });
-
+ path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets`;
break;
}
case GitHubSyncScope.RepositoryEnvironment:
default: {
- encryptedSecrets = await client.paginate("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets", {
- owner: destinationConfig.owner,
- repo: destinationConfig.repo,
- environment_name: destinationConfig.env
- });
+ path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets`;
break;
}
}
- return encryptedSecrets;
+ return makePaginatedGitHubRequest(
+ connection,
+ gatewayService,
+ path,
+ (data) => data.secrets
+ );
};
-const getPublicKey = async (client: Octokit, secretSync: TGitHubSyncWithCredentials) => {
- let publicKey: TGitHubPublicKey;
-
- const { destinationConfig } = secretSync;
+const getPublicKey = async (
+ secretSync: TGitHubSyncWithCredentials,
+ gatewayService: Pick,
+ token: string
+) => {
+ const { destinationConfig, connection } = secretSync;
+ let path: string;
switch (destinationConfig.scope) {
case GitHubSyncScope.Organization: {
- publicKey = (
- await client.request("GET /orgs/{org}/actions/secrets/public-key", {
- org: destinationConfig.org
- })
- ).data;
+ path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets/public-key`;
break;
}
case GitHubSyncScope.Repository: {
- publicKey = (
- await client.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
- owner: destinationConfig.owner,
- repo: destinationConfig.repo
- })
- ).data;
+ path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets/public-key`;
break;
}
case GitHubSyncScope.RepositoryEnvironment:
default: {
- publicKey = (
- await client.request("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets/public-key", {
- owner: destinationConfig.owner,
- repo: destinationConfig.repo,
- environment_name: destinationConfig.env
- })
- ).data;
+ path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets/public-key`;
break;
}
}
- return publicKey;
+ const response = await requestWithGitHubGateway(connection, gatewayService, {
+ url: `https://api.${connection.credentials.host || "github.com"}${path}`,
+ method: "GET",
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `Bearer ${token}`,
+ "X-GitHub-Api-Version": "2022-11-28"
+ }
+ });
+
+ return response.data;
};
const deleteSecret = async (
- client: Octokit,
secretSync: TGitHubSyncWithCredentials,
+ gatewayService: Pick,
+ token: string,
encryptedSecret: TGitHubSecret
) => {
- const { destinationConfig } = secretSync;
+ const { destinationConfig, connection } = secretSync;
+ let path: string;
switch (destinationConfig.scope) {
case GitHubSyncScope.Organization: {
- await client.request(`DELETE /orgs/{org}/actions/secrets/{secret_name}`, {
- org: destinationConfig.org,
- secret_name: encryptedSecret.name
- });
+ path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets/${encodeURIComponent(encryptedSecret.name)}`;
break;
}
case GitHubSyncScope.Repository: {
- await client.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
- owner: destinationConfig.owner,
- repo: destinationConfig.repo,
- secret_name: encryptedSecret.name
- });
+ path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets/${encodeURIComponent(encryptedSecret.name)}`;
break;
}
case GitHubSyncScope.RepositoryEnvironment:
default: {
- await client.request("DELETE /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", {
- owner: destinationConfig.owner,
- repo: destinationConfig.repo,
- environment_name: destinationConfig.env,
- secret_name: encryptedSecret.name
- });
+ path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets/${encodeURIComponent(encryptedSecret.name)}`;
break;
}
}
+
+ await requestWithGitHubGateway(connection, gatewayService, {
+ url: `https://api.${connection.credentials.host || "github.com"}${path}`,
+ method: "DELETE",
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `Bearer ${token}`,
+ "X-GitHub-Api-Version": "2022-11-28"
+ }
+ });
};
-const putSecret = async (client: Octokit, secretSync: TGitHubSyncWithCredentials, payload: TGitHubSecretPayload) => {
- const { destinationConfig } = secretSync;
+const putSecret = async (
+ secretSync: TGitHubSyncWithCredentials,
+ gatewayService: Pick,
+ token: string,
+ payload: TGitHubSecretPayload
+) => {
+ const { destinationConfig, connection } = secretSync;
+
+ let path: string;
+ let body: Record = payload;
switch (destinationConfig.scope) {
case GitHubSyncScope.Organization: {
const { visibility, selectedRepositoryIds } = destinationConfig;
-
- await client.request(`PUT /orgs/{org}/actions/secrets/{secret_name}`, {
- org: destinationConfig.org,
+ path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets/${encodeURIComponent(payload.secret_name)}`;
+ body = {
...payload,
visibility,
...(visibility === GitHubSyncVisibility.Selected && {
selected_repository_ids: selectedRepositoryIds
})
- });
+ };
break;
}
case GitHubSyncScope.Repository: {
- await client.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
- owner: destinationConfig.owner,
- repo: destinationConfig.repo,
- ...payload
- });
+ path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets/${encodeURIComponent(payload.secret_name)}`;
break;
}
case GitHubSyncScope.RepositoryEnvironment:
default: {
- await client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", {
- owner: destinationConfig.owner,
- repo: destinationConfig.repo,
- environment_name: destinationConfig.env,
- ...payload
- });
+ path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets/${encodeURIComponent(payload.secret_name)}`;
break;
}
}
+
+ await requestWithGitHubGateway(connection, gatewayService, {
+ url: `https://api.${connection.credentials.host || "github.com"}${path}`,
+ method: "PUT",
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `Bearer ${token}`,
+ "X-GitHub-Api-Version": "2022-11-28"
+ },
+ data: body
+ });
};
export const GithubSyncFns = {
- syncSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => {
+ syncSecrets: async (
+ secretSync: TGitHubSyncWithCredentials,
+ ogSecretMap: TSecretMap,
+ gatewayService: Pick
+ ) => {
+ const secretMap = Object.fromEntries(Object.entries(ogSecretMap).map(([i, v]) => [i.toUpperCase(), v]));
+
switch (secretSync.destinationConfig.scope) {
case GitHubSyncScope.Organization:
if (Object.values(secretMap).length > 1000) {
@@ -187,38 +202,40 @@ export const GithubSyncFns = {
);
}
- const client = getGitHubClient(secretSync.connection);
+ const { connection } = secretSync;
+ const token =
+ connection.method === GitHubConnectionMethod.OAuth
+ ? connection.credentials.accessToken
+ : await getGitHubAppAuthToken(connection);
- const encryptedSecrets = await getEncryptedSecrets(client, secretSync);
+ const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
+ const publicKey = await getPublicKey(secretSync, gatewayService, token);
- const publicKey = await getPublicKey(client, secretSync);
+ await sodium.ready;
+ for await (const key of Object.keys(secretMap)) {
+ // convert secret & base64 key to Uint8Array.
+ const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL);
+ const binarySecretValue = sodium.from_string(secretMap[key].value);
- await sodium.ready.then(async () => {
- for await (const key of Object.keys(secretMap)) {
- // convert secret & base64 key to Uint8Array.
- const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL);
- const binarySecretValue = sodium.from_string(secretMap[key].value);
+ // encrypt secret using libsodium
+ const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey);
- // encrypt secret using libsodium
- const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey);
+ // convert encrypted Uint8Array to base64
+ const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
- // convert encrypted Uint8Array to base64
- const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
-
- try {
- await putSecret(client, secretSync, {
- secret_name: key,
- encrypted_value: encryptedSecretValue,
- key_id: publicKey.key_id
- });
- } catch (error) {
- throw new SecretSyncError({
- error,
- secretKey: key
- });
- }
+ try {
+ await putSecret(secretSync, gatewayService, token, {
+ secret_name: key,
+ encrypted_value: encryptedSecretValue,
+ key_id: publicKey.key_id
+ });
+ } catch (error) {
+ throw new SecretSyncError({
+ error,
+ secretKey: key
+ });
}
- });
+ }
if (secretSync.syncOptions.disableSecretDeletion) return;
@@ -228,21 +245,31 @@ export const GithubSyncFns = {
continue;
if (!(encryptedSecret.name in secretMap)) {
- await deleteSecret(client, secretSync, encryptedSecret);
+ await deleteSecret(secretSync, gatewayService, token, encryptedSecret);
}
}
},
getSecrets: async (secretSync: TGitHubSyncWithCredentials) => {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
},
- removeSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => {
- const client = getGitHubClient(secretSync.connection);
+ removeSecrets: async (
+ secretSync: TGitHubSyncWithCredentials,
+ ogSecretMap: TSecretMap,
+ gatewayService: Pick
+ ) => {
+ const secretMap = Object.fromEntries(Object.entries(ogSecretMap).map(([i, v]) => [i.toUpperCase(), v]));
- const encryptedSecrets = await getEncryptedSecrets(client, secretSync);
+ const { connection } = secretSync;
+ const token =
+ connection.method === GitHubConnectionMethod.OAuth
+ ? connection.credentials.accessToken
+ : await getGitHubAppAuthToken(connection);
+
+ const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
for await (const encryptedSecret of encryptedSecrets) {
if (encryptedSecret.name in secretMap) {
- await deleteSecret(client, secretSync, encryptedSecret);
+ await deleteSecret(secretSync, gatewayService, token, encryptedSecret);
}
}
}
diff --git a/backend/src/services/secret-sync/render/render-sync-fns.ts b/backend/src/services/secret-sync/render/render-sync-fns.ts
index 9140136a0..71347f998 100644
--- a/backend/src/services/secret-sync/render/render-sync-fns.ts
+++ b/backend/src/services/secret-sync/render/render-sync-fns.ts
@@ -8,7 +8,26 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
-const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials) => {
+const MAX_RETRIES = 5;
+
+const retrySleep = async () =>
+ new Promise((resolve) => {
+ setTimeout(resolve, 60000);
+ });
+
+const makeRequestWithRetry = async (requestFn: () => Promise, attempt = 0): Promise => {
+ try {
+ return await requestFn();
+ } catch (error) {
+ if (isAxiosError(error) && error.response?.status === 429 && attempt < MAX_RETRIES) {
+ await retrySleep();
+ return await makeRequestWithRetry(requestFn, attempt + 1);
+ }
+ throw error;
+ }
+};
+
+const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials): Promise => {
const {
destinationConfig,
connection: {
@@ -22,20 +41,23 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential
do {
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
- const { data } = await request.get<
- {
- envVar: {
- key: string;
- value: string;
- };
- cursor: string;
- }[]
- >(url, {
- headers: {
- Authorization: `Bearer ${apiKey}`,
- Accept: "application/json"
- }
- });
+
+ const { data } = await makeRequestWithRetry(() =>
+ request.get<
+ {
+ envVar: {
+ key: string;
+ value: string;
+ };
+ cursor: string;
+ }[]
+ >(url, {
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ Accept: "application/json"
+ }
+ })
+ );
const secrets = data.map((item) => ({
key: item.envVar.key,
@@ -44,13 +66,20 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential
allSecrets.push(...secrets);
- cursor = data[data.length - 1]?.cursor;
+ if (data.length > 0 && data[data.length - 1]?.cursor) {
+ cursor = data[data.length - 1].cursor;
+ } else {
+ cursor = undefined;
+ }
} while (cursor);
return allSecrets;
};
-const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap, key: string) => {
+const batchUpdateEnvironmentSecrets = async (
+ secretSync: TRenderSyncWithCredentials,
+ envVars: Array<{ key: string; value: string }>
+): Promise => {
const {
destinationConfig,
connection: {
@@ -58,22 +87,17 @@ const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secr
}
} = secretSync;
- await request.put(
- `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${key}`,
- {
- key,
- value: secretMap[key].value
- },
- {
+ await makeRequestWithRetry(() =>
+ request.put(`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`, envVars, {
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
- }
+ })
);
};
-const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: Pick) => {
+const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
const {
destinationConfig,
connection: {
@@ -81,70 +105,81 @@ const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, s
}
} = secretSync;
- try {
- await request.delete(
- `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
+ await makeRequestWithRetry(() =>
+ request.post(
+ `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`,
+ {},
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
}
- );
- } catch (error) {
- if (isAxiosError(error) && error.response?.status === 404) {
- // If the secret does not exist, we can ignore this error
- return;
- }
-
- throw error;
- }
+ )
+ );
};
-const sleep = async () =>
- new Promise((resolve) => {
- setTimeout(resolve, 500);
- });
-
export const RenderSyncFns = {
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
- for await (const key of Object.keys(secretMap)) {
- // If value is empty skip it as render does not allow empty variables
- if (secretMap[key].value === "") {
- // eslint-disable-next-line no-continue
- continue;
+
+ const finalEnvVars: Array<{ key: string; value: string }> = [];
+
+ for (const renderSecret of renderSecrets) {
+ const shouldKeep =
+ secretMap[renderSecret.key] ||
+ (secretSync.syncOptions.disableSecretDeletion &&
+ !matchesSchema(renderSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema));
+
+ if (shouldKeep && !secretMap[renderSecret.key]) {
+ finalEnvVars.push({
+ key: renderSecret.key,
+ value: renderSecret.value
+ });
}
- await putEnvironmentSecret(secretSync, secretMap, key);
- await sleep();
}
- if (secretSync.syncOptions.disableSecretDeletion) return;
-
- for await (const renderSecret of renderSecrets) {
- if (!matchesSchema(renderSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
+ for (const [key, secret] of Object.entries(secretMap)) {
+ // Skip empty values as render does not allow empty variables
+ if (secret.value === "") {
// eslint-disable-next-line no-continue
continue;
-
- if (!secretMap[renderSecret.key]) {
- await deleteEnvironmentSecret(secretSync, renderSecret);
- await sleep();
}
+
+ finalEnvVars.push({
+ key,
+ value: secret.value
+ });
+ }
+
+ await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
+
+ if (secretSync.syncOptions.autoRedeployServices) {
+ await redeployService(secretSync);
}
},
+
getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise => {
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
return Object.fromEntries(renderSecrets.map((secret) => [secret.key, { value: secret.value ?? "" }]));
},
removeSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
- const encryptedSecrets = await getRenderEnvironmentSecrets(secretSync);
+ const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
+ const finalEnvVars: Array<{ key: string; value: string }> = [];
- for await (const encryptedSecret of encryptedSecrets) {
- if (encryptedSecret.key in secretMap) {
- await deleteEnvironmentSecret(secretSync, encryptedSecret);
- await sleep();
+ for (const renderSecret of renderSecrets) {
+ if (!(renderSecret.key in secretMap)) {
+ finalEnvVars.push({
+ key: renderSecret.key,
+ value: renderSecret.value
+ });
}
}
+ await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
+
+ if (secretSync.syncOptions.autoRedeployServices) {
+ await redeployService(secretSync);
+ }
}
};
diff --git a/backend/src/services/secret-sync/render/render-sync-schemas.ts b/backend/src/services/secret-sync/render/render-sync-schemas.ts
index 77414c17c..0d6e93987 100644
--- a/backend/src/services/secret-sync/render/render-sync-schemas.ts
+++ b/backend/src/services/secret-sync/render/render-sync-schemas.ts
@@ -20,23 +20,33 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
})
]);
+const RenderSyncOptionsSchema = z.object({
+ autoRedeployServices: z.boolean().optional().describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.RENDER.autoRedeployServices)
+});
+
const RenderSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
-export const RenderSyncSchema = BaseSecretSyncSchema(SecretSync.Render, RenderSyncOptionsConfig).extend({
+export const RenderSyncSchema = BaseSecretSyncSchema(
+ SecretSync.Render,
+ RenderSyncOptionsConfig,
+ RenderSyncOptionsSchema
+).extend({
destination: z.literal(SecretSync.Render),
destinationConfig: RenderSyncDestinationConfigSchema
});
export const CreateRenderSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Render,
- RenderSyncOptionsConfig
+ RenderSyncOptionsConfig,
+ RenderSyncOptionsSchema
).extend({
destinationConfig: RenderSyncDestinationConfigSchema
});
export const UpdateRenderSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Render,
- RenderSyncOptionsConfig
+ RenderSyncOptionsConfig,
+ RenderSyncOptionsSchema
).extend({
destinationConfig: RenderSyncDestinationConfigSchema.optional()
});
diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts
index caa6ddcee..6fa46f1e6 100644
--- a/backend/src/services/secret-sync/secret-sync-fns.ts
+++ b/backend/src/services/secret-sync/secret-sync-fns.ts
@@ -1,6 +1,7 @@
import { AxiosError } from "axios";
import handlebars from "handlebars";
+import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault";
import { BadRequestError } from "@app/lib/errors";
@@ -97,6 +98,7 @@ export const listSecretSyncOptions = () => {
type TSyncSecretDeps = {
appConnectionDAL: Pick;
kmsService: Pick;
+ gatewayService: Pick;
};
// Add schema to secret keys
@@ -191,7 +193,7 @@ export const SecretSyncFns = {
syncSecrets: (
secretSync: TSecretSyncWithCredentials,
secretMap: TSecretMap,
- { kmsService, appConnectionDAL }: TSyncSecretDeps
+ { kmsService, appConnectionDAL, gatewayService }: TSyncSecretDeps
): Promise => {
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
@@ -201,7 +203,7 @@ export const SecretSyncFns = {
case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub:
- return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap);
+ return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap, gatewayService);
case SecretSync.GCPSecretManager:
return GcpSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault:
@@ -395,7 +397,7 @@ export const SecretSyncFns = {
removeSecrets: (
secretSync: TSecretSyncWithCredentials,
secretMap: TSecretMap,
- { kmsService, appConnectionDAL }: TSyncSecretDeps
+ { kmsService, appConnectionDAL, gatewayService }: TSyncSecretDeps
): Promise => {
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
@@ -405,7 +407,7 @@ export const SecretSyncFns = {
case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub:
- return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap);
+ return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap, gatewayService);
case SecretSync.GCPSecretManager:
return GcpSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault:
diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts
index 5d788ea88..7bef7d8c7 100644
--- a/backend/src/services/secret-sync/secret-sync-queue.ts
+++ b/backend/src/services/secret-sync/secret-sync-queue.ts
@@ -4,6 +4,7 @@ import { Job } from "bullmq";
import { ProjectMembershipRole, SecretType } from "@app/db/schemas";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
+import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
@@ -96,6 +97,7 @@ type TSecretSyncQueueFactoryDep = {
resourceMetadataDAL: Pick;
folderCommitService: Pick;
licenseService: Pick;
+ gatewayService: Pick;
};
type SecretSyncActionJob = Job<
@@ -138,7 +140,8 @@ export const secretSyncQueueFactory = ({
secretVersionTagV2BridgeDAL,
resourceMetadataDAL,
folderCommitService,
- licenseService
+ licenseService,
+ gatewayService
}: TSecretSyncQueueFactoryDep) => {
const appCfg = getConfig();
@@ -353,7 +356,8 @@ export const secretSyncQueueFactory = ({
const importedSecrets = await SecretSyncFns.getSecrets(secretSync, {
appConnectionDAL,
- kmsService
+ kmsService,
+ gatewayService
});
if (!Object.keys(importedSecrets).length) return {};
@@ -481,7 +485,8 @@ export const secretSyncQueueFactory = ({
await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, {
appConnectionDAL,
- kmsService
+ kmsService,
+ gatewayService
});
isSynced = true;
@@ -730,7 +735,8 @@ export const secretSyncQueueFactory = ({
secretMap,
{
appConnectionDAL,
- kmsService
+ kmsService,
+ gatewayService
}
);
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts
index 4cbd1d783..c2a72f2e6 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts
@@ -875,6 +875,48 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
}
};
+ const findSecretsWithReminderRecipientsOld = async (ids: string[], limit: number, tx?: Knex) => {
+ try {
+ // Create a subquery to get limited secret IDs
+ const limitedSecretIds = (tx || db)(TableName.SecretV2)
+ .whereIn(`${TableName.SecretV2}.id`, ids)
+ .limit(limit)
+ .select("id");
+
+ // Join with all recipients for the limited secrets
+ const docs = await (tx || db)(TableName.SecretV2)
+ .whereIn(`${TableName.SecretV2}.id`, limitedSecretIds)
+ .leftJoin(TableName.Reminder, `${TableName.SecretV2}.id`, `${TableName.Reminder}.secretId`)
+ .leftJoin(
+ TableName.SecretReminderRecipients,
+ `${TableName.SecretV2}.id`,
+ `${TableName.SecretReminderRecipients}.secretId`
+ )
+ .select(selectAllTableCols(TableName.SecretV2))
+ .select(db.ref("userId").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientUserId"));
+
+ const data = sqlNestRelationships({
+ data: docs,
+ key: "id",
+ parentMapper: (el) => ({
+ _id: el.id,
+ ...SecretsV2Schema.parse(el)
+ }),
+ childrenMapper: [
+ {
+ key: "reminderRecipientUserId",
+ label: "recipients" as const,
+ mapper: ({ reminderRecipientUserId }) => reminderRecipientUserId
+ }
+ ]
+ });
+
+ return data;
+ } catch (error) {
+ throw new DatabaseError({ error, name: "findSecretsWithReminderRecipientsOld" });
+ }
+ };
+
return {
...secretOrm,
update,
@@ -893,6 +935,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
findOne,
find,
invalidateSecretCacheByProjectId,
- findSecretsWithReminderRecipients
+ findSecretsWithReminderRecipients,
+ findSecretsWithReminderRecipientsOld
};
};
diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
index 2fa0ffe9b..43daaf0df 100644
--- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
+++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts
@@ -25,6 +25,7 @@ import {
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
+import { scanSecretPolicyViolations } from "@app/ee/services/secret-scanning-v2/secret-scanning-v2-fns";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { DatabaseErrorCode } from "@app/lib/error-codes";
@@ -38,6 +39,7 @@ import { ActorType } from "../auth/auth-type";
import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
+import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TReminderServiceFactory } from "../reminder/reminder-types";
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
@@ -88,6 +90,7 @@ import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal";
type TSecretV2BridgeServiceFactoryDep = {
secretDAL: TSecretV2BridgeDALFactory;
+ projectDAL: Pick;
secretVersionDAL: TSecretVersionV2DALFactory;
kmsService: Pick;
secretVersionTagDAL: Pick;
@@ -126,6 +129,7 @@ export type TSecretV2BridgeServiceFactory = ReturnType ({ secretKey: el, secretPath, environment }))
@@ -506,6 +523,21 @@ export const secretV2BridgeServiceFactory = ({
const { secretName, secretValue } = inputSecret;
+ if (secretValue) {
+ const project = await projectDAL.findById(projectId);
+ await scanSecretPolicyViolations(
+ projectId,
+ secretPath,
+ [
+ {
+ secretKey: inputSecret.newSecretName || secretName,
+ secretValue
+ }
+ ],
+ project.secretDetectionIgnoreValues || []
+ );
+ }
+
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
@@ -1585,6 +1617,9 @@ export const secretV2BridgeServiceFactory = ({
if (secrets.length)
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
+ const project = await projectDAL.findById(projectId);
+ await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []);
+
// get all tags
const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds);
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : [];
@@ -1925,6 +1960,19 @@ export const secretV2BridgeServiceFactory = ({
});
await $validateSecretReferences(projectId, permission, secretReferences, tx);
+ const project = await projectDAL.findById(projectId);
+ await scanSecretPolicyViolations(
+ projectId,
+ secretPath,
+ secretsToUpdate
+ .filter((el) => el.secretValue)
+ .map((el) => ({
+ secretKey: el.newSecretName || el.secretKey,
+ secretValue: el.secretValue as string
+ })),
+ project.secretDetectionIgnoreValues || []
+ );
+
const bulkUpdatedSecrets = await fnSecretBulkUpdate({
folderId,
orgId: actorOrgId,
diff --git a/cli/.gitignore b/cli/.gitignore
deleted file mode 100644
index 8eb54d72b..000000000
--- a/cli/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-.infisical.json
-dist/
-agent-config.test.yaml
-.test.env
\ No newline at end of file
diff --git a/cli/.infisicalignore b/cli/.infisicalignore
deleted file mode 100644
index e5dfe29bc..000000000
--- a/cli/.infisicalignore
+++ /dev/null
@@ -1,3 +0,0 @@
-bea0ff6e05a4de73a5db625d4ae181a015b50855:frontend/components/utilities/attemptLogin.js:stripe-access-token:147
-bea0ff6e05a4de73a5db625d4ae181a015b50855:backend/src/json/integrations.json:generic-api-key:5
-1961b92340e5d2613acae528b886c842427ce5d0:frontend/components/utilities/attemptLogin.js:stripe-access-token:148
diff --git a/cli/agent-config.yaml b/cli/agent-config.yaml
deleted file mode 100644
index 210c21413..000000000
--- a/cli/agent-config.yaml
+++ /dev/null
@@ -1,37 +0,0 @@
-infisical:
- address: "https://app.infisical.com/"
-auth:
- type: "universal-auth"
- config:
- client-id: "./client-id"
- client-secret: "./client-secret"
- remove_client_secret_on_read: false
-sinks:
- - type: "file"
- config:
- path: "access-token"
-templates:
- - template-content: |
- {{- with secret "202f04d7-e4cb-43d4-a292-e893712d61fc" "dev" "/" }}
- {{- range . }}
- {{ .Key }}={{ .Value }}
- {{- end }}
- {{- end }}
- destination-path: my-dot-env-0.env
- config:
- polling-interval: 60s
- execute:
- command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d
-
- - base64-template-content: e3stIHdpdGggc2VjcmV0ICIyMDJmMDRkNy1lNGNiLTQzZDQtYTI5Mi1lODkzNzEyZDYxZmMiICJkZXYiICIvIiB9fQp7ey0gcmFuZ2UgLiB9fQp7eyAuS2V5IH19PXt7IC5WYWx1ZSB9fQp7ey0gZW5kIH19Cnt7LSBlbmQgfX0=
- destination-path: my-dot-env.env
- config:
- polling-interval: 60s
- execute:
- command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d
-
- - source-path: my-dot-ev-secret-template1
- destination-path: my-dot-env-1.env
- config:
- exec:
- command: mkdir hello-world1
diff --git a/cli/detect/baseline.go b/cli/detect/baseline.go
deleted file mode 100644
index eeaa2a73a..000000000
--- a/cli/detect/baseline.go
+++ /dev/null
@@ -1,103 +0,0 @@
-// MIT License
-
-// Copyright (c) 2019 Zachary Rice
-
-// 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.
-
-package detect
-
-import (
- "encoding/json"
- "fmt"
- "os"
- "path/filepath"
-
- "github.com/Infisical/infisical-merge/detect/report"
-)
-
-func IsNew(finding report.Finding, redact uint, baseline []report.Finding) bool {
- // Explicitly testing each property as it gives significantly better performance in comparison to cmp.Equal(). Drawback is that
- // the code requires maintenance if/when the Finding struct changes
- for _, b := range baseline {
- if finding.RuleID == b.RuleID &&
- finding.Description == b.Description &&
- finding.StartLine == b.StartLine &&
- finding.EndLine == b.EndLine &&
- finding.StartColumn == b.StartColumn &&
- finding.EndColumn == b.EndColumn &&
- (redact > 0 || (finding.Match == b.Match && finding.Secret == b.Secret)) &&
- finding.File == b.File &&
- finding.Commit == b.Commit &&
- finding.Author == b.Author &&
- finding.Email == b.Email &&
- finding.Date == b.Date &&
- finding.Message == b.Message &&
- // Omit checking finding.Fingerprint - if the format of the fingerprint changes, the users will see unexpected behaviour
- finding.Entropy == b.Entropy {
- return false
- }
- }
- return true
-}
-
-func LoadBaseline(baselinePath string) ([]report.Finding, error) {
- bytes, err := os.ReadFile(baselinePath)
- if err != nil {
- return nil, fmt.Errorf("could not open %s", baselinePath)
- }
-
- var previousFindings []report.Finding
- err = json.Unmarshal(bytes, &previousFindings)
- if err != nil {
- return nil, fmt.Errorf("the format of the file %s is not supported", baselinePath)
- }
-
- return previousFindings, nil
-}
-
-func (d *Detector) AddBaseline(baselinePath string, source string) error {
- if baselinePath != "" {
- absoluteSource, err := filepath.Abs(source)
- if err != nil {
- return err
- }
-
- absoluteBaseline, err := filepath.Abs(baselinePath)
- if err != nil {
- return err
- }
-
- relativeBaseline, err := filepath.Rel(absoluteSource, absoluteBaseline)
- if err != nil {
- return err
- }
-
- baseline, err := LoadBaseline(baselinePath)
- if err != nil {
- return err
- }
-
- d.baseline = baseline
- baselinePath = relativeBaseline
-
- }
-
- d.baselinePath = baselinePath
- return nil
-}
diff --git a/cli/detect/cmd/scm/scm.go b/cli/detect/cmd/scm/scm.go
deleted file mode 100644
index dddeffdf5..000000000
--- a/cli/detect/cmd/scm/scm.go
+++ /dev/null
@@ -1,70 +0,0 @@
-// MIT License
-
-// Copyright (c) 2019 Zachary Rice
-
-// 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.
-
-package scm
-
-import (
- "fmt"
- "strings"
-)
-
-type Platform int
-
-const (
- UnknownPlatform Platform = iota
- NoPlatform // Explicitly disable the feature
- GitHubPlatform
- GitLabPlatform
- AzureDevOpsPlatform
- BitBucketPlatform
- // TODO: Add others.
-)
-
-func (p Platform) String() string {
- return [...]string{
- "unknown",
- "none",
- "github",
- "gitlab",
- "azuredevops",
- "bitbucket",
- }[p]
-}
-
-func PlatformFromString(s string) (Platform, error) {
- switch strings.ToLower(s) {
- case "", "unknown":
- return UnknownPlatform, nil
- case "none":
- return NoPlatform, nil
- case "github":
- return GitHubPlatform, nil
- case "gitlab":
- return GitLabPlatform, nil
- case "azuredevops":
- return AzureDevOpsPlatform, nil
- case "bitbucket":
- return BitBucketPlatform, nil
- default:
- return UnknownPlatform, fmt.Errorf("invalid scm platform value: %s", s)
- }
-}
diff --git a/cli/detect/config/allowlist.go b/cli/detect/config/allowlist.go
deleted file mode 100644
index d91188f68..000000000
--- a/cli/detect/config/allowlist.go
+++ /dev/null
@@ -1,159 +0,0 @@
-// MIT License
-
-// Copyright (c) 2019 Zachary Rice
-
-// 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.
-
-package config
-
-import (
- "fmt"
- "strings"
-
- "golang.org/x/exp/maps"
-
- "github.com/Infisical/infisical-merge/detect/regexp"
-)
-
-type AllowlistMatchCondition int
-
-const (
- AllowlistMatchOr AllowlistMatchCondition = iota
- AllowlistMatchAnd
-)
-
-func (a AllowlistMatchCondition) String() string {
- return [...]string{
- "OR",
- "AND",
- }[a]
-}
-
-// Allowlist allows a rule to be ignored for specific
-// regexes, paths, and/or commits
-type Allowlist struct {
- // Short human readable description of the allowlist.
- Description string
-
- // MatchCondition determines whether all criteria must match.
- MatchCondition AllowlistMatchCondition
-
- // Commits is a slice of commit SHAs that are allowed to be ignored. Defaults to "OR".
- Commits []string
-
- // Paths is a slice of path regular expressions that are allowed to be ignored.
- Paths []*regexp.Regexp
-
- // Can be `match` or `line`.
- //
- // If `match` the _Regexes_ will be tested against the match of the _Rule.Regex_.
- //
- // If `line` the _Regexes_ will be tested against the entire line.
- //
- // If RegexTarget is empty, it will be tested against the found secret.
- RegexTarget string
-
- // Regexes is slice of content regular expressions that are allowed to be ignored.
- Regexes []*regexp.Regexp
-
- // StopWords is a slice of stop words that are allowed to be ignored.
- // This targets the _secret_, not the content of the regex match like the
- // Regexes slice.
- StopWords []string
-
- // validated is an internal flag to track whether `Validate()` has been called.
- validated bool
-}
-
-func (a *Allowlist) Validate() error {
- if a.validated {
- return nil
- }
-
- // Disallow empty allowlists.
- if len(a.Commits) == 0 &&
- len(a.Paths) == 0 &&
- len(a.Regexes) == 0 &&
- len(a.StopWords) == 0 {
- return fmt.Errorf("must contain at least one check for: commits, paths, regexes, or stopwords")
- }
-
- // Deduplicate commits and stopwords.
- if len(a.Commits) > 0 {
- uniqueCommits := make(map[string]struct{})
- for _, commit := range a.Commits {
- uniqueCommits[commit] = struct{}{}
- }
- a.Commits = maps.Keys(uniqueCommits)
- }
- if len(a.StopWords) > 0 {
- uniqueStopwords := make(map[string]struct{})
- for _, stopWord := range a.StopWords {
- uniqueStopwords[stopWord] = struct{}{}
- }
- a.StopWords = maps.Keys(uniqueStopwords)
- }
-
- a.validated = true
- return nil
-}
-
-// CommitAllowed returns true if the commit is allowed to be ignored.
-func (a *Allowlist) CommitAllowed(c string) (bool, string) {
- if a == nil || c == "" {
- return false, ""
- }
-
- for _, commit := range a.Commits {
- if commit == c {
- return true, c
- }
- }
- return false, ""
-}
-
-// PathAllowed returns true if the path is allowed to be ignored.
-func (a *Allowlist) PathAllowed(path string) bool {
- if a == nil || path == "" {
- return false
- }
- return anyRegexMatch(path, a.Paths)
-}
-
-// RegexAllowed returns true if the regex is allowed to be ignored.
-func (a *Allowlist) RegexAllowed(secret string) bool {
- if a == nil || secret == "" {
- return false
- }
- return anyRegexMatch(secret, a.Regexes)
-}
-
-func (a *Allowlist) ContainsStopWord(s string) (bool, string) {
- if a == nil || s == "" {
- return false, ""
- }
-
- s = strings.ToLower(s)
- for _, stopWord := range a.StopWords {
- if strings.Contains(s, strings.ToLower(stopWord)) {
- return true, stopWord
- }
- }
- return false, ""
-}
diff --git a/cli/detect/config/config.go b/cli/detect/config/config.go
deleted file mode 100644
index 10c6db7e0..000000000
--- a/cli/detect/config/config.go
+++ /dev/null
@@ -1,426 +0,0 @@
-// MIT License
-
-// Copyright (c) 2019 Zachary Rice
-
-// 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.
-
-package config
-
-import (
- _ "embed"
- "errors"
- "fmt"
- "sort"
- "strings"
-
- "github.com/spf13/viper"
-
- "github.com/Infisical/infisical-merge/detect/logging"
- "github.com/Infisical/infisical-merge/detect/regexp"
-)
-
-const DefaultScanConfigFileName = ".infisical-scan.toml"
-const DefaultScanConfigEnvName = "INFISICAL_SCAN_CONFIG"
-const DefaultInfisicalIgnoreFineName = ".infisicalignore"
-
-var (
- //go:embed gitleaks.toml
- DefaultConfig string
-
- // use to keep track of how many configs we can extend
- // yea I know, globals bad
- extendDepth int
-)
-
-const maxExtendDepth = 2
-
-// ViperConfig is the config struct used by the Viper config package
-// to parse the config file. This struct does not include regular expressions.
-// It is used as an intermediary to convert the Viper config to the Config struct.
-type ViperConfig struct {
- Title string
- Description string
- Extend Extend
- Rules []struct {
- ID string
- Description string
- Path string
- Regex string
- SecretGroup int
- Entropy float64
- Keywords []string
- Tags []string
-
- // Deprecated: this is a shim for backwards-compatibility.
- // TODO: Remove this in 9.x.
- AllowList *viperRuleAllowlist
- Allowlists []*viperRuleAllowlist
- }
- // Deprecated: this is a shim for backwards-compatibility.
- // TODO: Remove this in 9.x.
- AllowList *viperGlobalAllowlist
- Allowlists []*viperGlobalAllowlist
-}
-
-type viperRuleAllowlist struct {
- Description string
- Condition string
- Commits []string
- Paths []string
- RegexTarget string
- Regexes []string
- StopWords []string
-}
-
-type viperGlobalAllowlist struct {
- TargetRules []string
- viperRuleAllowlist `mapstructure:",squash"`
-}
-
-// Config is a configuration struct that contains rules and an allowlist if present.
-type Config struct {
- Title string
- Extend Extend
- Path string
- Description string
- Rules map[string]Rule
- Keywords map[string]struct{}
- // used to keep sarif results consistent
- OrderedRules []string
- Allowlists []*Allowlist
-}
-
-// Extend is a struct that allows users to define how they want their
-// configuration extended by other configuration files.
-type Extend struct {
- Path string
- URL string
- UseDefault bool
- DisabledRules []string
-}
-
-func (vc *ViperConfig) Translate() (Config, error) {
- var (
- keywords = make(map[string]struct{})
- orderedRules []string
- rulesMap = make(map[string]Rule)
- ruleAllowlists = make(map[string][]*Allowlist)
- )
-
- // Validate individual rules.
- for _, vr := range vc.Rules {
- var (
- pathPat *regexp.Regexp
- regexPat *regexp.Regexp
- )
- if vr.Path != "" {
- pathPat = regexp.MustCompile(vr.Path)
- }
- if vr.Regex != "" {
- regexPat = regexp.MustCompile(vr.Regex)
- }
- if vr.Keywords == nil {
- vr.Keywords = []string{}
- } else {
- for i, k := range vr.Keywords {
- keyword := strings.ToLower(k)
- keywords[keyword] = struct{}{}
- vr.Keywords[i] = keyword
- }
- }
- if vr.Tags == nil {
- vr.Tags = []string{}
- }
- cr := Rule{
- RuleID: vr.ID,
- Description: vr.Description,
- Regex: regexPat,
- SecretGroup: vr.SecretGroup,
- Entropy: vr.Entropy,
- Path: pathPat,
- Keywords: vr.Keywords,
- Tags: vr.Tags,
- }
-
- // Parse the rule allowlists, including the older format for backwards compatibility.
- if vr.AllowList != nil {
- // TODO: Remove this in v9.
- if len(vr.Allowlists) > 0 {
- return Config{}, fmt.Errorf("%s: [rules.allowlist] is deprecated, it cannot be used alongside [[rules.allowlist]]", cr.RuleID)
- }
- vr.Allowlists = append(vr.Allowlists, vr.AllowList)
- }
- for _, a := range vr.Allowlists {
- allowlist, err := parseAllowlist(a)
- if err != nil {
- return Config{}, fmt.Errorf("%s: [[rules.allowlists]] %w", cr.RuleID, err)
- }
- cr.Allowlists = append(cr.Allowlists, allowlist)
- }
- orderedRules = append(orderedRules, cr.RuleID)
- rulesMap[cr.RuleID] = cr
- }
-
- // Assemble the config.
- c := Config{
- Title: vc.Title,
- Description: vc.Description,
- Extend: vc.Extend,
- Rules: rulesMap,
- Keywords: keywords,
- OrderedRules: orderedRules,
- }
- // Parse the config allowlists, including the older format for backwards compatibility.
- if vc.AllowList != nil {
- // TODO: Remove this in v9.
- if len(vc.Allowlists) > 0 {
- return Config{}, errors.New("[allowlist] is deprecated, it cannot be used alongside [[allowlists]]")
- }
- vc.Allowlists = append(vc.Allowlists, vc.AllowList)
- }
- for _, a := range vc.Allowlists {
- allowlist, err := parseAllowlist(&a.viperRuleAllowlist)
- if err != nil {
- return Config{}, fmt.Errorf("[[allowlists]] %w", err)
- }
- // Allowlists with |targetRules| aren't added to the global list.
- if len(a.TargetRules) > 0 {
- for _, ruleID := range a.TargetRules {
- // It's not possible to validate |ruleID| until after extend.
- ruleAllowlists[ruleID] = append(ruleAllowlists[ruleID], allowlist)
- }
- } else {
- c.Allowlists = append(c.Allowlists, allowlist)
- }
- }
-
- if maxExtendDepth != extendDepth {
- // disallow both usedefault and path from being set
- if c.Extend.Path != "" && c.Extend.UseDefault {
- return Config{}, errors.New("unable to load config due to extend.path and extend.useDefault being set")
- }
- if c.Extend.UseDefault {
- if err := c.extendDefault(); err != nil {
- return Config{}, err
- }
- } else if c.Extend.Path != "" {
- if err := c.extendPath(); err != nil {
- return Config{}, err
- }
- }
- }
-
- // Validate the rules after everything has been assembled (including extended configs).
- if extendDepth == 0 {
- for _, rule := range c.Rules {
- if err := rule.Validate(); err != nil {
- return Config{}, err
- }
- }
-
- // Populate targeted configs.
- for ruleID, allowlists := range ruleAllowlists {
- rule, ok := c.Rules[ruleID]
- if !ok {
- return Config{}, fmt.Errorf("[[allowlists]] target rule ID '%s' does not exist", ruleID)
- }
- rule.Allowlists = append(rule.Allowlists, allowlists...)
- c.Rules[ruleID] = rule
- }
- }
-
- return c, nil
-}
-
-func parseAllowlist(a *viperRuleAllowlist) (*Allowlist, error) {
- var matchCondition AllowlistMatchCondition
- switch strings.ToUpper(a.Condition) {
- case "AND", "&&":
- matchCondition = AllowlistMatchAnd
- case "", "OR", "||":
- matchCondition = AllowlistMatchOr
- default:
- return nil, fmt.Errorf("unknown allowlist |condition| '%s' (expected 'and', 'or')", a.Condition)
- }
-
- // Validate the target.
- regexTarget := a.RegexTarget
- if regexTarget != "" {
- switch regexTarget {
- case "secret":
- regexTarget = ""
- case "match", "line":
- // do nothing
- default:
- return nil, fmt.Errorf("unknown allowlist |regexTarget| '%s' (expected 'match', 'line')", regexTarget)
- }
- }
- var allowlistRegexes []*regexp.Regexp
- for _, a := range a.Regexes {
- allowlistRegexes = append(allowlistRegexes, regexp.MustCompile(a))
- }
- var allowlistPaths []*regexp.Regexp
- for _, a := range a.Paths {
- allowlistPaths = append(allowlistPaths, regexp.MustCompile(a))
- }
-
- allowlist := &Allowlist{
- Description: a.Description,
- MatchCondition: matchCondition,
- Commits: a.Commits,
- Paths: allowlistPaths,
- RegexTarget: regexTarget,
- Regexes: allowlistRegexes,
- StopWords: a.StopWords,
- }
- if err := allowlist.Validate(); err != nil {
- return nil, err
- }
- return allowlist, nil
-}
-
-func (c *Config) GetOrderedRules() []Rule {
- var orderedRules []Rule
- for _, id := range c.OrderedRules {
- if _, ok := c.Rules[id]; ok {
- orderedRules = append(orderedRules, c.Rules[id])
- }
- }
- return orderedRules
-}
-
-func (c *Config) extendDefault() error {
- extendDepth++
- viper.SetConfigType("toml")
- if err := viper.ReadConfig(strings.NewReader(DefaultConfig)); err != nil {
- return fmt.Errorf("failed to load extended default config, err: %w", err)
- }
- defaultViperConfig := ViperConfig{}
- if err := viper.Unmarshal(&defaultViperConfig); err != nil {
- return fmt.Errorf("failed to load extended default config, err: %w", err)
- }
- cfg, err := defaultViperConfig.Translate()
- if err != nil {
- return fmt.Errorf("failed to load extended default config, err: %w", err)
-
- }
- logging.Debug().Msg("extending config with default config")
- c.extend(cfg)
- return nil
-}
-
-func (c *Config) extendPath() error {
- extendDepth++
- viper.SetConfigFile(c.Extend.Path)
- if err := viper.ReadInConfig(); err != nil {
- return fmt.Errorf("failed to load extended config, err: %w", err)
- }
- extensionViperConfig := ViperConfig{}
- if err := viper.Unmarshal(&extensionViperConfig); err != nil {
- return fmt.Errorf("failed to load extended config, err: %w", err)
- }
- cfg, err := extensionViperConfig.Translate()
- if err != nil {
- return fmt.Errorf("failed to load extended config, err: %w", err)
- }
- logging.Debug().Msgf("extending config with %s", c.Extend.Path)
- c.extend(cfg)
- return nil
-}
-
-func (c *Config) extendURL() {
- // TODO
-}
-
-func (c *Config) extend(extensionConfig Config) {
- // Get config name for helpful log messages.
- var configName string
- if c.Extend.Path != "" {
- configName = c.Extend.Path
- } else {
- configName = "default"
- }
- // Convert |Config.DisabledRules| into a map for ease of access.
- disabledRuleIDs := map[string]struct{}{}
- for _, id := range c.Extend.DisabledRules {
- if _, ok := extensionConfig.Rules[id]; !ok {
- logging.Warn().
- Str("rule-id", id).
- Str("config", configName).
- Msg("Disabled rule doesn't exist in extended config.")
- }
- disabledRuleIDs[id] = struct{}{}
- }
-
- for ruleID, baseRule := range extensionConfig.Rules {
- // Skip the rule.
- if _, ok := disabledRuleIDs[ruleID]; ok {
- logging.Debug().
- Str("rule-id", ruleID).
- Str("config", configName).
- Msg("Ignoring rule from extended config.")
- continue
- }
-
- currentRule, ok := c.Rules[ruleID]
- if !ok {
- // Rule doesn't exist, add it to the config.
- c.Rules[ruleID] = baseRule
- for _, k := range baseRule.Keywords {
- c.Keywords[k] = struct{}{}
- }
- c.OrderedRules = append(c.OrderedRules, ruleID)
- } else {
- // Rule exists, merge our changes into the base.
- if currentRule.Description != "" {
- baseRule.Description = currentRule.Description
- }
- if currentRule.Entropy != 0 {
- baseRule.Entropy = currentRule.Entropy
- }
- if currentRule.SecretGroup != 0 {
- baseRule.SecretGroup = currentRule.SecretGroup
- }
- if currentRule.Regex != nil {
- baseRule.Regex = currentRule.Regex
- }
- if currentRule.Path != nil {
- baseRule.Path = currentRule.Path
- }
- baseRule.Tags = append(baseRule.Tags, currentRule.Tags...)
- baseRule.Keywords = append(baseRule.Keywords, currentRule.Keywords...)
- for _, a := range currentRule.Allowlists {
- baseRule.Allowlists = append(baseRule.Allowlists, a)
- }
- // The keywords from the base rule and the extended rule must be merged into the global keywords list
- for _, k := range baseRule.Keywords {
- c.Keywords[k] = struct{}{}
- }
- c.Rules[ruleID] = baseRule
- }
- }
-
- // append allowlists, not attempting to merge
- for _, a := range extensionConfig.Allowlists {
- c.Allowlists = append(c.Allowlists, a)
- }
-
- // sort to keep extended rules in order
- sort.Strings(c.OrderedRules)
-}
diff --git a/cli/detect/config/gitleaks.toml b/cli/detect/config/gitleaks.toml
deleted file mode 100644
index 92a06a319..000000000
--- a/cli/detect/config/gitleaks.toml
+++ /dev/null
@@ -1,3130 +0,0 @@
-# This file has been auto-generated. Do not edit manually.
-# If you would like to contribute new rules, please use
-# cmd/generate/config/main.go and follow the contributing guidelines
-# at https://github.com/gitleaks/gitleaks/blob/master/CONTRIBUTING.md
-#
-# How the hell does secret scanning work? Read this:
-# https://lookingatcomputer.substack.com/p/regex-is-almost-all-you-need
-#
-# This is the default gitleaks configuration file.
-# Rules and allowlists are defined within this file.
-# Rules instruct gitleaks on what should be considered a secret.
-# Allowlists instruct gitleaks on what is allowed, i.e. not a secret.
-
-title = "gitleaks config"
-
-# TODO: change to [[allowlists]]
-[allowlist]
-description = "global allow lists"
-paths = [
- '''gitleaks\.toml''',
- '''(?i)\.(?:bmp|gif|jpe?g|png|svg|tiff?)$''',
- '''(?i)\.(?:eot|[ot]tf|woff2?)$''',
- '''(?i)\.(?:docx?|xlsx?|pdf|bin|socket|vsidx|v2|suo|wsuo|.dll|pdb|exe|gltf|zip)$''',
- '''go\.(?:mod|sum|work(?:\.sum)?)$''',
- '''(?:^|/)vendor/modules\.txt$''',
- '''(?:^|/)vendor/(?:github\.com|golang\.org/x|google\.golang\.org|gopkg\.in|istio\.io|k8s\.io|sigs\.k8s\.io)(?:/.*)?$''',
- '''(?:^|/)gradlew(?:\.bat)?$''',
- '''(?:^|/)gradle\.lockfile$''',
- '''(?:^|/)mvnw(?:\.cmd)?$''',
- '''(?:^|/)\.mvn/wrapper/MavenWrapperDownloader\.java$''',
- '''(?:^|/)node_modules(?:/.*)?$''',
- '''(?:^|/)(?:deno\.lock|npm-shrinkwrap\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$''',
- '''(?:^|/)bower_components(?:/.*)?$''',
- '''(?:^|/)(?:angular|bootstrap|jquery(?:-?ui)?|plotly|swagger-?ui)[a-zA-Z0-9.-]*(?:\.min)?\.js(?:\.map)?$''',
- '''(?:^|/)javascript\.json$''',
- '''(?:^|/)(?:Pipfile|poetry)\.lock$''',
- '''(?i)(?:^|/)(?:v?env|virtualenv)/lib(?:64)?(?:/.*)?$''',
- '''(?i)(?:^|/)(?:lib(?:64)?/python[23](?:\.\d{1,2})+|python/[23](?:\.\d{1,2})+/lib(?:64)?)(?:/.*)?$''',
- '''(?i)(?:^|/)[a-z0-9_.]+-[0-9.]+\.dist-info(?:/.+)?$''',
- '''(?:^|/)vendor/(?:bundle|ruby)(?:/.*?)?$''',
- '''\.gem$''',
- '''verification-metadata\.xml''',
- '''Database.refactorlog''',
-]
-regexes = [
- '''(?i)^true|false|null$''',
- '''^(?i:a+|b+|c+|d+|e+|f+|g+|h+|i+|j+|k+|l+|m+|n+|o+|p+|q+|r+|s+|t+|u+|v+|w+|x+|y+|z+|\*+|\.+)$''',
- '''^\$(?:\d+|{\d+})$''',
- '''^\$(?:[A-Z_]+|[a-z_]+)$''',
- '''^\${(?:[A-Z_]+|[a-z_]+)}$''',
- '''^\{\{[ \t]*[\w ().|]+[ \t]*}}$''',
- '''^\$\{\{[ \t]*(?:(?:env|github|secrets|vars)(?:\.[A-Za-z]\w+)+[\w "'&./=|]*)[ \t]*}}$''',
- '''^%(?:[A-Z_]+|[a-z_]+)%$''',
- '''^%[+\-# 0]?[bcdeEfFgGoOpqstTUvxX]$''',
- '''^\{\d{0,2}}$''',
- '''^@(?:[A-Z_]+|[a-z_]+)@$''',
- '''^/Users/(?i)[a-z0-9]+/[\w .-/]+$''',
- '''^/(?:bin|etc|home|opt|tmp|usr|var)/[\w ./-]+$''',
-]
-stopwords = [
- "abcdefghijklmnopqrstuvwxyz",
- "014df517-39d1-4453-b7b3-9930c563627c",
-]
-
-[[rules]]
-id = "1password-secret-key"
-description = "Uncovered a possible 1Password secret key, potentially compromising access to secrets in vaults."
-regex = '''\bA3-[A-Z0-9]{6}-(?:(?:[A-Z0-9]{11})|(?:[A-Z0-9]{6}-[A-Z0-9]{5}))-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}\b'''
-entropy = 3.8
-keywords = ["a3-"]
-
-[[rules]]
-id = "1password-service-account-token"
-description = "Uncovered a possible 1Password service account token, potentially compromising access to secrets in vaults."
-regex = '''ops_eyJ[a-zA-Z0-9+/]{250,}={0,3}'''
-entropy = 4
-keywords = ["ops_"]
-
-[[rules]]
-id = "adafruit-api-key"
-description = "Identified a potential Adafruit API Key, which could lead to unauthorized access to Adafruit services and sensitive data exposure."
-regex = '''(?i)[\w.-]{0,50}?(?:adafruit)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["adafruit"]
-
-[[rules]]
-id = "adobe-client-id"
-description = "Detected a pattern that resembles an Adobe OAuth Web Client ID, posing a risk of compromised Adobe integrations and data breaches."
-regex = '''(?i)[\w.-]{0,50}?(?:adobe)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = ["adobe"]
-
-[[rules]]
-id = "adobe-client-secret"
-description = "Discovered a potential Adobe Client Secret, which, if exposed, could allow unauthorized Adobe service access and data manipulation."
-regex = '''\b(p8e-(?i)[a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = ["p8e-"]
-
-[[rules]]
-id = "age-secret-key"
-description = "Discovered a potential Age encryption tool secret key, risking data decryption and unauthorized access to sensitive information."
-regex = '''AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}'''
-keywords = ["age-secret-key-1"]
-
-[[rules]]
-id = "airtable-api-key"
-description = "Uncovered a possible Airtable API Key, potentially compromising database access and leading to data leakage or alteration."
-regex = '''(?i)[\w.-]{0,50}?(?:airtable)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{17})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["airtable"]
-
-[[rules]]
-id = "algolia-api-key"
-description = "Identified an Algolia API Key, which could result in unauthorized search operations and data exposure on Algolia-managed platforms."
-regex = '''(?i)[\w.-]{0,50}?(?:algolia)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["algolia"]
-
-[[rules]]
-id = "alibaba-access-key-id"
-description = "Detected an Alibaba Cloud AccessKey ID, posing a risk of unauthorized cloud resource access and potential data compromise."
-regex = '''\b(LTAI(?i)[a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = ["ltai"]
-
-[[rules]]
-id = "alibaba-secret-key"
-description = "Discovered a potential Alibaba Cloud Secret Key, potentially allowing unauthorized operations and data access within Alibaba Cloud."
-regex = '''(?i)[\w.-]{0,50}?(?:alibaba)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{30})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = ["alibaba"]
-
-[[rules]]
-id = "asana-client-id"
-description = "Discovered a potential Asana Client ID, risking unauthorized access to Asana projects and sensitive task information."
-regex = '''(?i)[\w.-]{0,50}?(?:asana)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["asana"]
-
-[[rules]]
-id = "asana-client-secret"
-description = "Identified an Asana Client Secret, which could lead to compromised project management integrity and unauthorized access."
-regex = '''(?i)[\w.-]{0,50}?(?:asana)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["asana"]
-
-[[rules]]
-id = "atlassian-api-token"
-description = "Detected an Atlassian API token, posing a threat to project management and collaboration tool security and data confidentiality."
-regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:atlassian|confluence|jira)(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-zA-Z0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)|\b(ATATT3[A-Za-z0-9_\-=]{186})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3.5
-keywords = [
- "atlassian",
- "confluence",
- "jira",
- "atatt3",
-]
-
-[[rules]]
-id = "authress-service-client-access-key"
-description = "Uncovered a possible Authress Service Client Access Key, which may compromise access control services and sensitive data."
-regex = '''\b((?:sc|ext|scauth|authress)_(?i)[a-z0-9]{5,30}\.[a-z0-9]{4,6}\.(?-i:acc)[_-][a-z0-9-]{10,32}\.[a-z0-9+/_=-]{30,120})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = [
- "sc_",
- "ext_",
- "scauth_",
- "authress_",
-]
-
-[[rules]]
-id = "aws-access-token"
-description = "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms."
-regex = '''\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16})\b'''
-entropy = 3
-keywords = [
- "a3t",
- "akia",
- "asia",
- "abia",
- "acca",
-]
-[[rules.allowlists]]
-regexes = [
- '''.+EXAMPLE$''',
-]
-
-[[rules]]
-id = "azure-ad-client-secret"
-description = "Azure AD Client Secret"
-regex = '''(?:^|[\\'"\x60\s>=:(,)])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\\'"\x60\s<),])'''
-entropy = 3
-keywords = ["q~"]
-
-[[rules]]
-id = "beamer-api-token"
-description = "Detected a Beamer API token, potentially compromising content management and exposing sensitive notifications and updates."
-regex = '''(?i)[\w.-]{0,50}?(?:beamer)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(b_[a-z0-9=_\-]{44})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["beamer"]
-
-[[rules]]
-id = "bitbucket-client-id"
-description = "Discovered a potential Bitbucket Client ID, risking unauthorized repository access and potential codebase exposure."
-regex = '''(?i)[\w.-]{0,50}?(?:bitbucket)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["bitbucket"]
-
-[[rules]]
-id = "bitbucket-client-secret"
-description = "Discovered a potential Bitbucket Client Secret, posing a risk of compromised code repositories and unauthorized access."
-regex = '''(?i)[\w.-]{0,50}?(?:bitbucket)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{64})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["bitbucket"]
-
-[[rules]]
-id = "bittrex-access-key"
-description = "Identified a Bittrex Access Key, which could lead to unauthorized access to cryptocurrency trading accounts and financial loss."
-regex = '''(?i)[\w.-]{0,50}?(?:bittrex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["bittrex"]
-
-[[rules]]
-id = "bittrex-secret-key"
-description = "Detected a Bittrex Secret Key, potentially compromising cryptocurrency transactions and financial security."
-regex = '''(?i)[\w.-]{0,50}?(?:bittrex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["bittrex"]
-
-[[rules]]
-id = "cisco-meraki-api-key"
-description = "Cisco Meraki is a cloud-managed IT solution that provides networking, security, and device management through an easy-to-use interface."
-regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:(?-i:[Mm]eraki|MERAKI))(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{40})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["meraki"]
-
-[[rules]]
-id = "clickhouse-cloud-api-secret-key"
-description = "Identified a pattern that may indicate clickhouse cloud API secret key, risking unauthorized clickhouse cloud api access and data breaches on ClickHouse Cloud platforms."
-regex = '''\b(4b1d[A-Za-z0-9]{38})\b'''
-entropy = 3
-keywords = ["4b1d"]
-
-[[rules]]
-id = "clojars-api-token"
-description = "Uncovered a possible Clojars API token, risking unauthorized access to Clojure libraries and potential code manipulation."
-regex = '''(?i)CLOJARS_[a-z0-9]{60}'''
-entropy = 2
-keywords = ["clojars_"]
-
-[[rules]]
-id = "cloudflare-api-key"
-description = "Detected a Cloudflare API Key, potentially compromising cloud application deployments and operational security."
-regex = '''(?i)[\w.-]{0,50}?(?:cloudflare)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{40})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = ["cloudflare"]
-
-[[rules]]
-id = "cloudflare-global-api-key"
-description = "Detected a Cloudflare Global API Key, potentially compromising cloud application deployments and operational security."
-regex = '''(?i)[\w.-]{0,50}?(?:cloudflare)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{37})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = ["cloudflare"]
-
-[[rules]]
-id = "cloudflare-origin-ca-key"
-description = "Detected a Cloudflare Origin CA Key, potentially compromising cloud application deployments and operational security."
-regex = '''\b(v1\.0-[a-f0-9]{24}-[a-f0-9]{146})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = [
- "cloudflare",
- "v1.0-",
-]
-
-[[rules]]
-id = "codecov-access-token"
-description = "Found a pattern resembling a Codecov Access Token, posing a risk of unauthorized access to code coverage reports and sensitive data."
-regex = '''(?i)[\w.-]{0,50}?(?:codecov)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["codecov"]
-
-[[rules]]
-id = "cohere-api-token"
-description = "Identified a Cohere Token, posing a risk of unauthorized access to AI services and data manipulation."
-regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:cohere|CO_API_KEY)(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-zA-Z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 4
-keywords = [
- "cohere",
- "co_api_key",
-]
-
-[[rules]]
-id = "coinbase-access-token"
-description = "Detected a Coinbase Access Token, posing a risk of unauthorized access to cryptocurrency accounts and financial transactions."
-regex = '''(?i)[\w.-]{0,50}?(?:coinbase)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{64})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["coinbase"]
-
-[[rules]]
-id = "confluent-access-token"
-description = "Identified a Confluent Access Token, which could compromise access to streaming data platforms and sensitive data flow."
-regex = '''(?i)[\w.-]{0,50}?(?:confluent)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["confluent"]
-
-[[rules]]
-id = "confluent-secret-key"
-description = "Found a Confluent Secret Key, potentially risking unauthorized operations and data access within Confluent services."
-regex = '''(?i)[\w.-]{0,50}?(?:confluent)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["confluent"]
-
-[[rules]]
-id = "contentful-delivery-api-token"
-description = "Discovered a Contentful delivery API token, posing a risk to content management systems and data integrity."
-regex = '''(?i)[\w.-]{0,50}?(?:contentful)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{43})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["contentful"]
-
-[[rules]]
-id = "curl-auth-header"
-description = "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource."
-regex = '''\bcurl\b(?:.*?|.*?(?:[\r\n]{1,2}.*?){1,5})[ \t\n\r](?:-H|--header)(?:=|[ \t]{0,5})(?:"(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))"|'(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))')(?:\B|\s|\z)'''
-entropy = 2.75
-keywords = ["curl"]
-
-[[rules]]
-id = "curl-auth-user"
-description = "Discovered a potential basic authorization token provided in a curl command, which could compromise the curl accessed resource."
-regex = '''\bcurl\b(?:.*|.*(?:[\r\n]{1,2}.*){1,5})[ \t\n\r](?:-u|--user)(?:=|[ \t]{0,5})("(:[^"]{3,}|[^:"]{3,}:|[^:"]{3,}:[^"]{3,})"|'([^:']{3,}:[^']{3,})'|((?:"[^"]{3,}"|'[^']{3,}'|[\w$@.-]+):(?:"[^"]{3,}"|'[^']{3,}'|[\w${}@.-]+)))(?:\s|\z)'''
-entropy = 2
-keywords = ["curl"]
-[[rules.allowlists]]
-regexes = [
- '''[^:]+:(?:change(?:it|me)|pass(?:word)?|pwd|test|token|\*+|x+)''',
- '''['"]?<[^>]+>['"]?:['"]?<[^>]+>|<[^:]+:[^>]+>['"]?''',
- '''[^:]+:\[[^]]+]''',
- '''['"]?[^:]+['"]?:['"]?\$(?:\d|\w+|\{(?:\d|\w+)})['"]?''',
- '''\$\([^)]+\):\$\([^)]+\)''',
- '''['"]?\$?{{[^}]+}}['"]?:['"]?\$?{{[^}]+}}['"]?''',
-]
-
-[[rules]]
-id = "databricks-api-token"
-description = "Uncovered a Databricks API token, which may compromise big data analytics platforms and sensitive data processing."
-regex = '''\b(dapi[a-f0-9]{32}(?:-\d)?)(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["dapi"]
-
-[[rules]]
-id = "datadog-access-token"
-description = "Detected a Datadog Access Token, potentially risking monitoring and analytics data exposure and manipulation."
-regex = '''(?i)[\w.-]{0,50}?(?:datadog)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["datadog"]
-
-[[rules]]
-id = "defined-networking-api-token"
-description = "Identified a Defined Networking API token, which could lead to unauthorized network operations and data breaches."
-regex = '''(?i)[\w.-]{0,50}?(?:dnkey)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(dnkey-[a-z0-9=_\-]{26}-[a-z0-9=_\-]{52})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["dnkey"]
-
-[[rules]]
-id = "digitalocean-access-token"
-description = "Found a DigitalOcean OAuth Access Token, risking unauthorized cloud resource access and data compromise."
-regex = '''\b(doo_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["doo_v1_"]
-
-[[rules]]
-id = "digitalocean-pat"
-description = "Discovered a DigitalOcean Personal Access Token, posing a threat to cloud infrastructure security and data privacy."
-regex = '''\b(dop_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["dop_v1_"]
-
-[[rules]]
-id = "digitalocean-refresh-token"
-description = "Uncovered a DigitalOcean OAuth Refresh Token, which could allow prolonged unauthorized access and resource manipulation."
-regex = '''(?i)\b(dor_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["dor_v1_"]
-
-[[rules]]
-id = "discord-api-token"
-description = "Detected a Discord API key, potentially compromising communication channels and user data privacy on Discord."
-regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["discord"]
-
-[[rules]]
-id = "discord-client-id"
-description = "Identified a Discord client ID, which may lead to unauthorized integrations and data exposure in Discord applications."
-regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{18})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = ["discord"]
-
-[[rules]]
-id = "discord-client-secret"
-description = "Discovered a potential Discord client secret, risking compromised Discord bot integrations and data leaks."
-regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = ["discord"]
-
-[[rules]]
-id = "doppler-api-token"
-description = "Discovered a Doppler API token, posing a risk to environment and secrets management security."
-regex = '''dp\.pt\.(?i)[a-z0-9]{43}'''
-entropy = 2
-keywords = ["dp.pt."]
-
-[[rules]]
-id = "droneci-access-token"
-description = "Detected a Droneci Access Token, potentially compromising continuous integration and deployment workflows."
-regex = '''(?i)[\w.-]{0,50}?(?:droneci)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["droneci"]
-
-[[rules]]
-id = "dropbox-api-token"
-description = "Identified a Dropbox API secret, which could lead to unauthorized file access and data breaches in Dropbox storage."
-regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{15})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["dropbox"]
-
-[[rules]]
-id = "dropbox-long-lived-api-token"
-description = "Found a Dropbox long-lived API token, risking prolonged unauthorized access to cloud storage and sensitive data."
-regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["dropbox"]
-
-[[rules]]
-id = "dropbox-short-lived-api-token"
-description = "Discovered a Dropbox short-lived API token, posing a risk of temporary but potentially harmful data access and manipulation."
-regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(sl\.[a-z0-9\-=_]{135})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["dropbox"]
-
-[[rules]]
-id = "duffel-api-token"
-description = "Uncovered a Duffel API token, which may compromise travel platform integrations and sensitive customer data."
-regex = '''duffel_(?:test|live)_(?i)[a-z0-9_\-=]{43}'''
-entropy = 2
-keywords = ["duffel_"]
-
-[[rules]]
-id = "dynatrace-api-token"
-description = "Detected a Dynatrace API token, potentially risking application performance monitoring and data exposure."
-regex = '''dt0c01\.(?i)[a-z0-9]{24}\.[a-z0-9]{64}'''
-entropy = 4
-keywords = ["dt0c01."]
-
-[[rules]]
-id = "easypost-api-token"
-description = "Identified an EasyPost API token, which could lead to unauthorized postal and shipment service access and data exposure."
-regex = '''\bEZAK(?i)[a-z0-9]{54}\b'''
-entropy = 2
-keywords = ["ezak"]
-
-[[rules]]
-id = "easypost-test-api-token"
-description = "Detected an EasyPost test API token, risking exposure of test environments and potentially sensitive shipment data."
-regex = '''\bEZTK(?i)[a-z0-9]{54}\b'''
-entropy = 2
-keywords = ["eztk"]
-
-[[rules]]
-id = "etsy-access-token"
-description = "Found an Etsy Access Token, potentially compromising Etsy shop management and customer data."
-regex = '''(?i)[\w.-]{0,50}?(?:(?-i:ETSY|[Ee]tsy))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["etsy"]
-
-[[rules]]
-id = "facebook-access-token"
-description = "Discovered a Facebook Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure."
-regex = '''(?i)\b(\d{15,16}(\||%)[0-9a-z\-_]{27,40})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["facebook"]
-
-[[rules]]
-id = "facebook-page-access-token"
-description = "Discovered a Facebook Page Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure."
-regex = '''\b(EAA[MC](?i)[a-z0-9]{100,})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 4
-keywords = [
- "eaam",
- "eaac",
-]
-
-[[rules]]
-id = "facebook-secret"
-description = "Discovered a Facebook Application secret, posing a risk of unauthorized access to Facebook accounts and personal data exposure."
-regex = '''(?i)[\w.-]{0,50}?(?:facebook)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["facebook"]
-
-[[rules]]
-id = "fastly-api-token"
-description = "Uncovered a Fastly API key, which may compromise CDN and edge cloud services, leading to content delivery and security issues."
-regex = '''(?i)[\w.-]{0,50}?(?:fastly)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["fastly"]
-
-[[rules]]
-id = "finicity-api-token"
-description = "Detected a Finicity API token, potentially risking financial data access and unauthorized financial operations."
-regex = '''(?i)[\w.-]{0,50}?(?:finicity)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["finicity"]
-
-[[rules]]
-id = "finicity-client-secret"
-description = "Identified a Finicity Client Secret, which could lead to compromised financial service integrations and data breaches."
-regex = '''(?i)[\w.-]{0,50}?(?:finicity)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["finicity"]
-
-[[rules]]
-id = "finnhub-access-token"
-description = "Found a Finnhub Access Token, risking unauthorized access to financial market data and analytics."
-regex = '''(?i)[\w.-]{0,50}?(?:finnhub)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["finnhub"]
-
-[[rules]]
-id = "flickr-access-token"
-description = "Discovered a Flickr Access Token, posing a risk of unauthorized photo management and potential data leakage."
-regex = '''(?i)[\w.-]{0,50}?(?:flickr)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["flickr"]
-
-[[rules]]
-id = "flutterwave-encryption-key"
-description = "Uncovered a Flutterwave Encryption Key, which may compromise payment processing and sensitive financial information."
-regex = '''FLWSECK_TEST-(?i)[a-h0-9]{12}'''
-entropy = 2
-keywords = ["flwseck_test"]
-
-[[rules]]
-id = "flutterwave-public-key"
-description = "Detected a Finicity Public Key, potentially exposing public cryptographic operations and integrations."
-regex = '''FLWPUBK_TEST-(?i)[a-h0-9]{32}-X'''
-entropy = 2
-keywords = ["flwpubk_test"]
-
-[[rules]]
-id = "flutterwave-secret-key"
-description = "Identified a Flutterwave Secret Key, risking unauthorized financial transactions and data breaches."
-regex = '''FLWSECK_TEST-(?i)[a-h0-9]{32}-X'''
-entropy = 2
-keywords = ["flwseck_test"]
-
-[[rules]]
-id = "flyio-access-token"
-description = "Uncovered a Fly.io API key"
-regex = '''\b((?:fo1_[\w-]{43}|fm1[ar]_[a-zA-Z0-9+\/]{100,}={0,3}|fm2_[a-zA-Z0-9+\/]{100,}={0,3}))(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 4
-keywords = [
- "fo1_",
- "fm1",
- "fm2_",
-]
-
-[[rules]]
-id = "frameio-api-token"
-description = "Found a Frame.io API token, potentially compromising video collaboration and project management."
-regex = '''fio-u-(?i)[a-z0-9\-_=]{64}'''
-keywords = ["fio-u-"]
-
-[[rules]]
-id = "freemius-secret-key"
-description = "Detected a Freemius secret key, potentially exposing sensitive information."
-regex = '''(?i)["']secret_key["']\s*=>\s*["'](sk_[\S]{29})["']'''
-path = '''(?i)\.php$'''
-keywords = ["secret_key"]
-
-[[rules]]
-id = "freshbooks-access-token"
-description = "Discovered a Freshbooks Access Token, posing a risk to accounting software access and sensitive financial data exposure."
-regex = '''(?i)[\w.-]{0,50}?(?:freshbooks)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["freshbooks"]
-
-[[rules]]
-id = "gcp-api-key"
-description = "Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches."
-regex = '''\b(AIza[\w-]{35})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 4
-keywords = ["aiza"]
-[[rules.allowlists]]
-regexes = [
- '''AIzaSyabcdefghijklmnopqrstuvwxyz1234567''',
- '''AIzaSyAnLA7NfeLquW1tJFpx_eQCxoX-oo6YyIs''',
- '''AIzaSyCkEhVjf3pduRDt6d1yKOMitrUEke8agEM''',
- '''AIzaSyDMAScliyLx7F0NPDEJi1QmyCgHIAODrlU''',
- '''AIzaSyD3asb-2pEZVqMkmL6M9N6nHZRR_znhrh0''',
- '''AIzayDNSXIbFmlXbIE6mCzDLQAqITYefhixbX4A''',
- '''AIzaSyAdOS2zB6NCsk1pCdZ4-P6GBdi_UUPwX7c''',
- '''AIzaSyASWm6HmTMdYWpgMnjRBjxcQ9CKctWmLd4''',
- '''AIzaSyANUvH9H9BsUccjsu2pCmEkOPjjaXeDQgY''',
- '''AIzaSyA5_iVawFQ8ABuTZNUdcwERLJv_a_p4wtM''',
- '''AIzaSyA4UrcGxgwQFTfaI3no3t7Lt1sjmdnP5sQ''',
- '''AIzaSyDSb51JiIcB6OJpwwMicseKRhhrOq1cS7g''',
- '''AIzaSyBF2RrAIm4a0mO64EShQfqfd2AFnzAvvuU''',
- '''AIzaSyBcE-OOIbhjyR83gm4r2MFCu4MJmprNXsw''',
- '''AIzaSyB8qGxt4ec15vitgn44duC5ucxaOi4FmqE''',
- '''AIzaSyA8vmApnrHNFE0bApF4hoZ11srVL_n0nvY''',
-]
-
-[[rules]]
-id = "generic-api-key"
-description = "Detected a Generic API Key, potentially exposing access to various services and sensitive operations."
-regex = '''(?i)[\w.-]{0,50}?(?:access|auth|(?-i:[Aa]pi|API)|credential|creds|key|passw(?:or)?d|secret|token)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3.5
-keywords = [
- "access",
- "api",
- "auth",
- "key",
- "credential",
- "creds",
- "passwd",
- "password",
- "secret",
- "token",
-]
-[[rules.allowlists]]
-regexes = [
- '''^[a-zA-Z_.-]+$''',
-]
-[[rules.allowlists]]
-description = "Allowlist for Generic API Keys"
-regexTarget = "match"
-regexes = [
- '''(?i)(?:access(?:ibility|or)|access[_.-]?id|random[_.-]?access|api[_.-]?(?:id|name|version)|rapid|capital|[a-z0-9-]*?api[a-z0-9-]*?:jar:|author|X-MS-Exchange-Organization-Auth|Authentication-Results|(?:credentials?[_.-]?id|withCredentials)|(?:bucket|foreign|hot|idx|natural|primary|pub(?:lic)?|schema|sequence)[_.-]?key|(?:turkey)|key[_.-]?(?:alias|board|code|frame|id|length|mesh|name|pair|press(?:ed)?|ring|selector|signature|size|stone|storetype|word|up|down|left|right)|key[_.-]?vault[_.-]?(?:id|name)|keyVaultToStoreSecrets|key(?:store|tab)[_.-]?(?:file|path)|issuerkeyhash|(?-i:[DdMm]onkey|[DM]ONKEY)|keying|(?:secret)[_.-]?(?:length|name|size)|UserSecretsId|(?:csrf)[_.-]?token|(?:io\.jsonwebtoken[ \t]?:[ \t]?[\w-]+)|(?:api|credentials|token)[_.-]?(?:endpoint|ur[il])|public[_.-]?token|(?:key|token)[_.-]?file|(?-i:(?:[A-Z_]+=\n[A-Z_]+=|[a-z_]+=\n[a-z_]+=)(?:\n|\z))|(?-i:(?:[A-Z.]+=\n[A-Z.]+=|[a-z.]+=\n[a-z.]+=)(?:\n|\z)))''',
-]
-stopwords = [
- "000000",
- "6fe4476ee5a1832882e326b506d14126",
- "_ec2_",
- "aaaaaa",
- "about",
- "abstract",
- "academy",
- "acces",
- "account",
- "act-",
- "act.",
- "act_",
- "action",
- "active",
- "actively",
- "activity",
- "adapter",
- "add-",
- "add-on",
- "add.",
- "add_",
- "addon",
- "addres",
- "admin",
- "adobe",
- "advanced",
- "adventure",
- "agent",
- "agile",
- "air-",
- "air.",
- "air_",
- "ajax",
- "akka",
- "alert",
- "alfred",
- "algorithm",
- "all-",
- "all.",
- "all_",
- "alloy",
- "alpha",
- "amazon",
- "amqp",
- "analysi",
- "analytic",
- "analyzer",
- "android",
- "angular",
- "angularj",
- "animate",
- "animation",
- "another",
- "ansible",
- "answer",
- "ant-",
- "ant.",
- "ant_",
- "any-",
- "any.",
- "any_",
- "apache",
- "app-",
- "app.",
- "app_",
- "apple",
- "arch",
- "archive",
- "archived",
- "arduino",
- "array",
- "art-",
- "art.",
- "art_",
- "article",
- "asp-",
- "asp.",
- "asp_",
- "asset",
- "async",
- "atom",
- "attention",
- "audio",
- "audit",
- "aura",
- "auth",
- "author",
- "authorize",
- "auto",
- "automated",
- "automatic",
- "awesome",
- "aws_",
- "azure",
- "back",
- "backbone",
- "backend",
- "backup",
- "bar-",
- "bar.",
- "bar_",
- "base",
- "based",
- "bash",
- "basic",
- "batch",
- "been",
- "beer",
- "behavior",
- "being",
- "benchmark",
- "best",
- "beta",
- "better",
- "big-",
- "big.",
- "big_",
- "binary",
- "binding",
- "bit-",
- "bit.",
- "bit_",
- "bitcoin",
- "block",
- "blog",
- "board",
- "book",
- "bookmark",
- "boost",
- "boot",
- "bootstrap",
- "bosh",
- "bot-",
- "bot.",
- "bot_",
- "bower",
- "box-",
- "box.",
- "box_",
- "boxen",
- "bracket",
- "branch",
- "bridge",
- "browser",
- "brunch",
- "buffer",
- "bug-",
- "bug.",
- "bug_",
- "build",
- "builder",
- "building",
- "buildout",
- "buildpack",
- "built",
- "bundle",
- "busines",
- "but-",
- "but.",
- "but_",
- "button",
- "cache",
- "caching",
- "cakephp",
- "calendar",
- "call",
- "camera",
- "campfire",
- "can-",
- "can.",
- "can_",
- "canva",
- "captcha",
- "capture",
- "card",
- "carousel",
- "case",
- "cassandra",
- "cat-",
- "cat.",
- "cat_",
- "category",
- "center",
- "cento",
- "challenge",
- "change",
- "changelog",
- "channel",
- "chart",
- "chat",
- "cheat",
- "check",
- "checker",
- "chef",
- "ches",
- "chinese",
- "chosen",
- "chrome",
- "ckeditor",
- "clas",
- "classe",
- "classic",
- "clean",
- "cli-",
- "cli.",
- "cli_",
- "client",
- "clojure",
- "clone",
- "closure",
- "cloud",
- "club",
- "cluster",
- "cms-",
- "cms_",
- "coco",
- "code",
- "coding",
- "coffee",
- "color",
- "combination",
- "combo",
- "command",
- "commander",
- "comment",
- "commit",
- "common",
- "community",
- "compas",
- "compiler",
- "complete",
- "component",
- "composer",
- "computer",
- "computing",
- "con-",
- "con.",
- "con_",
- "concept",
- "conf",
- "config",
- "connect",
- "connector",
- "console",
- "contact",
- "container",
- "contao",
- "content",
- "contest",
- "context",
- "control",
- "convert",
- "converter",
- "conway'",
- "cookbook",
- "cookie",
- "cool",
- "copy",
- "cordova",
- "core",
- "couchbase",
- "couchdb",
- "countdown",
- "counter",
- "course",
- "craft",
- "crawler",
- "create",
- "creating",
- "creator",
- "credential",
- "crm-",
- "crm.",
- "crm_",
- "cros",
- "crud",
- "csv-",
- "csv.",
- "csv_",
- "cube",
- "cucumber",
- "cuda",
- "current",
- "currently",
- "custom",
- "daemon",
- "dark",
- "dart",
- "dash",
- "dashboard",
- "data",
- "database",
- "date",
- "day-",
- "day.",
- "day_",
- "dead",
- "debian",
- "debug",
- "debugger",
- "deck",
- "define",
- "del-",
- "del.",
- "del_",
- "delete",
- "demo",
- "deploy",
- "design",
- "designer",
- "desktop",
- "detection",
- "detector",
- "dev-",
- "dev.",
- "dev_",
- "develop",
- "developer",
- "device",
- "devise",
- "diff",
- "digital",
- "directive",
- "directory",
- "discovery",
- "display",
- "django",
- "dns-",
- "dns_",
- "doc-",
- "doc.",
- "doc_",
- "docker",
- "docpad",
- "doctrine",
- "document",
- "doe-",
- "doe.",
- "doe_",
- "dojo",
- "dom-",
- "dom.",
- "dom_",
- "domain",
- "don't",
- "done",
- "dot-",
- "dot.",
- "dot_",
- "dotfile",
- "download",
- "draft",
- "drag",
- "drill",
- "drive",
- "driven",
- "driver",
- "drop",
- "dropbox",
- "drupal",
- "dsl-",
- "dsl.",
- "dsl_",
- "dynamic",
- "easy",
- "ecdsa",
- "eclipse",
- "edit",
- "editing",
- "edition",
- "editor",
- "element",
- "emac",
- "email",
- "embed",
- "embedded",
- "ember",
- "emitter",
- "emulator",
- "encoding",
- "endpoint",
- "engine",
- "english",
- "enhanced",
- "entity",
- "entry",
- "env_",
- "episode",
- "erlang",
- "error",
- "espresso",
- "event",
- "evented",
- "example",
- "exchange",
- "exercise",
- "experiment",
- "expire",
- "exploit",
- "explorer",
- "export",
- "exporter",
- "expres",
- "ext-",
- "ext.",
- "ext_",
- "extended",
- "extension",
- "external",
- "extra",
- "extractor",
- "fabric",
- "facebook",
- "factory",
- "fake",
- "fast",
- "feature",
- "feed",
- "fewfwef",
- "ffmpeg",
- "field",
- "file",
- "filter",
- "find",
- "finder",
- "firefox",
- "firmware",
- "first",
- "fish",
- "fix-",
- "fix_",
- "flash",
- "flask",
- "flat",
- "flex",
- "flexible",
- "flickr",
- "flow",
- "fluent",
- "fluentd",
- "fluid",
- "folder",
- "font",
- "force",
- "foreman",
- "fork",
- "form",
- "format",
- "formatter",
- "forum",
- "foundry",
- "framework",
- "free",
- "friend",
- "friendly",
- "front-end",
- "frontend",
- "ftp-",
- "ftp.",
- "ftp_",
- "fuel",
- "full",
- "fun-",
- "fun.",
- "fun_",
- "func",
- "future",
- "gaia",
- "gallery",
- "game",
- "gateway",
- "gem-",
- "gem.",
- "gem_",
- "gen-",
- "gen.",
- "gen_",
- "general",
- "generator",
- "generic",
- "genetic",
- "get-",
- "get.",
- "get_",
- "getenv",
- "getting",
- "ghost",
- "gist",
- "git-",
- "git.",
- "git_",
- "github",
- "gitignore",
- "gitlab",
- "glas",
- "gmail",
- "gnome",
- "gnu-",
- "gnu.",
- "gnu_",
- "goal",
- "golang",
- "gollum",
- "good",
- "google",
- "gpu-",
- "gpu.",
- "gpu_",
- "gradle",
- "grail",
- "graph",
- "graphic",
- "great",
- "grid",
- "groovy",
- "group",
- "grunt",
- "guard",
- "gui-",
- "gui.",
- "gui_",
- "guide",
- "guideline",
- "gulp",
- "gwt-",
- "gwt.",
- "gwt_",
- "hack",
- "hackathon",
- "hacker",
- "hacking",
- "hadoop",
- "haml",
- "handler",
- "hardware",
- "has-",
- "has_",
- "hash",
- "haskell",
- "have",
- "haxe",
- "hello",
- "help",
- "helper",
- "here",
- "hero",
- "heroku",
- "high",
- "hipchat",
- "history",
- "home",
- "homebrew",
- "homepage",
- "hook",
- "host",
- "hosting",
- "hot-",
- "hot.",
- "hot_",
- "house",
- "how-",
- "how.",
- "how_",
- "html",
- "http",
- "hub-",
- "hub.",
- "hub_",
- "hubot",
- "human",
- "icon",
- "ide-",
- "ide.",
- "ide_",
- "idea",
- "identity",
- "idiomatic",
- "image",
- "impact",
- "import",
- "important",
- "importer",
- "impres",
- "index",
- "infinite",
- "info",
- "injection",
- "inline",
- "input",
- "inside",
- "inspector",
- "instagram",
- "install",
- "installer",
- "instant",
- "intellij",
- "interface",
- "internet",
- "interview",
- "into",
- "intro",
- "ionic",
- "iphone",
- "ipython",
- "irc-",
- "irc_",
- "iso-",
- "iso.",
- "iso_",
- "issue",
- "jade",
- "jasmine",
- "java",
- "jbos",
- "jekyll",
- "jenkin",
- "jetbrains",
- "job-",
- "job.",
- "job_",
- "joomla",
- "jpa-",
- "jpa.",
- "jpa_",
- "jquery",
- "json",
- "just",
- "kafka",
- "karma",
- "kata",
- "kernel",
- "keyboard",
- "kindle",
- "kit-",
- "kit.",
- "kit_",
- "kitchen",
- "knife",
- "koan",
- "kohana",
- "lab-",
- "lab.",
- "lab_",
- "lambda",
- "lamp",
- "language",
- "laravel",
- "last",
- "latest",
- "latex",
- "launcher",
- "layer",
- "layout",
- "lazy",
- "ldap",
- "leaflet",
- "league",
- "learn",
- "learning",
- "led-",
- "led.",
- "led_",
- "leetcode",
- "les-",
- "les.",
- "les_",
- "level",
- "leveldb",
- "lib-",
- "lib.",
- "lib_",
- "librarie",
- "library",
- "license",
- "life",
- "liferay",
- "light",
- "lightbox",
- "like",
- "line",
- "link",
- "linked",
- "linkedin",
- "linux",
- "lisp",
- "list",
- "lite",
- "little",
- "load",
- "loader",
- "local",
- "location",
- "lock",
- "log-",
- "log.",
- "log_",
- "logger",
- "logging",
- "logic",
- "login",
- "logstash",
- "longer",
- "look",
- "love",
- "lua-",
- "lua.",
- "lua_",
- "mac-",
- "mac.",
- "mac_",
- "machine",
- "made",
- "magento",
- "magic",
- "mail",
- "make",
- "maker",
- "making",
- "man-",
- "man.",
- "man_",
- "manage",
- "manager",
- "manifest",
- "manual",
- "map-",
- "map.",
- "map_",
- "mapper",
- "mapping",
- "markdown",
- "markup",
- "master",
- "math",
- "matrix",
- "maven",
- "md5",
- "mean",
- "media",
- "mediawiki",
- "meetup",
- "memcached",
- "memory",
- "menu",
- "merchant",
- "message",
- "messaging",
- "meta",
- "metadata",
- "meteor",
- "method",
- "metric",
- "micro",
- "middleman",
- "migration",
- "minecraft",
- "miner",
- "mini",
- "minimal",
- "mirror",
- "mit-",
- "mit.",
- "mit_",
- "mobile",
- "mocha",
- "mock",
- "mod-",
- "mod.",
- "mod_",
- "mode",
- "model",
- "modern",
- "modular",
- "module",
- "modx",
- "money",
- "mongo",
- "mongodb",
- "mongoid",
- "mongoose",
- "monitor",
- "monkey",
- "more",
- "motion",
- "moved",
- "movie",
- "mozilla",
- "mqtt",
- "mule",
- "multi",
- "multiple",
- "music",
- "mustache",
- "mvc-",
- "mvc.",
- "mvc_",
- "mysql",
- "nagio",
- "name",
- "native",
- "need",
- "neo-",
- "neo.",
- "neo_",
- "nest",
- "nested",
- "net-",
- "net.",
- "net_",
- "nette",
- "network",
- "new-",
- "new.",
- "new_",
- "next",
- "nginx",
- "ninja",
- "nlp-",
- "nlp.",
- "nlp_",
- "node",
- "nodej",
- "nosql",
- "not-",
- "not.",
- "not_",
- "note",
- "notebook",
- "notepad",
- "notice",
- "notifier",
- "now-",
- "now.",
- "now_",
- "number",
- "oauth",
- "object",
- "objective",
- "obsolete",
- "ocaml",
- "octopres",
- "official",
- "old-",
- "old.",
- "old_",
- "onboard",
- "online",
- "only",
- "open",
- "opencv",
- "opengl",
- "openshift",
- "openwrt",
- "option",
- "oracle",
- "org-",
- "org.",
- "org_",
- "origin",
- "original",
- "orm-",
- "orm.",
- "orm_",
- "osx-",
- "osx_",
- "our-",
- "our.",
- "our_",
- "out-",
- "out.",
- "out_",
- "output",
- "over",
- "overview",
- "own-",
- "own.",
- "own_",
- "pack",
- "package",
- "packet",
- "page",
- "panel",
- "paper",
- "paperclip",
- "para",
- "parallax",
- "parallel",
- "parse",
- "parser",
- "parsing",
- "particle",
- "party",
- "password",
- "patch",
- "path",
- "pattern",
- "payment",
- "paypal",
- "pdf-",
- "pdf.",
- "pdf_",
- "pebble",
- "people",
- "perl",
- "personal",
- "phalcon",
- "phoenix",
- "phone",
- "phonegap",
- "photo",
- "php-",
- "php.",
- "php_",
- "physic",
- "picker",
- "pipeline",
- "platform",
- "play",
- "player",
- "please",
- "plu-",
- "plu.",
- "plu_",
- "plug-in",
- "plugin",
- "plupload",
- "png-",
- "png.",
- "png_",
- "poker",
- "polyfill",
- "polymer",
- "pool",
- "pop-",
- "pop.",
- "pop_",
- "popcorn",
- "popup",
- "port",
- "portable",
- "portal",
- "portfolio",
- "post",
- "power",
- "powered",
- "powerful",
- "prelude",
- "pretty",
- "preview",
- "principle",
- "print",
- "pro-",
- "pro.",
- "pro_",
- "problem",
- "proc",
- "product",
- "profile",
- "profiler",
- "program",
- "progres",
- "project",
- "protocol",
- "prototype",
- "provider",
- "proxy",
- "public",
- "pull",
- "puppet",
- "pure",
- "purpose",
- "push",
- "pusher",
- "pyramid",
- "python",
- "quality",
- "query",
- "queue",
- "quick",
- "rabbitmq",
- "rack",
- "radio",
- "rail",
- "railscast",
- "random",
- "range",
- "raspberry",
- "rdf-",
- "rdf.",
- "rdf_",
- "react",
- "reactive",
- "read",
- "reader",
- "readme",
- "ready",
- "real",
- "real-time",
- "reality",
- "realtime",
- "recipe",
- "recorder",
- "red-",
- "red.",
- "red_",
- "reddit",
- "redi",
- "redmine",
- "reference",
- "refinery",
- "refresh",
- "registry",
- "related",
- "release",
- "remote",
- "rendering",
- "repo",
- "report",
- "request",
- "require",
- "required",
- "requirej",
- "research",
- "resource",
- "response",
- "resque",
- "rest",
- "restful",
- "resume",
- "reveal",
- "reverse",
- "review",
- "riak",
- "rich",
- "right",
- "ring",
- "robot",
- "role",
- "room",
- "router",
- "routing",
- "rpc-",
- "rpc.",
- "rpc_",
- "rpg-",
- "rpg.",
- "rpg_",
- "rspec",
- "ruby-",
- "ruby.",
- "ruby_",
- "rule",
- "run-",
- "run.",
- "run_",
- "runner",
- "running",
- "runtime",
- "rust",
- "rvm-",
- "rvm.",
- "rvm_",
- "salt",
- "sample",
- "sandbox",
- "sas-",
- "sas.",
- "sas_",
- "sbt-",
- "sbt.",
- "sbt_",
- "scala",
- "scalable",
- "scanner",
- "schema",
- "scheme",
- "school",
- "science",
- "scraper",
- "scratch",
- "screen",
- "script",
- "scroll",
- "scs-",
- "scs.",
- "scs_",
- "sdk-",
- "sdk.",
- "sdk_",
- "sdl-",
- "sdl.",
- "sdl_",
- "search",
- "secure",
- "security",
- "see-",
- "see.",
- "see_",
- "seed",
- "select",
- "selector",
- "selenium",
- "semantic",
- "sencha",
- "send",
- "sentiment",
- "serie",
- "server",
- "service",
- "session",
- "set-",
- "set.",
- "set_",
- "setting",
- "setup",
- "sha1",
- "sha2",
- "sha256",
- "share",
- "shared",
- "sharing",
- "sheet",
- "shell",
- "shield",
- "shipping",
- "shop",
- "shopify",
- "shortener",
- "should",
- "show",
- "showcase",
- "side",
- "silex",
- "simple",
- "simulator",
- "single",
- "site",
- "skeleton",
- "sketch",
- "skin",
- "slack",
- "slide",
- "slider",
- "slim",
- "small",
- "smart",
- "smtp",
- "snake",
- "snapshot",
- "snippet",
- "soap",
- "social",
- "socket",
- "software",
- "solarized",
- "solr",
- "solution",
- "solver",
- "some",
- "soon",
- "source",
- "space",
- "spark",
- "spatial",
- "spec",
- "sphinx",
- "spine",
- "spotify",
- "spree",
- "spring",
- "sprite",
- "sql-",
- "sql.",
- "sql_",
- "sqlite",
- "ssh-",
- "ssh.",
- "ssh_",
- "stack",
- "staging",
- "standard",
- "stanford",
- "start",
- "started",
- "starter",
- "startup",
- "stat",
- "statamic",
- "state",
- "static",
- "statistic",
- "statsd",
- "statu",
- "steam",
- "step",
- "still",
- "stm-",
- "stm.",
- "stm_",
- "storage",
- "store",
- "storm",
- "story",
- "strategy",
- "stream",
- "streaming",
- "string",
- "stripe",
- "structure",
- "studio",
- "study",
- "stuff",
- "style",
- "sublime",
- "sugar",
- "suite",
- "summary",
- "super",
- "support",
- "supported",
- "svg-",
- "svg.",
- "svg_",
- "svn-",
- "svn.",
- "svn_",
- "swagger",
- "swift",
- "switch",
- "switcher",
- "symfony",
- "symphony",
- "sync",
- "synopsi",
- "syntax",
- "system",
- "tab-",
- "tab.",
- "tab_",
- "table",
- "tag-",
- "tag.",
- "tag_",
- "talk",
- "target",
- "task",
- "tcp-",
- "tcp.",
- "tcp_",
- "tdd-",
- "tdd.",
- "tdd_",
- "team",
- "tech",
- "template",
- "term",
- "terminal",
- "testing",
- "tetri",
- "text",
- "textmate",
- "theme",
- "theory",
- "three",
- "thrift",
- "time",
- "timeline",
- "timer",
- "tiny",
- "tinymce",
- "tip-",
- "tip.",
- "tip_",
- "title",
- "todo",
- "todomvc",
- "token",
- "tool",
- "toolbox",
- "toolkit",
- "top-",
- "top.",
- "top_",
- "tornado",
- "touch",
- "tower",
- "tracker",
- "tracking",
- "traffic",
- "training",
- "transfer",
- "translate",
- "transport",
- "tree",
- "trello",
- "try-",
- "try.",
- "try_",
- "tumblr",
- "tut-",
- "tut.",
- "tut_",
- "tutorial",
- "tweet",
- "twig",
- "twitter",
- "type",
- "typo",
- "ubuntu",
- "uiview",
- "ultimate",
- "under",
- "unit",
- "unity",
- "universal",
- "unix",
- "update",
- "updated",
- "upgrade",
- "upload",
- "uploader",
- "uri-",
- "uri.",
- "uri_",
- "url-",
- "url.",
- "url_",
- "usage",
- "usb-",
- "usb.",
- "usb_",
- "use-",
- "use.",
- "use_",
- "used",
- "useful",
- "user",
- "using",
- "util",
- "utilitie",
- "utility",
- "vagrant",
- "validator",
- "value",
- "variou",
- "varnish",
- "version",
- "via-",
- "via.",
- "via_",
- "video",
- "view",
- "viewer",
- "vim-",
- "vim.",
- "vim_",
- "vimrc",
- "virtual",
- "vision",
- "visual",
- "vpn",
- "want",
- "warning",
- "watch",
- "watcher",
- "wave",
- "way-",
- "way.",
- "way_",
- "weather",
- "web-",
- "web_",
- "webapp",
- "webgl",
- "webhook",
- "webkit",
- "webrtc",
- "website",
- "websocket",
- "welcome",
- "what",
- "what'",
- "when",
- "where",
- "which",
- "why-",
- "why.",
- "why_",
- "widget",
- "wifi",
- "wiki",
- "win-",
- "win.",
- "win_",
- "window",
- "wip-",
- "wip.",
- "wip_",
- "within",
- "without",
- "wizard",
- "word",
- "wordpres",
- "work",
- "worker",
- "workflow",
- "working",
- "workshop",
- "world",
- "wrapper",
- "write",
- "writer",
- "writing",
- "written",
- "www-",
- "www.",
- "www_",
- "xamarin",
- "xcode",
- "xml-",
- "xml.",
- "xml_",
- "xmpp",
- "xxxxxx",
- "yahoo",
- "yaml",
- "yandex",
- "yeoman",
- "yet-",
- "yet.",
- "yet_",
- "yii-",
- "yii.",
- "yii_",
- "youtube",
- "yui-",
- "yui.",
- "yui_",
- "zend",
- "zero",
- "zip-",
- "zip.",
- "zip_",
- "zsh-",
- "zsh.",
- "zsh_",
-]
-[[rules.allowlists]]
-regexTarget = "line"
-regexes = [
- '''--mount=type=secret,''',
- '''import[ \t]+{[ \t\w,]+}[ \t]+from[ \t]+['"][^'"]+['"]''',
-]
-[[rules.allowlists]]
-condition = "AND"
-paths = [
- '''\.bb$''','''\.bbappend$''','''\.bbclass$''','''\.inc$''',
-]
-regexTarget = "line"
-regexes = [
- '''LICENSE[^=]*=\s*"[^"]+''',
- '''LIC_FILES_CHKSUM[^=]*=\s*"[^"]+''',
- '''SRC[^=]*=\s*"[a-zA-Z0-9]+''',
-]
-
-[[rules]]
-id = "github-app-token"
-description = "Identified a GitHub App Token, which may compromise GitHub application integrations and source code security."
-regex = '''(?:ghu|ghs)_[0-9a-zA-Z]{36}'''
-entropy = 3
-keywords = [
- "ghu_",
- "ghs_",
-]
-[[rules.allowlists]]
-paths = [
- '''(?:^|/)@octokit/auth-token/README\.md$''',
-]
-
-[[rules]]
-id = "github-fine-grained-pat"
-description = "Found a GitHub Fine-Grained Personal Access Token, risking unauthorized repository access and code manipulation."
-regex = '''github_pat_\w{82}'''
-entropy = 3
-keywords = ["github_pat_"]
-
-[[rules]]
-id = "github-oauth"
-description = "Discovered a GitHub OAuth Access Token, posing a risk of compromised GitHub account integrations and data leaks."
-regex = '''gho_[0-9a-zA-Z]{36}'''
-entropy = 3
-keywords = ["gho_"]
-
-[[rules]]
-id = "github-pat"
-description = "Uncovered a GitHub Personal Access Token, potentially leading to unauthorized repository access and sensitive content exposure."
-regex = '''ghp_[0-9a-zA-Z]{36}'''
-entropy = 3
-keywords = ["ghp_"]
-[[rules.allowlists]]
-paths = [
- '''(?:^|/)@octokit/auth-token/README\.md$''',
-]
-
-[[rules]]
-id = "github-refresh-token"
-description = "Detected a GitHub Refresh Token, which could allow prolonged unauthorized access to GitHub services."
-regex = '''ghr_[0-9a-zA-Z]{36}'''
-entropy = 3
-keywords = ["ghr_"]
-
-[[rules]]
-id = "gitlab-cicd-job-token"
-description = "Identified a GitLab CI/CD Job Token, potential access to projects and some APIs on behalf of a user while the CI job is running."
-regex = '''glcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}'''
-entropy = 3
-keywords = ["glcbt-"]
-
-[[rules]]
-id = "gitlab-deploy-token"
-description = "Identified a GitLab Deploy Token, risking access to repositories, packages and containers with write access."
-regex = '''gldt-[0-9a-zA-Z_\-]{20}'''
-entropy = 3
-keywords = ["gldt-"]
-
-[[rules]]
-id = "gitlab-feature-flag-client-token"
-description = "Identified a GitLab feature flag client token, risks exposing user lists and features flags used by an application."
-regex = '''glffct-[0-9a-zA-Z_\-]{20}'''
-entropy = 3
-keywords = ["glffct-"]
-
-[[rules]]
-id = "gitlab-feed-token"
-description = "Identified a GitLab feed token, risking exposure of user data."
-regex = '''glft-[0-9a-zA-Z_\-]{20}'''
-entropy = 3
-keywords = ["glft-"]
-
-[[rules]]
-id = "gitlab-incoming-mail-token"
-description = "Identified a GitLab incoming mail token, risking manipulation of data sent by mail."
-regex = '''glimt-[0-9a-zA-Z_\-]{25}'''
-entropy = 3
-keywords = ["glimt-"]
-
-[[rules]]
-id = "gitlab-kubernetes-agent-token"
-description = "Identified a GitLab Kubernetes Agent token, risking access to repos and registry of projects connected via agent."
-regex = '''glagent-[0-9a-zA-Z_\-]{50}'''
-entropy = 3
-keywords = ["glagent-"]
-
-[[rules]]
-id = "gitlab-oauth-app-secret"
-description = "Identified a GitLab OIDC Application Secret, risking access to apps using GitLab as authentication provider."
-regex = '''gloas-[0-9a-zA-Z_\-]{64}'''
-entropy = 3
-keywords = ["gloas-"]
-
-[[rules]]
-id = "gitlab-pat"
-description = "Identified a GitLab Personal Access Token, risking unauthorized access to GitLab repositories and codebase exposure."
-regex = '''glpat-[\w-]{20}'''
-entropy = 3
-keywords = ["glpat-"]
-
-[[rules]]
-id = "gitlab-pat-routable"
-description = "Identified a GitLab Personal Access Token (routable), risking unauthorized access to GitLab repositories and codebase exposure."
-regex = '''\bglpat-[0-9a-zA-Z_-]{27,300}\.[0-9a-z]{2}[0-9a-z]{7}\b'''
-entropy = 4
-keywords = ["glpat-"]
-
-[[rules]]
-id = "gitlab-ptt"
-description = "Found a GitLab Pipeline Trigger Token, potentially compromising continuous integration workflows and project security."
-regex = '''glptt-[0-9a-f]{40}'''
-entropy = 3
-keywords = ["glptt-"]
-
-[[rules]]
-id = "gitlab-rrt"
-description = "Discovered a GitLab Runner Registration Token, posing a risk to CI/CD pipeline integrity and unauthorized access."
-regex = '''GR1348941[\w-]{20}'''
-entropy = 3
-keywords = ["gr1348941"]
-
-[[rules]]
-id = "gitlab-runner-authentication-token"
-description = "Discovered a GitLab Runner Authentication Token, posing a risk to CI/CD pipeline integrity and unauthorized access."
-regex = '''glrt-[0-9a-zA-Z_\-]{20}'''
-entropy = 3
-keywords = ["glrt-"]
-
-[[rules]]
-id = "gitlab-runner-authentication-token-routable"
-description = "Discovered a GitLab Runner Authentication Token (Routable), posing a risk to CI/CD pipeline integrity and unauthorized access."
-regex = '''\bglrt-t\d_[0-9a-zA-Z_\-]{27,300}\.[0-9a-z]{2}[0-9a-z]{7}\b'''
-entropy = 4
-keywords = ["glrt-"]
-
-[[rules]]
-id = "gitlab-scim-token"
-description = "Discovered a GitLab SCIM Token, posing a risk to unauthorized access for a organization or instance."
-regex = '''glsoat-[0-9a-zA-Z_\-]{20}'''
-entropy = 3
-keywords = ["glsoat-"]
-
-[[rules]]
-id = "gitlab-session-cookie"
-description = "Discovered a GitLab Session Cookie, posing a risk to unauthorized access to a user account."
-regex = '''_gitlab_session=[0-9a-z]{32}'''
-entropy = 3
-keywords = ["_gitlab_session="]
-
-[[rules]]
-id = "gitter-access-token"
-description = "Uncovered a Gitter Access Token, which may lead to unauthorized access to chat and communication services."
-regex = '''(?i)[\w.-]{0,50}?(?:gitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{40})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["gitter"]
-
-[[rules]]
-id = "gocardless-api-token"
-description = "Detected a GoCardless API token, potentially risking unauthorized direct debit payment operations and financial data exposure."
-regex = '''(?i)[\w.-]{0,50}?(?:gocardless)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(live_(?i)[a-z0-9\-_=]{40})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = [
- "live_",
- "gocardless",
-]
-
-[[rules]]
-id = "grafana-api-key"
-description = "Identified a Grafana API key, which could compromise monitoring dashboards and sensitive data analytics."
-regex = '''(?i)\b(eyJrIjoi[A-Za-z0-9]{70,400}={0,3})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["eyjrijoi"]
-
-[[rules]]
-id = "grafana-cloud-api-token"
-description = "Found a Grafana cloud API token, risking unauthorized access to cloud-based monitoring services and data exposure."
-regex = '''(?i)\b(glc_[A-Za-z0-9+/]{32,400}={0,3})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["glc_"]
-
-[[rules]]
-id = "grafana-service-account-token"
-description = "Discovered a Grafana service account token, posing a risk of compromised monitoring services and data integrity."
-regex = '''(?i)\b(glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["glsa_"]
-
-[[rules]]
-id = "harness-api-key"
-description = "Identified a Harness Access Token (PAT or SAT), risking unauthorized access to a Harness account."
-regex = '''(?:pat|sat)\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9]{24}\.[a-zA-Z0-9]{20}'''
-keywords = [
- "pat.",
- "sat.",
-]
-
-[[rules]]
-id = "hashicorp-tf-api-token"
-description = "Uncovered a HashiCorp Terraform user/org API token, which may lead to unauthorized infrastructure management and security breaches."
-regex = '''(?i)[a-z0-9]{14}\.(?-i:atlasv1)\.[a-z0-9\-_=]{60,70}'''
-entropy = 3.5
-keywords = ["atlasv1"]
-
-[[rules]]
-id = "hashicorp-tf-password"
-description = "Identified a HashiCorp Terraform password field, risking unauthorized infrastructure configuration and security breaches."
-regex = '''(?i)[\w.-]{0,50}?(?:administrator_login_password|password)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}("[a-z0-9=_\-]{8,20}")(?:[\x60'"\s;]|\\[nr]|$)'''
-path = '''(?i)\.(?:tf|hcl)$'''
-entropy = 2
-keywords = [
- "administrator_login_password",
- "password",
-]
-
-[[rules]]
-id = "heroku-api-key"
-description = "Detected a Heroku API Key, potentially compromising cloud application deployments and operational security."
-regex = '''(?i)[\w.-]{0,50}?(?:heroku)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["heroku"]
-
-[[rules]]
-id = "hubspot-api-key"
-description = "Found a HubSpot API Token, posing a risk to CRM data integrity and unauthorized marketing operations."
-regex = '''(?i)[\w.-]{0,50}?(?:hubspot)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["hubspot"]
-
-[[rules]]
-id = "huggingface-access-token"
-description = "Discovered a Hugging Face Access token, which could lead to unauthorized access to AI models and sensitive data."
-regex = '''\b(hf_(?i:[a-z]{34}))(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = ["hf_"]
-
-[[rules]]
-id = "huggingface-organization-api-token"
-description = "Uncovered a Hugging Face Organization API token, potentially compromising AI organization accounts and associated data."
-regex = '''\b(api_org_(?i:[a-z]{34}))(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 2
-keywords = ["api_org_"]
-
-[[rules]]
-id = "infracost-api-token"
-description = "Detected an Infracost API Token, risking unauthorized access to cloud cost estimation tools and financial data."
-regex = '''\b(ico-[a-zA-Z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["ico-"]
-
-[[rules]]
-id = "intercom-api-key"
-description = "Identified an Intercom API Token, which could compromise customer communication channels and data privacy."
-regex = '''(?i)[\w.-]{0,50}?(?:intercom)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{60})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = ["intercom"]
-
-[[rules]]
-id = "intra42-client-secret"
-description = "Found a Intra42 client secret, which could lead to unauthorized access to the 42School API and sensitive data."
-regex = '''\b(s-s4t2(?:ud|af)-(?i)[abcdef0123456789]{64})(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = [
- "intra",
- "s-s4t2ud-",
- "s-s4t2af-",
-]
-
-[[rules]]
-id = "jfrog-api-key"
-description = "Found a JFrog API Key, posing a risk of unauthorized access to software artifact repositories and build pipelines."
-regex = '''(?i)[\w.-]{0,50}?(?:jfrog|artifactory|bintray|xray)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{73})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = [
- "jfrog",
- "artifactory",
- "bintray",
- "xray",
-]
-
-[[rules]]
-id = "jfrog-identity-token"
-description = "Discovered a JFrog Identity Token, potentially compromising access to JFrog services and sensitive software artifacts."
-regex = '''(?i)[\w.-]{0,50}?(?:jfrog|artifactory|bintray|xray)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)'''
-keywords = [
- "jfrog",
- "artifactory",
- "bintray",
- "xray",
-]
-
-[[rules]]
-id = "jwt"
-description = "Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data."
-regex = '''\b(ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/\\_-]{17,}\.(?:[a-zA-Z0-9\/\\_-]{10,}={0,2})?)(?:[\x60'"\s;]|\\[nr]|$)'''
-entropy = 3
-keywords = ["ey"]
-
-[[rules]]
-id = "jwt-base64"
-description = "Detected a Base64-encoded JSON Web Token, posing a risk of exposing encoded authentication and data exchange information."
-regex = '''\bZXlK(?:(?PaGJHY2lPaU)|(?PaGNIVWlPaU)|(?PaGNIWWlPaU)|(?PaGRXUWlPaU)|(?PaU5qUWlP)|(?PamNtbDBJanBi)|(?PamRIa2lPaU)|(?PbGNHc2lPbn)|(?PbGJtTWlPaU)|(?PcWEzVWlPaU)|(?PcWQyc2lPb)|(?PcGMzTWlPaU)|(?PcGRpSTZJ)|(?PcmFXUWlP)|(?PclpYbGZiM0J6SWpwY)|(?PcmRIa2lPaUp)|(?PdWIyNWpaU0k2)|(?Pd01tTWlP)|(?Pd01uTWlPaU)|(?Pd2NIUWlPaU)|(?PemRXSWlPaU)|(?PemRuUWlP)|(?PMFlXY2lPaU)|(?PMGVYQWlPaUp)|(?PMWNtd2l)|(?P
+ }
+ >
+
+
+
+
+ )}
+ />
+ );
+};
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx
index 50ea46ac0..cb7a40559 100644
--- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx
@@ -14,6 +14,7 @@ import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
import { AwsSecretsManagerSyncOptionsFields } from "./AwsSecretsManagerSyncOptionsFields";
+import { RenderSyncOptionsFields } from "./RenderSyncOptionsFields";
type Props = {
hideInitialSync?: boolean;
@@ -38,6 +39,9 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.AWSSecretsManager:
AdditionalSyncOptionsFieldsComponent = ;
break;
+ case SecretSync.Render:
+ AdditionalSyncOptionsFieldsComponent = ;
+ break;
case SecretSync.GitHub:
case SecretSync.GCPSecretManager:
case SecretSync.AzureKeyVault:
@@ -54,7 +58,6 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.OnePass:
case SecretSync.OCIVault:
case SecretSync.Heroku:
- case SecretSync.Render:
case SecretSync.Flyio:
case SecretSync.GitLab:
case SecretSync.CloudflarePages:
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx
index becc46c1d..15f5b6625 100644
--- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RenderSyncReviewFields.tsx
@@ -2,8 +2,29 @@ import { useFormContext } from "react-hook-form";
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
+import { Badge } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
+export const RenderSyncOptionsReviewFields = () => {
+ const { watch } = useFormContext();
+
+ const [{ autoRedeployServices }] = watch(["syncOptions"]);
+
+ return (
+
+ {autoRedeployServices ? (
+
+ Enabled
+
+ ) : (
+
+ Disabled
+
+ )}
+
+ );
+};
+
export const RenderSyncReviewFields = () => {
const { watch } = useFormContext();
const serviceName = watch("destinationConfig.serviceName");
diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx
index c1194a4c1..5ee53f2e3 100644
--- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx
+++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx
@@ -35,7 +35,7 @@ import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields";
-import { RenderSyncReviewFields } from "./RenderSyncReviewFields";
+import { RenderSyncOptionsReviewFields, RenderSyncReviewFields } from "./RenderSyncReviewFields";
import { SupabaseSyncReviewFields } from "./SupabaseSyncReviewFields";
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
@@ -121,6 +121,7 @@ export const SecretSyncReviewFields = () => {
break;
case SecretSync.Render:
DestinationFieldsComponent = ;
+ AdditionalSyncOptionsFieldsComponent = ;
break;
case SecretSync.Flyio:
DestinationFieldsComponent = ;
diff --git a/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts
index 16b213421..83e5347eb 100644
--- a/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts
+++ b/frontend/src/components/secret-syncs/forms/schemas/render-sync-destination-schema.ts
@@ -2,9 +2,13 @@ import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
-import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/render-sync";
+import { RenderSyncScope, RenderSyncType } from "@app/hooks/api/secretSyncs/types/render-sync";
-export const RenderSyncDestinationSchema = BaseSecretSyncSchema().merge(
+export const RenderSyncDestinationSchema = BaseSecretSyncSchema(
+ z.object({
+ autoRedeployServices: z.boolean().optional()
+ })
+).merge(
z.object({
destination: z.literal(SecretSync.Render),
destinationConfig: z.discriminatedUnion("scope", [
diff --git a/frontend/src/components/v2/AccessRestrictedBanner/AccessRestrictedBanner.tsx b/frontend/src/components/v2/AccessRestrictedBanner/AccessRestrictedBanner.tsx
new file mode 100644
index 000000000..1979039dd
--- /dev/null
+++ b/frontend/src/components/v2/AccessRestrictedBanner/AccessRestrictedBanner.tsx
@@ -0,0 +1,26 @@
+import { ReactNode } from "react";
+
+type Props = {
+ title?: string;
+ body?: ReactNode;
+};
+
+export const AccessRestrictedBanner = ({
+ title = "Access Restricted",
+
+ body = (
+ <>
+ Your current role doesn't provide access to this feature.
+
Contact your administrator to request access.
+ >
+ )
+}: Props) => {
+ return (
+
+ );
+};
diff --git a/frontend/src/components/v2/AccessRestrictedBanner/index.ts b/frontend/src/components/v2/AccessRestrictedBanner/index.ts
new file mode 100644
index 000000000..d60468572
--- /dev/null
+++ b/frontend/src/components/v2/AccessRestrictedBanner/index.ts
@@ -0,0 +1 @@
+export * from "./AccessRestrictedBanner";
diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx
index a42ee02a5..ee2f196b1 100644
--- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx
+++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx
@@ -1,5 +1,5 @@
import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react";
-import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
+import { faFolder, faKey, faLayerGroup } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Popover from "@radix-ui/react-popover";
@@ -79,7 +79,7 @@ export const InfisicalSecretInput = forwardRef(
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
- const [debouncedValue] = useDebounce(value, 500);
+ const [debouncedValue] = useDebounce(value, 100);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
@@ -142,7 +142,7 @@ export const InfisicalSecretInput = forwardRef(
const suggestions = useMemo(() => {
if (!isPopupOpen) return [];
// reset highlight whenever recomputation happens
- setHighlightedIndex(-1);
+ setHighlightedIndex(0);
const suggestionsArr: ReferenceItem[] = [];
const predicate = suggestionSource.predicate.toLowerCase();
@@ -298,17 +298,21 @@ export const InfisicalSecretInput = forwardRef(
}}
>
{suggestions.map((item, i) => {
let entryIcon;
+ let subText;
if (item.type === ReferenceType.SECRET) {
- entryIcon = faKey;
+ entryIcon =
;
+ subText = "Secret";
} else if (item.type === ReferenceType.ENVIRONMENT) {
- entryIcon = faCircle;
+ entryIcon =
;
+ subText = "Environment";
} else {
- entryIcon = faFolder;
+ entryIcon =
;
+ subText = "Folder";
}
return (
@@ -327,22 +331,22 @@ export const InfisicalSecretInput = forwardRef
(
}}
onMouseEnter={() => setHighlightedIndex(i)}
style={{ pointerEvents: "auto" }}
- className="flex items-center justify-between border-mineshaft-600 text-left"
+ className="flex w-full items-center justify-between border-mineshaft-600 text-left"
key={`secret-reference-secret-${i + 1}`}
>
-
-
-
+
+
{entryIcon}
+
+ {item.label}
+
+ {subText}
+
-
{item.label}
diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx
index f114c0f02..a232c0065 100644
--- a/frontend/src/components/v2/Select/Select.tsx
+++ b/frontend/src/components/v2/Select/Select.tsx
@@ -20,6 +20,7 @@ type Props = {
isMulti?: boolean;
iconClassName?: string;
dropdownContainerStyle?: React.CSSProperties;
+ side?: SelectPrimitive.SelectContentProps["side"];
};
export type SelectProps = Omit
& Props;
@@ -37,6 +38,7 @@ export const Select = forwardRef(
containerClassName,
iconClassName,
dropdownContainerStyle,
+ side,
...props
},
ref
@@ -78,6 +80,7 @@ export const Select = forwardRef(
= {
[SecretSync.AWSParameterStore]: { name: "AWS Parameter Store", image: "Amazon Web Services.png" },
diff --git a/frontend/src/hoc/withPermission/withPermission.tsx b/frontend/src/hoc/withPermission/withPermission.tsx
index ef814d958..529a74930 100644
--- a/frontend/src/hoc/withPermission/withPermission.tsx
+++ b/frontend/src/hoc/withPermission/withPermission.tsx
@@ -1,9 +1,8 @@
import { ComponentType } from "react";
import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability";
-import { faLock } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
+import { AccessRestrictedBanner } from "@app/components/v2";
import { TOrgPermission, useOrgPermission } from "@app/context";
type Props = (T extends AbilityTuple
@@ -14,11 +13,11 @@ type Props = (T extends AbilityTuple
: {
action: string;
subject: string;
- }) & { className?: string; containerClassName?: string };
+ }) & { containerClassName?: string };
export const withPermission = (
Component: ComponentType,
- { action, subject, className, containerClassName }: Props["abilities"]>
+ { action, subject, containerClassName }: Props["abilities"]>
) => {
const HOC = (hocProps: T) => {
const { permission } = useOrgPermission();
@@ -33,22 +32,7 @@ export const withPermission = (
containerClassName
)}
>
-
-
-
-
-
-
Access Restricted
-
- Your role has limited permissions, please
contact your admin to gain access
-
-
-
+
);
}
diff --git a/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx b/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx
index 564d44adc..7ba6efc57 100644
--- a/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx
+++ b/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx
@@ -1,14 +1,12 @@
import { ComponentType } from "react";
import { AbilityTuple } from "@casl/ability";
-import { faLock } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
+import { AccessRestrictedBanner } from "@app/components/v2";
import { useProjectPermission } from "@app/context";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
type Props = {
- className?: string;
containerClassName?: string;
action: T[0];
subject: T[1];
@@ -16,7 +14,7 @@ type Props = {
export const withProjectPermission = (
Component: ComponentType, "action" | "subject"> & T>,
- { action, subject, className, containerClassName }: Props
+ { action, subject, containerClassName }: Props
) => {
const HOC = (hocProps: Omit, "action" | "subject"> & T) => {
const { permission } = useProjectPermission();
@@ -31,23 +29,7 @@ export const withProjectPermission = (
containerClassName
)}
>
-
-
-
-
-
-
Permission Denied
-
- You do not have permission to this page.
Kindly contact your organization
- administrator
-
-
-
+
);
}
diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx
index 3b05ff61b..ad7917a8f 100644
--- a/frontend/src/hooks/api/accessApproval/mutation.tsx
+++ b/frontend/src/hooks/api/accessApproval/mutation.tsx
@@ -17,7 +17,7 @@ export const useCreateAccessApprovalPolicy = () => {
return useMutation