v0.2.0 is the most significant XCloak release to date. The focus was depth over breadth: hardening everything already built rather than adding more features.
Here's what changed.
Backend Security Hardening
httpOnly Cookie Auth and Refresh Token Rotation
Access tokens were previously returned only in the JSON response body and stored by the frontend in memory. This works but leaves a gap: there's no robust mechanism for token renewal without user re-login.
v0.2 introduces httpOnly cookie-based session management. On every login, the backend issues:
- An 8-hour access token as an httpOnly cookie (inaccessible to JavaScript)
- A 7-day refresh token as a second httpOnly cookie, scoped to
/api/auth/refreshonly (so it can't be accidentally sent to other endpoints)
The refresh token is rotated on every use — each call to /api/auth/refresh invalidates the old token and issues a new one. Old tokens are revoked in Redis. This protects against token theft even if a refresh token is somehow extracted.
Mobile clients (Dart/Android) additionally receive tokens in the response body because they don't use browser cookies.
Closing the Rate Limiter TOCTOU Race
The previous sliding-window rate limiter had a subtle race condition: it read the window count, then wrote the new entry in two separate Redis commands. Under concurrent requests, two requests could both pass the limit check before either incremented the counter.
The fix is a single atomic Lua script executed via Redis EVAL:
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window)
local count = redis.call('ZCARD', key)
if count >= limit then
return 0
end
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('EXPIRE', key, math.ceil(window / 1000))
return 1
ZREMRANGEBYSCORE, ZCARD, and ZADD happen atomically. No gap between read and write. This closes the race entirely. Full write-up: Closing the TOCTOU Gap in Redis Rate Limiting.
PostgreSQL RLS Operationally Active
XCloak had Row-Level Security policies defined since day one, but the app was connecting to PostgreSQL as a superuser — which bypasses RLS. The policies existed on paper but didn't enforce anything.
Migration 000057 creates the xcloak_app role (DML-only: SELECT, INSERT, UPDATE, DELETE on all tables, no DDL) and grants it default privileges for future migrations. The backend now uses a separate pool that connects as xcloak_app. The migration pool (DDL, runs on startup) still connects as the superuser.
With this change, RLS is load-bearing for every query. A bug in the application that forgets to set app.tenant_id returns no rows rather than all rows.
Kafka Event Bus Wired End-to-End
Five publish functions existed in the codebase and were never called. v0.2 wires them:
PublishFIMAlert— called fromfim_service.goon every FIM detectionPublishYARAMatch— called fromyara_service.goon every YARA hitPublishIncident— called fromincident_service.goon incident creationPublishAuditEvent— called fromaudit_service.gofor high-risk actionsPublishTaskDispatched/Completed— called fromtask_service.go
Six consumer goroutines process these events: FIM auto-quarantine, YARA auto-quarantine, incident webhook delivery, audit Splunk streaming, WebSocket broadcast, IOC correlation.
FIM and YARA matches now automatically create quarantine_file tasks in the pending-approval queue. Operators review and approve from the Tasks screen; the agent only acts after explicit approval.
Go Agent Enterprise Upgrade
Process-Aware Network Connections
The previous agent reported raw socket tuples: src_ip:port → dst_ip:port with no process context. This meant an analyst had to manually correlate network data with process data from a separate tab.
v0.2 maps every connection to its owning process using the Linux /proc filesystem:
/proc/net/tcp → hex-encoded socket table with inode numbers
/proc/<pid>/fd/ → symlinks like "socket:[inode]" for each open fd
/proc/<pid>/comm → process name
/proc/<pid>/exe → executable path (symlink)
The agent builds a reverse map: inode → PID → name/path, then enriches every TCP/UDP entry. The result: every connection in the UI shows the owning process name, PID, and exe path without any additional queries. Full write-up: How XCloak Maps Linux /proc/net to Process Names.
On Windows, the agent uses netstat -ano (which includes PID) and tasklist /fo csv (which maps PID → name), combined via a lookup table.
Full User Inventory
The previous user collector gathered: username, UID, shell. v0.2 expands this to 12 fields per user:
type User struct {
Username string
UID int
GID int
HomeDir string
Shell string
Groups []string // from /etc/group
SudoAccess bool // from /etc/sudoers + wheel/sudo/admin group
HasSSHKey bool // ~/.ssh/authorized_keys exists and non-empty
LastLogin string // from `last -n 1 <username>`
Enabled bool // /etc/shadow password field (!/* = locked)
}
Sudoers parsing covers /etc/sudoers, /etc/sudoers.d/*, and group membership checks (wheel, sudo, admin groups). This gives SOC analysts an accurate picture of lateral movement paths and privilege escalation vectors.
Android Mobile Agent Enterprise Upgrade
24 Posture Fields
The previous posture check collected 8 fields. v0.2 expands to 24:
- Battery level and charging state (via
dumpsys battery) - Storage total and free GB (via
df /data) - RAM total (via
/proc/meminfo) - Network type (WiFi/Mobile/Ethernet/None via
connectivity_plus) - VPN detection (via
NetworkInterface.list()— checks fortun*,ppp*,vpn*interfaces) - USB debugging (via
settings get global adb_enabled) - Unknown sources (via
settings get secure install_non_market_apps) - Security patch level (from
AndroidDeviceInfo) - Manufacturer and hardware board name
- Android SDK version
- Build fingerprint
Retry with Exponential Backoff
The previous ApiClient had no retry logic. Any transient network hiccup caused a check-in to fail silently. On a mobile device moving between WiFi and mobile networks, this was a frequent occurrence.
v0.2 adds backoff inside ApiClient._withRetry():
- Up to 3 retry attempts
- Delays: 500 ms → 1 s → 2 s (each + up to 200 ms random jitter)
- Retries on: HTTP 5xx, HTTP 429,
SocketException,TimeoutException - No retry on HTTP 4xx (client error — retrying won't help)
- 30-second per-request timeout via
Future.timeout()
Thundering Herd Prevention
If 500 devices all reboot simultaneously (e.g. after an OS update push), they would all start their background timers at the same time and hit the backend in a synchronized burst.
v0.2 adds _schedulePeriodic(), which applies random jitter (0–30 s) before the first tick of each timer. The second and subsequent ticks run on the fixed interval. On a 500-device fleet, check-ins are spread across 30 seconds instead of firing simultaneously. Full write-up: Building Enterprise Android MDM Without Google EMM.