From 3305b095fb43a3352255e472f38ba8f19b6d7c4b Mon Sep 17 00:00:00 2001 From: Salim Afiune Maya Date: Tue, 26 May 2020 09:26:54 -0600 Subject: [PATCH] feat(cli): avoid displaying API key secret (#115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cli): avoid displaying API key secret When users configure the CLI and insert their API secret key, such secret will now be formatted to avoid showing it on the screen. Example: ``` $ lacework configure ▸ Account: tech-ally ▸ Access Key ID: TECHALLY_3ACE24322ABBD3AC4435608BACEC4D638BEC6500DC12345 ▸ Secret Access Key: (*****************************f19e) You are all set! ``` Closes https://github.com/lacework/go-sdk/issues/114 Signed-off-by: Salim Afiune Maya --- cli/cmd/configure.go | 43 +++++++++++++++++-------- cli/cmd/prompt.go | 16 +++++++++- cli/cmd/prompt_test.go | 57 +++++++++++++++++++++++++++++++++ integration/configure_test.go | 59 +++++++++++++++++++++++++---------- 4 files changed, 145 insertions(+), 30 deletions(-) create mode 100644 cli/cmd/prompt_test.go diff --git a/cli/cmd/configure.go b/cli/cmd/configure.go index b7b70483e..b2e111fdf 100644 --- a/cli/cmd/configure.go +++ b/cli/cmd/configure.go @@ -21,6 +21,7 @@ package cmd import ( "bytes" "encoding/json" + "fmt" "io/ioutil" "path" @@ -118,43 +119,59 @@ func promptConfigureSetup() error { { Name: "account", Prompt: &survey.Input{ - Message: "Account: ", + Message: "Account:", Default: cli.Account, }, - Validate: promptRequiredStringLen(0, + Validate: promptRequiredStringLen(1, "The account subdomain of URL is required. (i.e. .lacework.net)", ), }, { Name: "api_key", Prompt: &survey.Input{ - Message: "Access Key ID: ", + Message: "Access Key ID:", Default: cli.KeyID, }, Validate: promptRequiredStringLen(55, "The API access key id must have more than 55 characters.", ), }, - { - Name: "api_secret", - Prompt: &survey.Input{ - Message: "Secret Access Key: ", - Default: cli.Secret, - }, - Validate: promptRequiredStringLen(30, - "The API secret access key must have more than 30 characters.", - ), + } + + secretQuest := &survey.Question{ + Name: "api_secret", + Validate: func(input interface{}) error { + str, ok := input.(string) + if !ok || len(str) < 30 { + if len(str) == 0 && len(cli.Secret) != 0 { + return nil + } + return errors.New("The API secret access key must have more than 30 characters.") + } + return nil }, } + secretMessage := "Secret Access Key:" + if len(cli.Secret) != 0 { + secretMessage = fmt.Sprintf("Secret Access Key: (%s)", formatSecret(4, cli.Secret)) + } + secretQuest.Prompt = &survey.Password{ + Message: secretMessage, + } + newCreds := credsDetails{} - err := survey.Ask(questions, &newCreds, + err := survey.Ask(append(questions, secretQuest), &newCreds, survey.WithIcons(promptIconsFunc), ) if err != nil { return err } + if len(newCreds.ApiSecret) == 0 { + newCreds.ApiSecret = cli.Secret + } + var ( profiles = Profiles{} buf = new(bytes.Buffer) diff --git a/cli/cmd/prompt.go b/cli/cmd/prompt.go index 0864290b9..d762d2970 100644 --- a/cli/cmd/prompt.go +++ b/cli/cmd/prompt.go @@ -22,9 +22,23 @@ import "github.com/pkg/errors" func promptRequiredStringLen(size int, err string) func(interface{}) error { return func(input interface{}) error { - if str, ok := input.(string); !ok || len(str) == 0 { + if str, ok := input.(string); !ok || len(str) < size { return errors.New(err) } return nil } } + +// @afiune add unit tests +func formatSecret(nToShow int, secret string) string { + secretSize := len(secret) + if secretSize <= nToShow { + return secret + } + + var chars = []byte(secret) + for i := 0; i < (secretSize - nToShow); i++ { + chars[i] = '*' + } + return string(chars) +} diff --git a/cli/cmd/prompt_test.go b/cli/cmd/prompt_test.go new file mode 100644 index 000000000..e243ddd28 --- /dev/null +++ b/cli/cmd/prompt_test.go @@ -0,0 +1,57 @@ +// +// Author:: Salim Afiune Maya () +// Copyright:: Copyright 2020, Lacework Inc. +// License:: Apache License, Version 2.0 +// +// 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 cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFormatSecret(t *testing.T) { + assert.Equal(t, + formatSecret(4, "_ab4c34d2df97babcd"), + "**************abcd", + "secrets are not being formatted correctly") + + assert.Equal(t, formatSecret(0, "_ab4c34d2df97babcd"), "******************") + assert.Equal(t, formatSecret(1, "_ab4c34d2df97babcd"), "*****************d") + assert.Equal(t, formatSecret(2, "_ab4c34d2df97babcd"), "****************cd") + assert.Equal(t, formatSecret(3, "_ab4c34d2df97babcd"), "***************bcd") + assert.Equal(t, formatSecret(4, "_ab4c34d2df97babcd"), "**************abcd") + assert.Equal(t, formatSecret(5, "_ab4c34d2df97babcd"), "*************babcd") + assert.Equal(t, formatSecret(6, "_ab4c34d2df97babcd"), "************7babcd") + assert.Equal(t, formatSecret(7, "_ab4c34d2df97babcd"), "***********97babcd") + assert.Equal(t, formatSecret(8, "_ab4c34d2df97babcd"), "**********f97babcd") + assert.Equal(t, formatSecret(9, "_ab4c34d2df97babcd"), "*********df97babcd") + assert.Equal(t, formatSecret(10, "_ab4c34d2df97babcd"), "********2df97babcd") + assert.Equal(t, formatSecret(11, "_ab4c34d2df97babcd"), "*******d2df97babcd") + assert.Equal(t, formatSecret(12, "_ab4c34d2df97babcd"), "******4d2df97babcd") + assert.Equal(t, formatSecret(13, "_ab4c34d2df97babcd"), "*****34d2df97babcd") + assert.Equal(t, formatSecret(14, "_ab4c34d2df97babcd"), "****c34d2df97babcd") + assert.Equal(t, formatSecret(15, "_ab4c34d2df97babcd"), "***4c34d2df97babcd") + assert.Equal(t, formatSecret(16, "_ab4c34d2df97babcd"), "**b4c34d2df97babcd") + assert.Equal(t, formatSecret(17, "_ab4c34d2df97babcd"), "*ab4c34d2df97babcd") + assert.Equal(t, formatSecret(18, "_ab4c34d2df97babcd"), "_ab4c34d2df97babcd") + assert.Equal(t, formatSecret(20, "_ab4c34d2df97babcd"), "_ab4c34d2df97babcd") + + // empty string + assert.Equal(t, formatSecret(0, ""), "") + assert.Equal(t, formatSecret(10, ""), "") +} diff --git a/integration/configure_test.go b/integration/configure_test.go index 3f8c01615..89689eca0 100644 --- a/integration/configure_test.go +++ b/integration/configure_test.go @@ -36,7 +36,7 @@ func TestConfigureCommand(t *testing.T) { c.ExpectString("Account:") c.SendLine("test-account") c.ExpectString("Access Key ID:") - c.SendLine("TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00") + c.SendLine("INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00") c.ExpectString("Secret Access Key:") c.SendLine("_00000000000000000000000000000000") c.ExpectString("You are all set!") @@ -46,7 +46,7 @@ func TestConfigureCommand(t *testing.T) { assert.Equal(t, `[default] account = "test-account" - api_key = "TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" + api_key = "INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" api_secret = "_00000000000000000000000000000000" `, laceworkTOML, "there is a problem with the generated config") } @@ -57,7 +57,7 @@ func TestConfigureCommandWithProfileFlag(t *testing.T) { c.ExpectString("Account:") c.SendLine("test-account") c.ExpectString("Access Key ID:") - c.SendLine("TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00") + c.SendLine("INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00") c.ExpectString("Secret Access Key:") c.SendLine("_00000000000000000000000000000000") c.ExpectString("You are all set!") @@ -67,14 +67,14 @@ func TestConfigureCommandWithProfileFlag(t *testing.T) { assert.Equal(t, `[my-profile] account = "test-account" - api_key = "TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" + api_key = "INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" api_secret = "_00000000000000000000000000000000" `, laceworkTOML, "there is a problem with the generated config") } func TestConfigureCommandWithJSONFileFlag(t *testing.T) { // create a JSON file similar to what the Lacework Web UI would provide - s := createJSONFileLikeWebUI(`{"keyId": "TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00","secret": "_cccccccccccccccccccccccccccccccc"}`) + s := createJSONFileLikeWebUI(`{"keyId": "INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00","secret": "_cccccccccccccccccccccccccccccccc"}`) defer os.Remove(s) _, laceworkTOML := runConfigureTest(t, @@ -92,7 +92,7 @@ func TestConfigureCommandWithJSONFileFlag(t *testing.T) { assert.Equal(t, `[default] account = "web-ui-test" - api_key = "TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" + api_key = "INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" api_secret = "_cccccccccccccccccccccccccccccccc" `, laceworkTOML, "there is a problem with the generated config") } @@ -112,7 +112,7 @@ func TestConfigureCommandWithJSONFileFlagError(t *testing.T) { func TestConfigureCommandWithEnvironmentVariables(t *testing.T) { os.Setenv("LW_ACCOUNT", "env-vars") - os.Setenv("LW_API_KEY", "TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00") + os.Setenv("LW_API_KEY", "INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00") os.Setenv("LW_API_SECRET", "_cccccccccccccccccccccccccccccccc") defer os.Setenv("LW_ACCOUNT", "") defer os.Setenv("LW_API_KEY", "") @@ -133,7 +133,7 @@ func TestConfigureCommandWithEnvironmentVariables(t *testing.T) { assert.Equal(t, `[default] account = "env-vars" - api_key = "TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" + api_key = "INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" api_secret = "_cccccccccccccccccccccccccccccccc" `, laceworkTOML, "there is a problem with the generated config") } @@ -151,13 +151,13 @@ func TestConfigureCommandWithAPIkeysFromFlags(t *testing.T) { }, "configure", "--account", "from-flags", - "--api_key", "TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00", + "--api_key", "INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00", "--api_secret", "_cccccccccccccccccccccccccccccccc", ) assert.Equal(t, `[default] account = "from-flags" - api_key = "TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" + api_key = "INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" api_secret = "_cccccccccccccccccccccccccccccccc" `, laceworkTOML, "there is a problem with the generated config") } @@ -171,7 +171,7 @@ func TestConfigureCommandWithExistingConfigAndMultiProfile(t *testing.T) { c.ExpectString("Account:") c.SendLine("super-cool-profile") c.ExpectString("Access Key ID:") - c.SendLine("TEST_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ") + c.SendLine("TEST_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ") c.ExpectString("Secret Access Key:") c.SendLine("_uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu") c.ExpectString("You are all set!") @@ -181,12 +181,12 @@ func TestConfigureCommandWithExistingConfigAndMultiProfile(t *testing.T) { assert.Equal(t, `[default] account = "test.account" - api_key = "TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" + api_key = "INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" api_secret = "_00000000000000000000000000000000" [dev] account = "dev.example" - api_key = "DEVDEV_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" + api_key = "DEVDEV_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC000" api_secret = "_11111111111111111111111111111111" [integration] @@ -196,11 +196,38 @@ func TestConfigureCommandWithExistingConfigAndMultiProfile(t *testing.T) { [new-profile] account = "super-cool-profile" - api_key = "TEST_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ" + api_key = "TEST_ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ" api_secret = "_uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu" `, laceworkTOML, "there is a problem with the generated config") } +func TestConfigureCommandErrors(t *testing.T) { + _, laceworkTOML := runConfigureTest(t, + func(c *expect.Console) { + c.ExpectString("Account:") + c.SendLine("") + c.ExpectString("The account subdomain of URL is required") + c.SendLine("my-account") + c.ExpectString("Access Key ID:") + c.SendLine("") + c.ExpectString("The API access key id must have more than 55 characters") + c.SendLine("INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00") + c.ExpectString("Secret Access Key:") + c.SendLine("") + c.ExpectString("The API secret access key must have more than 30 characters") + c.SendLine("_00000000000000000000000000000000") + c.ExpectString("You are all set!") + }, + "configure", + ) + + assert.Equal(t, `[default] + account = "my-account" + api_key = "INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00" + api_secret = "_00000000000000000000000000000000" +`, laceworkTOML, "there is a problem with the generated config") +} + func createJSONFileLikeWebUI(content string) string { contentBytes := []byte(content) tmpfile, err := ioutil.TempFile("", "json_file") @@ -269,7 +296,7 @@ func createTOMLConfig() string { configFile := filepath.Join(dir, ".lacework.toml") c := []byte(`[default] account = 'test.account' -api_key = 'TEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00' +api_key = 'INTTEST_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00' api_secret = '_00000000000000000000000000000000' [integration] @@ -279,7 +306,7 @@ api_secret = '_1234abdc00ff11vv22zz33xyz1234abc' [dev] account = 'dev.example' -api_key = 'DEVDEV_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC00' +api_key = 'DEVDEV_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AAABBBCCC000' api_secret = '_11111111111111111111111111111111' `) err = ioutil.WriteFile(configFile, c, 0644)