Skip to content

Commit

Permalink
feat(misconf): render causes for Terraform
Browse files Browse the repository at this point in the history
Signed-off-by: nikpivkin <[email protected]>
  • Loading branch information
nikpivkin committed Nov 1, 2024
1 parent efec326 commit 387bb0a
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 31 deletions.
20 changes: 13 additions & 7 deletions pkg/fanal/types/misconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/iac/scan/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (c *Code) IsCauseMultiline() bool {
}

const (
darkTheme = "solarized-dark256"
DarkTheme = "solarized-dark256"
lightTheme = "github"
)

Expand All @@ -89,7 +89,7 @@ type codeSettings struct {
}

var defaultCodeSettings = codeSettings{
theme: darkTheme,
theme: DarkTheme,
allowTruncation: true,
maxLines: 10,
includeHighlighted: true,
Expand All @@ -105,7 +105,7 @@ func OptionCodeWithTheme(theme string) CodeOption {

func OptionCodeWithDarkTheme() CodeOption {
return func(s *codeSettings) {
s.theme = darkTheme
s.theme = DarkTheme
}
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/iac/scan/flat.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -70,5 +71,6 @@ func (r *Result) Flatten() FlatResult {
StartLine: rng.GetStartLine(),
EndLine: rng.GetEndLine(),
},
RenderedCause: r.renderedCause,
}
}
28 changes: 18 additions & 10 deletions pkg/iac/scan/highlighting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Check failure on line 49 in pkg/iac/scan/highlighting.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

unnecessary conversion (unconvert)
globalCache.Set(key, lines)
return lines
}

func Highlight(filename string, input, theme string) (string, bool) {

Check failure on line 54 in pkg/iac/scan/highlighting.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

paramTypeCombine: func(filename string, input, theme string) (string, bool) could be replaced with func(filename, input, theme string) (string, bool) (gocritic)
style := styles.Get(theme)
if style == nil {
style = styles.Fallback
Expand All @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions pkg/iac/scan/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Result struct {
warning bool
traces []string
fsPath string
renderedCause RenderedCause
}

func (r Result) RegoNamespace() string {
Expand Down Expand Up @@ -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 += "/"
Expand Down
69 changes: 69 additions & 0 deletions pkg/iac/scanners/terraform/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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")

Check failure on line 129 in pkg/iac/scanners/terraform/executor/executor.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

unnecessary conversion (unconvert)
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())
Expand Down
8 changes: 8 additions & 0 deletions pkg/iac/types/range.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
10 changes: 7 additions & 3 deletions pkg/misconf/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand All @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions pkg/report/table/misconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down Expand Up @@ -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("<dim>%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()
Expand Down
3 changes: 2 additions & 1 deletion pkg/report/table/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package table_test

import (
"bytes"
"context"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -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)
})
Expand Down
15 changes: 8 additions & 7 deletions pkg/scanner/local/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
}
Expand Down

0 comments on commit 387bb0a

Please sign in to comment.