mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-09-04 13:36:35 +02:00
210 lines
8.0 KiB
Go
210 lines
8.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
)
|
|
|
|
// packageSourcePath resolves a file in this package's source directory, or
|
|
// skips the test when the source is not available. The Windows CI job runs
|
|
// pre-built test binaries with no checkout, so neither the working directory
|
|
// nor the build-time path recorded by runtime.Caller reaches the source
|
|
// there. Skipping is safe for these static checks: they analyze the same
|
|
// source on every platform, and the jobs with a checkout still enforce them.
|
|
func packageSourcePath(t *testing.T, name string) string {
|
|
t.Helper()
|
|
if _, err := os.Stat(name); err == nil {
|
|
return name
|
|
}
|
|
if _, thisFile, _, ok := runtime.Caller(0); ok {
|
|
p := filepath.Join(filepath.Dir(thisFile), name)
|
|
if _, err := os.Stat(p); err == nil {
|
|
return p
|
|
}
|
|
}
|
|
t.Skipf("package source for %s not available (pre-built test binary without a checkout); this static check runs where source is present", name)
|
|
return ""
|
|
}
|
|
|
|
// provisionBoundaryFiles are the files this lane's provisioning-hardening
|
|
// work audited: every terminal path they can reach must report a classified
|
|
// failure (a result file, an output line, a stage-scoped exit code), not a
|
|
// bare crash installer tooling cannot read. TestProvisioningTerminalPathAudit
|
|
// documents the audit by scenario; this test enforces it structurally so a
|
|
// later change cannot reintroduce a bare fatal without either classifying it
|
|
// or adding it here with a reason.
|
|
//
|
|
// Scope: the check sees direct calls (".Fatal()", "os.Exit", "panic") in the
|
|
// files listed here. A helper defined in one of these files is caught once,
|
|
// at the body that holds the call, so an indirect exit through it needs no
|
|
// second rule. A helper defined outside the list, such as
|
|
// checkHasElevatedPrivilege in service.go wired as the "start" PreRun, is
|
|
// outside the reach of this check. TestProvisioningTerminalPathAudit lists
|
|
// those by hand in its exclusion comment.
|
|
var provisionBoundaryFiles = []string{
|
|
"cli.go",
|
|
"commands_service_start.go",
|
|
"commands_run.go",
|
|
"prog.go",
|
|
"provision_result.go",
|
|
}
|
|
|
|
// provisionBoundaryFatalAllowlist is every call in the files above that
|
|
// still reaches a bare ".Fatal()" (on any logger chain), "os.Exit", or
|
|
// "panic", keyed
|
|
// by "file.go:FuncName" (FuncName includes the receiver type for methods,
|
|
// e.g. "prog.setDNS"). Each entry names why it is not a provisioning
|
|
// failure, or why it predates and sits outside the --cd-org boundary this
|
|
// lane hardened.
|
|
var provisionBoundaryFatalAllowlist = map[string]string{
|
|
"cli.go:RunMobile": `panic on a nil AppConfig is a programming error in the mobile host ` +
|
|
`app, at an entry point the CLI command tree never calls. Not reachable from ` +
|
|
`"ctrld start --cd-org".`,
|
|
|
|
"cli.go:run": `os.Exit(0) is the successful exit of the daemon-respawn launcher, right ` +
|
|
`after it starts the real background process. Not a failure.`,
|
|
|
|
"cli.go:readConfigFile": `shared CLI config-parsing helper used the same way by every ` +
|
|
`invocation mode (config-file, no-config, nextdns, --cd, --cd-org). Predates and is ` +
|
|
`orthogonal to the --cd-org provisioning boundary.`,
|
|
|
|
"cli.go:processNoConfigFlags": `shared CLI helper enforcing --listen/--primary_upstream in ` +
|
|
`no-config mode. Applies uniformly across every invocation mode, not specifically to ` +
|
|
`provisioning.`,
|
|
|
|
"cli.go:processListenFlag": `shared CLI helper parsing --listen. Applies uniformly across ` +
|
|
`every invocation mode, not specifically to provisioning.`,
|
|
|
|
"cli.go:readConfigWithNotice": `shared CLI helper (userHomeDir failure while locating the ` +
|
|
`default config file). Applies uniformly across every invocation mode, not specifically ` +
|
|
`to provisioning.`,
|
|
|
|
"cli.go:checkStrFlagEmpty": `shared flag-emptiness helper. Only an explicit empty --cd-org ` +
|
|
`is a provisioning-token value, classified separately inside this function; every other ` +
|
|
`flag (currently --cd) keeps its bare fatal.`,
|
|
|
|
"commands_service_start.go:ServiceCommand.Start": `the deactivation pin check's ` +
|
|
`os.Exit(126). Out of scope for this lane by contract ("pin-check 126 untouched").`,
|
|
|
|
"prog.go:prog.Stop": `the deactivation pin check's os.Exit(126). Out of scope for this ` +
|
|
`lane by contract ("pin-check 126 untouched").`,
|
|
|
|
"prog.go:prog.setDNS": `the runtime --intercept-mode validation (validInterceptMode), ` +
|
|
`distinct from the early "ctrld start" check that already reports ` +
|
|
`INTERCEPT_MODE_INVALID. Left alone per T6's own doc comment.`,
|
|
}
|
|
|
|
// funcDeclKey names a function declaration the way
|
|
// provisionBoundaryFatalAllowlist keys it: "FuncName" for a plain function,
|
|
// "ReceiverType.FuncName" for a method.
|
|
func funcDeclKey(fn *ast.FuncDecl) string {
|
|
if fn.Recv == nil || len(fn.Recv.List) == 0 {
|
|
return fn.Name.Name
|
|
}
|
|
recvType := fn.Recv.List[0].Type
|
|
if star, ok := recvType.(*ast.StarExpr); ok {
|
|
recvType = star.X
|
|
}
|
|
if ident, ok := recvType.(*ast.Ident); ok {
|
|
return ident.Name + "." + fn.Name.Name
|
|
}
|
|
return fn.Name.Name
|
|
}
|
|
|
|
// isBareFatalOrExitCall reports whether call is "<expr>.Fatal(...)" on any
|
|
// logger chain, "os.Exit(...)", or a bare "panic(...)".
|
|
func isBareFatalOrExitCall(call *ast.CallExpr) bool {
|
|
if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "panic" {
|
|
return true
|
|
}
|
|
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if sel.Sel.Name == "Fatal" {
|
|
return true
|
|
}
|
|
if sel.Sel.Name == "Exit" {
|
|
if ident, ok := sel.X.(*ast.Ident); ok && ident.Name == "os" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TestProvisionBoundaryHasNoUnlistedBareFatal parses every provisioning
|
|
// boundary file and flags a bare Fatal/os.Exit that is not in the allowlist
|
|
// above. A new one appearing here means a change added a crash the
|
|
// provisioning boundary's contract does not allow: classify it through
|
|
// failProvision/failProvisionUnclassified, or add it to the allowlist with a
|
|
// reason if it genuinely sits outside the boundary.
|
|
func TestProvisionBoundaryHasNoUnlistedBareFatal(t *testing.T) {
|
|
fset := token.NewFileSet()
|
|
for _, file := range provisionBoundaryFiles {
|
|
node, err := parser.ParseFile(fset, packageSourcePath(t, file), nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("could not parse %s: %v", file, err)
|
|
}
|
|
for _, decl := range node.Decls {
|
|
fn, ok := decl.(*ast.FuncDecl)
|
|
if !ok || fn.Body == nil {
|
|
continue
|
|
}
|
|
key := file + ":" + funcDeclKey(fn)
|
|
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok || !isBareFatalOrExitCall(call) {
|
|
return true
|
|
}
|
|
if _, allowed := provisionBoundaryFatalAllowlist[key]; !allowed {
|
|
pos := fset.Position(call.Pos())
|
|
t.Errorf("%s:%d: unlisted bare fatal/exit in %s - classify it through "+
|
|
"failProvision/failProvisionUnclassified, or add it to "+
|
|
"provisionBoundaryFatalAllowlist with a reason", file, pos.Line, key)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProvisionBoundaryAllowlistHasNoStaleEntries is the converse check: every
|
|
// allowlisted key must still name a real bare Fatal/os.Exit. A stale entry
|
|
// (the call was removed or reclassified) would silently widen the allowlist
|
|
// and hide a real regression the next time this test runs.
|
|
func TestProvisionBoundaryAllowlistHasNoStaleEntries(t *testing.T) {
|
|
fset := token.NewFileSet()
|
|
found := make(map[string]bool, len(provisionBoundaryFatalAllowlist))
|
|
for _, file := range provisionBoundaryFiles {
|
|
node, err := parser.ParseFile(fset, packageSourcePath(t, file), nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("could not parse %s: %v", file, err)
|
|
}
|
|
for _, decl := range node.Decls {
|
|
fn, ok := decl.(*ast.FuncDecl)
|
|
if !ok || fn.Body == nil {
|
|
continue
|
|
}
|
|
key := file + ":" + funcDeclKey(fn)
|
|
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok || !isBareFatalOrExitCall(call) {
|
|
return true
|
|
}
|
|
found[key] = true
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
for key := range provisionBoundaryFatalAllowlist {
|
|
if !found[key] {
|
|
t.Errorf("provisionBoundaryFatalAllowlist[%q] no longer matches any bare fatal/exit; remove the stale entry", key)
|
|
}
|
|
}
|
|
}
|