03f3585f16
internal/drift/drift.go: Detector (Watch via iter.Seq2, Aggregate,
Remediate with cooldown-on-success, Acknowledge), Config with tiered
cadence (critical 5s + Path units, standard 30s, default 60s).
internal/cli/drift.go: orca drift {show,watch,acknowledge,remediate,
config}. internal/emitter/drift_path.go: systemd Path+service unit
emitter (User=orca, ProtectSystem=strict). scripts/orca-drift-notify.sh
(sha256 event JSON), orca-remediate.sh (cooldown-on-success, transient
retry). Pre-flight gate (R-020, --force + per-ns scoping). orca
system user (REQ-111), NFS detection (D-233), orca job restart for
EnvironmentFile drift (D-235).
---ci---
project: orca
phase: 10b
milestone: v0.11
status: execute
---/ci---
148 lines
5.1 KiB
Go
148 lines
5.1 KiB
Go
// Package emitter: drift_path.go implements the systemd Path-unit
|
|
// emitter for critical drift paths (P10b, v0.11, REQ-105, R-018).
|
|
//
|
|
// For each critical path two units are emitted:
|
|
//
|
|
// - <orca-drift-<name>.path>: PathChanged=<path>,
|
|
// RateLimitIntervalSec=1s, RateLimitBurst=5 — systemd watches the
|
|
// path for changes and triggers the matching .service.
|
|
// - <orca-drift-<name>.service>: Type=oneshot,
|
|
// ExecStart=/usr/local/bin/orca-drift-notify.sh %f, User=orca,
|
|
// security-hardened (NoNewPrivileges=yes, ProtectSystem=strict,
|
|
// ReadWritePaths=/etc/orca/state/drift-events, ProtectHome=yes).
|
|
//
|
|
// The emitter takes a list of critical PathSpecs (from drift.Config)
|
|
// and produces the unit File artifacts. Unit names are derived from a
|
|
// slug of the path pattern: non-alphanumerics collapse to '-' so the
|
|
// name is systemd-friendly (no slashes, spaces, or brace chars).
|
|
package emitter
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// DriftPathUnit is the systemd Path + service unit emitter for critical
|
|
// drift paths (REQ-105).
|
|
type DriftPathUnit struct{}
|
|
|
|
// driftEventsDir is the directory the oneshot services write to
|
|
// (ReadWritePaths).
|
|
const driftEventsDir = "/etc/orca/state/drift-events"
|
|
|
|
// driftNotifyBin is the oneshot ExecStart binary.
|
|
const driftNotifyBin = "/usr/local/bin/orca-drift-notify.sh"
|
|
|
|
// RenderDriftUnits renders a pair of systemd units (a .path and a
|
|
// .service) for each critical path in specs. Returns the File slice
|
|
// ready for SSH-push to the peer.
|
|
func (DriftPathUnit) RenderDriftUnits(specs []DriftPathSpec) ([]File, error) {
|
|
if len(specs) == 0 {
|
|
return nil, nil
|
|
}
|
|
seen := make(map[string]bool, len(specs))
|
|
var files []File
|
|
for _, s := range specs {
|
|
if s.Pattern == "" {
|
|
return nil, fmt.Errorf("emitter/drift_path: empty pattern")
|
|
}
|
|
name := driftUnitName(s.Pattern)
|
|
if seen[name] {
|
|
continue
|
|
}
|
|
seen[name] = true
|
|
files = append(files, File{
|
|
Path: fmt.Sprintf("/etc/systemd/system/%s.path", name),
|
|
Content: renderDriftPathUnit(name, s.Pattern),
|
|
Mode: "0644",
|
|
})
|
|
files = append(files, File{
|
|
Path: fmt.Sprintf("/etc/systemd/system/%s.service", name),
|
|
Content: renderDriftServiceUnit(name),
|
|
Mode: "0644",
|
|
})
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
// DriftPathSpec is the minimal description the emitter needs: the
|
|
// pattern to watch and whether systemd Path units are requested (when
|
|
// false, no units are emitted — the path falls back to polling).
|
|
type DriftPathSpec struct {
|
|
Pattern string
|
|
SystemdPathUnit bool
|
|
}
|
|
|
|
// driftUnitName derives a systemd-friendly unit-name slug from a path
|
|
// pattern. Non-alphanumeric runes collapse to '-'. The result carries
|
|
// the orca-drift- prefix so the units are identifiable as orca-owned.
|
|
func driftUnitName(pattern string) string {
|
|
s := pattern
|
|
s = strings.TrimPrefix(s, "/")
|
|
var b strings.Builder
|
|
prevDash := false
|
|
for _, r := range s {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
|
b.WriteRune(r)
|
|
prevDash = false
|
|
} else {
|
|
if !prevDash {
|
|
b.WriteByte('-')
|
|
prevDash = true
|
|
}
|
|
}
|
|
}
|
|
name := strings.Trim(b.String(), "-")
|
|
if name == "" {
|
|
name = "root"
|
|
}
|
|
return "orca-drift-" + name
|
|
}
|
|
|
|
// renderDriftPathUnit renders the .path unit. PathChanged re-fires on
|
|
// every modification (inotify IN_MODIFY), RateLimitIntervalSec +
|
|
// RateLimitBurst bound the burst so a thrashing file does not spawn
|
|
// thousands of services (R-018).
|
|
func renderDriftPathUnit(name, path string) string {
|
|
var b strings.Builder
|
|
b.WriteString("[Unit]\n")
|
|
b.WriteString(fmt.Sprintf("Description=orca drift watch for %s\n", path))
|
|
b.WriteString("\n[Path]\n")
|
|
b.WriteString(fmt.Sprintf("PathChanged=%s\n", path))
|
|
b.WriteString("RateLimitIntervalSec=1s\n")
|
|
b.WriteString("RateLimitBurst=5\n")
|
|
b.WriteString("\n[Install]\n")
|
|
b.WriteString("WantedBy=multi-user.target\n")
|
|
return b.String()
|
|
}
|
|
|
|
// renderDriftServiceUnit renders the oneshot .service triggered by
|
|
// the .path unit. ExecStart receives the changed path via %f. Security
|
|
// hardening runs the service as User=orca with NoNewPrivileges=yes,
|
|
// ProtectSystem=strict (read-only /), and ReadWritePaths scoped to the
|
|
// drift-events dir so the script can write its event JSON. ProtectHome
|
|
// hides /root and /home (the orca user has no business there).
|
|
func renderDriftServiceUnit(name string) string {
|
|
var b strings.Builder
|
|
b.WriteString("[Unit]\n")
|
|
b.WriteString(fmt.Sprintf("Description=orca drift notify for %s\n", name))
|
|
b.WriteString(fmt.Sprintf("After=%s.path\n", name))
|
|
b.WriteString("\n[Service]\n")
|
|
b.WriteString("Type=oneshot\n")
|
|
b.WriteString(fmt.Sprintf("ExecStart=%s %%f\n", driftNotifyBin))
|
|
b.WriteString("User=orca\n")
|
|
b.WriteString("Group=orca\n")
|
|
b.WriteString("NoNewPrivileges=yes\n")
|
|
b.WriteString("ProtectSystem=strict\n")
|
|
b.WriteString(fmt.Sprintf("ReadWritePaths=%s\n", driftEventsDir))
|
|
b.WriteString("ProtectHome=yes\n")
|
|
b.WriteString("PrivateTmp=yes\n")
|
|
b.WriteString("ProtectKernelTunables=yes\n")
|
|
b.WriteString("ProtectKernelModules=yes\n")
|
|
b.WriteString("ProtectControlGroups=yes\n")
|
|
b.WriteString("RestrictSUIDSGID=yes\n")
|
|
b.WriteString("\n[Install]\n")
|
|
b.WriteString("WantedBy=multi-user.target\n")
|
|
return b.String()
|
|
}
|