diff --git a/cmd/lagoon-linter/validate.go b/cmd/lagoon-linter/validate.go index 8fa5324..cf48201 100644 --- a/cmd/lagoon-linter/validate.go +++ b/cmd/lagoon-linter/validate.go @@ -1,6 +1,6 @@ package main -import "github.com/amazeeio/lagoon-linter/internal/lagoonyml" +import "github.com/uselagoon/lagoon-linter/internal/lagoonyml" // ValidateCmd represents the validate command. type ValidateCmd struct { @@ -9,5 +9,5 @@ type ValidateCmd struct { // Run the validation of the Lagoon YAML. func (cmd *ValidateCmd) Run() error { - return lagoonyml.Lint(`.lagoon.yml`, lagoonyml.RouteAnnotation()) + return lagoonyml.Lint(cmd.LagoonYAML, lagoonyml.RouteAnnotation()) } diff --git a/go.mod b/go.mod index 53d5f16..e1c7406 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/amazeeio/lagoon-linter +module github.com/uselagoon/lagoon-linter go 1.17 diff --git a/internal/lagoonyml/lint_test.go b/internal/lagoonyml/lint_test.go index 2e4571d..d8b1401 100644 --- a/internal/lagoonyml/lint_test.go +++ b/internal/lagoonyml/lint_test.go @@ -3,7 +3,7 @@ package lagoonyml_test import ( "testing" - "github.com/amazeeio/lagoon-linter/internal/lagoonyml" + "github.com/uselagoon/lagoon-linter/internal/lagoonyml" ) func TestLint(t *testing.T) { @@ -15,8 +15,12 @@ func TestLint(t *testing.T) { input: "testdata/valid.lagoon.yml", valid: true, }, - "invalid .lagoon.yml": { - input: "testdata/invalid.lagoon.yml", + "invalid.0.lagoon.yml": { + input: "testdata/invalid.0.lagoon.yml", + valid: false, + }, + "invalid.1.lagoon.yml": { + input: "testdata/invalid.1.lagoon.yml", valid: false, }, } diff --git a/internal/lagoonyml/routeannotation.go b/internal/lagoonyml/routeannotation.go index 4235c7d..dafb464 100644 --- a/internal/lagoonyml/routeannotation.go +++ b/internal/lagoonyml/routeannotation.go @@ -3,16 +3,35 @@ package lagoonyml import ( "fmt" "regexp" + "strings" ) -// ServerSnippet is the annotation for server snippets with ingress-nginx. -const ServerSnippet = "nginx.ingress.kubernetes.io/server-snippet" +const ( + authSnippet = "nginx.ingress.kubernetes.io/auth-snippet" + configurationSnippet = "nginx.ingress.kubernetes.io/configuration-snippet" + modsecuritySnippet = "nginx.ingress.kubernetes.io/modsecurity-snippet" + serverSnippet = "nginx.ingress.kubernetes.io/server-snippet" +) // validSnippets is the allow-list of snippets that Lagoon will accept. -var validSnippets []*regexp.Regexp = []*regexp.Regexp{ - regexp.MustCompile(`^(rewrite +[^; ]+ +[^; ]+( (last|break|redirect|permanent))?;\n?)+$`), - regexp.MustCompile(`^(add_header +[^; ]+ +[^;]+;\n?)+$`), - regexp.MustCompile(`^(set_real_ip_from +[^; ]+;\n?)+$`), +var validServerSnippets = regexp.MustCompile( + `^(rewrite +[^; ]+ +[^; ]+( (last|break|redirect|permanent))?;|` + + `add_header +[^; ]+ +[^;]+;|` + + `set_real_ip_from +[^; ]+;|` + + ` )+$`) + +// validate returns true if the annotations are valid, and false otherwise. +func validate(annotations map[string]string, r *regexp.Regexp, + annotation string) (string, bool) { + if ss, ok := annotations[annotation]; ok { + for _, line := range strings.Split(ss, "\n") { + line = strings.TrimSpace(line) + if len(line) > 0 && !r.MatchString(line) { + return line, false + } + } + } + return "", true } // RouteAnnotation checks for valid annotations on defined routes. @@ -22,20 +41,34 @@ func RouteAnnotation() Linter { for _, routeMap := range e.Routes { for rName, lagoonRoutes := range routeMap { for _, lagoonRoute := range lagoonRoutes { - for iName, route := range lagoonRoute.Ingresses { - if ss, ok := route.Annotations[ServerSnippet]; ok { - valid := false - for _, v := range validSnippets { - if v.MatchString(ss) { - valid = true - break - } - } - if !valid { - return fmt.Errorf( - "invalid %s annotation on environment %s, route %s, ingress %s: %s", - ServerSnippet, eName, rName, iName, ss) - } + for iName, ingress := range lagoonRoute.Ingresses { + // auth-snippet + if _, ok := ingress.Annotations[authSnippet]; ok { + return fmt.Errorf( + "invalid %s annotation on environment %s, route %s, ingress %s: %s", + authSnippet, eName, rName, iName, + "this annotation is restricted") + } + // configuration-snippet + if _, ok := ingress.Annotations[configurationSnippet]; ok { + return fmt.Errorf( + "invalid %s annotation on environment %s, route %s, ingress %s: %s", + configurationSnippet, eName, rName, iName, + "this annotation is restricted") + } + // modsecurity-snippet + if _, ok := ingress.Annotations[modsecuritySnippet]; ok { + return fmt.Errorf( + "invalid %s annotation on environment %s, route %s, ingress %s: %s", + modsecuritySnippet, eName, rName, iName, + "this annotation is restricted") + } + // server-snippet + if annotation, ok := validate(ingress.Annotations, + validServerSnippets, serverSnippet); !ok { + return fmt.Errorf( + "invalid %s annotation on environment %s, route %s, ingress %s: %s", + serverSnippet, eName, rName, iName, annotation) } } } diff --git a/internal/lagoonyml/routeannotation_test.go b/internal/lagoonyml/routeannotation_test.go index 1d6f7b8..0297363 100644 --- a/internal/lagoonyml/routeannotation_test.go +++ b/internal/lagoonyml/routeannotation_test.go @@ -1,12 +1,10 @@ -package lagoonyml_test +package lagoonyml import ( "testing" - - "github.com/amazeeio/lagoon-linter/internal/lagoonyml" ) -func TestRouteAnnotation(t *testing.T) { +func TestServerSnippet(t *testing.T) { var testCases = map[string]struct { input string valid bool @@ -39,8 +37,12 @@ func TestRouteAnnotation(t *testing.T) { input: "add_header X-branch \"#main\";\n", valid: true, }, - "invalid double add_header": { + "valid double add_header": { input: "add_header X-Robots-Tag \"noindex, nofollow\"; add_header X-Robots-Tag \"noindex, nofollow\";", + valid: true, + }, + "invalid more_set_header": { + input: "more_set_headers \"Strict-Transport-Security: max-age=31536000\";\n", valid: false, }, "valid set_real_ip_from": { @@ -54,17 +56,76 @@ func TestRouteAnnotation(t *testing.T) { } for name, tc := range testCases { t.Run(name, func(tt *testing.T) { - l := lagoonyml.Lagoon{ - Environments: map[string]lagoonyml.Environment{ + l := Lagoon{ + Environments: map[string]Environment{ + "testenv": { + Routes: []map[string][]LagoonRoute{ + { + "nginx": { + { + Ingresses: map[string]Ingress{ + "www.example.com": { + Annotations: map[string]string{ + serverSnippet: tc.input, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + err := RouteAnnotation()(&l) + if tc.valid { + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + } else { + if err == nil { + tt.Fatalf("expected error, but got nil") + } + } + }) + } +} + +func TestRestrictedSnippets(t *testing.T) { + var testCases = map[string]struct { + input string + valid bool + }{ + "restrict configuration-snippet": { + input: "nginx.ingress.kubernetes.io/configuration-snippet", + valid: false, + }, + "restrict modsecurity-snippet": { + input: "nginx.ingress.kubernetes.io/modsecurity-snippet", + valid: false, + }, + "restrict auth-snippet": { + input: "nginx.ingress.kubernetes.io/auth-snippet", + valid: false, + }, + "allow whitelist-source-range": { + input: "nginx.ingress.kubernetes.io/whitelist-source-range", + valid: true, + }, + } + for name, tc := range testCases { + t.Run(name, func(tt *testing.T) { + l := Lagoon{ + Environments: map[string]Environment{ "testenv": { - Routes: []map[string][]lagoonyml.LagoonRoute{ + Routes: []map[string][]LagoonRoute{ { "nginx": { { - Ingresses: map[string]lagoonyml.Ingress{ + Ingresses: map[string]Ingress{ "www.example.com": { Annotations: map[string]string{ - lagoonyml.ServerSnippet: tc.input, + tc.input: "any value", }, }, }, @@ -75,7 +136,7 @@ func TestRouteAnnotation(t *testing.T) { }, }, } - err := lagoonyml.RouteAnnotation()(&l) + err := RouteAnnotation()(&l) if tc.valid { if err != nil { tt.Fatalf("unexpected error %v", err) diff --git a/internal/lagoonyml/testdata/invalid.lagoon.yml b/internal/lagoonyml/testdata/invalid.0.lagoon.yml similarity index 100% rename from internal/lagoonyml/testdata/invalid.lagoon.yml rename to internal/lagoonyml/testdata/invalid.0.lagoon.yml diff --git a/internal/lagoonyml/testdata/invalid.1.lagoon.yml b/internal/lagoonyml/testdata/invalid.1.lagoon.yml new file mode 100644 index 0000000..d2c8c22 --- /dev/null +++ b/internal/lagoonyml/testdata/invalid.1.lagoon.yml @@ -0,0 +1,18 @@ +environments: + main: + monitoring_urls: + - "https://www.example.com" + - "https://www.example.com/special_page" + routes: + - nginx: + - example.com + - "www.example.com": + tls-acme: 'true' + insecure: Redirect + hsts: max-age=31536000 + - "example.com": + annotations: + nginx.ingress.kubernetes.io/server-snippet: | + rewrite ^/redirect-test(.*) https://www.example.com/redirect-test$1 permanent; + nginx.ingress.kubernetes.io/configuration-snippet: | + more_set_headers "Strict-Transport-Security: max-age=31536000; includeSubDomains"; diff --git a/internal/lagoonyml/testdata/valid.lagoon.yml b/internal/lagoonyml/testdata/valid.lagoon.yml index a731274..a375078 100644 --- a/internal/lagoonyml/testdata/valid.lagoon.yml +++ b/internal/lagoonyml/testdata/valid.lagoon.yml @@ -14,3 +14,8 @@ environments: annotations: nginx.ingress.kubernetes.io/server-snippet: | set_real_ip_from 1.2.3.4/32; + - "dev.example.com": + annotations: + nginx.ingress.kubernetes.io/server-snippet: | + set_real_ip_from 1.2.3.4/32; + add_header Content-type text/plain;