diff --git a/cetusguard/rule.go b/cetusguard/rule.go index 8380a38..a0c45fc 100644 --- a/cetusguard/rule.go +++ b/cetusguard/rule.go @@ -32,7 +32,7 @@ var ( "DOMAIN_OR_IP": `(?:%DOMAIN%|%IP%)`, "HOST": `(?:%DOMAIN_OR_IP%(?::[0-9]+)?)`, - "IMAGE_ID": `(?:[a-fA-F0-9]+)`, + "IMAGE_ID": `%_OBJECT_ID%`, "IMAGE_COMPONENT": `(?:[a-zA-Z0-9]+(?:(?:\.|_{1,2}|-+)[a-zA-Z0-9]+)*)`, "IMAGE_TAG": `(?:[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127})`, "IMAGE_DIGEST": `(?:[a-zA-Z][a-zA-Z0-9]*(?:[_.+-][a-zA-Z][a-zA-Z0-9]*)*:[a-fA-F0-9]{32,})`, @@ -40,46 +40,50 @@ var ( "IMAGE_REFERENCE": `(?:%IMAGE_NAME%(?::%IMAGE_TAG%)?(?:@%IMAGE_DIGEST%)?)`, "IMAGE_ID_OR_REFERENCE": `(?:%IMAGE_ID%|%IMAGE_REFERENCE%)`, - "CONTAINER_ID": `(?:[a-fA-F0-9]+)`, - "CONTAINER_NAME": `(?:[a-zA-Z0-9][a-zA-Z0-9_.-]+)`, + "CONTAINER_ID": `%_OBJECT_ID%`, + "CONTAINER_NAME": `%_OBJECT_NAME%`, "CONTAINER_ID_OR_NAME": `(?:%CONTAINER_ID%|%CONTAINER_NAME%)`, - "VOLUME_ID": `(?:[a-fA-F0-9]+)`, - "VOLUME_NAME": `(?:[a-zA-Z0-9][a-zA-Z0-9_.-]+)`, + "VOLUME_ID": `%_OBJECT_ID%`, + "VOLUME_NAME": `%_OBJECT_NAME%`, "VOLUME_ID_OR_NAME": `(?:%VOLUME_ID%|%VOLUME_NAME%)`, - "NETWORK_ID": `(?:[a-fA-F0-9]+)`, + "NETWORK_ID": `%_OBJECT_ID%`, "NETWORK_NAME": `(?:[^/]+)`, "NETWORK_ID_OR_NAME": `(?:%NETWORK_ID%|%NETWORK_NAME%)`, - "PLUGIN_ID": `(?:[a-fA-F0-9]+)`, + "PLUGIN_ID": `%_OBJECT_ID%`, "PLUGIN_NAME": `%IMAGE_NAME%`, "PLUGIN_ID_OR_NAME": `(?:%PLUGIN_ID%|%PLUGIN_NAME%)`, - "API_PREFIX": `(?:/v[0-9]+(?:\.[0-9]+)*)?`, - "API_PREFIX_AUTH": `%API_PREFIX%/auth`, - "API_PREFIX_BUILD": `%API_PREFIX%/build`, - "API_PREFIX_COMMIT": `%API_PREFIX%/commit`, - "API_PREFIX_CONFIGS": `%API_PREFIX%/configs`, - "API_PREFIX_CONTAINERS": `%API_PREFIX%/containers`, - "API_PREFIX_DISTRIBUTION": `%API_PREFIX%/distribution`, - "API_PREFIX_EVENTS": `%API_PREFIX%/events`, - "API_PREFIX_EXEC": `%API_PREFIX%/exec`, - "API_PREFIX_GRPC": `%API_PREFIX%/grpc`, - "API_PREFIX_IMAGES": `%API_PREFIX%/images`, - "API_PREFIX_INFO": `%API_PREFIX%/info`, - "API_PREFIX_NETWORKS": `%API_PREFIX%/networks`, - "API_PREFIX_NODES": `%API_PREFIX%/nodes`, - "API_PREFIX_PING": `%API_PREFIX%/_ping`, - "API_PREFIX_PLUGINS": `%API_PREFIX%/plugins`, - "API_PREFIX_SECRETS": `%API_PREFIX%/secrets`, - "API_PREFIX_SERVICES": `%API_PREFIX%/services`, - "API_PREFIX_SESSION": `%API_PREFIX%/session`, - "API_PREFIX_SWARM": `%API_PREFIX%/swarm`, - "API_PREFIX_SYSTEM": `%API_PREFIX%/system`, - "API_PREFIX_TASKS": `%API_PREFIX%/tasks`, - "API_PREFIX_VERSION": `%API_PREFIX%/version`, - "API_PREFIX_VOLUMES": `%API_PREFIX%/volumes`, + "API_PREFIX": `(?:/v[0-9]+(?:\.[0-9]+)*)`, + "API_PREFIX_AUTH": `%API_PREFIX%?/auth`, + "API_PREFIX_BUILD": `%API_PREFIX%?/build`, + "API_PREFIX_COMMIT": `%API_PREFIX%?/commit`, + "API_PREFIX_CONFIGS": `%API_PREFIX%?/configs`, + "API_PREFIX_CONTAINERS": `%API_PREFIX%?/containers`, + "API_PREFIX_DISTRIBUTION": `%API_PREFIX%?/distribution`, + "API_PREFIX_EVENTS": `%API_PREFIX%?/events`, + "API_PREFIX_EXEC": `%API_PREFIX%?/exec`, + "API_PREFIX_GRPC": `%API_PREFIX%?/grpc`, + "API_PREFIX_IMAGES": `%API_PREFIX%?/images`, + "API_PREFIX_INFO": `%API_PREFIX%?/info`, + "API_PREFIX_NETWORKS": `%API_PREFIX%?/networks`, + "API_PREFIX_NODES": `%API_PREFIX%?/nodes`, + "API_PREFIX_PING": `%API_PREFIX%?/_ping`, + "API_PREFIX_PLUGINS": `%API_PREFIX%?/plugins`, + "API_PREFIX_SECRETS": `%API_PREFIX%?/secrets`, + "API_PREFIX_SERVICES": `%API_PREFIX%?/services`, + "API_PREFIX_SESSION": `%API_PREFIX%?/session`, + "API_PREFIX_SWARM": `%API_PREFIX%?/swarm`, + "API_PREFIX_SYSTEM": `%API_PREFIX%?/system`, + "API_PREFIX_TASKS": `%API_PREFIX%?/tasks`, + "API_PREFIX_VERSION": `%API_PREFIX%?/version`, + "API_PREFIX_VOLUMES": `%API_PREFIX%?/volumes`, + + // Private built-ins, may change in any version + "_OBJECT_ID": `(?:[a-fA-F0-9]+)`, + "_OBJECT_NAME": `(?:[a-zA-Z0-9][a-zA-Z0-9_.-]+)`, } ) diff --git a/cetusguard/rule_test.go b/cetusguard/rule_test.go index 72b8afa..9eeaeef 100644 --- a/cetusguard/rule_test.go +++ b/cetusguard/rule_test.go @@ -44,19 +44,19 @@ func TestBuildDefaultRules(t *testing.T) { func TestBuildValidRules(t *testing.T) { rawRules := map[string]Rule{ - "! Comment\nGET,HEAD %API_PREFIX%/test01\n": { + "! Comment\nGET,HEAD %API_PREFIX%?/test01\n": { Methods: map[string]bool{"GET": true, "HEAD": true}, Pattern: regexp.MustCompile(`^(?:/v[0-9]+(?:\.[0-9]+)*)?/test01$`), }, - "! Comment\r\nGET,HEAD %API_PREFIX%/test02\r\n": { + "! Comment\r\nGET,HEAD %API_PREFIX%?/test02\r\n": { Methods: map[string]bool{"GET": true, "HEAD": true}, Pattern: regexp.MustCompile(`^(?:/v[0-9]+(?:\.[0-9]+)*)?/test02$`), }, - "\n\n\n! Comment\n\n\nGET,HEAD %API_PREFIX%/test03\n\n\n": { + "\n\n\n! Comment\n\n\nGET,HEAD %API_PREFIX%?/test03\n\n\n": { Methods: map[string]bool{"GET": true, "HEAD": true}, Pattern: regexp.MustCompile(`^(?:/v[0-9]+(?:\.[0-9]+)*)?/test03$`), }, - " \t ! Comment\n \t GET,HEAD \t %API_PREFIX%/test04 \t ": { + " \t ! Comment\n \t GET,HEAD \t %API_PREFIX%?/test04 \t ": { Methods: map[string]bool{"GET": true, "HEAD": true}, Pattern: regexp.MustCompile(`^(?:/v[0-9]+(?:\.[0-9]+)*)?/test04$`), }, @@ -78,14 +78,14 @@ func TestBuildValidRules(t *testing.T) { func TestBuildInvalidRules(t *testing.T) { rawRules := []string{ - "%API_PREFIX%/test01", - ", %API_PREFIX%/test02", - "GET, %API_PREFIX%/test03", - "GET,HEAD, %API_PREFIX%/test04", - "GET %API_PREFIX%/[9-0]+/test05", - "GET %API_PREFIX%/\x81/test06", - "GET\n%API_PREFIX%/test07", - "GET\r\n%API_PREFIX%/test08", + "%API_PREFIX%?/test01", + ", %API_PREFIX%?/test02", + "GET, %API_PREFIX%?/test03", + "GET,HEAD, %API_PREFIX%?/test04", + "GET %API_PREFIX%?/[9-0]+/test05", + "GET %API_PREFIX%?/\x81/test06", + "GET\n%API_PREFIX%?/test07", + "GET\r\n%API_PREFIX%?/test08", } for _, v := range rawRules { @@ -171,3 +171,240 @@ func TestBuildRulesFromNonexistentPath(t *testing.T) { t.Errorf("builtRules = %v, want an error", builtRules) } } + +func TestDomainRegex(t *testing.T) { + re := regexp.MustCompile("^" + ruleBuiltins["DOMAIN"] + "$") + + testCases := map[string]bool{ + "": false, + "l": true, + "localhost": true, + "-localhost": false, + "localhost-": false, + "sub.example.test": true, + "sub.-example.test": false, + "sub.example-.test": false, + "001.test": true, + "xn--7o8h.test": true, + "a.a.a.a.a.a.a": true, + "a.a.a...a.a.a": false, + } + + for input, wanted := range testCases { + if result := re.MatchString(input); result != wanted { + t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) + } + } +} + +func TestIpv4Regex(t *testing.T) { + re := regexp.MustCompile("^" + ruleBuiltins["IPV4"] + "$") + + testCases := map[string]bool{ + "": false, + "0": false, + "0.0": false, + "0.0.0.0.0": false, + "0.0.0.0": true, + "255.255.255.255": true, + "1111.0.0.0": false, + } + + for input, wanted := range testCases { + if result := re.MatchString(input); result != wanted { + t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) + } + } +} + +func TestIpv6Regex(t *testing.T) { + re := regexp.MustCompile("^" + ruleBuiltins["IPV6"] + "$") + + testCases := map[string]bool{ + "": false, + "::": false, + "[]": false, + "[::]": true, + "[::1]": true, + "[f:f:f:f:f:f:f:f]": true, + "[f:f:f:f:f:f:f:x]": false, + "[f:f:f:f:f:f:f:f:f]": false, + "[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]": true, + } + + for input, wanted := range testCases { + if result := re.MatchString(input); result != wanted { + t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) + } + } +} + +func TestHostRegex(t *testing.T) { + re := regexp.MustCompile("^" + ruleBuiltins["HOST"] + "$") + + testCases := map[string]bool{ + "": false, + "localhost": true, + "localhost:": false, + "localhost:2375": true, + "localhost:aaaa": false, + "sub.example.test": true, + "sub.example.test:": false, + "sub.example.test:2375": true, + "sub.example.test:aaaa": false, + "127.0.0.1": true, + "127.0.0.1:": false, + "127.0.0.1:2375": true, + "127.0.0.1:aaaa": false, + "[::1]": true, + "[::1]:": false, + "[::1]:2375": true, + "[::1]:aaaa": false, + } + + for input, wanted := range testCases { + if result := re.MatchString(input); result != wanted { + t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) + } + } +} + +func TestObjectIdRegex(t *testing.T) { + re := regexp.MustCompile("^" + ruleBuiltins["_OBJECT_ID"] + "$") + + testCases := map[string]bool{ + "": false, + "0123456789abcdef": true, + "0123456789x": false, + } + + for input, wanted := range testCases { + if result := re.MatchString(input); result != wanted { + t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) + } + } +} + +func TestObjectNameRegex(t *testing.T) { + re := regexp.MustCompile("^" + ruleBuiltins["_OBJECT_NAME"] + "$") + + testCases := map[string]bool{ + "": false, + "x": false, + "x0": true, + "0x": true, + "xx": true, + "xx_.-": true, + "-xx": false, + "busybox": true, + } + + for input, wanted := range testCases { + if result := re.MatchString(input); result != wanted { + t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) + } + } +} + +func TestImageReferenceRegex(t *testing.T) { + re := regexp.MustCompile("^" + ruleBuiltins["IMAGE_REFERENCE"] + "$") + + testCases := map[string]bool{ + "": false, + "b": true, + "busybox": true, + "busybox:": false, + "busybox:l": true, + "busybox:latest": true, + "busybox:latest@": false, + "busybox:latest@sha256": false, + "busybox:latest@sha256:": false, + "busybox:latest@sha256:x": false, + "busybox:latest@sha256:09c731d73926315908778730f9e6068": false, + "busybox:latest@sha256:09c731d73926315908778730f9e60686": true, + "busybox:latest@sha256:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, + "busybox@sha256:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, + "busybox@sha256-:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": false, + "busybox@sha256-test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, + "busybox@sha256--test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": false, + "busybox@sha256-0test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": false, + "busybox@sha256-test0:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, + "busybox@sha256_test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, + "busybox@sha256.test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, + "busybox@sha256+test:09c731d73926315908778730f9e606864fb72f1523d5c1c81c02dc51563885ba": true, + "docker.io/busybox:latest": true, + "docker.io/library/busybox:latest": true, + "localhost:5000/library/busybox:latest": true, + "127.0.0.1:5000/library/busybox:latest": true, + "[::1]:5000/library/busybox:latest": true, + "-busybox:latest": false, + "busybox:-latest": false, + "docker.io/-library/busybox:latest": false, + "docker.io/foo/bar/busybox:latest": true, + "docker.io/foo.bar/busybox:latest": true, + "docker.io/foo..bar/busybox:latest": false, + "docker.io/foo_bar/busybox:latest": true, + "docker.io/foo__bar/busybox:latest": true, + "docker.io/foo___bar/busybox:latest": false, + "docker.io/foo-bar/busybox:latest": true, + "docker.io/foo--bar/busybox:latest": true, + "docker.io/foo---bar/busybox:latest": true, + } + + for input, wanted := range testCases { + if result := re.MatchString(input); result != wanted { + t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) + } + } +} + +func TestApiPrefixRegex(t *testing.T) { + re := regexp.MustCompile("^" + ruleBuiltins["API_PREFIX"] + "$") + + testCases := map[string]bool{ + "": false, + "/": false, + "/v": false, + "/v9": true, + "/v99": true, + "/v99.9": true, + "/v99.99": true, + "/v99.99.9": true, + "/v99.99.99": true, + "/9.9": false, + "/v9.9/": false, + "/v9.9.": false, + "/v.9.9": false, + "/v9..9": false, + "/va.a": false, + } + + for input, wanted := range testCases { + if result := re.MatchString(input); result != wanted { + t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) + } + } +} + +func TestApiPrefixPing(t *testing.T) { + re := regexp.MustCompile("^" + ruleBuiltins["API_PREFIX_PING"] + "$") + + testCases := map[string]bool{ + "": false, + "/": false, + "/_ping": true, + "/_ping/": false, + "/_pong": false, + "_ping": false, + "//_ping": false, + "/v9.9/": false, + "/v9.9/_ping": true, + "v9.9/_ping": false, + } + + for input, wanted := range testCases { + if result := re.MatchString(input); result != wanted { + t.Errorf("\"%s\" match = %t, want = %t", input, result, wanted) + } + } +}