guildhouse-spire-plugins/cmd/gsap-attestor/attestor.go
Tyler J King fe5e2cf3c6 feat(spire): gsap-attestor WorkloadAttestor plugin
SPIRE WorkloadAttestor that reads governance env vars from /proc/{pid}/environ
(walking up the process tree to find gsh) and emits gsap: selectors on workload
SVIDs. Maps BASCULE_* vars set by bascule-shell and future GSH_* vars to the
11-selector vocabulary defined in gsap-types/src/selectors.rs.

- pkg/gsap/selectors.go: shared Go constants mirroring Rust vocabulary
- cmd/gsap-attestor/: plugin implementation with /proc reading, process tree
  walking, capability ceiling translation, and fail-open for non-governed processes
- 28 tests covering selector extraction, proc parsing, tree walking, and depth limits
- Makefile, Dockerfile, deploy config updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-13 03:59:08 -04:00

136 lines
3.3 KiB
Go

package main
import (
"context"
"github.com/guildhouse-cooperative/guildhouse-spire-plugins/pkg/gsap"
)
type GsapAttestorConfig struct {
ProcRoot string `hcl:"proc_root"`
MaxDepth int `hcl:"max_depth"`
}
type GsapAttestor struct {
procRoot string
maxDepth int
}
func (a *GsapAttestor) Configure(cfg GsapAttestorConfig) error {
a.procRoot = cfg.ProcRoot
if a.procRoot == "" {
a.procRoot = "/proc"
}
a.maxDepth = cfg.MaxDepth
if a.maxDepth <= 0 {
a.maxDepth = 10
}
return nil
}
func (a *GsapAttestor) Attest(_ context.Context, pid int32) ([]string, error) {
currentPid := pid
for depth := 0; depth < a.maxDepth; depth++ {
env, err := readProcEnviron(a.procRoot, currentPid)
if err != nil {
return nil, nil
}
if isGoverned(env) {
return extractSelectors(env), nil
}
parentPid, err := getParentPid(a.procRoot, currentPid)
if err != nil || parentPid <= 1 || parentPid == currentPid {
break
}
currentPid = parentPid
}
return nil, nil
}
// governanceMarkers are env vars whose presence indicates a governed shell.
var governanceMarkers = []string{
"BASCULE_PRINCIPAL",
"GSH_DID",
"BASCULE_SESSION_ID",
"BASCULE_AUTH_METHOD",
}
func isGoverned(env map[string]string) bool {
for _, key := range governanceMarkers {
if v, ok := env[key]; ok && v != "" {
return true
}
}
return false
}
type envMapping struct {
envVar string
selectorKey string
transform func(string) string
}
// envMappings defines the env var → selector mapping with priority ordering.
// For selectors with fallbacks (e.g. principal_did), the primary source
// is listed first and checked in extractSelectors before the fallback.
var envMappings = []envMapping{
{"BASCULE_AUTH_METHOD", gsap.SelectorDriverID, nil},
{"BASCULE_SESSION_ID", gsap.SelectorContextID, nil},
{"BASCULE_CORPUS_CID", gsap.SelectorCorpusCID, nil},
{"GSH_ACCORD_TEMPLATE", gsap.SelectorAccordTemplate, nil},
{"GSH_PLAYBOOK", gsap.SelectorPlaybook, nil},
{"GSH_PARAMETERS_CID", gsap.SelectorParametersCID, nil},
{"GSH_SESSION_MODE", gsap.SelectorSessionMode, nil},
{"GSH_SHELL_CLASS", gsap.SelectorShellClass, nil},
}
func extractSelectors(env map[string]string) []string {
var selectors []string
emit := func(key, value string) {
if value != "" {
selectors = append(selectors, gsap.FormatSelector(key, value))
}
}
// principal_did: GSH_DID takes priority over BASCULE_PRINCIPAL
if v := env["GSH_DID"]; v != "" {
emit(gsap.SelectorPrincipalDID, v)
} else {
emit(gsap.SelectorPrincipalDID, env["BASCULE_PRINCIPAL"])
}
// posture_level: BASCULE_POSTURE_LEVEL takes priority over BASCULE_DEFCON_LEVEL
if v := env["BASCULE_POSTURE_LEVEL"]; v != "" {
emit(gsap.SelectorPostureLevel, v)
} else {
emit(gsap.SelectorPostureLevel, env["BASCULE_DEFCON_LEVEL"])
}
// capability_mask: GSH_CAPABILITY_MASK (hex) takes priority,
// then BASCULE_CAPABILITY_CEILING (name → hex translation)
if v := env["GSH_CAPABILITY_MASK"]; v != "" {
emit(gsap.SelectorCapabilityMask, v)
} else if v := env["BASCULE_CAPABILITY_CEILING"]; v != "" {
if hex, ok := gsap.CapabilityCeilingToHex[v]; ok {
emit(gsap.SelectorCapabilityMask, hex)
}
}
// Simple 1:1 mappings
for _, m := range envMappings {
v := env[m.envVar]
if v == "" {
continue
}
if m.transform != nil {
v = m.transform(v)
}
emit(m.selectorKey, v)
}
return selectors
}