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): variable support for Terraform Plan #7228

Merged
merged 2 commits into from
Aug 21, 2024
Merged
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
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,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 @@ -1367,7 +1367,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
}
}
Comment on lines +180 to +186
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mage:protoc command builds an image and runs it. The dockerfile does not need to be updated.

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")
}
Comment on lines +56 to +58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I understand the comment, we check the len to be zero to ensure that what we are unpacking is msgpack right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Terraforom stores cty values in msgpack encoding . If the byte length is 0, it means a invalid value is represented. I looked up the implementation in Terraform.


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

return &plan, nil
}
Loading