diff --git a/tools/refgen/security.go b/tools/refgen/security.go index 91e13e1..3a0535b 100644 --- a/tools/refgen/security.go +++ b/tools/refgen/security.go @@ -10,6 +10,9 @@ import ( "strings" ) +// limitsHeading is the README's section on what the model does not protect, beside its open items. +const limitsHeading = "What this does not protect" + // securityPages writes the security model's rules and its open items from spin's own // docs/security/README.md, where code cites each rule at the line that keeps it and a gate fails // the build when one is broken. A page that restated them went stale within a week of being @@ -53,6 +56,12 @@ func securityPages(spin, commit string) (map[string][]byte, error) { "[its rules](/reference/security/rules/).\n", source, commit[:9]) gaps.Write(bytes.TrimRight(open, "\n")) gaps.WriteString("\n") + // Older tags have no such section: the page is built from whichever tag the site documents. + if limits := section(raw, "## "+limitsHeading); limits != nil { + gaps.WriteString("\n## " + limitsHeading + "\n") + gaps.Write(bytes.TrimRight(limits, "\n")) + gaps.WriteString("\n") + } return map[string][]byte{ filepath.Join("security", "rules.md"): []byte(rules.String()), diff --git a/tools/refgen/security_test.go b/tools/refgen/security_test.go new file mode 100644 index 0000000..e1e8f67 --- /dev/null +++ b/tools/refgen/security_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// The known gaps are the README's open items and, where its tag has the section, what the model does +// not protect: a limit moved between the two stays on the page. +func TestKnownGapsCarryWhatTheModelDoesNotProtect(t *testing.T) { + const rules = "| I1 | a rule | keeps | gate |\n" + for name, tc := range map[string]struct { + readme string + want []string + }{ + "with the section": { + readme: rules + "\n## Open\n\n- an open item\n\n## What this does not protect\n\n- a limit\n\n## Next\n\nelse\n", + want: []string{"- an open item", "## What this does not protect", "- a limit"}, + }, + "without it": { + readme: rules + "\n## Open\n\n- an open item\n", + want: []string{"- an open item"}, + }, + } { + t.Run(name, func(t *testing.T) { + spin := t.TempDir() + path := filepath.Join(spin, "docs", "security", "README.md") + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(tc.readme), 0o644); err != nil { + t.Fatal(err) + } + pages, err := securityPages(spin, "0123456789abcdef") + if err != nil { + t.Fatal(err) + } + page := string(pages[filepath.Join("security", "known-gaps.md")]) + for _, want := range tc.want { + if !strings.Contains(page, want) { + t.Errorf("the page does not say %q:\n%s", want, page) + } + } + if strings.Contains(page, "else") { + t.Errorf("the page runs past its sections:\n%s", page) + } + }) + } +}