Skip to content

Commit db2c955

Browse files
authored
feat(misconf): variable support for Terraform Plan (#7228)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
1 parent efdbd8f commit db2c955

File tree

16 files changed

+446
-10
lines changed

16 files changed

+446
-10
lines changed

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,8 @@ require (
358358
github.com/transparency-dev/merkle v0.0.2 // indirect
359359
github.com/ulikunitz/xz v0.5.11 // indirect
360360
github.com/vbatts/tar-split v0.11.5 // indirect
361+
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
362+
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
361363
github.com/xanzy/ssh-agent v0.3.3 // indirect
362364
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
363365
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -1368,7 +1368,11 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X
13681368
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
13691369
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
13701370
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
1371+
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
1372+
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
13711373
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
1374+
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
1375+
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
13721376
github.com/xanzy/go-gitlab v0.102.0 h1:ExHuJ1OTQ2yt25zBMMj0G96ChBirGYv8U7HyUiYkZ+4=
13731377
github.com/xanzy/go-gitlab v0.102.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI=
13741378
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=

magefiles/magefile.go

+18-5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ var (
3030
}
3131
)
3232

33+
var protoFiles = []string{
34+
"pkg/iac/scanners/terraformplan/snapshot/planproto/planfile.proto",
35+
}
36+
3337
func init() {
3438
slog.SetDefault(log.New(log.NewHandler(os.Stderr, nil))) // stdout is suppressed in mage
3539
}
@@ -154,11 +158,11 @@ func Mock(dir string) error {
154158
func Protoc() error {
155159
// It is called in the protoc container
156160
if _, ok := os.LookupEnv("TRIVY_PROTOC_CONTAINER"); ok {
157-
protoFiles, err := findProtoFiles()
161+
rpcProtoFiles, err := findRPCProtoFiles()
158162
if err != nil {
159163
return err
160164
}
161-
for _, file := range protoFiles {
165+
for _, file := range rpcProtoFiles {
162166
// Check if the generated Go file is up-to-date
163167
dst := strings.TrimSuffix(file, ".proto") + ".pb.go"
164168
if updated, err := target.Path(dst, file); err != nil {
@@ -173,6 +177,13 @@ func Protoc() error {
173177
return err
174178
}
175179
}
180+
181+
for _, file := range protoFiles {
182+
if err := sh.RunV("protoc", ".", "paths=source_relative", "--go_out", ".", "--go_opt",
183+
"paths=source_relative", file); err != nil {
184+
return err
185+
}
186+
}
176187
return nil
177188
}
178189

@@ -331,11 +342,13 @@ func Fmt() error {
331342
}
332343

333344
// Format proto files
334-
protoFiles, err := findProtoFiles()
345+
rpcProtoFiles, err := findRPCProtoFiles()
335346
if err != nil {
336347
return err
337348
}
338-
for _, file := range protoFiles {
349+
350+
allProtoFiles := append(protoFiles, rpcProtoFiles...)
351+
for _, file := range allProtoFiles {
339352
if err = sh.Run("clang-format", "-i", file); err != nil {
340353
return err
341354
}
@@ -422,7 +435,7 @@ func (Docs) Generate() error {
422435
return sh.RunWith(ENV, "go", "run", "-tags=mage_docs", "./magefiles")
423436
}
424437

425-
func findProtoFiles() ([]string, error) {
438+
func findRPCProtoFiles() ([]string, error) {
426439
var files []string
427440
err := filepath.WalkDir("rpc", func(path string, d fs.DirEntry, err error) error {
428441
switch {

pkg/iac/scanners/terraform/parser/option.go

+11
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package parser
33
import (
44
"io/fs"
55

6+
"github.com/zclconf/go-cty/cty"
7+
68
"github.com/aquasecurity/trivy/pkg/iac/scanners/options"
79
)
810

911
type ConfigurableTerraformParser interface {
1012
options.ConfigurableParser
1113
SetTFVarsPaths(...string)
14+
SetTFVars(vars map[string]cty.Value)
1215
SetStopOnHCLError(bool)
1316
SetWorkspaceName(string)
1417
SetAllowDownloads(bool)
@@ -26,6 +29,14 @@ func OptionWithTFVarsPaths(paths ...string) options.ParserOption {
2629
}
2730
}
2831

32+
func OptionsWithTfVars(vars map[string]cty.Value) options.ParserOption {
33+
return func(p options.ConfigurableParser) {
34+
if tf, ok := p.(ConfigurableTerraformParser); ok {
35+
tf.SetTFVars(vars)
36+
}
37+
}
38+
}
39+
2940
func OptionStopOnHCLError(stop bool) options.ParserOption {
3041
return func(p options.ConfigurableParser) {
3142
if tf, ok := p.(ConfigurableTerraformParser); ok {

pkg/iac/scanners/terraform/parser/parser.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type Parser struct {
3939
moduleBlock *terraform.Block
4040
files []sourceFile
4141
tfvarsPaths []string
42+
tfvars map[string]cty.Value
4243
stopOnHCLError bool
4344
workspaceName string
4445
underlying *hclparse.Parser
@@ -59,6 +60,10 @@ func (p *Parser) SetTFVarsPaths(s ...string) {
5960
p.tfvarsPaths = s
6061
}
6162

63+
func (p *Parser) SetTFVars(vars map[string]cty.Value) {
64+
p.tfvars = vars
65+
}
66+
6267
func (p *Parser) SetStopOnHCLError(b bool) {
6368
p.stopOnHCLError = b
6469
}
@@ -90,6 +95,7 @@ func New(moduleFS fs.FS, moduleSource string, opts ...options.ParserOption) *Par
9095
moduleFS: moduleFS,
9196
moduleSource: moduleSource,
9297
configsFS: moduleFS,
98+
tfvars: make(map[string]cty.Value),
9399
}
94100

95101
for _, option := range opts {
@@ -215,10 +221,15 @@ func (p *Parser) Load(ctx context.Context) (*evaluator, error) {
215221
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))
216222

217223
var inputVars map[string]cty.Value
218-
if p.moduleBlock != nil {
224+
225+
switch {
226+
case p.moduleBlock != nil:
219227
inputVars = p.moduleBlock.Values().AsValueMap()
220228
p.debug.Log("Added %d input variables from module definition.", len(inputVars))
221-
} else {
229+
case len(p.tfvars) > 0:
230+
inputVars = p.tfvars
231+
p.debug.Log("Added %d input variables from tfvars.", len(inputVars))
232+
default:
222233
inputVars, err = loadTFVars(p.configsFS, p.tfvarsPaths)
223234
if err != nil {
224235
return nil, err

pkg/iac/scanners/terraform/parser/parser_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -1746,6 +1746,33 @@ func TestTFVarsFileDoesNotExist(t *testing.T) {
17461746
assert.ErrorContains(t, err, "file does not exist")
17471747
}
17481748

1749+
func Test_OptionsWithTfVars(t *testing.T) {
1750+
fs := testutil.CreateFS(t, map[string]string{
1751+
"main.tf": `resource "test" "this" {
1752+
foo = var.foo
1753+
}
1754+
variable "foo" {}
1755+
`})
1756+
1757+
parser := New(fs, "", OptionsWithTfVars(
1758+
map[string]cty.Value{
1759+
"foo": cty.StringVal("bar"),
1760+
},
1761+
))
1762+
1763+
require.NoError(t, parser.ParseFS(context.TODO(), "."))
1764+
1765+
modules, _, err := parser.EvaluateAll(context.TODO())
1766+
require.NoError(t, err)
1767+
assert.Len(t, modules, 1)
1768+
1769+
rootModule := modules[0]
1770+
1771+
blocks := rootModule.GetResourcesByType("test")
1772+
assert.Len(t, blocks, 1)
1773+
assert.Equal(t, "bar", blocks[0].GetAttribute("foo").Value().AsString())
1774+
}
1775+
17491776
func TestDynamicWithIterator(t *testing.T) {
17501777
fsys := fstest.MapFS{
17511778
"main.tf": &fstest.MapFile{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package snapshot
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/zclconf/go-cty/cty"
8+
ctymsgpack "github.com/zclconf/go-cty/cty/msgpack"
9+
"google.golang.org/protobuf/proto"
10+
11+
"github.com/aquasecurity/trivy/pkg/iac/scanners/terraformplan/snapshot/planproto"
12+
)
13+
14+
type DynamicValue []byte
15+
16+
func (v DynamicValue) Decode(ty cty.Type) (cty.Value, error) {
17+
if v == nil {
18+
return cty.NilVal, nil
19+
}
20+
21+
return ctymsgpack.Unmarshal([]byte(v), ty)
22+
}
23+
24+
type Plan struct {
25+
variableValues map[string]DynamicValue
26+
}
27+
28+
func (p Plan) inputVariables() (map[string]cty.Value, error) {
29+
vars := make(map[string]cty.Value)
30+
for k, v := range p.variableValues {
31+
val, err := v.Decode(cty.DynamicPseudoType)
32+
if err != nil {
33+
return nil, err
34+
}
35+
vars[k] = val
36+
}
37+
return vars, nil
38+
}
39+
40+
func readTfPlan(r io.Reader) (*Plan, error) {
41+
b, err := io.ReadAll(r)
42+
if err != nil {
43+
return nil, fmt.Errorf("failed to read plan: %w", err)
44+
}
45+
46+
var rawPlan planproto.Plan
47+
if err := proto.Unmarshal(b, &rawPlan); err != nil {
48+
return nil, fmt.Errorf("failed to unmarshal plan: %w", err)
49+
}
50+
51+
plan := Plan{
52+
variableValues: make(map[string]DynamicValue),
53+
}
54+
55+
for k, v := range rawPlan.Variables {
56+
if len(v.Msgpack) == 0 { // len(0) because that's the default value for a "bytes" in protobuf
57+
return nil, fmt.Errorf("dynamic value does not have msgpack serialization")
58+
}
59+
60+
plan.variableValues[k] = DynamicValue(v.Msgpack)
61+
}
62+
63+
return &plan, nil
64+
}

0 commit comments

Comments
 (0)