guildhouse-spire-plugins/cmd/gsap-attestor/attestor.go
Tyler J King 490c813586 fix(gsap-attestor): use spire-plugin-sdk for SPIRE compatibility
The original implementation used hashicorp/go-plugin directly with a
custom handshake, which SPIRE rejected. Switch to spire-plugin-sdk's
pluginmain.Serve() for correct WorkloadAttestor protocol negotiation,
implement ConfigServer for plugin_data parsing, and return selector
values in key:value format (SPIRE infers the type prefix from the
plugin name). Config decoding tries JSON first (chart renders YAML
as JSON) then falls back to HCL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-13 06:37:37 -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 `json:"proc_root" hcl:"proc_root"`
MaxDepth int `json:"max_depth" 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, 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
}