Commit Graph
105 Commits
Author SHA1 Message Date
mrhid6 82bcc5776f fix: Fixed vuln scanning
Chart Release / chart (push) Successful in 15s
Server Deploy / deploy (push) Successful in 4m7s
2026-08-07 11:13:58 +01:00
mrhid6 5db49b6b0e feat: Vuln debug logs
Chart Release / chart (push) Successful in 11s
Server Deploy / deploy (push) Successful in 1m47s
2026-08-07 10:50:08 +01:00
mrhid6 4ff8fc8d51 docs: document the workload registry
Chart Release / chart (push) Successful in 11s
Server Deploy / deploy (push) Successful in 8m45s
Agent Release / build (push) Successful in 10m54s
Agent Release / msi (push) Successful in 2m31s
2026-08-07 09:09:18 +01:00
mrhid6 fd4c51f3db feat: workload registry REST API 2026-08-07 09:01:46 +01:00
mrhid6 cf9d85b3cd feat: store workload reports and route log results 2026-08-07 08:56:26 +01:00
mrhid6 0838d1d735 feat: models and indexes for the workload registry 2026-08-07 08:45:00 +01:00
mrhid6 f60c509b47 feat: vuln_scanning entitlement and documentation
Chart Release / chart (push) Successful in 12s
Server Deploy / deploy (push) Failing after 1m11s
Agent Release / build (push) Successful in 1m0s
Agent Release / msi (push) Successful in 2m18s
Adds license.FeatureVulnScanning as the one name for the feature and a
catalogue row per deployment/tier, following console and oidc: features
are opt-in per customer, so no plan bundles it.

Documents the subsystem in CLAUDE.md, including that ScopedCollections is
the canonical registry instance deletion derives from — there is no
separate deletion list, which the plan had wrong.
2026-08-06 14:44:11 +01:00
mrhid6 5dda3b5c4a feat: vulnerability scanning pipeline, matcher, scheduler and API
Completes tasks 10-15 and fixes what was outstanding:

- vulndb.Pull implemented with oras-go, streaming the ~50MB layer and
  staging both files before replacing either, so a failed pull leaves the
  previous database intact rather than a half-written one.
- db.go: Vulnerability.Severity is a string, not trivy Severity, so the
  int conversion did not compile. Severity now resolves vendor (highest
  when vendors disagree) then NVD then unknown, and CVSS is read too.
- findings.go: added sweepFixedFindings plus the fleet query, severity
  counts, rescan flag and accept/unaccept the API needs.
- vulnrules.go: added rule CRUD and the digest builder. ResolveTargets
  returns []models.Server, not []string, so filterByServers was wrong.
- api/vulnerabilities.go was an empty file while handlers.go registered
  twelve routes against it; written, grouped by CVE.
- shared/mail: added the missing sender. The templates were orphaned and
  the HTML one was a copy of the text one, defining "subject" (which
  html/template would escape) and emitting no markup. render.go parses
  every template in init(), so a bad one panics server, admin and sitesvc
  at boot — go build never runs init(), which is why nothing complained.
- notify: digests dispatch through their own path so SMTP gets the digest
  template rather than arriving dressed as a monitor alert.
2026-08-06 14:33:46 +01:00
mrhid6 583f60771c feat: store agent package reports and serve the collect flag
VulnScanningEnabled reads GetLicenseState(...).Feature("vuln_scanning")
and requires an active licence, never switching on tier. ReportPackages
re-checks it server-side: the agent flag is the optimisation, this is
the boundary.
2026-08-06 13:19:39 +01:00
mrhid6 3a6d24fe0e feat: models and indexes for package inventory and CVE findings
Adds server_packages, vuln_findings and vuln_alert_rules to
ScopedCollections rather than to a separate deletion list. purgeInstance
derives its collection list from that registry, so instance deletion
follows automatically and there is no second copy to drift.
2026-08-06 11:59:00 +01:00
mrhid6 d0e1cc4ad6 feat: cron arithmetic and persisted workflow schedules 2026-08-04 13:51:10 +01:00
mrhid6 d1b3cd2f74 feat: target workflow runs by tag selector 2026-08-04 13:36:43 +01:00
mrhid6 fef0b7c7a1 feat: read and write server tags, resolve targets from the database 2026-08-04 13:33:00 +01:00
mrhid6 efd29dc259 feat: parse tag filters and resolve targets as ids union tag selector 2026-08-04 13:31:04 +01:00
mrhid6 13cd41d202 feat: validate server tags and add the model field 2026-08-04 13:30:06 +01:00
mrhid6 1f08e90009 feat: Removed email alert settings
Chart Release / chart (push) Successful in 10s
Server Deploy / deploy (push) Successful in 5m15s
2026-08-03 14:40:15 +01:00
mrhid6 537b8758ff fix: purge auth_providers when reaping an instance
auth_providers was missing from ScopedCollections, so reap.go's
scopedCollectionsForPurge() (derived from that list) never deleted an
instance's providers, leaving orphaned rows holding encrypted client
secrets forever. Verified migration 0004's $rename over org_id->instance_id
is a no-op here since auth_providers never carried org_id.
2026-08-03 14:11:42 +01:00
mrhid6 c03360333b fix: single source of truth for local-login lockout rescue
HandleLocalLogin and HandleListPublicProviders each computed their own
answer to whether password sign-in must stay available, and they could
disagree: an instance with local login off and a licence that lapses
loses its only provider and its password form in the same moment, with
no endpoint left to recover. services.LocalLoginPermitted is now the
one predicate both call.
2026-08-03 14:11:22 +01:00
mrhid6 dde47de145 feat: auth provider REST API and public provider discovery 2026-08-03 10:51:35 +01:00
mrhid6 e22faebfcd feat: migrate instance_oidc into auth_providers (0005) 2026-08-03 10:38:52 +01:00
mrhid6 e2b01b62a5 feat: add local_login_enabled setting with absent-means-on default 2026-08-03 10:36:48 +01:00
mrhid6 0858693d57 feat: add auth provider service layer and lockout guard 2026-08-03 10:33:36 +01:00
mrhid6 8699dc5b7e fix: Renew presence on sub/pub
Chart Release / chart (push) Successful in 18s
Server Deploy / deploy (push) Successful in 56s
2026-07-31 16:58:50 +01:00
mrhid6 f611cae438 feat: Better debugging for console
Chart Release / chart (push) Successful in 11s
Server Deploy / deploy (push) Successful in 1m22s
2026-07-31 16:00:58 +01:00
mrhid6 6f86496f10 fix: Ffixes to console
Chart Release / chart (push) Successful in 18s
Server Deploy / deploy (push) Successful in 1m9s
2026-07-31 15:05:21 +01:00
mrhid6 165114471f fix: Fixes to running on kubernetes
Chart Release / chart (push) Failing after 13s
Server Deploy / deploy (push) Successful in 6m35s
2026-07-31 10:34:10 +01:00
mrhid6 c0bec3737b feat: route every console session through the agent relay 2026-07-29 13:07:30 +01:00
mrhid6 20a302f84a feat: add OpenConsoleProxy service facade 2026-07-29 12:55:36 +01:00
mrhid6 bc6c7cdb5a feat: Updated docker file
Server Deploy / deploy (push) Successful in 2m35s
2026-07-28 16:04:51 +01:00
mrhid6 31e0000306 feat: default steps are read only
Server Deploy / deploy (push) Successful in 1m46s
2026-07-28 13:34:08 +01:00
mrhid6 dabe6fe3aa feat: Updated package path to match repo
Agent Release / build (push) Failing after 25s
Agent Release / msi (push) Skipped
Server Deploy / deploy (push) Successful in 2m47s
2026-07-28 10:01:40 +01:00
mrhid6andClaude Opus 5 c4e6ad5485 feat(license): metered licensing — catalogue, entitlements, and enforcement
Server Deploy / deploy (push) Successful in 5m22s
Implements spec 7 tasks 2-10 on top of the six-plan payload from task 1.

Admin: plans re-keyed on (deployment, tier); new catalogue collection holds
every Paddle price ID (one row per priceable component); new entitlements
collection holds desired beside granted. admin/internal/catalogue owns both
folds — entitlement to licence limits, and entitlement to Paddle line items —
so the base allowance is subtracted in exactly one place. licensing.Issue now
snapshots the instance's granted entitlement, never desired. Free is enforced
per account AND deployment. Staff endpoints for plans, catalogue and
entitlements; Free self-hosted can be claimed and renewed on its annual term;
the reaper stays cloud-only.

Server: enforces the monitor cap, audit-log retention (daily sweep, skips
Unlimited and lapsed instances), and gates the OIDC callback. Unset limits are
filled from the seed plan at the single decode site so old blobs never read as
zero.

Frontends: adminsite gains a catalogue price-ID editor, six-plan allowance
screen, and a catalogue-driven PlanConfigurator mounted on the staff instance
page. web shows monitors, audit retention and support level on the licence page.

Docs: CLAUDE.md, spec index and plan 5 preamble updated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 09:37:39 +01:00
mrhid6andClaude Opus 5 a05a74cf4d feat(server): refuse local edits to hq-sourced users
The API is the boundary; hiding the control in web/ is a courtesy. A role
editable in two places is a role with two answers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:31:27 +01:00
mrhid6andClaude Opus 5 4e90e8619f feat(server): reap Free instances whose licence lapsed
The control plane owns deletion because it is the only service that knows
what an instance is made of; mirroring that collection list into admin
would drift, and a drift here deletes the wrong rows.

Defaults OFF. Eligibility is three positive assertions — Free tier, an
expiry that exists, and an expiry past the window — so a missing or stale
field is never eligible. The instance document is deleted last, making an
interrupted purge retryable rather than orphaning rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:23:37 +01:00
mrhid6andClaude Opus 5 d703bbc4e8 feat(server): scope every user lookup by instance
users is unique on (instance_id, email) now, so an unscoped FindOne could
return an arbitrary one of several matching users. On the login path that
means signing someone into a tenant that is not theirs.

GetUserByEmail is deleted rather than left unused. Local sign-in resolves
its instance from the host, falling back to the single instance a
self-hosted deployment has, and refuses to guess otherwise. The OIDC
cross-instance guard goes: a scoped lookup cannot return another
instance's user, which is a stronger guarantee than the check it replaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:54:13 +01:00
mrhid6 8e5f35f40c revert(server): drop the licence grandfather migration
Instances stay read-only until a licence is set. No licence is generated by
the control plane, which keeps the signing key out of it entirely.
2026-07-24 15:49:46 +01:00
mrhid6 09a39090c6 feat(server): grandfather existing cloud instances onto Professional 2026-07-24 15:26:26 +01:00
mrhid6 8d88b16f20 feat(server): keep monitors and in-flight runs going when a licence lapses
Monitor execution and the workflow runner are deliberately unguarded: billing
state must not take away a customer's ability to know their infrastructure is
on fire, and killing a run midway leaves a half-configured server.

The plan called for a server-limit check in gRPC Register. Left out: a server
row only comes from CreateServer, which already checks the cap, so counting in
Register counts the caller itself and would reject a legitimate agent at
exactly the cap.
2026-07-24 15:15:06 +01:00
mrhid6 855537c535 feat(server): enforce licence limits on servers, secret groups and channels 2026-07-24 15:11:03 +01:00
mrhid6 1d3fcebb28 feat(server): resolve licence state per instance 2026-07-24 15:09:47 +01:00
mrhid6 e4d7569a1c refactor(server): correct stale org wording in messages 2026-07-24 15:07:54 +01:00
mrhid6 c3e363eccc refactor(server): finish the Org to Instance rename
Private identifiers plan 0b's naming map missed, plus the OrgOIDC model type.
No wire format, database field or route changes.
2026-07-24 15:07:06 +01:00
mrhid6 539403cccf fix(server): drop stale indexes before renaming the tenant key
Two defects found by running migration 0004 against a seeded legacy database.

A unique index on org_id treats a missing org_id as null. Renaming the field
strips it, so the second document collided and the whole update failed:

  E11000 duplicate key error collection: instance_oidc index: org_id_1
  dup key: { org_id: null }

The index cleanup therefore has to run BEFORE the field rename, not after.
rename-rollback needs the symmetric step for instance_id, or reverting hits
the same wall.

The detection also silently matched nothing: the driver decodes an index key
document as bson.D, not bson.M, so the type assertion always failed and no
index was ever dropped. IndexKeyedOn now handles both.
2026-07-24 14:15:05 +01:00
mrhid6 4f041d2f4b refactor(server): rename Org to Instance
Adds migration 0004_org_to_instance, the ScopedCollections list, the
AssertNoScopedCollectionMissed boot check, and moves EnsureAuthIndexes into
its own file.

Two ordering constraints the rename exposed, both now enforced and commented:

- 0004 must run BEFORE EnsureAuthIndexes. The index builder creates
  instances.slug, which would create an empty instances collection and make
  0004 refuse to rename orgs onto an existing target.
- Migrations 0001 to 0003 run BEFORE 0004 and still read and write org_id, so
  they use a private legacyOrg struct rather than shared/models.
2026-07-24 13:58:41 +01:00
mrhid6 76d3111a72 refactor(server): use shared models, provision and indexes
Also removes the duplicate Slugify in stepscan.go; workflow step slugs now
use the shared definition too.
2026-07-24 13:42:44 +01:00
mrhid6 e798365be2 feat: Removed comments
Server Deploy / deploy (push) Failing after 1m13s
2026-07-24 09:56:54 +01:00
mrhid6 1a6cf03c03 feat: Removed comments
Server Deploy / deploy (push) Failing after 1m59s
2026-07-24 09:51:30 +01:00
mrhid6 0b7e55d301 fix(server): adopt the migrated org on bootstrap; survive fresh install
Two failures found by tracing the migration path against a real upgrade.

Bootstrap orphaned the entire dataset. On upgrade, 0001 creates the
Default org and stamps every legacy document with it, but the instance
has no users, so the operator must run /auth/bootstrap to get in — and
that unconditionally created a SECOND org and put the owner in it. Every
org-scoped read then filtered on the new org, so the operator would log
into an empty Vantage while all their data sat under "default". Nothing
errored and agents kept syncing, so it presented as total data loss.
Bootstrap now adopts the sole existing org, renaming and re-slugging it,
and only creates one when no org exists. More than one org with no users
is refused rather than guessed.

Fresh installs crash-looped. Nothing creates the settings collection
before EnsureSettingsIndexes, so DropOne returned NamespaceNotFound (26),
isIndexNotFound matched only IndexNotFound (27), and that check is fatal.
The same early return also skipped index creation in the secrets and
workflow ensures.

Also: only insert the backfill org on ErrNoDocuments, so a transient read
error can't race the fatal unique slug index; run 0002 before 0003 so the
settings migration can't be pushed into its ambiguous branch; fail 0002's
ambiguous case with a remedy instead of continuing into a fatal index
build; and skip non-string ids in the owner backfill rather than aborting.
2026-07-22 10:51:42 +01:00
mrhid6 77a92787fb fix: repair migration collection names and cross-cutting scoping gaps
Findings from the final whole-branch review.

- scopedCollections named "audit" and "channels", but the code writes to
  audit_logs and notification_channels. On upgrade from single-tenant,
  legacy audit events and channels would never get org_id, becoming
  invisible to org-filtered reads while channels silently stopped firing
  — and the detection loop counted the wrong names, so the 0001 marker
  could be written having migrated nothing. Names fixed, plus migration
  0003 so an incorrectly-migrated instance converges with a fresh one.
- EnsureAuthIndexes failure is now fatal. GetUserByEmail is unscoped and
  the OIDC cross-org guard compares against whichever duplicate Mongo
  returns first, so users.email uniqueness is a security invariant, and a
  legacy collection with duplicate emails is the realistic upgrade case.
- Evict the per-org OIDC provider cache on save; rotating away from a
  compromised IdP previously had no effect until restart.
- Build the oauth2 config per request instead of mutating a shared cached
  pointer outside the mutex, which raced on RedirectURL between
  concurrent logins for the same org.
- Stamp org_id on console_sessions, incidents and monitor_rollups, the
  last collections with no tenant column. 0003 derives their org from the
  owning server/monitor rather than defaulting, so one org's console
  history and incident timeline cannot merge into another's.
- Seed default steps when an org is created, not only at boot.
- Reject an empty session OrgID at the middleware.
- Derive the app root label from APP_ROOT_LABEL instead of hardcoding
  "vantage", which silently disabled the host guard off that domain.
- Stop caching negative slug lookups, so a new org's subdomain resolves
  immediately.
2026-07-22 10:44:17 +01:00
mrhid6 aa31cd8a10 fix: validate roles, guard last owner, scope bootstrap status
Security review of e70b2f0. The UI gating was correctly backed by
RequireRole everywhere; these are the missing validation gaps behind it.

- UpdateUserRole and createOrgUser accepted any role string verbatim, so
  an admin could self-promote to owner, create an owner outright, or set
  a junk role that silently stripped a user's access. Roles are now
  whitelisted, only an owner may grant or remove the owner role, and an
  actor cannot change their own.
- Neither demote nor delete guarded the last owner, so an org could reach
  zero owners. Both now refuse when no owner would remain, returning 409.
  Self-delete rejected.
- CountUsers counted across all orgs, so a locked-out org could never
  re-bootstrap once another tenant existed, and the unauthenticated
  bootstrap-status endpoint reported instance-wide state. It now answers
  per-org on an org host, falling back to global only on the apex.
- HandleMe repeats the middleware's host/org check; it sits outside the
  middleware so it can still return its own 401.
- Post-bootstrap now sends the new owner to their org host's login page.
  The session cookie is deliberately scoped to the exact host, so the old
  redirect landed them unauthenticated.
- AuthProvider renders an error state instead of mounting the shell with
  a null user when /auth/me fails for a reason other than 401.
- api.ts unwraps {"error": ...} so these messages render as text.
2026-07-22 10:26:21 +01:00