Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for colorized output #347

Merged
merged 17 commits into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/hashicorp/hcl/v2 v2.3.0
github.com/hashicorp/terraform v0.12.28
github.com/iancoleman/strcase v0.1.1
github.com/mattn/go-isatty v0.0.5
github.com/open-policy-agent/opa v0.22.0
github.com/pelletier/go-toml v1.8.0
github.com/pkg/errors v0.9.1
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
Expand Down
18 changes: 18 additions & 0 deletions pkg/cli/output_writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package cli

import (
"github.com/accurics/terrascan/pkg/termcolor"
"io"
"os"
)

// NewOutputWriter gets a new io.Writer based on os.Stdout.
// If param useColors=true, the writer will colorize the output
func NewOutputWriter(useColors bool) io.Writer {

// Color codes will corrupt output, so suppress if not on terminal
if useColors {
return termcolor.NewColorizedWriter(os.Stdout)
}
return os.Stdout
}
8 changes: 5 additions & 3 deletions pkg/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (

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

// create a new runtime executor for processing IaC
executor, err := runtime.NewExecutor(iacType, iacVersion, cloudType, iacFilePath,
Expand All @@ -41,10 +41,12 @@ func Run(iacType, iacVersion, cloudType, iacFilePath, iacDirPath, configFile,
return
}

outputWriter := NewOutputWriter(useColors)

if configOnly {
writer.Write(format, results.ResourceConfig, os.Stdout)
writer.Write(format, results.ResourceConfig, outputWriter)
} else {
writer.Write(format, results.Violations, os.Stdout)
writer.Write(format, results.Violations, outputWriter)
}

if results.Violations.ViolationStore.Count.TotalCount != 0 && flag.Lookup("test.v") == nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestRun(t *testing.T) {

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, "", "", tt.configOnly)
Run(tt.iacType, tt.iacVersion, tt.cloudType, tt.iacFilePath, tt.iacDirPath, tt.configFile, "", "", tt.configOnly, false)
})
}
}
31 changes: 30 additions & 1 deletion pkg/cli/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ package cli

import (
"fmt"
"os"
"strings"

iacProvider "github.com/accurics/terrascan/pkg/iac-providers"
"github.com/accurics/terrascan/pkg/policy"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
Expand All @@ -41,6 +43,9 @@ var (
IacDirPath string
//ConfigOnly will output resource config (should only be used for debugging purposes)
ConfigOnly bool
// UseColors indicates whether to use color output
UseColors bool
useColors string // used for flag processing
)

var scanCmd = &cobra.Command{
Expand All @@ -51,14 +56,36 @@ var scanCmd = &cobra.Command{
Detect compliance and security violations across Infrastructure as Code to mitigate risk before provisioning cloud native infrastructure.
`,
PreRun: func(cmd *cobra.Command, args []string) {
switch strings.ToLower(useColors) {
case "auto":
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
UseColors = true
} else {
UseColors = false
}

case "true":
fallthrough
case "t":
fallthrough
case "y":
fallthrough
case "1":
fallthrough
case "force":
UseColors = true

default:
UseColors = false
}
initial(cmd, args)
},
Run: scan,
}

func scan(cmd *cobra.Command, args []string) {
zap.S().Debug("running terrascan in cli mode")
Run(IacType, IacVersion, PolicyType, IacFilePath, IacDirPath, ConfigFile, PolicyPath, OutputType, ConfigOnly)
Run(IacType, IacVersion, PolicyType, IacFilePath, IacDirPath, ConfigFile, PolicyPath, OutputType, ConfigOnly, UseColors)
}

func init() {
Expand All @@ -69,6 +96,8 @@ func init() {
scanCmd.Flags().StringVarP(&IacDirPath, "iac-dir", "d", ".", "path to a directory containing one or more IaC files")
scanCmd.Flags().StringVarP(&PolicyPath, "policy-path", "p", "", "policy path directory")
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.MarkFlagRequired("policy-type")
RegisterCommand(rootCmd, scanCmd)
}
52 changes: 52 additions & 0 deletions pkg/termcolor/color_patterns.json.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[
{
"key-pattern":"description",
"value-pattern": ".*?",
"key-style": "",
"value-style": "Fg#0c0"
},
{
"key-pattern": "severity",
"value-style": "? HIGH = Fg#f00 ? MEDIUM = Fg#c84 ? LOW = Fg#cc0"
},
{
"key-pattern": "resource_name",
"value-style": "Fg#0ff | Bold"
},
{
"key-pattern": "resource_type",
"value-style": "Fg#0cc"
},
{
"key-pattern": "file",
"value-style": "Fg#fff | Bold"
},
{
"key-pattern": "low",
"value-pattern": "\\d+",
"key-style": "Fg#cc0",
"value-style": "Fg#cc0"
},
{
"key-pattern": "medium",
"value-pattern": "\\d+",
"key-style": "Fg#c84",
"value-style": "Fg#c84"
},
{
"key-pattern": "high",
"value-pattern": "\\d+",
"key-style": "Fg#f00",
"value-style": "Fg#f00"
},
{
"key-pattern": "count",
"value-pattern": "-",
"key-style": "Reverse"
},
{
"key-pattern": "rule_name",
"key-style": "Reverse",
"value-style": "Reverse"
}
]
142 changes: 142 additions & 0 deletions pkg/termcolor/colorpatterns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package termcolor

import (
"encoding/json"
"fmt"
"go.uber.org/zap"
"io/ioutil"
"os"
"regexp"
)

var (
// ColorPatterns contains the coloring rules
ColorPatterns map[*regexp.Regexp]FieldStyle
patternFile string

defaultValuePattern = `.*?`
)

// Style contains a style spec for termcolor.Colorize()
type Style string

// FieldStyle contains the styles for a particular output field
type FieldStyle struct {
KeyStyle Style
ValueStyle Style
}

// FieldSpec defines a key/value pattern that the color patterns look for.
type FieldSpec struct {
KeyPattern string
ValuePattern string
}

type colorPatternSerialized struct {
KeyStyle string `json:"key-style"`
ValueStyle string `json:"value-style"`
KeyPattern string `json:"key-pattern"`
ValuePattern string `json:"value-pattern"`
}

/* -------------------------------------------
* Patterns which define the output to color.
*
* The patterns are applied to the rendered output. Currently YAML
* and JSON are supported.
*
* The format is roughly:
* {<key-pattern>, <value-pattern>}: {<key-style>, <value-style>}
*
* Where <*-pattern> is a regexp and <*-style> is a style appropriate
* for Colorize()
**/

var defaultColorPatterns = map[FieldSpec]FieldStyle{
{"description", defaultValuePattern}: {"", "Fg#0c0"},
{"severity", defaultValuePattern}: {"", "?HIGH=Fg#f00?MEDIUM=Fg#c84?LOW=Fg#cc0"},
{"resource_name", defaultValuePattern}: {"", "Fg#0ff|Bold"},
{"resource_type", defaultValuePattern}: {"", "Fg#0cc"},
{"file", defaultValuePattern}: {"", "Fg#fff|Bold"},
{"low", `\d+`}: {"Fg#cc0", "Fg#cc0"},
{"medium", `\d+`}: {"Fg#c84", "Fg#c84"},
{"high", `\d+`}: {"Fg#f00", "Fg#f00"},

{"count", ""}: {"Bg#ccc|Fg#000", ""},
{"rule_name", defaultValuePattern}: {"Bg#ccc|Fg#000", ""},
}

func init() {
cf := os.Getenv("TERRASCAN_COLORS_FILE")
if len(cf) > 0 {
patternFile = cf
}
}

// GetColorPatterns loads the map used by the colorizer
func GetColorPatterns() map[*regexp.Regexp]FieldStyle {
var patterns map[FieldSpec]FieldStyle
var pdata []byte

if len(ColorPatterns) > 0 {
return ColorPatterns
}

if len(patternFile) > 0 {
var err error
pdata, err = ioutil.ReadFile(patternFile)
if err != nil {
zap.S().Warnf("Unable to read color patterns: %v", err)
zap.S().Warn("Will proceed with defaults")
}
}

if len(pdata) > 0 {
patterns = make(map[FieldSpec]FieldStyle)
var pd = make([]colorPatternSerialized, 0)

err := json.Unmarshal(pdata, &pd)
if err != nil {
zap.S().Warnf("Unable to process color patterns from %s: %v", patternFile, err)
zap.S().Warn("Will proceed with defaults")
patterns = defaultColorPatterns
}

for _, item := range pd {
fsp := FieldSpec{
KeyPattern: item.KeyPattern,
ValuePattern: item.ValuePattern,
}
fs := FieldStyle{
KeyStyle: Style(item.KeyStyle),
ValueStyle: Style(item.ValueStyle),
}

if len(fsp.ValuePattern) == 0 {
fsp.ValuePattern = defaultValuePattern
} else if fsp.ValuePattern == "-" {
fsp.ValuePattern = ""
}
patterns[fsp] = fs
}
} else {
patterns = defaultColorPatterns
}

ColorPatterns = make(map[*regexp.Regexp]FieldStyle, len(patterns))

/* Build the regexp needed for the different patterns */
for ptn, fmts := range patterns {
var rePtn string

/* rePtn should process a whole line and have 5 subgroups */
if len(ptn.ValuePattern) == 0 {
rePtn = fmt.Sprintf(`^([-\s]*"?)(%s)("?:\s*?)()(.*?)\s*$`, ptn.KeyPattern)
} else {
rePtn = fmt.Sprintf(`^([-\s]*"?)(%s)("?: "?)(%s)("?,?)\s*$`, ptn.KeyPattern, ptn.ValuePattern)
}
ColorPatterns[regexp.MustCompile("(?m)"+rePtn)] = fmts
}

return ColorPatterns
}
Loading