Migration 036 converts every TIMESTAMP column to TIMESTAMPTZ across 20+ tables,
interpreting existing values as UTC. Server handlers, queries, services, and
agent files converted from time.Now() to time.Now().UTC() consistently.
Monotonic-paired time.Now() sites preserved where needed.
The general-case "any Git remote" adapter. source_ref is a clone URL —
HTTPS or git:// — anything go-git can resolve. Fetch performs the
equivalent of `git ls-remote --tags <url>` against the remote, filters
to tag refs (skipping the "^{}" peel suffix for annotated tags), and
picks the highest by CompareVersions (which is why the rc10/rc2 fix
matters: this adapter actually relies on numeric-aware suffix sort).
Uses go-git's in-memory storage so nothing hits disk and there's no
runtime dependency on the git binary in the server image. ListContext
respects the syncer's per-row 20-second timeout via the passed ctx.
Anonymous-only. Private repos belong on the dedicated github / gitea /
gitlab / bitbucket adapters which honor their token env vars; the
git_tags adapter is for the long tail (kernel.org, savannah, any
non-platform Git remote).
Dep additions: github.com/go-git/go-git/v5 plus its tree. The go.mod
hygiene upgrades to golang.org/x/* came along with `go get`; no API
changes affect existing code.
PublishedAt is left nil — ls-remote returns refs and SHAs, not tag
dates, and a follow-up fetch-by-tag would defeat the "no disk, no
clone" design. Operators who want the date should track via the
hosted-platform adapters.
Three more ReleaseSource adapters, all parallel in shape to GitHub:
- gitea — /api/v1/repos/{owner}/{repo}/releases/latest, host from
REDFLAG_GITEA_HOST (mandatory; empty = adapter still
registers but every Fetch surfaces the misconfig on
tracked_software.last_error), token from REDFLAG_GITEA_TOKEN.
- gitlab — /api/v4/projects/{url-encoded path}/releases/permalink/latest,
host from REDFLAG_GITLAB_HOST (defaults to gitlab.com),
token via PRIVATE-TOKEN header from REDFLAG_GITLAB_TOKEN.
source_ref supports nested namespaces (group/subgroup/project).
- bitbucket — Bitbucket Cloud has no "latest release" endpoint, so we
list /2.0/repositories/{ws}/{repo}/refs/tags?sort=-name and
pick the highest via the new CompareVersions (lex-sort from
Bitbucket isn't semver-aware; this closes the loop on the
previous commit). Token from REDFLAG_BITBUCKET_TOKEN.
owner/repo splitting goes through the shared splitOwnerRepo helper
introduced with the github adapter.
Registered in cmd/server/main.go alongside the other adapters.
Adds the third ReleaseSource adapter, dispatched by source="github".
source_ref is "owner/repo" (e.g. "kubernetes/kubernetes"). Calls
GET /repos/{owner}/{repo}/releases/latest and maps tag_name +
published_at + html_url onto the normalized Release shape.
Auth via REDFLAG_GITHUB_TOKEN env var, optional. Unauthenticated cap
is 60 req/hr per source IP across the syncer; authenticated cap is
5000/hr. The 403-with-X-RateLimit-Remaining=0 case surfaces a
distinct error pointing operators at the token, instead of returning
a generic "github said no."
splitOwnerRepo helper will also be used by the gitea/bitbucket
adapters in the next commit (they share the "owner/repo" ref shape),
which is why it's package-level rather than inlined.
Registered in cmd/server/main.go upstream compose block.
The scheduler created agent commands with an empty Signature field and
handed them to CreateCommand, while admin-initiated commands went
through SigningService.SignCommand first. Result: scheduled scans /
update checks reached agents unsigned and were either dropped at
verify time or — worse — slipped through if signing enforcement was
lax. ETHOS §2 ("Security is Non-Negotiable") doesn't permit either.
Wire signingService through NewScheduler. In the worker's processJob,
hard-fail if the service is missing or disabled rather than silently
emitting an unsigned command, and sign the command before CreateCommand.
Reconciliation note in vanguards-memories/system/vanguard-state.md
(line 51) already marked this DONE for 2026-05-23; this commit makes
the working tree match the memory.
The suffix tail of CompareVersions was a plain string compare, so any
multi-digit prerelease counter sorted wrong: "-rc10" lex-compared less
than "-rc2" because '1' < '2'. Drift severity for a project doing rc
iterations would flap unpredictably and git-tag adapters (next commit)
would pick the wrong "highest" tag.
Replace the lex tail with compareSuffix: strip '+build' metadata per
SemVer §10, then walk both sides chunk-by-chunk where each chunk is a
maximal run of digits or non-digits. Digit chunks compare numerically;
non-digit chunks compare lexically; mixed chunks let the digit run sort
lesser (SemVer §11.4.3, also matches rpmvercmp). Shorter prefix wins
when common chunks are equal (SemVer §11.4.4).
Still hand-rolled rather than golang.org/x/mod/semver — that library
rejects "15.4" outright and Repology/endoflife return such versions.
Test additions cover the rc10/rc2 case, dot-separated SemVer
prereleases, build-metadata stripping, alpha<beta<rc lex ordering,
the "more identifiers wins" rule, postgres-style "15rc1" suffixes,
and date-based majors like "20231130-1.fc40".
IsWithinMaintenanceWindow returned false when the table was empty,
blocking every install with 403. Now: no windows configured = unrestricted.
Windows configured = only allow inside them (unchanged behavior).
Six files: compiled-in AgentVersion/ConfigVersion, LATEST_AGENT_VERSION env
default, downloads config_template agent_version, security min_agent_version,
and Makefile ldflags for native agent builds. docker-compose.yml was already
fixed. The Dockerfile uses the ARG BUILD_VERSION so it picks up the compose
default.
Migration 020 created the security_settings table with updated_at /
updated_by but no created_at / created_by columns. Query code
(database/queries/security_settings.go) SELECTs and INSERTs both
create-side fields, causing "failed to initialize default security
settings" at server startup — the dashboard's security panel then
shows hardcoded defaults instead of DB-backed values.
Adds both columns with NOT NULL + DEFAULT NOW() on created_at and a
nullable FK to users on created_by, matching the updated_by shape.
No backfill scaffolding (no live clients per release stance).
Closes AUDIT_TASKS.md §4.
Single-approve at /updates/:id/approve has always run the OSV.dev
ecosystem check for npm/PyPI packages, but the bulk endpoint
/updates/approve called BulkApproveUpdates directly — a single DB
write with no vulnerability lookup. So selecting N items in the UI
silently bypassed a check the README advertises.
ApproveUpdates now mirrors the single-approve loop: GetUpdateByID,
NeedsSupplyChainCheck, CheckOSVVulnerabilities, then
ApproveUpdateWithVulns when the OSV query returns CVEs (preserving
supply_chain_vulns / supply_chain_checked_at in metadata) or plain
ApproveUpdate when clean. Per-package warnings are aggregated and
returned to the caller. Fail-open semantics from the single-approve
path carry through — OSV unreachable does not block approval.
Also removed server/internal/api/handlers/update_handler.go.
UnifiedUpdateHandler was a parallel implementation of every
UpdateHandler method but NewUnifiedUpdateHandler was never called
from main.go or anywhere else. The file was confusing on grep and
masked which approve path was actually wired.
The legacy "updates" virtual subsystem was deprecated (scheduler.go:159
skips it), but five paths kept re-creating the row in agent_subsystems:
- agent migration detection flagged missing "updates" subsystem as a
missing security feature, prompting the executor to re-add it to the
local config on every startup migration
- server install-config template, scanner-timeout list, and intervals
map all kept "updates" alive in the config artifact sent to agents
Removed at all five sites. Scheduler skip logic, per-scanner mapping
helper (subsystems.go:236), and update-report data path remain — they
are not subsystem-row creators.
Historical migration 024_disable_updates_subsystem left intact.
Registration retries hit the (agent_id, subsystem) unique constraint,
which errored out the INSERT and poisoned the entire transaction. The
handler treated this as non-fatal, but PostgreSQL doesn't allow any
further statements in an aborted tx.
Adds ON CONFLICT DO NOTHING + treats sql.ErrNoRows as success.
Migration 033 adds the 'received' status to agent_commands so the server can
distinguish "agent confirmed receipt" from "sent but may be lost in flight."
Stuck-command re-issuance now excludes received commands — the TimeoutService
handles the longer timeout for those (default 30m) vs the per-poll re-issuer
(sent/pending at 5m).
The agent side: disk-persists executed command IDs to survive restart (closes
the in-memory-only dedup gap), reports received_command_ids on each check-in so
the server transitions sent→received before issuing new work, and authenticates
binary downloads with JWT+X-Machine-ID (was unauthenticated http.Get — would
401 in production).
TimeoutService extended with reconcileAgentUpdates: clears is_updating when
current_version matches updating_to_version (success), or after a 15m threshold
(timeout, with system_event) so the dashboard never shows "updating" forever.
isVersionUpgrade replaced with utils.IsNewerVersion (no panic on 2-part
versions, no false-reject on 4-part).
MarkCommand* failures elevated from [WARNING] to [ERROR] + should_retry
response hint so agents know to re-deliver results (silent drops were ETHOS #1
violations).
Fixes: build broken on public since eac8a012 (command.go accidentally emptied).