Watch
1
0
Fork
You've already forked RedFlag
0
Commit graph

471 commits

Author SHA1 Message Date
Fimeg
8ced1d5a47 fix: make Windows installer tolerate missing Ed25519 verifier 2026-06-08 10:47:07 -04:00
Fimeg
4dc750b687 fix: agent install URL preserves port for non-localhost hosts
getServerUrl() was stripping the port when hostname != localhost,
generating install commands on port 80. Now uses window.location.port
directly — the browser's host:port is always reachable by the agent.
2026-06-08 09:25:46 -04:00
Fimeg
7ae7429878 fix: bake reachable host into install script, not server bind addr
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.
2026-06-08 09:15:26 -04:00
Fimeg
495d5fb3a7 fix: serve Windows install script as CRLF
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.
2026-06-08 09:07:43 -04:00
Fimeg
f811b47e0b fix: Windows one-liner self-authenticates again (irm | iex)
The Windows installer relied on a Mandatory=$true -Token param, but the
one-liner ran the script with no -Token -> PowerShell dropped to an
interactive prompt and the install looked hung.

Bake the token/server into the rendered script like the Linux template
already does, drop param() and #Requires (both no-op under iex; runtime
admin check still enforces elevation), and switch the command to
'irm ... | iex' so the body actually pipes. -Token/-Server/-Skip become
RF_TOKEN/RF_SERVER/RF_SKIP_SERVICE_INSTALL env overrides.
2026-06-08 08:44:27 -04:00
Fimeg
40f57edefc v0.2.6.4 2026-06-08 08:14:24 -04:00
Fimeg
b09453e849 install the version that cleared the soak window, not the bleeding edge
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.
2026-06-08 08:14:20 -04:00
Fimeg
ae3db516c0 let the agent spot sunshine and give the overview tab a face for it
observe-only: the agent folds detected integrations into its system-info
report under metadata.integrations; the dashboard renders what it reports.
nothing reaches into the host — an integration can be watched, not commanded.
2026-06-08 08:14:13 -04:00
Fimeg
f136c12902 shed agent writes when the db pool runs dry
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.
2026-06-07 21:16:53 -04:00
Fimeg
cf061a13a1 seal the settings path so secrets land encrypted
the encrypt/decrypt hooks existed but the write path wrote values straight to
the column, so a sensitive setting would have gone in as plaintext. closed it:

- sensitive values serialize + encrypt before they persist (base64 aes-gcm over
  the json), and updates now carry the is_encrypted flag through
- non-sensitive settings are untouched — still plain json, same as before
- audit log redacts sensitive old/new values instead of recording them raw

round-trip test proves sensitive values never hit the column in plaintext and
still decrypt back; non-sensitive stay readable. no backfill needed — nothing
writes secrets through this path yet, the defaults are empty.

builds clean, vet quiet, tests pass.
2026-06-07 21:16:39 -04:00
Fimeg
f7882241c7 give the system a window you can scrape, and patch a migration that couldn't run twice
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.
2026-06-07 20:59:29 -04:00
Fimeg
ae411cf5d4 say it out loud when the advisory feed goes dark
the breaker fails open so a down osv never blocks a patch — good — but the
auto-confirm gate is fail-closed, so a dark feed quietly stops auto-approval
and parks packages unvetted. that truth was sitting in the logs where nobody
looks. now it's a banner.

- /api/v1/health/advisory: breaker state + count of deferred packages
  (self-healing — a successful recheck clears the flag) + a degraded flag
- amber bar in the layout, only when degraded: "feed offline, auto-approval
  suspended, manual still works." says feed-down isn't patching-down
- narrow slice of the gate-visibility work; the full posture panel stays in
  its own session

builds clean, tsc's happy.
2026-06-07 20:17:18 -04:00
Fimeg
e99ad6e8c7 gave the server a breaker so a sulking upstream can't drag it down
osv.dev or repology going dark used to mean every check sat there burning its
30s timeout, one after another. now there's a breaker (ported the agent's, it's
already proven) wrapping both:

- osv: one breaker over the batch + single-query paths. trips after 5 fails in
  a minute, fails open while tripped — an unreachable advisory feed never blocks
  a patch. that's the whole sovereignty bet
- repology: same deal, best-effort, 404 doesn't count against it
- both visible at /health/tasks so you can see them trip and heal

db-pool shedding (503 + retry-after) is the other half — left it for later, the
pool bump + bounded background already took most of that pressure off.

race detector's clean.
2026-06-07 19:45:36 -04:00
Fimeg
d25f6ea030 swept the cobwebs, stopped re-dialing the same three hosts
three more off the scale list:

- rate-limit map now gets swept on a cadence (taskrunner.Every) instead of
  growing forever — nobody was calling the cleanup. first old ticker moved
  onto the runner
- subsystem load was one db query per agent at startup; now it's a single
  ANY($1) for all the online ones. 100 agents, 1 query
- outbound http clients (osv, registries, upstream, agent) were inheriting
  the stock transport that keeps 2 idle conns per host — so every scan burst
  re-dialed. shared tuned transport now, 10 per host, 90s idle

builds clean both modules.
2026-06-07 19:35:32 -04:00
Fimeg
82018bfb80 taught the rocks to stop tripping over each other under load
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.
2026-06-07 19:17:08 -04:00
Fimeg
5b1a16ca3e v0.2.6.2 — osv scans moved to detection, soak gate grew up
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.
2026-06-07 14:44:41 -04:00
Fimeg
b82649967e RECONCILE-001: scan-set closure (close-by-absence) + v0.2.6.1
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
2026-06-06 20:46:29 -04:00
Fimeg
edf799d8d5 feat: filter/search primitives — composable UI components + hooks
Replace monolithic FilterBar/useFilterState with proper primitives:

Components (pure UI, zero state):
- SearchInput — controlled search input with icon + clear
- FilterPill — colored dismissible chip
- FilterDropdown — styled select with label + active state
- FilterCountButton — clickable stat card with count + color

Hooks (single responsibility):
- useDebounce — generic value debounce
- useFilterUrl — URL param sync only
- useQueryParser — raw query → parsed text + filters + pills
- useMultimodalFilter — composes above two for key:value search

Pure functions:
- queryParser.ts — parseQuery/buildQueryString

Migrated all 6 pages:
- Docker: full multimodal search with key:value parsing, clickable stat cards
- Updates: SearchInput + FilterDropdown + FilterPill with clear all
- Agents: SearchInput + FilterDropdown (status, OS)
- LiveOperations: SearchInput + FilterDropdown
- TokenManagement: SearchInput
- History: SearchInput + useDebounce
2026-06-05 22:40:33 -04:00
Fimeg
b346c1386e v0.2.6.0 2026-06-05 21:34:12 -04:00
Fimeg
cea3986fae fix: SPA nav hygiene + history crosslinks
- Layout/Dashboard refresh buttons use queryClient.invalidateQueries, not reload()
- LiveOperations view-update/view-agent use navigate(), not window.open new tab
- Updates.tsx filter sync uses setSearchParams, not window.history.replaceState
- ChatTimeline: View Agent shows hostname; package links go to /updates/package/:type/:name
2026-06-05 21:30:45 -04:00
Fimeg
1ec7999d8f fix: code review batch 1 + dead store setting
- #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)
2026-06-05 21:26:44 -04:00
Fimeg
f005255e68 feat: docker enrichment pipeline, package detail, update history pagination 2026-06-05 21:18:06 -04:00
Fimeg
d1424e8377 feat: FEAT-002 metadata pipeline, GATE-005 soak gate, BRIDGE-001 auto-discovery
FEAT-002: Server-side metadata pipeline
- ReportUpdates merges PackageDescription/CVEList/KBID/SizeBytes into metadata JSONB
- UpdateCurrentStateInTx uses JSONB merge (||) instead of replace
- Fixes current_version bug (missing EXCLUDED.current_version)
- EnrichFromMetadata() on UpdateState populates display fields from metadata
- mergedVulnerabilities() deduplicates agent CVEs with OSV.dev results
- VulnerabilityEntry struct for unified vulnerability representation

GATE-005: Version soak-gate
- Migration 050: selected_version column, version_soak_overrides table
- soak_gate.go: SoakGateConfig/EvaluateSoakGate (14-day default, block enforcement)
- POST /updates/:id/install-version endpoint with soak evaluation
- EnqueueDryRun reads COALESCE(selected_version, available_version)
- Override journals to system_events (recordSoakOverride)

BRIDGE-001: Tracked software auto-discovery
- Migration 051: repology_slug/container_image_pattern/binary_probe on tracked_software,
  repology_aliases table, match_method/package_name on agent_tracked_software
- RepologyCache: fetches /api/v1/project/{slug}/packages, normalizes repos to ecosystems
- ReconciliationQueries: MatchByRepology/MatchByContainer/MatchByExactName
- Reconciler: cascade matching service with hourly loop, hooks into ReportUpdates
- UpsertReconciled: preserves manual operator bindings
- COMMON_SEEDS updated with repology_slug values
- Syncer updated with 24h alias refresh ticker
2026-06-05 17:43:11 -04:00
Fimeg
938d0bee35 v0.2.5.2 2026-06-05 16:37:56 -04:00
Fimeg
cad1554d94 feat: SETTINGS-001 reversible token encryption + one-liner restore
Reversible AES-256-GCM encryption for registration tokens so the
install one-liner can auto-fill the token value again. Migration 049
adds token_encrypted column; token_hash kept for lookup.

AgentManagement.tsx gets platform cards (Linux, Windows; macOS = soon)
with generateInstallCommand for all platforms. Inert until backend lands.

system_event_logger.go extracted from deleted event_stream.go —
SystemEventLogger survives, unified substrate does not.

security_settings_service.go and secrets_manager.go simplified.
2026-06-05 16:17:47 -04:00
Fimeg
4ccbf8c5fd fix: heartbeat auto-queue treats duplicate-pending as benign (ETHOS #4)
queueSystemHeartbeat now checks for the unique violation on
idx_agent_pending_subsystem and logs at INFO instead of WARN.
A duplicate means the desired heartbeat is already in flight —
not a failure. Suppresses the scary WARN on every rapid-polling
enable where a heartbeat was already pending.
2026-06-05 16:17:36 -04:00
Fimeg
5d7babff22 fix: History 500, rename to GetFleetActivity, remove dead unified substrate
GetFleetActivity replaces GetAllUnifiedHistory — filters now apply once on
the outer aliased result instead of per-arm, fixing the agent_id ambiguity
when the logs arm joins update_packages (the 500).

Three new UNION arms: update_events, update_version_history, system_events.
Package name joined to update_logs via update_package_id.

Dead code removed: event_stream.go (handler + service), UnifiedEventTimeline.tsx,
useEvents.ts — orphaned from the abandoned unified path (HANDOFF-2026-06-05).
SystemEventLogger extracted to system_event_logger.go (unstaged, next commit).
2026-06-05 16:17:27 -04:00
Fimeg
62f2764260 v0.2.5.1: lifecycle, live operations, unified history
Lifecycle:
- ReopenUpdate + ResolveUpdate replace RetryUpdate; routes for
  GET /updates/:id/lifecycle and POST reopen/resolve
- confirmUpdateCommand marks update_agent completed on version attestation
- migration 048: started/running added to update_logs.result
- helper atomic_replace_binary: copy-to-sibling then rename() (ETXTBSY)

Live operations:
- event_stream service + /events endpoints, LiveOperations page
- capability-token queries for the live view

History + CVE:
- History page reads /events/recent: filterable lifecycle/command/
  system/orchestrator timeline with agent crosslinks
- CVE drill-down: OSV parse carries CVSS vector, fixed version, published
  date, severity; issuer-linked aliases (CVE->NVD, GHSA->GitHub, ALSA->errata)
- DependencyClosureTree: one shared closure component in update detail
- STARTED (blue spinner) and PARTIAL (amber) result badges
2026-06-05 09:13:42 -04:00
Fimeg
6c331e909b update_logs.result: add started/partial/running — fix agent-report badge semantics
The schema only allowed success/failed/partial. The agent sends 'started' as a
progress report and 'partial_failure' when a multi-scanner scan had mixed
results. Both were being remapped to 'failed' by the server's fallthrough
default, so the timeline showed a red FAILED badge for 'starting agent update'
and for scans where only some scanners errored.

Two-value migration (no-data):
  - Add 'started' and 'running' to the update_logs.result CHECK constraint
  - Add them to isValidResult so they pass through without remapping
  - Fix the fallthrough switch: partial_failure -> partial (not -> failed)
  - Clean up if/else chain to a switch while we're in there

UI:
  - STARTED badge (blue spinner) for progress reports
  - PARTIAL badge (amber triangle) for partial results
  - Both ChatTimeline and HistoryTimeline updated

Event renderer:
  - 'started' -> 'Agent binary update initiated'
  - 'partial' for install/update_agent cases
2026-06-05 09:13:42 -04:00
Fimeg
0dcfe25705 unified agent+helper upgrade: closure carries both binaries
Server: mintAgentSelfToken includes helper in closure, sends
helper_download_url + helper_checksum in command params.

Agent: downloads and stages both binaries, passes --helper-file to helper.

Helper: parses --helper-file, separates closure into agent+helper entries,
self-updates helper binary first, then installs agent. Falls back to
agent-only if closure has 1 entry (backward compatible).
2026-06-05 09:13:42 -04:00
Fimeg
b30261791d fix: result file 0644 so agent can read back from root-owned helper
The helper runs as root via systemd-run. With 0640 root:root, the
unprivileged agent user cannot read the result. The result directory
is 0700 agent-owned which already blocks other local users.
2026-06-05 09:13:42 -04:00
Fimeg
482e5a9aad security: path traversal, file perms, sudoers/polkit scope, staging cleanup
- consumer.go: safeTokenFilename() blocks path traversal via token ID
- consumer.go: TOCTOU sanity check on result token_id
- main.rs: result file written 0640 (was 0644)
- linux.sh.tmpl: sudoers wildcards restricted to tokens/* and results/*
- linux.sh.tmpl: polkit scoped to manage-transient-units
- agent_update.go: clean up pending-upgrade.bin on failure
- updates.go: clear is_updating flag on failed update_agent
- bump 0.2.3.7
2026-06-05 09:13:42 -04:00
Fimeg
cff31d6106 v0.2.3.5: unlock self-update + gated installs on fresh hosts
We kept claiming self-update worked. On a clean box it didn't.

- linux.sh.tmpl: install a polkit rule so the service user can invoke the
  helper via systemd-run. Without it every gated install and self-update
  hit auth_admin and died on a TTY-less service.
- self-update: drop the post-update .bak sweep. It ran unprivileged against
  a root-owned backup and could only ever log permission-denied. The helper
  already keeps .bak as the single rollback slot.
- metrics/docker reports: stop finalizing the command at ingest. It raced
  ReportLog and 409'd the history-bearing log, silently dropping system and
  docker scans from History. ReportLog is the sole finalize point now, same
  as dnf/storage.
2026-06-05 09:13:42 -04:00
Fimeg
5758b26875 swap uuid lib, windows installer pass, README/RAF copy
- google/uuid -> gofrs/uuid/v5 across server + agent
- windows.go: cross-platform binding cleanup
- linux install template: disable sudo lecture for TTY-less service user
- README: XZ/SolarWinds lede, stable-release note, single attack-surface block
2026-06-03 15:39:49 -04:00
Fimeg
6327c13460 fix: metadata race, gate predicate drift, OSV concurrency, closure pre-check
ApproveUpdateWithVulns uses JSONB merge (||) instead of full replace —
concurrent checkClosureAndAdvance no longer loses its keys.

evaluateSupplyChainHold gates on ClosureCleared not ClosureChecked —
manual and auto paths now share the predicate for real.

RunOSVChecks fans out batches with goroutines bounded by the 4-slot
semaphore instead of running them sequentially.

Zero-dep capability path verifies a pinned closure exists before
transitioning to installing — no more opaque mint failure after state
change.
2026-06-01 15:21:50 -04:00
Fimeg
7a154e11ee docs: changelog gets its own life, README tells the truth about v0.2.3.1
changelog entries for v0.2.2.0 (state machine + orchestrator), v0.2.3.0
(OSV batch, closure-wide checks), v0.2.3.1 (vuln is a full stop).
README condensed to point at CHANGELOG.md, gate description updated
from "soon" to what it actually does now.
2026-06-01 15:06:39 -04:00
Fimeg
36923bb509 ops: supply chain section now tells the truth — advisory fails open, the gate doesn't 2026-06-01 09:56:09 -04:00
Fimeg
dc3bfdf493 supply chain gate: a vuln is a full stop now — admin signs for it or it doesn't ship
delivery plumbing that got us there:
- acks clear on result-recorded, not command lifecycle status (no more 34-deep recycling)
- timeouts, cancels, dropped acks/receipts, failed actions all land in history instead of dying on stdout
- one shared closure-cleared predicate so auto-confirm and manual approve can't drift

override waives the vuln call only and gets journaled; signing and hash verification stay non-negotiable.
2026-06-01 09:55:22 -04:00
Fimeg
1b6892ad2d I forgot to commit all the working files - let's start the day off from a clean tree 2026-06-01 08:09:28 -04:00
Fimeg
37a2302628 docs: promote 7zip dependency-resolution shot to the top showcase row 2026-05-31 22:11:00 -04:00
Fimeg
2b02f65dbd v0.2.3.1: bump version (README, versions.go, compose) 2026-05-31 22:07:40 -04:00
Fimeg
91e097f54a 0.2.3.0: README honesty pass + fresh screenshots — version, clone count, and OSV now tells the truth about checking your deps' deps 2026-05-31 22:00:09 -04:00
Fimeg
33c92ba04d 0.2.3.0: auto-confirm now frisks the whole dependency closure for CVEs, not just the package you asked for (the quiet deps are always the ones carrying) 2026-05-31 21:56:35 -04:00
Fimeg
b7eba59dd8 0.2.3.0: fix dnf dry-run success detection, clear stale auth on logout
dnf DryRun: --assumeno cancels the transaction, so DNF exits non-zero
even on a dry run that resolved cleanly — the old check failed those
(curl-style single-package upgrades with no extra deps showed FAILED).
But non-empty stdout is not success either: 'No match for argument',
'Nothing to do', and 'Error:' all print output and exit non-zero, and
treating them as success would mint a capability token for a transaction
that never installs (fail-open). Gate on an actually-resolved
transaction (a 'Transaction Summary' block, which never coexists with
'Nothing to do') instead.

web logout: clear the zustand persist key (auth-storage) alongside
auth_token and user, so a JWT from a prior server reinstall (JWT_SECRET
rotation) does not survive a logout + re-login cycle. Drop the redundant
localStorage removal in Layout — the store owns logout cleanup.
2026-05-31 21:19:32 -04:00
Fimeg
fe3a759029 0.2.3.0: switch OSV checks to batch endpoint, global concurrency cap
- Replace per-package HTTP requests with /v1/querybatch (100 per POST)
- Process-wide semaphore (4 concurrent batches) shared across all callers
- 300 packages: 3 HTTP calls instead of 300
- 30s timeout for batch requests
- Failure recording unchanged: no checked_at = retry next cycle
2026-05-31 18:04:07 -04:00
Fimeg
e8f69212be 0.2.3.0: fix OSV supply-chain checks — bounded concurrency, persist-driven dedup
- Replace unbounded goroutine fan-out with bounded pool (8 concurrent)
  so 300-package dnf scans no longer timeout every request against api.osv.dev
- Drop in-memory osvDedup sync.Map; gate on persisted supply_chain_checked_at
  so the dedup survives restart and failed checks retry naturally
- On query failure, record the error without checked_at so the package stays
  a candidate for the next cycle (ETHOS: errors are history, assume failure)
- Shared RunOSVChecks in services/ used by both scan path and startup backfill
- Add FreshSupplyChainPackages query for persist-driven freshness lookup
- Bump version to 0.2.3.0
2026-05-31 17:54:09 -04:00
Fimeg
6cae9300c4 v0.2.2.0: lifecycle orchestrator foundation (LIFECYCLE-003)
Add server/internal/orchestrator — a stateless, DB-driven service that advances
the package update lifecycle instead of leaving every transition to an operator
button press. It holds no state of its own: each 60s sweep re-reads from the DB,
so a restart reconciles on the next tick, and every advance rides the
LIFECYCLE-001 guarded transition (idempotent by construction).

- Auto-approval is policy-gated by policy.auto_approve_max_severity (default
  off). Eligible pending packages are approved and their dry-run enqueued
  (-> checking_dependencies). It never advances to installing on its own, never
  runs when allow_dry_runs=false, never approves packages carrying
  supply_chain_vulns, and an unrecognized policy value fails safe to disabled.
- Stuck-state recovery: checking_dependencies past 30m re-enqueues the dry-run
  up to twice then fails the package; installing past 60m fails it;
  pending_dependencies past 24h logs a stale count without changing state.
- Synchronous fast-path: ReportUpdates fires OnPackagesDiscovered() so
  auto-approve runs at scan time, serialized with the timer via TryLock.

Supporting changes: exported TransitionByID/TransitionByPackage and
GetPackagesInStatus/BumpRetryCounter on UpdateQueries; GetPolicyString on the
settings service; EnqueueDryRun extracted from the manual dry-run endpoint and
shared with the orchestrator. Unit tests cover policy gating, requeue-then-fail,
and the install timeout.
2026-05-31 17:10:25 -04:00
Fimeg
2a0c800659 v0.2.2.0: enforce package state machine across all transitions, vuln dashboard
Route every current_package_state status change through one transitionStatus
path: read the observed status, validate against PackageStatusTransitions,
run a status-guarded UPDATE, record terminal history. Replaces ten raw-SQL
transition functions whose WHERE guards validated nothing and silently
no-op'd on an illegal state. ApproveUpdate, the Reject/Install/Set* family,
BulkApprove and UpdatePackageStatus now share the core; illegal moves return
a named from->to error instead of a silent miss, and concurrent callers are
caught by the guarded row count.

Migration 047 renames the terminal success state updated -> installed in
current_package_state and update_version_history, realigning both CHECK
constraints with the Go PackageStatus/HistoryStatus constants.
UpdateStats updated_updates -> installed_updates to match.

UpdateCurrentStateInTx documents its reconcile CASE as the SQL twin of
models.ReconcileFromScan so the two stay in lockstep.

Dashboard: vulnerable-package count surfaced in AttentionPanel, plus a
Vulnerable quick-filter on the Updates view.
2026-05-31 16:39:58 -04:00
Fimeg
6c5c3cb6c0 v0.2.1.3: fix dry-run version targeting, migration 046, helper cgroup access, UI refresh 2026-05-31 11:52:36 -04:00
Fimeg
fcabaefe82 agent: terminal backoff for dead credentials instead of retry loop
When ErrRefreshTokenInvalid or ErrMachineMismatch fires, the agent now
waits 10 minutes between poll attempts instead of exponential backoff.
These are permanent states — no amount of retrying fixes a dead
refresh token or a machine_id mismatch. The agent stays alive and
visible, waiting for operator intervention through the dashboard.
2026-05-30 15:21:06 -04:00