Skip to content

Commit

Permalink
feat(misconf): variable support for Terraform Plan (#7228)
Browse files Browse the repository at this point in the history
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
  • Loading branch information
nikpivkin authored Aug 21, 2024
1 parent efdbd8f commit db2c955
Show file tree
Hide file tree
Showing 16 changed files with 446 additions and 10 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ require (
github.com/transparency-dev/merkle v0.0.2 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1368,7 +1368,11 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xanzy/go-gitlab v0.102.0 h1:ExHuJ1OTQ2yt25zBMMj0G96ChBirGYv8U7HyUiYkZ+4=
github.com/xanzy/go-gitlab v0.102.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
Expand Down
23 changes: 18 additions & 5 deletions magefiles/magefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ var (
}
)

var protoFiles = []string{
"pkg/iac/scanners/terraformplan/snapshot/planproto/planfile.proto",
}

func init() {
slog.SetDefault(log.New(log.NewHandler(os.Stderr, nil))) // stdout is suppressed in mage
}
Expand Down Expand Up @@ -154,11 +158,11 @@ func Mock(dir string) error {
func Protoc() error {
// It is called in the protoc container
if _, ok := os.LookupEnv("TRIVY_PROTOC_CONTAINER"); ok {
protoFiles, err := findProtoFiles()
rpcProtoFiles, err := findRPCProtoFiles()
if err != nil {
return err
}
for _, file := range protoFiles {
for _, file := range rpcProtoFiles {
// Check if the generated Go file is up-to-date
dst := strings.TrimSuffix(file, ".proto") + ".pb.go"
if updated, err := target.Path(dst, file); err != nil {
Expand All @@ -173,6 +177,13 @@ func Protoc() error {
return err
}
}

for _, file := range protoFiles {
if err := sh.RunV("protoc", ".", "paths=source_relative", "--go_out", ".", "--go_opt",
"paths=source_relative", file); err != nil {
return err
}
}
return nil
}

Expand Down Expand Up @@ -331,11 +342,13 @@ func Fmt() error {
}

// Format proto files
protoFiles, err := findProtoFiles()
rpcProtoFiles, err := findRPCProtoFiles()
if err != nil {
return err
}
for _, file := range protoFiles {

allProtoFiles := append(protoFiles, rpcProtoFiles...)
for _, file := range allProtoFiles {
if err = sh.Run("clang-format", "-i", file); err != nil {
return err
}
Expand Down Expand Up @@ -422,7 +435,7 @@ func (Docs) Generate() error {
return sh.RunWith(ENV, "go", "run", "-tags=mage_docs", "./magefiles")
}

func findProtoFiles() ([]string, error) {
func findRPCProtoFiles() ([]string, error) {
var files []string
err := filepath.WalkDir("rpc", func(path string, d fs.DirEntry, err error) error {
switch {
Expand Down
11 changes: 11 additions & 0 deletions pkg/iac/scanners/terraform/parser/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package parser
import (
"io/fs"

"github.com/zclconf/go-cty/cty"

"github.com/aquasecurity/trivy/pkg/iac/scanners/options"
)

type ConfigurableTerraformParser interface {
options.ConfigurableParser
SetTFVarsPaths(...string)
SetTFVars(vars map[string]cty.Value)
SetStopOnHCLError(bool)
SetWorkspaceName(string)
SetAllowDownloads(bool)
Expand All @@ -26,6 +29,14 @@ func OptionWithTFVarsPaths(paths ...string) options.ParserOption {
}
}

func OptionsWithTfVars(vars map[string]cty.Value) options.ParserOption {
return func(p options.ConfigurableParser) {
if tf, ok := p.(ConfigurableTerraformParser); ok {
tf.SetTFVars(vars)
}
}
}

func OptionStopOnHCLError(stop bool) options.ParserOption {
return func(p options.ConfigurableParser) {
if tf, ok := p.(ConfigurableTerraformParser); ok {
Expand Down
15 changes: 13 additions & 2 deletions pkg/iac/scanners/terraform/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Parser struct {
moduleBlock *terraform.Block
files []sourceFile
tfvarsPaths []string
tfvars map[string]cty.Value
stopOnHCLError bool
workspaceName string
underlying *hclparse.Parser
Expand All @@ -59,6 +60,10 @@ func (p *Parser) SetTFVarsPaths(s ...string) {
p.tfvarsPaths = s
}

func (p *Parser) SetTFVars(vars map[string]cty.Value) {
p.tfvars = vars
}

func (p *Parser) SetStopOnHCLError(b bool) {
p.stopOnHCLError = b
}
Expand Down Expand Up @@ -90,6 +95,7 @@ func New(moduleFS fs.FS, moduleSource string, opts ...options.ParserOption) *Par
moduleFS: moduleFS,
moduleSource: moduleSource,
configsFS: moduleFS,
tfvars: make(map[string]cty.Value),
}

for _, option := range opts {
Expand Down Expand Up @@ -215,10 +221,15 @@ func (p *Parser) Load(ctx context.Context) (*evaluator, error) {
p.debug.Log("Read %d block(s) and %d ignore(s) for module '%s' (%d file[s])...", len(blocks), len(ignores), p.moduleName, len(p.files))

var inputVars map[string]cty.Value
if p.moduleBlock != nil {

switch {
case p.moduleBlock != nil:
inputVars = p.moduleBlock.Values().AsValueMap()
p.debug.Log("Added %d input variables from module definition.", len(inputVars))
} else {
case len(p.tfvars) > 0:
inputVars = p.tfvars
p.debug.Log("Added %d input variables from tfvars.", len(inputVars))
default:
inputVars, err = loadTFVars(p.configsFS, p.tfvarsPaths)
if err != nil {
return nil, err
Expand Down
27 changes: 27 additions & 0 deletions pkg/iac/scanners/terraform/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1746,6 +1746,33 @@ func TestTFVarsFileDoesNotExist(t *testing.T) {
assert.ErrorContains(t, err, "file does not exist")
}

func Test_OptionsWithTfVars(t *testing.T) {
fs := testutil.CreateFS(t, map[string]string{
"main.tf": `resource "test" "this" {
foo = var.foo
}
variable "foo" {}
`})

parser := New(fs, "", OptionsWithTfVars(
map[string]cty.Value{
"foo": cty.StringVal("bar"),
},
))

require.NoError(t, parser.ParseFS(context.TODO(), "."))

modules, _, err := parser.EvaluateAll(context.TODO())
require.NoError(t, err)
assert.Len(t, modules, 1)

rootModule := modules[0]

blocks := rootModule.GetResourcesByType("test")
assert.Len(t, blocks, 1)
assert.Equal(t, "bar", blocks[0].GetAttribute("foo").Value().AsString())
}

func TestDynamicWithIterator(t *testing.T) {
fsys := fstest.MapFS{
"main.tf": &fstest.MapFile{
Expand Down
64 changes: 64 additions & 0 deletions pkg/iac/scanners/terraformplan/snapshot/plan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package snapshot

import (
"fmt"
"io"

"github.com/zclconf/go-cty/cty"
ctymsgpack "github.com/zclconf/go-cty/cty/msgpack"
"google.golang.org/protobuf/proto"

"github.com/aquasecurity/trivy/pkg/iac/scanners/terraformplan/snapshot/planproto"
)

type DynamicValue []byte

func (v DynamicValue) Decode(ty cty.Type) (cty.Value, error) {
if v == nil {
return cty.NilVal, nil
}

return ctymsgpack.Unmarshal([]byte(v), ty)
}

type Plan struct {
variableValues map[string]DynamicValue
}

func (p Plan) inputVariables() (map[string]cty.Value, error) {
vars := make(map[string]cty.Value)
for k, v := range p.variableValues {
val, err := v.Decode(cty.DynamicPseudoType)
if err != nil {
return nil, err
}
vars[k] = val
}
return vars, nil
}

func readTfPlan(r io.Reader) (*Plan, error) {
b, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("failed to read plan: %w", err)
}

var rawPlan planproto.Plan
if err := proto.Unmarshal(b, &rawPlan); err != nil {
return nil, fmt.Errorf("failed to unmarshal plan: %w", err)
}

plan := Plan{
variableValues: make(map[string]DynamicValue),
}

for k, v := range rawPlan.Variables {
if len(v.Msgpack) == 0 { // len(0) because that's the default value for a "bytes" in protobuf
return nil, fmt.Errorf("dynamic value does not have msgpack serialization")
}

plan.variableValues[k] = DynamicValue(v.Msgpack)
}

return &plan, nil
}
Loading

0 comments on commit db2c955

Please sign in to comment.