From 387bb0a94bcafb4650953d427da5e3b7cf8f48cf Mon Sep 17 00:00:00 2001 From: nikpivkin Date: Fri, 1 Nov 2024 17:15:07 +0600 Subject: [PATCH] feat(misconf): render causes for Terraform Signed-off-by: nikpivkin --- pkg/fanal/types/misconf.go | 20 ++++-- pkg/iac/scan/code.go | 6 +- pkg/iac/scan/flat.go | 2 + pkg/iac/scan/highlighting.go | 28 +++++--- pkg/iac/scan/result.go | 10 +++ .../scanners/terraform/executor/executor.go | 69 +++++++++++++++++++ pkg/iac/types/range.go | 8 +++ pkg/misconf/scanner.go | 10 ++- pkg/report/table/misconfig.go | 18 +++++ pkg/report/table/table_test.go | 3 +- pkg/scanner/local/scan.go | 15 ++-- 11 files changed, 158 insertions(+), 31 deletions(-) diff --git a/pkg/fanal/types/misconf.go b/pkg/fanal/types/misconf.go index 706220f0a1c7..70201e33fdbc 100644 --- a/pkg/fanal/types/misconf.go +++ b/pkg/fanal/types/misconf.go @@ -30,13 +30,14 @@ type MisconfResult struct { type MisconfResults []MisconfResult type CauseMetadata struct { - Resource string `json:",omitempty"` - Provider string `json:",omitempty"` - Service string `json:",omitempty"` - StartLine int `json:",omitempty"` - EndLine int `json:",omitempty"` - Code Code `json:",omitempty"` - Occurrences []Occurrence `json:",omitempty"` + Resource string `json:",omitempty"` + Provider string `json:",omitempty"` + Service string `json:",omitempty"` + StartLine int `json:",omitempty"` + EndLine int `json:",omitempty"` + Code Code `json:",omitempty"` + Occurrences []Occurrence `json:",omitempty"` + RenderedCause RenderedCause `json:",omitempty"` } type Occurrence struct { @@ -45,6 +46,11 @@ type Occurrence struct { Location Location } +type RenderedCause struct { + Raw string `json:",omitempty"` + Highlighted string `json:",omitempty"` +} + type Code struct { Lines []Line } diff --git a/pkg/iac/scan/code.go b/pkg/iac/scan/code.go index 8388dd4dbf6e..f1551a8ae8ac 100644 --- a/pkg/iac/scan/code.go +++ b/pkg/iac/scan/code.go @@ -77,7 +77,7 @@ func (c *Code) IsCauseMultiline() bool { } const ( - darkTheme = "solarized-dark256" + DarkTheme = "solarized-dark256" lightTheme = "github" ) @@ -89,7 +89,7 @@ type codeSettings struct { } var defaultCodeSettings = codeSettings{ - theme: darkTheme, + theme: DarkTheme, allowTruncation: true, maxLines: 10, includeHighlighted: true, @@ -105,7 +105,7 @@ func OptionCodeWithTheme(theme string) CodeOption { func OptionCodeWithDarkTheme() CodeOption { return func(s *codeSettings) { - s.theme = darkTheme + s.theme = DarkTheme } } diff --git a/pkg/iac/scan/flat.go b/pkg/iac/scan/flat.go index a3abc143d273..7b81e8284b47 100755 --- a/pkg/iac/scan/flat.go +++ b/pkg/iac/scan/flat.go @@ -23,6 +23,7 @@ type FlatResult struct { Resource string `json:"resource"` Occurrences []Occurrence `json:"occurrences,omitempty"` Location FlatRange `json:"location"` + RenderedCause RenderedCause `json:"rendered_cause"` } type FlatRange struct { @@ -70,5 +71,6 @@ func (r *Result) Flatten() FlatResult { StartLine: rng.GetStartLine(), EndLine: rng.GetEndLine(), }, + RenderedCause: r.renderedCause, } } diff --git a/pkg/iac/scan/highlighting.go b/pkg/iac/scan/highlighting.go index df260130d599..a92e4b9fc3b3 100644 --- a/pkg/iac/scan/highlighting.go +++ b/pkg/iac/scan/highlighting.go @@ -41,12 +41,17 @@ func highlight(fsKey, filename string, startLine, endLine int, input, theme stri return lines } - lexer := lexers.Match(filename) - if lexer == nil { - lexer = lexers.Fallback + highlighted, ok := Highlight(filename, input, theme) + if !ok { + return nil } - lexer = chroma.Coalesce(lexer) + lines := strings.Split(string(highlighted), "\n") + globalCache.Set(key, lines) + return lines +} + +func Highlight(filename string, input, theme string) (string, bool) { style := styles.Get(theme) if style == nil { style = styles.Fallback @@ -56,20 +61,23 @@ func highlight(fsKey, filename string, startLine, endLine int, input, theme stri formatter = formatters.Fallback } + lexer := lexers.Match(filename) + if lexer == nil { + lexer = lexers.Fallback + } + lexer = chroma.Coalesce(lexer) + iterator, err := lexer.Tokenise(nil, input) if err != nil { - return nil + return "", false } var buffer bytes.Buffer if err := formatter.Format(&buffer, style, iterator); err != nil { - return nil + return "", false } - raw := shiftANSIOverLineEndings(buffer.Bytes()) - lines := strings.Split(string(raw), "\n") - globalCache.Set(key, lines) - return lines + return string(shiftANSIOverLineEndings(buffer.Bytes())), true } func shiftANSIOverLineEndings(input []byte) []byte { diff --git a/pkg/iac/scan/result.go b/pkg/iac/scan/result.go index 3109ae00b641..0769d52c0800 100644 --- a/pkg/iac/scan/result.go +++ b/pkg/iac/scan/result.go @@ -32,6 +32,7 @@ type Result struct { warning bool traces []string fsPath string + renderedCause RenderedCause } func (r Result) RegoNamespace() string { @@ -105,6 +106,15 @@ func (r Result) Traces() []string { return r.traces } +type RenderedCause struct { + Raw string + Highlighted string +} + +func (r *Result) WithRenderedCause(cause RenderedCause) { + r.renderedCause = cause +} + func (r *Result) AbsolutePath(fsRoot string, metadata iacTypes.Metadata) string { if strings.HasSuffix(fsRoot, ":") { fsRoot += "/" diff --git a/pkg/iac/scanners/terraform/executor/executor.go b/pkg/iac/scanners/terraform/executor/executor.go index 8e14f778e5b4..cd075250b1d0 100644 --- a/pkg/iac/scanners/terraform/executor/executor.go +++ b/pkg/iac/scanners/terraform/executor/executor.go @@ -4,7 +4,9 @@ import ( "fmt" "runtime" "sort" + "strings" + "github.com/hashicorp/hcl/v2/hclwrite" "github.com/samber/lo" "github.com/zclconf/go-cty/cty" @@ -98,9 +100,76 @@ func (e *Executor) Execute(modules terraform.Modules) (scan.Results, error) { results = e.filterResults(results) e.sortResults(results) + for i, res := range results { + if res.Status() != scan.StatusFailed { + continue + } + + res.WithRenderedCause(renderCause(modules, res.Range())) + results[i] = res + } + return results, nil } +func renderCause(modules terraform.Modules, causeRng types.Range) scan.RenderedCause { + tfBlock := findBlockByRange(modules, causeRng) + if tfBlock == nil { + return scan.RenderedCause{} + } + + f := hclwrite.NewEmptyFile() + block := f.Body().AppendNewBlock(tfBlock.Type(), tfBlock.Labels()) + + if !writeBlock(tfBlock, block, causeRng) { + return scan.RenderedCause{} + } + + cause := string(hclwrite.Format(f.Bytes())) + cause = strings.TrimSuffix(string(cause), "\n") + highlighted, _ := scan.Highlight(causeRng.GetFilename(), cause, scan.DarkTheme) + return scan.RenderedCause{ + Raw: cause, + Highlighted: highlighted, + } +} + +func writeBlock(tfBlock *terraform.Block, block *hclwrite.Block, causeRng types.Range) bool { + var found bool + for _, attr := range tfBlock.Attributes() { + if !attr.GetMetadata().Range().Covers(causeRng) || attr.IsLiteral() { + continue + } + found = true + block.Body().SetAttributeValue(attr.Name(), attr.Value()) + } + + for _, childTfBlock := range tfBlock.AllBlocks() { + if !childTfBlock.GetMetadata().Range().Covers(causeRng) { + continue + } + childBlock := hclwrite.NewBlock(childTfBlock.Type(), nil) + + attrFound := writeBlock(childTfBlock, childBlock, causeRng) + if attrFound { + block.Body().AppendBlock(childBlock) + } + found = found || attrFound + } + + return found +} + +func findBlockByRange(modules terraform.Modules, causeRng types.Range) *terraform.Block { + for _, block := range modules.GetBlocks() { + blockRng := block.GetMetadata().Range() + if blockRng.GetFilename() == causeRng.GetFilename() && blockRng.Includes(causeRng) { + return block + } + } + return nil +} + func (e *Executor) filterResults(results scan.Results) scan.Results { if len(e.resultsFilters) > 0 && len(results) > 0 { before := len(results.GetIgnored()) diff --git a/pkg/iac/types/range.go b/pkg/iac/types/range.go index 754ee29c9675..06978af38462 100755 --- a/pkg/iac/types/range.go +++ b/pkg/iac/types/range.go @@ -153,3 +153,11 @@ func (r Range) Validate() error { } return nil } + +func (r Range) Includes(other Range) bool { + return r.startLine < other.startLine && r.endLine > other.endLine +} + +func (r Range) Covers(other Range) bool { + return r.startLine <= other.startLine && r.endLine >= other.endLine +} diff --git a/pkg/misconf/scanner.go b/pkg/misconf/scanner.go index 1aa2a5cd5c16..97a08274e09b 100644 --- a/pkg/misconf/scanner.go +++ b/pkg/misconf/scanner.go @@ -453,7 +453,7 @@ func ResultsToMisconf(configType types.ConfigType, scannerName string, results s ruleID = result.Rule().Aliases[0] } - cause := NewCauseWithCode(result) + cause := NewCauseWithCode(result, flattened) misconfResult := types.MisconfResult{ Namespace: result.RegoNamespace(), @@ -498,8 +498,7 @@ func ResultsToMisconf(configType types.ConfigType, scannerName string, results s return types.ToMisconfigurations(misconfs) } -func NewCauseWithCode(underlying scan.Result) types.CauseMetadata { - flat := underlying.Flatten() +func NewCauseWithCode(underlying scan.Result, flat scan.FlatResult) types.CauseMetadata { cause := types.CauseMetadata{ Resource: flat.Resource, Provider: flat.RuleProvider.DisplayName(), @@ -522,6 +521,11 @@ func NewCauseWithCode(underlying scan.Result) types.CauseMetadata { // failures can happen either due to lack of // OR misconfiguration of something if underlying.Status() == scan.StatusFailed { + cause.RenderedCause = types.RenderedCause{ + Raw: flat.RenderedCause.Raw, + Highlighted: flat.RenderedCause.Highlighted, + } + if code, err := underlying.GetCode(); err == nil { cause.Code = types.Code{ Lines: lo.Map(code.Lines, func(l scan.Line, i int) types.Line { diff --git a/pkg/report/table/misconfig.go b/pkg/report/table/misconfig.go index 41f111d883c9..3398f5062762 100644 --- a/pkg/report/table/misconfig.go +++ b/pkg/report/table/misconfig.go @@ -107,6 +107,7 @@ func (r *misconfigRenderer) printSingleDivider() { func (r *misconfigRenderer) renderSingle(misconf types.DetectedMisconfiguration) { r.renderSummary(misconf) r.renderCode(misconf) + r.renderCause(misconf) r.printf("\r\n\r\n") } @@ -211,6 +212,23 @@ func (r *misconfigRenderer) renderCode(misconf types.DetectedMisconfiguration) { } } +func (r *misconfigRenderer) renderCause(misconf types.DetectedMisconfiguration) { + cause := misconf.CauseMetadata.RenderedCause + if cause.Raw == "" { + return + } + + content := cause.Raw + if cause.Highlighted != "" { + content = cause.Highlighted + } + + r.printf("%s\r\n", "Rendered cause:") + r.printSingleDivider() + r.println(content) + r.printSingleDivider() +} + func (r *misconfigRenderer) outputTrace() { blue := color.New(color.FgBlue).SprintFunc() green := color.New(color.FgGreen).SprintfFunc() diff --git a/pkg/report/table/table_test.go b/pkg/report/table/table_test.go index d52dda0dc232..d15866621c98 100644 --- a/pkg/report/table/table_test.go +++ b/pkg/report/table/table_test.go @@ -2,6 +2,7 @@ package table_test import ( "bytes" + "context" "testing" "github.com/stretchr/testify/assert" @@ -86,7 +87,7 @@ Total: 1 (MEDIUM: 0, HIGH: 1) dbTypes.SeverityMedium, }, } - err := writer.Write(nil, types.Report{Results: tc.results}) + err := writer.Write(context.TODO(), types.Report{Results: tc.results}) require.NoError(t, err) assert.Equal(t, tc.expectedOutput, tableWritten.String(), tc.name) }) diff --git a/pkg/scanner/local/scan.go b/pkg/scanner/local/scan.go index 58cd4cc00167..b938e8b0c0a1 100644 --- a/pkg/scanner/local/scan.go +++ b/pkg/scanner/local/scan.go @@ -363,13 +363,14 @@ func toDetectedMisconfiguration(res ftypes.MisconfResult, defaultSeverity dbType Layer: layer, Traces: res.Traces, CauseMetadata: ftypes.CauseMetadata{ - Resource: res.Resource, - Provider: res.Provider, - Service: res.Service, - StartLine: res.StartLine, - EndLine: res.EndLine, - Code: res.Code, - Occurrences: res.Occurrences, + Resource: res.Resource, + Provider: res.Provider, + Service: res.Service, + StartLine: res.StartLine, + EndLine: res.EndLine, + Code: res.Code, + Occurrences: res.Occurrences, + RenderedCause: res.RenderedCause, }, } }