diff --git a/attestation/environment/blocklist.go b/attestation/environment/blocklist.go index 0bb20e47..df8a98ce 100644 --- a/attestation/environment/blocklist.go +++ b/attestation/environment/blocklist.go @@ -14,6 +14,13 @@ package environment +import ( + "strings" + + "github.com/gobwas/glob" + "github.com/in-toto/go-witness/log" +) + // sourced from https://github.com/Puliczek/awesome-list-of-secrets-in-environment-variables/blob/main/raw_list.txt func DefaultBlockList() map[string]struct{} { return map[string]struct{}{ @@ -103,12 +110,37 @@ func DefaultBlockList() map[string]struct{} { // FilterEnvironmentArray expects an array of strings representing environment variables. Each element of the array is expected to be in the format of "KEY=VALUE". // blockList is the list of elements to filter from variables, and for each element of variables that does not appear in the blockList onAllowed will be called. func FilterEnvironmentArray(variables []string, blockList map[string]struct{}, onAllowed func(key, val, orig string)) { + filterGlobList := []glob.Glob{} + + for k := range blockList { + if strings.Contains(k, "*") { + filterGlobCompiled, err := glob.Compile(k) + if err != nil { + log.Errorf("obfuscate glob pattern could not be interpreted: %w", err) + } + + filterGlobList = append(filterGlobList, filterGlobCompiled) + } + } + for _, v := range variables { key, val := splitVariable(v) + filterOut := false + if _, inBlockList := blockList[key]; inBlockList { - continue + filterOut = true } - onAllowed(key, val, v) + + for _, glob := range filterGlobList { + if glob.Match(key) { + filterOut = true + break + } + } + + if ! filterOut { + onAllowed(key, val, v) + } } } diff --git a/attestation/environment/environment.go b/attestation/environment/environment.go index 48db70a4..b09d9e31 100644 --- a/attestation/environment/environment.go +++ b/attestation/environment/environment.go @@ -15,12 +15,14 @@ package environment import ( + "fmt" "os" "os/user" "runtime" "strings" "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/registry" "github.com/invopop/jsonschema" ) @@ -33,8 +35,10 @@ const ( // This is a hacky way to create a compile time error in case the attestor // doesn't implement the expected interfaces. var ( - _ attestation.Attestor = &Attestor{} - _ EnvironmentAttestor = &Attestor{} + _ attestation.Attestor = &Attestor{} + _ EnvironmentAttestor = &Attestor{} + defaultBlockSensitiveVarsEnabled = false + defaultDisableSensitiveVarsDefault = false ) type EnvironmentAttestor interface { @@ -47,9 +51,50 @@ type EnvironmentAttestor interface { } func init() { - attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { - return New() - }) + attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { return New() }, + registry.BoolConfigOption( + "block-sensitive-vars", + "Switch from obfuscate to blocking variables which removes them from the output completely.", + defaultBlockSensitiveVarsEnabled, + func(a attestation.Attestor, blockSensitiveVarsEnabled bool) (attestation.Attestor, error) { + envAttestor, ok := a.(*Attestor) + if !ok { + return a, fmt.Errorf("unexpected attestor type: %T is not a environment attestor", a) + } + + WithBlockVarsEnabled(blockSensitiveVarsEnabled)(envAttestor) + return envAttestor, nil + }, + ), + registry.BoolConfigOption( + "disable-default-sensitive-vars", + "Disable the default list of sensitive vars and only use the items mentioned by --attestor-environment-sensitive-key.", + defaultDisableSensitiveVarsDefault, + func(a attestation.Attestor, disableSensitiveVarsDefault bool) (attestation.Attestor, error) { + envAttestor, ok := a.(*Attestor) + if !ok { + return a, fmt.Errorf("unexpected attestor type: %T is not a environment attestor", a) + } + + WithDisableDefaultSensitiveList(disableSensitiveVarsDefault)(envAttestor) + return envAttestor, nil + }, + ), + registry.StringSliceConfigOption( + "sensitive-key", + "Add keys to the list of sensitive environment keys.", + []string{}, + func(a attestation.Attestor, additionalKeys []string) (attestation.Attestor, error) { + envAttestor, ok := a.(*Attestor) + if !ok { + return a, fmt.Errorf("unexpected attestor type: %T is not a environment attestor", a) + } + + WithAdditionalKeys(additionalKeys)(envAttestor) + return envAttestor, nil + }, + ), + ) } type Attestor struct { @@ -58,30 +103,42 @@ type Attestor struct { Username string `json:"username"` Variables map[string]string `json:"variables,omitempty"` - blockList map[string]struct{} - obfuscateList map[string]struct{} + sensitiveVarsList map[string]struct{} + addSensitiveVarsList map[string]struct{} + blockVarsEnabled bool + disableSensitiveVarsDefault bool } type Option func(*Attestor) -func WithBlockList(blockList map[string]struct{}) Option { +// WithBlockVarsEnabled will make the blocking (removing) of vars the acting behavior. +// The default behavior is obfuscation of variables. +func WithBlockVarsEnabled(blockVarsEnabled bool) Option { return func(a *Attestor) { - a.blockList = blockList + a.blockVarsEnabled = blockVarsEnabled } } -func WithObfuscateList(obfuscateList map[string]struct{}) Option { +// WithAdditionalKeys add additional keys to final list that is checked for sensitive variables. +func WithAdditionalKeys(additionalKeys []string) Option { return func(a *Attestor) { - for key, value := range obfuscateList { - a.obfuscateList[key] = value + for _, value := range additionalKeys { + a.addSensitiveVarsList[value] = struct{}{} } } } +// WithDisableDefaultSensitiveList will disable the default list and only use the additional keys. +func WithDisableDefaultSensitiveList(disableSensitiveVarsDefault bool) Option { + return func(a *Attestor) { + a.disableSensitiveVarsDefault = disableSensitiveVarsDefault + } +} + func New(opts ...Option) *Attestor { attestor := &Attestor{ - blockList: DefaultBlockList(), - obfuscateList: DefaultObfuscateList(), + sensitiveVarsList: DefaultSensitiveEnvList(), + addSensitiveVarsList: map[string]struct{}{}, } for _, opt := range opts { @@ -119,13 +176,26 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { a.Username = user.Username } - FilterEnvironmentArray(os.Environ(), a.blockList, func(key, val, _ string) { - a.Variables[key] = val - }) + // Prepare sensitive keys list. + var finalSensitiveKeysList map[string]struct{} + if a.disableSensitiveVarsDefault { + a.sensitiveVarsList = nil + } + finalSensitiveKeysList = a.sensitiveVarsList + for k, v := range a.addSensitiveVarsList { + finalSensitiveKeysList[k] = v + } - ObfuscateEnvironmentArray(a.Variables, a.obfuscateList, func(key, val, _ string) { - a.Variables[key] = val - }) + // Block or obfuscate + if a.blockVarsEnabled { + FilterEnvironmentArray(os.Environ(), finalSensitiveKeysList, func(key, val, _ string) { + a.Variables[key] = val + }) + } else { + ObfuscateEnvironmentArray(os.Environ(), finalSensitiveKeysList, func(key, val, _ string) { + a.Variables[key] = val + }) + } return nil } diff --git a/attestation/environment/obfuscate.go b/attestation/environment/obfuscate.go index 6898cb00..e010431c 100644 --- a/attestation/environment/obfuscate.go +++ b/attestation/environment/obfuscate.go @@ -21,20 +21,9 @@ import ( "github.com/in-toto/go-witness/log" ) -// sourced from https://github.com/Puliczek/awesome-list-of-secrets-in-environment-variables/blob/main/raw_list.txt -func DefaultObfuscateList() map[string]struct{} { - return map[string]struct{}{ - "*_TOKEN": {}, - "SECRET_*": {}, - "*_API_KEY": {}, - "*_PASSWORD": {}, - "*_JWT": {}, - } -} - // FilterEnvironmentArray expects an array of strings representing environment variables. Each element of the array is expected to be in the format of "KEY=VALUE". // blockList is the list of elements to filter from variables, and for each element of variables that does not appear in the blockList onAllowed will be called. -func ObfuscateEnvironmentArray(variables map[string]string, obfuscateList map[string]struct{}, onAllowed func(key, val, orig string)) { +func ObfuscateEnvironmentArray(variables []string, obfuscateList map[string]struct{}, onAllowed func(key, val, orig string)) { obfuscateGlobList := []glob.Glob{} for k := range obfuscateList { @@ -48,8 +37,8 @@ func ObfuscateEnvironmentArray(variables map[string]string, obfuscateList map[st } } - for key, v := range variables { - val := v + for _, v := range variables { + key, val := splitVariable(v) if _, inObfuscateList := obfuscateList[key]; inObfuscateList { val = "******" diff --git a/attestation/environment/sensitive_env_vars.go b/attestation/environment/sensitive_env_vars.go new file mode 100644 index 00000000..df816b10 --- /dev/null +++ b/attestation/environment/sensitive_env_vars.go @@ -0,0 +1,112 @@ +// Copyright 2024 The Witness Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package environment + +// DefaultSensitiveEnvList return a list of known sensitive environment keys. +// +// Eplicit list sourced from https://github.com/Puliczek/awesome-list-of-secrets-in-environment-variables/blob/main/raw_list.txt +func DefaultSensitiveEnvList() map[string]struct{} { + return map[string]struct{}{ + + // Glob pattern list + "*_TOKEN": {}, + "SECRET_*": {}, + "*_API_KEY": {}, + "*_PASSWORD": {}, + "*_JWT": {}, + + // Explicit list + "AWS_ACCESS_KEY_ID": {}, + "AWS_SECRET_ACCESS_KEY": {}, + "AMAZON_AWS_ACCESS_KEY_ID": {}, + "AMAZON_AWS_SECRET_ACCESS_KEY": {}, + "ALGOLIA_API_KEY": {}, + "AZURE_CLIENT_ID": {}, + "AZURE_CLIENT_SECRET": {}, + "AZURE_USERNAME": {}, + "AZURE_PASSWORD": {}, + "MSI_ENDPOINT": {}, + "MSI_SECRET": {}, + "binance_api": {}, + "binance_secret": {}, + "BITTREX_API_KEY": {}, + "BITTREX_API_SECRET": {}, + "CF_PASSWORD": {}, + "CF_USERNAME": {}, + "CODECLIMATE_REPO_TOKEN": {}, + "COVERALLS_REPO_TOKEN": {}, + "CIRCLE_TOKEN": {}, + "DIGITALOCEAN_ACCESS_TOKEN": {}, + "DOCKER_EMAIL": {}, + "DOCKER_PASSWORD": {}, + "DOCKER_USERNAME": {}, + "DOCKERHUB_PASSWORD": {}, + "FACEBOOK_APP_ID": {}, + "FACEBOOK_APP_SECRET": {}, + "FACEBOOK_ACCESS_TOKEN": {}, + "FIREBASE_TOKEN": {}, + "FOSSA_API_KEY": {}, + "GH_TOKEN": {}, + "GH_ENTERPRISE_TOKEN": {}, + "GOOGLE_APPLICATION_CREDENTIALS": {}, + "GOOGLE_API_KEY": {}, + "CI_DEPLOY_USER": {}, + "CI_DEPLOY_PASSWORD": {}, + "GITLAB_USER_LOGIN": {}, + "CI_JOB_JWT": {}, + "CI_JOB_JWT_V2": {}, + "CI_JOB_TOKEN": {}, + "HEROKU_API_KEY": {}, + "HEROKU_API_USER": {}, + "MAILGUN_API_KEY": {}, + "MCLI_PRIVATE_API_KEY": {}, + "MCLI_PUBLIC_API_KEY": {}, + "NGROK_TOKEN": {}, + "NGROK_AUTH_TOKEN": {}, + "NPM_AUTH_TOKEN": {}, + "OKTA_CLIENT_ORGURL": {}, + "OKTA_CLIENT_TOKEN": {}, + "OKTA_OAUTH2_CLIENTSECRET": {}, + "OKTA_OAUTH2_CLIENTID": {}, + "OKTA_AUTHN_GROUPID": {}, + "OS_USERNAME": {}, + "OS_PASSWORD": {}, + "PERCY_TOKEN": {}, + "SAUCE_ACCESS_KEY": {}, + "SAUCE_USERNAME": {}, + "SENTRY_AUTH_TOKEN": {}, + "SLACK_TOKEN": {}, + "SNYK_TOKEN": {}, + "square_access_token": {}, + "square_oauth_secret": {}, + "STRIPE_API_KEY": {}, + "STRIPE_DEVICE_NAME": {}, + "SURGE_TOKEN": {}, + "SURGE_LOGIN": {}, + "TWILIO_ACCOUNT_SID": {}, + "CONSUMER_KEY": {}, + "CONSUMER_SECRET": {}, + "TRAVIS_SUDO": {}, + "TRAVIS_OS_NAME": {}, + "TRAVIS_SECURE_ENV_VARS": {}, + "VAULT_TOKEN": {}, + "VAULT_CLIENT_KEY": {}, + "TOKEN": {}, + "VULTR_ACCESS": {}, + "VULTR_SECRET": {}, + "ACTIONS_RUNTIME_TOKEN": {}, + "ACTIONS_ID_TOKEN_REQUEST_TOKEN": {}, + } +}