From 8b274bede48ee4fc921c653bee7905cad0ee6480 Mon Sep 17 00:00:00 2001 From: Jon Jarboe Date: Thu, 24 Sep 2020 15:34:16 -0700 Subject: [PATCH 1/5] add support for colored output --- pkg/cli/run.go | 38 ++++++- pkg/cli/run_test.go | 2 +- pkg/cli/scan.go | 5 +- pkg/termcolor/termcolor.go | 185 ++++++++++++++++++++++++++++++++ pkg/termcolor/termcolor_test.go | 14 +++ pkg/termcolor/writer.go | 79 ++++++++++++++ pkg/termcolor/writer_test.go | 115 ++++++++++++++++++++ 7 files changed, 433 insertions(+), 5 deletions(-) create mode 100644 pkg/termcolor/termcolor.go create mode 100644 pkg/termcolor/termcolor_test.go create mode 100644 pkg/termcolor/writer.go create mode 100644 pkg/termcolor/writer_test.go diff --git a/pkg/cli/run.go b/pkg/cli/run.go index ccd76602a..b0885db35 100644 --- a/pkg/cli/run.go +++ b/pkg/cli/run.go @@ -19,14 +19,19 @@ package cli import ( "flag" "os" + "io" + "strings" + + "github.com/mattn/go-isatty" "github.com/accurics/terrascan/pkg/runtime" "github.com/accurics/terrascan/pkg/writer" + "github.com/accurics/terrascan/pkg/termcolor" ) // 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 string) { // create a new runtime executor for processing IaC executor, err := runtime.NewExecutor(iacType, iacVersion, cloudType, iacFilePath, @@ -41,10 +46,37 @@ func Run(iacType, iacVersion, cloudType, iacFilePath, iacDirPath, configFile, return } + // Color codes will corrupt output, so suppress if not on terminal + switch strings.ToLower(useColors) { + case "auto": + if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) { + useColors = "true" + } else { + useColors = "false" + } + + case "true": // nothing to do + case "t": fallthrough + case "y": fallthrough + case "1": fallthrough + case "force": + useColors = "true" + + default: + useColors = "false" + } + + var outputWriter io.Writer + if useColors == "true" { + outputWriter = termcolor.NewColorizedWriter(os.Stdout) + } else { + outputWriter = os.Stdout + } + 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 { diff --git a/pkg/cli/run_test.go b/pkg/cli/run_test.go index 6c9ecfe81..aafc572b5 100644 --- a/pkg/cli/run_test.go +++ b/pkg/cli/run_test.go @@ -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, "auto") }) } } diff --git a/pkg/cli/scan.go b/pkg/cli/scan.go index 3177fafdf..e5142e852 100644 --- a/pkg/cli/scan.go +++ b/pkg/cli/scan.go @@ -41,6 +41,8 @@ var ( IacDirPath string //ConfigOnly will output resource config (should only be used for debugging purposes) ConfigOnly bool + // UseColors enables color output (t, f, auto) + UseColors string ) var scanCmd = &cobra.Command{ @@ -58,7 +60,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, ConfigOnly) + Run(IacType, IacVersion, PolicyType, IacFilePath, IacDirPath, ConfigFile, PolicyPath, OutputType, ConfigOnly, UseColors) } func init() { @@ -69,6 +71,7 @@ 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)") + scanCmd.Flags().StringVar(&UseColors, "use-colors", "auto", "color output (auto, t, f)") scanCmd.MarkFlagRequired("policy-type") RegisterCommand(rootCmd, scanCmd) } diff --git a/pkg/termcolor/termcolor.go b/pkg/termcolor/termcolor.go new file mode 100644 index 000000000..d0d8fcd1b --- /dev/null +++ b/pkg/termcolor/termcolor.go @@ -0,0 +1,185 @@ +package termcolor + +import ( + "strconv" + "math" + "strings" + "fmt" +) + +var ( + // ANSI terminal control codes + ColorPrefix = "\u001b[" + ColorSuffix = "m" + + Reset = ColorPrefix+"0"+ColorSuffix + + Bold = "1" // Bold effect applies to text only (not background) + Underline = "4" // Underline text + Reverse = "7" // Use reverse text (swap foreground and background) +) + +// ANSI color code for color "hex", applies to foreground +func Fg(hex string) string { + return "38;5;" + strconv.Itoa(int(HexToColor256(hex))) +} + +// ANSI color code for color "hex", applies to background +func Bg(hex string) string { + return "48;5;" + strconv.Itoa(int(HexToColor256(hex))) +} + +// Convert input "hex" into an ANSI color code (xterm-256) +// hex may be a set of 3 or 6 hexadecimal digits for RGB values +func HexToColor256(hex string) uint8 { + return RgbToColor256(HexToRgb(hex)) +} + +// Convert the red, green, blue tuple into an ANSI color code (xterm-256) +func RgbToColor256(red, green, blue uint8) uint8 { + // red, green, blue range 0-255 on input + + if red == green && red == blue { + // Grayscale + if red == 255 { + // Bright white + return 15 + } else if red == 0 { + // Black + return 0 + } + return 232 + uint8(math.Round(float64(red)/10.65)) + } else { + return (36 * ColorToAnsiIndex(red) + + 6 * ColorToAnsiIndex(green) + + ColorToAnsiIndex(blue) ) + 16 + } +} + +// Converts a uint8 color value (0-255) into an ANSI color value (0-5) +func ColorToAnsiIndex(c uint8) uint8 { + return uint8(math.Round(float64(c)/51.0)) +} + +// Convert a 3 or 6 digit hexadecimal string, representing RGB values, +// into separate R,G,B values +func HexToRgb(hex string) (r,g,b uint8) { + switch len(hex) { + case 6: + r = HexToUint8(hex[:2]) + g = HexToUint8(hex[2:4]) + b = HexToUint8(hex[4:]) + case 3: + r = HexToUint8(hex[ :1] + hex[ :1]) + g = HexToUint8(hex[1:2] + hex[1:2]) + b = HexToUint8(hex[2:3] + hex[2:3]) + default: + panic(fmt.Sprintf("Unsupported color %s", hex)) + } + return +} + +func HexToUint8(hexbyte string) uint8 { + val,_ := strconv.ParseUint(hexbyte, 16, 8) + return uint8(val) +} + +// Expands a style string into an ANSI control code +func ExpandStyle(style string) string { + switch { + case strings.HasPrefix(style, "Fg#"): + fgstyle := style[3:] + return Fg(fgstyle) + case strings.HasPrefix(style, "Bg#"): + bgstyle := style[3:] + return Bg(bgstyle) + case style == "Bold": + return Bold + case style == "Underline": + return Underline + case style == "Reverse": + return Reverse + default: + panic(fmt.Sprintf("Unhandled style [%s]", style)) + } + return style +} + +/* + * Colorize "message" with "style". + * + * Style may contain multiple conditions, delimited by "?". Such + * conditional parts are called "clauses". + * + * Clauses may contain an "=", in which case the part before the "=" + * is the pattern which "message" must match, and the part after the + * "=" is the style to use. + * + * If no "?" is present, then there is no pattern or clause; all of + * "message" will be output in the specified "style". + * + * The style is formatted like "part[|part[|..]].". + * + * Each Part may be a color specification or an effect specification. + * + * Color specifications look like "Fg#rgb" or "Bg#rgb". Fg applies + * color rgb to the foreground; Gb to the background. Rgb consists of + * red, green, and blur components in hexadecimal format. Rgb may be + * 3 or 6 digits long, consisting of 1 or 2 digits for r, g, and b. + * Effects are listed above, such as Bold or Underline. + * + * Examples: + * Fg#fff changes the foreground color to white + * Fg#ffff00|Bold changes the foreground color to bold yellow + * ?Y=Fg#0f0|Bold?N=Fg#f00 uses a green foreground if the message + * matches "Y", or red if it matches "N" +**/ +func Colorize(style, message string) string { + var sb strings.Builder + + if len(message) == 0 { + return message + } + + for _,clause := range strings.Split(style, "?") { + // ignore whitespace + clause = strings.TrimSpace(clause) + + // Skip if there is an empty clause + if len(clause) == 0 { + continue + } + + /* If we need to match a specific pattern, skip any patterns + * that don't match. + **/ + if strings.Contains(clause, "=") { + pattern := strings.TrimSpace(clause[:strings.Index(clause,"=")]) + style = strings.TrimSpace(clause[len(pattern)+1:]) + + if pattern != message { + style = "" + continue + } + break + } + } + + if len(style) == 0 { + return message + } + + parts := make([]string,0) + + sb.WriteString(ColorPrefix) + for _,s := range strings.Split(style, "|") { + parts = append( parts, ExpandStyle(strings.TrimSpace(s))) + } + sb.WriteString(strings.Join(parts,";")) + sb.WriteString(ColorSuffix) + sb.WriteString(message) + sb.WriteString(Reset) + + return sb.String() +} + diff --git a/pkg/termcolor/termcolor_test.go b/pkg/termcolor/termcolor_test.go new file mode 100644 index 000000000..0c429a005 --- /dev/null +++ b/pkg/termcolor/termcolor_test.go @@ -0,0 +1,14 @@ +package termcolor + +import ( + "testing" + "strings" +) + +func TestColoring(t *testing.T) { + s := "FORCED FAILURE" + s = Colorize("Fg#fff|Bg#f00|Bold", s) + if !strings.HasPrefix(s, ColorPrefix) { + t.Errorf("expected console escape code %v, got %v", []byte(ColorPrefix), []byte(s[:len(ColorPrefix)])) + } +} diff --git a/pkg/termcolor/writer.go b/pkg/termcolor/writer.go new file mode 100644 index 000000000..347adc2d6 --- /dev/null +++ b/pkg/termcolor/writer.go @@ -0,0 +1,79 @@ +package termcolor + +import ( + "io" + "fmt" + "regexp" +) + +/* ------------------------------------------- + * 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: + * {, }: {, } + * + * Where <*-pattern> is a regexp and <*-style> is a style appropriate + * for Colorize() +**/ +var color_patterns = map[struct { key,value string}][2]string { + {"description",".*?"}: {"", "Fg#0c0"}, + {"severity",".*?"}: {"", "?HIGH=Fg#f00?MEDIUM=Fg#c84?LOW=Fg#cc0"}, + {"resource_name",".*?"}: {"", "Fg#0ff|Bold"}, + {"resource_type",".*?"}: {"", "Fg#0cc"}, + {"file",".*?"}: {"", "Fg#fff|Bold"}, + {"low",".*?"}: {"Fg#cc0", "Fg#cc0"}, + {"medium",".*?"}: {"Fg#c84", "Fg#c84"}, + {"high",".*?"}: {"Fg#f00", "Fg#f00"}, + + {"count",""}: {"Bg#ccc|Fg#000", ""}, + {"rule_name",".*?"}: {"Bg#ccc|Fg#000", ""}, +} + +var ( + patterns map[*regexp.Regexp][2]string +) + +type ColorizedWriter struct { + writer io.Writer +} + +func NewColorizedWriter(w io.Writer) ColorizedWriter { + return ColorizedWriter{w} +} + +func init() { + patterns = make(map[*regexp.Regexp][2]string, len(color_patterns)) + + /* Build the regexp needed for the different patterns */ + for ptn,fmts := range color_patterns { + var rePtn string + + /* rePtn should process a whole line and have 5 subgroups */ + if len(ptn.value) == 0 { + rePtn = fmt.Sprintf(`^([-\s]*"?)(%s)("?:\s*?)()(.*?)\s*$`, ptn.key) + } else { + rePtn = fmt.Sprintf(`^([-\s]*"?)(%s)("?: "?)(%s)("?,?)\s*$`, ptn.key, ptn.value) + } + patterns[regexp.MustCompile("(?m)"+rePtn)] = fmts + } +} + +func (me ColorizedWriter) Write(p []byte) (n int, err error) { + /* Before output is written, perform color substitutions */ + for ptn,style := range patterns { + p = ptn.ReplaceAllFunc(p, func(matched []byte) []byte { + groups := ptn.FindStringSubmatch(string(matched)) + return []byte(fmt.Sprintf( "%s%s%s%s%s", + groups[1], + Colorize(style[0], string(groups[2])), + groups[3], + Colorize(style[1], string(groups[4])), + groups[5] )) + }); + } + + return me.writer.Write( p ) +} diff --git a/pkg/termcolor/writer_test.go b/pkg/termcolor/writer_test.go new file mode 100644 index 000000000..f476b0ceb --- /dev/null +++ b/pkg/termcolor/writer_test.go @@ -0,0 +1,115 @@ +package termcolor + +import ( + "testing" + "strings" + "regexp" + + "github.com/accurics/terrascan/pkg/writer" + "github.com/accurics/terrascan/pkg/results" +) + +var ( + jsonData, yamlData strings.Builder +) + +func buildStore() *results.ViolationStore { + res := results.NewViolationStore() + + res.AddResult(&results.Violation{ + RuleName: "rule name", + Description: "description", + RuleID: "rule id", + Severity: "severity", + Category: "category", + ResourceName: "resource name", + ResourceType: "resource type", + File: "file", + LineNumber: 1, + }) + + return res +} + +func init() { + res := buildStore() + + w := NewColorizedWriter(&jsonData) + + err := writer.Write("json", res, w) + if err != nil { + panic(err) + } + + w = NewColorizedWriter(&yamlData) + + err = writer.Write("yaml", res, w) + if err != nil { + panic(err) + } +} + +func verifyLineWithStringIsColorized(s string, buf string, t *testing.T) { + re := regexp.MustCompile(`(?m)^(.*`+s+`.*)$`) + m := re.FindString(buf) + if !strings.Contains(m, ColorPrefix) { + t.Errorf("%s not colorized [%v]\n%s", s, m, buf) + } +} + +func TestYAMLBogusSeverityIsNotColorized(t *testing.T) { + re := regexp.MustCompile(`(?m)^(.*severity.*)$`) + m := re.FindString(yamlData.String()) + if strings.Contains(m, ColorPrefix) { + t.Errorf("severity is colorized [%v]\n%s", m, yamlData.String()) + } +} + +func TestYAMLRuleNameIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("rule_name", yamlData.String(), t) +} +func TestYAMLDescriptionIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("description", yamlData.String(), t) +} +func TestYAMLSeverityIsColorized(t *testing.T) { + + res := buildStore() + yw := &strings.Builder{} + w := NewColorizedWriter(yw) + + // HIGH, MEDIUM, LOW + testSeverity := func(sev string) { + res.Violations[0].Severity = sev + yw.Reset() + err := writer.Write("yaml", res, w) + if err != nil { + panic(err) + } + verifyLineWithStringIsColorized("severity", yw.String(), t) + } + testSeverity("HIGH") + testSeverity("MEDIUM") + testSeverity("LOW") +} +func TestYAMLResourceNameIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("resource_name", yamlData.String(), t) +} +func TestYAMLResourceTypeIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("resource_type", yamlData.String(), t) +} +func TestYAMLFileIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("file", yamlData.String(), t) +} + +func TestYAMLCountIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("count", yamlData.String(), t) +} +func TestYAMLCountLowIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("low", yamlData.String(), t) +} +func TestYAMLCountMediumIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("medium", yamlData.String(), t) +} +func TestYAMLCountHighIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("high", yamlData.String(), t) +} From 388a93bc989f52342d9c093da48afae1e9ceb38f Mon Sep 17 00:00:00 2001 From: Jon Jarboe Date: Thu, 24 Sep 2020 15:39:15 -0700 Subject: [PATCH 2/5] update module files --- go.mod | 1 + go.sum | 1 + 2 files changed, 2 insertions(+) diff --git a/go.mod b/go.mod index 499c94dce..fcbf4b37d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 69a15dfe4..e33f3c00a 100644 --- a/go.sum +++ b/go.sum @@ -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= From bf715f37f6bb1679924eeabf40169747680dcdc1 Mon Sep 17 00:00:00 2001 From: Jon Jarboe Date: Thu, 24 Sep 2020 16:33:05 -0700 Subject: [PATCH 3/5] add JSON termcolor tests --- pkg/termcolor/writer_test.go | 61 ++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/pkg/termcolor/writer_test.go b/pkg/termcolor/writer_test.go index f476b0ceb..f8996a939 100644 --- a/pkg/termcolor/writer_test.go +++ b/pkg/termcolor/writer_test.go @@ -57,6 +57,8 @@ func verifyLineWithStringIsColorized(s string, buf string, t *testing.T) { } } +/////////// YAML + func TestYAMLBogusSeverityIsNotColorized(t *testing.T) { re := regexp.MustCompile(`(?m)^(.*severity.*)$`) m := re.FindString(yamlData.String()) @@ -113,3 +115,62 @@ func TestYAMLCountMediumIsColorized(t *testing.T) { func TestYAMLCountHighIsColorized(t *testing.T) { verifyLineWithStringIsColorized("high", yamlData.String(), t) } + +/////////// JSON + +func TestJSONBogusSeverityIsNotColorized(t *testing.T) { + re := regexp.MustCompile(`(?m)^(.*severity.*)$`) + m := re.FindString(jsonData.String()) + if strings.Contains(m, ColorPrefix) { + t.Errorf("severity is colorized [%v]\n%s", m, jsonData.String()) + } +} + +func TestJSONRuleNameIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("rule_name", jsonData.String(), t) +} +func TestJSONDescriptionIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("description", jsonData.String(), t) +} +func TestJSONSeverityIsColorized(t *testing.T) { + + res := buildStore() + yw := &strings.Builder{} + w := NewColorizedWriter(yw) + + // HIGH, MEDIUM, LOW + testSeverity := func(sev string) { + res.Violations[0].Severity = sev + yw.Reset() + err := writer.Write("json", res, w) + if err != nil { + panic(err) + } + verifyLineWithStringIsColorized("severity", yw.String(), t) + } + testSeverity("HIGH") + testSeverity("MEDIUM") + testSeverity("LOW") +} +func TestJSONResourceNameIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("resource_name", jsonData.String(), t) +} +func TestJSONResourceTypeIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("resource_type", jsonData.String(), t) +} +func TestJSONFileIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("file", jsonData.String(), t) +} + +func TestJSONCountIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("count", jsonData.String(), t) +} +func TestJSONCountLowIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("low", jsonData.String(), t) +} +func TestJSONCountMediumIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("medium", jsonData.String(), t) +} +func TestJSONCountHighIsColorized(t *testing.T) { + verifyLineWithStringIsColorized("high", jsonData.String(), t) +} From 2a0e3acaae8e971afb9ba196255db0fe80dd5324 Mon Sep 17 00:00:00 2001 From: Jon Jarboe Date: Thu, 24 Sep 2020 17:30:02 -0700 Subject: [PATCH 4/5] clean up flag handling --- pkg/cli/output_writer.go | 16 ++++++++++++++++ pkg/cli/run.go | 34 ++-------------------------------- pkg/cli/scan.go | 28 +++++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 35 deletions(-) create mode 100644 pkg/cli/output_writer.go diff --git a/pkg/cli/output_writer.go b/pkg/cli/output_writer.go new file mode 100644 index 000000000..9b07e3845 --- /dev/null +++ b/pkg/cli/output_writer.go @@ -0,0 +1,16 @@ +package cli + +import ( + "io" + "os" + "github.com/accurics/terrascan/pkg/termcolor" +) + +func NewOutputWriter(useColors bool) io.Writer { + + // Color codes will corrupt output, so suppress if not on terminal + if useColors == true { + return termcolor.NewColorizedWriter(os.Stdout) + } + return os.Stdout +} diff --git a/pkg/cli/run.go b/pkg/cli/run.go index b0885db35..4b247571b 100644 --- a/pkg/cli/run.go +++ b/pkg/cli/run.go @@ -19,19 +19,14 @@ package cli import ( "flag" "os" - "io" - "strings" - - "github.com/mattn/go-isatty" "github.com/accurics/terrascan/pkg/runtime" "github.com/accurics/terrascan/pkg/writer" - "github.com/accurics/terrascan/pkg/termcolor" ) // Run executes terrascan in CLI mode func Run(iacType, iacVersion, cloudType, iacFilePath, iacDirPath, configFile, - policyPath, format string, configOnly bool, useColors string) { + policyPath, format string, configOnly bool, useColors bool) { // create a new runtime executor for processing IaC executor, err := runtime.NewExecutor(iacType, iacVersion, cloudType, iacFilePath, @@ -46,32 +41,7 @@ func Run(iacType, iacVersion, cloudType, iacFilePath, iacDirPath, configFile, return } - // Color codes will corrupt output, so suppress if not on terminal - switch strings.ToLower(useColors) { - case "auto": - if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) { - useColors = "true" - } else { - useColors = "false" - } - - case "true": // nothing to do - case "t": fallthrough - case "y": fallthrough - case "1": fallthrough - case "force": - useColors = "true" - - default: - useColors = "false" - } - - var outputWriter io.Writer - if useColors == "true" { - outputWriter = termcolor.NewColorizedWriter(os.Stdout) - } else { - outputWriter = os.Stdout - } + outputWriter := NewOutputWriter(useColors) if configOnly { writer.Write(format, results.ResourceConfig, outputWriter) diff --git a/pkg/cli/scan.go b/pkg/cli/scan.go index e5142e852..7050f17f0 100644 --- a/pkg/cli/scan.go +++ b/pkg/cli/scan.go @@ -17,6 +17,7 @@ package cli import ( + "os" "fmt" "strings" @@ -24,6 +25,7 @@ import ( "github.com/accurics/terrascan/pkg/policy" "github.com/spf13/cobra" "go.uber.org/zap" + "github.com/mattn/go-isatty" ) var ( @@ -41,8 +43,9 @@ var ( IacDirPath string //ConfigOnly will output resource config (should only be used for debugging purposes) ConfigOnly bool - // UseColors enables color output (t, f, auto) - UseColors string + // UseColors indicates whether to use color output + UseColors bool + useColors string // used for flag processing ) var scanCmd = &cobra.Command{ @@ -53,6 +56,24 @@ 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, @@ -71,7 +92,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)") - scanCmd.Flags().StringVar(&UseColors, "use-colors", "auto", "color output (auto, t, f)") + // 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) } From 2eed6bc4ddf54cdf7c8ec9e5d221c78dfb1ba6b5 Mon Sep 17 00:00:00 2001 From: Jon Jarboe Date: Thu, 24 Sep 2020 22:26:46 -0700 Subject: [PATCH 5/5] refactor styling to support styles file --- pkg/termcolor/color_patterns.json.sample | 52 +++++++++ pkg/termcolor/colorpatterns.go | 135 +++++++++++++++++++++++ pkg/termcolor/termcolor.go | 16 ++- pkg/termcolor/writer.go | 54 +-------- 4 files changed, 200 insertions(+), 57 deletions(-) create mode 100644 pkg/termcolor/color_patterns.json.sample create mode 100644 pkg/termcolor/colorpatterns.go diff --git a/pkg/termcolor/color_patterns.json.sample b/pkg/termcolor/color_patterns.json.sample new file mode 100644 index 000000000..d6c93f228 --- /dev/null +++ b/pkg/termcolor/color_patterns.json.sample @@ -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" + } +] diff --git a/pkg/termcolor/colorpatterns.go b/pkg/termcolor/colorpatterns.go new file mode 100644 index 000000000..66aa72fec --- /dev/null +++ b/pkg/termcolor/colorpatterns.go @@ -0,0 +1,135 @@ +package termcolor + +import ( + "os" + "fmt" + "regexp" + "encoding/json" + "io/ioutil" + "go.uber.org/zap" +) + +var ( + ColorPatterns map[*regexp.Regexp]FieldStyle + pattern_file string + + defaultValuePattern = `.*?` +) + +type Style string + +type FieldStyle struct { + KeyStyle Style + ValueStyle Style +} +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: + * {, }: {, } + * + * Where <*-pattern> is a regexp and <*-style> is a style appropriate + * for Colorize() +**/ + +var default_color_patterns = 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 { + pattern_file = cf + } +} + +func GetColorPatterns() map[*regexp.Regexp]FieldStyle { + var patterns map[FieldSpec]FieldStyle + var pdata []byte + + if len(ColorPatterns) > 0 { + return ColorPatterns + } + + if len(pattern_file) > 0 { + var err error + pdata,err = ioutil.ReadFile(pattern_file) + 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", pattern_file, err) + zap.S().Warn("Will proceed with defaults") + patterns = default_color_patterns + } + + 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 = default_color_patterns + } + + 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 +} diff --git a/pkg/termcolor/termcolor.go b/pkg/termcolor/termcolor.go index d0d8fcd1b..78f71e891 100644 --- a/pkg/termcolor/termcolor.go +++ b/pkg/termcolor/termcolor.go @@ -4,7 +4,7 @@ import ( "strconv" "math" "strings" - "fmt" + "go.uber.org/zap" ) var ( @@ -74,7 +74,8 @@ func HexToRgb(hex string) (r,g,b uint8) { g = HexToUint8(hex[1:2] + hex[1:2]) b = HexToUint8(hex[2:3] + hex[2:3]) default: - panic(fmt.Sprintf("Unsupported color %s", hex)) + zap.S().Errorf("Unsupported color %s", hex) + return uint8(255),uint8(255),uint8(255) } return } @@ -100,7 +101,7 @@ func ExpandStyle(style string) string { case style == "Reverse": return Reverse default: - panic(fmt.Sprintf("Unhandled style [%s]", style)) + zap.S().Warnf("Unhandled style [%s]", style) } return style } @@ -134,9 +135,11 @@ func ExpandStyle(style string) string { * ?Y=Fg#0f0|Bold?N=Fg#f00 uses a green foreground if the message * matches "Y", or red if it matches "N" **/ -func Colorize(style, message string) string { +func Colorize(st Style, message string) string { var sb strings.Builder + style := string(st) + if len(message) == 0 { return message } @@ -154,8 +157,9 @@ func Colorize(style, message string) string { * that don't match. **/ if strings.Contains(clause, "=") { - pattern := strings.TrimSpace(clause[:strings.Index(clause,"=")]) - style = strings.TrimSpace(clause[len(pattern)+1:]) + eq := strings.Index(clause,"=") + pattern := strings.TrimSpace(clause[:eq]) + style = strings.TrimSpace(clause[eq+1:]) if pattern != message { style = "" diff --git a/pkg/termcolor/writer.go b/pkg/termcolor/writer.go index 347adc2d6..153144745 100644 --- a/pkg/termcolor/writer.go +++ b/pkg/termcolor/writer.go @@ -3,37 +3,6 @@ package termcolor import ( "io" "fmt" - "regexp" -) - -/* ------------------------------------------- - * 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: - * {, }: {, } - * - * Where <*-pattern> is a regexp and <*-style> is a style appropriate - * for Colorize() -**/ -var color_patterns = map[struct { key,value string}][2]string { - {"description",".*?"}: {"", "Fg#0c0"}, - {"severity",".*?"}: {"", "?HIGH=Fg#f00?MEDIUM=Fg#c84?LOW=Fg#cc0"}, - {"resource_name",".*?"}: {"", "Fg#0ff|Bold"}, - {"resource_type",".*?"}: {"", "Fg#0cc"}, - {"file",".*?"}: {"", "Fg#fff|Bold"}, - {"low",".*?"}: {"Fg#cc0", "Fg#cc0"}, - {"medium",".*?"}: {"Fg#c84", "Fg#c84"}, - {"high",".*?"}: {"Fg#f00", "Fg#f00"}, - - {"count",""}: {"Bg#ccc|Fg#000", ""}, - {"rule_name",".*?"}: {"Bg#ccc|Fg#000", ""}, -} - -var ( - patterns map[*regexp.Regexp][2]string ) type ColorizedWriter struct { @@ -44,33 +13,16 @@ func NewColorizedWriter(w io.Writer) ColorizedWriter { return ColorizedWriter{w} } -func init() { - patterns = make(map[*regexp.Regexp][2]string, len(color_patterns)) - - /* Build the regexp needed for the different patterns */ - for ptn,fmts := range color_patterns { - var rePtn string - - /* rePtn should process a whole line and have 5 subgroups */ - if len(ptn.value) == 0 { - rePtn = fmt.Sprintf(`^([-\s]*"?)(%s)("?:\s*?)()(.*?)\s*$`, ptn.key) - } else { - rePtn = fmt.Sprintf(`^([-\s]*"?)(%s)("?: "?)(%s)("?,?)\s*$`, ptn.key, ptn.value) - } - patterns[regexp.MustCompile("(?m)"+rePtn)] = fmts - } -} - func (me ColorizedWriter) Write(p []byte) (n int, err error) { /* Before output is written, perform color substitutions */ - for ptn,style := range patterns { + for ptn,style := range GetColorPatterns() { p = ptn.ReplaceAllFunc(p, func(matched []byte) []byte { groups := ptn.FindStringSubmatch(string(matched)) return []byte(fmt.Sprintf( "%s%s%s%s%s", groups[1], - Colorize(style[0], string(groups[2])), + Colorize(style.KeyStyle, string(groups[2])), groups[3], - Colorize(style[1], string(groups[4])), + Colorize(style.ValueStyle, string(groups[4])), groups[5] )) }); }