package composepolicy import ( "encoding/json" "strings" "testing" "server-deploy/internal/appbundle" ) func fixture() (appbundle.Manifest, map[string]any) { image := "example/api@sha256:" + strings.Repeat("a", 64) m := appbundle.Manifest{Components: []appbundle.Component{{Name: "api", Image: image}}} d := map[string]any{ "services": map[string]any{"api": map[string]any{ "image": image, "user": "1000:1000", "read_only": true, "cap_drop": []string{"ALL"}, "security_opt": []string{"no-new-privileges:true"}, "restart": "unless-stopped", "networks": []string{"backend"}, "volumes": []any{map[string]any{"type": "volume", "source": "data", "target": "/data", "read_only": false}}, }}, "networks": map[string]any{"backend": map[string]any{"internal": true}}, "volumes": map[string]any{"data": map[string]any{}}, } return m, d } func TestAcceptRestrictedService(t *testing.T) { m, d := fixture() raw, _ := json.Marshal(d) if err := Check(m, raw); err != nil { t.Fatal(err) } } func TestRejectPrivilegeAndExternalInputs(t *testing.T) { for _, field := range []string{"privileged", "build", "container_name", "network_mode", "pid", "ipc", "userns_mode", "devices", "cap_add", "env_file", "environment", "extends", "ports", "labels", "use_api_socket", "post_start", "pre_stop", "command", "entrypoint", "volumes_from", "develop", "provider"} { t.Run(field, func(t *testing.T) { m, d := fixture() d["services"].(map[string]any)["api"].(map[string]any)[field] = "secret-sentinel" raw, _ := json.Marshal(d) err := Check(m, raw) if err == nil || strings.Contains(err.Error(), "secret-sentinel") { t.Fatal("forbidden field accepted or echoed") } }) } } func TestRejectUnsafeValuesAndReferences(t *testing.T) { mutations := map[string]func(map[string]any, map[string]any){ "image drift": func(d, s map[string]any) { s["image"] = "example/api:latest" }, "root": func(d, s map[string]any) { s["user"] = "0:0" }, "named user": func(d, s map[string]any) { s["user"] = "root" }, "writable root": func(d, s map[string]any) { s["read_only"] = false }, "caps": func(d, s map[string]any) { s["cap_drop"] = []string{} }, "escalation": func(d, s map[string]any) { s["security_opt"] = []string{"seccomp:unconfined"} }, "implicit network": func(d, s map[string]any) { s["networks"] = []string{} }, "other network": func(d, s map[string]any) { s["networks"] = []string{"default"} }, "external network": func(d, s map[string]any) { d["networks"] = map[string]any{"backend": map[string]any{"internal": true, "external": true}} }, "outbound network": func(d, s map[string]any) { d["networks"] = map[string]any{"backend": map[string]any{"internal": false}} }, "bind": func(d, s map[string]any) { s["volumes"].([]any)[0].(map[string]any)["type"] = "bind" }, "host source": func(d, s map[string]any) { s["volumes"].([]any)[0].(map[string]any)["source"] = "/var/run/docker.sock" }, "missing volume": func(d, s map[string]any) { d["volumes"] = map[string]any{} }, "volume driver": func(d, s map[string]any) { d["volumes"] = map[string]any{"data": map[string]any{"driver_opts": map[string]string{"device": "/"}}} }, "root mount": func(d, s map[string]any) { s["volumes"].([]any)[0].(map[string]any)["target"] = "/" }, "traversal": func(d, s map[string]any) { s["volumes"].([]any)[0].(map[string]any)["target"] = "/data/../etc" }, "interpolation": func(d, s map[string]any) { s["user"] = "${UID}:1000" }, "include": func(d, s map[string]any) { d["include"] = []string{"/secret"} }, "null service": func(d, s map[string]any) { d["services"].(map[string]any)["api"] = nil }, "missing security": func(d, s map[string]any) { delete(s, "security_opt") }, "null volume": func(d, s map[string]any) { d["volumes"] = map[string]any{"data": nil} }, "extra service": func(d, s map[string]any) { d["services"].(map[string]any)["rogue"] = s }, "no services": func(d, s map[string]any) { d["services"] = map[string]any{} }, } for name, mutate := range mutations { t.Run(name, func(t *testing.T) { m, d := fixture() s := d["services"].(map[string]any)["api"].(map[string]any) mutate(d, s) raw, _ := json.Marshal(d) if Check(m, raw) == nil { t.Fatal("unsafe document accepted") } }) } } func TestRejectMalformedDocument(t *testing.T) { m, d := fixture() raw, _ := json.Marshal(d) for _, input := range []string{"services: {}", string(raw) + "{}", strings.Replace(string(raw), `"read_only":true`, `"read_only":true,"read_only":false`, 1), strings.Repeat(" ", 4<<20) + string(raw)} { if Check(m, []byte(input)) == nil { t.Fatal("malformed input accepted") } } } func TestMountAndIdentityBoundaries(t *testing.T) { for _, target := range []string{"/proc/x", "/var", "/var/run/docker.sock", "/sys", "/dev", "/etc", "/run", "/usr/local", "/lib", "/lib64", "/bin", "/sbin", "/data/", "/data//x", "/data/$HOME", `C:\data`, "/" + strings.Repeat("a", 241)} { m, d := fixture() s := d["services"].(map[string]any)["api"].(map[string]any) s["volumes"].([]any)[0].(map[string]any)["target"] = target raw, _ := json.Marshal(d) if Check(m, raw) == nil { t.Errorf("accepted protected/noncanonical target %q", target) } } for _, user := range []string{"1000:0", "01:1000", "+1:1000", "-1:1000", "4294967295:1000", "4294967296:1000", "1", "1:2:3"} { m, d := fixture() d["services"].(map[string]any)["api"].(map[string]any)["user"] = user raw, _ := json.Marshal(d) if Check(m, raw) == nil { t.Errorf("accepted ambiguous/root identity %q", user) } } } func TestTwoServicesRequireExactManifestAndSeparateVolumes(t *testing.T) { m, d := fixture() raw, _ := json.Marshal(d) var copyDoc map[string]any if err := json.Unmarshal(raw, ©Doc); err != nil { t.Fatal(err) } second := copyDoc["services"].(map[string]any)["api"].(map[string]any) second["volumes"].([]any)[0].(map[string]any)["source"] = "db-data" d["services"].(map[string]any)["db"] = second d["volumes"].(map[string]any)["db-data"] = map[string]any{} m.Components = append(m.Components, appbundle.Component{Name: "db", Image: m.Components[0].Image}) raw, _ = json.Marshal(d) if err := Check(m, raw); err != nil { t.Fatal("valid separate-service volumes rejected", err) } second["volumes"].([]any)[0].(map[string]any)["source"] = "data" delete(d["volumes"].(map[string]any), "db-data") raw, _ = json.Marshal(d) if Check(m, raw) == nil { t.Fatal("shared service data accepted") } } func TestOverlappingAndUnusedVolumesRejected(t *testing.T) { for _, target := range []string{"/data", "/data/nested", "/other"} { m, d := fixture() d["volumes"].(map[string]any)["second"] = map[string]any{} if target != "/other" { s := d["services"].(map[string]any)["api"].(map[string]any) s["volumes"] = append(s["volumes"].([]any), map[string]any{"type": "volume", "source": "second", "target": target, "read_only": false}) } raw, _ := json.Marshal(d) if Check(m, raw) == nil { t.Fatal("overlapping or unused volume accepted") } } }