Skip to content

Commit

Permalink
Merge pull request #431 from patilpankaj212/human_readable_output
Browse files Browse the repository at this point in the history
support for human readable format
  • Loading branch information
kanchwala-yusuf committed Dec 16, 2020
2 parents 9c5476c + c79dce5 commit 7cc4f16
Show file tree
Hide file tree
Showing 17 changed files with 663 additions and 57 deletions.
2 changes: 1 addition & 1 deletion pkg/cli/register.go
Expand Up @@ -56,7 +56,7 @@ func setDefaultCommandIfNonePresent() {
func Execute() {
rootCmd.PersistentFlags().StringVarP(&LogLevel, "log-level", "l", "info", "log level (debug, info, warn, error, panic, fatal)")
rootCmd.PersistentFlags().StringVarP(&LogType, "log-type", "x", "console", "log output type (console, json)")
rootCmd.PersistentFlags().StringVarP(&OutputType, "output", "o", "yaml", "output type (json, yaml, xml)")
rootCmd.PersistentFlags().StringVarP(&OutputType, "output", "o", "human", "output type (human, json, yaml, xml)")
rootCmd.PersistentFlags().StringVarP(&ConfigFile, "config-path", "c", "", "config file path")

// Function to execute before processing commands
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/root.go
Expand Up @@ -25,7 +25,7 @@ var (
LogLevel string
// LogType Logging output type (console, json)
LogType string
// OutputType Violation output type (text, json, yaml, xml)
// OutputType Violation output type (human, json, yaml, xml)
OutputType string
// ConfigFile Config file path
ConfigFile string
Expand Down
64 changes: 49 additions & 15 deletions pkg/cli/run.go
Expand Up @@ -17,9 +17,11 @@
package cli

import (
"errors"
"flag"
"os"
"path/filepath"
"strings"

"github.com/accurics/terrascan/pkg/downloader"
"github.com/accurics/terrascan/pkg/runtime"
Expand All @@ -28,26 +30,26 @@ import (
"go.uber.org/zap"
)

const (
humanOutputFormat = "human"
)

// Run executes terrascan in CLI mode
func Run(iacType, iacVersion string, cloudType []string,
iacFilePath, iacDirPath, configFile string, policyPath []string,
format, remoteType, remoteURL string, configOnly, useColors bool) {
format, remoteType, remoteURL string, configOnly, useColors, verbose bool) {

// temp dir to download the remote repo
tempDir := filepath.Join(os.TempDir(), utils.GenRandomString(6))
defer os.RemoveAll(tempDir)

// download remote repository
d := downloader.NewDownloader()
path, err := d.DownloadWithType(remoteType, remoteURL, tempDir)
if err == downloader.ErrEmptyURLType {
// url and type empty, proceed with regular scanning
zap.S().Debugf("remote url and type not configured, proceeding with regular scanning")
} else if err != nil {
// some error while downloading remote repository
path, err := downloadRemoteRepository(remoteType, remoteURL, tempDir)
if err != nil {
return
} else {
// successfully downloaded remote repository
}

if path != "" {
iacDirPath = path
}

Expand All @@ -64,16 +66,48 @@ func Run(iacType, iacVersion string, cloudType []string,
return
}

// write results to console
err = writeResults(results, useColors, verbose, configOnly, format)
if err != nil {
zap.S().Error("failed to write results", zap.Error(err))
return
}

if results.Violations.ViolationStore.Summary.ViolatedPolicies != 0 && flag.Lookup("test.v") == nil {
os.RemoveAll(tempDir)
os.Exit(3)
}
}

func downloadRemoteRepository(remoteType, remoteURL, tempDir string) (string, error) {
d := downloader.NewDownloader()
path, err := d.DownloadWithType(remoteType, remoteURL, tempDir)
if err == downloader.ErrEmptyURLType {
// url and type empty, proceed with regular scanning
zap.S().Debugf("remote url and type not configured, proceeding with regular scanning")
} else if err != nil {
// some error while downloading remote repository
return path, err
}
return path, nil
}

func writeResults(results runtime.Output, useColors, verbose, configOnly bool, format string) error {
// add verbose flag to the scan summary
results.Violations.ViolationStore.Summary.ShowViolationDetails = verbose

outputWriter := NewOutputWriter(useColors)

if configOnly {
// human readable output doesn't support --config-only flag
// if --config-only flag is set, then exit with an error
// asking the user to use yaml or json output format
if strings.EqualFold(format, humanOutputFormat) {
return errors.New("please use yaml or json output format when using --config-only flag")
}
writer.Write(format, results.ResourceConfig, outputWriter)
} else {
writer.Write(format, results.Violations, outputWriter)
}

if results.Violations.ViolationStore.Count.TotalCount != 0 && flag.Lookup("test.v") == nil {
os.RemoveAll(tempDir)
os.Exit(3)
}
return nil
}
136 changes: 135 additions & 1 deletion pkg/cli/run_test.go
Expand Up @@ -17,7 +17,15 @@
package cli

import (
"os"
"path/filepath"
"testing"

"github.com/accurics/terrascan/pkg/iac-providers/output"
"github.com/accurics/terrascan/pkg/policy"
"github.com/accurics/terrascan/pkg/results"
"github.com/accurics/terrascan/pkg/runtime"
"github.com/accurics/terrascan/pkg/utils"
)

func TestRun(t *testing.T) {
Expand All @@ -26,10 +34,12 @@ func TestRun(t *testing.T) {
iacType string
iacVersion string
cloudType []string
format string
iacFilePath string
iacDirPath string
configFile string
configOnly bool
verbose bool
stdOut string
want string
wantErr error
Expand All @@ -56,11 +66,135 @@ func TestRun(t *testing.T) {
iacFilePath: "testdata/run-test/config-only.yaml",
configOnly: true,
},
{
name: "config-only flag true with human readable format",
cloudType: []string{"terraform"},
iacFilePath: "testdata/run-test/config-only.tf",
configOnly: true,
format: "human",
},
{
name: "config-only flag false with human readable format",
cloudType: []string{"k8s"},
iacFilePath: "testdata/run-test/config-only.yaml",
format: "human",
},
}

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
Run(tt.iacType, tt.iacVersion, tt.cloudType, tt.iacFilePath, tt.iacDirPath, tt.configFile, []string{}, "", "", "", tt.configOnly, false)
Run(tt.iacType, tt.iacVersion, tt.cloudType, tt.iacFilePath, tt.iacDirPath, tt.configFile, []string{}, tt.format, "", "", tt.configOnly, false, tt.verbose)
})
}
}

func TestWriteResults(t *testing.T) {
testInput := runtime.Output{
ResourceConfig: output.AllResourceConfigs{},
Violations: policy.EngineOutput{
ViolationStore: &results.ViolationStore{},
},
}
type args struct {
results runtime.Output
useColors bool
verbose bool
configOnly bool
format string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "config only true with human readable output format",
args: args{
results: testInput,
configOnly: true,
format: "human",
},
wantErr: true,
},
{
name: "config only true with non human readable output format",
args: args{
results: testInput,
configOnly: true,
format: "json",
},
wantErr: false,
},
{
name: "config only false",
args: args{
results: testInput,
configOnly: false,
format: "human",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := writeResults(tt.args.results, tt.args.useColors, tt.args.verbose, tt.args.configOnly, tt.args.format); (err != nil) != tt.wantErr {
t.Errorf("writeResults() error = gotErr: %v, wantErr: %v", err, tt.wantErr)
}
})
}
}

func TestDownloadRemoteRepository(t *testing.T) {
testTempdir := filepath.Join(os.TempDir(), utils.GenRandomString(6))

type args struct {
remoteType string
remoteURL string
tempDir string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "blank input paramters",
args: args{
remoteType: "",
remoteURL: "",
tempDir: "",
},
},
{
name: "invalid input parameters",
args: args{
remoteType: "test",
remoteURL: "test",
tempDir: "test",
},
wantErr: true,
},
{
name: "valid inputs paramters",
args: args{
remoteType: "git",
remoteURL: "github.com/accurics/terrascan",
tempDir: testTempdir,
},
want: testTempdir,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := downloadRemoteRepository(tt.args.remoteType, tt.args.remoteURL, tt.args.tempDir)
if (err != nil) != tt.wantErr {
t.Errorf("downloadRemoteRepository() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("downloadRemoteRepository() = %v, want %v", got, tt.want)
}
})
}
}
6 changes: 5 additions & 1 deletion pkg/cli/scan.go
Expand Up @@ -60,6 +60,9 @@ var (
// UseColors indicates whether to use color output
UseColors bool
useColors string // used for flag processing

// Verbose indicates whether to display all fields in default human readlbe output
Verbose bool
)

var scanCmd = &cobra.Command{
Expand Down Expand Up @@ -100,7 +103,7 @@ Detect compliance and security violations across Infrastructure as Code to mitig
func scan(cmd *cobra.Command, args []string) {
zap.S().Debug("running terrascan in cli mode")
Run(IacType, IacVersion, PolicyType, IacFilePath, IacDirPath, ConfigFile,
PolicyPath, OutputType, RemoteType, RemoteURL, ConfigOnly, UseColors)
PolicyPath, OutputType, RemoteType, RemoteURL, ConfigOnly, UseColors, Verbose)
}

func init() {
Expand All @@ -115,5 +118,6 @@ func init() {
scanCmd.Flags().BoolVarP(&ConfigOnly, "config-only", "", false, "will output resource config (should only be used for debugging purposes)")
// flag passes a string, but we normalize to bool in PreRun
scanCmd.Flags().StringVar(&useColors, "use-colors", "auto", "color output (auto, t, f)")
scanCmd.Flags().BoolVarP(&Verbose, "verbose", "v", false, "will show violations with details (applicable for default output)")
RegisterCommand(rootCmd, scanCmd)
}
11 changes: 7 additions & 4 deletions pkg/policy/opa/engine.go
Expand Up @@ -303,16 +303,16 @@ func (e *Engine) reportViolation(regoData *RegoData, resource *output.ResourceCo

severity := regoData.Metadata.Severity
if strings.ToLower(severity) == "high" {
e.results.ViolationStore.Count.HighCount++
e.results.ViolationStore.Summary.HighCount++
} else if strings.ToLower(severity) == "medium" {
e.results.ViolationStore.Count.MediumCount++
e.results.ViolationStore.Summary.MediumCount++
} else if strings.ToLower(severity) == "low" {
e.results.ViolationStore.Count.LowCount++
e.results.ViolationStore.Summary.LowCount++
} else {
zap.S().Warn("invalid severity found in rule definition",
zap.String("rule id", violation.RuleID), zap.String("severity", severity))
}
e.results.ViolationStore.Count.TotalCount++
e.results.ViolationStore.Summary.ViolatedPolicies++

e.results.ViolationStore.AddResult(&violation)
}
Expand Down Expand Up @@ -390,5 +390,8 @@ func (e *Engine) Evaluate(engineInput policy.EngineInput) (policy.EngineOutput,
}

e.stats.runTime = time.Since(start)

// add the rule count of the policy engine to result summary
e.results.ViolationStore.Summary.TotalPolicies += e.stats.ruleCount
return e.results, nil
}
2 changes: 1 addition & 1 deletion pkg/policy/types.go
Expand Up @@ -33,6 +33,6 @@ func (me EngineOutput) AsViolationStore() results.ViolationStore {
}
return results.ViolationStore{
Violations: me.Violations,
Count: me.Count,
Summary: me.Summary,
}
}

0 comments on commit 7cc4f16

Please sign in to comment.