1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-23 03:03:05 +00:00

Compare commits

..

141 Commits

Author SHA1 Message Date
27030138ec Merge pull request from Infisical/add-refresh-token-cli
add refresh token to cli
2023-05-24 18:53:52 -04:00
c37ce4eaea add refresh token to cli 2023-05-24 18:51:42 -04:00
17647587f9 remove tests for time being 2023-05-24 10:48:11 -04:00
f3dc7fcf7b add timout to pull requests 2023-05-24 10:48:11 -04:00
e65c6568e1 Modify convention for PostHog isPaid attr to be tier-based instead of slug 2023-05-24 10:26:06 +03:00
9d40a96633 Update README.md 2023-05-23 20:22:01 -04:00
859fe09ac6 Merge pull request from Infisical/maidul98-patch-1
add pre commit install command to README.md
2023-05-23 20:20:57 -04:00
d0d6419d4d add pre commit install command to README.md 2023-05-23 20:20:10 -04:00
8b05ce11f7 add pre commit to husky 2023-05-23 20:15:39 -04:00
a7fb0786f9 improve pre commit docs 2023-05-23 19:45:10 -04:00
f2de1778cb catch case when hook path is default 2023-05-23 19:34:31 -04:00
952cf47b9a Merge branch 'main' of https://github.com/Infisical/infisical 2023-05-23 15:41:43 -07:00
1d17596af1 added boolean flag for the plan in posthog logging 2023-05-23 15:41:34 -07:00
01385687e0 make posthog failed calls level=debug 2023-05-23 18:16:13 -04:00
d2e3aa15b0 patch standalone docker image 2023-05-23 17:16:32 -04:00
96607153dc Modularize getOrganizationPlan function 2023-05-23 23:54:54 +03:00
a8502377c7 Add endpoint for updating organization plan 2023-05-23 23:14:20 +03:00
5aa99001cc Merge pull request from Infisical/connect-to-license-server
Added add/remove/get organization payment methods and get cloud plans…
2023-05-23 22:39:35 +03:00
83dd35299c Added add/remove/get organization payment methods and get cloud plans from license server 2023-05-23 22:28:41 +03:00
b5b2f402ad add missing required envrs 2023-05-23 14:09:45 -04:00
ec34572087 patch invite only 2023-05-23 13:18:21 -04:00
7f7d120c2f Merge pull request from Infisical/connect-to-license-server
Add support for fetching plan details from license server
2023-05-23 17:02:20 +03:00
899d46514c Add forwarding usedSeats and subscription quantity to license server on org member add/delete 2023-05-23 16:59:13 +03:00
658df21189 Add auto install pre commit 2023-05-23 00:09:00 -04:00
8341faddc5 Add support for pulling plan details from license server with LICENSE_KEY, LICENSE_SERVER_KEY 2023-05-22 15:43:33 +03:00
8e3a23e6d8 fix prod node img for standalone 2023-05-22 08:18:50 -04:00
1c89474159 hello 2023-05-19 17:23:15 -04:00
2f765600b1 add pre-commit hook 2023-05-19 17:20:27 -04:00
d9057216b5 remove keyring access during telemetry 2023-05-19 16:10:59 -04:00
6aab90590f add version to cli run telemtry 2023-05-19 12:24:49 -04:00
f7466d4855 update cli telemetry 2023-05-19 12:20:37 -04:00
ea2565ed35 Merge pull request from Infisical/cli-telemetry
Cli telemetry
2023-05-19 10:55:27 -04:00
4586656b85 add post hog api to go releaser and update cli telemetry 2023-05-19 10:49:57 -04:00
e4953398df add telemetry to cli 2023-05-19 00:16:26 -04:00
7722231656 Merge pull request from Infisical/infisical-scan-docs
Infisical scan docs
2023-05-18 15:59:51 -04:00
845a476974 add secret scanning to README.md 2023-05-18 15:57:48 -04:00
fc19a17f4b update readme with scaning feature 2023-05-18 15:42:25 -04:00
0890b1912f Merge pull request from Infisical/infisical-scan-docs
add docs for infisical scan
2023-05-18 15:20:26 -04:00
82ecc2d7dc add secret scanning to resources 2023-05-18 15:18:29 -04:00
460bdbb91c Merge pull request from Infisical/snyk-upgrade-76cf9e766d00cfa629a2db56d3b5fc39
[Snyk] Upgrade posthog-js from 1.53.4 to 1.54.0
2023-05-18 14:57:16 -04:00
446a63a917 add docs for infisical scan 2023-05-18 14:55:39 -04:00
d67cb7b507 Merge pull request from Infisical/add-gitleak
rebrand and small tweeks
2023-05-18 12:07:26 -04:00
353ff63298 rebrand and small tweeks 2023-05-18 12:04:17 -04:00
9f40266f5c fix: upgrade posthog-js from 1.53.4 to 1.54.0
Snyk has created this PR to upgrade posthog-js from 1.53.4 to 1.54.0.

See this package in npm:
https://www.npmjs.com/package/posthog-js

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/53d4ecb6-6cc1-4918-aa73-bf9cae4ffd13?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-18 11:58:06 +00:00
8af8a1d3d5 Merge pull request from Infisical/add-gitleak
add gitleak to cli
2023-05-17 13:20:40 -04:00
631423fbc8 Merge pull request from Infisical/snyk-upgrade-32d764d8893bf7596281cd2751bb5f9b
[Snyk] Upgrade @aws-sdk/client-secrets-manager from 3.317.0 to 3.319.0
2023-05-17 13:11:54 -04:00
4383779377 Merge pull request from Infisical/snyk-upgrade-efa6b99248f4e9459845f26b359fc5c8
[Snyk] Upgrade aws-sdk from 2.1362.0 to 2.1364.0
2023-05-17 13:11:37 -04:00
8249043826 add testing files and create create scan command 2023-05-17 13:08:00 -04:00
20294ee233 Fixed the const issue 2023-05-17 09:27:12 -07:00
c5a924e935 Merge pull request from Infisical/gitlab-envs
Add support for custom environments in GitLab integration
2023-05-17 14:31:00 +03:00
429bfd27b2 Add support for custom environments in GitLab integration 2023-05-17 14:25:18 +03:00
c99c873d78 fix: upgrade @aws-sdk/client-secrets-manager from 3.317.0 to 3.319.0
Snyk has created this PR to upgrade @aws-sdk/client-secrets-manager from 3.317.0 to 3.319.0.

See this package in npm:
https://www.npmjs.com/package/@aws-sdk/client-secrets-manager

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-16 19:34:58 +00:00
092a6911ce fix: upgrade aws-sdk from 2.1362.0 to 2.1364.0
Snyk has created this PR to upgrade aws-sdk from 2.1362.0 to 2.1364.0.

See this package in npm:
https://www.npmjs.com/package/aws-sdk

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-16 19:34:50 +00:00
a9b642e618 Merge branch 'main' of https://github.com/Infisical/infisical 2023-05-15 16:34:02 -07:00
919ddf5de2 removed console log 2023-05-15 16:33:44 -07:00
89a89af4e6 improving UX for the onboarding experience 2023-05-15 16:33:11 -07:00
b3e68cf3fb add gitleak to cli 2023-05-15 19:31:36 -04:00
960063e61a Merge pull request from Infisical/snyk-upgrade-e333c5ab909cc9a88c7a6d9fc95a58ed
[Snyk] Upgrade @aws-sdk/client-secrets-manager from 3.312.0 to 3.317.0
2023-05-15 17:48:35 -04:00
abf4eaf6db Merge pull request from Infisical/snyk-upgrade-64f35bb43ebf5a87403747b8d7956c34
[Snyk] Upgrade fs from 0.0.1-security to 0.0.2
2023-05-15 17:48:25 -04:00
739f97f5c9 Merge pull request from Infisical/snyk-upgrade-6fd25092a72767ffb9954920488a4cd5
[Snyk] Upgrade @sentry/node from 7.47.0 to 7.49.0
2023-05-15 17:48:06 -04:00
faed5c1821 Merge pull request from Infisical/snyk-upgrade-f1503bf1fac2c534c106f41288ce944d
[Snyk] Upgrade aws-sdk from 2.1360.0 to 2.1362.0
2023-05-15 17:47:54 -04:00
c95598aaa6 Merge pull request from akhilmhdh/fix/compose-fail
fix: docker-compose failing due to missing frontend i18n file
2023-05-15 17:47:33 -04:00
e791684f4d fix: docker-compose failing due to missing frontend i18n file 2023-05-16 00:19:03 +05:30
d32c5fb869 update the dev stripe product id 2023-05-15 07:31:17 -07:00
abbf1918dc Added limits to the number of projects in an org 2023-05-14 18:25:27 -07:00
876d0119d3 Merge pull request from parthvnp/feature/457
Add example in CLI usage docs to show how to utilize secrets in shell aliases
2023-05-13 11:23:27 -04:00
6d70dc437e update cli usage docs 2023-05-13 11:22:38 -04:00
174e22a2bc put aliases docs in Accordion 2023-05-13 11:17:17 -04:00
f4815641d8 fixed the bug with smaller icon buttons 2023-05-11 18:11:04 -07:00
5b95c255ec fix: upgrade aws-sdk from 2.1360.0 to 2.1362.0
Snyk has created this PR to upgrade aws-sdk from 2.1360.0 to 2.1362.0.

See this package in npm:
https://www.npmjs.com/package/aws-sdk

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-11 22:13:48 +00:00
3123f6fc1f fix: upgrade @sentry/node from 7.47.0 to 7.49.0
Snyk has created this PR to upgrade @sentry/node from 7.47.0 to 7.49.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/node

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-11 22:13:44 +00:00
a913cd97a4 fix: upgrade @aws-sdk/client-secrets-manager from 3.312.0 to 3.317.0
Snyk has created this PR to upgrade @aws-sdk/client-secrets-manager from 3.312.0 to 3.317.0.

See this package in npm:
https://www.npmjs.com/package/@aws-sdk/client-secrets-manager

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-11 22:13:39 +00:00
781e0b24c8 add docs for spring boot maven 2023-05-10 21:08:38 -04:00
28de8cddd7 Merge pull request from Infisical/snyk-upgrade-9b34558b947330a3bd25eec1f2f2e55c
[Snyk] Upgrade aws-sdk from 2.1358.0 to 2.1360.0
2023-05-10 10:31:42 -04:00
ed3e53f9a3 Merge pull request from Infisical/snyk-upgrade-5eadfb75ba47539ba48a83549a83d185
[Snyk] Upgrade @godaddy/terminus from 4.11.2 to 4.12.0
2023-05-10 10:31:30 -04:00
9cb4d5abb7 improve docker compose and add standalone docs 2023-05-09 22:07:48 -04:00
efdd1e64c4 Merge pull request from Infisical/snyk-upgrade-852b174d6fb41e4afff7bae352b8818d
[Snyk] Upgrade @fortawesome/react-fontawesome from 0.1.19 to 0.2.0
2023-05-09 17:52:12 -07:00
5b3be6063f Merge pull request from akhilmhdh/feat/new-login
refactor(ui): changed frontend to normal i18n without SSR
2023-05-09 17:50:10 -07:00
12c399d4a9 fix typo in k8 docs 2023-05-09 17:34:20 -04:00
ecd17e1d6d refine k8 deploy docs 2023-05-09 17:32:57 -04:00
fb4c811414 update detailed kubernetes helm docs 2023-05-09 16:41:20 -04:00
3561c589b1 refactor(ui): changed frontend to normal i18n without SSR 2023-05-09 23:28:23 +05:30
420d71d923 add membership validate to folder get 2023-05-09 10:23:41 -04:00
3db5c040c3 fix: upgrade @godaddy/terminus from 4.11.2 to 4.12.0
Snyk has created this PR to upgrade @godaddy/terminus from 4.11.2 to 4.12.0.

See this package in npm:
https://www.npmjs.com/package/@godaddy/terminus

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-09 08:42:48 +00:00
b4f336a5bb fix: upgrade aws-sdk from 2.1358.0 to 2.1360.0
Snyk has created this PR to upgrade aws-sdk from 2.1358.0 to 2.1360.0.

See this package in npm:
https://www.npmjs.com/package/aws-sdk

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-09 08:42:43 +00:00
43e61c94f0 get folder by id 2023-05-08 21:01:26 -04:00
69fa4a80c5 update check for CLI update 2023-05-08 16:43:28 -04:00
cf9e8b8a6b patch login bug when override empty 2023-05-08 16:09:57 -04:00
c6d5498a42 add dangling prefix for aur 2023-05-08 10:59:24 -04:00
7aa5ef844c Update CLI usage docs to showcase the ability to inject environment variables in shell aliases 2023-05-08 01:04:35 -04:00
ad7972e7e1 Merge pull request from Infisical/snyk-upgrade-5957efd1bee99a5df1416be8165fe61a
[Snyk] Upgrade @aws-sdk/client-secrets-manager from 3.309.0 to 3.312.0
2023-05-07 19:13:46 -04:00
c6d8f24968 Merge pull request from Infisical/snyk-upgrade-2f346d9e1a2c15f0c0929d23f00e36e5
[Snyk] Upgrade @sentry/tracing from 7.47.0 to 7.48.0
2023-05-07 19:13:36 -04:00
d8ff0bef0d add semantic-version gh action 2023-05-07 19:09:24 -04:00
29b96246b9 add back osx cross build 2023-05-07 17:00:12 -04:00
8503c9355b add completions for aurs 2023-05-07 16:55:42 -04:00
ddf0a272f6 back out of dir for archive file 2023-05-07 15:41:23 -04:00
e3980f8666 bring back completions and man page for cli 2023-05-07 15:27:19 -04:00
d52534b185 Dashboard UI update 2023-05-07 12:24:40 -07:00
db07a033e1 Add filter query param to getApps for Netlify integration 2023-05-06 22:19:00 +03:00
3c71bcaa8d fix: upgrade @sentry/tracing from 7.47.0 to 7.48.0
Snyk has created this PR to upgrade @sentry/tracing from 7.47.0 to 7.48.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/tracing

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-06 01:50:17 +00:00
476d0be101 Merge pull request from akhilmhdh/fix/token-cache-change
fix(ui): resolved token missing due to cache invalidation
2023-05-05 14:53:23 -07:00
2eff7b6128 set internal port for render 2023-05-05 16:26:03 -04:00
d8a781af1f remove health check 2023-05-05 16:06:28 -04:00
8b42f4f998 typo in doc 2023-05-05 15:50:19 -04:00
da127a3c0a update step 2 of fly.io 2023-05-05 15:48:03 -04:00
d4aa75a182 update self hosting docs layout 2023-05-05 15:42:41 -04:00
d097003e9b set sync=false for mongo db url render 2023-05-05 14:37:24 -04:00
b615a5084e update render IaC template 2023-05-05 14:31:11 -04:00
379f086828 add render IaC 2023-05-05 14:28:25 -04:00
f11a7d0f87 fix(ui): resolved token missing due to cache invalidation 2023-05-05 21:56:26 +05:30
f5aeb85c62 rename standalone docker image 2023-05-05 08:43:57 -04:00
2966aa6eda Merge pull request from akhilmhdh/feat/dashboard-v2
feat(ui): fixed lagging issues with new dashboard
2023-05-04 15:38:00 -07:00
b1f2515731 fixed minor bugs and updated the design 2023-05-04 15:31:06 -07:00
c5094ec37d patch copy invite link 2023-05-04 18:27:09 -04:00
6c745f617d add org id to complete invite link 2023-05-04 17:50:36 -04:00
82995fbd02 feat(ui): fixed lagging issues with new dashboard 2023-05-04 20:45:26 +05:30
8d09a45454 fix: upgrade @aws-sdk/client-secrets-manager from 3.309.0 to 3.312.0
Snyk has created this PR to upgrade @aws-sdk/client-secrets-manager from 3.309.0 to 3.312.0.

See this package in npm:
https://www.npmjs.com/package/@aws-sdk/client-secrets-manager

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-04 00:03:35 +00:00
38f578c4ae Fixed the issue with favicon 2023-05-03 16:06:50 -07:00
65b12eee5e update standlone gwf 2023-05-03 17:22:32 -04:00
9043db4727 add github workflow to release standalone app 2023-05-03 17:14:24 -04:00
0eceeb6aa9 create standalone infisical docker file 2023-05-03 16:57:19 -04:00
2d2bbbd0ad Update README.md 2023-05-03 15:51:15 -04:00
c9b4e11539 add note to ENCRYPTION_KEY to indicate non prod 2023-05-03 15:48:20 -04:00
fd4ea97e18 remove default smtp since Infisical no longer requires SMTP 2023-05-03 15:45:16 -04:00
49d2ecc460 switch install command to run prod docker compose 2023-05-03 15:41:11 -04:00
ca31a70032 Merge pull request from Infisical/gmail-smtp-support
Add support for Gmail SMTP + docs
2023-05-03 18:34:49 +03:00
3334338eaa Add Gmail SMTP option + docs 2023-05-03 18:28:20 +03:00
6d5e281811 add helm version requirement 2023-05-02 11:11:41 -04:00
87d36ac47a Merge pull request from Infisical/snyk-upgrade-78c720000b2ea0a6b50d66fd8a2a84f9
[Snyk] Upgrade bigint-conversion from 2.3.0 to 2.4.0
2023-05-01 20:44:29 -04:00
b72e1198df Merge pull request from Infisical/snyk-upgrade-965bd6eb4d7e75fef5c7e8cb5d4a3e5a
[Snyk] Upgrade mongoose from 6.10.4 to 6.10.5
2023-05-01 20:44:14 -04:00
837ea2ef40 add sem var to docker image workflow 2023-05-01 20:43:28 -04:00
b462ca3e89 Patch missing function invocation for GitLab envar 2023-05-01 22:38:01 +03:00
f639f682c9 Merge pull request from Spelchure/removing-sentry-logs
Replace Sentry error handling logic
2023-05-01 22:35:11 +03:00
365fcb3044 fix: upgrade mongoose from 6.10.4 to 6.10.5
Snyk has created this PR to upgrade mongoose from 6.10.4 to 6.10.5.

See this package in npm:
https://www.npmjs.com/package/mongoose

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-01 17:58:22 +00:00
01d9695153 fix: upgrade bigint-conversion from 2.3.0 to 2.4.0
Snyk has created this PR to upgrade bigint-conversion from 2.3.0 to 2.4.0.

See this package in npm:
https://www.npmjs.com/package/bigint-conversion

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/35057e82-ed7d-4e19-ba4d-719a42135cd6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-05-01 17:58:17 +00:00
21eb1815c4 feat: remove try-catch blocks for handling errors in middleware 2023-05-01 17:14:39 +03:00
85f3ae95b6 Merge pull request from Infisical/update-docs
Add local machine to deployment options
2023-05-01 16:04:11 +03:00
e888eed1bf Add local machine to deployment options 2023-05-01 16:02:46 +03:00
addac63700 fix broken link for start guide 2023-04-30 15:56:10 -04:00
fd9387a25e fix: upgrade fs from 0.0.1-security to 0.0.2
Snyk has created this PR to upgrade fs from 0.0.1-security to 0.0.2.

See this package in npm:
https://www.npmjs.com/package/fs

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/53d4ecb6-6cc1-4918-aa73-bf9cae4ffd13?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-28 20:41:59 +00:00
b17a40d83e fix: upgrade @fortawesome/react-fontawesome from 0.1.19 to 0.2.0
Snyk has created this PR to upgrade @fortawesome/react-fontawesome from 0.1.19 to 0.2.0.

See this package in npm:
https://www.npmjs.com/package/@fortawesome/react-fontawesome

See this project in Snyk:
https://app.snyk.io/org/maidul98/project/53d4ecb6-6cc1-4918-aa73-bf9cae4ffd13?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-04-28 20:41:56 +00:00
498 changed files with 21498 additions and 11290 deletions
.dockerignore.env.example
.github/workflows
.goreleaser.yaml
.husky
.pre-commit-config.yaml.pre-commit-hooks.yamlDockerfile.standalone-infisicalREADME.md
backend
cli
.infisicalignore
config
detect
go.modgo.summain.go
packages
report
testdata
baseline
config
expected
repos
nogit
small
staged
symlinks
file_symlink
source_file
tmp
docker-compose.dev.yml
docs
ecosystem.config.js
frontend
next-i18next.config.jsnext.config.jspackage-lock.jsonpackage.json
public/locales
en
es
fr
ko
pt-BR
tr
src
components
const.ts
ee/components
hooks/api
i18n.ts
layouts/AppLayout
AppLayout.tsx
components/NavBar
pages
reactQuery.ts
views
DashboardPage
Settings
OrgSettingsPage
OrgSettingsPage.tsx
components
OrgMembersTable
OrgNameChangeSection
OrgServiceAccountsTable
ProjectSettingsPage
ProjectSettingsPage.tsx
components
AutoCapitalizationSection
CopyProjectIDSection
ProjectIndexSecretsSection
ProjectNameChangeSection
ServiceTokenSection
nginx
render.yaml

2
.dockerignore Normal file

@ -0,0 +1,2 @@
backend/node_modules
frontend/node_modules

@ -1,5 +1,6 @@
# Keys
# Required key for platform encryption/decryption ops
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NOT BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# JWT
@ -30,14 +31,12 @@ MONGO_PASSWORD=example
# Required
SITE_URL=http://localhost:8080
# Mail/SMTP
SMTP_HOST=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_PORT=587
SMTP_SECURE=false
SMTP_FROM_ADDRESS=
SMTP_FROM_NAME=Infisical
# Mail/SMTP
SMTP_HOST=
SMTP_PORT=
SMTP_NAME=
SMTP_USERNAME=
SMTP_PASSWORD=
# Integration
# Optional only if integration is used

@ -13,6 +13,7 @@ jobs:
check-be-pr:
name: Check
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: ☁️ Checkout source
@ -26,17 +27,17 @@ jobs:
- name: 📦 Install dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
- name: 📁 Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: be-test-results
path: |
./backend/reports
./backend/coverage
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
# - name: 📁 Upload test results
# uses: actions/upload-artifact@v3
# if: always()
# with:
# name: be-test-results
# path: |
# ./backend/reports
# ./backend/coverage
- name: 🏗️ Run build
run: npm run build
working-directory: backend

@ -2,40 +2,35 @@ name: Check Frontend Pull Request
on:
pull_request:
types: [ opened, synchronize ]
types: [opened, synchronize]
paths:
- 'frontend/**'
- '!frontend/README.md'
- '!frontend/.*'
- 'frontend/.eslintrc.js'
- "frontend/**"
- "!frontend/README.md"
- "!frontend/.*"
- "frontend/.eslintrc.js"
jobs:
check-fe-pr:
name: Check
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
-
name: ☁️ Checkout source
- name: ☁️ Checkout source
uses: actions/checkout@v3
-
name: 🔧 Setup Node 16
- name: 🔧 Setup Node 16
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
node-version: "16"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
-
name: 📦 Install dependencies
- name: 📦 Install dependencies
run: npm ci --only-production --ignore-scripts
working-directory: frontend
# -
# name: 🧪 Run tests
# run: npm run test:ci
# working-directory: frontend
-
name: 🏗️ Run build
- name: 🏗️ Run build
run: npm run build
working-directory: frontend

@ -1,12 +1,17 @@
name: Build, Publish and Deploy to Gamma
on: [workflow_dispatch]
on:
push:
tags:
- "infisical/v*.*.*"
jobs:
backend-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
@ -51,15 +56,19 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: backend
tags: infisical/backend:${{ steps.commit.outputs.short }},
tags: |
infisical/backend:${{ steps.commit.outputs.short }}
infisical/backend:latest
infisical/backend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
frontend-image:
name: Build frontend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Save commit hashes for tag
@ -100,8 +109,10 @@ jobs:
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: infisical/frontend:${{ steps.commit.outputs.short }},
tags: |
infisical/frontend:${{ steps.commit.outputs.short }}
infisical/frontend:latest
infisical/frontend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}

@ -0,0 +1,68 @@
name: Release standalone docker image
on: [workflow_dispatch]
jobs:
infisical-standalone:
name: Build infisical standalone image
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- uses: paulhatch/semantic-version@v5.0.2
id: version
with:
# The prefix to use to identify tags
tag_prefix: "infisical-standalone/v"
# A string which, if present in a git commit, indicates that a change represents a
# major (breaking) change, supports regular expressions wrapped with '/'
major_pattern: "(MAJOR)"
# Same as above except indicating a minor change, supports regular expressions wrapped with '/'
minor_pattern: "(MINOR)"
# A string to determine the format of the version output
version_format: "${major}.${minor}.${patch}-prerelease${increment}"
# Optional path to check for changes. If any changes are detected in the path the
# 'changed' output will true. Enter multiple paths separated by spaces.
change_path: "backend,frontend"
# Prevents pre-v1.0.0 version from automatically incrementing the major version.
# If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the version_type output is unchanged.
enable_prerelease_mode: true
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: version output
run: |
echo "Output Value: ${{ steps.version.outputs.major }}"
echo "Output Value: ${{ steps.version.outputs.minor }}"
echo "Output Value: ${{ steps.version.outputs.patch }}"
echo "Output Value: ${{ steps.version.outputs.version }}"
echo "Output Value: ${{ steps.version.outputs.version_type }}"
echo "Output Value: ${{ steps.version.outputs.increment }}"
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
tags: |
infisical/infisical:latest
infisical/infisical:${{ steps.commit.outputs.short }}
platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical

@ -46,6 +46,7 @@ jobs:
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 }}

@ -18,7 +18,9 @@ monorepo:
builds:
- id: darwin-build
binary: infisical
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
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:
@ -36,7 +38,9 @@ builds:
env:
- CGO_ENABLED=0
binary: infisical
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
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:
@ -65,10 +69,10 @@ archives:
- goos: windows
format: zip
files:
- README*
- LICENSE*
- manpages/*
- completions/*
- ../README*
- ../LICENSE*
- ../manpages/*
- ../completions/*
release:
replace_existing_draft: true
@ -98,12 +102,12 @@ brews:
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"
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
@ -121,15 +125,15 @@ nfpms:
- 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
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:
@ -156,6 +160,15 @@ aurs:
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: cli/docker/Dockerfile

@ -3,3 +3,5 @@
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
infisical scan git-changes --staged -v

5
.pre-commit-config.yaml Normal file

@ -0,0 +1,5 @@
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.16.3
hooks:
- id: gitleaks

6
.pre-commit-hooks.yaml Normal file

@ -0,0 +1,6 @@
- id: infisical-scan
name: Scan for hardcoded secrets
description: Will scan for hardcoded secrets using Infisical CLI
entry: infisical scan git-changes --verbose --redact --staged
language: golang
pass_filenames: false

@ -0,0 +1,102 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
FROM node:16-alpine AS frontend-dependencies
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
# Install dependencies
RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed
FROM node:16-alpine AS frontend-builder
WORKDIR /app
# Copy dependencies
COPY --from=frontend-dependencies /app/node_modules ./node_modules
# Copy all files
COPY /frontend .
ENV NODE_ENV production
ENV NEXT_PUBLIC_ENV production
ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
# Build
RUN npm run build
# Production image
FROM node:16-alpine AS frontend-runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown nextjs:nodejs ./public/data
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
FROM node:16-alpine AS backend-build
WORKDIR /app
COPY backend/package*.json ./
RUN npm ci --only-production
COPY /backend .
RUN npm run build
# Production stage
FROM node:16-alpine AS backend-runner
WORKDIR /app
COPY backend/package*.json ./
RUN npm ci --only-production
COPY --from=backend-build /app .
# Production stage
FROM node:16-alpine AS production
WORKDIR /
# Install PM2
RUN npm install -g pm2
# Copy ecosystem.config.js
COPY ecosystem.config.js .
RUN apk add --no-cache nginx
COPY nginx/default-stand-alone-docker.conf /etc/nginx/nginx.conf
COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app/ /app/
EXPOSE 80
ENV HTTPS_ENABLED false
CMD ["pm2-runtime", "start", "ecosystem.config.js"]

@ -78,15 +78,34 @@ To set up and run Infisical locally, make sure you have Git and Docker installed
Linux/macOS:
```console
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.dev.yml up --build
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.yml up
```
Windows Command Prompt:
```console
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.dev.yml up --build
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.yml up
```
Create an account at `http://localhost:80`
### Scan and prevent secret leaks
On top managing secrets with Infisical, you can also scan for over 140+ secret types in your files, directories and git repositories.
To scan your full git history, run:
```
infisical scan --verbose
```
Install pre commit hook to scan each commit before you push to your repository
```
infisical scan install --pre-commit-hook
```
Lean about Infisical's code scanning feature [here](https://infisical.com/docs/cli/scanning-overview)
## Open-source vs. paid

1981
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,19 +1,19 @@
{
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.309.0",
"@godaddy/terminus": "^4.11.2",
"@aws-sdk/client-secrets-manager": "^3.319.0",
"@godaddy/terminus": "^4.12.0",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.41.0",
"@sentry/tracing": "^7.47.0",
"@sentry/node": "^7.49.0",
"@sentry/tracing": "^7.48.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"argon2": "^0.30.3",
"await-to-js": "^3.0.0",
"aws-sdk": "^2.1338.0",
"aws-sdk": "^2.1364.0",
"axios": "^1.3.5",
"axios-retry": "^3.4.0",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2",
"bigint-conversion": "^2.4.0",
"builder-pattern": "^2.2.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
@ -30,7 +30,8 @@
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.10.4",
"mongoose": "^6.10.5",
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.6.0",
"query-string": "^7.1.3",

@ -5,7 +5,7 @@ const client = new InfisicalClient({
});
export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000;
export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue == undefined ? false : (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue;
export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue === 'true'
export const getEncryptionKey = async () => (await client.getSecret('ENCRYPTION_KEY')).secretValue;
export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
@ -45,12 +45,19 @@ export const getSmtpUsername = async () => (await client.getSecret('SMTP_USERNAM
export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWORD')).secretValue;
export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue;
export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical';
export const getLicenseKey = async () => (await client.getSecret('LICENSE_KEY')).secretValue;
export const getLicenseServerKey = async () => (await client.getSecret('LICENSE_SERVER_KEY')).secretValue;
export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com';
// TODO: deprecate from here
export const getStripeProductStarter = async () => (await client.getSecret('STRIPE_PRODUCT_STARTER')).secretValue;
export const getStripeProductPro = async () => (await client.getSecret('STRIPE_PRODUCT_PRO')).secretValue;
export const getStripeProductTeam = async () => (await client.getSecret('STRIPE_PRODUCT_TEAM')).secretValue;
export const getStripePublishableKey = async () => (await client.getSecret('STRIPE_PUBLISHABLE_KEY')).secretValue;
export const getStripeSecretKey = async () => (await client.getSecret('STRIPE_SECRET_KEY')).secretValue;
export const getStripeWebhookSecret = async () => (await client.getSecret('STRIPE_WEBHOOK_SECRET')).secretValue;
export const getTelemetryEnabled = async () => (await client.getSecret('TELEMETRY_ENABLED')).secretValue !== 'false' && true;
export const getLoopsApiKey = async () => (await client.getSecret('LOOPS_API_KEY')).secretValue;
export const getSmtpConfigured = async () => (await client.getSecret('SMTP_HOST')).secretValue == '' || (await client.getSecret('SMTP_HOST')).secretValue == undefined ? false : true

@ -1,10 +1,24 @@
import axios from 'axios';
import axiosRetry from 'axios-retry';
import {
getLicenseServerKeyAuthToken,
setLicenseServerKeyAuthToken,
getLicenseKeyAuthToken,
setLicenseKeyAuthToken
} from './storage';
import {
getLicenseKey,
getLicenseServerKey,
getLicenseServerUrl
} from './index';
const axiosInstance = axios.create();
// should have JWT to interact with the license server
export const licenseServerKeyRequest = axios.create();
export const licenseKeyRequest = axios.create();
export const standardRequest = axios.create();
// add retry functionality to the axios instance
axiosRetry(axiosInstance, {
axiosRetry(standardRequest, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
retryCondition: (error) => {
@ -13,4 +27,98 @@ axiosRetry(axiosInstance, {
},
});
export default axiosInstance;
export const refreshLicenseServerKeyToken = async () => {
const licenseServerKey = await getLicenseServerKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-server-login`, {},
{
headers: {
'X-API-KEY': licenseServerKey
}
}
);
setLicenseServerKeyAuthToken(token);
return token;
}
export const refreshLicenseKeyToken = async () => {
const licenseKey = await getLicenseKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-login`, {},
{
headers: {
'X-API-KEY': licenseKey
}
}
);
setLicenseKeyAuthToken(token);
return token;
}
licenseServerKeyRequest.interceptors.request.use((config) => {
const token = getLicenseServerKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseServerKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseServerKeyToken();
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
return licenseServerKeyRequest(originalRequest);
}
return Promise.reject(err);
});
licenseKeyRequest.interceptors.request.use((config) => {
const token = getLicenseKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseKeyToken();
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
return licenseKeyRequest(originalRequest);
}
return Promise.reject(err);
});

@ -0,0 +1,30 @@
const MemoryLicenseServerKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken
};
};
const MemoryLicenseKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken
};
};
const licenseServerTokenStorage = MemoryLicenseServerKeyTokenStorage();
const licenseTokenStorage = MemoryLicenseKeyTokenStorage();
export const getLicenseServerKeyAuthToken = licenseServerTokenStorage.getToken;
export const setLicenseServerKeyAuthToken = licenseServerTokenStorage.setToken;
export const getLicenseKeyAuthToken = licenseTokenStorage.getToken;
export const setLicenseKeyAuthToken = licenseTokenStorage.setToken;

@ -16,7 +16,7 @@ import {
INTEGRATION_VERCEL_API_URL,
INTEGRATION_RAILWAY_API_URL
} from '../../variables';
import request from '../../config/request';
import { standardRequest } from '../../config/request';
/***
* Return integration authorization with id [integrationAuthId]
@ -229,7 +229,7 @@ export const getIntegrationAuthVercelBranches = async (req: Request, res: Respon
let branches: string[] = [];
if (appId && appId !== '') {
const { data }: { data: VercelBranch[] } = await request.get(
const { data }: { data: VercelBranch[] } = await standardRequest.get(
`${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`,
{
params,
@ -292,7 +292,7 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R
projectId: appId
}
const { data: { data: { environments: { edges } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
const { data: { data: { environments: { edges } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables,
}, {
@ -372,7 +372,7 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
id: appId
}
const { data: { data: { project: { services: { edges } } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
const { data: { data: { project: { services: { edges } } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables
}, {

@ -135,6 +135,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
}
if (!inviteeMembershipOrg) {
await new MembershipOrg({
user: invitee,
inviteEmail: inviteeEmail,
@ -188,7 +189,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
});
if (!(await getSmtpConfigured())) {
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}`
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
}
}
@ -217,10 +218,10 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user, token;
try {
const {
email,
const {
email,
organizationId,
code
code
} = req.body;
user = await User.findOne({ email }).select('+publicKey');
@ -246,6 +247,10 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
// membership can be approved and redirected to login/dashboard
membershipOrg.status = ACCEPTED;
await membershipOrg.save();
await updateSubscriptionOrgQuantity({
organizationId
});
return res.status(200).send({
message: 'Successfully verified email',

@ -19,7 +19,8 @@ export const getOrganizations = async (req: Request, res: Response) => {
try {
organizations = (
await MembershipOrg.find({
user: req.user._id
user: req.user._id,
status: ACCEPTED
}).populate('organization')
).map((m) => m.organization);
} catch (err) {

@ -86,4 +86,22 @@ export const deleteFolder = async (req: Request, res: Response) => {
}
res.send()
}
// TODO: validate workspace
export const getFolderById = async (req: Request, res: Response) => {
const { folderId } = req.params
const folder = await Folder.findById(folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" })
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId: folder.workspace as any,
acceptedRoles: [ADMIN, MEMBER]
});
res.send({ folder })
}

@ -21,14 +21,6 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
try {
email = req.body.email;
if (await getInviteOnlySignup()) {
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
const userCount = await User.countDocuments({})
if (userCount != 0) {
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
}
}
const user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) {
// case: user has already completed account
@ -74,6 +66,14 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
});
}
if (await getInviteOnlySignup()) {
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
const userCount = await User.countDocuments({})
if (userCount != 0) {
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
}
}
// verify email
if (await getSmtpConfigured()) {
await checkEmailVerification({

@ -1,26 +1,24 @@
import to from 'await-to-js';
import { Types } from 'mongoose';
import { Request, Response } from 'express';
import { ISecret, Secret } from '../../models';
import { ISecret, Secret, Workspace } from '../../models';
import { IAction, SecretVersion } from '../../ee/models';
import {
SECRET_PERSONAL,
SECRET_SHARED,
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS
} from '../../variables';
import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../../utils/errors';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services';
import { EESecretService, EELogService, EELicenseService } from '../../ee/services';
import { TelemetryService, SecretService } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog';
import { PERMISSION_WRITE_SECRETS } from '../../variables';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag';
import _, { eq } from 'lodash';
import _ from 'lodash';
import {
BatchSecretRequest,
BatchSecret
@ -48,6 +46,12 @@ export const batchSecrets = async (req: Request, res: Response) => {
environment: string;
requests: BatchSecretRequest[];
} = req.body;
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1;
const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = [];
@ -139,7 +143,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
environment,
workspaceId,
channel,
userAgent: req.headers?.['user-agent']
userAgent: req.headers?.['user-agent'],
isPaid
}
});
}
@ -226,7 +231,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
environment,
workspaceId,
channel,
userAgent: req.headers?.['user-agent']
userAgent: req.headers?.['user-agent'],
isPaid
}
});
}
@ -261,7 +267,8 @@ export const batchSecrets = async (req: Request, res: Response) => {
environment,
workspaceId,
channel: channel,
userAgent: req.headers?.['user-agent']
userAgent: req.headers?.['user-agent'],
isPaid
}
});
}
@ -376,6 +383,12 @@ export const createSecrets = async (req: Request, res: Response) => {
}
}
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1;
let listOfSecretsToCreate;
if (Array.isArray(req.body.secrets)) {
// case: create multiple secrets
@ -531,7 +544,8 @@ export const createSecrets = async (req: Request, res: Response) => {
environment,
workspaceId,
channel: channel,
userAgent: req.headers?.['user-agent']
userAgent: req.headers?.['user-agent'],
isPaid
}
});
}
@ -595,6 +609,12 @@ export const getSecrets = async (req: Request, res: Response) => {
const normalizedPath = normalizePath(secretsPath as string)
const folders = await getFoldersInDirectory(workspaceId as string, environment as string, normalizedPath)
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1;
// secrets to return
let secrets: ISecret[] = [];
@ -727,7 +747,8 @@ export const getSecrets = async (req: Request, res: Response) => {
environment,
workspaceId,
channel,
userAgent: req.headers?.['user-agent']
userAgent: req.headers?.['user-agent'],
isPaid
}
});
}
@ -938,6 +959,12 @@ export const updateSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key)
})
const workspace = await Workspace.findById(key);
if (!workspace) throw WorkspaceNotFoundError();
const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString());
const isPaid = orgPlan.tier < 1;
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
@ -950,7 +977,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
environment: workspaceSecretObj[key][0].environment,
workspaceId: key,
channel: channel,
userAgent: req.headers?.['user-agent']
userAgent: req.headers?.['user-agent'],
isPaid
}
});
}
@ -1072,6 +1100,14 @@ export const deleteSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(key)
});
const organizationId = (
await Workspace.findOne({
_id: key
})
)?.organization?.toString();
const orgPlan = await EELicenseService.getOrganizationPlan(organizationId || '');
const isPaid = orgPlan.slug != 'starter';
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
@ -1084,7 +1120,8 @@ export const deleteSecrets = async (req: Request, res: Response) => {
environment: workspaceSecretObj[key][0].environment,
workspaceId: key,
channel: channel,
userAgent: req.headers?.['user-agent']
userAgent: req.headers?.['user-agent'],
isPaid
}
});
}

@ -7,8 +7,9 @@ import {
} from '../../helpers/signup';
import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import request from '../../config/request';
import { standardRequest } from '../../config/request';
import { getLoopsApiKey, getHttpsEnabled } from '../../config';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
/**
* Complete setting up user by adding their personal and auth information as part of the
@ -87,6 +88,19 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
user
});
// update organization membership statuses that are
// invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email,
status: INVITED
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
});
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
@ -109,7 +123,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await request.post("https://app.loops.so/api/v1/events/send", {
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
"firstName": firstName,
@ -206,9 +220,20 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
if (!user)
throw new Error('Failed to complete account for non-existent user');
// update organization membership statuses that are
// invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email,
status: INVITED
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
});
await MembershipOrg.updateMany(
{
inviteEmail: email,

@ -0,0 +1,34 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import { EELicenseService } from '../../services';
import { getLicenseServerUrl } from '../../../config';
import { licenseServerKeyRequest } from '../../../config/request';
/**
* Return available cloud product information.
* Note: Nicely formatted to easily construct a table from
* @param req
* @param res
* @returns
*/
export const getCloudProducts = async (req: Request, res: Response) => {
try {
const billingCycle = req.query['billing-cycle'] as string;
if (EELicenseService.instanceType === 'cloud') {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
return res.status(200).send(data);
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
}
return res.status(200).send({
head: [],
rows: []
});
}

@ -1,15 +1,19 @@
import * as stripeController from './stripeController';
import * as secretController from './secretController';
import * as secretSnapshotController from './secretSnapshotController';
import * as organizationsController from './organizationsController';
import * as workspaceController from './workspaceController';
import * as actionController from './actionController';
import * as membershipController from './membershipController';
import * as cloudProductsController from './cloudProductsController';
export {
stripeController,
secretController,
secretSnapshotController,
organizationsController,
workspaceController,
actionController,
membershipController
membershipController,
cloudProductsController
}

@ -0,0 +1,84 @@
import { Request, Response } from 'express';
import { getLicenseServerUrl } from '../../../config';
import { licenseServerKeyRequest } from '../../../config/request';
import { EELicenseService } from '../../services';
/**
* Return the organization's current plan and allowed feature set
*/
export const getOrganizationPlan = async (req: Request, res: Response) => {
const plan = await EELicenseService.getOrganizationPlan(req.organization._id.toString());
// cache fetched plan for organization
EELicenseService.localFeatureSet.set(req.organization._id.toString(), plan);
return res.status(200).send({
plan
});
}
/**
* Update the organization plan to product with id [productId]
* @param req
* @param res
* @returns
*/
export const updateOrganizationPlan = async (req: Request, res: Response) => {
const {
productId
} = req.body;
const { data } = await licenseServerKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan`,
{
productId
}
);
return res.status(200).send(data);
}
/**
* Return the organization's payment methods on file
*/
export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
const { data: { pmtMethods } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`
);
return res.status(200).send({
pmtMethods
});
}
/**
* Return a Stripe session URL to add payment method for organization
*/
export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
const {
success_url,
cancel_url
} = req.body;
const { data: { url } } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`,
{
success_url,
cancel_url
}
);
return res.status(200).send({
url
});
}
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
const { pmtMethodId } = req.params;
const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`,
);
return res.status(200).send(data);
}

@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Action } from '../models';
import {
@ -36,33 +35,25 @@ const createActionUpdateSecret = async ({
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
let action;
try {
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
secretIds,
n: 2
}))
.map((s) => ({
oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id
}));
action = await new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create update secret action');
}
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
secretIds,
n: 2
}))
.map((s) => ({
oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id
}));
const action = await new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
}).save();
return action;
}
@ -90,33 +81,25 @@ const createActionSecret = async ({
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
let action;
try {
// case: action is adding, deleting, or reading secrets
// -> add new secret versions
const latestSecretVersions = (await getLatestSecretVersionIds({
secretIds
}))
.map((s) => ({
newSecretVersion: s.versionId
}));
action = await new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action create/read/delete secret action');
}
// case: action is adding, deleting, or reading secrets
// -> add new secret versions
const latestSecretVersions = (await getLatestSecretVersionIds({
secretIds
}))
.map((s) => ({
newSecretVersion: s.versionId
}));
const action = await new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
}).save();
return action;
}
@ -140,19 +123,12 @@ const createActionClient = ({
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
}) => {
let action;
try {
action = new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create client action');
}
const action = new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId
}).save();
return action;
}
@ -181,40 +157,34 @@ const createActionHelper = async ({
secretIds?: Types.ObjectId[];
}) => {
let action;
try {
switch (name) {
case ACTION_LOGIN:
case ACTION_LOGOUT:
action = await createActionClient({
name,
userId
});
break;
case ACTION_ADD_SECRETS:
case ACTION_READ_SECRETS:
case ACTION_DELETE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionSecret({
name,
userId,
workspaceId,
secretIds
});
break;
case ACTION_UPDATE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionUpdateSecret({
name,
userId,
workspaceId,
secretIds
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action');
switch (name) {
case ACTION_LOGIN:
case ACTION_LOGOUT:
action = await createActionClient({
name,
userId
});
break;
case ACTION_ADD_SECRETS:
case ACTION_READ_SECRETS:
case ACTION_DELETE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionSecret({
name,
userId,
workspaceId,
secretIds
});
break;
case ACTION_UPDATE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionUpdateSecret({
name,
userId,
workspaceId,
secretIds
});
break;
}
return action;
@ -222,4 +192,4 @@ const createActionHelper = async ({
export {
createActionHelper
};
};

@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Log,
@ -32,27 +31,20 @@ const createLogHelper = async ({
channel: string;
ipAddress: string;
}) => {
let log;
try {
log = await new Log({
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name),
actions,
channel,
ipAddress
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create log');
}
const log = await new Log({
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name),
actions,
channel,
ipAddress
}).save();
return log;
}
export {
createLogHelper
}
}

@ -1,14 +1,6 @@
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Secret,
ISecret,
} from '../../models';
import {
SecretSnapshot,
SecretVersion,
ISecretVersion
} from '../models';
import { Types } from "mongoose";
import { Secret, ISecret } from "../../models";
import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
/**
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
@ -19,56 +11,53 @@ import {
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
*/
const takeSecretSnapshotHelper = async ({
workspaceId
workspaceId,
}: {
workspaceId: Types.ObjectId;
workspaceId: Types.ObjectId;
}) => {
const secretIds = (
await Secret.find(
{
workspace: workspaceId,
},
"_id"
)
).map((s) => s._id);
let secretSnapshot;
try {
const secretIds = (await Secret.find({
workspace: workspaceId
}, '_id')).map((s) => s._id);
const latestSecretVersions = (
await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
},
},
},
{
$group: {
_id: "$secret",
version: { $max: "$version" },
versionId: { $max: "$_id" }, // secret version id
},
},
{
$sort: { version: -1 },
},
]).exec()
).map((s) => s.versionId);
const latestSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // secret version id
}
},
{
$sort: { version: -1 }
}
])
.exec())
.map((s) => s.versionId);
const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
}).sort({ version: -1 });
const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId
}).sort({ version: -1 });
const secretSnapshot = await new SecretSnapshot({
workspace: workspaceId,
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions,
}).save();
secretSnapshot = await new SecretSnapshot({
workspace: workspaceId,
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to take a secret snapshot');
}
return secretSnapshot;
}
return secretSnapshot;
};
/**
* Add secret versions [secretVersions] to the SecretVersion collection.
@ -77,93 +66,79 @@ const takeSecretSnapshotHelper = async ({
* @returns {SecretVersion[]} newSecretVersions - new secret versions
*/
const addSecretVersionsHelper = async ({
secretVersions
secretVersions,
}: {
secretVersions: ISecretVersion[]
secretVersions: ISecretVersion[];
}) => {
let newSecretVersions;
try {
newSecretVersions = await SecretVersion.insertMany(secretVersions);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(`Failed to add secret versions [err=${err}]`);
}
const newSecretVersions = await SecretVersion.insertMany(secretVersions);
return newSecretVersions;
}
return newSecretVersions;
};
const markDeletedSecretVersionsHelper = async ({
secretIds
secretIds,
}: {
secretIds: Types.ObjectId[];
secretIds: Types.ObjectId[];
}) => {
try {
await SecretVersion.updateMany({
secret: { $in: secretIds }
}, {
isDeleted: true
}, {
new: true
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to mark secret versions as deleted');
}
}
await SecretVersion.updateMany(
{
secret: { $in: secretIds },
},
{
isDeleted: true,
},
{
new: true,
}
);
};
/**
* Initialize secret versioning by setting previously unversioned
* secrets to version 1 and begin populating secret versions.
*/
const initSecretVersioningHelper = async () => {
try {
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: "secretversions",
localField: "_id",
foreignField: "secret",
as: "versions",
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: 'secretversions',
localField: '_id',
foreignField: 'secret',
as: 'versions',
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map((s, idx) => new SecretVersion({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment
}))
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to ensure that secrets are versioned');
}
}
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map(
(s, idx) =>
new SecretVersion({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment,
})
),
});
}
};
export {
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper
}
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper,
};

@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { SecretVersion } from '../models';
@ -13,41 +12,32 @@ const getLatestSecretVersionIds = async ({
}: {
secretIds: Types.ObjectId[];
}) => {
interface LatestSecretVersionId {
_id: Types.ObjectId;
version: number;
versionId: Types.ObjectId;
}
let latestSecretVersionIds: LatestSecretVersionId[];
try {
latestSecretVersionIds = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // id of latest secret version
}
},
{
$sort: { version: -1 }
const latestSecretVersionIds = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
}
])
.exec());
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get latest secret versions');
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // id of latest secret version
}
},
{
$sort: { version: -1 }
}
])
.exec());
return latestSecretVersionIds;
}
@ -66,40 +56,32 @@ const getLatestNSecretSecretVersionIds = async ({
secretIds: Types.ObjectId[];
n: number;
}) => {
// TODO: optimize query
let latestNSecretVersions;
try {
latestNSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
},
},
const latestNSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
},
{
$sort: { version: -1 },
},
{
$group: {
_id: "$secret",
versions: { $push: "$$ROOT" },
},
},
{
$sort: { version: -1 },
},
{
$group: {
_id: "$secret",
versions: { $push: "$$ROOT" },
},
{
$project: {
_id: 0,
secret: "$_id",
versions: { $slice: ["$versions", n] },
},
}
]));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get latest n secret versions');
}
},
{
$project: {
_id: 0,
secret: "$_id",
versions: { $slice: ["$versions", n] },
},
}
]));
return latestNSecretVersions;
}

@ -0,0 +1,20 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
validateRequest
} from '../../../middleware';
import { query } from 'express-validator';
import { cloudProductsController } from '../../controllers/v1';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
query('billing-cycle').exists().isIn(['monthly', 'yearly']),
validateRequest,
cloudProductsController.getCloudProducts
);
export default router;

@ -1,11 +1,15 @@
import secret from './secret';
import secretSnapshot from './secretSnapshot';
import organizations from './organizations';
import workspace from './workspace';
import action from './action';
import cloudProducts from './cloudProducts';
export {
secret,
secretSnapshot,
organizations,
workspace,
action
action,
cloudProducts
}

@ -0,0 +1,87 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireOrganizationAuth,
validateRequest
} from '../../../middleware';
import { param, body } from 'express-validator';
import { organizationsController } from '../../controllers/v1';
import {
OWNER, ADMIN, MEMBER, ACCEPTED
} from '../../../variables';
router.get(
'/:organizationId/plan',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
validateRequest,
organizationsController.getOrganizationPlan
);
router.patch(
'/:organizationId/plan',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
body('productId').exists().isString(),
validateRequest,
organizationsController.updateOrganizationPlan
);
router.get(
'/:organizationId/billing-details/payment-methods',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
validateRequest,
organizationsController.getOrganizationPmtMethods
);
router.post(
'/:organizationId/billing-details/payment-methods',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
body('success_url').exists().isString(),
body('cancel_url').exists().isString(),
validateRequest,
organizationsController.addOrganizationPmtMethod
);
router.delete(
'/:organizationId/billing-details/payment-methods/:pmtMethodId',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
validateRequest,
organizationsController.deleteOrganizationPmtMethod
);
export default router;

@ -7,7 +7,7 @@ import {
requireAuth,
validateRequest
} from '../../../middleware';
import { param, body } from 'express-validator';
import { param } from 'express-validator';
import { ADMIN, MEMBER } from '../../../variables';
import { secretSnapshotController } from '../../controllers/v1';

@ -1,12 +1,124 @@
import NodeCache from 'node-cache';
import * as Sentry from '@sentry/node';
import {
getLicenseKey,
getLicenseServerKey,
getLicenseServerUrl
} from '../../config';
import {
licenseKeyRequest,
licenseServerKeyRequest,
refreshLicenseServerKeyToken,
refreshLicenseKeyToken
} from '../../config/request';
import { Organization } from '../../models';
import { OrganizationNotFoundError } from '../../utils/errors';
interface FeatureSet {
_id: string | null;
slug: 'starter' | 'team' | 'pro' | 'enterprise' | null;
tier: number | null;
projectLimit: number | null;
memberLimit: number | null;
secretVersioning: boolean;
pitRecovery: boolean;
rbac: boolean;
customRateLimits: boolean;
customAlerts: boolean;
auditLogs: boolean;
}
/**
* Class to handle Enterprise Edition license actions
* Class to handle license/plan configurations:
* - Infisical Cloud: Fetch and cache customer plans in [localFeatureSet]
* - Self-hosted regular: Use default global feature set
* - Self-hosted enterprise: Fetch and update global feature set
*/
class EELicenseService {
private readonly _isLicenseValid: boolean;
private readonly _isLicenseValid: boolean; // TODO: deprecate
public instanceType: 'self-hosted' | 'enterprise-self-hosted' | 'cloud' = 'self-hosted';
public globalFeatureSet: FeatureSet = {
_id: null,
slug: null,
tier: -1,
projectLimit: null,
memberLimit: null,
secretVersioning: true,
pitRecovery: true,
rbac: true,
customRateLimits: true,
customAlerts: true,
auditLogs: false
}
public localFeatureSet: NodeCache;
constructor(licenseKey: string) {
constructor() {
this._isLicenseValid = true;
this.localFeatureSet = new NodeCache({
stdTTL: 300
});
}
public async getOrganizationPlan(organizationId: string) {
try {
if (this.instanceType === 'cloud') {
const cachedPlan = this.localFeatureSet.get(organizationId);
if (cachedPlan) return cachedPlan;
const organization = await Organization.findById(organizationId);
if (!organization) throw OrganizationNotFoundError();
const { data: { currentPlan } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`
);
return currentPlan;
}
} catch (err) {
return this.globalFeatureSet;
}
return this.globalFeatureSet;
}
public async initGlobalFeatureSet() {
const licenseServerKey = await getLicenseServerKey();
const licenseKey = await getLicenseKey();
try {
if (licenseServerKey) {
// license server key is present -> validate it
const token = await refreshLicenseServerKeyToken()
if (token) {
this.instanceType = 'cloud';
}
return;
}
if (licenseKey) {
// license key is present -> validate it
const token = await refreshLicenseKeyToken();
if (token) {
const { data: { currentPlan } } = await licenseKeyRequest.get(
`${await getLicenseServerUrl()}/api/license/v1/plan`
);
this.globalFeatureSet = currentPlan;
this.instanceType = 'enterprise-self-hosted';
}
}
} catch (err) {
// case: self-hosted free
Sentry.setUser(null);
Sentry.captureException(err);
}
}
public get isLicenseValid(): boolean {
@ -14,4 +126,4 @@ class EELicenseService {
}
}
export default new EELicenseService('N/A');
export default new EELicenseService();

@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';

@ -1,41 +1,34 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Types } from "mongoose";
import {
Bot,
BotKey,
Secret,
ISecret,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
generateKeyPair,
encryptSymmetric,
decryptSymmetric,
decryptAsymmetric
} from '../utils/crypto';
Bot,
BotKey,
Secret,
ISecret,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
} from "../models";
import {
SECRET_SHARED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { getEncryptionKey } from '../config';
import { BotNotFoundError, UnauthorizedRequestError } from '../utils/errors';
generateKeyPair,
encryptSymmetric,
decryptSymmetric,
decryptAsymmetric,
} from "../utils/crypto";
import {
validateMembership
} from '../helpers/membership';
import {
validateUserClientForWorkspace
} from '../helpers/user';
import {
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
SECRET_SHARED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
} from "../variables";
import { getEncryptionKey } from "../config";
import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors";
import { validateMembership } from "../helpers/membership";
import { validateUserClientForWorkspace } from "../helpers/user";
import { validateServiceAccountClientForWorkspace } from "../helpers/serviceAccount";
/**
* Validate authenticated clients for bot with id [botId] based
@ -46,99 +39,104 @@ import {
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/
const validateClientForBot = async ({
authData,
botId,
acceptedRoles
authData,
botId,
acceptedRoles,
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
botId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
botId: Types.ObjectId;
acceptedRoles: Array<"admin" | "member">;
}) => {
const bot = await Bot.findById(botId);
if (!bot) throw BotNotFoundError();
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles
});
return bot;
}
const bot = await Bot.findById(botId);
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: bot.workspace
});
if (!bot) throw BotNotFoundError();
return bot;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for bot'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles
});
return bot;
}
throw BotNotFoundError({
message: 'Failed client authorization for bot'
if (
authData.authMode === AUTH_MODE_JWT &&
authData.authPayload instanceof User
) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles,
});
}
return bot;
}
if (
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
authData.authPayload instanceof ServiceAccount
) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: bot.workspace,
});
return bot;
}
if (
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
authData.authPayload instanceof ServiceTokenData
) {
throw UnauthorizedRequestError({
message: "Failed service token authorization for bot",
});
}
if (
authData.authMode === AUTH_MODE_API_KEY &&
authData.authPayload instanceof User
) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles,
});
return bot;
}
throw BotNotFoundError({
message: "Failed client authorization for bot",
});
};
/**
* Create an inactive bot with name [name] for workspace with id [workspaceId]
* @param {Object} obj
* @param {Object} obj
* @param {String} obj.name - name of bot
* @param {String} obj.workspaceId - id of workspace that bot belongs to
*/
const createBot = async ({
name,
workspaceId,
name,
workspaceId,
}: {
name: string;
workspaceId: Types.ObjectId;
name: string;
workspaceId: Types.ObjectId;
}) => {
let bot;
try {
const { publicKey, privateKey } = generateKeyPair();
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: privateKey,
key: await getEncryptionKey()
});
const { publicKey, privateKey } = generateKeyPair();
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: privateKey,
key: await getEncryptionKey(),
});
bot = await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create bot');
}
return bot;
}
const bot = await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
}).save();
return bot;
};
/**
* Return decrypted secrets for workspace with id [workspaceId]
@ -148,125 +146,105 @@ const createBot = async ({
* @param {String} obj.environment - environment
*/
const getSecretsHelper = async ({
workspaceId,
environment
workspaceId,
environment,
}: {
workspaceId: Types.ObjectId;
environment: string;
workspaceId: Types.ObjectId;
environment: string;
}) => {
const content = {} as any;
try {
const key = await getKey({ workspaceId });
const secrets = await Secret.find({
workspace: workspaceId,
environment,
type: SECRET_SHARED
});
secrets.forEach((secret: ISecret) => {
const secretKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const content = {} as any;
const key = await getKey({ workspaceId: workspaceId.toString() });
const secrets = await Secret.find({
workspace: workspaceId,
environment,
type: SECRET_SHARED,
});
const secretValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
secrets.forEach((secret: ISecret) => {
const secretKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key,
});
content[secretKey] = secretValue;
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get secrets');
}
const secretValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key,
});
return content;
}
content[secretKey] = secretValue;
});
return content;
};
/**
* Return bot's copy of the workspace key for workspace
* Return bot's copy of the workspace key for workspace
* with id [workspaceId]
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key
*/
const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
let key;
try {
const botKey = await BotKey.findOne({
workspace: workspaceId
}).populate<{ sender: IUser }>('sender', 'publicKey');
if (!botKey) throw new Error('Failed to find bot key');
const bot = await Bot.findOne({
workspace: workspaceId
}).select('+encryptedPrivateKey +iv +tag');
if (!bot) throw new Error('Failed to find bot');
if (!bot.isActive) throw new Error('Bot is not active');
const privateKeyBot = decryptSymmetric({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: await getEncryptionKey()
});
key = decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get workspace key');
}
return key;
}
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
const botKey = await BotKey.findOne({
workspace: workspaceId,
}).populate<{ sender: IUser }>("sender", "publicKey");
if (!botKey) throw new Error("Failed to find bot key");
const bot = await Bot.findOne({
workspace: workspaceId,
}).select("+encryptedPrivateKey +iv +tag");
if (!bot) throw new Error("Failed to find bot");
if (!bot.isActive) throw new Error("Bot is not active");
const privateKeyBot = decryptSymmetric({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: await getEncryptionKey(),
});
const key = decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
return key;
};
/**
* Return symmetrically encrypted [plaintext] using the
* key for workspace with id [workspaceId]
* key for workspace with id [workspaceId]
* @param {Object} obj1
* @param {String} obj1.workspaceId - id of workspace
* @param {String} obj1.plaintext - plaintext to encrypt
*/
const encryptSymmetricHelper = async ({
workspaceId,
plaintext
workspaceId,
plaintext,
}: {
workspaceId: Types.ObjectId;
plaintext: string;
workspaceId: Types.ObjectId;
plaintext: string;
}) => {
try {
const key = await getKey({ workspaceId });
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext,
key
});
return ({
ciphertext,
iv,
tag
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric encryption with bot');
}
}
const key = await getKey({ workspaceId: workspaceId.toString() });
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext,
key,
});
return {
ciphertext,
iv,
tag,
};
};
/**
* Return symmetrically decrypted [ciphertext] using the
* key for workspace with id [workspaceId]
@ -277,40 +255,31 @@ const encryptSymmetricHelper = async ({
* @param {String} obj.tag - tag
*/
const decryptSymmetricHelper = async ({
workspaceId,
workspaceId,
ciphertext,
iv,
tag,
}: {
workspaceId: Types.ObjectId;
ciphertext: string;
iv: string;
tag: string;
}) => {
const key = await getKey({ workspaceId: workspaceId.toString() });
const plaintext = decryptSymmetric({
ciphertext,
iv,
tag
}: {
workspaceId: Types.ObjectId;
ciphertext: string;
iv: string;
tag: string;
}) => {
let plaintext;
try {
const key = await getKey({ workspaceId });
const plaintext = decryptSymmetric({
ciphertext,
iv,
tag,
key
});
return plaintext;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric decryption with bot');
}
return plaintext;
}
tag,
key,
});
return plaintext;
};
export {
validateClientForBot,
createBot,
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper
}
validateClientForBot,
createBot,
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper,
};

@ -1,14 +1,13 @@
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { Bot, IBot } from '../models';
import { EVENT_PUSH_SECRETS } from '../variables';
import { IntegrationService } from '../services';
import { Types } from "mongoose";
import { Bot, IBot } from "../models";
import { EVENT_PUSH_SECRETS } from "../variables";
import { IntegrationService } from "../services";
interface Event {
name: string;
workspaceId: Types.ObjectId;
environment?: string;
payload: any;
name: string;
workspaceId: Types.ObjectId;
environment?: string;
payload: any;
}
/**
@ -19,39 +18,25 @@ interface Event {
* @param {String} obj.event.workspaceId - id of workspace that event is part of
* @param {Object} obj.event.payload - payload of event (depends on event)
*/
const handleEventHelper = async ({
event
}: {
event: Event;
}) => {
const {
workspaceId,
environment
} = event;
// TODO: moduralize bot check into separate function
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
});
if (!bot) return;
try {
switch (event.name) {
case EVENT_PUSH_SECRETS:
IntegrationService.syncIntegrations({
workspaceId,
environment
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
}
const handleEventHelper = async ({ event }: { event: Event }) => {
const { workspaceId, environment } = event;
export {
handleEventHelper
}
// TODO: moduralize bot check into separate function
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true,
});
if (!bot) return;
switch (event.name) {
case EVENT_PUSH_SECRETS:
IntegrationService.syncIntegrations({
workspaceId,
environment,
});
break;
}
};
export { handleEventHelper };

@ -256,7 +256,7 @@ const syncIntegrationsHelper = async ({
integration,
integrationAuth,
secrets,
accessId: access.accessId,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken
});
}
@ -482,4 +482,4 @@ export {
getIntegrationAuthAccessHelper,
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper
}
}

@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Key, IKey } from '../models';
interface Key {
@ -27,36 +26,30 @@ const pushKeys = async ({
workspaceId: string;
keys: Key[];
}): Promise<void> => {
try {
// filter out already-inserted keys
const keysSet = new Set(
(
await Key.find(
{
workspace: workspaceId
},
'receiver'
)
).map((k: IKey) => k.receiver.toString())
);
// filter out already-inserted keys
const keysSet = new Set(
(
await Key.find(
{
workspace: workspaceId
},
'receiver'
)
).map((k: IKey) => k.receiver.toString())
);
keys = keys.filter((key) => !keysSet.has(key.userId));
keys = keys.filter((key) => !keysSet.has(key.userId));
// add new shared keys only
await Key.insertMany(
keys.map((k) => ({
encryptedKey: k.encryptedKey,
nonce: k.nonce,
sender: userId,
receiver: k.userId,
workspace: workspaceId
}))
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to push access keys');
}
// add new shared keys only
await Key.insertMany(
keys.map((k) => ({
encryptedKey: k.encryptedKey,
nonce: k.nonce,
sender: userId,
receiver: k.userId,
workspace: workspaceId
}))
);
};
export { pushKeys };

@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
MembershipOrg,
@ -144,15 +143,7 @@ const validateMembershipOrg = async ({
* @return {Object} membershipOrg - membership
*/
const findMembershipOrg = (queryObj: any) => {
let membershipOrg;
try {
membershipOrg = MembershipOrg.findOne(queryObj);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to find organization membership');
}
const membershipOrg = MembershipOrg.findOne(queryObj);
return membershipOrg;
};
@ -175,33 +166,27 @@ const addMembershipsOrg = async ({
roles: string[];
statuses: string[];
}) => {
try {
const operations = userIds.map((userId, idx) => {
return {
updateOne: {
filter: {
user: userId,
organization: organizationId,
role: roles[idx],
status: statuses[idx]
},
update: {
user: userId,
organization: organizationId,
role: roles[idx],
status: statuses[idx]
},
upsert: true
}
};
});
const operations = userIds.map((userId, idx) => {
return {
updateOne: {
filter: {
user: userId,
organization: organizationId,
role: roles[idx],
status: statuses[idx]
},
update: {
user: userId,
organization: organizationId,
role: roles[idx],
status: statuses[idx]
},
upsert: true
}
};
});
await MembershipOrg.bulkWrite(operations as any);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to add users to organization');
}
await MembershipOrg.bulkWrite(operations as any);
};
/**
@ -214,43 +199,36 @@ const deleteMembershipOrg = async ({
}: {
membershipOrgId: string;
}) => {
let deletedMembershipOrg;
try {
deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
_id: membershipOrgId
});
const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
_id: membershipOrgId
});
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
// delete keys associated with organization membership
if (deletedMembershipOrg?.user) {
// case: organization membership had a registered user
// delete keys associated with organization membership
if (deletedMembershipOrg?.user) {
// case: organization membership had a registered user
const workspaces = (
await Workspace.find({
organization: deletedMembershipOrg.organization
})
).map((w) => w._id.toString());
const workspaces = (
await Workspace.find({
organization: deletedMembershipOrg.organization
})
).map((w) => w._id.toString());
await Membership.deleteMany({
user: deletedMembershipOrg.user,
workspace: {
$in: workspaces
}
});
await Membership.deleteMany({
user: deletedMembershipOrg.user,
workspace: {
$in: workspaces
}
});
await Key.deleteMany({
receiver: deletedMembershipOrg.user,
workspace: {
$in: workspaces
}
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete organization membership');
}
await Key.deleteMany({
receiver: deletedMembershipOrg.user,
workspace: {
$in: workspaces
}
});
}
return deletedMembershipOrg;
};

@ -1,39 +1,44 @@
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import { Types } from 'mongoose';
import Stripe from "stripe";
import { Types } from "mongoose";
import {
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import { Organization, MembershipOrg } from '../models';
import {
ACCEPTED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
OWNER
} from '../variables';
import {
getStripeSecretKey,
getStripeProductPro,
getStripeProductTeam,
getStripeProductStarter
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
} from "../models";
import { Organization, MembershipOrg } from "../models";
import {
ACCEPTED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
OWNER,
} from "../variables";
import {
getStripeSecretKey,
getStripeProductPro,
getStripeProductTeam,
getStripeProductStarter,
} from "../config";
import {
UnauthorizedRequestError,
OrganizationNotFoundError,
} from "../utils/errors";
import { validateUserClientForOrganization } from "../helpers/user";
import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount";
import {
EELicenseService
} from '../ee/services';
import {
getLicenseServerUrl
} from '../config';
import {
UnauthorizedRequestError,
OrganizationNotFoundError
} from '../utils/errors';
import {
validateUserClientForOrganization
} from '../helpers/user';
import {
validateServiceAccountClientForOrganization
} from '../helpers/serviceAccount';
licenseServerKeyRequest,
licenseKeyRequest
} from '../config/request';
/**
* Validate accepted clients for organization with id [organizationId]
@ -42,69 +47,80 @@ import {
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against
*/
const validateClientForOrganization = async ({
authData,
organizationId,
acceptedRoles,
acceptedStatuses
authData,
organizationId,
acceptedRoles,
acceptedStatuses,
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
organizationId: Types.ObjectId;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
organizationId: Types.ObjectId;
acceptedRoles: Array<"owner" | "admin" | "member">;
acceptedStatuses: Array<"invited" | "accepted">;
}) => {
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: 'Failed to find organization'
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses
});
return ({ organization, membershipOrg });
}
const organization = await Organization.findById(organizationId);
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForOrganization({
serviceAccount: authData.authPayload,
organization
});
return ({ organization });
}
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization",
});
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for organization'
});
}
if (
authData.authMode === AUTH_MODE_JWT &&
authData.authPayload instanceof User
) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses,
});
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses
});
return ({ organization, membershipOrg });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for organization'
});
}
return { organization, membershipOrg };
}
if (
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
authData.authPayload instanceof ServiceAccount
) {
await validateServiceAccountClientForOrganization({
serviceAccount: authData.authPayload,
organization,
});
return { organization };
}
if (
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
authData.authPayload instanceof ServiceTokenData
) {
throw UnauthorizedRequestError({
message: "Failed service token authorization for organization",
});
}
if (
authData.authMode === AUTH_MODE_API_KEY &&
authData.authPayload instanceof User
) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses,
});
return { organization, membershipOrg };
}
throw UnauthorizedRequestError({
message: "Failed client authorization for organization",
});
};
/**
* Create an organization with name [name]
@ -114,43 +130,37 @@ const validateClientForOrganization = async ({
* @param {Object} organization - new organization
*/
const createOrganization = async ({
name,
email
name,
email,
}: {
name: string;
email: string;
name: string;
email: string;
}) => {
let organization;
try {
// register stripe account
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
let organization;
// register stripe account
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: "2022-08-01",
});
if (await getStripeSecretKey()) {
const customer = await stripe.customers.create({
email,
description: name
});
if (await getStripeSecretKey()) {
const customer = await stripe.customers.create({
email,
description: name,
});
organization = await new Organization({
name,
customerId: customer.id
}).save();
} else {
organization = await new Organization({
name
}).save();
}
organization = await new Organization({
name,
customerId: customer.id,
}).save();
} else {
organization = await new Organization({
name,
}).save();
}
await initSubscriptionOrg({ organizationId: organization._id });
} catch (err) {
Sentry.setUser({ email });
Sentry.captureException(err);
throw new Error(`Failed to create organization [err=${err}]`);
}
await initSubscriptionOrg({ organizationId: organization._id });
return organization;
return organization;
};
/**
@ -162,57 +172,52 @@ const createOrganization = async ({
* @return {Subscription} obj.subscription - new subscription
*/
const initSubscriptionOrg = async ({
organizationId
organizationId,
}: {
organizationId: Types.ObjectId;
organizationId: Types.ObjectId;
}) => {
let stripeSubscription;
let subscription;
try {
// find organization
const organization = await Organization.findOne({
_id: organizationId
});
let stripeSubscription;
let subscription;
if (organization) {
if (organization.customerId) {
// initialize starter subscription with quantity of 0
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// find organization
const organization = await Organization.findOne({
_id: organizationId,
});
const productToPriceMap = {
starter: await getStripeProductStarter(),
team: await getStripeProductTeam(),
pro: await getStripeProductPro()
};
if (organization) {
if (organization.customerId) {
// initialize starter subscription with quantity of 0
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: "2022-08-01",
});
stripeSubscription = await stripe.subscriptions.create({
customer: organization.customerId,
items: [
{
price: productToPriceMap['starter'],
quantity: 1
}
],
payment_behavior: 'default_incomplete',
proration_behavior: 'none',
expand: ['latest_invoice.payment_intent']
});
}
} else {
throw new Error('Failed to initialize free organization subscription');
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to initialize free organization subscription');
}
const productToPriceMap = {
starter: await getStripeProductStarter(),
team: await getStripeProductTeam(),
pro: await getStripeProductPro(),
};
return {
stripeSubscription,
subscription
};
stripeSubscription = await stripe.subscriptions.create({
customer: organization.customerId,
items: [
{
price: productToPriceMap["starter"],
quantity: 1,
},
],
payment_behavior: "default_incomplete",
proration_behavior: "none",
expand: ["latest_invoice.payment_intent"],
});
}
} else {
throw new Error("Failed to initialize free organization subscription");
}
return {
stripeSubscription,
subscription,
};
};
/**
@ -222,54 +227,54 @@ const initSubscriptionOrg = async ({
* @param {Number} obj.organizationId - id of subscription's organization
*/
const updateSubscriptionOrgQuantity = async ({
organizationId
organizationId,
}: {
organizationId: string;
organizationId: string;
}) => {
let stripeSubscription;
try {
// find organization
const organization = await Organization.findOne({
_id: organizationId
});
let stripeSubscription;
// find organization
const organization = await Organization.findOne({
_id: organizationId,
});
if (organization && organization.customerId) {
const quantity = await MembershipOrg.countDocuments({
organization: organizationId,
status: ACCEPTED
});
if (organization && organization.customerId) {
if (EELicenseService.instanceType === 'cloud') {
// instance of Infisical is a cloud instance
const quantity = await MembershipOrg.countDocuments({
organization: new Types.ObjectId(organizationId),
status: ACCEPTED,
});
await licenseServerKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`,
{
quantity
}
);
}
if (EELicenseService.instanceType === 'enterprise-self-hosted') {
// instance of Infisical is an enterprise self-hosted instance
const usedSeats = await MembershipOrg.countDocuments({
status: ACCEPTED
});
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
await licenseKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license/v1/license`,
{
usedSeats
}
);
}
}
const subscription = (
await stripe.subscriptions.list({
customer: organization.customerId
})
).data[0];
stripeSubscription = await stripe.subscriptions.update(subscription.id, {
items: [
{
id: subscription.items.data[0].id,
price: subscription.items.data[0].price.id,
quantity
}
]
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
return stripeSubscription;
return stripeSubscription;
};
export {
validateClientForOrganization,
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity
};
validateClientForOrganization,
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity,
};

File diff suppressed because it is too large Load Diff

@ -1,16 +1,15 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { TokenData } from '../models';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import { Types } from "mongoose";
import { TokenData } from "../models";
import crypto from "crypto";
import bcrypt from "bcrypt";
import {
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
} from '../variables';
import { UnauthorizedRequestError } from '../utils/errors';
import { getSaltRounds } from '../config';
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET,
} from "../variables";
import { UnauthorizedRequestError } from "../utils/errors";
import { getSaltRounds } from "../config";
/**
* Create and store a token in the database for purpose [type]
@ -22,194 +21,197 @@ import { getSaltRounds } from '../config';
* @returns {String} token - the created token
*/
const createTokenHelper = async ({
type,
email,
phoneNumber,
organizationId
type,
email,
phoneNumber,
organizationId,
}: {
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
type:
| "emailConfirmation"
| "emailMfa"
| "organizationInvitation"
| "passwordReset";
email?: string;
phoneNumber?: string;
organizationId?: Types.ObjectId;
}) => {
let token, expiresAt, triesLeft;
// generate random token based on specified token use-case
// type [type]
switch (type) {
case TOKEN_EMAIL_CONFIRMATION:
// generate random 6-digit code
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
expiresAt = new Date(new Date().getTime() + 86400000);
break;
case TOKEN_EMAIL_MFA:
// generate random 6-digit code
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
triesLeft = 5;
expiresAt = new Date(new Date().getTime() + 300000);
break;
case TOKEN_EMAIL_ORG_INVITATION:
// generate random hex
token = crypto.randomBytes(16).toString("hex");
expiresAt = new Date(new Date().getTime() + 259200000);
break;
case TOKEN_EMAIL_PASSWORD_RESET:
// generate random hex
token = crypto.randomBytes(16).toString("hex");
expiresAt = new Date(new Date().getTime() + 86400000);
break;
default:
token = crypto.randomBytes(16).toString("hex");
expiresAt = new Date();
break;
}
interface TokenDataQuery {
type: string;
email?: string;
phoneNumber?: string;
organizationId?: Types.ObjectId
}) => {
let token, expiresAt, triesLeft;
try {
// generate random token based on specified token use-case
// type [type]
switch (type) {
case TOKEN_EMAIL_CONFIRMATION:
// generate random 6-digit code
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
expiresAt = new Date((new Date()).getTime() + 86400000);
break;
case TOKEN_EMAIL_MFA:
// generate random 6-digit code
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
triesLeft = 5;
expiresAt = new Date((new Date()).getTime() + 300000);
break;
case TOKEN_EMAIL_ORG_INVITATION:
// generate random hex
token = crypto.randomBytes(16).toString('hex');
expiresAt = new Date((new Date()).getTime() + 259200000);
break;
case TOKEN_EMAIL_PASSWORD_RESET:
// generate random hex
token = crypto.randomBytes(16).toString('hex');
expiresAt = new Date((new Date()).getTime() + 86400000);
break;
default:
token = crypto.randomBytes(16).toString('hex');
expiresAt = new Date();
break;
}
interface TokenDataQuery {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
}
interface TokenDataUpdate {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
tokenHash: string;
triesLeft?: number;
expiresAt: Date;
}
organization?: Types.ObjectId;
}
const query: TokenDataQuery = { type };
const update: TokenDataUpdate = {
type,
tokenHash: await bcrypt.hash(token, await getSaltRounds()),
expiresAt
}
interface TokenDataUpdate {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
tokenHash: string;
triesLeft?: number;
expiresAt: Date;
}
if (email) {
query.email = email;
update.email = email;
}
if (phoneNumber) {
query.phoneNumber = phoneNumber;
update.phoneNumber = phoneNumber;
}
if (organizationId) {
query.organization = organizationId
update.organization = organizationId
}
if (triesLeft) {
update.triesLeft = triesLeft;
}
await TokenData.findOneAndUpdate(
query,
update,
{
new: true,
upsert: true
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(
"Failed to create token"
);
}
return token;
}
const query: TokenDataQuery = { type };
const update: TokenDataUpdate = {
type,
tokenHash: await bcrypt.hash(token, await getSaltRounds()),
expiresAt,
};
if (email) {
query.email = email;
update.email = email;
}
if (phoneNumber) {
query.phoneNumber = phoneNumber;
update.phoneNumber = phoneNumber;
}
if (organizationId) {
query.organization = organizationId;
update.organization = organizationId;
}
if (triesLeft) {
update.triesLeft = triesLeft;
}
await TokenData.findOneAndUpdate(query, update, {
new: true,
upsert: true,
});
return token;
};
/**
*
*
* @param {Object} obj
* @param {String} obj.email - email associated with the token
* @param {String} obj.token - value of the token
*/
const validateTokenHelper = async ({
type,
email,
phoneNumber,
organizationId,
token
type,
email,
phoneNumber,
organizationId,
token,
}: {
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
type:
| "emailConfirmation"
| "emailMfa"
| "organizationInvitation"
| "passwordReset";
email?: string;
phoneNumber?: string;
organizationId?: Types.ObjectId;
token: string;
}) => {
interface Query {
type: string;
email?: string;
phoneNumber?: string;
organizationId?: Types.ObjectId;
token: string;
}) => {
interface Query {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
}
organization?: Types.ObjectId;
}
const query: Query = { type };
const query: Query = { type };
if (email) { query.email = email; }
if (phoneNumber) { query.phoneNumber = phoneNumber; }
if (organizationId) { query.organization = organizationId; }
if (email) {
query.email = email;
}
if (phoneNumber) {
query.phoneNumber = phoneNumber;
}
if (organizationId) {
query.organization = organizationId;
}
const tokenData = await TokenData.findOne(query).select('+tokenHash');
if (!tokenData) throw new Error('Failed to find token to validate');
if (tokenData.expiresAt < new Date()) {
// case: token expired
await TokenData.findByIdAndDelete(tokenData._id);
throw UnauthorizedRequestError({
message: 'MFA session expired. Please log in again',
context: {
code: 'mfa_expired'
}
});
}
const tokenData = await TokenData.findOne(query).select("+tokenHash");
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
if (!isValid) {
// case: token is not valid
if (tokenData?.triesLeft !== undefined) {
// case: token has a try-limit
if (tokenData.triesLeft === 1) {
// case: token is out of tries
await TokenData.findByIdAndDelete(tokenData._id);
} else {
// case: token has more than 1 try left
await TokenData.findByIdAndUpdate(tokenData._id, {
triesLeft: tokenData.triesLeft - 1
}, {
new: true
});
}
if (!tokenData) throw new Error("Failed to find token to validate");
throw UnauthorizedRequestError({
message: 'MFA code is invalid',
context: {
code: 'mfa_invalid',
triesLeft: tokenData.triesLeft - 1
}
});
}
throw UnauthorizedRequestError({
message: 'MFA code is invalid',
context: {
code: 'mfa_invalid'
}
});
}
// case: token is valid
if (tokenData.expiresAt < new Date()) {
// case: token expired
await TokenData.findByIdAndDelete(tokenData._id);
}
throw UnauthorizedRequestError({
message: "MFA session expired. Please log in again",
context: {
code: "mfa_expired",
},
});
}
export {
createTokenHelper,
validateTokenHelper
}
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
if (!isValid) {
// case: token is not valid
if (tokenData?.triesLeft !== undefined) {
// case: token has a try-limit
if (tokenData.triesLeft === 1) {
// case: token is out of tries
await TokenData.findByIdAndDelete(tokenData._id);
} else {
// case: token has more than 1 try left
await TokenData.findByIdAndUpdate(
tokenData._id,
{
triesLeft: tokenData.triesLeft - 1,
},
{
new: true,
}
);
}
throw UnauthorizedRequestError({
message: "MFA code is invalid",
context: {
code: "mfa_invalid",
triesLeft: tokenData.triesLeft - 1,
},
});
}
throw UnauthorizedRequestError({
message: "MFA code is invalid",
context: {
code: "mfa_invalid",
},
});
}
// case: token is valid
await TokenData.findByIdAndDelete(tokenData._id);
};
export { createTokenHelper, validateTokenHelper };

@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
IUser,
@ -28,16 +27,9 @@ import {
* @returns {Object} user - the initialized user
*/
const setupAccount = async ({ email }: { email: string }) => {
let user;
try {
user = await new User({
email
}).save();
} catch (err) {
Sentry.setUser({ email });
Sentry.captureException(err);
throw new Error('Failed to set up account');
}
const user = await new User({
email
}).save();
return user;
};
@ -89,34 +81,27 @@ const completeAccount = async ({
salt: string;
verifier: string;
}) => {
let user;
try {
const options = {
new: true
};
user = await User.findByIdAndUpdate(
userId,
{
firstName,
lastName,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
options
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to complete account set up');
}
const options = {
new: true
};
const user = await User.findByIdAndUpdate(
userId,
{
firstName,
lastName,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
options
);
return user;
};

@ -89,7 +89,7 @@ const validateClientForWorkspace = async ({
requiredPermissions
});
return ({ membership });
return ({ membership, workspace });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
@ -123,7 +123,7 @@ const validateClientForWorkspace = async ({
requiredPermissions
});
return ({ membership });
return ({ membership, workspace });
}
throw UnauthorizedRequestError({

@ -1,4 +1,3 @@
import mongoose from 'mongoose';
import dotenv from 'dotenv';
dotenv.config();
import express from 'express';
@ -6,6 +5,7 @@ import helmet from 'helmet';
import cors from 'cors';
import * as Sentry from '@sentry/node';
import { DatabaseService } from './services';
import { EELicenseService } from './ee/services';
import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp';
import { TelemetryService } from './services';
@ -25,7 +25,9 @@ import {
workspace as eeWorkspaceRouter,
secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter,
action as eeActionRouter
action as eeActionRouter,
organizations as eeOrganizationsRouter,
cloudProducts as eeCloudProductsRouter
} from './ee/routes/v1';
import {
signup as v1SignupRouter,
@ -74,14 +76,15 @@ import {
getNodeEnv,
getPort,
getSentryDSN,
getSiteURL,
getSmtpHost
getSiteURL
} from './config';
const main = async () => {
TelemetryService.logTelemetryMessage();
setTransporter(await initSmtp());
await EELicenseService.initGlobalFeatureSet();
await DatabaseService.initDatabase(await getMongoURL());
if ((await getNodeEnv()) !== 'test') {
Sentry.init({
@ -119,6 +122,8 @@ const main = async () => {
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
app.use('/api/v1/organizations', eeOrganizationsRouter);
app.use('/api/v1/cloud-products', eeCloudProductsRouter);
// v1 routes (default)
app.use('/api/v1/signup', v1SignupRouter);

@ -1,7 +1,6 @@
import * as Sentry from "@sentry/node";
import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models";
import request from '../config/request';
import { standardRequest } from "../config/request";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@ -26,7 +25,7 @@ import {
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL
INTEGRATION_SUPABASE_API_URL,
} from "../variables";
interface App {
@ -47,87 +46,80 @@ interface App {
const getApps = async ({
integrationAuth,
accessToken,
teamId
teamId,
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
teamId?: string;
}) => {
let apps: App[] = [];
try {
switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
apps = [];
break;
case INTEGRATION_AWS_PARAMETER_STORE:
apps = [];
break;
case INTEGRATION_AWS_SECRET_MANAGER:
apps = [];
break;
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken,
});
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
integrationAuth,
accessToken,
});
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
accessToken,
});
break;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
accessToken,
});
break;
case INTEGRATION_GITLAB:
apps = await getAppsGitlab({
accessToken,
teamId
});
break;
case INTEGRATION_RENDER:
apps = await getAppsRender({
accessToken,
});
break;
case INTEGRATION_RAILWAY:
apps = await getAppsRailway({
accessToken
});
break;
case INTEGRATION_FLYIO:
apps = await getAppsFlyio({
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
apps = await getAppsCircleCI({
accessToken,
});
break;
case INTEGRATION_TRAVISCI:
apps = await getAppsTravisCI({
accessToken,
})
break;
case INTEGRATION_SUPABASE:
apps = await getAppsSupabase({
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get integration apps");
switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
apps = [];
break;
case INTEGRATION_AWS_PARAMETER_STORE:
apps = [];
break;
case INTEGRATION_AWS_SECRET_MANAGER:
apps = [];
break;
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken,
});
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
integrationAuth,
accessToken,
});
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
accessToken,
});
break;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
accessToken,
});
break;
case INTEGRATION_GITLAB:
apps = await getAppsGitlab({
accessToken,
teamId,
});
break;
case INTEGRATION_RENDER:
apps = await getAppsRender({
accessToken,
});
break;
case INTEGRATION_RAILWAY:
apps = await getAppsRailway({
accessToken,
});
break;
case INTEGRATION_FLYIO:
apps = await getAppsFlyio({
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
apps = await getAppsCircleCI({
accessToken,
});
break;
case INTEGRATION_TRAVISCI:
apps = await getAppsTravisCI({
accessToken,
});
break;
case INTEGRATION_SUPABASE:
apps = await getAppsSupabase({
accessToken,
});
break;
}
return apps;
@ -141,25 +133,18 @@ const getApps = async ({
* @returns {String} apps.name - name of Heroku app
*/
const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const res = (
await request.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
},
})
).data;
const res = (
await standardRequest.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
},
})
).data;
apps = res.map((a: any) => ({
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Heroku integration apps");
}
const apps = res.map((a: any) => ({
name: a.name,
}));
return apps;
};
@ -178,33 +163,26 @@ const getAppsVercel = async ({
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let apps;
try {
const res = (
await request.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
...(integrationAuth?.teamId
? {
params: {
teamId: integrationAuth.teamId,
},
}
: {}),
})
).data;
const res = (
await standardRequest.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
...(integrationAuth?.teamId
? {
params: {
teamId: integrationAuth.teamId,
},
}
: {}),
})
).data;
apps = res.projects.map((a: any) => ({
name: a.name,
appId: a.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Vercel integration apps");
}
const apps = res.projects.map((a: any) => ({
name: a.name,
appId: a.id,
}));
return apps;
};
@ -218,43 +196,41 @@ const getAppsVercel = async ({
*/
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
const apps: any = [];
try {
let page = 1;
const perPage = 10;
let hasMorePages = true;
// paginate through all sites
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage)
});
let page = 1;
const perPage = 10;
let hasMorePages = true;
const { data } = await request.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
// paginate through all sites
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage),
filter: 'all'
});
const { data } = await standardRequest.get(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
});
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.site_id
});
});
if (data.length < perPage) {
hasMorePages = false;
"Accept-Encoding": "application/json",
},
}
);
page++;
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.site_id,
});
});
if (data.length < perPage) {
hasMorePages = false;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Netlify integration apps");
page++;
}
return apps;
@ -268,67 +244,59 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.name - name of Github site
*/
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
interface GitHubApp {
id: string;
name: string;
permissions: {
admin: boolean;
};
owner: {
login: string;
interface GitHubApp {
id: string;
name: string;
permissions: {
admin: boolean;
};
owner: {
login: string;
};
}
const octokit = new Octokit({
auth: accessToken,
});
const getAllRepos = async () => {
let repos: GitHubApp[] = [];
let page = 1;
const per_page = 100;
let hasMore = true;
while (hasMore) {
const response = await octokit.request(
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
{
per_page,
page,
}
);
if (response.data.length > 0) {
repos = repos.concat(response.data);
page++;
} else {
hasMore = false;
}
}
const octokit = new Octokit({
auth: accessToken,
return repos;
};
const repos = await getAllRepos();
const apps = repos
.filter((a: GitHubApp) => a.permissions.admin === true)
.map((a: GitHubApp) => {
return {
appId: a.id,
name: a.name,
owner: a.owner.login,
};
});
const getAllRepos = async () => {
let repos: GitHubApp[] = [];
let page = 1;
const per_page = 100;
let hasMore = true;
while (hasMore) {
const response = await octokit.request(
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
{
per_page,
page,
}
);
if (response.data.length > 0) {
repos = repos.concat(response.data);
page++;
} else {
hasMore = false;
}
}
return repos;
};
const repos = await getAllRepos();
apps = repos
.filter((a: GitHubApp) => a.permissions.admin === true)
.map((a: GitHubApp) => {
return {
appId: a.id,
name: a.name,
owner: a.owner.login,
};
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Github repos");
}
return apps;
};
@ -341,29 +309,20 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.appId - id of Render service
*/
const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
let apps: any;
try {
const res = (
await request.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Accept-Encoding': 'application/json',
},
})
).data;
apps = res
.map((a: any) => ({
name: a.service.name,
appId: a.service.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Render services");
}
const res = (
await standardRequest.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
"Accept-Encoding": "application/json",
},
})
).data;
const apps = res.map((a: any) => ({
name: a.service.name,
appId: a.service.id,
}));
return apps;
};
@ -376,49 +335,51 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.name - name of Railway project
* @returns {String} apps.appId - id of Railway project
*
*/
*/
const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
let apps: any[] = [];
try {
const query = `
query GetProjects($userId: String, $teamId: String) {
projects(userId: $userId, teamId: $teamId) {
edges {
node {
id
name
}
const query = `
query GetProjects($userId: String, $teamId: String) {
projects(userId: $userId, teamId: $teamId) {
edges {
node {
id
name
}
}
}
`;
}
`;
const variables = {};
const variables = {};
const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, {
const {
data: {
data: {
projects: { edges },
},
},
} = await standardRequest.post(
INTEGRATION_RAILWAY_API_URL,
{
query,
variables,
}, {
},
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
});
apps = edges.map((e: any) => ({
name: e.node.name,
appId: e.node.id
}));
}
);
const apps = edges.map((e: any) => ({
name: e.node.name,
appId: e.node.id,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Railway services");
}
return apps;
}
};
/**
* Return list of apps for Fly.io integration
@ -428,41 +389,40 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.name - name of Fly.io apps
*/
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
let apps;
try {
const query = `
query($role: String) {
apps(type: "container", first: 400, role: $role) {
nodes {
id
name
hostname
}
const query = `
query($role: String) {
apps(type: "container", first: 400, role: $role) {
nodes {
id
name
hostname
}
}
`;
}
`;
const res = (await request.post(INTEGRATION_FLYIO_API_URL, {
query,
variables: {
role: null,
const res = (
await standardRequest.post(
INTEGRATION_FLYIO_API_URL,
{
query,
variables: {
role: null,
},
},
}, {
headers: {
Authorization: "Bearer " + accessToken,
'Accept': 'application/json',
'Accept-Encoding': 'application/json',
},
})).data.data.apps.nodes;
{
headers: {
Authorization: "Bearer " + accessToken,
Accept: "application/json",
"Accept-Encoding": "application/json",
},
}
)
).data.data.apps.nodes;
apps = res.map((a: any) => ({
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Fly.io apps");
}
const apps = res.map((a: any) => ({
name: a.name,
}));
return apps;
};
@ -475,63 +435,43 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.name - name of CircleCI apps
*/
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
let apps: any;
try {
const res = (
await request.get(
`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
},
}
)
).data
const res = (
await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
},
})
).data;
const apps = res?.map((a: any) => {
return {
name: a?.reponame,
};
});
apps = res?.map((a: any) => {
return {
name: a?.reponame
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get CircleCI projects");
}
return apps;
};
const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
let apps: any;
try {
const res = (
await request.get(
`${INTEGRATION_TRAVISCI_API_URL}/repos`,
{
headers: {
"Authorization": `token ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
).data;
const res = (
await standardRequest.get(`${INTEGRATION_TRAVISCI_API_URL}/repos`, {
headers: {
Authorization: `token ${accessToken}`,
"Accept-Encoding": "application/json",
},
})
).data;
const apps = res?.map((a: any) => {
return {
name: a?.slug?.split("/")[1],
appId: a?.id,
};
});
apps = res?.map((a: any) => {
return {
name: a?.slug?.split("/")[1],
appId: a?.id,
}
});
}catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get TravisCI projects");
}
return apps;
}
};
/**
* Return list of repositories for GitLab integration
@ -540,112 +480,98 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
* @returns {Object[]} apps - names of GitLab sites
* @returns {String} apps.name - name of GitLab site
*/
const getAppsGitlab = async ({
const getAppsGitlab = async ({
accessToken,
teamId
teamId,
}: {
accessToken: string;
teamId?: string;
}) => {
const apps: App[] = [];
let page = 1;
const perPage = 10;
let hasMorePages = true;
try {
if (teamId) {
// case: fetch projects for group with id [teamId] in GitLab
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage)
});
if (teamId) {
// case: fetch projects for group with id [teamId] in GitLab
const { data } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
{
params,
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
);
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage),
});
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id
});
});
if (data.length < perPage) {
hasMorePages = false;
const { data } = await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
page++;
}
} else {
// case: fetch projects for individual in GitLab
const { id } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/user`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
).data;
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage)
);
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id,
});
});
const { data } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
{
params,
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
);
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id
});
});
if (data.length < perPage) {
hasMorePages = false;
}
page++;
if (data.length < perPage) {
hasMorePages = false;
}
page++;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get GitLab projects");
}
return apps;
}
} else {
// case: fetch projects for individual in GitLab
const { id } = (
await standardRequest.get(`${INTEGRATION_GITLAB_API_URL}/v4/user`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
})
).data;
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage),
});
const { data } = await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id,
});
});
if (data.length < perPage) {
hasMorePages = false;
}
page++;
}
}
return apps;
};
/**
* Return list of projects for Supabase integration
@ -655,30 +581,23 @@ const getAppsGitlab = async ({
* @returns {String} apps.name - name of Supabase app
*/
const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => {
let apps: any;
try {
const { data } = await request.get(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
const { data } = await standardRequest.get(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
const apps = data.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
apps = data.map((a: any) => {
return {
name: a.name,
appId: a.id
};
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Supabase projects');
}
return apps;
};

@ -1,5 +1,4 @@
import * as Sentry from '@sentry/node';
import request from '../config/request';
import { standardRequest } from "../config/request";
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
@ -12,8 +11,8 @@ import {
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL
} from '../variables';
INTEGRATION_GITLAB_TOKEN_URL,
} from "../variables";
import {
getSiteURL,
getClientIdAzure,
@ -26,8 +25,8 @@ import {
getClientIdGitHub,
getClientSecretGitHub,
getClientIdGitLab,
getClientSecretGitLab
} from '../config';
getClientSecretGitLab,
} from "../config";
interface ExchangeCodeAzureResponse {
token_type: string;
@ -93,49 +92,43 @@ interface ExchangeCodeGitlabResponse {
*/
const exchangeCode = async ({
integration,
code
code,
}: {
integration: string;
code: string;
}) => {
let obj = {} as any;
try {
switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
obj = await exchangeCodeAzure({
code
});
break;
case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({
code
});
break;
case INTEGRATION_VERCEL:
obj = await exchangeCodeVercel({
code
});
break;
case INTEGRATION_NETLIFY:
obj = await exchangeCodeNetlify({
code
});
break;
case INTEGRATION_GITHUB:
obj = await exchangeCodeGithub({
code
});
break;
case INTEGRATION_GITLAB:
obj = await exchangeCodeGitlab({
code
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange');
switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
obj = await exchangeCodeAzure({
code,
});
break;
case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({
code,
});
break;
case INTEGRATION_VERCEL:
obj = await exchangeCodeVercel({
code,
});
break;
case INTEGRATION_NETLIFY:
obj = await exchangeCodeNetlify({
code,
});
break;
case INTEGRATION_GITHUB:
obj = await exchangeCodeGithub({
code,
});
break;
case INTEGRATION_GITLAB:
obj = await exchangeCodeGitlab({
code,
});
}
return obj;
@ -143,43 +136,33 @@ const exchangeCode = async ({
/**
* Return [accessToken] for Azure OAuth2 code-token exchange
* @param param0
* @param param0
*/
const exchangeCodeAzure = async ({
code
}: {
code: string;
}) => {
const exchangeCodeAzure = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
let res: ExchangeCodeAzureResponse;
try {
res = (await request.post(
const res: ExchangeCodeAzureResponse = (
await standardRequest.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
grant_type: "authorization_code",
code: code,
scope: 'https://vault.azure.net/.default openid offline_access',
scope: "https://vault.azure.net/.default openid offline_access",
client_id: await getClientIdAzure(),
client_secret: await getClientSecretAzure(),
redirect_uri: `${await getSiteURL()}/integrations/azure-key-vault/oauth2/callback`
redirect_uri: `${await getSiteURL()}/integrations/azure-key-vault/oauth2/callback`,
} as any)
)).data;
)
).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Azure');
}
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
return ({
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
});
}
accessExpiresAt,
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
@ -191,38 +174,28 @@ const exchangeCodeAzure = async ({
* @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeHeroku = async ({
code
}: {
code: string;
}) => {
let res: ExchangeCodeHerokuResponse;
const exchangeCodeHeroku = async ({ code }: { code: string }) => {
const accessExpiresAt = new Date();
try {
res = (await request.post(
const res: ExchangeCodeHerokuResponse = (
await standardRequest.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
grant_type: "authorization_code",
code: code,
client_secret: await getClientSecretHeroku()
client_secret: await getClientSecretHeroku(),
} as any)
)).data;
)
).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Heroku');
}
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
return ({
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
});
}
accessExpiresAt,
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
@ -235,30 +208,23 @@ const exchangeCodeHeroku = async ({
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeVercel = async ({ code }: { code: string }) => {
let res: ExchangeCodeVercelResponse;
try {
res = (
await request.post(
INTEGRATION_VERCEL_TOKEN_URL,
new URLSearchParams({
code: code,
client_id: await getClientIdVercel(),
client_secret: await getClientSecretVercel(),
redirect_uri: `${await getSiteURL()}/integrations/vercel/oauth2/callback`
} as any)
)
).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(`Failed OAuth2 code-token exchange with Vercel [err=${err}]`);
}
const res: ExchangeCodeVercelResponse = (
await standardRequest.post(
INTEGRATION_VERCEL_TOKEN_URL,
new URLSearchParams({
code: code,
client_id: await getClientIdVercel(),
client_secret: await getClientSecretVercel(),
redirect_uri: `${await getSiteURL()}/integrations/vercel/oauth2/callback`,
} as any)
)
).data;
return {
accessToken: res.access_token,
refreshToken: null,
accessExpiresAt: null,
teamId: res.team_id
teamId: res.team_id,
};
};
@ -273,47 +239,39 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeNetlify = async ({ code }: { code: string }) => {
let res: ExchangeCodeNetlifyResponse;
let accountId;
try {
res = (
await request.post(
INTEGRATION_NETLIFY_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: await getClientIdNetlify(),
client_secret: await getClientSecretNetlify(),
redirect_uri: `${await getSiteURL()}/integrations/netlify/oauth2/callback`
} as any)
)
).data;
const res: ExchangeCodeNetlifyResponse = (
await standardRequest.post(
INTEGRATION_NETLIFY_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: await getClientIdNetlify(),
client_secret: await getClientSecretNetlify(),
redirect_uri: `${await getSiteURL()}/integrations/netlify/oauth2/callback`,
} as any)
)
).data;
const res2 = await request.get('https://api.netlify.com/api/v1/sites', {
const res2 = await standardRequest.get("https://api.netlify.com/api/v1/sites", {
headers: {
Authorization: `Bearer ${res.access_token}`,
},
});
const res3 = (
await standardRequest.get("https://api.netlify.com/api/v1/accounts", {
headers: {
Authorization: `Bearer ${res.access_token}`
}
});
Authorization: `Bearer ${res.access_token}`,
},
})
).data;
const res3 = (
await request.get('https://api.netlify.com/api/v1/accounts', {
headers: {
Authorization: `Bearer ${res.access_token}`
}
})
).data;
accountId = res3[0].id;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Netlify');
}
const accountId = res3[0].id;
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accountId
accountId,
};
};
@ -328,33 +286,25 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeGithub = async ({ code }: { code: string }) => {
let res: ExchangeCodeGithubResponse;
try {
res = (
await request.get(INTEGRATION_GITHUB_TOKEN_URL, {
params: {
client_id: await getClientIdGitHub(),
client_secret: await getClientSecretGitHub(),
code: code,
redirect_uri: `${await getSiteURL()}/integrations/github/oauth2/callback`
},
headers: {
'Accept': 'application/json',
'Accept-Encoding': 'application/json'
}
})
).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Github');
}
const res: ExchangeCodeGithubResponse = (
await standardRequest.get(INTEGRATION_GITHUB_TOKEN_URL, {
params: {
client_id: await getClientIdGitHub(),
client_secret: await getClientSecretGitHub(),
code: code,
redirect_uri: `${await getSiteURL()}/integrations/github/oauth2/callback`,
},
headers: {
Accept: "application/json",
"Accept-Encoding": "application/json",
},
})
).data;
return {
accessToken: res.access_token,
refreshToken: null,
accessExpiresAt: null
accessExpiresAt: null,
};
};
@ -369,42 +319,32 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeGitlab = async ({ code }: { code: string }) => {
let res: ExchangeCodeGitlabResponse;
const accessExpiresAt = new Date();
try {
res = (
await request.post(
INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: await getClientIdGitLab(),
client_secret: await getClientSecretGitLab(),
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`
} as any),
{
headers: {
"Accept-Encoding": "application/json",
}
}
)
).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Gitlab');
}
const res: ExchangeCodeGitlabResponse = (
await standardRequest.post(
INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: await getClientIdGitLab(),
client_secret: await getClientSecretGitLab(),
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
)
).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt
accessExpiresAt,
};
}
};
export { exchangeCode };

@ -1,29 +1,24 @@
import * as Sentry from '@sentry/node';
import request from '../config/request';
import { standardRequest } from "../config/request";
import { IIntegrationAuth } from "../models";
import {
IIntegrationAuth
} from '../models';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_GITLAB,
} from '../variables';
} from "../variables";
import {
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL
} from '../variables';
import {
IntegrationService
} from '../services';
INTEGRATION_GITLAB_TOKEN_URL,
} from "../variables";
import { IntegrationService } from "../services";
import {
getSiteURL,
getClientIdAzure,
getClientSecretAzure,
getClientSecretHeroku,
getClientIdGitLab,
getClientSecretGitLab
} from '../config';
getClientSecretGitLab,
} from "../config";
interface RefreshTokenAzureResponse {
token_type: string;
@ -60,60 +55,57 @@ interface RefreshTokenGitLabResponse {
*/
const exchangeRefresh = async ({
integrationAuth,
refreshToken
refreshToken,
}: {
integrationAuth: IIntegrationAuth;
refreshToken: string;
}) => {
interface TokenDetails {
accessToken: string;
refreshToken: string;
accessExpiresAt: Date;
}
let tokenDetails: TokenDetails;
try {
switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
tokenDetails = await exchangeRefreshAzure({
refreshToken
});
break;
case INTEGRATION_HEROKU:
tokenDetails = await exchangeRefreshHeroku({
refreshToken
});
break;
case INTEGRATION_GITLAB:
tokenDetails = await exchangeRefreshGitLab({
refreshToken
});
break;
default:
throw new Error('Failed to exchange token for incompatible integration');
}
if (tokenDetails?.accessToken && tokenDetails?.refreshToken && tokenDetails?.accessExpiresAt) {
await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: tokenDetails.accessToken,
accessExpiresAt: tokenDetails.accessExpiresAt
switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
tokenDetails = await exchangeRefreshAzure({
refreshToken,
});
await IntegrationService.setIntegrationAuthRefresh({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: tokenDetails.refreshToken
break;
case INTEGRATION_HEROKU:
tokenDetails = await exchangeRefreshHeroku({
refreshToken,
});
}
return tokenDetails.accessToken;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token');
break;
case INTEGRATION_GITLAB:
tokenDetails = await exchangeRefreshGitLab({
refreshToken,
});
break;
default:
throw new Error("Failed to exchange token for incompatible integration");
}
if (
tokenDetails?.accessToken &&
tokenDetails?.refreshToken &&
tokenDetails?.accessExpiresAt
) {
await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: tokenDetails.accessToken,
accessExpiresAt: tokenDetails.accessExpiresAt,
});
await IntegrationService.setIntegrationAuthRefresh({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: tokenDetails.refreshToken,
});
}
return tokenDetails.accessToken;
};
/**
@ -124,38 +116,30 @@ const exchangeRefresh = async ({
* @returns
*/
const exchangeRefreshAzure = async ({
refreshToken
refreshToken,
}: {
refreshToken: string;
}) => {
try {
const accessExpiresAt = new Date();
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
client_id: await getClientIdAzure(),
scope: 'openid offline_access',
refresh_token: refreshToken,
grant_type: 'refresh_token',
client_secret: await getClientSecretAzure()
} as any)
);
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
const accessExpiresAt = new Date();
const { data }: { data: RefreshTokenAzureResponse } = await standardRequest.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
client_id: await getClientIdAzure(),
scope: "openid offline_access",
refresh_token: refreshToken,
grant_type: "refresh_token",
client_secret: await getClientSecretAzure(),
} as any)
);
return ({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get refresh OAuth2 access token for Azure');
}
}
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt,
};
};
/**
* Return new access token by exchanging refresh token [refreshToken] for the
@ -165,39 +149,31 @@ const exchangeRefreshAzure = async ({
* @returns
*/
const exchangeRefreshHeroku = async ({
refreshToken
refreshToken,
}: {
refreshToken: string;
}) => {
try {
const accessExpiresAt = new Date();
const {
data
}: {
data: RefreshTokenHerokuResponse
} = await request.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_secret: await getClientSecretHeroku()
} as any)
);
const accessExpiresAt = new Date();
const {
data,
}: {
data: RefreshTokenHerokuResponse;
} = await standardRequest.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_secret: await getClientSecretHeroku(),
} as any)
);
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return ({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to refresh OAuth2 access token for Heroku');
}
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt,
};
};
/**
@ -208,45 +184,38 @@ const exchangeRefreshHeroku = async ({
* @returns
*/
const exchangeRefreshGitLab = async ({
refreshToken
refreshToken,
}: {
refreshToken: string;
}) => {
try {
const accessExpiresAt = new Date();
const {
data
}: {
data: RefreshTokenGitLabResponse
} = await request.post(
INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: await getClientIdGitLab,
client_secret: await getClientSecretGitLab(),
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`
} as any),
{
headers: {
"Accept-Encoding": "application/json",
}
});
const accessExpiresAt = new Date();
const {
data,
}: {
data: RefreshTokenGitLabResponse;
} = await standardRequest.post(
INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: await getClientIdGitLab(),
client_secret: await getClientSecretGitLab(),
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
);
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return ({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to refresh OAuth2 access token for GitLab');
}
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt,
};
};
export { exchangeRefresh };

@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import {
IIntegrationAuth,
IntegrationAuth,
@ -22,34 +21,28 @@ const revokeAccess = async ({
accessToken: string;
}) => {
let deletedIntegrationAuth;
try {
// add any integration-specific revocation logic
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
break;
case INTEGRATION_VERCEL:
break;
case INTEGRATION_NETLIFY:
break;
case INTEGRATION_GITHUB:
break;
case INTEGRATION_GITLAB:
break;
}
// add any integration-specific revocation logic
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
break;
case INTEGRATION_VERCEL:
break;
case INTEGRATION_NETLIFY:
break;
case INTEGRATION_GITHUB:
break;
case INTEGRATION_GITLAB:
break;
}
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id
});
if (deletedIntegrationAuth) {
await Integration.deleteMany({
integrationAuth: deletedIntegrationAuth._id
});
if (deletedIntegrationAuth) {
await Integration.deleteMany({
integrationAuth: deletedIntegrationAuth._id
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete integration authorization');
}
return deletedIntegrationAuth;

@ -37,8 +37,7 @@ import {
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL
} from "../variables";
import request from '../config/request';
import axios from "axios";
import { standardRequest} from '../config/request';
/**
* Sync/push [secrets] to [app] in integration named [integration]
@ -215,7 +214,7 @@ const syncSecretsAzureKeyVault = async ({
let result: GetAzureKeyVaultSecret[] = [];
try {
while (url) {
const res = await request.get(url, {
const res = await standardRequest.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`
}
@ -242,7 +241,7 @@ const syncSecretsAzureKeyVault = async ({
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/');
}
const azureKeyVaultSecret = await request.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
const azureKeyVaultSecret = await standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
@ -308,7 +307,7 @@ const syncSecretsAzureKeyVault = async ({
while (!isSecretSet && maxTries > 0) {
// try to set secret
try {
await request.put(
await standardRequest.put(
`${integration.app}/secrets/${key}?api-version=7.3`,
{
value
@ -325,7 +324,7 @@ const syncSecretsAzureKeyVault = async ({
} catch (err) {
const error: any = err;
if (error?.response?.data?.error?.innererror?.code === 'ObjectIsDeletedButRecoverable') {
await request.post(
await standardRequest.post(
`${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, {},
{
headers: {
@ -355,7 +354,7 @@ const syncSecretsAzureKeyVault = async ({
for await (const deleteSecret of deleteSecrets) {
const { key } = deleteSecret;
await request.delete(`${integration.app}/secrets/${key}?api-version=7.3`, {
await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
@ -568,7 +567,7 @@ const syncSecretsHeroku = async ({
}) => {
try {
const herokuSecrets = (
await request.get(
await standardRequest.get(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
{
headers: {
@ -586,7 +585,7 @@ const syncSecretsHeroku = async ({
}
});
await request.patch(
await standardRequest.patch(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
secrets,
{
@ -642,7 +641,7 @@ const syncSecretsVercel = async ({
: {}),
};
const vercelSecrets: VercelSecret[] = (await request.get(
const vercelSecrets: VercelSecret[] = (await standardRequest.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{
params,
@ -675,7 +674,7 @@ const syncSecretsVercel = async ({
for await (const vercelSecret of vercelSecrets) {
if (vercelSecret.type === 'encrypted') {
// case: secret is encrypted -> need to decrypt
const decryptedSecret = (await request.get(
const decryptedSecret = (await standardRequest.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`,
{
params,
@ -747,7 +746,7 @@ const syncSecretsVercel = async ({
// Sync/push new secrets
if (newSecrets.length > 0) {
await request.post(
await standardRequest.post(
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
newSecrets,
{
@ -763,7 +762,7 @@ const syncSecretsVercel = async ({
for await (const secret of updateSecrets) {
if (secret.type !== 'sensitive') {
const { id, ...updatedSecret } = secret;
await request.patch(
await standardRequest.patch(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
updatedSecret,
{
@ -778,7 +777,7 @@ const syncSecretsVercel = async ({
}
for await (const secret of deleteSecrets) {
await request.delete(
await standardRequest.delete(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
params,
@ -837,7 +836,7 @@ const syncSecretsNetlify = async ({
});
const res = (
await request.get(
await standardRequest.get(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
{
params: getParams,
@ -951,7 +950,7 @@ const syncSecretsNetlify = async ({
});
if (newSecrets.length > 0) {
await request.post(
await standardRequest.post(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
newSecrets,
{
@ -966,7 +965,7 @@ const syncSecretsNetlify = async ({
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: NetlifySecret) => {
await request.patch(
await standardRequest.patch(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
{
context: secret.values[0].context,
@ -985,7 +984,7 @@ const syncSecretsNetlify = async ({
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (key: string) => {
await request.delete(
await standardRequest.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
{
params: syncParams,
@ -1000,7 +999,7 @@ const syncSecretsNetlify = async ({
if (deleteSecretValues.length > 0) {
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
await request.delete(
await standardRequest.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
{
params: syncParams,
@ -1151,7 +1150,7 @@ const syncSecretsRender = async ({
accessToken: string;
}) => {
try {
await request.put(
await standardRequest.put(
`${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`,
Object.keys(secrets).map((key) => ({
key,
@ -1203,7 +1202,7 @@ const syncSecretsRailway = async ({
variables: secrets
};
await request.post(INTEGRATION_RAILWAY_API_URL, {
await standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables: {
input,
@ -1261,7 +1260,7 @@ const syncSecretsFlyio = async ({
}
`;
await request.post(INTEGRATION_FLYIO_API_URL, {
await standardRequest.post(INTEGRATION_FLYIO_API_URL, {
query: SetSecrets,
variables: {
input: {
@ -1296,7 +1295,7 @@ const syncSecretsFlyio = async ({
}
}`;
const getSecretsRes = (await request.post(INTEGRATION_FLYIO_API_URL, {
const getSecretsRes = (await standardRequest.post(INTEGRATION_FLYIO_API_URL, {
query: GetSecrets,
variables: {
appName: integration.app,
@ -1332,7 +1331,7 @@ const syncSecretsFlyio = async ({
}
}`;
await request.post(INTEGRATION_FLYIO_API_URL, {
await standardRequest.post(INTEGRATION_FLYIO_API_URL, {
query: DeleteSecrets,
variables: {
input: {
@ -1373,7 +1372,7 @@ const syncSecretsCircleCI = async ({
}) => {
try {
const circleciOrganizationDetail = (
await request.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, {
await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
@ -1386,7 +1385,7 @@ const syncSecretsCircleCI = async ({
// sync secrets to CircleCI
Object.keys(secrets).forEach(
async (key) =>
await request.post(
await standardRequest.post(
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
{
name: key,
@ -1403,7 +1402,7 @@ const syncSecretsCircleCI = async ({
// get secrets from CircleCI
const getSecretsRes = (
await request.get(
await standardRequest.get(
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
{
headers: {
@ -1417,7 +1416,7 @@ const syncSecretsCircleCI = async ({
// delete secrets from CircleCI
getSecretsRes.forEach(async (sec: any) => {
if (!(sec.name in secrets)) {
await request.delete(
await standardRequest.delete(
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`,
{
headers: {
@ -1454,7 +1453,7 @@ const syncSecretsTravisCI = async ({
try {
// get secrets from travis-ci
const getSecretsRes = (
await request.get(
await standardRequest.get(
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`,
{
headers: {
@ -1476,7 +1475,7 @@ const syncSecretsTravisCI = async ({
if (!(key in getSecretsRes)) {
// case: secret does not exist in travis ci
// -> add secret
await request.post(
await standardRequest.post(
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`,
{
env_var: {
@ -1495,7 +1494,7 @@ const syncSecretsTravisCI = async ({
} else {
// case: secret exists in travis ci
// -> update/set secret
await request.patch(
await standardRequest.patch(
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`,
{
env_var: {
@ -1517,7 +1516,7 @@ const syncSecretsTravisCI = async ({
for await (const key of Object.keys(getSecretsRes)) {
if (!(key in secrets)){
// delete secret
await request.delete(
await standardRequest.delete(
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`,
{
headers: {
@ -1554,9 +1553,15 @@ const syncSecretsGitLab = async ({
accessToken: string;
}) => {
try {
interface GitLabSecret {
key: string;
value: string;
environment_scope: string;
}
// get secrets from gitlab
const getSecretsRes = (
await request.get(
const getSecretsRes: GitLabSecret[] = (
await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
{
headers: {
@ -1565,12 +1570,16 @@ const syncSecretsGitLab = async ({
},
}
)
).data;
)
.data
.filter((secret: GitLabSecret) =>
secret.environment_scope === integration.targetEnvironment
);
for await (const key of Object.keys(secrets)) {
const existingSecret = getSecretsRes.find((s: any) => s.key == key);
if (!existingSecret) {
await request.post(
await standardRequest.post(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
{
key: key,
@ -1578,7 +1587,7 @@ const syncSecretsGitLab = async ({
protected: false,
masked: false,
raw: false,
environment_scope:'*'
environment_scope: integration.targetEnvironment
},
{
headers: {
@ -1589,29 +1598,31 @@ const syncSecretsGitLab = async ({
}
)
} else {
// udpate secret
await request.put(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}`,
{
...existingSecret,
value: secrets[existingSecret.key]
},
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
// update secret
if (secrets[key] !== existingSecret.value) {
await standardRequest.put(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
{
...existingSecret,
value: secrets[existingSecret.key]
},
}
)
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
}
);
}
}
}
// delete secrets
for await (const sec of getSecretsRes) {
if (!(sec.key in secrets)) {
await request.delete(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}`,
await standardRequest.delete(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
@ -1620,7 +1631,7 @@ const syncSecretsGitLab = async ({
);
}
}
}catch (err) {
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to GitLab");
@ -1645,7 +1656,7 @@ const syncSecretsSupabase = async ({
accessToken: string;
}) => {
try {
const { data: getSecretsRes } = await request.get(
const { data: getSecretsRes } = await standardRequest.get(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {
@ -1665,7 +1676,7 @@ const syncSecretsSupabase = async ({
}
);
await request.post(
await standardRequest.post(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
modifiedFormatForSecretInjection,
{
@ -1683,7 +1694,7 @@ const syncSecretsSupabase = async ({
}
});
await request.delete(
await standardRequest.delete(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {

@ -1,4 +1,3 @@
import * as Sentry from "@sentry/node";
import {
IIntegrationAuth
} from '../models';
@ -6,7 +5,7 @@ import {
INTEGRATION_GITLAB,
INTEGRATION_GITLAB_API_URL
} from '../variables';
import request from '../config/request';
import { standardRequest } from '../config/request';
interface Team {
name: string;
@ -31,21 +30,15 @@ const getTeams = async ({
}) => {
let teams: Team[] = [];
try {
switch (integrationAuth.integration) {
case INTEGRATION_GITLAB:
teams = await getTeamsGitLab({
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration teams');
switch (integrationAuth.integration) {
case INTEGRATION_GITLAB:
teams = await getTeamsGitLab({
accessToken
});
break;
}
return teams;
}
@ -63,30 +56,24 @@ const getTeamsGitLab = async ({
accessToken: string;
}) => {
let teams: Team[] = [];
try {
const res = (await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
const res = (await standardRequest.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
)).data;
teams = res.map((t: any) => ({
name: t.name,
teamId: t.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get GitLab integration teams");
}
}
)).data;
teams = res.map((t: any) => ({
name: t.name,
teamId: t.id
}));
return teams;
}
export {
getTeams
}
}

@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { IntegrationAuth, IWorkspace } from '../models';

@ -1,8 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { validateMembership } from '../helpers/membership';
import { validateClientForWorkspace } from '../helpers/workspace';
import { UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query';
@ -31,7 +29,7 @@ const requireWorkspaceAuth = ({
const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined;
// validate clients
const { membership } = await validateClientForWorkspace({
const { membership, workspace } = await validateClientForWorkspace({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
@ -43,6 +41,10 @@ const requireWorkspaceAuth = ({
if (membership) {
req.membership = membership;
}
if (workspace) {
req.workspace = workspace;
}
return next();
};

@ -21,6 +21,7 @@ export interface IIntegration {
workspace: Types.ObjectId;
environment: string;
isActive: boolean;
url: string;
app: string;
appId: string;
owner: string;
@ -63,6 +64,11 @@ const integrationSchema = new Schema<IIntegration>(
type: Boolean,
required: true,
},
url: {
// for custom self-hosted integrations (e.g. self-hosted GitHub enterprise)
type: String,
default: null
},
app: {
// name of app in provider
type: String,

@ -6,7 +6,7 @@ import {
validateRequest
} from '../../middleware';
import { body, param } from 'express-validator';
import { createFolder, deleteFolder } from '../../controllers/v1/secretsFolderController';
import { createFolder, deleteFolder, getFolderById } from '../../controllers/v1/secretsFolderController';
import { ADMIN, MEMBER } from '../../variables';
router.post(
@ -36,5 +36,15 @@ router.delete(
deleteFolder
);
router.get(
'/:folderId',
requireAuth({
acceptedAuthModes: ['jwt']
}),
param('folderId').exists(),
validateRequest,
getFolderById
);
export default router;

@ -3,7 +3,8 @@ import {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL
SMTP_HOST_ZOHOMAIL,
SMTP_HOST_GMAIL
} from '../variables';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import * as Sentry from '@sentry/node';
@ -46,6 +47,12 @@ export const initSmtp = async () => {
}
break;
case SMTP_HOST_ZOHOMAIL:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
case SMTP_HOST_GMAIL:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'

@ -1,7 +1,6 @@
import nacl from 'tweetnacl';
import util from 'tweetnacl-util';
import AesGCM from './aes-gcm';
import * as Sentry from '@sentry/node';
/**
* Return new base64, NaCl, public-private key pair.
@ -38,20 +37,13 @@ const encryptAsymmetric = ({
publicKey: string;
privateKey: string;
}) => {
let nonce, ciphertext;
try {
nonce = nacl.randomBytes(24);
ciphertext = nacl.box(
util.decodeUTF8(plaintext),
nonce,
util.decodeBase64(publicKey),
util.decodeBase64(privateKey)
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform asymmetric encryption');
}
const nonce = nacl.randomBytes(24);
const ciphertext = nacl.box(
util.decodeUTF8(plaintext),
nonce,
util.decodeBase64(publicKey),
util.decodeBase64(privateKey)
);
return {
ciphertext: util.encodeBase64(ciphertext),
@ -80,19 +72,12 @@ const decryptAsymmetric = ({
publicKey: string;
privateKey: string;
}): string => {
let plaintext: any;
try {
plaintext = nacl.box.open(
util.decodeBase64(ciphertext),
util.decodeBase64(nonce),
util.decodeBase64(publicKey),
util.decodeBase64(privateKey)
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform asymmetric decryption');
}
const plaintext: any = nacl.box.open(
util.decodeBase64(ciphertext),
util.decodeBase64(nonce),
util.decodeBase64(publicKey),
util.decodeBase64(privateKey)
);
return util.encodeUTF8(plaintext);
};
@ -110,17 +95,8 @@ const encryptSymmetric = ({
plaintext: string;
key: string;
}) => {
let ciphertext, iv, tag;
try {
const obj = AesGCM.encrypt(plaintext, key);
ciphertext = obj.ciphertext;
iv = obj.iv;
tag = obj.tag;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric encryption');
}
const obj = AesGCM.encrypt(plaintext, key);
const { ciphertext, iv, tag } = obj;
return {
ciphertext,
@ -150,15 +126,7 @@ const decryptSymmetric = ({
tag: string;
key: string;
}): string => {
let plaintext;
try {
plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric decryption');
}
const plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
return plaintext;
};

@ -55,7 +55,8 @@ import {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL
SMTP_HOST_ZOHOMAIL,
SMTP_HOST_GMAIL
} from './smtp';
import { PLAN_STARTER, PLAN_PRO } from './stripe';
import {
@ -138,6 +139,7 @@ export {
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL,
SMTP_HOST_GMAIL,
PLAN_STARTER,
PLAN_PRO,
MFA_METHOD_EMAIL,

@ -2,10 +2,12 @@ const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net';
const SMTP_HOST_MAILGUN = 'smtp.mailgun.org';
const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com';
const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com';
const SMTP_HOST_GMAIL = 'smtp.gmail.com';
export {
SMTP_HOST_SENDGRID,
SMTP_HOST_MAILGUN,
SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL
SMTP_HOST_ZOHOMAIL,
SMTP_HOST_GMAIL
}

@ -28,14 +28,14 @@ describe('Crypto', () => {
test('should throw error if publicKey is undefined', () => {
expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption');
}).toThrowError('invalid encoding');
});
test('should throw error if publicKey is empty string', () => {
publicKey = '';
expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption');
}).toThrowError('bad public key size');
});
});
@ -47,14 +47,14 @@ describe('Crypto', () => {
test('should throw error if privateKey is undefined', () => {
expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption');
}).toThrowError('invalid encoding');
});
test('should throw error if privateKey is empty string', () => {
privateKey = '';
expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption');
}).toThrowError('bad secret key size');
});
});
@ -66,7 +66,7 @@ describe('Crypto', () => {
test('should throw error if plaintext is undefined', () => {
expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption');
}).toThrowError('expected string');
});
test('should encrypt plaintext containing special characters', () => {
@ -130,7 +130,7 @@ describe('Crypto', () => {
publicKey,
privateKey
});
}).toThrowError('Failed to perform asymmetric decryption');
}).toThrowError('invalid encoding');
});
test('should throw error if nonce is modified', () => {
@ -149,7 +149,7 @@ describe('Crypto', () => {
publicKey,
privateKey
});
}).toThrowError('Failed to perform asymmetric decryption');
}).toThrowError('invalid encoding');
});
});
});
@ -170,7 +170,7 @@ describe('Crypto', () => {
const invalidKey = 'invalid-key';
expect(() => {
encryptSymmetric({ plaintext, key: invalidKey });
}).toThrowError('Failed to perform symmetric encryption');
}).toThrowError('Invalid key length');
});
test('should throw an error when invalid key is provided', () => {
@ -179,7 +179,7 @@ describe('Crypto', () => {
expect(() => {
encryptSymmetric({ plaintext, key: invalidKey });
}).toThrowError('Failed to perform symmetric encryption');
}).toThrowError('Invalid key length');
});
});
@ -209,7 +209,7 @@ describe('Crypto', () => {
tag,
key
});
}).toThrowError('Failed to perform symmetric decryption');
}).toThrowError('Unsupported state or unable to authenticate data');
});
test('should fail if iv is modified', () => {
@ -221,7 +221,7 @@ describe('Crypto', () => {
tag,
key
});
}).toThrowError('Failed to perform symmetric decryption');
}).toThrowError('Unsupported state or unable to authenticate data');
});
test('should fail if tag is modified', () => {
@ -233,7 +233,7 @@ describe('Crypto', () => {
tag: modifiedTag,
key
});
}).toThrowError('Failed to perform symmetric decryption');
}).toThrowError(/Invalid authentication tag length: \d+/);
});
test('should throw an error when decryption fails', () => {
@ -245,7 +245,7 @@ describe('Crypto', () => {
tag,
key: invalidKey
});
}).toThrowError('Failed to perform symmetric decryption');
}).toThrowError('Invalid key length');
});
});
});

3
cli/.infisicalignore Normal file

@ -0,0 +1,3 @@
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

85
cli/config/allowlist.go Normal file

@ -0,0 +1,85 @@
// 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 (
"regexp"
"strings"
)
// 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
// Regexes is slice of content regular expressions that are allowed to be ignored.
Regexes []*regexp.Regexp
// RegexTarget
RegexTarget string
// Paths is a slice of path regular expressions that are allowed to be ignored.
Paths []*regexp.Regexp
// Commits is a slice of commit SHAs that are allowed to be ignored.
Commits []string
// 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
}
// CommitAllowed returns true if the commit is allowed to be ignored.
func (a *Allowlist) CommitAllowed(c string) bool {
if c == "" {
return false
}
for _, commit := range a.Commits {
if commit == c {
return true
}
}
return false
}
// PathAllowed returns true if the path is allowed to be ignored.
func (a *Allowlist) PathAllowed(path string) bool {
return anyRegexMatch(path, a.Paths)
}
// RegexAllowed returns true if the regex is allowed to be ignored.
func (a *Allowlist) RegexAllowed(s string) bool {
return anyRegexMatch(s, a.Regexes)
}
func (a *Allowlist) ContainsStopWord(s string) bool {
s = strings.ToLower(s)
for _, stopWord := range a.StopWords {
if strings.Contains(s, strings.ToLower(stopWord)) {
return true
}
}
return false
}

@ -0,0 +1,115 @@
// 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 (
"regexp"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCommitAllowed(t *testing.T) {
tests := []struct {
allowlist Allowlist
commit string
commitAllowed bool
}{
{
allowlist: Allowlist{
Commits: []string{"commitA"},
},
commit: "commitA",
commitAllowed: true,
},
{
allowlist: Allowlist{
Commits: []string{"commitB"},
},
commit: "commitA",
commitAllowed: false,
},
{
allowlist: Allowlist{
Commits: []string{"commitB"},
},
commit: "",
commitAllowed: false,
},
}
for _, tt := range tests {
assert.Equal(t, tt.commitAllowed, tt.allowlist.CommitAllowed(tt.commit))
}
}
func TestRegexAllowed(t *testing.T) {
tests := []struct {
allowlist Allowlist
secret string
regexAllowed bool
}{
{
allowlist: Allowlist{
Regexes: []*regexp.Regexp{regexp.MustCompile("matchthis")},
},
secret: "a secret: matchthis, done",
regexAllowed: true,
},
{
allowlist: Allowlist{
Regexes: []*regexp.Regexp{regexp.MustCompile("matchthis")},
},
secret: "a secret",
regexAllowed: false,
},
}
for _, tt := range tests {
assert.Equal(t, tt.regexAllowed, tt.allowlist.RegexAllowed(tt.secret))
}
}
func TestPathAllowed(t *testing.T) {
tests := []struct {
allowlist Allowlist
path string
pathAllowed bool
}{
{
allowlist: Allowlist{
Paths: []*regexp.Regexp{regexp.MustCompile("path")},
},
path: "a path",
pathAllowed: true,
},
{
allowlist: Allowlist{
Paths: []*regexp.Regexp{regexp.MustCompile("path")},
},
path: "a ???",
pathAllowed: false,
},
}
for _, tt := range tests {
assert.Equal(t, tt.pathAllowed, tt.allowlist.PathAllowed(tt.path))
}
}

279
cli/config/config.go Normal file

@ -0,0 +1,279 @@
// 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"
"fmt"
"regexp"
"strings"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
//go:embed infisical-scan.toml
var DefaultConfig string
// use to keep track of how many configs we can extend
// yea I know, globals bad
var extendDepth int
const maxExtendDepth = 2
const DefaultScanConfigFileName = ".infisical-scan.toml"
const DefaultScanConfigEnvName = "INFISICAL_SCAN_CONFIG"
const DefaultInfisicalIgnoreFineName = ".infisicalignore"
// 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 {
Description string
Extend Extend
Rules []struct {
ID string
Description string
Entropy float64
SecretGroup int
Regex string
Keywords []string
Path string
Tags []string
Allowlist struct {
RegexTarget string
Regexes []string
Paths []string
Commits []string
StopWords []string
}
}
Allowlist struct {
RegexTarget string
Regexes []string
Paths []string
Commits []string
StopWords []string
}
}
// Config is a configuration struct that contains rules and an allowlist if present.
type Config struct {
Extend Extend
Path string
Description string
Rules map[string]Rule
Allowlist Allowlist
Keywords []string
// used to keep sarif results consistent
orderedRules []string
}
// 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
}
func (vc *ViperConfig) Translate() (Config, error) {
var (
keywords []string
orderedRules []string
)
rulesMap := make(map[string]Rule)
for _, r := range vc.Rules {
var allowlistRegexes []*regexp.Regexp
for _, a := range r.Allowlist.Regexes {
allowlistRegexes = append(allowlistRegexes, regexp.MustCompile(a))
}
var allowlistPaths []*regexp.Regexp
for _, a := range r.Allowlist.Paths {
allowlistPaths = append(allowlistPaths, regexp.MustCompile(a))
}
if r.Keywords == nil {
r.Keywords = []string{}
} else {
for _, k := range r.Keywords {
keywords = append(keywords, strings.ToLower(k))
}
}
if r.Tags == nil {
r.Tags = []string{}
}
var configRegex *regexp.Regexp
var configPathRegex *regexp.Regexp
if r.Regex == "" {
configRegex = nil
} else {
configRegex = regexp.MustCompile(r.Regex)
}
if r.Path == "" {
configPathRegex = nil
} else {
configPathRegex = regexp.MustCompile(r.Path)
}
r := Rule{
Description: r.Description,
RuleID: r.ID,
Regex: configRegex,
Path: configPathRegex,
SecretGroup: r.SecretGroup,
Entropy: r.Entropy,
Tags: r.Tags,
Keywords: r.Keywords,
Allowlist: Allowlist{
RegexTarget: r.Allowlist.RegexTarget,
Regexes: allowlistRegexes,
Paths: allowlistPaths,
Commits: r.Allowlist.Commits,
StopWords: r.Allowlist.StopWords,
},
}
orderedRules = append(orderedRules, r.RuleID)
if r.Regex != nil && r.SecretGroup > r.Regex.NumSubexp() {
return Config{}, fmt.Errorf("%s invalid regex secret group %d, max regex secret group %d", r.Description, r.SecretGroup, r.Regex.NumSubexp())
}
rulesMap[r.RuleID] = r
}
var allowlistRegexes []*regexp.Regexp
for _, a := range vc.Allowlist.Regexes {
allowlistRegexes = append(allowlistRegexes, regexp.MustCompile(a))
}
var allowlistPaths []*regexp.Regexp
for _, a := range vc.Allowlist.Paths {
allowlistPaths = append(allowlistPaths, regexp.MustCompile(a))
}
c := Config{
Description: vc.Description,
Extend: vc.Extend,
Rules: rulesMap,
Allowlist: Allowlist{
RegexTarget: vc.Allowlist.RegexTarget,
Regexes: allowlistRegexes,
Paths: allowlistPaths,
Commits: vc.Allowlist.Commits,
StopWords: vc.Allowlist.StopWords,
},
Keywords: keywords,
orderedRules: orderedRules,
}
if maxExtendDepth != extendDepth {
// disallow both usedefault and path from being set
if c.Extend.Path != "" && c.Extend.UseDefault {
log.Fatal().Msg("unable to load config due to extend.path and extend.useDefault being set")
}
if c.Extend.UseDefault {
c.extendDefault()
} else if c.Extend.Path != "" {
c.extendPath()
}
}
return c, nil
}
func (c *Config) OrderedRules() []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() {
extendDepth++
viper.SetConfigType("toml")
if err := viper.ReadConfig(strings.NewReader(DefaultConfig)); err != nil {
log.Fatal().Msgf("failed to load extended config, err: %s", err)
return
}
defaultViperConfig := ViperConfig{}
if err := viper.Unmarshal(&defaultViperConfig); err != nil {
log.Fatal().Msgf("failed to load extended config, err: %s", err)
return
}
cfg, err := defaultViperConfig.Translate()
if err != nil {
log.Fatal().Msgf("failed to load extended config, err: %s", err)
return
}
log.Debug().Msg("extending config with default config")
c.extend(cfg)
}
func (c *Config) extendPath() {
extendDepth++
viper.SetConfigFile(c.Extend.Path)
if err := viper.ReadInConfig(); err != nil {
log.Fatal().Msgf("failed to load extended config, err: %s", err)
return
}
extensionViperConfig := ViperConfig{}
if err := viper.Unmarshal(&extensionViperConfig); err != nil {
log.Fatal().Msgf("failed to load extended config, err: %s", err)
return
}
cfg, err := extensionViperConfig.Translate()
if err != nil {
log.Fatal().Msgf("failed to load extended config, err: %s", err)
return
}
log.Debug().Msgf("extending config with %s", c.Extend.Path)
c.extend(cfg)
}
func (c *Config) extendURL() {
// TODO
}
func (c *Config) extend(extensionConfig Config) {
for ruleID, rule := range extensionConfig.Rules {
if _, ok := c.Rules[ruleID]; !ok {
log.Trace().Msgf("adding %s to base config", ruleID)
c.Rules[ruleID] = rule
c.Keywords = append(c.Keywords, rule.Keywords...)
}
}
// append allowlists, not attempting to merge
c.Allowlist.Commits = append(c.Allowlist.Commits,
extensionConfig.Allowlist.Commits...)
c.Allowlist.Paths = append(c.Allowlist.Paths,
extensionConfig.Allowlist.Paths...)
c.Allowlist.Regexes = append(c.Allowlist.Regexes,
extensionConfig.Allowlist.Regexes...)
}

170
cli/config/config_test.go Normal file

@ -0,0 +1,170 @@
// 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"
"regexp"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)
const configPath = "../testdata/config/"
func TestTranslate(t *testing.T) {
tests := []struct {
cfgName string
cfg Config
wantError error
}{
{
cfgName: "allow_aws_re",
cfg: Config{
Rules: map[string]Rule{"aws-access-key": {
Description: "AWS Access Key",
Regex: regexp.MustCompile("(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
Tags: []string{"key", "AWS"},
Keywords: []string{},
RuleID: "aws-access-key",
Allowlist: Allowlist{
Regexes: []*regexp.Regexp{
regexp.MustCompile("AKIALALEMEL33243OLIA"),
},
},
},
},
},
},
{
cfgName: "allow_commit",
cfg: Config{
Rules: map[string]Rule{"aws-access-key": {
Description: "AWS Access Key",
Regex: regexp.MustCompile("(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
Tags: []string{"key", "AWS"},
Keywords: []string{},
RuleID: "aws-access-key",
Allowlist: Allowlist{
Commits: []string{"allowthiscommit"},
},
},
},
},
},
{
cfgName: "allow_path",
cfg: Config{
Rules: map[string]Rule{"aws-access-key": {
Description: "AWS Access Key",
Regex: regexp.MustCompile("(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
Tags: []string{"key", "AWS"},
Keywords: []string{},
RuleID: "aws-access-key",
Allowlist: Allowlist{
Paths: []*regexp.Regexp{
regexp.MustCompile(".go"),
},
},
},
},
},
},
{
cfgName: "entropy_group",
cfg: Config{
Rules: map[string]Rule{"discord-api-key": {
Description: "Discord API key",
Regex: regexp.MustCompile(`(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]`),
RuleID: "discord-api-key",
Allowlist: Allowlist{},
Entropy: 3.5,
SecretGroup: 3,
Tags: []string{},
Keywords: []string{},
},
},
},
},
{
cfgName: "bad_entropy_group",
cfg: Config{},
wantError: fmt.Errorf("Discord API key invalid regex secret group 5, max regex secret group 3"),
},
{
cfgName: "base",
cfg: Config{
Rules: map[string]Rule{
"aws-access-key": {
Description: "AWS Access Key",
Regex: regexp.MustCompile("(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
Tags: []string{"key", "AWS"},
Keywords: []string{},
RuleID: "aws-access-key",
},
"aws-secret-key": {
Description: "AWS Secret Key",
Regex: regexp.MustCompile(`(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}`),
Tags: []string{"key", "AWS"},
Keywords: []string{},
RuleID: "aws-secret-key",
},
"aws-secret-key-again": {
Description: "AWS Secret Key",
Regex: regexp.MustCompile(`(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}`),
Tags: []string{"key", "AWS"},
Keywords: []string{},
RuleID: "aws-secret-key-again",
},
},
},
},
}
for _, tt := range tests {
viper.Reset()
viper.AddConfigPath(configPath)
viper.SetConfigName(tt.cfgName)
viper.SetConfigType("toml")
err := viper.ReadInConfig()
if err != nil {
t.Error(err)
}
var vc ViperConfig
err = viper.Unmarshal(&vc)
if err != nil {
t.Error(err)
}
cfg, err := vc.Translate()
if tt.wantError != nil {
if err == nil {
t.Errorf("expected error")
}
assert.Equal(t, tt.wantError, err)
}
assert.Equal(t, cfg.Rules, tt.cfg.Rules)
}
}

File diff suppressed because it is too large Load Diff

43
cli/config/rule.go Normal file

@ -0,0 +1,43 @@
package config
import (
"regexp"
)
// Rules contain information that define details on how to detect secrets
type Rule struct {
// Description is the description of the rule.
Description string
// RuleID is a unique identifier for this rule
RuleID string
// Entropy is a float representing the minimum shannon
// entropy a regex group must have to be considered a secret.
Entropy float64
// SecretGroup is an int used to extract secret from regex
// match and used as the group that will have its entropy
// checked if `entropy` is set.
SecretGroup int
// Regex is a golang regular expression used to detect secrets.
Regex *regexp.Regexp
// Path is a golang regular expression used to
// filter secrets by path
Path *regexp.Regexp
// Tags is an array of strings used for metadata
// and reporting purposes.
Tags []string
// Keywords are used for pre-regex check filtering. Rules that contain
// keywords will perform a quick string compare check to make sure the
// keyword(s) are in the content being scanned.
Keywords []string
// Allowlist allows a rule to be ignored for specific
// regexes, paths, and/or commits
Allowlist Allowlist
}

24
cli/config/utils.go Normal file

@ -0,0 +1,24 @@
package config
import (
"regexp"
)
func anyRegexMatch(f string, res []*regexp.Regexp) bool {
for _, re := range res {
if regexMatched(f, re) {
return true
}
}
return false
}
func regexMatched(f string, re *regexp.Regexp) bool {
if re == nil {
return false
}
if re.FindString(f) != "" {
return true
}
return false
}

87
cli/detect/baseline.go Normal file

@ -0,0 +1,87 @@
// 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"
"io"
"os"
"github.com/rs/zerolog/log"
"github.com/Infisical/infisical-merge/report"
)
func IsNew(finding report.Finding, 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 maintanance if/when the Finding struct changes
for _, b := range baseline {
if finding.Author == b.Author &&
finding.Commit == b.Commit &&
finding.Date == b.Date &&
finding.Description == b.Description &&
finding.Email == b.Email &&
finding.EndColumn == b.EndColumn &&
finding.EndLine == b.EndLine &&
finding.Entropy == b.Entropy &&
finding.File == b.File &&
// Omit checking finding.Fingerprint - if the format of the fingerprint changes, the users will see unexpected behaviour
finding.Match == b.Match &&
finding.Message == b.Message &&
finding.RuleID == b.RuleID &&
finding.Secret == b.Secret &&
finding.StartColumn == b.StartColumn &&
finding.StartLine == b.StartLine {
return false
}
}
return true
}
func LoadBaseline(baselinePath string) ([]report.Finding, error) {
var previousFindings []report.Finding
jsonFile, err := os.Open(baselinePath)
if err != nil {
return nil, fmt.Errorf("could not open %s", baselinePath)
}
defer func() {
if cerr := jsonFile.Close(); cerr != nil {
log.Warn().Err(cerr).Msg("problem closing jsonFile handle")
}
}()
bytes, err := io.ReadAll(jsonFile)
if err != nil {
return nil, fmt.Errorf("could not read data from the file %s", baselinePath)
}
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
}

160
cli/detect/baseline_test.go Normal file

@ -0,0 +1,160 @@
// 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 (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/Infisical/infisical-merge/report"
)
func TestIsNew(t *testing.T) {
tests := []struct {
findings report.Finding
baseline []report.Finding
expect bool
}{
{
findings: report.Finding{
Author: "a",
Commit: "0000",
},
baseline: []report.Finding{
{
Author: "a",
Commit: "0000",
},
},
expect: false,
},
{
findings: report.Finding{
Author: "a",
Commit: "0000",
},
baseline: []report.Finding{
{
Author: "a",
Commit: "0002",
},
},
expect: true,
},
{
findings: report.Finding{
Author: "a",
Commit: "0000",
Tags: []string{"a", "b"},
},
baseline: []report.Finding{
{
Author: "a",
Commit: "0000",
Tags: []string{"a", "c"},
},
},
expect: false, // Updated tags doesn't make it a new finding
},
}
for _, test := range tests {
assert.Equal(t, test.expect, IsNew(test.findings, test.baseline))
}
}
func TestFileLoadBaseline(t *testing.T) {
tests := []struct {
Filename string
ExpectedError error
}{
{
Filename: "../testdata/baseline/baseline.csv",
ExpectedError: errors.New("the format of the file ../testdata/baseline/baseline.csv is not supported"),
},
{
Filename: "../testdata/baseline/baseline.sarif",
ExpectedError: errors.New("the format of the file ../testdata/baseline/baseline.sarif is not supported"),
},
{
Filename: "../testdata/baseline/notfound.json",
ExpectedError: errors.New("could not open ../testdata/baseline/notfound.json"),
},
}
for _, test := range tests {
_, err := LoadBaseline(test.Filename)
assert.Equal(t, test.ExpectedError.Error(), err.Error())
}
}
func TestIgnoreIssuesInBaseline(t *testing.T) {
tests := []struct {
findings []report.Finding
baseline []report.Finding
expectCount int
}{
{
findings: []report.Finding{
{
Author: "a",
Commit: "5",
},
},
baseline: []report.Finding{
{
Author: "a",
Commit: "5",
},
},
expectCount: 0,
},
{
findings: []report.Finding{
{
Author: "a",
Commit: "5",
Fingerprint: "a",
},
},
baseline: []report.Finding{
{
Author: "a",
Commit: "5",
Fingerprint: "b",
},
},
expectCount: 0,
},
}
for _, test := range tests {
d, _ := NewDetectorDefaultConfig()
d.baseline = test.baseline
for _, finding := range test.findings {
d.addFinding(finding)
}
assert.Equal(t, test.expectCount, len(d.findings))
}
}

652
cli/detect/detect.go Normal file

@ -0,0 +1,652 @@
// 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 (
"bufio"
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/h2non/filetype"
"github.com/Infisical/infisical-merge/config"
"github.com/Infisical/infisical-merge/detect/git"
"github.com/Infisical/infisical-merge/report"
"github.com/fatih/semgroup"
"github.com/gitleaks/go-gitdiff/gitdiff"
ahocorasick "github.com/petar-dambovaliev/aho-corasick"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
// Type used to differentiate between git scan types:
// $ gitleaks detect
// $ gitleaks protect
// $ gitleaks protect staged
type GitScanType int
const (
DetectType GitScanType = iota
ProtectType
ProtectStagedType
gitleaksAllowSignature = "infisical-scan:ignore"
)
// Detector is the main detector struct
type Detector struct {
// Config is the configuration for the detector
Config config.Config
// Redact is a flag to redact findings. This is exported
// so users using gitleaks as a library can set this flag
// without calling `detector.Start(cmd *cobra.Command)`
Redact bool
// verbose is a flag to print findings
Verbose bool
// files larger than this will be skipped
MaxTargetMegaBytes int
// followSymlinks is a flag to enable scanning symlink files
FollowSymlinks bool
// NoColor is a flag to disable color output
NoColor bool
// commitMap is used to keep track of commits that have been scanned.
// This is only used for logging purposes and git scans.
commitMap map[string]bool
// findingMutex is to prevent concurrent access to the
// findings slice when adding findings.
findingMutex *sync.Mutex
// findings is a slice of report.Findings. This is the result
// of the detector's scan which can then be used to generate a
// report.
findings []report.Finding
// prefilter is a ahocorasick struct used for doing efficient string
// matching given a set of words (keywords from the rules in the config)
prefilter ahocorasick.AhoCorasick
// a list of known findings that should be ignored
baseline []report.Finding
// path to baseline
baselinePath string
// gitleaksIgnore
gitleaksIgnore map[string]bool
}
// Fragment contains the data to be scanned
type Fragment struct {
// Raw is the raw content of the fragment
Raw string
// FilePath is the path to the file if applicable
FilePath string
SymlinkFile string
// CommitSHA is the SHA of the commit if applicable
CommitSHA string
// newlineIndices is a list of indices of newlines in the raw content.
// This is used to calculate the line location of a finding
newlineIndices [][]int
// keywords is a map of all the keywords contain within the contents
// of this fragment
keywords map[string]bool
}
// NewDetector creates a new detector with the given config
func NewDetector(cfg config.Config) *Detector {
builder := ahocorasick.NewAhoCorasickBuilder(ahocorasick.Opts{
AsciiCaseInsensitive: true,
MatchOnlyWholeWords: false,
MatchKind: ahocorasick.LeftMostLongestMatch,
DFA: true,
})
return &Detector{
commitMap: make(map[string]bool),
gitleaksIgnore: make(map[string]bool),
findingMutex: &sync.Mutex{},
findings: make([]report.Finding, 0),
Config: cfg,
prefilter: builder.Build(cfg.Keywords),
}
}
// NewDetectorDefaultConfig creates a new detector with the default config
func NewDetectorDefaultConfig() (*Detector, error) {
viper.SetConfigType("toml")
err := viper.ReadConfig(strings.NewReader(config.DefaultConfig))
if err != nil {
return nil, err
}
var vc config.ViperConfig
err = viper.Unmarshal(&vc)
if err != nil {
return nil, err
}
cfg, err := vc.Translate()
if err != nil {
return nil, err
}
return NewDetector(cfg), nil
}
func (d *Detector) AddGitleaksIgnore(gitleaksIgnorePath string) error {
log.Debug().Msg("found .gitleaksignore file")
file, err := os.Open(gitleaksIgnorePath)
if err != nil {
return err
}
// https://github.com/securego/gosec/issues/512
defer func() {
if err := file.Close(); err != nil {
log.Warn().Msgf("Error closing .gitleaksignore file: %s\n", err)
}
}()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
d.gitleaksIgnore[scanner.Text()] = true
}
return 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
}
// DetectBytes scans the given bytes and returns a list of findings
func (d *Detector) DetectBytes(content []byte) []report.Finding {
return d.DetectString(string(content))
}
// DetectString scans the given string and returns a list of findings
func (d *Detector) DetectString(content string) []report.Finding {
return d.Detect(Fragment{
Raw: content,
})
}
// detectRule scans the given fragment for the given rule and returns a list of findings
func (d *Detector) detectRule(fragment Fragment, rule config.Rule) []report.Finding {
var findings []report.Finding
// check if filepath or commit is allowed for this rule
if rule.Allowlist.CommitAllowed(fragment.CommitSHA) ||
rule.Allowlist.PathAllowed(fragment.FilePath) {
return findings
}
if rule.Path != nil && rule.Regex == nil {
// Path _only_ rule
if rule.Path.Match([]byte(fragment.FilePath)) {
finding := report.Finding{
Description: rule.Description,
File: fragment.FilePath,
SymlinkFile: fragment.SymlinkFile,
RuleID: rule.RuleID,
Match: fmt.Sprintf("file detected: %s", fragment.FilePath),
Tags: rule.Tags,
}
return append(findings, finding)
}
} else if rule.Path != nil {
// if path is set _and_ a regex is set, then we need to check both
// so if the path does not match, then we should return early and not
// consider the regex
if !rule.Path.Match([]byte(fragment.FilePath)) {
return findings
}
}
// if path only rule, skip content checks
if rule.Regex == nil {
return findings
}
// If flag configure and raw data size bigger then the flag
if d.MaxTargetMegaBytes > 0 {
rawLength := len(fragment.Raw) / 1000000
if rawLength > d.MaxTargetMegaBytes {
log.Debug().Msgf("skipping file: %s scan due to size: %d", fragment.FilePath, rawLength)
return findings
}
}
matchIndices := rule.Regex.FindAllStringIndex(fragment.Raw, -1)
for _, matchIndex := range matchIndices {
// extract secret from match
secret := strings.Trim(fragment.Raw[matchIndex[0]:matchIndex[1]], "\n")
// determine location of match. Note that the location
// in the finding will be the line/column numbers of the _match_
// not the _secret_, which will be different if the secretGroup
// value is set for this rule
loc := location(fragment, matchIndex)
if matchIndex[1] > loc.endLineIndex {
loc.endLineIndex = matchIndex[1]
}
finding := report.Finding{
Description: rule.Description,
File: fragment.FilePath,
SymlinkFile: fragment.SymlinkFile,
RuleID: rule.RuleID,
StartLine: loc.startLine,
EndLine: loc.endLine,
StartColumn: loc.startColumn,
EndColumn: loc.endColumn,
Secret: secret,
Match: secret,
Tags: rule.Tags,
Line: fragment.Raw[loc.startLineIndex:loc.endLineIndex],
}
if strings.Contains(fragment.Raw[loc.startLineIndex:loc.endLineIndex],
gitleaksAllowSignature) {
continue
}
// extract secret from secret group if set
if rule.SecretGroup != 0 {
groups := rule.Regex.FindStringSubmatch(secret)
if len(groups) <= rule.SecretGroup || len(groups) == 0 {
// Config validation should prevent this
continue
}
secret = groups[rule.SecretGroup]
finding.Secret = secret
}
// check if the regexTarget is defined in the allowlist "regexes" entry
allowlistTarget := finding.Secret
switch rule.Allowlist.RegexTarget {
case "match":
allowlistTarget = finding.Match
case "line":
allowlistTarget = finding.Line
}
globalAllowlistTarget := finding.Secret
switch d.Config.Allowlist.RegexTarget {
case "match":
globalAllowlistTarget = finding.Match
case "line":
globalAllowlistTarget = finding.Line
}
if rule.Allowlist.RegexAllowed(allowlistTarget) ||
d.Config.Allowlist.RegexAllowed(globalAllowlistTarget) {
continue
}
// check if the secret is in the list of stopwords
if rule.Allowlist.ContainsStopWord(finding.Secret) ||
d.Config.Allowlist.ContainsStopWord(finding.Secret) {
continue
}
// check entropy
entropy := shannonEntropy(finding.Secret)
finding.Entropy = float32(entropy)
if rule.Entropy != 0.0 {
if entropy <= rule.Entropy {
// entropy is too low, skip this finding
continue
}
// NOTE: this is a goofy hack to get around the fact there golang's regex engine
// does not support positive lookaheads. Ideally we would want to add a
// restriction on generic rules regex that requires the secret match group
// contains both numbers and alphabetical characters, not just alphabetical characters.
// What this bit of code does is check if the ruleid is prepended with "generic" and enforces the
// secret contains both digits and alphabetical characters.
// TODO: this should be replaced with stop words
if strings.HasPrefix(rule.RuleID, "generic") {
if !containsDigit(secret) {
continue
}
}
}
findings = append(findings, finding)
}
return findings
}
// GitScan accepts a *gitdiff.File channel which contents a git history generated from
// the output of `git log -p ...`. startGitScan will look at each file (patch) in the history
// and determine if the patch contains any findings.
func (d *Detector) DetectGit(source string, logOpts string, gitScanType GitScanType) ([]report.Finding, error) {
var (
gitdiffFiles <-chan *gitdiff.File
err error
)
switch gitScanType {
case DetectType:
gitdiffFiles, err = git.GitLog(source, logOpts)
if err != nil {
return d.findings, err
}
case ProtectType:
gitdiffFiles, err = git.GitDiff(source, false)
if err != nil {
return d.findings, err
}
case ProtectStagedType:
gitdiffFiles, err = git.GitDiff(source, true)
if err != nil {
return d.findings, err
}
}
s := semgroup.NewGroup(context.Background(), 4)
for gitdiffFile := range gitdiffFiles {
gitdiffFile := gitdiffFile
// skip binary files
if gitdiffFile.IsBinary || gitdiffFile.IsDelete {
continue
}
// Check if commit is allowed
commitSHA := ""
if gitdiffFile.PatchHeader != nil {
commitSHA = gitdiffFile.PatchHeader.SHA
if d.Config.Allowlist.CommitAllowed(gitdiffFile.PatchHeader.SHA) {
continue
}
}
d.addCommit(commitSHA)
s.Go(func() error {
for _, textFragment := range gitdiffFile.TextFragments {
if textFragment == nil {
return nil
}
fragment := Fragment{
Raw: textFragment.Raw(gitdiff.OpAdd),
CommitSHA: commitSHA,
FilePath: gitdiffFile.NewName,
}
for _, finding := range d.Detect(fragment) {
d.addFinding(augmentGitFinding(finding, textFragment, gitdiffFile))
}
}
return nil
})
}
if err := s.Wait(); err != nil {
return d.findings, err
}
log.Info().Msgf("%d commits scanned.", len(d.commitMap))
log.Debug().Msg("Note: this number might be smaller than expected due to commits with no additions")
if git.ErrEncountered {
return d.findings, fmt.Errorf("%s", "git error encountered, see logs")
}
return d.findings, nil
}
type scanTarget struct {
Path string
Symlink string
}
// DetectFiles accepts a path to a source directory or file and begins a scan of the
// file or directory.
func (d *Detector) DetectFiles(source string) ([]report.Finding, error) {
s := semgroup.NewGroup(context.Background(), 4)
paths := make(chan scanTarget)
s.Go(func() error {
defer close(paths)
return filepath.Walk(source,
func(path string, fInfo os.FileInfo, err error) error {
if err != nil {
return err
}
if fInfo.Name() == ".git" && fInfo.IsDir() {
return filepath.SkipDir
}
if fInfo.Size() == 0 {
return nil
}
if fInfo.Mode().IsRegular() {
paths <- scanTarget{
Path: path,
Symlink: "",
}
}
if fInfo.Mode().Type() == fs.ModeSymlink && d.FollowSymlinks {
realPath, err := filepath.EvalSymlinks(path)
if err != nil {
return err
}
realPathFileInfo, _ := os.Stat(realPath)
if realPathFileInfo.IsDir() {
log.Debug().Msgf("found symlinked directory: %s -> %s [skipping]", path, realPath)
return nil
}
paths <- scanTarget{
Path: realPath,
Symlink: path,
}
}
return nil
})
})
for pa := range paths {
p := pa
s.Go(func() error {
b, err := os.ReadFile(p.Path)
if err != nil {
return err
}
mimetype, err := filetype.Match(b)
if err != nil {
return err
}
if mimetype.MIME.Type == "application" {
return nil // skip binary files
}
fragment := Fragment{
Raw: string(b),
FilePath: p.Path,
}
if p.Symlink != "" {
fragment.SymlinkFile = p.Symlink
}
for _, finding := range d.Detect(fragment) {
// need to add 1 since line counting starts at 1
finding.EndLine++
finding.StartLine++
d.addFinding(finding)
}
return nil
})
}
if err := s.Wait(); err != nil {
return d.findings, err
}
return d.findings, nil
}
// DetectReader accepts an io.Reader and a buffer size for the reader in KB
func (d *Detector) DetectReader(r io.Reader, bufSize int) ([]report.Finding, error) {
reader := bufio.NewReader(r)
buf := make([]byte, 0, 1000*bufSize)
findings := []report.Finding{}
for {
n, err := reader.Read(buf[:cap(buf)])
buf = buf[:n]
if err != nil {
if err != io.EOF {
return findings, err
}
break
}
fragment := Fragment{
Raw: string(buf),
}
for _, finding := range d.Detect(fragment) {
findings = append(findings, finding)
if d.Verbose {
printFinding(finding, d.NoColor)
}
}
}
return findings, nil
}
// Detect scans the given fragment and returns a list of findings
func (d *Detector) Detect(fragment Fragment) []report.Finding {
var findings []report.Finding
// initiate fragment keywords
fragment.keywords = make(map[string]bool)
// check if filepath is allowed
if fragment.FilePath != "" && (d.Config.Allowlist.PathAllowed(fragment.FilePath) ||
fragment.FilePath == d.Config.Path || (d.baselinePath != "" && fragment.FilePath == d.baselinePath)) {
return findings
}
// add newline indices for location calculation in detectRule
fragment.newlineIndices = regexp.MustCompile("\n").FindAllStringIndex(fragment.Raw, -1)
// build keyword map for prefiltering rules
normalizedRaw := strings.ToLower(fragment.Raw)
matches := d.prefilter.FindAll(normalizedRaw)
for _, m := range matches {
fragment.keywords[normalizedRaw[m.Start():m.End()]] = true
}
for _, rule := range d.Config.Rules {
if len(rule.Keywords) == 0 {
// if not keywords are associated with the rule always scan the
// fragment using the rule
findings = append(findings, d.detectRule(fragment, rule)...)
continue
}
fragmentContainsKeyword := false
// check if keywords are in the fragment
for _, k := range rule.Keywords {
if _, ok := fragment.keywords[strings.ToLower(k)]; ok {
fragmentContainsKeyword = true
}
}
if fragmentContainsKeyword {
findings = append(findings, d.detectRule(fragment, rule)...)
}
}
return filter(findings, d.Redact)
}
// addFinding synchronously adds a finding to the findings slice
func (d *Detector) addFinding(finding report.Finding) {
if finding.Commit == "" {
finding.Fingerprint = fmt.Sprintf("%s:%s:%d", finding.File, finding.RuleID, finding.StartLine)
} else {
finding.Fingerprint = fmt.Sprintf("%s:%s:%s:%d", finding.Commit, finding.File, finding.RuleID, finding.StartLine)
}
// check if we should ignore this finding
if _, ok := d.gitleaksIgnore[finding.Fingerprint]; ok {
log.Debug().Msgf("ignoring finding with Fingerprint %s",
finding.Fingerprint)
return
}
if d.baseline != nil && !IsNew(finding, d.baseline) {
log.Debug().Msgf("baseline duplicate -- ignoring finding with Fingerprint %s", finding.Fingerprint)
return
}
d.findingMutex.Lock()
d.findings = append(d.findings, finding)
if d.Verbose {
printFinding(finding, d.NoColor)
}
d.findingMutex.Unlock()
}
// addCommit synchronously adds a commit to the commit slice
func (d *Detector) addCommit(commit string) {
d.commitMap[commit] = true
}

754
cli/detect/detect_test.go Normal file

@ -0,0 +1,754 @@
// 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 (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/Infisical/infisical-merge/config"
"github.com/Infisical/infisical-merge/report"
)
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
func TestDetect(t *testing.T) {
tests := []struct {
cfgName string
baselinePath string
fragment Fragment
// NOTE: for expected findings, all line numbers will be 0
// because line deltas are added _after_ the finding is created.
// I.e, if the finding is from a --no-git file, the line number will be
// increase by 1 in DetectFromFiles(). If the finding is from git,
// the line number will be increased by the patch delta.
expectedFindings []report.Finding
wantError error
}{
{
cfgName: "simple",
fragment: Fragment{
Raw: `awsToken := \"AKIALALEMEL33243OKIA\ // infisical-scan:ignore"`,
FilePath: "tmp.go",
},
expectedFindings: []report.Finding{},
},
{
cfgName: "simple",
fragment: Fragment{
Raw: `awsToken := \
\"AKIALALEMEL33243OKIA\ // infisical-scan:ignore"
`,
FilePath: "tmp.go",
},
expectedFindings: []report.Finding{},
},
{
cfgName: "simple",
fragment: Fragment{
Raw: `awsToken := \"AKIALALEMEL33243OKIA\"
// infisical-scan:ignore"
`,
FilePath: "tmp.go",
},
expectedFindings: []report.Finding{
{
Description: "AWS Access Key",
Secret: "AKIALALEMEL33243OKIA",
Match: "AKIALALEMEL33243OKIA",
File: "tmp.go",
Line: `awsToken := \"AKIALALEMEL33243OKIA\"`,
RuleID: "aws-access-key",
Tags: []string{"key", "AWS"},
StartLine: 0,
EndLine: 0,
StartColumn: 15,
EndColumn: 34,
Entropy: 3.1464393,
},
},
},
{
cfgName: "escaped_character_group",
fragment: Fragment{
Raw: `pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB`,
FilePath: "tmp.go",
},
expectedFindings: []report.Finding{
{
Description: "PyPI upload token",
Secret: "pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB",
Match: "pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB",
Line: `pypi-AgEIcHlwaS5vcmcAAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAA-AAAAAAAAAAB`,
File: "tmp.go",
RuleID: "pypi-upload-token",
Tags: []string{"key", "pypi"},
StartLine: 0,
EndLine: 0,
StartColumn: 1,
EndColumn: 86,
Entropy: 1.9606875,
},
},
},
{
cfgName: "simple",
fragment: Fragment{
Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
FilePath: "tmp.go",
},
expectedFindings: []report.Finding{
{
Description: "AWS Access Key",
Secret: "AKIALALEMEL33243OLIA",
Match: "AKIALALEMEL33243OLIA",
Line: `awsToken := \"AKIALALEMEL33243OLIA\"`,
File: "tmp.go",
RuleID: "aws-access-key",
Tags: []string{"key", "AWS"},
StartLine: 0,
EndLine: 0,
StartColumn: 15,
EndColumn: 34,
Entropy: 3.0841837,
},
},
},
{
cfgName: "simple",
fragment: Fragment{
Raw: `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
FilePath: "tmp.sh",
},
expectedFindings: []report.Finding{
{
Description: "Sidekiq Secret",
Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
Secret: "cafebabe:deadbeef",
Line: `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
File: "tmp.sh",
RuleID: "sidekiq-secret",
Tags: []string{},
Entropy: 2.6098502,
StartLine: 0,
EndLine: 0,
StartColumn: 8,
EndColumn: 60,
},
},
},
{
cfgName: "simple",
fragment: Fragment{
Raw: `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
FilePath: "tmp.sh",
},
expectedFindings: []report.Finding{
{
Description: "Sidekiq Secret",
Match: "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
Secret: "cafebabe:deadbeef",
File: "tmp.sh",
Line: `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
RuleID: "sidekiq-secret",
Tags: []string{},
Entropy: 2.6098502,
StartLine: 0,
EndLine: 0,
StartColumn: 21,
EndColumn: 74,
},
},
},
{
cfgName: "simple",
fragment: Fragment{
Raw: `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
FilePath: "tmp.sh",
},
expectedFindings: []report.Finding{
{
Description: "Sidekiq Sensitive URL",
Match: "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
Secret: "cafeb4b3:d3adb33f",
File: "tmp.sh",
Line: `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
RuleID: "sidekiq-sensitive-url",
Tags: []string{},
Entropy: 2.984234,
StartLine: 0,
EndLine: 0,
StartColumn: 8,
EndColumn: 58,
},
},
},
{
cfgName: "allow_aws_re",
fragment: Fragment{
Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
FilePath: "tmp.go",
},
expectedFindings: []report.Finding{},
},
{
cfgName: "allow_path",
fragment: Fragment{
Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
FilePath: "tmp.go",
},
expectedFindings: []report.Finding{},
},
{
cfgName: "allow_commit",
fragment: Fragment{
Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
FilePath: "tmp.go",
CommitSHA: "allowthiscommit",
},
expectedFindings: []report.Finding{},
},
{
cfgName: "entropy_group",
fragment: Fragment{
Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
FilePath: "tmp.go",
},
expectedFindings: []report.Finding{
{
Description: "Discord API key",
Match: "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
Line: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
File: "tmp.go",
RuleID: "discord-api-key",
Tags: []string{},
Entropy: 3.7906237,
StartLine: 0,
EndLine: 0,
StartColumn: 7,
EndColumn: 93,
},
},
},
{
cfgName: "generic_with_py_path",
fragment: Fragment{
Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
FilePath: "tmp.go",
},
expectedFindings: []report.Finding{},
},
{
cfgName: "generic_with_py_path",
fragment: Fragment{
Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
FilePath: "tmp.py",
},
expectedFindings: []report.Finding{
{
Description: "Generic API Key",
Match: "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
Secret: "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
Line: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
File: "tmp.py",
RuleID: "generic-api-key",
Tags: []string{},
Entropy: 3.7906237,
StartLine: 0,
EndLine: 0,
StartColumn: 22,
EndColumn: 93,
},
},
},
{
cfgName: "path_only",
fragment: Fragment{
Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
FilePath: "tmp.py",
},
expectedFindings: []report.Finding{
{
Description: "Python Files",
Match: "file detected: tmp.py",
File: "tmp.py",
RuleID: "python-files-only",
Tags: []string{},
},
},
},
{
cfgName: "bad_entropy_group",
fragment: Fragment{
Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
FilePath: "tmp.go",
},
expectedFindings: []report.Finding{},
wantError: fmt.Errorf("Discord API key invalid regex secret group 5, max regex secret group 3"),
},
{
cfgName: "simple",
fragment: Fragment{
Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
FilePath: filepath.Join(configPath, "simple.toml"),
},
expectedFindings: []report.Finding{},
},
{
cfgName: "allow_global_aws_re",
fragment: Fragment{
Raw: `awsToken := \"AKIALALEMEL33243OLIA\"`,
FilePath: "tmp.go",
},
expectedFindings: []report.Finding{},
},
{
cfgName: "generic_with_py_path",
fragment: Fragment{
Raw: `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
FilePath: "tmp.py",
},
expectedFindings: []report.Finding{},
},
{
cfgName: "path_only",
baselinePath: ".baseline.json",
fragment: Fragment{
Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
FilePath: ".baseline.json",
},
expectedFindings: []report.Finding{},
},
}
for _, tt := range tests {
viper.Reset()
viper.AddConfigPath(configPath)
viper.SetConfigName(tt.cfgName)
viper.SetConfigType("toml")
err := viper.ReadInConfig()
if err != nil {
t.Error(err)
}
var vc config.ViperConfig
err = viper.Unmarshal(&vc)
if err != nil {
t.Error(err)
}
cfg, err := vc.Translate()
cfg.Path = filepath.Join(configPath, tt.cfgName+".toml")
if tt.wantError != nil {
if err == nil {
t.Errorf("expected error")
}
assert.Equal(t, tt.wantError, err)
}
d := NewDetector(cfg)
d.baselinePath = tt.baselinePath
findings := d.Detect(tt.fragment)
assert.ElementsMatch(t, tt.expectedFindings, findings)
}
}
// TestFromGit tests the FromGit function
func TestFromGit(t *testing.T) {
tests := []struct {
cfgName string
source string
logOpts string
expectedFindings []report.Finding
}{
{
source: filepath.Join(repoBasePath, "small"),
cfgName: "simple",
expectedFindings: []report.Finding{
{
Description: "AWS Access Key",
StartLine: 20,
EndLine: 20,
StartColumn: 19,
EndColumn: 38,
Line: "\n awsToken := \"AKIALALEMEL33243OLIA\"",
Secret: "AKIALALEMEL33243OLIA",
Match: "AKIALALEMEL33243OLIA",
File: "main.go",
Date: "2021-11-02T23:37:53Z",
Commit: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587",
Author: "Zachary Rice",
Email: "zricer@protonmail.com",
Message: "Accidentally add a secret",
RuleID: "aws-access-key",
Tags: []string{"key", "AWS"},
Entropy: 3.0841837,
Fingerprint: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587:main.go:aws-access-key:20",
},
{
Description: "AWS Access Key",
StartLine: 9,
EndLine: 9,
StartColumn: 17,
EndColumn: 36,
Secret: "AKIALALEMEL33243OLIA",
Match: "AKIALALEMEL33243OLIA",
Line: "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
File: "foo/foo.go",
Date: "2021-11-02T23:48:06Z",
Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca",
Author: "Zach Rice",
Email: "zricer@protonmail.com",
Message: "adding foo package with secret",
RuleID: "aws-access-key",
Tags: []string{"key", "AWS"},
Entropy: 3.0841837,
Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
},
},
},
{
source: filepath.Join(repoBasePath, "small"),
logOpts: "--all foo...",
cfgName: "simple",
expectedFindings: []report.Finding{
{
Description: "AWS Access Key",
StartLine: 9,
EndLine: 9,
StartColumn: 17,
EndColumn: 36,
Secret: "AKIALALEMEL33243OLIA",
Line: "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
Match: "AKIALALEMEL33243OLIA",
Date: "2021-11-02T23:48:06Z",
File: "foo/foo.go",
Commit: "491504d5a31946ce75e22554cc34203d8e5ff3ca",
Author: "Zach Rice",
Email: "zricer@protonmail.com",
Message: "adding foo package with secret",
RuleID: "aws-access-key",
Tags: []string{"key", "AWS"},
Entropy: 3.0841837,
Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
},
},
},
}
err := moveDotGit("dotGit", ".git")
if err != nil {
t.Fatal(err)
}
defer func() {
if err := moveDotGit(".git", "dotGit"); err != nil {
t.Error(err)
}
}()
for _, tt := range tests {
viper.AddConfigPath(configPath)
viper.SetConfigName("simple")
viper.SetConfigType("toml")
err = viper.ReadInConfig()
if err != nil {
t.Error(err)
}
var vc config.ViperConfig
err = viper.Unmarshal(&vc)
if err != nil {
t.Error(err)
}
cfg, err := vc.Translate()
if err != nil {
t.Error(err)
}
detector := NewDetector(cfg)
findings, err := detector.DetectGit(tt.source, tt.logOpts, DetectType)
if err != nil {
t.Error(err)
}
for _, f := range findings {
f.Match = "" // remove lines cause copying and pasting them has some wack formatting
}
assert.ElementsMatch(t, tt.expectedFindings, findings)
}
}
func TestFromGitStaged(t *testing.T) {
tests := []struct {
cfgName string
source string
logOpts string
expectedFindings []report.Finding
}{
{
source: filepath.Join(repoBasePath, "staged"),
cfgName: "simple",
expectedFindings: []report.Finding{
{
Description: "AWS Access Key",
StartLine: 7,
EndLine: 7,
StartColumn: 18,
EndColumn: 37,
Line: "\n\taws_token2 := \"AKIALALEMEL33243OLIA\" // this one is not",
Match: "AKIALALEMEL33243OLIA",
Secret: "AKIALALEMEL33243OLIA",
File: "api/api.go",
SymlinkFile: "",
Commit: "",
Entropy: 3.0841837,
Author: "",
Email: "",
Date: "0001-01-01T00:00:00Z",
Message: "",
Tags: []string{
"key",
"AWS",
},
RuleID: "aws-access-key",
Fingerprint: "api/api.go:aws-access-key:7",
},
},
},
}
err := moveDotGit("dotGit", ".git")
if err != nil {
t.Fatal(err)
}
defer func() {
if err := moveDotGit(".git", "dotGit"); err != nil {
t.Error(err)
}
}()
for _, tt := range tests {
viper.AddConfigPath(configPath)
viper.SetConfigName("simple")
viper.SetConfigType("toml")
err = viper.ReadInConfig()
if err != nil {
t.Error(err)
}
var vc config.ViperConfig
err = viper.Unmarshal(&vc)
if err != nil {
t.Error(err)
}
cfg, err := vc.Translate()
if err != nil {
t.Error(err)
}
detector := NewDetector(cfg)
detector.AddGitleaksIgnore(filepath.Join(tt.source, ".gitleaksignore"))
findings, err := detector.DetectGit(tt.source, tt.logOpts, ProtectStagedType)
if err != nil {
t.Error(err)
}
for _, f := range findings {
f.Match = "" // remove lines cause copying and pasting them has some wack formatting
}
assert.ElementsMatch(t, tt.expectedFindings, findings)
}
}
// TestFromFiles tests the FromFiles function
func TestFromFiles(t *testing.T) {
tests := []struct {
cfgName string
source string
expectedFindings []report.Finding
}{
{
source: filepath.Join(repoBasePath, "nogit"),
cfgName: "simple",
expectedFindings: []report.Finding{
{
Description: "AWS Access Key",
StartLine: 20,
EndLine: 20,
StartColumn: 16,
EndColumn: 35,
Match: "AKIALALEMEL33243OLIA",
Secret: "AKIALALEMEL33243OLIA",
Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
File: "../testdata/repos/nogit/main.go",
SymlinkFile: "",
RuleID: "aws-access-key",
Tags: []string{"key", "AWS"},
Entropy: 3.0841837,
Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
},
},
},
{
source: filepath.Join(repoBasePath, "nogit", "main.go"),
cfgName: "simple",
expectedFindings: []report.Finding{
{
Description: "AWS Access Key",
StartLine: 20,
EndLine: 20,
StartColumn: 16,
EndColumn: 35,
Match: "AKIALALEMEL33243OLIA",
Secret: "AKIALALEMEL33243OLIA",
Line: "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
File: "../testdata/repos/nogit/main.go",
RuleID: "aws-access-key",
Tags: []string{"key", "AWS"},
Entropy: 3.0841837,
Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
},
},
},
}
for _, tt := range tests {
viper.AddConfigPath(configPath)
viper.SetConfigName("simple")
viper.SetConfigType("toml")
err := viper.ReadInConfig()
if err != nil {
t.Error(err)
}
var vc config.ViperConfig
err = viper.Unmarshal(&vc)
if err != nil {
t.Error(err)
}
cfg, _ := vc.Translate()
detector := NewDetector(cfg)
detector.FollowSymlinks = true
findings, err := detector.DetectFiles(tt.source)
if err != nil {
t.Error(err)
}
assert.ElementsMatch(t, tt.expectedFindings, findings)
}
}
func TestDetectWithSymlinks(t *testing.T) {
tests := []struct {
cfgName string
source string
expectedFindings []report.Finding
}{
{
source: filepath.Join(repoBasePath, "symlinks/file_symlink"),
cfgName: "simple",
expectedFindings: []report.Finding{
{
Description: "Asymmetric Private Key",
StartLine: 1,
EndLine: 1,
StartColumn: 1,
EndColumn: 35,
Match: "-----BEGIN OPENSSH PRIVATE KEY-----",
Secret: "-----BEGIN OPENSSH PRIVATE KEY-----",
Line: "-----BEGIN OPENSSH PRIVATE KEY-----",
File: "../testdata/repos/symlinks/source_file/id_ed25519",
SymlinkFile: "../testdata/repos/symlinks/file_symlink/symlinked_id_ed25519",
RuleID: "apkey",
Tags: []string{"key", "AsymmetricPrivateKey"},
Entropy: 3.587164,
Fingerprint: "../testdata/repos/symlinks/source_file/id_ed25519:apkey:1",
},
},
},
}
for _, tt := range tests {
viper.AddConfigPath(configPath)
viper.SetConfigName("simple")
viper.SetConfigType("toml")
err := viper.ReadInConfig()
if err != nil {
t.Error(err)
}
var vc config.ViperConfig
err = viper.Unmarshal(&vc)
if err != nil {
t.Error(err)
}
cfg, _ := vc.Translate()
detector := NewDetector(cfg)
detector.FollowSymlinks = true
findings, err := detector.DetectFiles(tt.source)
if err != nil {
t.Error(err)
}
assert.ElementsMatch(t, tt.expectedFindings, findings)
}
}
func moveDotGit(from, to string) error {
repoDirs, err := os.ReadDir("../testdata/repos")
if err != nil {
return err
}
for _, dir := range repoDirs {
if to == ".git" {
_, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), "dotGit"))
if os.IsNotExist(err) {
// dont want to delete the only copy of .git accidentally
continue
}
os.RemoveAll(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), ".git"))
}
if !dir.IsDir() {
continue
}
_, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from))
if os.IsNotExist(err) {
continue
}
err = os.Rename(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from),
fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), to))
if err != nil {
return err
}
}
return nil
}

143
cli/detect/git/git.go Normal file

@ -0,0 +1,143 @@
// 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 git
import (
"bufio"
"io"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/gitleaks/go-gitdiff/gitdiff"
"github.com/rs/zerolog/log"
)
var ErrEncountered bool
// GitLog returns a channel of gitdiff.File objects from the
// git log -p command for the given source.
func GitLog(source string, logOpts string) (<-chan *gitdiff.File, error) {
sourceClean := filepath.Clean(source)
var cmd *exec.Cmd
if logOpts != "" {
args := []string{"-C", sourceClean, "log", "-p", "-U0"}
args = append(args, strings.Split(logOpts, " ")...)
cmd = exec.Command("git", args...)
} else {
cmd = exec.Command("git", "-C", sourceClean, "log", "-p", "-U0",
"--full-history", "--all")
}
log.Debug().Msgf("executing: %s", cmd.String())
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
go listenForStdErr(stderr)
if err := cmd.Start(); err != nil {
return nil, err
}
// HACK: to avoid https://github.com/zricethezav/gitleaks/issues/722
time.Sleep(50 * time.Millisecond)
return gitdiff.Parse(cmd, stdout)
}
// GitDiff returns a channel of gitdiff.File objects from
// the git diff command for the given source.
func GitDiff(source string, staged bool) (<-chan *gitdiff.File, error) {
sourceClean := filepath.Clean(source)
var cmd *exec.Cmd
cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0", ".")
if staged {
cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0",
"--staged", ".")
}
log.Debug().Msgf("executing: %s", cmd.String())
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
go listenForStdErr(stderr)
if err := cmd.Start(); err != nil {
return nil, err
}
// HACK: to avoid https://github.com/zricethezav/gitleaks/issues/722
time.Sleep(50 * time.Millisecond)
return gitdiff.Parse(cmd, stdout)
}
// listenForStdErr listens for stderr output from git and prints it to stdout
// then exits with exit code 1
func listenForStdErr(stderr io.ReadCloser) {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
// if git throws one of the following errors:
//
// exhaustive rename detection was skipped due to too many files.
// you may want to set your diff.renameLimit variable to at least
// (some large number) and retry the command.
//
// inexact rename detection was skipped due to too many files.
// you may want to set your diff.renameLimit variable to at least
// (some large number) and retry the command.
//
// we skip exiting the program as git log -p/git diff will continue
// to send data to stdout and finish executing. This next bit of
// code prevents gitleaks from stopping mid scan if this error is
// encountered
if strings.Contains(scanner.Text(),
"exhaustive rename detection was skipped") ||
strings.Contains(scanner.Text(),
"inexact rename detection was skipped") ||
strings.Contains(scanner.Text(),
"you may want to set your diff.renameLimit") {
log.Warn().Msg(scanner.Text())
} else {
log.Error().Msgf("[git] %s", scanner.Text())
// asynchronously set this error flag to true so that we can
// capture a log message and exit with a non-zero exit code
// This value should get set before the `git` command exits so it's
// safe-ish, although I know I know, bad practice.
ErrEncountered = true
}
}
}

158
cli/detect/git/git_test.go Normal file

@ -0,0 +1,158 @@
package git_test
// TODO: commenting out this test for now because it's flaky. Alternatives to consider to get this working:
// -- use `git stash` instead of `restore()`
// const repoBasePath = "../../testdata/repos/"
// const expectPath = "../../testdata/expected/"
// func TestGitLog(t *testing.T) {
// tests := []struct {
// source string
// logOpts string
// expected string
// }{
// {
// source: filepath.Join(repoBasePath, "small"),
// expected: filepath.Join(expectPath, "git", "small.txt"),
// },
// {
// source: filepath.Join(repoBasePath, "small"),
// expected: filepath.Join(expectPath, "git", "small-branch-foo.txt"),
// logOpts: "--all foo...",
// },
// }
// err := moveDotGit("dotGit", ".git")
// if err != nil {
// t.Fatal(err)
// }
// defer func() {
// if err = moveDotGit(".git", "dotGit"); err != nil {
// t.Fatal(err)
// }
// }()
// for _, tt := range tests {
// files, err := git.GitLog(tt.source, tt.logOpts)
// if err != nil {
// t.Error(err)
// }
// var diffSb strings.Builder
// for f := range files {
// for _, tf := range f.TextFragments {
// diffSb.WriteString(tf.Raw(gitdiff.OpAdd))
// }
// }
// expectedBytes, err := os.ReadFile(tt.expected)
// if err != nil {
// t.Error(err)
// }
// expected := string(expectedBytes)
// if expected != diffSb.String() {
// // write string builder to .got file using os.Create
// err = os.WriteFile(strings.Replace(tt.expected, ".txt", ".got.txt", 1), []byte(diffSb.String()), 0644)
// if err != nil {
// t.Error(err)
// }
// t.Error("expected: ", expected, "got: ", diffSb.String())
// }
// }
// }
// func TestGitDiff(t *testing.T) {
// tests := []struct {
// source string
// expected string
// additions string
// target string
// }{
// {
// source: filepath.Join(repoBasePath, "small"),
// expected: "this line is added\nand another one",
// additions: "this line is added\nand another one",
// target: filepath.Join(repoBasePath, "small", "main.go"),
// },
// }
// err := moveDotGit("dotGit", ".git")
// if err != nil {
// t.Fatal(err)
// }
// defer func() {
// if err = moveDotGit(".git", "dotGit"); err != nil {
// t.Fatal(err)
// }
// }()
// for _, tt := range tests {
// noChanges, err := os.ReadFile(tt.target)
// if err != nil {
// t.Error(err)
// }
// err = os.WriteFile(tt.target, []byte(tt.additions), 0644)
// if err != nil {
// restore(tt.target, noChanges, t)
// t.Error(err)
// }
// files, err := git.GitDiff(tt.source, false)
// if err != nil {
// restore(tt.target, noChanges, t)
// t.Error(err)
// }
// for f := range files {
// sb := strings.Builder{}
// for _, tf := range f.TextFragments {
// sb.WriteString(tf.Raw(gitdiff.OpAdd))
// }
// if sb.String() != tt.expected {
// restore(tt.target, noChanges, t)
// t.Error("expected: ", tt.expected, "got: ", sb.String())
// }
// }
// restore(tt.target, noChanges, t)
// }
// }
// func restore(path string, data []byte, t *testing.T) {
// err := os.WriteFile(path, data, 0644)
// if err != nil {
// t.Fatal(err)
// }
// }
// func moveDotGit(from, to string) error {
// repoDirs, err := os.ReadDir("../../testdata/repos")
// if err != nil {
// return err
// }
// for _, dir := range repoDirs {
// if to == ".git" {
// _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), "dotGit"))
// if os.IsNotExist(err) {
// // dont want to delete the only copy of .git accidentally
// continue
// }
// os.RemoveAll(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), ".git"))
// }
// if !dir.IsDir() {
// continue
// }
// _, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from))
// if os.IsNotExist(err) {
// continue
// }
// err = os.Rename(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from),
// fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), to))
// if err != nil {
// return err
// }
// }
// return nil
// }

101
cli/detect/location.go Normal file

@ -0,0 +1,101 @@
// 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
// Location represents a location in a file
type Location struct {
startLine int
endLine int
startColumn int
endColumn int
startLineIndex int
endLineIndex int
}
func location(fragment Fragment, matchIndex []int) Location {
var (
prevNewLine int
location Location
lineSet bool
_lineNum int
)
start := matchIndex[0]
end := matchIndex[1]
// default startLineIndex to 0
location.startLineIndex = 0
// Fixes: https://github.com/zricethezav/gitleaks/issues/1037
// When a fragment does NOT have any newlines, a default "newline"
// will be counted to make the subsequent location calculation logic work
// for fragments will no newlines.
if len(fragment.newlineIndices) == 0 {
fragment.newlineIndices = [][]int{
{len(fragment.Raw), len(fragment.Raw) + 1},
}
}
for lineNum, pair := range fragment.newlineIndices {
_lineNum = lineNum
newLineByteIndex := pair[0]
if prevNewLine <= start && start < newLineByteIndex {
lineSet = true
location.startLine = lineNum
location.endLine = lineNum
location.startColumn = (start - prevNewLine) + 1 // +1 because counting starts at 1
location.startLineIndex = prevNewLine
location.endLineIndex = newLineByteIndex
}
if prevNewLine < end && end <= newLineByteIndex {
location.endLine = lineNum
location.endColumn = (end - prevNewLine)
location.endLineIndex = newLineByteIndex
}
prevNewLine = pair[0]
}
if !lineSet {
// if lines never get set then that means the secret is most likely
// on the last line of the diff output and the diff output does not have
// a newline
location.startColumn = (start - prevNewLine) + 1 // +1 because counting starts at 1
location.endColumn = (end - prevNewLine)
location.startLine = _lineNum + 1
location.endLine = _lineNum + 1
// search for new line byte index
i := 0
for end+i < len(fragment.Raw) {
if fragment.Raw[end+i] == '\n' {
break
}
if fragment.Raw[end+i] == '\r' {
break
}
i++
}
location.endLineIndex = end + i
}
return location
}

@ -0,0 +1,82 @@
// 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 (
"testing"
)
// TestGetLocation tests the getLocation function.
func TestGetLocation(t *testing.T) {
tests := []struct {
linePairs [][]int
start int
end int
wantLocation Location
}{
{
linePairs: [][]int{
{0, 39},
{40, 55},
{56, 57},
},
start: 35,
end: 38,
wantLocation: Location{
startLine: 1,
startColumn: 36,
endLine: 1,
endColumn: 38,
startLineIndex: 0,
endLineIndex: 40,
},
},
{
linePairs: [][]int{
{0, 39},
{40, 55},
{56, 57},
},
start: 40,
end: 44,
wantLocation: Location{
startLine: 2,
startColumn: 1,
endLine: 2,
endColumn: 4,
startLineIndex: 40,
endLineIndex: 56,
},
},
}
for _, test := range tests {
loc := location(Fragment{newlineIndices: test.linePairs}, []int{test.start, test.end})
if loc != test.wantLocation {
t.Errorf("\nstartLine %d\nstartColumn: %d\nendLine: %d\nendColumn: %d\nstartLineIndex: %d\nendlineIndex %d",
loc.startLine, loc.startColumn, loc.endLine, loc.endColumn, loc.startLineIndex, loc.endLineIndex)
t.Error("got", loc, "want", test.wantLocation)
}
}
}

211
cli/detect/utils.go Normal file

@ -0,0 +1,211 @@
// 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"
"math"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/Infisical/infisical-merge/report"
"github.com/gitleaks/go-gitdiff/gitdiff"
"github.com/rs/zerolog/log"
)
// augmentGitFinding updates the start and end line numbers of a finding to include the
// delta from the git diff
func augmentGitFinding(finding report.Finding, textFragment *gitdiff.TextFragment, f *gitdiff.File) report.Finding {
if !strings.HasPrefix(finding.Match, "file detected") {
finding.StartLine += int(textFragment.NewPosition)
finding.EndLine += int(textFragment.NewPosition)
}
if f.PatchHeader != nil {
finding.Commit = f.PatchHeader.SHA
finding.Message = f.PatchHeader.Message()
if f.PatchHeader.Author != nil {
finding.Author = f.PatchHeader.Author.Name
finding.Email = f.PatchHeader.Author.Email
}
finding.Date = f.PatchHeader.AuthorDate.UTC().Format(time.RFC3339)
}
return finding
}
// shannonEntropy calculates the entropy of data using the formula defined here:
// https://en.wiktionary.org/wiki/Shannon_entropy
// Another way to think about what this is doing is calculating the number of bits
// needed to on average encode the data. So, the higher the entropy, the more random the data, the
// more bits needed to encode that data.
func shannonEntropy(data string) (entropy float64) {
if data == "" {
return 0
}
charCounts := make(map[rune]int)
for _, char := range data {
charCounts[char]++
}
invLength := 1.0 / float64(len(data))
for _, count := range charCounts {
freq := float64(count) * invLength
entropy -= freq * math.Log2(freq)
}
return entropy
}
// filter will dedupe and redact findings
func filter(findings []report.Finding, redact bool) []report.Finding {
var retFindings []report.Finding
for _, f := range findings {
include := true
if strings.Contains(strings.ToLower(f.RuleID), "generic") {
for _, fPrime := range findings {
if f.StartLine == fPrime.StartLine &&
f.Commit == fPrime.Commit &&
f.RuleID != fPrime.RuleID &&
strings.Contains(fPrime.Secret, f.Secret) &&
!strings.Contains(strings.ToLower(fPrime.RuleID), "generic") {
genericMatch := strings.Replace(f.Match, f.Secret, "REDACTED", -1)
betterMatch := strings.Replace(fPrime.Match, fPrime.Secret, "REDACTED", -1)
log.Trace().Msgf("skipping %s finding (%s), %s rule takes precendence (%s)", f.RuleID, genericMatch, fPrime.RuleID, betterMatch)
include = false
break
}
}
}
if redact {
f.Redact()
}
if include {
retFindings = append(retFindings, f)
}
}
return retFindings
}
func printFinding(f report.Finding, noColor bool) {
// trim all whitespace and tabs
f.Line = strings.TrimSpace(f.Line)
f.Secret = strings.TrimSpace(f.Secret)
f.Match = strings.TrimSpace(f.Match)
isFileMatch := strings.HasPrefix(f.Match, "file detected:")
skipColor := noColor
finding := ""
var secret lipgloss.Style
// Matches from filenames do not have a |line| or |secret|
if !isFileMatch {
matchInLineIDX := strings.Index(f.Line, f.Match)
secretInMatchIdx := strings.Index(f.Match, f.Secret)
skipColor = false
if matchInLineIDX == -1 || noColor {
skipColor = true
matchInLineIDX = 0
}
start := f.Line[0:matchInLineIDX]
startMatchIdx := 0
if matchInLineIDX > 20 {
startMatchIdx = matchInLineIDX - 20
start = "..." + f.Line[startMatchIdx:matchInLineIDX]
}
matchBeginning := lipgloss.NewStyle().SetString(f.Match[0:secretInMatchIdx]).Foreground(lipgloss.Color("#f5d445"))
secret = lipgloss.NewStyle().SetString(f.Secret).
Bold(true).
Italic(true).
Foreground(lipgloss.Color("#f05c07"))
matchEnd := lipgloss.NewStyle().SetString(f.Match[secretInMatchIdx+len(f.Secret):]).Foreground(lipgloss.Color("#f5d445"))
lineEndIdx := matchInLineIDX + len(f.Match)
if len(f.Line)-1 <= lineEndIdx {
lineEndIdx = len(f.Line) - 1
}
lineEnd := f.Line[lineEndIdx:]
if len(f.Secret) > 100 {
secret = lipgloss.NewStyle().SetString(f.Secret[0:100] + "...").
Bold(true).
Italic(true).
Foreground(lipgloss.Color("#f05c07"))
}
if len(lineEnd) > 20 {
lineEnd = lineEnd[0:20] + "..."
}
finding = fmt.Sprintf("%s%s%s%s%s\n", strings.TrimPrefix(strings.TrimLeft(start, " "), "\n"), matchBeginning, secret, matchEnd, lineEnd)
}
if skipColor || isFileMatch {
fmt.Printf("%-12s %s\n", "Finding:", f.Match)
fmt.Printf("%-12s %s\n", "Secret:", f.Secret)
} else {
fmt.Printf("%-12s %s", "Finding:", finding)
fmt.Printf("%-12s %s\n", "Secret:", secret)
}
fmt.Printf("%-12s %s\n", "RuleID:", f.RuleID)
fmt.Printf("%-12s %f\n", "Entropy:", f.Entropy)
if f.File == "" {
fmt.Println("")
return
}
fmt.Printf("%-12s %s\n", "File:", f.File)
fmt.Printf("%-12s %d\n", "Line:", f.StartLine)
if f.Commit == "" {
fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint)
fmt.Println("")
return
}
fmt.Printf("%-12s %s\n", "Commit:", f.Commit)
fmt.Printf("%-12s %s\n", "Author:", f.Author)
fmt.Printf("%-12s %s\n", "Email:", f.Email)
fmt.Printf("%-12s %s\n", "Date:", f.Date)
fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint)
fmt.Println("")
}
func containsDigit(s string) bool {
for _, c := range s {
switch c {
case '1', '2', '3', '4', '5', '6', '7', '8', '9':
return true
}
}
return false
}

@ -4,12 +4,20 @@ go 1.19
require (
github.com/99designs/keyring v1.2.2
github.com/charmbracelet/lipgloss v0.5.0
github.com/fatih/semgroup v1.2.0
github.com/gitleaks/go-gitdiff v0.8.0
github.com/h2non/filetype v1.1.3
github.com/mattn/go-isatty v0.0.14
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
github.com/muesli/mango-cobra v1.2.0
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
github.com/rs/zerolog v1.26.1
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.8.0
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
golang.org/x/term v0.5.0
)
@ -19,22 +27,42 @@ require (
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denisbrodbeck/machineid v1.0.1 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-openapi/errors v0.20.2 // indirect
github.com/go-openapi/strfmt v0.21.3 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/muesli/mango v0.1.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
go.mongodb.org/mongo-driver v1.10.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
@ -43,6 +71,5 @@ require (
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/manifoldco/promptui v0.9.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/pflag v1.0.5 // indirect
)

@ -1,9 +1,59 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
@ -13,6 +63,13 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
@ -20,10 +77,30 @@ github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM=
github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/semgroup v1.2.0 h1:h/OLXwEM+3NNyAdZEpMiH1OzfplU09i2qXPVThGZvyg=
github.com/fatih/semgroup v1.2.0/go.mod h1:1KAD4iIYfXjE4U13B48VM4z9QUwV5Tt8O4rS879kgm8=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gitleaks/go-gitdiff v0.8.0 h1:7aExTZm+K/M/EQKOyYcub8rIAdWK6ONxPGuRzxmWW+0=
github.com/gitleaks/go-gitdiff v0.8.0/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8=
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o=
@ -32,35 +109,153 @@ github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPr
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
@ -72,75 +267,465 @@ github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbY
github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA=
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 h1:STjmj0uFfRryL9fzRA/OupNppeAID6QJYPMavTL7jtY=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a h1:Ey0XWvrg6u6hyIn1Kd/jCCmL+bMv9El81tvuGBbxZGg=
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.mongodb.org/mongo-driver v1.10.0 h1:UtV6N5k14upNp4LTduX0QCufG124fSu25Wz9tu94GLg=
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

@ -3,8 +3,15 @@ Copyright (c) 2023 Infisical Inc.
*/
package main
import "github.com/Infisical/infisical-merge/packages/cmd"
import (
"os"
"github.com/Infisical/infisical-merge/packages/cmd"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
cmd.Execute()
}

@ -2,10 +2,11 @@ package api
import (
"fmt"
"net/http"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const USER_AGENT = "cli"
@ -179,6 +180,19 @@ func CallLogin2V2(httpClient *resty.Client, request GetLoginTwoV2Request) (GetLo
SetBody(request).
Post(fmt.Sprintf("%v/v2/auth/login2", config.INFISICAL_URL))
cookies := response.Cookies()
// Find a cookie by name
cookieName := "jid"
var refreshToken *http.Cookie
for _, cookie := range cookies {
if cookie.Name == cookieName {
refreshToken = cookie
break
}
}
loginTwoV2Response.RefreshToken = refreshToken.Value
if err != nil {
return GetLoginTwoV2Response{}, fmt.Errorf("CallLogin2V2: Unable to complete api request [err=%s]", err)
}
@ -222,7 +236,7 @@ func CallIsAuthenticated(httpClient *resty.Client) bool {
}
if response.IsError() {
log.Debugln(fmt.Errorf("CallIsAuthenticated: Unsuccessful response: [response=%v]", response))
log.Debug().Msgf("CallIsAuthenticated: Unsuccessful response: [response=%v]", response)
return false
}
@ -247,3 +261,26 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib
return accessibleEnvironmentsResponse, nil
}
func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToken string) (GetNewAccessTokenWithRefreshTokenResponse, error) {
var newAccessToken GetNewAccessTokenWithRefreshTokenResponse
response, err := httpClient.
R().
SetResult(&newAccessToken).
SetHeader("User-Agent", USER_AGENT).
SetCookie(&http.Cookie{
Name: "jid",
Value: refreshToken,
}).
Post(fmt.Sprintf("%v/v1/auth/token", config.INFISICAL_URL))
if err != nil {
return GetNewAccessTokenWithRefreshTokenResponse{}, err
}
if response.IsError() {
return GetNewAccessTokenWithRefreshTokenResponse{}, fmt.Errorf("CallGetNewAccessTokenWithRefreshToken: Unsuccessful response: [response=%v]", response)
}
return newAccessToken, nil
}

@ -281,6 +281,7 @@ type GetLoginTwoV2Response struct {
ProtectedKey string `json:"protectedKey"`
ProtectedKeyIV string `json:"protectedKeyIV"`
ProtectedKeyTag string `json:"protectedKeyTag"`
RefreshToken string `json:"RefreshToken"`
}
type VerifyMfaTokenRequest struct {
@ -314,3 +315,7 @@ type VerifyMfaTokenErrorResponse struct {
Application string `json:"application"`
Extra []interface{} `json:"extra"`
}
type GetNewAccessTokenWithRefreshTokenResponse struct {
Token string `json:"token"`
}

@ -11,7 +11,8 @@ import (
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
log "github.com/sirupsen/logrus"
"github.com/posthog/posthog-go"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
@ -30,11 +31,6 @@ var exportCmd = &cobra.Command{
DisableFlagsInUseLine: true,
Example: "infisical export --env=prod --format=json > secrets.json",
Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
toggleDebug(cmd, args)
// util.RequireLogin()
// util.RequireLocalWorkspaceFile()
},
Run: func(cmd *cobra.Command, args []string) {
environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") {
@ -100,6 +96,8 @@ var exportCmd = &cobra.Command{
}
fmt.Print(output)
Telemetry.CaptureEvent("cli-command:export", posthog.NewProperties().Set("secretsCount", len(secrets)).Set("version", util.CLI_VERSION))
},
}
@ -175,8 +173,7 @@ func formatAsJson(envs []models.SingleEnvironmentVariable) string {
// Dump as a json array
json, err := json.Marshal(envs)
if err != nil {
log.Errorln("Unable to marshal environment variables to JSON")
log.Debugln(err)
log.Err(err).Msgf("Unable to marshal environment variables to JSON")
return ""
}
return string(json)

@ -12,14 +12,15 @@ import (
"github.com/Infisical/infisical-merge/packages/util"
"github.com/go-resty/resty/v2"
"github.com/manifoldco/promptui"
log "github.com/sirupsen/logrus"
"github.com/posthog/posthog-go"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
// runCmd represents the run command
var initCmd = &cobra.Command{
Use: "init",
Short: "Used to initialize your project with Infisical",
Short: "Used to connect your local project with Infisical project",
DisableFlagsInUseLine: true,
Example: "infisical init",
Args: cobra.ExactArgs(0),
@ -30,8 +31,8 @@ var initCmd = &cobra.Command{
if util.WorkspaceConfigFileExistsInCurrentPath() {
shouldOverride, err := shouldOverrideWorkspacePrompt()
if err != nil {
log.Errorln("Unable to parse your answer")
log.Debug(err)
log.Error().Msg("Unable to parse your answer")
log.Debug().Err(err)
return
}
@ -78,6 +79,9 @@ var initCmd = &cobra.Command{
if err != nil {
util.HandleError(err)
}
Telemetry.CaptureEvent("cli-command:init", posthog.NewProperties().Set("version", util.CLI_VERSION))
},
}

@ -1,27 +0,0 @@
package cmd
import (
"fmt"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var debugLogging bool
type PlainFormatter struct {
}
func (f *PlainFormatter) Format(entry *log.Entry) ([]byte, error) {
return []byte(fmt.Sprintf("%s\n", entry.Message)), nil
}
func toggleDebug(cmd *cobra.Command, args []string) {
if debugLogging {
log.Info("Debug logs enabled")
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.TextFormatter{})
} else {
plainFormatter := new(PlainFormatter)
log.SetFormatter(plainFormatter)
}
}

@ -22,7 +22,8 @@ import (
"github.com/fatih/color"
"github.com/go-resty/resty/v2"
"github.com/manifoldco/promptui"
log "github.com/sirupsen/logrus"
"github.com/posthog/posthog-go"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"golang.org/x/crypto/argon2"
)
@ -44,12 +45,11 @@ var loginCmd = &cobra.Command{
Use: "login",
Short: "Login into your Infisical account",
DisableFlagsInUseLine: true,
PreRun: toggleDebug,
Run: func(cmd *cobra.Command, args []string) {
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
// if the key can't be found or there is an error getting current credentials from key ring, allow them to override
if err != nil && (strings.Contains(err.Error(), "The specified item could not be found in the keyring") || strings.Contains(err.Error(), "unable to get key from Keyring") || strings.Contains(err.Error(), "GetUserCredsFromKeyRing")) {
log.Debug(err)
log.Debug().Err(err)
} else if err != nil {
util.HandleError(err)
}
@ -67,7 +67,7 @@ var loginCmd = &cobra.Command{
//override domain
domainQuery := true
if config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL {
if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL {
overrideDomain, err := DomainOverridePrompt()
if err != nil {
util.HandleError(err)
@ -97,8 +97,8 @@ var loginCmd = &cobra.Command{
loginOneResponse, loginTwoResponse, err := getFreshUserCredentials(email, password)
if err != nil {
log.Infoln("Unable to authenticate with the provided credentials, please try again")
log.Debugln(err)
log.Warn().Msg("Unable to authenticate with the provided credentials, please ensure your email and password are correct")
log.Debug().Err(err)
return
}
@ -152,7 +152,7 @@ var loginCmd = &cobra.Command{
var decryptedPrivateKey []byte
if loginTwoResponse.EncryptionVersion == 1 {
log.Debug("Login version 1")
log.Debug().Msg("Login version 1")
encryptedPrivateKey, _ := base64.StdEncoding.DecodeString(loginTwoResponse.EncryptedPrivateKey)
tag, err := base64.StdEncoding.DecodeString(loginTwoResponse.Tag)
if err != nil {
@ -175,7 +175,7 @@ var loginCmd = &cobra.Command{
decryptedPrivateKey = computedDecryptedPrivateKey
} else if loginTwoResponse.EncryptionVersion == 2 {
log.Debug("Login version 2")
log.Debug().Msg("Login version 2")
protectedKey, err := base64.StdEncoding.DecodeString(loginTwoResponse.ProtectedKey)
if err != nil {
util.HandleError(err)
@ -239,22 +239,23 @@ var loginCmd = &cobra.Command{
}
if string(decryptedPrivateKey) == "" || email == "" || loginTwoResponse.Token == "" {
log.Debugf("[decryptedPrivateKey=%s] [email=%s] [loginTwoResponse.Token=%s]", string(decryptedPrivateKey), email, loginTwoResponse.Token)
log.Debug().Msgf("[decryptedPrivateKey=%s] [email=%s] [loginTwoResponse.Token=%s]", string(decryptedPrivateKey), email, loginTwoResponse.Token)
util.PrintErrorMessageAndExit("We were unable to fetch required details to complete your login. Run with -d to see more info")
}
userCredentialsToBeStored := &models.UserCredentials{
Email: email,
PrivateKey: string(decryptedPrivateKey),
JTWToken: loginTwoResponse.Token,
Email: email,
PrivateKey: string(decryptedPrivateKey),
JTWToken: loginTwoResponse.Token,
RefreshToken: loginTwoResponse.RefreshToken,
}
err = util.StoreUserCredsInKeyRing(userCredentialsToBeStored)
if err != nil {
currentVault, _ := util.GetCurrentVaultBackend()
log.Errorf("Unable to store your credentials in system vault [%s]. Rerun with flag -d to see full logs", currentVault)
log.Errorln("To trouble shoot further, read https://infisical.com/docs/cli/faq")
log.Debugln(err)
log.Error().Msgf("Unable to store your credentials in system vault [%s]. Rerun with flag -d to see full logs", currentVault)
log.Error().Msgf("\nTo trouble shoot further, read https://infisical.com/docs/cli/faq")
log.Debug().Err(err)
return
}
@ -276,6 +277,7 @@ var loginCmd = &cobra.Command{
plainBold.Println("\nQuick links")
fmt.Println("- Learn to inject secrets into your application at https://infisical.com/docs/cli/usage")
fmt.Println("- Stuck? Join our slack for quick support https://infisical.com/slack")
Telemetry.CaptureEvent("cli-command:login", posthog.NewProperties().Set("infisical-backend", config.INFISICAL_URL).Set("version", util.CLI_VERSION))
},
}
@ -397,7 +399,7 @@ func askForLoginCredentials() (email string, password string, err error) {
}
func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2Response, *api.GetLoginTwoV2Response, error) {
log.Debugln("getFreshUserCredentials:", "email", email, "password", password)
log.Debug().Msg(fmt.Sprint("getFreshUserCredentials: ", "email", email, "password: ", password))
httpClient := resty.New()
httpClient.SetRetryCount(5)
@ -413,7 +415,7 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R
})
if err != nil {
util.HandleError(err)
return nil, nil, err
}
// **** Login 2

@ -0,0 +1,20 @@
# MANAGED BY INFISICAL CLI (Do not modify): START
infisicalScanEnabled=$(git config --bool hooks.infisical-scan)
if [ "$infisicalScanEnabled" != "false" ]; then
infisical scan git-changes -v --staged
exitCode=$?
if [ $exitCode -eq 1 ]; then
echo "Commit blocked: Infisical scan has uncovered secrets in your git commit"
echo "To disable the Infisical scan precommit hook run the following command:"
echo ""
echo " git config hooks.infisical-scan false"
echo ""
exit 1
fi
else
echo 'Warning: infisical scan precommit disabled'
fi
# MANAGED BY INFISICAL CLI (Do not modify): END

@ -0,0 +1,20 @@
#!/bin/sh
# MANAGED BY INFISICAL CLI (Do not modify): START
infisicalScanEnabled=$(git config --bool hooks.infisical-scan)
if [ "$infisicalScanEnabled" != "false" ]; then
infisical scan git-changes -v --staged
exitCode=$?
if [ $exitCode -eq 1 ]; then
echo "Commit blocked: Infisical scan has uncovered secrets in your git commit"
echo "To disable the Infisical scan precommit hook run the following command:"
echo ""
echo " git config hooks.infisical-scan false"
echo ""
exit 1
fi
else
echo 'Warning: infisical scan precommit disabled'
fi
# MANAGED BY INFISICAL CLI (Do not modify): END

@ -7,18 +7,16 @@ import (
"os"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/posthog/posthog-go"
"github.com/spf13/cobra"
)
var resetCmd = &cobra.Command{
Use: "reset",
Short: "Used delete all Infisical related data on your machine",
Short: "Used to delete all Infisical related data on your machine",
DisableFlagsInUseLine: true,
Example: "infisical reset",
Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
toggleDebug(cmd, args)
},
Run: func(cmd *cobra.Command, args []string) {
// delete config
_, pathToDir, err := util.GetFullConfigFilePath()
@ -40,6 +38,7 @@ var resetCmd = &cobra.Command{
util.DeleteBackupSecrets()
util.PrintSuccessMessage("Reset successful")
Telemetry.CaptureEvent("cli-command:reset", posthog.NewProperties().Set("version", util.CLI_VERSION))
},
}

@ -5,13 +5,19 @@ package cmd
import (
"os"
"strings"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/telemetry"
"github.com/Infisical/infisical-merge/packages/util"
)
var Telemetry *telemetry.Telemetry
var rootCmd = &cobra.Command{
Use: "infisical",
Short: "Infisical CLI is used to inject environment variables into any process",
@ -30,11 +36,14 @@ func Execute() {
}
func init() {
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
rootCmd.PersistentFlags().BoolVarP(&debugLogging, "debug", "d", false, "Enable verbose logging")
cobra.OnInitialize(initLog)
rootCmd.PersistentFlags().StringP("log-level", "l", "info", "log level (trace, debug, info, warn, error, fatal)")
rootCmd.PersistentFlags().Bool("telemetry", true, "Infisical collects non-sensitive telemetry data to enhance features and improve user experience. Participation is voluntary")
rootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", util.INFISICAL_DEFAULT_API_URL, "Point the CLI to your own backend [can also set via environment variable name: INFISICAL_API_URL]")
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
util.CheckForUpdate()
if !util.IsRunningInDocker() {
util.CheckForUpdate()
}
}
// if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment
@ -45,4 +54,30 @@ func init() {
}
}
isTelemetryOn, _ := rootCmd.PersistentFlags().GetBool("telemetry")
Telemetry = telemetry.NewTelemetry(isTelemetryOn)
}
func initLog() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
ll, err := rootCmd.Flags().GetString("log-level")
if err != nil {
log.Fatal().Msg(err.Error())
}
switch strings.ToLower(ll) {
case "trace":
zerolog.SetGlobalLevel(zerolog.TraceLevel)
case "debug":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "info":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "warn":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "err", "error":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
case "fatal":
zerolog.SetGlobalLevel(zerolog.FatalLevel)
default:
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
}

@ -15,7 +15,8 @@ import (
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/fatih/color"
log "github.com/sirupsen/logrus"
"github.com/posthog/posthog-go"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
@ -28,7 +29,6 @@ var runCmd = &cobra.Command{
Use: "run [any infisical run command flags] -- [your application start command]",
Short: "Used to inject environments variables into your application process",
DisableFlagsInUseLine: true,
PreRun: toggleDebug,
Args: func(cmd *cobra.Command, args []string) error {
// Check if the --command flag has been set
commandFlagSet := cmd.Flags().Changed("command")
@ -124,7 +124,9 @@ var runCmd = &cobra.Command{
env = append(env, s)
}
log.Debugf("injecting the following environment variables into shell: %v", env)
log.Debug().Msgf("injecting the following environment variables into shell: %v", env)
Telemetry.CaptureEvent("cli-command:run", posthog.NewProperties().Set("secretsCount", len(secrets)).Set("environment", environmentName).Set("isUsingServiceToken", infisicalToken != "").Set("single-command", strings.Join(args, " ")).Set("multi-command", cmd.Flag("command").Value.String()).Set("version", util.CLI_VERSION))
if cmd.Flags().Changed("command") {
command := cmd.Flag("command").Value.String()
@ -217,7 +219,7 @@ func executeMultipleCommandWithEnvs(fullCommand string, secretsCount int, env []
cmd.Env = env
color.Green("Injecting %v Infisical secrets into your application process", secretsCount)
log.Debugf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand)
log.Debug().Msgf("executing command: %s %s %s \n", shell[0], shell[1], fullCommand)
return execCmd(cmd)
}

586
cli/packages/cmd/scan.go Normal file

@ -0,0 +1,586 @@
// 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 cmd
import (
_ "embed"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/Infisical/infisical-merge/config"
"github.com/Infisical/infisical-merge/detect"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/Infisical/infisical-merge/report"
"github.com/manifoldco/promptui"
"github.com/posthog/posthog-go"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const configDescription = `config file path
order of precedence:
1. --config flag
2. env var INFISICAL_SCAN_CONFIG
3. (--source/-s)/.infisical-scan.toml
If none of the three options are used, then Infisical will use the default scan config`
//go:embed pre-commit-script/pre-commit.sh
var preCommitTemplate []byte
//go:embed pre-commit-script/pre-commit-without-bang.sh
var preCommitTemplateAppend []byte
const (
defaultHooksPath = ".git/hooks/"
preCommitFile = "pre-commit"
)
func init() {
// scan flag for only scan command
scanCmd.Flags().String("log-opts", "", "git log options")
scanCmd.Flags().Bool("no-git", false, "treat git repo as a regular directory and scan those files, --log-opts has no effect on the scan when --no-git is set")
scanCmd.Flags().Bool("pipe", false, "scan input from stdin, ex: `cat some_file | infisical scan --pipe`")
scanCmd.Flags().Bool("follow-symlinks", false, "scan files that are symlinks to other files")
// global scan flags
scanCmd.PersistentFlags().StringP("config", "c", "", configDescription)
scanCmd.PersistentFlags().Int("exit-code", 1, "exit code when leaks have been encountered")
scanCmd.PersistentFlags().StringP("source", "s", ".", "path to source")
scanCmd.PersistentFlags().StringP("report-path", "r", "", "report file")
scanCmd.PersistentFlags().StringP("report-format", "f", "json", "output format (json, csv, sarif)")
scanCmd.PersistentFlags().StringP("baseline-path", "b", "", "path to baseline with issues that can be ignored")
scanCmd.PersistentFlags().BoolP("verbose", "v", false, "show verbose output from scan (which file, where in the file, what secret)")
scanCmd.PersistentFlags().BoolP("no-color", "", false, "turn off color for verbose output")
scanCmd.PersistentFlags().Int("max-target-megabytes", 0, "files larger than this will be skipped")
scanCmd.PersistentFlags().Bool("redact", false, "redact secrets from logs and stdout")
// scan git changes command flags
scanGitChangesCmd.Flags().Bool("staged", false, "detect secrets in a --staged state")
scanGitChangesCmd.Flags().String("log-opts", "", "git log options")
// find config source
err := viper.BindPFlag("config", scanCmd.PersistentFlags().Lookup("config"))
if err != nil {
log.Fatal().Msgf("err binding config %s", err.Error())
}
// add flags to main
scanCmd.AddCommand(scanGitChangesCmd)
rootCmd.AddCommand(scanCmd)
installCmd.Flags().Bool("pre-commit-hook", false, "installs pre commit hook for Git repository")
scanCmd.AddCommand(installCmd)
}
func initScanConfig(cmd *cobra.Command) {
cfgPath, err := cmd.Flags().GetString("config")
if err != nil {
log.Fatal().Msg(err.Error())
}
if cfgPath != "" {
viper.SetConfigFile(cfgPath)
log.Debug().Msgf("using scan config %s from `--config`", cfgPath)
} else if os.Getenv(config.DefaultScanConfigEnvName) != "" {
envPath := os.Getenv(config.DefaultScanConfigEnvName)
viper.SetConfigFile(envPath)
log.Debug().Msgf("using scan config from %s env var: %s", config.DefaultScanConfigEnvName, envPath)
} else {
source, err := cmd.Flags().GetString("source")
if err != nil {
log.Fatal().Msg(err.Error())
}
fileInfo, err := os.Stat(source)
if err != nil {
log.Fatal().Msg(err.Error())
}
if !fileInfo.IsDir() {
log.Debug().Msgf("unable to load scan config from %s since --source=%s is a file, using default config",
filepath.Join(source, config.DefaultScanConfigFileName), source)
viper.SetConfigType("toml")
if err = viper.ReadConfig(strings.NewReader(config.DefaultConfig)); err != nil {
log.Fatal().Msgf("err reading toml %s", err.Error())
}
return
}
if _, err := os.Stat(filepath.Join(source, config.DefaultScanConfigFileName)); os.IsNotExist(err) {
log.Debug().Msgf("no scan config found in path %s, using default scan config", filepath.Join(source, config.DefaultScanConfigFileName))
viper.SetConfigType("toml")
if err = viper.ReadConfig(strings.NewReader(config.DefaultConfig)); err != nil {
log.Fatal().Msgf("err reading default scan config toml %s", err.Error())
}
return
} else {
log.Debug().Msgf("using existing scan config %s from `(--source)/%s`", filepath.Join(source, config.DefaultScanConfigFileName), config.DefaultScanConfigFileName)
}
viper.AddConfigPath(source)
viper.SetConfigName(config.DefaultScanConfigFileName)
viper.SetConfigType("toml")
}
if err := viper.ReadInConfig(); err != nil {
log.Fatal().Msgf("unable to load scan config, err: %s", err)
}
}
var installCmd = &cobra.Command{
Use: "install",
Short: "Install scanning scripts and tools. Use --help flag to see all options",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
installPrecommit := cmd.Flags().Changed("pre-commit-hook")
if installPrecommit {
hooksPath, err := getHooksPath()
if err != nil {
fmt.Printf("Error: %s\n", err)
return
}
if hooksPath != ".git/hooks" {
defaultHookOverride, err := overrideDefaultHooksPath(hooksPath)
if err != nil {
fmt.Printf("Error: %s\n", err)
}
if defaultHookOverride {
ConfigureGitHooksPath()
log.Info().Msgf("To switch back previous githooks manager run: git config core.hooksPath %s\n", hooksPath)
return
} else {
log.Warn().Msgf("To automatically configure this hook, you need to switch the path of the Hooks. Alternatively, you can manually configure this hook by setting your pre-commit script to run command [infisical scan git-changes -v --staged].\n")
return
}
}
err = createOrUpdatePreCommitFile(hooksPath)
if err != nil {
fmt.Printf("Error: %s\n", err)
return
}
log.Info().Msgf("Pre-commit hook successfully added. Infisical scan should now run on each commit you make\n")
Telemetry.CaptureEvent("cli-command:install --pre-commit-hook", posthog.NewProperties().Set("version", util.CLI_VERSION))
return
}
}}
var scanCmd = &cobra.Command{
Use: "scan",
Short: "Scan for leaked secrets in git history, directories, and files",
Run: func(cmd *cobra.Command, args []string) {
initScanConfig(cmd)
var (
vc config.ViperConfig
findings []report.Finding
err error
)
// Load config
if err = viper.Unmarshal(&vc); err != nil {
log.Fatal().Err(err).Msg("Failed to load config")
}
cfg, err := vc.Translate()
if err != nil {
log.Fatal().Err(err).Msg("Failed to load config")
}
cfg.Path, _ = cmd.Flags().GetString("config")
// start timer
start := time.Now()
// Setup detector
detector := detect.NewDetector(cfg)
detector.Config.Path, err = cmd.Flags().GetString("config")
if err != nil {
log.Fatal().Err(err).Msg("")
}
source, err := cmd.Flags().GetString("source")
if err != nil {
log.Fatal().Err(err).Msg("")
}
// if config path is not set, then use the {source}/.infisical-scan.toml path.
// note that there may not be a `{source}/.infisical-scan.toml` file, this is ok.
if detector.Config.Path == "" {
detector.Config.Path = filepath.Join(source, config.DefaultScanConfigFileName)
}
// set verbose flag
if detector.Verbose, err = cmd.Flags().GetBool("verbose"); err != nil {
log.Fatal().Err(err).Msg("")
}
// set redact flag
if detector.Redact, err = cmd.Flags().GetBool("redact"); err != nil {
log.Fatal().Err(err).Msg("")
}
if detector.MaxTargetMegaBytes, err = cmd.Flags().GetInt("max-target-megabytes"); err != nil {
log.Fatal().Err(err).Msg("")
}
// set color flag
if detector.NoColor, err = cmd.Flags().GetBool("no-color"); err != nil {
log.Fatal().Err(err).Msg("")
}
if fileExists(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)) {
if err = detector.AddGitleaksIgnore(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)); err != nil {
log.Fatal().Err(err).Msg("could not call AddInfisicalIgnore")
}
}
// ignore findings from the baseline (an existing report in json format generated earlier)
baselinePath, _ := cmd.Flags().GetString("baseline-path")
if baselinePath != "" {
err = detector.AddBaseline(baselinePath, source)
if err != nil {
log.Error().Msgf("Could not load baseline. The path must point to report generated by `infisical scan` using the default format: %s", err)
}
}
// set follow symlinks flag
if detector.FollowSymlinks, err = cmd.Flags().GetBool("follow-symlinks"); err != nil {
log.Fatal().Err(err).Msg("")
}
// set exit code
exitCode, err := cmd.Flags().GetInt("exit-code")
if err != nil {
log.Fatal().Err(err).Msg("could not get exit code")
}
// determine what type of scan:
// - git: scan the history of the repo
// - no-git: scan files by treating the repo as a plain directory
noGit, err := cmd.Flags().GetBool("no-git")
if err != nil {
log.Fatal().Err(err).Msg("could not call GetBool() for no-git")
}
fromPipe, err := cmd.Flags().GetBool("pipe")
if err != nil {
log.Fatal().Err(err)
}
log.Info().Msgf("scanning for exposed secrets...")
// start the detector scan
if noGit {
findings, err = detector.DetectFiles(source)
if err != nil {
// don't exit on error, just log it
log.Error().Err(err).Msg("")
}
} else if fromPipe {
findings, err = detector.DetectReader(os.Stdin, 10)
if err != nil {
// log fatal to exit, no need to continue since a report
// will not be generated when scanning from a pipe...for now
log.Fatal().Err(err).Msg("")
}
} else {
var logOpts string
logOpts, err = cmd.Flags().GetString("log-opts")
if err != nil {
log.Fatal().Err(err).Msg("")
}
findings, err = detector.DetectGit(source, logOpts, detect.DetectType)
if err != nil {
// don't exit on error, just log it
log.Error().Err(err).Msg("")
}
}
// log info about the scan
if err == nil {
log.Info().Msgf("scan completed in %s", FormatDuration(time.Since(start)))
if len(findings) != 0 {
log.Warn().Msgf("leaks found: %d", len(findings))
} else {
log.Info().Msg("no leaks found")
}
} else {
log.Warn().Msgf("partial scan completed in %s", FormatDuration(time.Since(start)))
if len(findings) != 0 {
log.Warn().Msgf("%d leaks found in partial scan", len(findings))
} else {
log.Warn().Msg("no leaks found in partial scan")
}
}
Telemetry.CaptureEvent("cli-command:scan", posthog.NewProperties().Set("risks", len(findings)).Set("version", util.CLI_VERSION))
// write report if desired
reportPath, _ := cmd.Flags().GetString("report-path")
ext, _ := cmd.Flags().GetString("report-format")
if reportPath != "" {
if err := report.Write(findings, cfg, ext, reportPath); err != nil {
log.Fatal().Err(err).Msg("could not write")
}
}
if err != nil {
os.Exit(1)
}
if len(findings) != 0 {
os.Exit(exitCode)
}
},
}
var scanGitChangesCmd = &cobra.Command{
Use: "git-changes",
Short: "Scan for secrets in uncommitted changes in a git repo",
Run: func(cmd *cobra.Command, args []string) {
initScanConfig(cmd)
var vc config.ViperConfig
if err := viper.Unmarshal(&vc); err != nil {
log.Fatal().Err(err).Msg("Failed to load config")
}
cfg, err := vc.Translate()
if err != nil {
log.Fatal().Err(err).Msg("Failed to load config")
}
cfg.Path, _ = cmd.Flags().GetString("config")
exitCode, _ := cmd.Flags().GetInt("exit-code")
staged, _ := cmd.Flags().GetBool("staged")
start := time.Now()
// Setup detector
detector := detect.NewDetector(cfg)
detector.Config.Path, err = cmd.Flags().GetString("config")
if err != nil {
log.Fatal().Err(err).Msg("")
}
source, err := cmd.Flags().GetString("source")
if err != nil {
log.Fatal().Err(err).Msg("")
}
// if config path is not set, then use the {source}/.infisical-scan.toml path.
// note that there may not be a `{source}/.infisical-scan.toml` file, this is ok.
if detector.Config.Path == "" {
detector.Config.Path = filepath.Join(source, config.DefaultScanConfigFileName)
}
// set verbose flag
if detector.Verbose, err = cmd.Flags().GetBool("verbose"); err != nil {
log.Fatal().Err(err).Msg("")
}
// set redact flag
if detector.Redact, err = cmd.Flags().GetBool("redact"); err != nil {
log.Fatal().Err(err).Msg("")
}
if detector.MaxTargetMegaBytes, err = cmd.Flags().GetInt("max-target-megabytes"); err != nil {
log.Fatal().Err(err).Msg("")
}
// set color flag
if detector.NoColor, err = cmd.Flags().GetBool("no-color"); err != nil {
log.Fatal().Err(err).Msg("")
}
if fileExists(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)) {
if err = detector.AddGitleaksIgnore(filepath.Join(source, config.DefaultInfisicalIgnoreFineName)); err != nil {
log.Fatal().Err(err).Msg("could not call AddInfisicalIgnore")
}
}
// get log options for git scan
logOpts, err := cmd.Flags().GetString("log-opts")
if err != nil {
log.Fatal().Err(err).Msg("")
}
log.Info().Msgf("scanning for exposed secrets...")
// start git scan
var findings []report.Finding
if staged {
findings, err = detector.DetectGit(source, logOpts, detect.ProtectStagedType)
} else {
findings, err = detector.DetectGit(source, logOpts, detect.ProtectType)
}
if err != nil {
// don't exit on error, just log it
log.Error().Err(err).Msg("")
}
// log info about the scan
log.Info().Msgf("scan completed in %s", FormatDuration(time.Since(start)))
if len(findings) != 0 {
log.Warn().Msgf("leaks found: %d", len(findings))
} else {
log.Info().Msg("no leaks found")
}
Telemetry.CaptureEvent("cli-command:scan git-changes", posthog.NewProperties().Set("risks", len(findings)).Set("version", util.CLI_VERSION))
reportPath, _ := cmd.Flags().GetString("report-path")
ext, _ := cmd.Flags().GetString("report-format")
if reportPath != "" {
if err = report.Write(findings, cfg, ext, reportPath); err != nil {
log.Fatal().Err(err).Msg("")
}
}
if len(findings) != 0 {
os.Exit(exitCode)
}
},
}
func fileExists(fileName string) bool {
// check for a .infisicalignore file
info, err := os.Stat(fileName)
if err != nil && !os.IsNotExist(err) {
return false
}
if info != nil && err == nil {
if !info.IsDir() {
return true
}
}
return false
}
func FormatDuration(d time.Duration) string {
scale := 100 * time.Second
// look for the max scale that is smaller than d
for scale > d {
scale = scale / 10
}
return d.Round(scale / 100).String()
}
func overrideDefaultHooksPath(managedHook string) (bool, error) {
YES := "Yes"
NO := "No"
options := []string{YES, NO}
optionsPrompt := promptui.Select{
Label: fmt.Sprintf("Your hooks path is set to [%s] but needs to be [.git/hooks] for automatic configuration. Would you like to switch? ", managedHook),
Items: options,
Size: 2,
}
_, selectedOption, err := optionsPrompt.Run()
if err != nil {
return false, err
}
return selectedOption == YES, err
}
func ConfigureGitHooksPath() {
cmd := exec.Command("git", "config", "core.hooksPath", ".git/hooks")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal().Msgf("Failed to configure git hooks path: %v", err)
}
}
// GetGitRoot returns the root directory of the current Git repository.
func GetGitRoot() (string, error) {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get git root directory: %w", err)
}
gitRoot := strings.TrimSpace(string(output)) // Remove any trailing newline
return gitRoot, nil
}
func getHooksPath() (string, error) {
out, err := exec.Command("git", "config", "core.hooksPath").Output()
if err != nil {
if len(out) == 0 {
out = []byte(".git/hooks") // set the default hook
} else {
log.Error().Msgf("Failed to get Git hooks path: %s\nOutput: %s\n", err, out)
}
}
hooksPath := strings.TrimSpace(string(out))
return hooksPath, nil
}
func createOrUpdatePreCommitFile(hooksPath string) error {
// File doesn't exist, create a new one
rootGitRepoPath, err := GetGitRoot()
if err != nil {
return err
}
filePath := fmt.Sprintf("%s/%s/%s", rootGitRepoPath, hooksPath, preCommitFile)
_, err = os.Stat(filePath)
if err == nil {
// File already exists, check if it contains the managed comments
content, err := ioutil.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read pre-commit file: %s", err)
}
if strings.Contains(string(content), "# MANAGED BY INFISICAL CLI (Do not modify): START") &&
strings.Contains(string(content), "# MANAGED BY INFISICAL CLI (Do not modify): END") {
return nil
}
// File already exists, append the template content
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY, 0755)
if err != nil {
return fmt.Errorf("failed to open pre-commit file: %s", err)
}
defer file.Close()
_, err = file.Write(preCommitTemplateAppend)
if err != nil {
return fmt.Errorf("failed to append to pre-commit file: %s", err)
}
} else if os.IsNotExist(err) {
err = os.WriteFile(filePath, preCommitTemplate, 0755)
if err != nil {
return fmt.Errorf("failed to create pre-commit file: %s", err)
}
} else {
// Error occurred while checking file status
return fmt.Errorf("failed to check pre-commit file status: %s", err)
}
return nil
}

@ -19,7 +19,8 @@ import (
"github.com/Infisical/infisical-merge/packages/util"
"github.com/Infisical/infisical-merge/packages/visualize"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
"github.com/posthog/posthog-go"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
@ -28,7 +29,6 @@ var secretsCmd = &cobra.Command{
Short: "Used to create, read update and delete secrets",
Use: "secrets",
DisableFlagsInUseLine: true,
PreRun: toggleDebug,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
environmentName, _ := cmd.Flags().GetString("env")
@ -64,6 +64,7 @@ var secretsCmd = &cobra.Command{
}
visualize.PrintAllSecretDetails(secrets)
Telemetry.CaptureEvent("cli-command:secrets", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION))
},
}
@ -73,7 +74,6 @@ var secretsGetCmd = &cobra.Command{
Use: "get [secrets]",
DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1),
PreRun: toggleDebug,
Run: getSecretsByNames,
}
@ -83,7 +83,6 @@ var secretsGenerateExampleEnvCmd = &cobra.Command{
Use: "generate-example-env",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
PreRun: toggleDebug,
Run: generateExampleEnv,
}
@ -92,7 +91,6 @@ var secretsSetCmd = &cobra.Command{
Short: "Used set secrets",
Use: "set [secrets]",
DisableFlagsInUseLine: true,
PreRun: toggleDebug,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
util.RequireLocalWorkspaceFile()
@ -134,7 +132,7 @@ var secretsSetCmd = &cobra.Command{
currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey)
if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 {
log.Debugf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey)
log.Debug().Msgf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey)
util.PrintErrorMessageAndExit("Some required user credentials are missing to generate your [plainTextEncryptionKey]. Please run [infisical login] then try again")
}
@ -270,6 +268,8 @@ var secretsSetCmd = &cobra.Command{
}
visualize.Table(headers, rows)
Telemetry.CaptureEvent("cli-command:secrets set", posthog.NewProperties().Set("version", util.CLI_VERSION))
},
}
@ -278,7 +278,6 @@ var secretsDeleteCmd = &cobra.Command{
Short: "Used to delete secrets by name",
Use: "delete [secrets]",
DisableFlagsInUseLine: true,
PreRun: toggleDebug,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
environmentName, _ := cmd.Flags().GetString("env")
@ -338,6 +337,7 @@ var secretsDeleteCmd = &cobra.Command{
fmt.Printf("secret name(s) [%v] have been deleted from your project \n", strings.Join(args, ", "))
Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION))
},
}
@ -382,6 +382,7 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
}
visualize.PrintAllSecretDetails(requestedSecrets)
Telemetry.CaptureEvent("cli-command:secrets get", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION))
}
func generateExampleEnv(cmd *cobra.Command, args []string) {
@ -570,6 +571,8 @@ func generateExampleEnv(cmd *cobra.Command, args []string) {
fmt.Println(strings.Join(dashedList, ""))
}
fmt.Println(strings.Join(fullyGeneratedDocuments, ""))
Telemetry.CaptureEvent("cli-command:generate-example-env", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION))
}
func CenterString(s string, numStars int) string {

@ -8,6 +8,7 @@ import (
"github.com/Infisical/infisical-merge/packages/models"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/manifoldco/promptui"
"github.com/posthog/posthog-go"
"github.com/spf13/cobra"
)
@ -78,6 +79,8 @@ var switchCmd = &cobra.Command{
if err != nil {
util.HandleError(err, "")
}
Telemetry.CaptureEvent("cli-command:user switch", posthog.NewProperties().Set("numberOfLoggedInProfiles", len(loggedInProfiles)).Set("version", util.CLI_VERSION))
},
}
@ -174,7 +177,7 @@ var domainCmd = &cobra.Command{
if err != nil {
util.HandleError(err, "")
}
Telemetry.CaptureEvent("cli-command:user domain", posthog.NewProperties().Set("version", util.CLI_VERSION))
},
}

@ -4,9 +4,12 @@ Copyright (c) 2023 Infisical Inc.
package cmd
import (
"fmt"
"github.com/99designs/keyring"
"github.com/Infisical/infisical-merge/packages/util"
log "github.com/sirupsen/logrus"
"github.com/posthog/posthog-go"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
@ -15,25 +18,24 @@ var vaultSetCmd = &cobra.Command{
Use: "set [vault-name]",
Short: "Used to set the vault backend to store your login details securely at rest",
DisableFlagsInUseLine: true,
PreRun: toggleDebug,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
wantedVaultTypeName := args[0]
currentVaultBackend, err := util.GetCurrentVaultBackend()
if err != nil {
log.Errorf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
return
}
if wantedVaultTypeName == string(currentVaultBackend) {
log.Errorf("You are already on vault backend [%s]", currentVaultBackend)
log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend)
return
}
if isVaultToSwitchToValid(wantedVaultTypeName) {
configFile, err := util.GetConfigFile()
if err != nil {
log.Errorf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
return
}
@ -42,13 +44,15 @@ var vaultSetCmd = &cobra.Command{
err = util.WriteConfigFile(&configFile)
if err != nil {
log.Errorf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err)
log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err)
return
}
log.Infof("Successfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]", currentVaultBackend, wantedVaultTypeName)
fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]", currentVaultBackend, wantedVaultTypeName)
Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
} else {
log.Errorf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, keyring.AvailableBackends())
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, keyring.AvailableBackends())
}
},
}
@ -58,7 +62,6 @@ var vaultCmd = &cobra.Command{
Use: "vault",
Short: "Used to manage where your Infisical login token is saved on your machine",
DisableFlagsInUseLine: true,
PreRun: toggleDebug,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
printAvailableVaultBackends()
@ -66,17 +69,19 @@ var vaultCmd = &cobra.Command{
}
func printAvailableVaultBackends() {
log.Infof("The following vaults are available on your system:")
fmt.Printf("The following vaults are available on your system:")
for _, backend := range keyring.AvailableBackends() {
log.Infof("- %s", backend)
fmt.Printf("\n- %s", backend)
}
currentVaultBackend, err := util.GetCurrentVaultBackend()
if err != nil {
log.Errorf("printAvailableVaultBackends: unable to print the available vault backend because of error [err=%s]", err)
log.Error().Msgf("printAvailableVaultBackends: unable to print the available vault backend because of error [err=%s]", err)
}
log.Infof("\nYou are currently using [%s] vault to store your login credentials", string(currentVaultBackend))
Telemetry.CaptureEvent("cli-command:vault", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("version", util.CLI_VERSION))
fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials", string(currentVaultBackend))
}
// Checks if the vault that the user wants to switch to is a valid available vault

@ -5,9 +5,10 @@ import (
)
type UserCredentials struct {
Email string `json:"email"`
PrivateKey string `json:"privateKey"`
JTWToken string `json:"JTWToken"`
Email string `json:"email"`
PrivateKey string `json:"privateKey"`
JTWToken string `json:"JTWToken"`
RefreshToken string `json:"RefreshToken"`
}
// The file struct for Infisical config file

Some files were not shown because too many files have changed in this diff Show More