Skip to content

Commit

Permalink
feat(cli): implement JSON format for all commands
Browse files Browse the repository at this point in the history
We are adding a new Global Flag called `--json` that users can use to
switch the CLI output format to display JSON instead of the default,
human readable format.

To make this setting persistent on the current terminal, a user can
export the environment variable `LW_JSON=1`.

Signed-off-by: Salim Afiune Maya <[email protected]>
  • Loading branch information
afiune committed Apr 21, 2020
1 parent 1b8b561 commit c7d4fee
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 52 deletions.
7 changes: 1 addition & 6 deletions cli/cmd/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"fmt"
"strings"

"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/spf13/cobra"

Expand Down Expand Up @@ -96,13 +95,9 @@ func runApiCommand(_ *cobra.Command, args []string) error {
return errors.Wrap(err, "unable to send the request")
}

pretty, err := cli.JsonF.Marshal(*response)
if err != nil {
cli.Log.Debugw("unable to pretty print JSON response", "raw", response)
if err := cli.OutputJSON(*response); err != nil {
return errors.Wrap(err, "unable to format json response")
}

fmt.Fprintln(color.Output, string(pretty))
return nil
}

Expand Down
19 changes: 19 additions & 0 deletions cli/cmd/cli_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type cliState struct {
JsonF *prettyjson.Formatter
Log *zap.SugaredLogger

jsonOutput bool
profileDetails map[string]interface{}
}

Expand Down Expand Up @@ -123,6 +124,24 @@ func (c *cliState) VerifySettings() error {
return nil
}

func (c *cliState) EnableJSONOutput() {
cli.Log.Info("switch output to json format")
cli.jsonOutput = true
}

func (c *cliState) EnableHumanOutput() {
cli.Log.Info("switch output to human format")
cli.jsonOutput = false
}

func (c *cliState) JSONOutput() bool {
return cli.jsonOutput
}

func (c *cliState) HumanOutput() bool {
return !cli.jsonOutput
}

// loadStateFromViper loads parameters and environment variables
// coming from viper into the CLI state
func (c *cliState) loadStateFromViper() {
Expand Down
4 changes: 4 additions & 0 deletions cli/cmd/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ var (
return errors.Wrap(err, "unable to get integrations")
}

if cli.JSONOutput() {
return cli.OutputJSON(integrations.Data)
}

table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Integration GUID", "Name", "Type", "Status", "State"})
table.SetBorder(false)
Expand Down
45 changes: 45 additions & 0 deletions cli/cmd/outputs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// 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 (
"fmt"
"os"

"github.com/fatih/color"
)

// OutputJSON will print out the JSON representation of the provided data
func (c *cliState) OutputJSON(v interface{}) error {
pretty, err := c.JsonF.Marshal(v)
if err != nil {
cli.Log.Debugw("unable to pretty print JSON object", "raw", v)
return err
}
fmt.Fprintln(color.Output, string(pretty))
return nil
}

// OutputHumanRead will print out the provided message if the cli state is
// configured to talk to humans, to switch to json format use --json
func (c *cliState) OutputHuman(format string, a ...interface{}) {
if c.HumanOutput() {
fmt.Fprintf(os.Stdout, format, a...)
}
}
21 changes: 17 additions & 4 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ func init() {
rootCmd.PersistentFlags().Bool("nocolor", false,
"turn off colors",
)
rootCmd.PersistentFlags().Bool("json", false,
"switch commands output from human readable to json format",
)
rootCmd.PersistentFlags().StringP("profile", "p", "",
"switch between profiles configured at ~/.lacework.toml",
)
Expand All @@ -97,6 +100,7 @@ func init() {

errcheckWARN(viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")))
errcheckWARN(viper.BindPFlag("nocolor", rootCmd.PersistentFlags().Lookup("nocolor")))
errcheckWARN(viper.BindPFlag("json", rootCmd.PersistentFlags().Lookup("json")))
errcheckWARN(viper.BindPFlag("profile", rootCmd.PersistentFlags().Lookup("profile")))
errcheckWARN(viper.BindPFlag("account", rootCmd.PersistentFlags().Lookup("account")))
errcheckWARN(viper.BindPFlag("api_key", rootCmd.PersistentFlags().Lookup("api_key")))
Expand All @@ -121,19 +125,28 @@ func initConfig() {
cli.LogLevel = "DEBUG"
}

// initialize a Lacework logger
cli.Log = lwlogger.New(cli.LogLevel).Sugar()

if viper.GetBool("nocolor") {
cli.Log.Info("turning off colors")
cli.JsonF.DisabledColor = true
}

if viper.GetBool("json") {
cli.EnableJSONOutput()
}

// by default the cli logs are going to be visualized in
// a console format unless the user wants the opposite
if os.Getenv("LW_LOG_FORMAT") == "" {
os.Setenv("LW_LOG_FORMAT", "CONSOLE")
if cli.JSONOutput() {
os.Setenv("LW_LOG_FORMAT", "JSON")
} else {
os.Setenv("LW_LOG_FORMAT", "CONSOLE")
}
}

// initialize a Lacework logger
cli.Log = lwlogger.New(cli.LogLevel).Sugar()

// try to read config file
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
Expand Down
99 changes: 57 additions & 42 deletions cli/cmd/vulnerability.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Arguments:
)
}

fmt.Printf(
cli.OutputHuman(
"A new vulnerability scan has been requested. (request_id: %s)\n\n",
scan.Data.RequestID,
)
Expand All @@ -124,17 +124,15 @@ Arguments:
"param", "--poll",
"request_id", scan.Data.RequestID,
)
report, err := pollScanStatus(scan.Data.RequestID, lacework)
if err != nil {
return errors.Wrap(err, "unable to track scan status")
}
return pollScanStatus(scan.Data.RequestID, lacework)
}

cli.Log.Info("scan completed, displaying report")
fmt.Println(report)
} else {
fmt.Println("To track the progress of the scan, use the command:")
fmt.Printf(" $ lacework vulnerability scan show %s\n", scan.Data.RequestID)
if cli.JSONOutput() {
return cli.OutputJSON(scan.Data)
}

cli.OutputHuman("To track the progress of the scan, use the command:\n")
cli.OutputHuman(" $ lacework vulnerability scan show %s\n", scan.Data.RequestID)
return nil
},
}
Expand All @@ -154,27 +152,33 @@ Arguments:
}

if vulCmdState.Poll {
report, err := pollScanStatus(args[0], lacework)
if err != nil {
return errors.Wrap(err, "unable to poll scan status")
}
// To make the report easy to read, add an empty carrier return
fmt.Println("")
fmt.Println(report)
return nil
cli.Log.Infow("tracking scan progress",
"param", "--poll",
"request_id", args[0],
)
return pollScanStatus(args[0], lacework)
}

report, err, scanning := checkScanStatus(args[0], lacework)
if err != nil {
return err
}

// if the returned scan report is not scanning, add an empty carrier
// return to make the report easy to read
if !scanning {
fmt.Println("")
if cli.JSONOutput() {
return cli.OutputJSON(report)
}

// if the scan is still running, display a nice message
if scanning {
cli.OutputHuman(
"The vulnerability scan is still running. (request_id: %s)\n\n",
args[0],
)
cli.OutputHuman("Use --poll to poll until the vulnerability scan completes.\n")
} else {
cli.OutputHuman("\n")
cli.OutputHuman(buildVulnerabilityReport(report))
}
fmt.Println(report)
return nil
},
}
Expand Down Expand Up @@ -219,8 +223,12 @@ Arguments:
status := report.CheckStatus()
switch status {
case "Success":
fmt.Println("")
fmt.Println(buildVulnerabilityReport(&report.Data))
if cli.JSONOutput() {
return cli.OutputJSON(report.Data)
}

cli.OutputHuman("\n")
cli.OutputHuman(buildVulnerabilityReport(&report.Data))
case "NotFound":
return errors.Errorf(
"unable to find any vulnerability report for image '%s'",
Expand Down Expand Up @@ -268,54 +276,61 @@ func init() {
)
}

func pollScanStatus(requestID string, lacework *api.Client) (string, error) {
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
s.Suffix = " Scan running..."
s.Start()
defer s.Stop()
func pollScanStatus(requestID string, lacework *api.Client) error {
var s *spinner.Spinner
if cli.HumanOutput() {
// humans like spinners (\o/)
s = spinner.New(spinner.CharSets[9], 100*time.Millisecond)
s.Suffix = " Scan running..."
s.Start()
}

for {
report, err, retry := checkScanStatus(requestID, lacework)
if err != nil {
return "", err
return err
}

if retry {
time.Sleep(vulCmdState.PollInterval)
continue
}

return report, nil
if cli.JSONOutput() {
return cli.OutputJSON(report)
}

s.Stop()
cli.OutputHuman(buildVulnerabilityReport(report))
return nil
}
}

func checkScanStatus(requestID string, lacework *api.Client) (string, error, bool) {
cli.Log.Debugw("verifying status of vulnerability scan", "request_id", requestID)
func checkScanStatus(requestID string, lacework *api.Client) (*api.VulContainerReport, error, bool) {
cli.Log.Infow("verifying status of vulnerability scan", "request_id", requestID)
scan, err := lacework.Vulnerabilities.ScanStatus(requestID)
if err != nil {
return "", errors.Wrap(err, "unable to verify status of the vulnerability scan"), false
return nil, errors.Wrap(err, "unable to verify status of the vulnerability scan"), false
}

cli.Log.Debugw("vulnerability scan", "details", scan)
status := scan.CheckStatus()
switch status {
case "Success":
return buildVulnerabilityReport(&scan.Data), nil, false
return &scan.Data, nil, false
case "Scanning":
msg := "The vulnerability scan is still running.\n\n"
msg = msg + "Use --poll to poll until the vulnerability scan completes."
return msg, nil, true
return &scan.Data, nil, true
case "NotFound":
return "", errors.Errorf(
return nil, errors.Errorf(
"unable to find any vulnerability scan with request id '%s'",
requestID,
), false
case "Failed":
return "", errors.New(
return nil, errors.New(
"the vulnerability scan failed to execute. Use '--debug' to troubleshoot.",
), false
default:
return "", errors.New(
return nil, errors.New(
"unable to get status from vulnerability scan. Use '--debug' to troubleshoot.",
), false
}
Expand Down

0 comments on commit c7d4fee

Please sign in to comment.