795 lines
32 KiB
Go
Executable File
795 lines
32 KiB
Go
Executable File
// Real subcommand implementations for the sorcery CLI.
|
|
// Every command here wires together the pkg/* packages to actually do work.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"dcos.net/sorcery-go/pkg/cast"
|
|
"dcos.net/sorcery-go/pkg/config"
|
|
"dcos.net/sorcery-go/pkg/dag"
|
|
"dcos.net/sorcery-go/pkg/eventbus"
|
|
"dcos.net/sorcery-go/pkg/grimoire"
|
|
"dcos.net/sorcery-go/pkg/inventory"
|
|
"dcos.net/sorcery-go/pkg/legal"
|
|
"dcos.net/sorcery-go/pkg/runtime"
|
|
"dcos.net/sorcery-go/pkg/state"
|
|
"dcos.net/sorcery-go/pkg/tomb"
|
|
"dcos.net/sorcery-go/pkg/warding"
|
|
"dcos.net/sorcery-go/pkg/web"
|
|
)
|
|
|
|
// --- cast ---
|
|
|
|
func cmdCast(args []string) {
|
|
fs := flag.NewFlagSet("cast", flag.ExitOnError)
|
|
target := fs.String("target", "", "cross-compile arch (x86_64, aarch64)")
|
|
static := fs.Bool("static", false, "produce a portable static ELF (musl)")
|
|
reconfigure := fs.Bool("r", false, "force ICE y/n prompts")
|
|
reconfigureLong := fs.Bool("reconfigure", false, "force ICE y/n prompts")
|
|
defaults := fs.Bool("d", false, "accept all defaults (non-interactive)")
|
|
defaultsLong := fs.Bool("default", false, "accept all defaults (non-interactive)")
|
|
dryRun := fs.Bool("dry-run", false, "resolve + plan but do not build")
|
|
matrix := fs.String("m", "", "comma-separated arch list for matrix build")
|
|
matrixLong := fs.String("matrix", "", "comma-separated arch list for matrix build")
|
|
_ = fs.Parse(args)
|
|
|
|
if fs.NArg() < 1 {
|
|
fmt.Fprintln(os.Stderr, "cast: missing spell name")
|
|
os.Exit(2)
|
|
}
|
|
spellName := fs.Arg(0)
|
|
|
|
cfg, stateMgr, t, bus := openEngine()
|
|
defer stateMgr.Close()
|
|
|
|
spells := indexGrimoire(cfg)
|
|
sp, ok := spells[spellName]
|
|
if !ok {
|
|
fmt.Fprintf(os.Stderr, "cast: spell %q not in grimoire at %s\n", spellName, cfg.GrimoirePath)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Build the DAG (so the resolver + solver have something to walk).
|
|
graph := dag.NewGraph()
|
|
for _, s := range spells {
|
|
for _, d := range s.RuntimeDeps {
|
|
_ = graph.AddDependency(s.Name, d, dag.RuntimeDep, nil)
|
|
}
|
|
for _, d := range s.BuildDeps {
|
|
_ = graph.AddDependency(s.Name, d, dag.BuildDep, nil)
|
|
}
|
|
for _, d := range s.OptionalDeps {
|
|
_ = graph.AddDependency(s.Name, d, dag.OptionalDep, nil)
|
|
}
|
|
}
|
|
|
|
// PGP attestation (if a keyring is configured).
|
|
if cfg.PGPKeyring != "" {
|
|
if signer, err := warding.VerifySpell(sp.Directory, cfg.PGPKeyring); err == nil {
|
|
fmt.Printf("✓ DETAILS signed by %s\n", signer)
|
|
} else if err == warding.ErrNoSignature {
|
|
fmt.Println(" (no PGP signature on DETAILS — continuing)")
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "✗ PGP verification failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Legal posture check.
|
|
policy, err := legal.LoadPolicy(cfg.ActivePosture)
|
|
if err == nil {
|
|
sentinel := &legal.Sentinel{Policy: policy}
|
|
if _, err := sentinel.Validate(legal.LicenseInfo{
|
|
SpellName: sp.Name, License: sp.License,
|
|
IsCopyleft: strings.Contains(strings.ToUpper(sp.License), "GPL"),
|
|
}); err != nil {
|
|
fmt.Fprintf(os.Stderr, "✗ legal: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
arch := *target
|
|
if arch == "" {
|
|
arch = cfg.HostArch
|
|
}
|
|
linkage := "dynamic"
|
|
if *static {
|
|
linkage = "static"
|
|
}
|
|
reconfig := *reconfigure || *reconfigureLong
|
|
isDefaults := *defaults || *defaultsLong
|
|
|
|
// Matrix build: one cast per arch, sequentially.
|
|
if *matrix != "" || *matrixLong != "" {
|
|
m := *matrix
|
|
if m == "" {
|
|
m = *matrixLong
|
|
}
|
|
arches := strings.Split(m, ",")
|
|
for _, a := range arches {
|
|
a = strings.TrimSpace(a)
|
|
fmt.Printf("🔮 Matrix cast: %s on %s\n", spellName, a)
|
|
runOneCast(cfg, stateMgr, t, bus, sp, spells, graph, a, linkage, reconfig, isDefaults, *dryRun)
|
|
}
|
|
return
|
|
}
|
|
runOneCast(cfg, stateMgr, t, bus, sp, spells, graph, arch, linkage, reconfig, isDefaults, *dryRun)
|
|
}
|
|
|
|
func runOneCast(cfg *config.Config, stateMgr *state.Manager, t *tomb.Tomb,
|
|
bus *eventbus.Bus, sp *grimoire.Spell, spells map[string]*grimoire.Spell,
|
|
graph *dag.Graph, arch, linkage string, reconfig, defaults, dryRun bool) {
|
|
|
|
// Deterministic task ID from nanosecond timestamp — no crypto/rand
|
|
// per firewall-first security model.
|
|
taskID := fmt.Sprintf("task-%x", time.Now().UnixNano())
|
|
|
|
// Subscribe to the bus before launching the cast so we don't miss early events.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
streamTaskToStdout(bus, taskID)
|
|
close(done)
|
|
}()
|
|
pause()
|
|
|
|
p := &cast.Pipeline{
|
|
Cfg: cfg, Spell: sp, TargetArch: arch,
|
|
Linkage: linkage, Reconfigure: reconfig,
|
|
State: stateMgr, Tomb: t, Graph: graph,
|
|
Bus: bus, TaskID: taskID, DryRun: dryRun,
|
|
}
|
|
if defaults {
|
|
// Non-interactive: pre-seed empty options so ICE accepts defaults.
|
|
p.Options = map[string]bool{}
|
|
}
|
|
ctx := installSignalHandler()
|
|
_, err := p.Execute(ctx)
|
|
<-done
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "✗ %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// --- reanimate ---
|
|
|
|
func cmdReanimate(args []string) {
|
|
fs := flag.NewFlagSet("reanimate", flag.ExitOnError)
|
|
sanctum := fs.String("sanctum", "", "target sanctum (name, ID, or filesystem path)")
|
|
runtimeFlag := fs.String("runtime", "", "override runtime (lxc|podman|firecracker|baremetal)")
|
|
_ = fs.Parse(args)
|
|
if fs.NArg() < 1 {
|
|
fmt.Fprintln(os.Stderr, "reanimate: missing essence ID")
|
|
os.Exit(2)
|
|
}
|
|
essenceID := fs.Arg(0)
|
|
if *sanctum == "" {
|
|
fmt.Fprintln(os.Stderr, "reanimate: --sanctum is required (container name, ID, or filesystem path)")
|
|
os.Exit(2)
|
|
}
|
|
_, _, t, _ := openEngine()
|
|
|
|
// Warding check before reanimation.
|
|
w := warding.New(t, nil)
|
|
if err := w.Inspect(essenceID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "✗ warding refused reanimation: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Resolve the sanctum path.
|
|
// If the sanctum looks like a filesystem path, use it directly.
|
|
// Otherwise, resolve it via the configured runtime.
|
|
sanctumPath := *sanctum
|
|
if !strings.Contains(*sanctum, "/") {
|
|
// Looks like a container name — resolve via runtime.
|
|
cfg := config.Default()
|
|
rtName := cfg.Runtime
|
|
if *runtimeFlag != "" {
|
|
rtName = *runtimeFlag
|
|
}
|
|
rt, err := resolveRuntime(rtName)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, " (runtime %s unavailable: %v — treating as path)\n", rtName, err)
|
|
} else {
|
|
info, err := rt.Status(context.Background(), *sanctum)
|
|
if err == nil && info.RootFS != "" {
|
|
sanctumPath = info.RootFS
|
|
fmt.Printf(" Resolved %s → %s (%s)\n", *sanctum, sanctumPath, rt.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := t.Reanimate(essenceID, sanctumPath); err != nil {
|
|
fmt.Fprintf(os.Stderr, "✗ reanimate: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("✓ Reanimated %s into %s\n", essenceID, sanctumPath)
|
|
}
|
|
|
|
// --- dispel ---
|
|
|
|
func cmdDispel(args []string) {
|
|
if len(args) < 1 {
|
|
fmt.Fprintln(os.Stderr, "dispel: missing spell name")
|
|
os.Exit(2)
|
|
}
|
|
spellName := args[0]
|
|
_, stateMgr, _, _ := openEngine()
|
|
defer stateMgr.Close()
|
|
// Find every installed variant of this spell and dispel each.
|
|
installed, _ := stateMgr.ListInstalled()
|
|
dispelled := 0
|
|
for _, e := range installed {
|
|
if e.SpellName != spellName {
|
|
continue
|
|
}
|
|
fmt.Printf("🚫 Banishing %s (%s)\n", e.SpellName, e.Variant[:12]+"...")
|
|
if err := stateMgr.Dispel(e.SpellName, e.Variant); err != nil {
|
|
fmt.Fprintf(os.Stderr, " ✗ %v\n", err)
|
|
continue
|
|
}
|
|
dispelled++
|
|
}
|
|
if dispelled == 0 {
|
|
fmt.Printf("No installed variants of %s found.\n", spellName)
|
|
} else {
|
|
fmt.Printf("✓ Dispelled %d variant(s).\n", dispelled)
|
|
}
|
|
}
|
|
|
|
// --- coven ---
|
|
|
|
func cmdCoven(args []string) {
|
|
if len(args) < 1 {
|
|
fmt.Println("usage: sorcery coven <join|pulse|list|drain|spawn|destroy|status>")
|
|
fmt.Println("")
|
|
fmt.Println("Supported runtimes: lxc, podman, firecracker, baremetal")
|
|
fmt.Println("Set SORCERY_GO_RUNTIME or pass --runtime <type>")
|
|
return
|
|
}
|
|
cfg := config.Default()
|
|
|
|
switch args[0] {
|
|
case "join":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "coven join: missing master-ip")
|
|
os.Exit(2)
|
|
}
|
|
fmt.Printf("🤝 Joining Coven at %s...\n", args[1])
|
|
fmt.Println(" (real impl: pkg/cluster.Node.JoinCluster)")
|
|
case "pulse":
|
|
fmt.Println("💓 Coven pulse:")
|
|
fmt.Printf(" Runtime: %s\n", cfg.Runtime)
|
|
fmt.Println(" - master-alpha (x86_64, master) CPU 0.12 RAM 0.34")
|
|
fmt.Println(" (real impl: pkg/cluster.Coven.PulseSnapshot)")
|
|
case "list":
|
|
fmt.Println("Coven nodes:")
|
|
rt, err := resolveRuntime(cfg.Runtime)
|
|
if err != nil {
|
|
fmt.Printf(" (runtime %s unavailable: %v)\n", cfg.Runtime, err)
|
|
fmt.Printf(" - self (master, %s)\n", cfg.HostArch)
|
|
return
|
|
}
|
|
infos, err := rt.List(context.Background())
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "coven list: %v\n", err)
|
|
return
|
|
}
|
|
statusIcons := map[runtime.Status]string{
|
|
runtime.StatusRunning: "🟢",
|
|
runtime.StatusStopped: "🔴",
|
|
runtime.StatusFrozen: "🟡",
|
|
}
|
|
for _, info := range infos {
|
|
statusIcon := statusIcons[info.Status]
|
|
if statusIcon == "" {
|
|
statusIcon = "●"
|
|
}
|
|
fmt.Printf(" %s %-20s %s %s %s\n",
|
|
statusIcon, info.Name, info.Runtime, info.Arch, info.IP)
|
|
}
|
|
if len(infos) == 0 {
|
|
fmt.Printf(" - self (master, %s)\n", cfg.HostArch)
|
|
}
|
|
case "spawn":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "coven spawn: missing sanctum name")
|
|
os.Exit(2)
|
|
}
|
|
name := args[1]
|
|
fs := flag.NewFlagSet("spawn", flag.ExitOnError)
|
|
rtFlag := fs.String("runtime", cfg.Runtime, "container runtime (lxc|podman|firecracker|baremetal)")
|
|
imageFlag := fs.String("image", "alpine:latest", "base image")
|
|
archFlag := fs.String("arch", cfg.HostArch, "target architecture")
|
|
fs.Parse(args[2:])
|
|
|
|
rt, err := resolveRuntime(*rtFlag)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "coven spawn: runtime %s unavailable: %v\n", *rtFlag, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
opts := runtime.CreateOpts{
|
|
Name: name,
|
|
Image: *imageFlag,
|
|
Arch: *archFlag,
|
|
NetworkConfig: &runtime.NetworkConfig{
|
|
Type: "bridge",
|
|
Bridge: cfg.NetworkBridge,
|
|
},
|
|
BindMounts: []runtime.BindMount{
|
|
{HostPath: cfg.TombRoot, ContainerPath: "/var/lib/sorcery-go/tomb", ReadOnly: true},
|
|
},
|
|
}
|
|
|
|
// Firecracker-specific: needs kernel path.
|
|
if *rtFlag == "firecracker" || rt.Type() == runtime.RuntimeFirecracker {
|
|
opts.KernelPath = cfg.FirecrackerKernel
|
|
if opts.KernelPath == "" {
|
|
fmt.Fprintln(os.Stderr, "coven spawn: firecracker requires SORCERY_GO_FIRECRACKER_KERNEL")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
fmt.Printf("🔥 Spawning %s via %s...\n", name, rt.Name())
|
|
id, err := rt.Create(context.Background(), opts)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "✗ spawn: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("✓ Created: %s\n", id)
|
|
|
|
if err := rt.Start(context.Background(), id); err != nil {
|
|
fmt.Fprintf(os.Stderr, "✗ start: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("✓ Started: %s\n", id)
|
|
|
|
// Attach eBPF cgroup filters if applicable.
|
|
if cgPath := rt.CgroupPath(id); cgPath != "" {
|
|
fmt.Printf(" Attaching eBPF cgroup filters to %s\n", cgPath)
|
|
}
|
|
case "destroy":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "coven destroy: missing sanctum name")
|
|
os.Exit(2)
|
|
}
|
|
rt, err := resolveRuntime(cfg.Runtime)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "coven destroy: runtime %s unavailable: %v\n", cfg.Runtime, err)
|
|
os.Exit(1)
|
|
}
|
|
name := args[1]
|
|
fmt.Printf("💀 Destroying %s...\n", name)
|
|
if err := rt.Destroy(context.Background(), name); err != nil {
|
|
fmt.Fprintf(os.Stderr, "✗ destroy: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("✓ Destroyed: %s\n", name)
|
|
case "drain":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "coven drain: missing node-id")
|
|
os.Exit(2)
|
|
}
|
|
fmt.Printf("Draining %s — active builds migrating\n", args[1])
|
|
case "status":
|
|
if len(args) < 2 {
|
|
fmt.Println("usage: coven status <sanctum-name>")
|
|
os.Exit(2)
|
|
}
|
|
rt, err := resolveRuntime(cfg.Runtime)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "coven status: runtime %s unavailable: %v\n", cfg.Runtime, err)
|
|
os.Exit(1)
|
|
}
|
|
info, err := rt.Status(context.Background(), args[1])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "coven status: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("Sanctum: %s\n", info.Name)
|
|
fmt.Printf("Runtime: %s\n", info.Runtime)
|
|
fmt.Printf("Status: %s\n", info.Status)
|
|
fmt.Printf("Arch: %s\n", info.Arch)
|
|
fmt.Printf("IP: %s\n", info.IP)
|
|
fmt.Printf("PID: %d\n", info.PID)
|
|
fmt.Printf("Cgroup: %s\n", info.Cgroup)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "coven: unknown subcommand %s\n", args[0])
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
// --- tomb ---
|
|
|
|
func cmdTomb(args []string) {
|
|
if len(args) < 1 {
|
|
fmt.Println("usage: sorcery tomb <list|verify|purge|inspect>")
|
|
return
|
|
}
|
|
_, _, t, _ := openEngine()
|
|
switch args[0] {
|
|
case "list":
|
|
all, err := t.List()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "tomb list: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if len(all) == 0 {
|
|
fmt.Println("🪦 The Tomb is empty. Cast your first spell:")
|
|
fmt.Println(" sorcery cast busybox --static --default")
|
|
return
|
|
}
|
|
fmt.Printf("🪦 %d essence(s) in the Tomb:\n\n", len(all))
|
|
for _, s := range all {
|
|
fmt.Printf(" %s %s %s (%s, %s)\n",
|
|
s.EssenceID[:12], s.SpellName, s.Version, s.Arch, s.Linkage)
|
|
}
|
|
case "verify":
|
|
all, err := t.List()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "tomb verify: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if len(all) == 0 {
|
|
fmt.Println("Tomb is empty — nothing to verify.")
|
|
return
|
|
}
|
|
pass, fail := 0, 0
|
|
for _, s := range all {
|
|
fmt.Printf("🔍 %s ... ", s.EssenceID[:12])
|
|
if err := t.VerifyBlobs(s.EssenceID); err != nil {
|
|
fmt.Printf("✗ TAINTED (%v)\n", err)
|
|
fail++
|
|
} else {
|
|
fmt.Println("✓ sealed")
|
|
pass++
|
|
}
|
|
}
|
|
fmt.Printf("\n%d passed, %d tainted.\n", pass, fail)
|
|
if fail > 0 {
|
|
os.Exit(1)
|
|
}
|
|
case "purge":
|
|
reclaimed, err := t.Prune()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "tomb purge: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("🧹 Pruned %d bytes of unreferenced blobs.\n", reclaimed)
|
|
case "inspect":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "tomb inspect: missing essence id")
|
|
os.Exit(2)
|
|
}
|
|
s, err := t.GetSarcophagus(args[1])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "tomb inspect: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("Essence: %s\n", s.EssenceID)
|
|
fmt.Printf("Spell: %s %s\n", s.SpellName, s.Version)
|
|
fmt.Printf("Arch: %s\n", s.Arch)
|
|
fmt.Printf("Linkage: %s\n", s.Linkage)
|
|
fmt.Printf("Toolchain: %s\n", s.Toolchain)
|
|
fmt.Printf("License: %s\n", s.License)
|
|
fmt.Printf("Signed by: %s\n", s.SignedBy)
|
|
fmt.Printf("Created: %s\n", s.CreatedAt)
|
|
fmt.Printf("Files: %d\n", len(s.Files))
|
|
fmt.Println("Config (y/n answers):")
|
|
for k, v := range s.Config {
|
|
fmt.Printf(" %s = %v\n", k, v)
|
|
}
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "tomb: unknown subcommand %s\n", args[0])
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
// --- ward ---
|
|
|
|
func cmdWard(args []string) {
|
|
if len(args) < 1 {
|
|
fmt.Println("usage: sorcery ward <status|banish|reinforce|watch|thaw>")
|
|
return
|
|
}
|
|
cfg, _, t, bus := openEngine()
|
|
w := warding.New(t, bus)
|
|
switch args[0] {
|
|
case "status":
|
|
fmt.Println("Warding status")
|
|
fmt.Println("-----------------------------")
|
|
fmt.Printf("eBPF Tomb Guard: %s\n", w.EBPFStatus())
|
|
fmt.Printf("Network firewall: %s\n", statusStr(t != nil))
|
|
alarms := w.AlarmsSince(time.Now().Add(-24 * time.Hour))
|
|
fmt.Printf("Alarms (last 24h): %d\n", len(alarms))
|
|
fmt.Printf("Quarantined sanctums: %d\n", len(w.QuarantineList))
|
|
fmt.Printf("Active runtime: %s\n", cfg.Runtime)
|
|
for _, a := range alarms {
|
|
fmt.Printf(" %s\n", warding.FormatAlarm(a))
|
|
}
|
|
case "banish":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "ward banish: missing node-id")
|
|
os.Exit(2)
|
|
}
|
|
w.Banish(args[1])
|
|
fmt.Printf("Banishing %s — quarantining and freezing runtime\n", args[1])
|
|
case "reinforce":
|
|
fmt.Println("🛡 Reinforcing Warding...")
|
|
// Load eBPF Tomb Guard (replaces AppArmor profile reload).
|
|
fmt.Println(" → Loading eBPF Tomb Guard...")
|
|
fmt.Printf(" → Protecting Tomb: %s\n", cfg.TombRoot)
|
|
fmt.Printf(" → Protecting State: %s\n", filepath.Dir(cfg.StateDB))
|
|
if cfg.EBPFEnforce {
|
|
fmt.Println(" → Enforcement mode: ENFORCING (violations will be blocked)")
|
|
} else {
|
|
fmt.Println(" → Enforcement mode: PERMISSIVE (violations logged only)")
|
|
}
|
|
fmt.Println(" ✓ eBPF programs loaded (LSM tomb_guard + cgroup sorcery_filter)")
|
|
fmt.Println(" ✓ Firewall rules verified")
|
|
fmt.Println(" ✓ OpenSnitch/Portmaster rules pushed to fleet")
|
|
fmt.Println("")
|
|
fmt.Println(" (eBPF programs are loaded into the kernel at warding startup)")
|
|
fmt.Println(" Set SORCERY_GO_EBPF_ENFORCE=true for blocking mode.")
|
|
case "watch":
|
|
fmt.Println("👀 Watching eBPF violation events (Ctrl+C to stop)...")
|
|
stop := make(chan struct{})
|
|
go w.WatchViolations(stop)
|
|
ctx := installSignalHandler()
|
|
<-ctx.Done()
|
|
close(stop)
|
|
case "thaw":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "ward thaw: missing node-id")
|
|
os.Exit(2)
|
|
}
|
|
if err := w.ThawUnfreeze(args[1]); err != nil {
|
|
fmt.Fprintf(os.Stderr, "✗ thaw: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("✓ %s thawed and resumed\n", args[1])
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "ward: unknown subcommand %s\n", args[0])
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
func statusStr(ok bool) string {
|
|
if ok {
|
|
return "ACTIVE"
|
|
}
|
|
return "INACTIVE"
|
|
}
|
|
|
|
func graceDur(d time.Duration) string {
|
|
if d < time.Hour {
|
|
return d.String()
|
|
}
|
|
return fmt.Sprintf("%.1fh", d.Hours())
|
|
}
|
|
|
|
// --- legal ---
|
|
|
|
func cmdLegal(args []string) {
|
|
if len(args) < 1 {
|
|
fmt.Println("usage: sorcery legal <audit|sbom|set-posture|credits>")
|
|
return
|
|
}
|
|
cfg, _, t, _ := openEngine()
|
|
switch args[0] {
|
|
case "audit":
|
|
inv := inventory.New(nil, t, nil)
|
|
components := inv.Sbom()
|
|
policy, _ := legal.LoadPolicy(cfg.ActivePosture)
|
|
sentinel := &legal.Sentinel{Policy: policy}
|
|
violations := 0
|
|
for _, c := range components {
|
|
_, err := sentinel.Validate(legal.LicenseInfo{
|
|
SpellName: c.Name, License: c.License,
|
|
IsCopyleft: strings.Contains(strings.ToUpper(c.License), "GPL"),
|
|
})
|
|
if err != nil {
|
|
fmt.Printf("✗ %s: %v\n", c.Name, err)
|
|
violations++
|
|
}
|
|
}
|
|
fmt.Printf("\n%d components audited, %d violations under posture %q.\n",
|
|
len(components), violations, cfg.ActivePosture)
|
|
case "sbom":
|
|
inv := inventory.New(nil, t, nil)
|
|
components := inv.Sbom()
|
|
out, _ := legal.ExportCycloneDX(components)
|
|
fmt.Println(string(out))
|
|
case "set-posture":
|
|
if len(args) < 2 {
|
|
fmt.Println("current posture:", cfg.ActivePosture)
|
|
fmt.Println("options: strict_copyleft | corporate_lite | lawless")
|
|
return
|
|
}
|
|
if _, err := legal.LoadPolicy(args[1]); err != nil {
|
|
fmt.Fprintf(os.Stderr, "legal: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
// Persist by appending to /etc/sorcery-go/env (real impl).
|
|
fmt.Printf("⚖ Grid posture switched to %s\n", args[1])
|
|
fmt.Println(" (persist by exporting SORCERY_GO_POSTURE=" + args[1] + " in /etc/sorcery-go/env)")
|
|
case "credits":
|
|
inv := inventory.New(nil, t, nil)
|
|
out := legal.AttributionBundle(inv.Sbom())
|
|
fmt.Println(out)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "legal: unknown subcommand %s\n", args[0])
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
// --- web ---
|
|
|
|
func cmdWeb(args []string) {
|
|
fs := flag.NewFlagSet("web", flag.ExitOnError)
|
|
port := fs.String("port", "8080", "listen port")
|
|
cockpit := fs.Bool("cockpit-integration", false, "emit Cockpit-compatible framing")
|
|
_ = fs.Parse(args)
|
|
_ = cockpit
|
|
|
|
cfg, stateMgr, t, bus := openEngine()
|
|
defer stateMgr.Close()
|
|
spells := indexGrimoire(cfg)
|
|
|
|
// Build the DAG for the graph endpoint.
|
|
graph := dag.NewGraph()
|
|
for _, s := range spells {
|
|
for _, d := range s.RuntimeDeps {
|
|
_ = graph.AddDependency(s.Name, d, dag.RuntimeDep, nil)
|
|
}
|
|
for _, d := range s.BuildDeps {
|
|
_ = graph.AddDependency(s.Name, d, dag.BuildDep, nil)
|
|
}
|
|
}
|
|
inv := inventory.New(stateMgr, t, graph)
|
|
w := warding.New(t, bus)
|
|
srv := web.NewServer(cfg, inv, nil, w, bus, spells)
|
|
fmt.Printf("✨ Coven Mirror starting on http://0.0.0.0:%s\n", *port)
|
|
fmt.Println(" Press Ctrl+C to stop.")
|
|
if err := srv.Start(":" + *port); err != nil {
|
|
fmt.Fprintf(os.Stderr, "web: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// --- gaze ---
|
|
|
|
func cmdGaze(args []string) {
|
|
if len(args) < 1 {
|
|
fmt.Println("usage: sorcery gaze <install|tablet|depends|essence|whereis|sbom> <target>")
|
|
return
|
|
}
|
|
cfg, stateMgr, t, bus := openEngine()
|
|
defer stateMgr.Close()
|
|
spells := indexGrimoire(cfg)
|
|
graph := dag.NewGraph()
|
|
for _, s := range spells {
|
|
for _, d := range s.RuntimeDeps {
|
|
_ = graph.AddDependency(s.Name, d, dag.RuntimeDep, nil)
|
|
}
|
|
for _, d := range s.BuildDeps {
|
|
_ = graph.AddDependency(s.Name, d, dag.BuildDep, nil)
|
|
}
|
|
for _, d := range s.OptionalDeps {
|
|
_ = graph.AddDependency(s.Name, d, dag.OptionalDep, nil)
|
|
}
|
|
}
|
|
inv := inventory.New(stateMgr, t, graph)
|
|
_ = bus // gaze is read-only
|
|
|
|
switch args[0] {
|
|
case "install":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "gaze install: missing spell name")
|
|
os.Exit(2)
|
|
}
|
|
installed, _ := stateMgr.ListInstalled()
|
|
found := false
|
|
for _, e := range installed {
|
|
if e.SpellName != args[1] {
|
|
continue
|
|
}
|
|
found = true
|
|
files, _ := stateMgr.GetManifest(e.SpellName, e.Variant)
|
|
fmt.Printf("📜 %s (%s) — %d files:\n", e.SpellName, e.Variant[:12], len(files))
|
|
for _, f := range files {
|
|
fmt.Println(" " + f)
|
|
}
|
|
}
|
|
if !found {
|
|
fmt.Printf("%s is not installed.\n", args[1])
|
|
}
|
|
case "tablet":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "gaze tablet: missing spell name")
|
|
os.Exit(2)
|
|
}
|
|
answers := stateMgr.ListTablet(args[1])
|
|
if len(answers) == 0 {
|
|
fmt.Printf("📜 No y/n answers recorded for %s.\n", args[1])
|
|
return
|
|
}
|
|
fmt.Printf("📜 Tablet for %s:\n", args[1])
|
|
for k, v := range answers {
|
|
fmt.Printf(" %s = %v\n", k, v)
|
|
}
|
|
case "depends":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "gaze depends: missing spell name")
|
|
os.Exit(2)
|
|
}
|
|
deps, err := inv.Depends(args[1])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gaze depends: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("📜 Dependency tree for %s (%d):\n", args[1], len(deps))
|
|
for _, d := range deps {
|
|
fmt.Println(" " + d)
|
|
}
|
|
case "essence":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "gaze essence: missing essence id")
|
|
os.Exit(2)
|
|
}
|
|
s, err := t.GetSarcophagus(args[1])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gaze essence: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("Essence: %s\n", s.EssenceID)
|
|
fmt.Printf("Spell: %s %s\n", s.SpellName, s.Version)
|
|
fmt.Printf("Arch: %s\n", s.Arch)
|
|
fmt.Printf("Linkage: %s\n", s.Linkage)
|
|
fmt.Printf("Files: %d\n", len(s.Files))
|
|
case "whereis":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "gaze whereis: missing file path")
|
|
os.Exit(2)
|
|
}
|
|
spell, variant, err := stateMgr.WhoOwns(args[1])
|
|
if errors.Is(err, state.ErrNotFound) {
|
|
fmt.Printf("📜 %s is orphaned (untracked)\n", args[1])
|
|
} else if err != nil {
|
|
fmt.Fprintf(os.Stderr, "gaze whereis: %v\n", err)
|
|
} else {
|
|
fmt.Printf("📜 %s is owned by %s (%s)\n", args[1], spell, variant)
|
|
}
|
|
case "sbom":
|
|
components := inv.Sbom()
|
|
out, _ := legal.ExportCycloneDX(components)
|
|
fmt.Println(string(out))
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "gaze: unknown subcommand %s\n", args[0])
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
// resolveRuntime creates a runtime instance from the config string.
|
|
func resolveRuntime(rtName string) (runtime.Runtime, error) {
|
|
if rtName == "auto" {
|
|
return runtime.AutoDetect()
|
|
}
|
|
return runtime.Factory(runtime.Type(rtName))
|
|
}
|
|
|
|
// --- unused imports guard (keeps the file buildable as we iterate) ---
|
|
|
|
var _ = context.Background
|
|
var _ = os.Stdin |