From 3f125355245a4bc64585f8c5d1c9c5fa3d543775 Mon Sep 17 00:00:00 2001 From: Danielle Zephyr Malament Date: Tue, 17 Dec 2024 17:08:17 -0500 Subject: [PATCH] Remove all punctuation from anchors in policies.md --- examples/policies-no-rego.md | 34 ++++++++-- examples/policies.md | 50 ++++++++++++-- examples/policy_markdown_punctuation/src.rego | 31 +++++++++ .../policy_markdown_punctuation/src_test.rego | 7 ++ internal/commands/document.go | 65 ++++++++++++++++++- 5 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 examples/policy_markdown_punctuation/src.rego create mode 100644 examples/policy_markdown_punctuation/src_test.rego diff --git a/examples/policies-no-rego.md b/examples/policies-no-rego.md index 4661c7fc..d55fdd07 100644 --- a/examples/policies-no-rego.md +++ b/examples/policies-no-rego.md @@ -10,7 +10,7 @@ * [P1005: Pods must not run with access to the host IPC](#p1005-pods-must-not-run-with-access-to-the-host-ipc) * [P1006: Pods must not run with access to the host networking](#p1006-pods-must-not-run-with-access-to-the-host-networking) * [P1007: Pods must not run with access to the host PID namespace](#p1007-pods-must-not-run-with-access-to-the-host-pid-namespace) -* [P1008: Pods must run as non-root](#p1008-pods-must-run-as-non-root) +* [P1008: Pods must run as non\-root](#p1008-pods-must-run-as-non-root) * [P1009: PodSecurityPolicies must require all capabilities are dropped](#p1009-podsecuritypolicies-must-require-all-capabilities-are-dropped) * [P1010: PodSecurityPolicies must not allow privileged escalation](#p1010-podsecuritypolicies-must-not-allow-privileged-escalation) * [P1011: PodSecurityPolicies must not allow access to the host aliases](#p1011-podsecuritypolicies-must-not-allow-access-to-the-host-aliases) @@ -21,13 +21,14 @@ * [P2001: Images must not use the latest tag](#p2001-images-must-not-use-the-latest-tag) * [P2002: Containers must define resource constraints](#p2002-containers-must-define-resource-constraints) * [P2005: Roles must not allow use of privileged PodSecurityPolicies](#p2005-roles-must-not-allow-use-of-privileged-podsecuritypolicies) -* [P2006: Tenants' containers must not run as privileged](#p2006-tenants'-containers-must-not-run-as-privileged) +* [P2006: Tenants' containers must not run as privileged](#p2006-tenants-containers-must-not-run-as-privileged) ## Warnings * [P0001: Deprecated Deployment and DaemonSet API](#p0001-deprecated-deployment-and-daemonset-api) +* [P0003: Title \_wórds\_ with\-punct\_uation\_ \!"\#$%&'\(\)\*\+,\./:;\<=\>?@\[\\\]^\`\{\|\}~mark ](#p0003-title-_wórds_-with-punct_uation_-mark) * [P2003: Containers should not have a writable root filesystem](#p2003-containers-should-not-have-a-writable-root-filesystem) -* [P2004: PodSecurityPolicies should require that a read-only root filesystem is set](#p2004-podsecuritypolicies-should-require-that-a-read-only-root-filesystem-is-set) +* [P2004: PodSecurityPolicies should require that a read\-only root filesystem is set](#p2004-podsecuritypolicies-should-require-that-a-read-only-root-filesystem-is-set) ## P0002: Required Labels @@ -168,7 +169,7 @@ boundary. _source: [pod_deny_host_pid](pod_deny_host_pid)_ -## P1008: Pods must run as non-root +## P1008: Pods must run as non\-root **Severity:** Violation @@ -341,7 +342,7 @@ _source: [container_deny_without_resource_constraints](container_deny_without_re **Resources:** -- rbac.authorization.k8s.io/Role +- rbac\.authorization\.k8s\.io/Role Workloads not running in the exempted namespaces must not use PodSecurityPolicies with privileged permissions. @@ -359,7 +360,7 @@ _source: [role_deny_use_privileged_psps](role_deny_use_privileged_psps)_ - apps/Deployment - apps/StatefulSet -**MatchLabels:** is-tenant=true +**MatchLabels:** is\-tenant=true Privileged containers can easily escalate to root privileges on the node. As such containers running as privileged or with sufficient capabilities granted @@ -386,6 +387,25 @@ the version for both of these resources must be `apps/v1`. _source: [any_warn_deprecated_api_versions](any_warn_deprecated_api_versions)_ +## P0003: Title \_wórds\_ with\-punct\_uation\_ \!"\#$%&'\(\)\*\+,\./:;\<=\>?@\[\\\]^\`\{\|\}~mark + +**Severity:** Warning + +**Resources:** + +- \*/Pod + +**MatchLabels:** \_test\_=true + +**Parameters:** + +* \_param\_name\_: array of string + +This is only here to test and illustrate _punctuation_ / Markdown handling + + +_source: [policy_markdown_punctuation](policy_markdown_punctuation)_ + ## P2003: Containers should not have a writable root filesystem **Severity:** Warning @@ -403,7 +423,7 @@ important to make the root filesystem read-only. _source: [container_warn_no_ro_fs](container_warn_no_ro_fs)_ -## P2004: PodSecurityPolicies should require that a read-only root filesystem is set +## P2004: PodSecurityPolicies should require that a read\-only root filesystem is set **Severity:** Warning diff --git a/examples/policies.md b/examples/policies.md index ef86bf47..9aa32518 100755 --- a/examples/policies.md +++ b/examples/policies.md @@ -10,7 +10,7 @@ * [P1005: Pods must not run with access to the host IPC](#p1005-pods-must-not-run-with-access-to-the-host-ipc) * [P1006: Pods must not run with access to the host networking](#p1006-pods-must-not-run-with-access-to-the-host-networking) * [P1007: Pods must not run with access to the host PID namespace](#p1007-pods-must-not-run-with-access-to-the-host-pid-namespace) -* [P1008: Pods must run as non-root](#p1008-pods-must-run-as-non-root) +* [P1008: Pods must run as non\-root](#p1008-pods-must-run-as-non-root) * [P1009: PodSecurityPolicies must require all capabilities are dropped](#p1009-podsecuritypolicies-must-require-all-capabilities-are-dropped) * [P1010: PodSecurityPolicies must not allow privileged escalation](#p1010-podsecuritypolicies-must-not-allow-privileged-escalation) * [P1011: PodSecurityPolicies must not allow access to the host aliases](#p1011-podsecuritypolicies-must-not-allow-access-to-the-host-aliases) @@ -21,13 +21,14 @@ * [P2001: Images must not use the latest tag](#p2001-images-must-not-use-the-latest-tag) * [P2002: Containers must define resource constraints](#p2002-containers-must-define-resource-constraints) * [P2005: Roles must not allow use of privileged PodSecurityPolicies](#p2005-roles-must-not-allow-use-of-privileged-podsecuritypolicies) -* [P2006: Tenants' containers must not run as privileged](#p2006-tenants'-containers-must-not-run-as-privileged) +* [P2006: Tenants' containers must not run as privileged](#p2006-tenants-containers-must-not-run-as-privileged) ## Warnings * [P0001: Deprecated Deployment and DaemonSet API](#p0001-deprecated-deployment-and-daemonset-api) +* [P0003: Title \_wórds\_ with\-punct\_uation\_ \!"\#$%&'\(\)\*\+,\./:;\<=\>?@\[\\\]^\`\{\|\}~mark ](#p0003-title-_wórds_-with-punct_uation_-mark) * [P2003: Containers should not have a writable root filesystem](#p2003-containers-should-not-have-a-writable-root-filesystem) -* [P2004: PodSecurityPolicies should require that a read-only root filesystem is set](#p2004-podsecuritypolicies-should-require-that-a-read-only-root-filesystem-is-set) +* [P2004: PodSecurityPolicies should require that a read\-only root filesystem is set](#p2004-podsecuritypolicies-should-require-that-a-read-only-root-filesystem-is-set) ## P0002: Required Labels @@ -383,7 +384,7 @@ pod_has_hostpid if { _source: [pod_deny_host_pid](pod_deny_host_pid)_ -## P1008: Pods must run as non-root +## P1008: Pods must run as non\-root **Severity:** Violation @@ -812,7 +813,7 @@ _source: [container_deny_without_resource_constraints](container_deny_without_re **Resources:** -- rbac.authorization.k8s.io/Role +- rbac\.authorization\.k8s\.io/Role Workloads not running in the exempted namespaces must not use PodSecurityPolicies with privileged permissions. @@ -872,7 +873,7 @@ _source: [role_deny_use_privileged_psps](role_deny_use_privileged_psps)_ - apps/Deployment - apps/StatefulSet -**MatchLabels:** is-tenant=true +**MatchLabels:** is\-tenant=true Privileged containers can easily escalate to root privileges on the node. As such containers running as privileged or with sufficient capabilities granted @@ -954,6 +955,41 @@ warn contains msg if { _source: [any_warn_deprecated_api_versions](any_warn_deprecated_api_versions)_ +## P0003: Title \_wórds\_ with\-punct\_uation\_ \!"\#$%&'\(\)\*\+,\./:;\<=\>?@\[\\\]^\`\{\|\}~mark + +**Severity:** Warning + +**Resources:** + +- \*/Pod + +**MatchLabels:** \_test\_=true + +**Parameters:** + +* \_param\_name\_: array of string + +This is only here to test and illustrate _punctuation_ / Markdown handling + +### Rego + +```rego +package policy_markdown_punctuation + +import data.lib.core +import future.keywords.contains +import future.keywords.if + +policyID := "P0003" + +warn contains msg if { + core.apiVersion == "foo/bar" + msg := core.format_with_id("Title tester", policyID) +} +``` + +_source: [policy_markdown_punctuation](policy_markdown_punctuation)_ + ## P2003: Containers should not have a writable root filesystem **Severity:** Warning @@ -1003,7 +1039,7 @@ no_read_only_filesystem(container) if { _source: [container_warn_no_ro_fs](container_warn_no_ro_fs)_ -## P2004: PodSecurityPolicies should require that a read-only root filesystem is set +## P2004: PodSecurityPolicies should require that a read\-only root filesystem is set **Severity:** Warning diff --git a/examples/policy_markdown_punctuation/src.rego b/examples/policy_markdown_punctuation/src.rego new file mode 100644 index 00000000..1465a2b6 --- /dev/null +++ b/examples/policy_markdown_punctuation/src.rego @@ -0,0 +1,31 @@ +# METADATA +# title: " Title _wórds_ with-punct_uation_ !\"#$%&'()*+,./:;<=>?@[\\]^`{|}~mark " +# description: This is only here to test and illustrate _punctuation_ / +# Markdown handling +# custom: +# matchers: +# kinds: +# - apiGroups: +# - "*" +# kinds: +# - Pod +# labelSelector: +# matchLabels: +# _test_: "true" +# parameters: +# _param_name_: +# type: array +# items: +# type: string +package policy_markdown_punctuation + +import data.lib.core +import future.keywords.contains +import future.keywords.if + +policyID := "P0003" + +warn contains msg if { + core.apiVersion == "foo/bar" + msg := core.format_with_id("Title tester", policyID) +} diff --git a/examples/policy_markdown_punctuation/src_test.rego b/examples/policy_markdown_punctuation/src_test.rego new file mode 100644 index 00000000..0ee9f9ce --- /dev/null +++ b/examples/policy_markdown_punctuation/src_test.rego @@ -0,0 +1,7 @@ +package policy_markdown_punctuation + +import future.keywords.if + +test_ignoreme if { + count(warn) == 1 with input as {"apiVersion": "foo/bar"} +} diff --git a/internal/commands/document.go b/internal/commands/document.go index e3e98362..bffe51af 100644 --- a/internal/commands/document.go +++ b/internal/commands/document.go @@ -5,9 +5,11 @@ import ( "fmt" "os" "path/filepath" + "regexp" "sort" "strings" "text/template" + "unicode" "github.com/go-sprout/sprout/sprigin" "github.com/plexsystems/konstraint/internal/rego" @@ -39,6 +41,29 @@ type Document struct { //go:embed document_template.tpl var docTemplate string +var ( + // One or more spaces + multiSpaceRE = regexp.MustCompile(` +`) + + // Escape the characters on this list: https://www.markdownguide.org/basic-syntax/#characters-you-can-escape + // (Admittedly, a few of these seem... odd.) + markdownReplacer = strings.NewReplacer( + "\\", "\\\\", "`", "\\`", "*", "\\*", "_", "\\_", "{", "\\{", "}", "\\}", "[", "\\[", "]", "\\]", "<", "\\<", + ">", "\\>", "(", "\\(", ")", "\\)", "#", "\\#", "+", "\\+", "-", "\\-", ".", "\\.", "!", "\\!", "|", "\\|", + ) + + // Space -> -, remove all ASCII punctuation except - and _ + // + // (This is part of the GitHub anchor algorithm, but see below regarding tabs and other whitespace. Ref: + // https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#section-links) + anchorReplacer = strings.NewReplacer( + " ", "-", + "!", "", "\"", "", "#", "", "$", "", "%", "", "&", "", "'", "", "(", "", ")", "", "*", "", "+", "", ",", "", + ".", "", "/", "", ":", "", ";", "", "<", "", "=", "", ">", "", "?", "", "@", "", "[", "", "\\", "", "]", "", + "^", "", "`", "", "{", "", "|", "", "}", "", "~", "", + ) +) + func newDocCommand() *cobra.Command { cmd := cobra.Command{ Use: "doc ", @@ -186,8 +211,37 @@ func getDocumentation(path string, outputDirectory string) (map[rego.Severity][] documentTitle = fmt.Sprintf("%s: %s", policy.PolicyID(), documentTitle) } - anchor := strings.ToLower(strings.ReplaceAll(documentTitle, " ", "-")) - anchor = strings.ReplaceAll(anchor, ":", "") + // Tabs in Markdown headings are handled inconsistently across different parsers when it comes to matching + // anchors; some expect them to be removed (which is what GitHub does), while some expect them to be changed to + // -. Changing tabs to spaces beforehand solves that problem, and we'll handle other whitespace the same way + // just in case. Incidentally, handling of other Unicode characters varies; for instance, letters with accents + // are handled normally, but emoji seem to break some parsers, and a circled s (ⓢ, U+24E2) works, but + // markdownlint complains about it. Here, I think, is the place to draw the line in terms of how much + // intervention to do to be sure the anchors work. + var spacedDocumentTitle strings.Builder + for _, rune := range documentTitle { + if unicode.IsSpace(rune) { + spacedDocumentTitle.WriteString(" ") + } else { + spacedDocumentTitle.WriteString(string(rune)) + } + } + documentTitle = spacedDocumentTitle.String() + + // Similarly, parsers differ in whether they collapse multiple spaces when matching anchors. The safe thing is + // to collapse them before it can become an issue. + documentTitle = multiSpaceRE.ReplaceAllString(documentTitle, " ") + + // The GitHub anchor algorithm says that Markdown is removed before conversion. However, '_foo_bar_' counts as + // 'foo_bar' in italics (which are removed), whereas 'foo_bar' is just text. Since only full parsing can + // determine what is actually functional Markdown, it's safest to just escape all of the Markdown characters. + // (That means that Markdown won't work in titles, but it's probably a reasonable tradeoff.) Plus, of course, + // some characters (such as []) would actually break the generated link otherwise. + documentTitle = markdownReplacer.Replace(documentTitle) + + // Skip non-U+0020-whitespace and Markdown removal because we handled them above. Ref: + // https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#section-links + anchor := anchorReplacer.Replace(strings.TrimSpace(strings.ToLower(documentTitle))) legacyMatchers, err := policy.Matchers() if err != nil { @@ -209,6 +263,9 @@ func getDocumentation(path string, outputDirectory string) (map[rego.Severity][] logger.Warn("No kind matchers set, this can lead to poor policy performance.") matchResources = append(matchResources, "Any Resource") } + for i := range matchResources { + matchResources[i] = markdownReplacer.Replace(matchResources[i]) + } var matchLabels string if policy.AnnotationLabelSelectorMatcher() != nil { @@ -216,6 +273,7 @@ func getDocumentation(path string, outputDirectory string) (map[rego.Severity][] } else { matchLabels = legacyMatchers.MatchLabelsMatcher.String() } + matchLabels = markdownReplacer.Replace(matchLabels) parameters := policy.Parameters() if len(policy.AnnotationParameters()) > 0 { @@ -224,6 +282,9 @@ func getDocumentation(path string, outputDirectory string) (map[rego.Severity][] sort.Slice(parameters, func(i, j int) bool { return parameters[i].Name < parameters[j].Name }) + for i := range parameters { + parameters[i].Name = markdownReplacer.Replace(parameters[i].Name) + } header := Header{ Title: documentTitle,