AZ
Back to constellation
SecurityDraft

Sentova

Cross-device active-protection endpoint agent — a Go protection service plus a privileged Windows service that enforces only server-signed, verified directives.

Go 1.26go.work workspaceWailsWindows WFPETWDPAPIEd25519 directivesNamed-pipe IPC

Overview

What it is

Sentova is cross-device active protection: installable desktop (Windows / macOS / Linux) and mobile (Android / iOS / iPadOS) apps backed by a single Go protection service. Five detection modules run per device — network_filter, ioc_scan, posture, behavior_monitor, ransomware_guard — and a server-side engine derives one of three device verdicts. Honesty is a stated product requirement: it does not claim to stop every attack, and a per-cell platform tracker is the single source of truth for what actually works today. In this build (P1), only the Windows desktop column ships real, tested enforcement across all five modules; every other platform cell is a specced code seam or a roadmap item, and the mobile scaffold renders that truthfully in its capability panel.

VerdictMeaning
PROTECTEDNo active indicators; posture within tolerance.
AT_RISKWeakened posture or a provisional/unconfirmed signal.
COMPROMISEDA confirmed indicator or an executed enforcement trip.

One Go workspace, both sides sharing core

The repo is a single go.work workspace pinned to Go 1.26.2 with toolchain go1.26.5 — a stdlib-CVE-patched compiler adopted without raising the language version. The workspace uses five directories that resolve to four logical modules (the fifth, apps/desktop/e2e, is a separate test module):

Module pathDirectoryRole
sentova/serverserverBackend modular monolith (:9700)
sentova/corecoreShared pure logic, imported by both sides
sentova/desktopapps/desktopWails app (unprivileged UI)
sentova/desktop-serviceapps/desktop/servicePrivileged Windows service

The critical decision is core/: dsig (directive signing), feed (Ed25519 signed hash-prefix feed), hashprefix (Tier-1 filter), normalize, vocab, wire. Because the exact same package is compiled into the server that signs directives and the agent that verifies them, the wire / feed / directive formats are byte-for-byte identical and cannot drift between the two sides.

Five modules, six targets — the capability matrix

Each module's decision logic is platform-independent Go; what varies per OS is whether it is wired to a real enforcement mechanism. The tracker states every cell as one of four states, and the design docs (not the tracker) are the source of truth where they disagree.

ModuleWhat it doeswindowsmacoslinuxandroidiosipados
network_filterBlocks blocklisted/phishing domains + infra (WFP on Windows)SHIPPED-P1SPECCED-SEAM (P2)SPECCED-SEAM (P2)SPECCED-SEAM (P2)SPECCED-SEAM (P3)SPECCED-SEAM (P3)
ioc_scanMatches STIX 2.1 indicators against files/processes/domains/hashesSHIPPED-P1SPECCED-SEAM (P2)SPECCED-SEAM (P2)SPECCED-SEAM (P2)N/A-OSN/A-OS
postureScores hardening (patch age, disk encryption, firewall, lock, root/jailbreak)SHIPPED-P1SPECCED-SEAM (P2)SPECCED-SEAM (P2)SPECCED-SEAM (P2)SPECCED-SEAM (P3)SPECCED-SEAM (P3)
behavior_monitorProcess/file/network activity with PID attribution (ETW) + module healthSHIPPED-P1SPECCED-SEAM (P2)SPECCED-SEAM (P2)ROADMAP-P2+N/A-OSN/A-OS
ransomware_guardCanary files + mass-encryption heuristics with real kill/quarantineSHIPPED-P1SPECCED-SEAM (P2)SPECCED-SEAM (P2)ROADMAP-P2+N/A-OSN/A-OS

N/A-OS is deliberately distinct from ROADMAP: on iOS/iPadOS the OS forecloses file/process scanning and behavioral tracing, so those cells will not ship by platform design rather than "coming later". P2 targets a macOS System Extension, a Linux root daemon (fanotify/eBPF) and Android native; P3 an iOS/iPadOS Network Extension content filter.

Privilege separation

The desktop agent is two processes with a hard privilege boundary:

  • A Wails app (Go + WebView2 + relocated React) holds no privilege and is only an IPC client — it can request, never enforce.
  • A privileged service, sentova-serviced, runs as SYSTEM, exposes no network listener, and is the only process that can terminate or quarantine.

They talk over a go-winio named pipe (github.com/Microsoft/go-winio), and the privileged module's only third-party dependencies are that pipe library and golang.org/x/sys for the WFP/ETW/DPAPI/ACL Win32 bindings — a deliberately tiny trusted surface for the component that holds all the power.

The OS-abstraction seam is one interface file (platform/ports.go); windows.go implements it for real, darwin.go/linux.go are not-wired seams. Every port is designed around an attack it must survive — e.g. the process killer must re-confirm identity before it acts, closing the PID-reuse window:

// ProcessKiller terminates a process by PID. Implementations MUST re-check the
// process identity (expectedName) before killing to avoid PID-reuse races.
type ProcessKiller interface {
    Kill(pid uint32, expectedName string) error
}

The signed-directive enforcement path

An agent that can kill and quarantine is a loaded weapon pointed at the machine it protects, so the path from "an enforcement decision" to "the privileged code that acts" is the most safety-critical thing in the product. It is a sequence of fail-closed gates:

  1. IPC authorize. The connecting peer's token SID must equal the enrolling user's SID — resolved once at Accept by impersonation, not per-request, to dodge a PID-reuse TOCTOU. Mutating ops additionally require integrity level >= IntegrityMedium; a Low-IL / sandboxed caller is refused, and a failure to read the level is itself a rejection. A relayed enforce_directive must also re-verify against the pinned server key — the channel is never trusted on a relayed directive's word alone.
  2. VerifyDirective — signature before everything. A fixed six-step order (ported verbatim from the retired Rust implementation) so no forged field is consulted before the signature is proven: device-id binding → alg == ed25519 → resolve pinned key by keyId (unknown ⇒ reject) → recompute the signed core over canonical params and verify the Ed25519 signature → only then expiry → known type + per-platform allowlist.
core := dsig.DirectiveSignedCore(env.ID, env.DeviceID, env.Type, paramsHash,
    env.CreatedAt, env.ExpiresAt, env.Nonce)
if err := dsig.VerifyB64(key, []byte(core), env.Sig.Value); err != nil {
    return Verified{}, directiveRejectedf("signature: %v", err)
}
// expiry checked AFTER signature so a forged expiresAt can't help
if expires < nowUnix { return Verified{}, directiveRejectedf("directive expired") }
  1. One shared Enforcer. A single struct is the only place a verified directive is driven through the kill / quarantine / network-filter ports, shared by the IPC path, the check-in loop, and confirm_directive. Destructive kill_process/quarantine_file are held for in-app confirmation; nil ports on a non-elevated run yield a non-applied "degraded" ack rather than a false success.
// Shared by the IPC path, the check-in loop, and confirm_directive so a directive
// enforces AT MOST ONCE (bounded by a shared ReplayGuard).
type Enforcer struct {
    killer      platform.ProcessKiller
    quarantiner platform.Quarantiner
    netFilter   platform.NetworkFilterBackend
    guard       *agent.ReplayGuard
    // ...
}
  1. Bounded at-most-once ReplayGuard. An expiry-keyed seen-cache keyed on both id and nonce; a re-seen id/nonce is rejected as replay after first acceptance, entries drop once the directive's own expiry passes, and capacity is bounded by evicting the soonest-expiring keys under a flood. Because the same guard sits behind all three entrypoints, dedup cannot be bypassed by choosing a path.

Local-first ransomware response

A canary → correlate → kill/quarantine loop responds to a ransomware trip without a server round trip. It drains canary events, runs ransomware_guard decision logic, and on a critical trip immediately drives the local-first response (kill the attributed process, quarantine the touched file). Filesystem-layer events carry no PID, so an unattributed trip still quarantines the file and simply skips the kill. A disabled ransomware_guard toggle takes no automated action but still records an honest alert — and every "what Sentova did" string reflects what actually happened, enforced by a banned-claims grep over shipped strings that forbids a fixed success sentence.

On-device scan honesty

The Windows ioc_scan feeds a platform target enumerator into an in-memory Tier-1 hash-prefix filter, single-flighted per scope so a burst of same-scope rescans cannot spawn parallel disk walks. A not-ready rescan does not consume a replay slot, so a redelivery can still scan; a Tier-1 hit is provisional until a Tier-2 confirm; a clean pass reports "no known indicators found" rather than silence.

At-rest sealing and ACL hardening

  • DPAPI sealerCryptProtectData/CryptUnprotectData bind the device Ed25519 seed blob to Sentova with fixed app entropy: CurrentUser scope for a user-run dev build (works non-elevated, genuinely unit-tested), machine scope for the installed SYSTEM service, and never a UI prompt.
  • ACL hardening — a protected DACL on %ProgramData%Sentova and the quarantine store, closing the "0600 is ignored on NTFS → BUILTINUsers can read" hole. It is elevation-gated: on a non-elevated dev run it is skipped (applied=false) rather than locking the user out.

Authentication / IAM (the backend is the IdP)

argon2id passwords; EdDSA (Ed25519) access JWTs with a 15-minute TTL and refresh rotation; opt-in TOTP (AES-256-GCM-sealed secret, argon2id recovery codes) and WebAuthn passkeys with sign-count-regression fail-closed; an MFA step-up that returns 401 mfaRequired with a ticket and stamps amr/mfa/aal claims. Authorization is a roleless groups+policies PDP with AWS-style resource names (srn:sentova:<service>:<accountId>:<type>/<id>), default-deny and explicit-deny-wins.

Testing and security posture

Enforcement, IPC/verify, module logic, the replay guard, correlation and scan are all covered by Go unit tests using fake ports, so they run anywhere. The split is stated as plainly as the repo's own tracker states it: the live system-mutating execution of WFP filtering, ETW telemetry and the TerminateProcess kill primitive is elevation-gated and was not executed on this non-elevated build. What was exercised non-elevated: the authenticated named-pipe round-trip including an integrity-gated mutating op for a same-user peer, and the CurrentUser DPAPI seal round-trip. A kernel/SYSTEM-equal attacker is explicitly out of scope for P1.

Related write-ups

Write-up in progress

A companion post for this project is being drafted. Check back soon.