oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package guards
2
3import (
4 "go/ast"
5 "go/parser"
6 "go/token"
7 "strconv"
8 "strings"
9 "testing"
10)
11
12// Gate 9 (§12) scans what this app logs through slog. Anything that
13// writes somewhere else is not scanned and cannot be — so the rule is
14// that nothing else may write.
15//
16// This is not theoretical. chi's own middleware.Logger builds
17// log.New(os.Stdout, "", log.LstdFlags) at package init and writes the
18// request line — URL included — straight to stdout, touching neither
19// slog nor the log package default. middleware.Recoverer writes the
20// panic and stack to os.Stderr the same way. Either one is the most
21// natural thing for someone to reach for when adding request logging in
22// step 2, and either one would carry an address past a green gate.
23const chiMiddleware = "github.com/go-chi/chi/v5/middleware"
24
25// forbiddenSelectors are names that write outside slog, keyed by IMPORT
26// PATH rather than by the identifier they happen to be spelled with.
27// os.Stdout/os.Stderr are the raw sinks; the log package's own printers
28// go to its default writer, which is not the recorder either.
29//
30// Keying on the path is what makes `import goos "os"; goos.Stderr` fail
31// too. An earlier version compared the identifier to the literal "os",
32// which an alias walks straight past — and which also fired on a local
33// variable that happened to be named log.
34var forbiddenSelectors = map[string]map[string]string{
35 "os": {
36 "Stdout": "writes outside slog, so gate 9 cannot see it",
37 "Stderr": "writes outside slog, so gate 9 cannot see it",
38 },
39 "log": {
40 "New": "builds a logger gate 9 cannot capture; use slog",
41 "Print": "writes to the log default, not slog",
42 "Printf": "writes to the log default, not slog",
43 "Println": "writes to the log default, not slog",
44 "Fatal": "writes to the log default, not slog",
45 "Fatalf": "writes to the log default, not slog",
46 },
47}
48
49// TestNoLogSinkOutsideSlog fails on any non-test file that imports chi's
50// middleware package or names a writer gate 9 cannot observe.
51//
52// cmd/home/main.go is in scope: it is where a stray fmt.Fprintln(os.Stderr)
53// would most plausibly land. os.Exit and os.Getenv are untouched by this
54// rule — only the two stream variables are named.
55func TestNoLogSinkOutsideSlog(t *testing.T) {
56 root := repoRoot(t)
57 for _, path := range goFiles(t, root) {
58 if strings.HasSuffix(path, "_test.go") {
59 continue
60 }
61 rel := relPath(t, root, path)
62
63 for _, imp := range fileImports(t, path) {
64 if imp == chiMiddleware {
65 t.Errorf("%s: imports %q, whose Logger writes to os.Stdout and Recoverer to os.Stderr — neither is visible to gate 9", rel, chiMiddleware)
66 }
67 }
68
69 fset := token.NewFileSet()
70 f, err := parser.ParseFile(fset, path, nil, 0)
71 if err != nil {
72 t.Fatalf("guards: parse %s: %v", rel, err)
73 }
74
75 // Local identifier -> import path, so an alias resolves to the
76 // package it actually names.
77 local := map[string]string{}
78 for _, imp := range f.Imports {
79 ipath, err := strconv.Unquote(imp.Path.Value)
80 if err != nil {
81 t.Fatalf("guards: unquote import in %s: %v", rel, err)
82 }
83 name := ipath[strings.LastIndex(ipath, "/")+1:]
84 if imp.Name != nil {
85 name = imp.Name.Name
86 }
87 // A dot-import puts the package's names in file scope
88 // unqualified, so os.Stderr becomes a bare Stderr with no
89 // selector to match. Refuse the import rather than try to
90 // resolve it.
91 if name == "." {
92 if _, forbidden := forbiddenSelectors[ipath]; forbidden {
93 t.Errorf("%s:%d: dot-imports %q, which hides its sinks from this guard",
94 rel, fset.Position(imp.Pos()).Line, ipath)
95 }
96 continue
97 }
98 local[name] = ipath
99 }
100
101 // The separate native CLI returns the approved structured public result.
102 // Permit only that argument, not other raw writes in the same file.
103 structuredResult := map[*ast.SelectorExpr]bool{}
104 if rel == "cmd/oficina/main_linux.go" {
105 ast.Inspect(f, func(n ast.Node) bool {
106 call, ok := n.(*ast.CallExpr)
107 if !ok || len(call.Args) != 2 {
108 return true
109 }
110 fun, ok := call.Fun.(*ast.SelectorExpr)
111 if !ok || fun.Sel.Name != "Run" {
112 return true
113 }
114 pkg, ok := fun.X.(*ast.Ident)
115 if !ok || local[pkg.Name] != "amadan.net/oficina/home/internal/provisioner" {
116 return true
117 }
118 sink, ok := call.Args[1].(*ast.SelectorExpr)
119 if !ok || sink.Sel.Name != "Stdout" {
120 return true
121 }
122 owner, ok := sink.X.(*ast.Ident)
123 if ok && local[owner.Name] == "os" {
124 structuredResult[sink] = true
125 }
126 return true
127 })
128 }
129 ast.Inspect(f, func(n ast.Node) bool {
130 sel, ok := n.(*ast.SelectorExpr)
131 if !ok {
132 return true
133 }
134 ident, ok := sel.X.(*ast.Ident)
135 if !ok {
136 return true
137 }
138 ipath, isPackage := local[ident.Name]
139 if !isPackage {
140 return true
141 }
142 if why, bad := forbiddenSelectors[ipath][sel.Sel.Name]; bad && !structuredResult[sel] {
143 t.Errorf("%s:%d: %s.%s (%s) — %s",
144 rel, fset.Position(sel.Pos()).Line, ident.Name, sel.Sel.Name, ipath, why)
145 }
146 return true
147 })
148 }
149}
150