-
Notifications
You must be signed in to change notification settings - Fork 4
/
main.go
259 lines (226 loc) · 6.88 KB
/
main.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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/clientcmd"
yaml "sigs.k8s.io/yaml/goyaml.v3"
)
var ruleFile string
type rule struct {
Kind string `yaml:"kind"`
CheckType string `yaml:"checkType"`
Title string `yaml:"title"`
Expression string `yaml:"expression"`
Inputs []input `yaml:"inputs"`
ErrorMessage string `yaml:"errorMessage"`
}
type input struct {
// Name of the input, it could be referenced in the expression
Name string `yaml:"name"`
// Type of the input, it could be a kube GroupVersionResource or a file or a kube api path
Type string `yaml:"type"`
ApiGroup string `yaml:"apiGroup"`
Version string `yaml:"version"`
Resource string `yaml:"resource"`
SubResource string `yaml:"subResource,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
Path string `yaml:"path"`
}
func main() {
validate()
r := read(ruleFile)
// collect dependencies (e.g., get file metadata information or Kubernetes resource information)
resultMap := collect(r)
declsList := []*expr.Decl{}
for k, _ := range resultMap {
// build a CEL environment with the rule expression
declsList = append(declsList, decls.NewVar(k, decls.Dyn))
}
mapStrDyn := cel.MapType(cel.StringType, cel.DynType)
var jsonenvOpts cel.EnvOption = cel.Function("parseJSON",
cel.MemberOverload("parseJSON_string",
[]*cel.Type{cel.StringType}, mapStrDyn, cel.UnaryBinding(parseJSONString)))
var yamlenvOpts cel.EnvOption = cel.Function("parseYAML",
cel.MemberOverload("parseYAML_string",
[]*cel.Type{cel.StringType}, mapStrDyn, cel.UnaryBinding(parseYAMLString)))
// build a CEL environment with the rule expression
env, err := cel.NewEnv(
cel.Declarations(declsList...), jsonenvOpts, yamlenvOpts,
)
if err != nil {
panic(fmt.Sprintf("Failed to create CEL environment: %s", err))
}
// compile the CEL expression
ast, issues := env.Compile(r.Expression)
if issues.Err() != nil {
panic(fmt.Sprintf("Failed to compile CEL expression: %s", issues.Err()))
}
evalVars := map[string]interface{}{}
for k, v := range resultMap {
evalVars[k] = toCelValue(v)
}
// evaluate the CEL program with the inputs
prg, err := env.Program(ast)
if err != nil {
panic(fmt.Sprintf("Failed to create CEL program: %s", err))
}
out, _, err := prg.Eval(evalVars)
if err != nil {
if strings.HasPrefix(err.Error(), "no such key:") {
fmt.Printf("Warning: %s in %s/%s\n", err, r.Inputs[0].Resource, r.Inputs[0].SubResource)
fmt.Printf("Evaluation result: false\n")
return
}
panic(fmt.Sprintf("Failed to evaluate CEL expression: %s", err))
}
// report the findings
if out.Value() == false {
fmt.Println(r.ErrorMessage)
}
fmt.Printf("%s: %v\n", r.Title, out)
}
func parseJSONString(val ref.Val) ref.Val {
str := val.(types.String)
decodedVal := map[string]interface{}{}
err := json.Unmarshal([]byte(str), &decodedVal)
if err != nil {
return types.NewErr("failed to decode '%v' in parseJSON: %w", str, err)
}
r, err := types.NewRegistry()
if err != nil {
return types.NewErr("failed to create a new registry in parseJSON: %w", err)
}
return types.NewDynamicMap(r, decodedVal)
}
func parseYAMLString(val ref.Val) ref.Val {
str := val.(types.String)
decodedVal := map[string]interface{}{}
err := yaml.Unmarshal([]byte(str), &decodedVal)
if err != nil {
return types.NewErr("failed to decode '%v' in parseYAML: %w", str, err)
}
r, err := types.NewRegistry()
if err != nil {
return types.NewErr("failed to create a new registry in parseJSON: %w", err)
}
return types.NewDynamicMap(r, decodedVal)
}
func toCelValue(u interface{}) interface{} {
if unstruct, ok := u.(*unstructured.Unstructured); ok {
return unstruct.Object
}
if unstructList, ok := u.(*unstructured.UnstructuredList); ok {
list := []interface{}{}
for _, item := range unstructList.Items {
list = append(list, item.Object)
}
return map[string]interface{}{
"items": list,
}
}
return nil
}
// Validate inputs
func validate() {
flag.StringVar(&ruleFile, "i", "", "Path to a YAML file containing a rule")
flag.Parse()
if ruleFile == "" {
panic("Must use -i to pass in a rule file")
}
}
// Read a file as a given path `p` and return a rule struct that represents the
// YAML contents.
func read(p string) *rule {
r := rule{}
f, err := os.ReadFile(p)
if err != nil {
panic(fmt.Sprintf("Failed to read %s: %s", p, err))
}
err = yaml.Unmarshal(f, &r)
if err != nil {
panic(fmt.Sprintf("Failed to parse YAML: %s", err))
}
return &r
}
// Collect all the dependencies for a given rule
func collect(r *rule) map[string]interface{} {
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
panic("You must export KUBECONFIG for this tool to fetch Kubernetes resources")
}
// add logging here
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
panic(err.Error())
}
// Create a new dynamic client
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
panic(err)
}
// create context
ctx := context.TODO()
resultMap := make(map[string]interface{})
// fetch the resources and store them in a map
if r.CheckType == "Platform" {
if r.Inputs != nil {
for _, input := range r.Inputs {
if input.Type == "KubeGroupVersionResource" {
// fetch the resource
// add logging here
gvr := schema.GroupVersionResource{
Group: input.ApiGroup,
Version: input.Version,
Resource: input.Resource,
}
results := &unstructured.UnstructuredList{}
result := &unstructured.Unstructured{}
if input.SubResource != "" {
if input.Namespace == "" {
result, err = dynamicClient.Resource(gvr).Get(ctx, input.SubResource, metav1.GetOptions{})
if err != nil {
panic(err)
}
} else {
result, err = dynamicClient.Resource(gvr).Namespace(input.Namespace).Get(ctx, input.SubResource, metav1.GetOptions{})
if err != nil {
panic(err)
}
}
resultMap[input.Name] = result
} else {
if input.Namespace == "" {
results, err = dynamicClient.Resource(gvr).List(ctx, metav1.ListOptions{})
if err != nil {
panic(err)
}
} else {
results, err = dynamicClient.Resource(gvr).Namespace(input.Namespace).List(ctx, metav1.ListOptions{})
if err != nil {
panic(err)
}
}
resultMap[input.Name] = results
}
if results == nil && result == nil {
panic("Failed to fetch the resource")
}
}
}
}
}
return resultMap
}