Skip to content

Commit 2302056

Browse files
authored
Merge pull request #17 from coder/stevenmasley/evaluate_step_hook
chore(terraform): hook into evaluateStep behavior with custom hooks
2 parents ea6663a + 97409b8 commit 2302056

File tree

4 files changed

+95
-0
lines changed

4 files changed

+95
-0
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ type evaluator struct {
3939
parentParser *Parser
4040
allowDownloads bool
4141
skipCachedModules bool
42+
// stepHooks are functions that are called after each evaluation step.
43+
// They can be used to provide additional semantics to other terraform blocks.
44+
stepHooks []EvaluateStepHook
4245
}
4346

4447
func newEvaluator(
@@ -56,6 +59,7 @@ func newEvaluator(
5659
logger *log.Logger,
5760
allowDownloads bool,
5861
skipCachedModules bool,
62+
stepHooks []EvaluateStepHook,
5963
) *evaluator {
6064

6165
// create a context to store variables and make functions available
@@ -88,9 +92,12 @@ func newEvaluator(
8892
logger: logger,
8993
allowDownloads: allowDownloads,
9094
skipCachedModules: skipCachedModules,
95+
stepHooks: stepHooks,
9196
}
9297
}
9398

99+
type EvaluateStepHook func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value)
100+
94101
func (e *evaluator) evaluateStep() {
95102

96103
e.ctx.Set(e.getValuesByBlockType("variable"), "var")
@@ -104,6 +111,10 @@ func (e *evaluator) evaluateStep() {
104111
e.ctx.Set(e.getValuesByBlockType("data"), "data")
105112
e.ctx.Set(e.getValuesByBlockType("output"), "output")
106113
e.ctx.Set(e.getValuesByBlockType("module"), "module")
114+
115+
for _, hook := range e.stepHooks {
116+
hook(e.ctx, e.blocks, e.inputVars)
117+
}
107118
}
108119

109120
// exportOutputs is used to export module outputs to the parent module

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import (
1010

1111
type Option func(p *Parser)
1212

13+
func OptionWithEvalHook(hooks EvaluateStepHook) Option {
14+
return func(p *Parser) {
15+
p.stepHooks = append(p.stepHooks, hooks)
16+
}
17+
}
18+
1319
func OptionWithTFVarsPaths(paths ...string) Option {
1420
return func(p *Parser) {
1521
p.tfvarsPaths = paths

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type Parser struct {
5555
// cwd is optional, if left to empty string, 'os.Getwd'
5656
// will be used for populating 'path.cwd' in terraform.
5757
cwd string
58+
stepHooks []EvaluateStepHook
5859
}
5960

6061
// New creates a new Parser
@@ -70,6 +71,7 @@ func New(moduleFS fs.FS, moduleSource string, opts ...Option) *Parser {
7071
configsFS: moduleFS,
7172
logger: slog.Default(),
7273
tfvars: make(map[string]cty.Value),
74+
stepHooks: make([]EvaluateStepHook, 0),
7375
}
7476

7577
for _, option := range opts {
@@ -322,6 +324,7 @@ func (p *Parser) Load(_ context.Context) (*evaluator, error) {
322324
p.logger.With(log.Prefix("terraform evaluator")),
323325
p.allowDownloads,
324326
p.skipCachedModules,
327+
p.stepHooks,
325328
), nil
326329
}
327330

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"github.com/aquasecurity/trivy/internal/testutil"
1919
"github.com/aquasecurity/trivy/pkg/iac/terraform"
20+
tfcontext "github.com/aquasecurity/trivy/pkg/iac/terraform/context"
2021
"github.com/aquasecurity/trivy/pkg/log"
2122
"github.com/aquasecurity/trivy/pkg/set"
2223
)
@@ -2280,6 +2281,80 @@ func TestTFVarsFileDoesNotExist(t *testing.T) {
22802281
assert.ErrorContains(t, err, "file does not exist")
22812282
}
22822283

2284+
func Test_OptionsWithEvalHook(t *testing.T) {
2285+
fs := testutil.CreateFS(t, map[string]string{
2286+
"main.tf": `
2287+
data "your_custom_data" "this" {
2288+
default = ["foo", "foh", "fum"]
2289+
unaffected = "bar"
2290+
}
2291+
2292+
// Testing the hook affects some value, which is used in another evaluateStep
2293+
// action (expanding blocks)
2294+
data "random_thing" "that" {
2295+
dynamic "repeated" {
2296+
for_each = data.your_custom_data.this.value
2297+
content {
2298+
value = repeated.value
2299+
}
2300+
}
2301+
}
2302+
2303+
locals {
2304+
referenced = data.your_custom_data.this.value
2305+
static_ref = data.your_custom_data.this.unaffected
2306+
}
2307+
`})
2308+
2309+
parser := New(fs, "", OptionWithEvalHook(
2310+
// A basic example of how to have a 'default' value for a data block.
2311+
// To see a more practical example, see how 'evaluateVariable' handles
2312+
// the 'default' value of a variable.
2313+
func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) {
2314+
dataBlocks := blocks.OfType("data")
2315+
for _, block := range dataBlocks {
2316+
if len(block.Labels()) >= 1 && block.Labels()[0] == "your_custom_data" {
2317+
def := block.GetAttribute("default")
2318+
ctx.Set(cty.ObjectVal(map[string]cty.Value{
2319+
"value": def.Value(),
2320+
}), "data", "your_custom_data", "this")
2321+
}
2322+
}
2323+
2324+
},
2325+
))
2326+
2327+
require.NoError(t, parser.ParseFS(t.Context(), "."))
2328+
2329+
modules, _, err := parser.EvaluateAll(t.Context())
2330+
require.NoError(t, err)
2331+
assert.Len(t, modules, 1)
2332+
2333+
rootModule := modules[0]
2334+
2335+
// Check the default value of the data block
2336+
blocks := rootModule.GetDatasByType("your_custom_data")
2337+
assert.Len(t, blocks, 1)
2338+
expList := cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("foh"), cty.StringVal("fum")})
2339+
assert.True(t, expList.Equals(blocks[0].GetAttribute("default").Value()).True(), "default value matched list")
2340+
assert.Equal(t, "bar", blocks[0].GetAttribute("unaffected").Value().AsString())
2341+
2342+
// Check the referenced 'data.your_custom_data.this.value' exists in the eval
2343+
// context, and it is the default value of the data block.
2344+
locals := rootModule.GetBlocks().OfType("locals")
2345+
assert.Len(t, locals, 1)
2346+
assert.True(t, expList.Equals(locals[0].GetAttribute("referenced").Value()).True(), "referenced value matched list")
2347+
assert.Equal(t, "bar", locals[0].GetAttribute("static_ref").Value().AsString())
2348+
2349+
// Check the dynamic block is expanded correctly
2350+
dynamicBlocks := rootModule.GetDatasByType("random_thing")
2351+
assert.Len(t, dynamicBlocks, 1)
2352+
assert.Len(t, dynamicBlocks[0].GetBlocks("repeated"), 3)
2353+
for i, repeat := range dynamicBlocks[0].GetBlocks("repeated") {
2354+
assert.Equal(t, expList.Index(cty.NumberIntVal(int64(i))), repeat.GetAttribute("value").Value())
2355+
}
2356+
}
2357+
22832358
func Test_OptionsWithTfVars(t *testing.T) {
22842359
fs := testutil.CreateFS(t, map[string]string{
22852360
"main.tf": `resource "test" "this" {

0 commit comments

Comments
 (0)