1 package main
2
3 import (
4 "encoding/json"
5 "flag"
6 "log"
7 "os"
8 "path"
9 "strings"
10
11 "github.com/Microsoft/hcsshim/internal/guestpath"
12 rpi "github.com/Microsoft/hcsshim/internal/regopolicyinterpreter"
13 "github.com/Microsoft/hcsshim/pkg/securitypolicy"
14 )
15
16 type command struct {
17 Name string `json:"name"`
18 Input map[string]interface{} `json:"input"`
19 }
20
21 var (
22 commandsPath = flag.String("commands", "", "path commands JSON file")
23 policyPath = flag.String("policy", "", "path to policy Rego file")
24 dataPath = flag.String("data", "", "path initial data state JSON file (optional)")
25 logPath = flag.String("log", "", "path to output log file")
26 logLevelName = flag.String("logLevel", "Info", "None|Info|Results|Metadata")
27 )
28
29 func readCommands() []command {
30 content, err := os.ReadFile(*commandsPath)
31 if err != nil {
32 log.Fatal(err)
33 }
34
35 var commands []command
36 err = json.Unmarshal(content, &commands)
37 if err != nil {
38 log.Fatal(err)
39 }
40
41 return commands
42 }
43
44 func createInterpreter() *rpi.RegoPolicyInterpreter {
45 content, err := os.ReadFile(*policyPath)
46 if err != nil {
47 log.Fatal(err)
48 }
49
50 policyCode := string(content)
51
52 var logLevel rpi.LogLevel
53 switch strings.ToLower(*logLevelName) {
54 case "none":
55 logLevel = rpi.LogNone
56
57 case "info":
58 logLevel = rpi.LogInfo
59
60 case "results":
61 logLevel = rpi.LogResults
62
63 case "metadata":
64 logLevel = rpi.LogMetadata
65
66 default:
67 log.Fatalf("unrecognized log level: %s", *logLevelName)
68 }
69
70 var data map[string]interface{}
71 if len(*dataPath) > 0 {
72 contents, err := os.ReadFile(*dataPath)
73 if err != nil {
74 log.Fatal(err)
75 }
76
77 err = json.Unmarshal(contents, &data)
78 if err != nil {
79 log.Fatalf("error loading initial data state: %v", err)
80 }
81 } else {
82 data = map[string]interface{}{
83 "defaultMounts": []interface{}{},
84 "privilegedMounts": []interface{}{},
85 "sandboxPrefix": guestpath.SandboxMountPrefix,
86 "hugePagesPrefix": guestpath.HugePagesMountPrefix,
87 "defaultPrivilegedCapabilities": securitypolicy.DefaultPrivilegedCapabilities(),
88 "defaultUnprivilegedCapabilities": securitypolicy.DefaultUnprivilegedCapabilities(),
89 }
90 }
91
92 r, err := rpi.NewRegoPolicyInterpreter(policyCode, data)
93 if err != nil {
94 log.Fatal(err)
95 }
96
97 if len(*logPath) > 0 {
98 if _, err := os.Stat(*logPath); err == nil {
99 os.Remove(*logPath)
100 }
101
102 if logLevel == rpi.LogNone {
103 log.Println("logPath provided but logLevel set to None: turning off logging")
104 } else {
105 log.Printf("turning on logging to %s with level %s\n", *logPath, *logLevelName)
106 }
107 err = r.EnableLogging(*logPath, logLevel)
108 if err != nil {
109 log.Fatal(err)
110 }
111 }
112
113 r.AddModule("framework.rego", &rpi.RegoModule{Namespace: "framework", Code: securitypolicy.FrameworkCode})
114 r.AddModule("api.rego", &rpi.RegoModule{Namespace: "api", Code: securitypolicy.APICode})
115
116 return r
117 }
118
119 func parseNamespace(rego string) string {
120 lines := strings.Split(rego, "\n")
121 parts := strings.Split(lines[0], " ")
122 if parts[0] != "package" || len(parts) < 2 {
123 log.Fatal("package definition required on first line of Rego module")
124 }
125
126 namespace := parts[1]
127 return namespace
128 }
129
130 func loadLocalFragment(commandsDir string, input map[string]interface{}) *rpi.RegoModule {
131 var localPath string
132 var ok bool
133 if localPath, ok = input["local_path"].(string); !ok {
134 log.Println(input)
135 log.Fatal("'load_fragment' requires a 'local_path' member in 'input' which points to a local Rego file with the fragment logic")
136 }
137
138 content, err := os.ReadFile(localPath)
139 if err != nil {
140 localPath = path.Join(commandsDir, localPath)
141 content, err = os.ReadFile(localPath)
142 if err != nil {
143 log.Fatalf("unable to load fragment: %v", err)
144 }
145 }
146
147 code := string(content)
148 return &rpi.RegoModule{
149 Namespace: parseNamespace(code),
150 Feed: input["feed"].(string),
151 Issuer: input["issuer"].(string),
152 Code: code,
153 }
154 }
155
156 func main() {
157 flag.Parse()
158 if flag.NArg() != 0 || len(*policyPath) == 0 || len(*commandsPath) == 0 {
159 flag.Usage()
160 os.Exit(1)
161 }
162
163 commands := readCommands()
164 rego := createInterpreter()
165
166 for i, command := range commands {
167 var fragment *rpi.RegoModule
168 if command.Name == "load_fragment" {
169 fragment = loadLocalFragment(path.Dir(*commandsPath), command.Input)
170 rego.AddModule(fragment.ID(), fragment)
171 }
172
173 result, err := rego.Query("data.policy."+command.Name, command.Input)
174 if err != nil {
175 inputJSON, _ := json.Marshal(command.Input)
176 log.Fatalf("query of %s with input %s failed with error %v",
177 command.Name,
178 inputJSON,
179 err)
180 }
181
182 addModule, _ := result.Bool("add_module")
183
184 removeModule := true
185 if fragment != nil && addModule {
186 removeModule = false
187 }
188
189 allowed, err := result.Bool("allowed")
190 if err != nil {
191 log.Fatalf("policy result missing required `allowed` key: %v", err)
192 }
193
194 if allowed {
195 log.Printf("%02d> %s ok\n", i, command.Name)
196 } else {
197 log.Printf("%02d> %s not allowed", i, command.Name)
198 command.Input["rule"] = command.Name
199 result, err = rego.Query("data.policy.reason", command.Input)
200 if err != nil {
201 log.Fatalf("unable to get reason for failure: %v", err)
202 }
203
204 if !result.IsEmpty() {
205 errors, _ := result.Value("errors")
206 log.Printf("Reason: %v", errors)
207 }
208 }
209
210 if removeModule && fragment != nil {
211 rego.RemoveModule(fragment.ID())
212 }
213 }
214 }
215
View as plain text