Skip to content

Commit

Permalink
feat(cli): avoid displaying API key secret (#115)
Browse files Browse the repository at this point in the history
* 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 #114

Signed-off-by: Salim Afiune Maya <[email protected]>
  • Loading branch information
afiune authored May 26, 2020
1 parent 348e527 commit 3305b09
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 30 deletions.
43 changes: 30 additions & 13 deletions cli/cmd/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package cmd
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"path"

Expand Down Expand Up @@ -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. <ACCOUNT>.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)
Expand Down
16 changes: 15 additions & 1 deletion cli/cmd/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
57 changes: 57 additions & 0 deletions cli/cmd/prompt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Author:: Salim Afiune Maya (<[email protected]>)
// 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, ""), "")
}
59 changes: 43 additions & 16 deletions integration/configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
Expand All @@ -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")
}
Expand All @@ -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!")
Expand All @@ -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,
Expand All @@ -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")
}
Expand All @@ -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", "")
Expand All @@ -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")
}
Expand All @@ -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")
}
Expand All @@ -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!")
Expand All @@ -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]
Expand All @@ -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")
Expand Down Expand Up @@ -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]
Expand All @@ -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)
Expand Down

0 comments on commit 3305b09

Please sign in to comment.