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.
| Verdict | Meaning |
|---|---|
PROTECTED | No active indicators; posture within tolerance. |
AT_RISK | Weakened posture or a provisional/unconfirmed signal. |
COMPROMISED | A 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 path | Directory | Role |
|---|---|---|
sentova/server | server | Backend modular monolith (:9700) |
sentova/core | core | Shared pure logic, imported by both sides |
sentova/desktop | apps/desktop | Wails app (unprivileged UI) |
sentova/desktop-service | apps/desktop/service | Privileged 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.
| Module | What it does | windows | macos | linux | android | ios | ipados |
|---|---|---|---|---|---|---|---|
network_filter | Blocks blocklisted/phishing domains + infra (WFP on Windows) | SHIPPED-P1 | SPECCED-SEAM (P2) | SPECCED-SEAM (P2) | SPECCED-SEAM (P2) | SPECCED-SEAM (P3) | SPECCED-SEAM (P3) |
ioc_scan | Matches STIX 2.1 indicators against files/processes/domains/hashes | SHIPPED-P1 | SPECCED-SEAM (P2) | SPECCED-SEAM (P2) | SPECCED-SEAM (P2) | N/A-OS | N/A-OS |
posture | Scores hardening (patch age, disk encryption, firewall, lock, root/jailbreak) | SHIPPED-P1 | SPECCED-SEAM (P2) | SPECCED-SEAM (P2) | SPECCED-SEAM (P2) | SPECCED-SEAM (P3) | SPECCED-SEAM (P3) |
behavior_monitor | Process/file/network activity with PID attribution (ETW) + module health | SHIPPED-P1 | SPECCED-SEAM (P2) | SPECCED-SEAM (P2) | ROADMAP-P2+ | N/A-OS | N/A-OS |
ransomware_guard | Canary files + mass-encryption heuristics with real kill/quarantine | SHIPPED-P1 | SPECCED-SEAM (P2) | SPECCED-SEAM (P2) | ROADMAP-P2+ | N/A-OS | N/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:
- 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 relayedenforce_directivemust also re-verify against the pinned server key — the channel is never trusted on a relayed directive's word alone. - 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") }
- 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. Destructivekill_process/quarantine_fileare 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
// ...
}
- Bounded at-most-once ReplayGuard. An expiry-keyed seen-cache keyed on
both
idandnonce; a re-seen id/nonce is rejected asreplayafter 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 sealer —
CryptProtectData/CryptUnprotectDatabind 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%Sentovaand 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.