Skip to content
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
1 change: 1 addition & 0 deletions pkg/fanal/types/misconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type MisconfResult struct {
Message string `json:",omitempty"`
PolicyMetadata `json:",omitzero"`
CauseMetadata `json:",omitzero"`
FindingID string `json:",omitempty"`

// For debugging
Traces []string `json:",omitempty"`
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 {
Occurrences []Occurrence `json:"occurrences,omitempty"`
Location FlatRange `json:"location"`
RenderedCause RenderedCause `json:"rendered_cause"`
CausePath string
}

type FlatRange struct {
Expand Down Expand Up @@ -70,5 +71,6 @@ func (r *Result) Flatten() FlatResult {
EndLine: rng.GetEndLine(),
},
RenderedCause: r.renderedCause,
CausePath: r.causePath,
}
}
5 changes: 5 additions & 0 deletions pkg/iac/scan/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Result struct {
traces []string
fsPath string
renderedCause RenderedCause
causePath string
}

func (r Result) RegoNamespace() string {
Expand Down Expand Up @@ -110,6 +111,10 @@ func (r *Result) WithRenderedCause(cause RenderedCause) {
r.renderedCause = cause
}

func (r *Result) WithCausePath(p string) {
r.causePath = p
}

func (r *Result) AbsolutePath(fsRoot string, metadata iacTypes.Metadata) string {
if strings.HasSuffix(fsRoot, ":") {
fsRoot += "/"
Expand Down
4 changes: 4 additions & 0 deletions pkg/iac/scanners/cloudformation/parser/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ func (r *Resource) Range() iacTypes.Range {
return r.rng
}

func (r *Resource) Properties() map[string]*Property {
return r.properties
}

func (r *Resource) SourceFormat() SourceFormat {
return r.ctx.SourceFormat
}
Expand Down
71 changes: 71 additions & 0 deletions pkg/iac/scanners/cloudformation/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io/fs"
"strings"

adapter "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation"
"github.com/aquasecurity/trivy/pkg/iac/rego"
Expand Down Expand Up @@ -126,5 +127,75 @@ func (s *Scanner) scanFileContext(ctx context.Context, regoScanner *rego.Scanner
)
}

for i, res := range results {
if res.Status() != scan.StatusFailed {
continue
}

resource := findResourceByRange(cfCtx, res.Range())
if resource == nil {
continue
}

res.WithCausePath(buildCausePath(resource, res.Range()))
results[i] = res
}

return results, nil
}

func findResourceByRange(fctx *parser.FileContext, rng types.Range) *parser.Resource {
for _, r := range fctx.Resources {
if r.Range().GetFilename() == rng.GetFilename() && r.Range().Covers(rng) {
return r
}
}
return nil
}

// buildCausePath returns a deterministic logical path to the property or nested property
// that matches the given range. The path is suitable for constructing fingerprints.
func buildCausePath(resource *parser.Resource, rng types.Range) string {
parts := []string{
resource.Type(),
resource.ID(),
}

var walk func(name string, prop *parser.Property) bool
walk = func(name string, prop *parser.Property) bool {
propRng := prop.Metadata().Range()
if propRng.Match(rng) {
parts = append(parts, name)
return true
}

if propRng.Includes(rng) {
switch v := prop.Value.(type) {
case []*parser.Property:
for i, child := range v {
childName := fmt.Sprintf("%s[%d/%d]", name, i, len(v))
if walk(childName, child) {
return true
}
}
case map[string]*parser.Property:
for childName, child := range v {
if walk(childName, child) {
return true
}
}
}
return true
}

return false
}

for name, p := range resource.Properties() {
if walk(name, p) {
break
}
}

return strings.Join(parts, ".")
}
78 changes: 66 additions & 12 deletions pkg/iac/scanners/terraform/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,20 +97,30 @@ func (e *Executor) Execute(ctx context.Context, modules terraform.Modules, baseP
continue
}

res.WithRenderedCause(e.renderCause(modules, res.Range()))
tfBlock := findBlockByRange(modules, res.Range())
if tfBlock == nil {
continue
}

res.WithRenderedCause(e.renderCause(tfBlock, res.Range()))
res.WithCausePath(buildCausePath(tfBlock, res.Range()))
results[i] = res
}

return results, nil
}

func (e *Executor) renderCause(modules terraform.Modules, causeRng types.Range) scan.RenderedCause {
tfBlock := findBlockForCause(modules, causeRng)
if tfBlock == nil {
e.logger.Debug("No matching Terraform block found", log.String("cause_range", causeRng.String()))
return scan.RenderedCause{}
func findBlockByRange(modules terraform.Modules, rng types.Range) *terraform.Block {
for _, block := range modules.GetBlocks() {
blockRng := block.GetMetadata().Range()
if blockRng.GetFilename() == rng.GetFilename() && blockRng.Covers(rng) {
return block
}
}
return nil
}

func (e *Executor) renderCause(tfBlock *terraform.Block, causeRng types.Range) scan.RenderedCause {
block := hclwrite.NewBlock(tfBlock.Type(), normalizeBlockLables(tfBlock))

if !writeBlock(tfBlock, block, causeRng) {
Expand Down Expand Up @@ -170,14 +180,58 @@ func writeBlock(tfBlock *terraform.Block, block *hclwrite.Block, causeRng types.
return found
}

func findBlockForCause(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
func buildCausePath(tfBlock *terraform.Block, causeRng types.Range) string {
// Always include the top-level block parts
parts := []string{
tfBlock.Type(),
tfBlock.TypeLabel(),
tfBlock.NameLabel(),
}

var walk func(b *terraform.Block) bool
walk = func(b *terraform.Block) bool {
for _, attr := range b.Attributes() {
if attr.GetMetadata().Range().Match(causeRng) {
parts = append(parts, attr.Name())
return true // stop traversal if cause matches this attribute
}
}

typeCount := make(map[string]int)
for _, child := range b.AllBlocks() {
typeCount[child.Type()]++
}
typeIndex := make(map[string]int)

for _, child := range b.AllBlocks() {
childRng := child.GetMetadata().Range()

idx := typeIndex[child.Type()]
typeIndex[child.Type()]++

part := child.Type()
if total := typeCount[child.Type()]; total > 1 {
// Include index and total to uniquely identify this block among siblings
part = fmt.Sprintf("%s[%d/%d]", child.Type(), idx, total-1)
}

if childRng.Match(causeRng) {
parts = append(parts, part)
return true // stop if cause matches this block
}

if childRng.Includes(causeRng) {
parts = append(parts, part)
if walk(child) {
return true // stop if found in deeper level
}
}
}
return false
}
return nil

walk(tfBlock)
return strings.Join(parts, ".")
}

func (e *Executor) filterResults(results scan.Results) scan.Results {
Expand Down
5 changes: 5 additions & 0 deletions pkg/iac/types/range.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,8 @@ func (r Range) Includes(other Range) bool {
func (r Range) Covers(other Range) bool {
return r.startLine <= other.startLine && r.endLine >= other.endLine
}

// Match returns true if 'r' exactly matches 'other'.
func (r Range) Match(other Range) bool {
return r.startLine == other.startLine && r.endLine == other.endLine
}
18 changes: 17 additions & 1 deletion pkg/misconf/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,8 @@ func ResultsToMisconf(configType types.ConfigType, scannerName string, results s

cause := NewCauseWithCode(result, flattened)

filePath := flattened.Location.Filename

misconfResult := types.MisconfResult{
Namespace: result.RegoNamespace(),
Query: query,
Expand All @@ -501,9 +503,10 @@ func ResultsToMisconf(configType types.ConfigType, scannerName string, results s
},
CauseMetadata: cause,
Traces: result.Traces(),
// Build finding ID using file path, rule ID, and logical path to the cause
FindingID: newFindingID(configType, filePath, result.Rule().ID, flattened.CausePath),
}

filePath := flattened.Location.Filename
misconf, ok := misconfs[filePath]
if !ok {
misconf = types.Misconfiguration{
Expand All @@ -525,6 +528,19 @@ func ResultsToMisconf(configType types.ConfigType, scannerName string, results s
return types.ToMisconfigurations(misconfs)
}

func newFindingID(configType types.ConfigType, filePath, checkID, causePath string) string {
if causePath == "" {
return ""
}

switch configType {
case types.Terraform, types.CloudFormation:
return filePath + "@" + checkID + "@" + causePath
default:
return ""
}
}

func NewCauseWithCode(underlying scan.Result, flat scan.FlatResult) types.CauseMetadata {
cause := types.CauseMetadata{
Resource: flat.Resource,
Expand Down
24 changes: 24 additions & 0 deletions pkg/scan/local/fingerprint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package local

import (
"crypto/sha256"
"encoding/hex"

"github.com/aquasecurity/trivy/pkg/types"
)

const (
FingerprintVersion = 1
FingerprintAlgorithm = "sha256"
)

func computeFingerprint(findingID string) types.Fingerprint {
if findingID == "" {
return types.Fingerprint{}
}
hash := sha256.Sum256([]byte(findingID))
return types.Fingerprint{
Hash: FingerprintAlgorithm + ":" + hex.EncodeToString(hash[:]),
FindingID: findingID,
}
}
1 change: 1 addition & 0 deletions pkg/scan/local/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ func toDetectedMisconfiguration(res ftypes.MisconfResult, defaultSeverity dbType
Occurrences: res.Occurrences,
RenderedCause: res.RenderedCause,
},
Fingerprint: computeFingerprint(res.FindingID),
}
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/types/misconfiguration.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ package types

import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"

type Fingerprint struct {
Hash string `json:",omitempty"`
FindingID string `json:",omitempty"`
}

// DetectedMisconfiguration holds detected misconfigurations
type DetectedMisconfiguration struct {
Type string `json:",omitempty"`
Expand All @@ -19,6 +24,7 @@ type DetectedMisconfiguration struct {
Status MisconfStatus `json:",omitempty"`
Layer ftypes.Layer `json:",omitzero"`
CauseMetadata ftypes.CauseMetadata `json:",omitzero"`
Fingerprint Fingerprint `json:",omitzero"`

// For debugging
Traces []string `json:",omitempty"`
Expand Down
Loading