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>
136 lines
3.3 KiB
Go
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
|
|
}
|