// 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: // // - .path>: PathChanged=, // RateLimitIntervalSec=1s, RateLimitBurst=5 — systemd watches the // path for changes and triggers the matching .service. // - .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() }