Replaces the flat battery x display matrix (which misclassified laptops
as phones) with layered detection: container -> vm -> SMBIOS chassis ->
ARM fallback. Adds laptop, vm, container device types across agent,
migration 062, server validation, web icons/types.
DEVICE-002: ARM machine-ID fallback — device-tree model + /etc/machine-id
combo, then /proc/cpuinfo Serial (all-zero rejected), before the weak
hostname fallback. Hardware-bound IDs on DMI-less devices.
DEVICE-001: agent detects device_type (server/desktop/phone/tablet) from
/sys signals — system battery (scope=Device peripherals excluded, UPS
excluded), DRM connector state, framebuffer min-dimension for phone/tablet
split. Reports device_type/device_model/os_distro in registration and
system-info paths.
SERVER-001: migration 061 — device_type, device_type_manual (operator
override, never agent-written), device_model, os_distro on agents.
effective_device_type computed into every serialized agent.
SERVER-002: PUT /admin/agents/:id/device-type — set/clear override,
enum-validated, journaled.
WEB-001: device-type icons + fleet filter, device model in list, detail
header badge with reclassify dropdown, os_distro surfaced.
INSTALL-003: arm64 install path unblocked — helper (required manifest
component) now cross-built aarch64-unknown-linux-musl via rust-lld in the
server image, signed at boot (helperArches += arm64), listed in the release
manifest. Install template already handled uname -m and pacman.
Plus in-flight: desktop tray wiring, enrollment page polish, CI workflow
updates, RAF session-broker/pacman-scanner docs, native installer scaffold.
Desktop:
- Windows tray cross-compiled (cargo-xwin), installed with per-user
autostart Run key; tray actions trigger_scan/approve_update wired to
the local API
- Linux tray off the service child-spawn path — XDG autostart only, kills
the double-launch
- signalDesktopRestart no longer no-ops on Windows (taskkill /F /IM)
- server serves /desktop/:platform/:arch
Web:
- TokenManagement + AgentManagement folded into one Agents & Enrollment
settings page (useRegistrationTokens hook)
Agent/server:
- platform-aware self-update staging (constants/paths.go), no more
hardcoded /var/lib/redflag
- consumer helper gated: sudo systemd-run on Linux, child proc elsewhere
- migration 060 drops the never-used token_seats table
- droppage of dead constructors and orphaned windows.go service methods
Most forges hand back a "latest stable" that ignores prereleases. Fine
until something ships only prereleases for a stretch — us, through
alpha, where every tag under v0.3.0 goes out as a prerelease. Against
that, /releases/latest either freezes or returns nothing, and the
dashboard reads like nothing's moving.
Add a per-row track_prereleases flag. When it's on, the forgejo adapter
walks the full release list and considers prereleases when picking the
highest version; off (the default) keeps stable-only behavior, so the
other adapters don't change. Seed our own self-row on so we stop
looking frozen.
Renamed gitea_releases to forgejo_releases while in there — the wire
format is Forgejo's, Codeberg runs it, and the old name was a misnomer.
Legacy source="gitea" rows still resolve through an alias.
SEC-028 -- a rotated-out server signing key must stop being trusted even when the agent cannot phone home. pubkey.go: bounded stale-cache window on public-key fetch failure; past the window (or when cache age is unknown) it fails closed instead of trusting the cached key indefinitely. Window length is operator policy (command_signing.stale_key_max_age_hours, default 168h/7d) delivered fleet-wide via GET /agents/:id/config; the [1h, 30d] clamp and the existence of the ceiling are doctrine, not knobs. verification.go: CheckKeyRotation refuses when the named key_id is not in the server active set (no primary fallback), and applies the same bounded-stale ceiling to the active-set fetch-failure path so key_id'd commands are no weaker than keyless ones. Server carries the default + 1-720h validation; web surfaces it in Security Settings.
SEC-029 -- the standalone OSV.dev client retries transient transport/5xx/429 with exponential backoff and trips a process-wide circuit breaker after a run of failures, fast-failing to 'unreachable'. Verdict semantics unchanged and still fail-closed; the resilience only stops a transient scanner blip from forcing an operator override.
GATE-004 #4 -- Consumer.ProcessToken holds a mutex so the replay-state guards are never raced by a concurrent caller. Today's single caller (the poll loop) never overlaps; this enforces the one-token-at-a-time invariant for future callers (local-API trigger, retry worker).
RAF/verification/03 and RAF/security/05 document the key-path and OSV changes. ETHOS #3, #4; forward-only doctrine.
dep-scan.sh gates go/npm/cargo on push and bakes an attested posture into the
release — embedded in the server, signed into the manifest. Reasoning and the
two Moby exceptions are in SECURITY.md.
(posture-builder runs rustup; bookworm's cargo is too old for cargo-audit.)
The stats handler initialized updates_by_type but never filled it, so the
"Updates by Type" card always rendered empty (Codeberg #10). Add
UpdateQueries.GetUpdatesByType (grouped by package_type, non-terminal scope)
and wire it into GetDashboardStats.
Severity bars were sized against total_updates (all statuses) while the
severity counts are scoped to non-terminal rows — a scope mismatch. Bars now
size against the sum of the scoped severity values, so they form a true
breakdown that always sums to 100% and never overflows.
Also closed the silent error-swallows in GetDashboardStats: each sub-count
failure is now logged [ERROR] [server] [stats] instead of vanishing.
INSTALL-001: manifest schema with components+artifacts, CI generation
in release gate, manifest-driven install template with --guided and
--checkoff modes, post-install provisioning checks, desktop joins
version lockstep (bump-version.sh + CI build + gate enforcement).
Setup.tsx reduced to primitives (FormSection, TextField, Alert).
sync.Once on TimeoutService/Reconciler/Syncer stop channels.
syncOne returns its error so admin SyncNow reports real failures.
OSV vuln unmarshal failure now logged, still fails closed.
resolveServerURL: operator-configured REDFLAG_PUBLIC_URL wins;
request Host only as fallback with a [WARN]. Applied to install
scripts, registration responses, and fleet-join. Host header is
attacker-controllable on pre-auth endpoints.
AgentLifecycleService, ConfigService, BuildService, ArtifactService,
AgentBuildHandler — added as a unification pass (e56888e6), never
instantiated by any commit since. ~600 lines.
Server container shares the bootstrap .env with postgres, so the
current password is already in our environment. Hardcoded literal
kept only as fallback for the shipped default.
Malformed agent metadata could panic the server (rapid_polling
fields, buffered event metadata, timeout params). scanner_config
asserted uuid.UUID on a user_id the middleware stores as string —
guaranteed panic on both admin endpoints.
Store the fleet-join TOTP seed as AES-256-GCM ciphertext instead of
a SHA-256 hash. Hash-only can never verify a time code without the
host disclosing the seed — which made the 2FA a second cleartext
shared secret. Seed crosses the wire once, at token creation over
the admin-authenticated channel; the join request carries only the
6-digit code.
TriggerProcessScan was building an AgentCommand without Source, which hit
the agent_commands_source_check constraint every time. Every other
command-creation site sets this correctly; this one got missed.
Thanks to QiTechCo for the catch during v0.2.8.0 dev testing.
Co-Authored-By: Xzaviar <user@QiTechCo.qitc.io>
The limiter is in-memory; a restart clears all counters, so an attacker
who can force one gets a fresh budget. For 60s after boot every limit
runs at half its configured budget (min 1), making a restart strictly
worse for the attacker while per-key limits keep reconnecting agents
comfortable. Restart semantics documented in OPERATIONS.md §6.
A token in the query string leaks to shell history, process lists, and
server access logs. The install endpoint now reads X-Registration-Token;
a query-string token is refused with guidance and is never echoed back
or logged. Server-built and web-UI install commands updated (curl -H,
irm -Headers).
Rename CVE/vulnerability language to advisory/threat/fix across the stack:
- Dashboard: installed_cve_count→open_threat_count, security_update_count→available_fix_count
- Update detail: Known Vulnerabilities→Security Advisories
- AdvisoryType() helper for human-readable advisory ID prefixes
- clearVulnsOnInstall on installed transition with per-advisory security event logging
- StatsHandler takes checkInInterval for online/offline threshold
- AttentionPanel re-keyed on open-threats / available-fixes
- NewOrchestrator initialises a log-only TeeLogger (nil buffer) so the
Windows service path never carries a nil logger into executeScan
- rpmEVRAhead returns false when all epoch/version/release components
compare equal; adds test cases for "0:2.0-1" vs "2.0-1" normalisation
- processDesktopSelfToken splits replayCheckAndRecordAgent into
replayCheckAgent (before install) + recordAgentTokenConsumed (after
successful install) so a transient install failure does not permanently
consume the token
- Comments on helperSelfStagingPath (Go) and DEFAULT_HELPER_SELF_SOURCE
(Rust) name each other as the cross-language counterpart
Extract serverSetupRequest type and resolveSetupSigningKeys(): when no keys
are provided the server generates a fresh Ed25519 pair (existing behaviour);
when a private key is provided it is validated and the public key derived from
it (public key may be omitted or supplied for cross-check). Mismatched pairs
are rejected 400. Remove configure-secrets route from welcome-mode router
(was only usable with Docker socket mounted, unreachable in that mode).
Add inferPublicURL() helper to fill publicURL from X-Forwarded-* headers when
the operator omits it. pq.QuoteLiteral() used for password in ALTER USER.
Tests: generate-when-missing, use-provided-pair, reject-mismatched-pair.
Agent-side: reads /proc for all PIDs with 25+ fields (identity, resources,
state, disk I/O, elevation) plus related data on drill-down (open files,
sockets, pipes, env keys, memory map, namespaces, listening ports). Pure
/proc reads, no subprocess spawns.
Server-side: dedicated tables (agent_process_snapshots, agent_processes,
agent_process_related) with JSONB for related data. On-demand scan via
scan_processes command, last-10-snapshot retention. Four endpoints:
report, get latest, get detail, trigger scan.
UI: new Processes tab in agent detail with sortable/filterable table,
search by name/cmdline, state/user filters, and ProcessDetailModal with
tabs for Overview, Network, Files, Environment, Memory, Namespaces.
OSV fail-closed paths, safeTokenFilename traversal, bind check,
disallowed package types covered in agent/internal/supplychain.
Artifact hash resolution and dnf scanner test fixes. Server update
handlers extended for the local-approve flow with OSV version test
coverage.
- README: version v0.2.6.8, corrected stale gate claim, updated changelog
- .env.example: merged two competing files into one, deleted bootstrap duplicate
- ErrorBoundary: new component wrapping app, prevents white-screen crashes
- Layout sidebar: version display from /api/health, Docs link to GitHub
- client-logger: debug/trace logger gated behind localStorage.redflag_debug=1,
routes through existing /logs/client-error server endpoint (ETHOS #1)
- All web console.log calls rerouted through client-logger instead of deleted
- Server health endpoint returns version field
- Server accepts client_debug/client_trace in error_type validation
- Dockerfiles: pinned alpine:latest->3.21, nginx:alpine->1.27-alpine,
added HEALTHCHECK directives
- docker-compose: healthcheck blocks for server and web services
- .dockerignore: created to slim Docker build context
After successful install (confirm_dependencies or capability receipt),
pin the installed version as selected_version instead of clearing it.
This prevents subsequent scans from silently advancing to a newer
unapproved version. The pin is the enforcement mechanism.
- evaluateSupplyChainHold checks version row osv_status for target
version, closing hole where we OSV-check one version but install
another.
- confirm_dependencies uses TransitionByPackageFrom with source-state
guard (StatusInstalling → Installed/Failed).
- Clears pinned version on successful install.
- Version bump to v0.2.6.7.
- useHeartbeatStatus reads from agent metadata (single source of
truth) instead of separate endpoint (eliminated split-brain).
- Online/offline threshold fixed to 10min to match server (was 15min).
- Dashboard polling lowered from 30s to 15s.
- Toggle feedback: invalidate agent queries immediately, clear
loading state after 2s.
- Removed command-table fallback from GetHeartbeatStatus endpoint,
dead helper functions, and unused GetRecentHeartbeatCommands.
getServerURL returned localhost:31337 (the server's own bind address), so
a remote agent's binary/manifest/config fetches pointed at itself and
failed with "Unable to connect". Prefer the host the client actually
reached us on (Host + X-Forwarded-Proto, which nginx forwards), falling
back to PublicURL then the configured bind addr. Also set charset=utf-8 so
irm stops mangling the ✓/⚠ glyphs.
Windows PowerShell 5.1's parser chokes on here-strings in LF-only .ps1
files — the embedded config template blew up with a cascade of
"Unexpected token ':'". Convert the windows script to CRLF at serve time
(parses clean in 5.1 and 7; .ps1 should be CRLF anyway). Linux stays LF.
the supply-chain check was judging available_version while the gate could
install a different one. re-run OSV against the version we actually install
(operator-pinned, else gated), and auto-pin the newest soak-aged clean
version when the gate's enforced — forward-only, operator force-pin wins.
last half of S8. the pool bump and bounded background took most of the pressure
off, but a full fleet can still pin every connection. now there's a valve:
- middleware reads db.DB.Stats(); at the saturation threshold it answers 503 +
retry-after instead of letting writes queue into a deadlock
- fails open — unlimited pool never sheds, default only trips at 100% in-use.
tunable via REDFLAG_DB_SHED_UTILIZATION and _RETRY_AFTER_SECONDS
- wired to the agent write group only. health, metrics, auth, register/renew
and the dashboard stay reachable so you can still watch it and log in while
it's hot
table-driven test across the branches, race clean.
OBS-001A: an authenticated /metrics so something other than a human reading
logs can watch the box. no new dep — emits prometheus text straight from the
counters we already keep:
- /metrics behind a dedicated bearer token (sha-256 hash stored, plaintext only
from REDFLAG_METRICS_TOKEN for bootstrap). constant-time compare, rotates
without a restart, disabled by default, never an open route
- exports db pool, taskrunner snapshot, scheduler + queue, breaker state, and
the deferred-advisory count — read live on each scrape, bounded labels only
- settings + migration 054 for observability.metrics_enabled / _token_hash
also fixed migration 046 — it added a column and an index without IF NOT EXISTS
and backfilled off a column it then drops, so it couldn't survive a second run.
guarded every step; the idempotency lint is green again. only the migrations
the runner hasn't recorded see the change, so live dbs don't care.
builds clean, vet quiet, new tests pass.
server was sized for a campfire, not a fleet. 25 db connections, every agent
report flinging goroutines into the void, the syncer plodding one repo at a
time while clutching a lock nobody needed. loosened the choke points:
- db pool 25 -> 100 + connection lifetime, all env-tunable
- bounded pool for the report-path fire-and-forget work; /health/tasks to
watch it breathe. no more unbounded goroutine spray per report
- upstream syncer runs concurrent now, dropped the dead mutex around repology
fetches, reconciler single-flights instead of locking through the whole crawl
- scheduler caps jobs per tick so an aligned fleet can't stampede the db
- swatted a context-cancel bug that was quietly killing immediate syncs
builds clean, race detector's calm.
approval stopped re-scanning osv; it just reads what detection already
found. soak gate + age gate are real settings now (env→db→default), and
the dead soak-override column + table got composted.
Treats each ecosystem scan as the authoritative full set for that
(agent, ecosystem) pair. Packages absent from a successful scan that
are still in a waiting state (pending/approved) are closed to installed
with out-of-band provenance — no operator action required.
State machine:
- Added pending/approved → installed edges (out-of-band resolution path)
- Added installed → pending edge (reactivation when a new version reappears)
- ReconcileFromScan updated to match: installed now reopens, ignored/failed preserved
Server (ReportUpdates):
- closeScanAbsentRows goroutine: diff waiting rows against reported set,
transition absent rows via transitionStatus (guarded UPDATE, idempotent)
- Provenance stamping: redflag_receipt if a consumed capability token exists,
out_of_band otherwise
- System event emitted per closure for audit trail
- scanEcosystemSupported gate: dnf/apt only; failed/partial scans never close rows
Agent:
- UpdateReport extended with Ecosystem + ScanSucceeded fields
- APT/DNF scan handlers now always report on successful scan (even 0 updates)
- HandleScanAPT/DNF/Updates: report failure is non-fatal (transport problem,
scan succeeded locally)
Queries:
- GetTrackedNonResting: scoped to pending/approved only — in-flight states
(checking_dependencies, pending_dependencies, installing) are orchestrator-owned
- TransitionByID: routes closure through the state machine
- HasConsumedTokenForUpdate: provenance check for the reconciler
- UpdateCurrentStateInTx SQL CASE: installed now reopens to pending on re-scan
Tests: reconcile_test.go (5 unit tests including load-bearing
TestWaitingStatesResolveOutOfBand), reconcile_test.go handler tests (7 sub-tests).
Bump: v0.2.6.1
- #1: ReconcileAll goroutine uses context.Background, not request ctx
- #4: InstallVersion now checks maintenance window before dry-run
- #5+#23: tickAliases fetches stale slugs once before loop; rename shadowing var
- #6: UpsertReconciled sql.ErrNoRows on manual conflict is a no-op, not a warn
- #7: normalizeRepoToEcosystem uses ordered slice, not non-deterministic map
- #8: MatchByContainer ILIKE escapes % and _ metacharacters via REPLACE
- #9: EnqueueDryRun uses target_version key for selected_version, keeps available_version for freshness
- #13: InstallVersion drops second GetUpdateByID, mutates struct locally
- #14: EnrichFromMetadata reserved map is package-level var, not per-call alloc
- #21: recordGateOverride shared helper; recordSupplyChain/SoakOverride delegate to it
- store.ts: remove dead notificationsEnabled setting (no callers outside store)