generated from terraform-linters/tflint-ruleset-template
/
engine.go
211 lines (186 loc) · 5.57 KB
/
engine.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
package opa
import (
"context"
"fmt"
"io"
"os"
"strings"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/hcl/v2"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/loader"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/tester"
"github.com/open-policy-agent/opa/topdown"
"github.com/open-policy-agent/opa/topdown/print"
"github.com/open-policy-agent/opa/version"
"github.com/terraform-linters/tflint-plugin-sdk/logger"
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
)
// Engine evaluates policies and returns issues.
// In other words, this is a wrapper of rego.New(...).Eval().
type Engine struct {
store storage.Store
modules map[string]*ast.Module
print print.Hook
traceWriter io.Writer
runtime *ast.Term
}
// NewEngine returns a new engine based on the policies loaded
func NewEngine(ret *loader.Result) (*Engine, error) {
store, err := ret.Store()
if err != nil {
return nil, err
}
logWriter := logger.Logger().StandardWriter(&hclog.StandardLoggerOptions{ForceLevel: hclog.Debug})
printer := topdown.NewPrintHook(logWriter)
// If TFLINT_OPA_TRACE is set, print traces to the debug log.
var traceWriter io.Writer
trace := os.Getenv("TFLINT_OPA_TRACE")
if trace != "" && trace != "false" && trace != "0" {
traceWriter = logWriter
}
return &Engine{
store: store,
modules: ret.ParsedModules(),
print: printer,
traceWriter: traceWriter,
runtime: runtime(),
}, nil
}
// Issue is the result of the query.
type Issue struct {
Message string
Range hcl.Range
}
// RunQuery executes a query referencing a rule and returns the generated
// Set document as Result.
// rego.ResultSet is parsed according to the following conventions:
//
// - All rules should be under the "tflint" package
// - Rule should return a tflint.issue()
//
// Example:
//
// ```
//
// deny_test[issue] {
// [condition]
//
// issue := tflint.issue("not allowed", resource.decl_range)
// }
//
// ```
func (e *Engine) RunQuery(rule *Rule, runner tflint.Runner) ([]*Issue, error) {
traceEnabled := e.traceWriter != nil
options := []func(*rego.Rego){
// All rules should be under the "tflint" package
rego.Query(fmt.Sprintf("data.tflint.%s", rule.RegoName())),
// Makes it possible to refer to the loaded YAML/JSON as the "data" document
rego.Store(e.store),
// Enable strict mode
rego.Strict(true),
// Enable strict-builtin-errors to return custom function errors immediately
rego.StrictBuiltinErrors(true),
// Enable print() to invoke logger.Debug()
rego.EnablePrintStatements(true),
rego.PrintHook(e.print),
// Enable trace() if TFLINT_OPA_TRACE=true
rego.Trace(traceEnabled),
// Enable opa.runtime().env/version/commit
rego.Runtime(e.runtime),
}
// Set policies
for _, m := range e.modules {
options = append(options, rego.ParsedModule(m))
}
// Enable custom functions (e.g. terraform.resources)
// Mock functions are usually not needed outside of testing,
// but are provided for compilation.
options = append(options, Functions(runner)...)
options = append(options, MockFunctions()...)
instance := rego.New(options...)
rs, err := instance.Eval(context.Background())
if err != nil {
return nil, err
}
if traceEnabled {
rego.PrintTrace(e.traceWriter, instance)
}
var issues []*Issue
for _, result := range rs {
for _, expr := range result.Expressions {
values, ok := expr.Value.([]any)
if !ok {
return nil, fmt.Errorf("issue is not set, got %T", expr.Value)
}
for _, value := range values {
ret, err := jsonToIssue(value, "issue")
if err != nil {
return nil, err
}
issues = append(issues, ret)
}
}
}
return issues, err
}
// RunTest runs a policy test. The details are hidden inside open-policy-agent/opa/tester
// and this is a wrapper of it. Test results are emitted as issues if failed or errored.
//
// A runner is provided, but in many cases the runner is never actually used,
// as test runners are generated inside mock functions. See TesterMockFunctions for details.
func (e *Engine) RunTest(rule *TestRule, runner tflint.Runner) ([]*Issue, error) {
traceEnabled := e.traceWriter != nil
testRunner := tester.NewRunner().
SetStore(e.store).
CapturePrintOutput(true).
EnableTracing(traceEnabled).
SetRuntime(e.runtime).
SetModules(e.modules).
AddCustomBuiltins(append(TesterFunctions(runner), TesterMockFunctions()...)).
Filter(rule.RegoName())
ch, err := testRunner.RunTests(context.Background(), nil)
if err != nil {
return nil, err
}
var issues []*Issue
for ret := range ch {
if ret.Error != nil {
// Location is not included as it is not an issue for HCL.
issues = append(issues, &Issue{
Message: fmt.Sprintf("test errored: %s", ret.Error),
})
continue
}
if ret.Output != nil {
logger.Debug(string(ret.Output))
}
if traceEnabled {
topdown.PrettyTrace(e.traceWriter, ret.Trace)
}
if ret.Fail {
issues = append(issues, &Issue{
Message: "test failed",
})
}
}
return issues, nil
}
func runtime() *ast.Term {
env := ast.NewObject()
for _, pair := range os.Environ() {
parts := strings.SplitN(pair, "=", 2)
if len(parts) == 1 {
env.Insert(ast.StringTerm(parts[0]), ast.NullTerm())
} else if len(parts) > 1 {
env.Insert(ast.StringTerm(parts[0]), ast.StringTerm(parts[1]))
}
}
obj := ast.NewObject()
obj.Insert(ast.StringTerm("env"), ast.NewTerm(env))
obj.Insert(ast.StringTerm("version"), ast.StringTerm(version.Version))
obj.Insert(ast.StringTerm("commit"), ast.StringTerm(version.Vcs))
return ast.NewTerm(obj)
}