Skip to content

Commit

Permalink
feat(cli): programatic access to profile data (#225)
Browse files Browse the repository at this point in the history
We are adding two new subcommands to the `lacework configure` command.

The `lacework configure list` command will list all profiles configured
into the configuration file `~/.lacework.toml`

The `lacework configure show <config_key>` command prints the current
computed configuration data from the specified configuration key. The
order of precedence to compute the configuration is flags, environment
variables, and the configuration file ~/.lacework.toml.

The available configuration keys are:
* `profile`
* `account`
* `api_secret`
* `api_key`

To show the configuration from a different profile, use the flag `--profile`
```
$ lacework configure show account --profile my-profile
```

Closes #209

Signed-off-by: Salim Afiune Maya <[email protected]>
  • Loading branch information
afiune committed Oct 7, 2020
1 parent 5b91e1e commit ab7ce7c
Show file tree
Hide file tree
Showing 12 changed files with 448 additions and 28 deletions.
21 changes: 21 additions & 0 deletions cli/cmd/cli_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"time"

"github.com/BurntSushi/toml"
"github.com/briandowns/spinner"
"github.com/fatih/color"
prettyjson "github.com/hokaccha/go-prettyjson"
Expand Down Expand Up @@ -116,6 +117,26 @@ func (c *cliState) LoadState() error {
return nil
}

// LoadProfiles loads all the profiles from the configuration file
func (c *cliState) LoadProfiles() (Profiles, error) {
var (
profiles = Profiles{}
confPath = viper.ConfigFileUsed()
)

if confPath == "" {
return profiles, errors.New("unable to load profiles. No configuration file found.")
}

cli.Log.Debugw("decoding config", "path", confPath)
if _, err := toml.DecodeFile(confPath, &profiles); err != nil {
return profiles, errors.Wrap(err, "unable to decode profiles from config")
}

cli.Log.Debugw("profiles loaded from config", "profiles", profiles)
return profiles, nil
}

// VerifySettings checks if the CLI state has the neccessary settings to run,
// if not, it throws an error with breadcrumbs to help the user configure the CLI
func (c *cliState) VerifySettings() error {
Expand Down
3 changes: 3 additions & 0 deletions cli/cmd/cli_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ package cmd

import "github.com/AlecAivazis/survey/v2"

// used by configure.go
var configureListCmdSetProfileEnv = `$ export LW_PROFILE="my-profile"`

// promptIconsFuncs configures the prompt icons for Unix systems
var promptIconsFunc = func(icons *survey.IconSet) {
icons.Question.Text = "▸"
Expand Down
3 changes: 3 additions & 0 deletions cli/cmd/cli_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ package cmd

import "github.com/AlecAivazis/survey/v2"

// used by configure.go
var configureListCmdSetProfileEnv = `C:\> $env:LW_PROFILE = 'my-profile'`

// promptIconsFuncs configures the prompt icons for Windows systems
var promptIconsFunc = func(icons *survey.IconSet) {
icons.Question.Text = ">"
Expand Down
140 changes: 126 additions & 14 deletions cli/cmd/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"regexp"
"sort"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/BurntSushi/toml"
homedir "github.com/mitchellh/go-homedir"
"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand Down Expand Up @@ -85,33 +88,116 @@ var (
Use: "configure",
Short: "configure the Lacework CLI",
Args: cobra.NoArgs,
Long: `
Configure settings that the Lacework CLI uses to interact with the Lacework
Long: `Configure settings that the Lacework CLI uses to interact with the Lacework
platform. These include your Lacework account, API access key and secret.
To create a set of API keys, log in to your Lacework account via WebUI and
navigate to Settings > API Keys and click + Create New. Enter a name for
the key and an optional description, then click Save. To get the secret key,
download the generated API key file.
Use the argument --json_file to preload the downloaded API key file.
Use the flag --json_file to preload the downloaded API key file.
If this command is run with no arguments, the Lacework CLI will store all
If this command is run with no flags, the Lacework CLI will store all
settings under the default profile. The information in the default profile
is used any time you run a Lacework CLI command that doesn't explicitly
specify a profile to use.
You can configure multiple profiles by using the --profile argument. If a
config file does not exist (the default location is ~/.lacework.toml), the
Lacework CLI will create it for you.`,
You can configure multiple profiles by using the --profile flag. If a
config file does not exist (the default location is ~/.lacework.toml),
the Lacework CLI will create it for you.`,
RunE: func(_ *cobra.Command, _ []string) error {
return promptConfigureSetup()
},
}

configureListCmd = &cobra.Command{
Use: "list",
Short: "list all configured profiles at ~/.lacework.toml",
Args: cobra.NoArgs,
Long: `List all profiles configured into the config file ~/.lacework.toml
To switch to a different profile permanently in your current terminal,
export the environment variable:
` + configureListCmdSetProfileEnv,
RunE: func(_ *cobra.Command, _ []string) error {
profiles, err := cli.LoadProfiles()
if err != nil {
return err
}

var (
strBuilder = &strings.Builder{}
table = tablewriter.NewWriter(strBuilder)
)

table.SetBorder(false)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetHeader([]string{"Profile", "Account", "API Key", "API Secret"})
table.AppendBulk(buildProfilesTableContent(cli.Profile, profiles))
table.Render()

cli.OutputHuman(strBuilder.String())
return nil
},
}

configureGetCmd = &cobra.Command{
Use: "show <config_key>",
Short: "show current configuration data",
Args: cobra.ExactArgs(1),
Long: `Prints the current computed configuration data from the specified configuration
key. The order of precedence to compute the configuration is flags, environment
variables, and the configuration file ~/.lacework.toml.
The available configuration keys are:
* profile
* account
* api_secret
* api_key
To show the configuration from a different profile, use the flag --profile.
$ lacework configure show account --profile my-profile`,
RunE: func(_ *cobra.Command, args []string) error {
data, ok := showConfigurationDataFromKey(args[0])
if !ok {
return errors.New("unknown configuration key. (available: profile, account, api_secret, api_key)")
}

if data == "" {
// @afiune something is not set correctly, here is a big
// exeption to exit the CLI in a non-standard way
os.Exit(1)
}

cli.OutputHuman(data)
cli.OutputHuman("\n")
return nil
},
}
)

func showConfigurationDataFromKey(key string) (string, bool) {
switch key {
case "profile":
return cli.Profile, true
case "account":
return cli.Account, true
case "api_secret":
return cli.Secret, true
case "api_key":
return cli.KeyID, true
default:
return "", false
}
}

func init() {
rootCmd.AddCommand(configureCmd)
configureCmd.AddCommand(configureListCmd)
configureCmd.AddCommand(configureGetCmd)

configureCmd.Flags().StringVarP(&configureJsonFile,
"json_file", "j", "", "loads the generated API key JSON file from the WebUI",
Expand Down Expand Up @@ -226,10 +312,10 @@ func promptConfigureSetup() error {

var (
profiles = Profiles{}
buf = new(bytes.Buffer)
confPath = viper.ConfigFileUsed()
buf = new(bytes.Buffer)
err error
)

if confPath == "" {
home, err := homedir.Dir()
if err != nil {
Expand All @@ -240,11 +326,10 @@ func promptConfigureSetup() error {
"path", confPath,
)
} else {
cli.Log.Debugw("decoding config", "path", confPath)
if _, err := toml.DecodeFile(confPath, &profiles); err != nil {
return errors.Wrap(err, "unable to decode profiles from config")
profiles, err = cli.LoadProfiles()
if err != nil {
return err
}
cli.Log.Debugw("profiles loaded from config, updating", "profiles", profiles)
}

profiles[cli.Profile] = newCreds
Expand All @@ -253,7 +338,7 @@ func promptConfigureSetup() error {
return err
}

err := ioutil.WriteFile(confPath, buf.Bytes(), 0600)
err = ioutil.WriteFile(confPath, buf.Bytes(), 0600)
if err != nil {
return err
}
Expand All @@ -274,3 +359,30 @@ func loadKeysFromJsonFile(file string) (*apiKeyDetails, error) {
err = json.Unmarshal(jsonData, &auth)
return &auth, err
}

func buildProfilesTableContent(current string, profiles Profiles) [][]string {
out := [][]string{}
for profile, creds := range profiles {
out = append(out, []string{
profile,
creds.Account,
creds.ApiKey,
formatSecret(4, creds.ApiSecret),
})
}

// order by severity
sort.Slice(out, func(i, j int) bool {
return out[i][0] < out[j][0]
})

for i := range out {
if out[i][0] == current {
out[i][0] = fmt.Sprintf("> %s", out[i][0])
} else {
out[i][0] = fmt.Sprintf(" %s", out[i][0])
}
}

return out
}
3 changes: 2 additions & 1 deletion cli/cmd/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ For example, to list all events from the last day with severity medium and above
// eventShowCmd represents the show sub-command inside the event command
eventShowCmd = &cobra.Command{
Use: "show <event_id>",
Short: "Show details about a specific event",
Short: "show details about a specific event",
Long: "Show details about a specific event.",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
cli.Log.Infow("requesting event details", "event_id", args[0])
Expand Down
11 changes: 8 additions & 3 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ var (
Use: "lacework",
Short: "A tool to manage the Lacework cloud security platform.",
SilenceErrors: true,
Long: `
The Lacework Command Line Interface is a tool that helps you manage the
Long: `The Lacework Command Line Interface is a tool that helps you manage the
Lacework cloud security platform. Use it to manage compliance reports,
external integrations, vulnerability scans, and other operations.
Expand All @@ -54,6 +53,10 @@ This will prompt you for your Lacework account and a set of API access keys.`,
case "help [command]", "configure", "version", "generate-pkg-manifest":
return nil
default:
// @afiune no need to create a client for any configure command
if cmd.HasParent() && cmd.Parent().Use == "configure" {
return nil
}
return cli.NewClient()
}
},
Expand Down Expand Up @@ -162,7 +165,9 @@ func initConfig() {
cli.Log.Debugw("configuration file not found")
} else {
// the config file was found but another error was produced
exitwith(errors.Wrap(err, "Error: unable to read in config"))
errcheckWARN(rootCmd.Help())
cli.OutputHuman("\n")
exitwith(errors.Wrap(err, "unable to read in config file ~/.lacework.toml"))
}
} else {
cli.Log.Debugw("using configuration file",
Expand Down
Loading

0 comments on commit ab7ce7c

Please sign in to comment.