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

feat(misconf): render causes for Terraform #7852

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
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(highlighted, "\n")
globalCache.Set(key, lines)
return lines
}

func Highlight(filename, input, theme string) (string, bool) {
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
84 changes: 84 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,91 @@ 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 := hclwrite.NewBlock(tfBlock.Type(), normalizeBlockLables(tfBlock))

if !writeBlock(tfBlock, block, causeRng) {
return scan.RenderedCause{}
}

f.Body().AppendBlock(block)

cause := string(hclwrite.Format(f.Bytes()))
cause = strings.TrimSuffix(cause, "\n")
highlighted, _ := scan.Highlight(causeRng.GetFilename(), cause, scan.DarkTheme)
return scan.RenderedCause{
Raw: cause,
Highlighted: highlighted,
}
}

func normalizeBlockLables(block *terraform.Block) []string {
labels := block.Labels()
if block.IsExpanded() {
nameLabel := labels[len(labels)-1]
idx := strings.LastIndex(nameLabel, "[")
if idx != -1 {
labels[len(labels)-1] = nameLabel[:idx]
}
}

return labels
}

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
127 changes: 127 additions & 0 deletions pkg/iac/scanners/terraform/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"context"
"fmt"
"strconv"
"strings"
"testing"
"testing/fstest"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -1217,3 +1219,128 @@ resource "aws_iam_policy" "bad_configuration" {
assert.Len(t, results, 4)
})
}

func TestRenderedCause(t *testing.T) {

check := `# METADATA
# title: S3 Data should be versioned
# custom:
# id: AVD-AWS-0090
# avd_id: AVD-AWS-0090
package user.aws.s3.aws0090

import rego.v1

deny contains res if {
some bucket in input.aws.s3.buckets
not bucket.versioning.enabled.value
res := result.new(
"Bucket does not have versioning enabled",
bucket.versioning.enabled
)
}
`

tests := []struct {
name string
fsys fstest.MapFS
expected string
}{
{
name: "just misconfigured resource",
fsys: fstest.MapFS{
"main.tf": &fstest.MapFile{Data: []byte(`
locals {
versioning = false
}

resource "aws_s3_bucket" "test" {
bucket = "test"

versioning {
enabled = local.versioning
}
}
`)},
},
expected: `resource "aws_s3_bucket" "test" {
versioning {
enabled = false
}
}`,
},
{
name: "misconfigured resource instance",
fsys: fstest.MapFS{
"main.tf": &fstest.MapFile{Data: []byte(`
locals {
versioning = false
}

resource "aws_s3_bucket" "test" {
count = 1
bucket = "test"

versioning {
enabled = local.versioning
}
}
`)},
},
expected: `resource "aws_s3_bucket" "test" {
versioning {
enabled = false
}
}`,
},
{
name: "misconfigured resource instance in the module",
fsys: fstest.MapFS{
"main.tf": &fstest.MapFile{Data: []byte(`
module "bucket" {
source = "../modules/bucket"
}
`),
},
"modules/bucket/main.tf": &fstest.MapFile{Data: []byte(`
locals {
versioning = false
}

resource "aws_s3_bucket" "test" {
count = 1
bucket = "test"

versioning {
enabled = local.versioning
}
}`)},
},
expected: `resource "aws_s3_bucket" "test" {
versioning {
enabled = false
}
}`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scanner := New(
ScannerWithAllDirectories(true),
rego.WithEmbeddedLibraries(true),
rego.WithPolicyReader(strings.NewReader(check)),
rego.WithPolicyNamespaces("user"),
)

results, err := scanner.ScanFS(context.TODO(), tt.fsys, ".")
require.NoError(t, err)

failed := results.GetFailed()

assert.Len(t, failed, 1)

assert.Equal(t, tt.expected, failed[0].Flatten().RenderedCause.Raw)
})
}
}
Loading
Loading