Compare commits
4
Commits
9ba3d4a61f
...
5cee53dc5f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cee53dc5f | ||
|
|
81248bb159 | ||
|
|
6354d54de8 | ||
|
|
da6d64f95c |
@@ -147,7 +147,7 @@ with `"schedule"` as the actor, so there is no second dispatch path and the run
|
||||
detail page needed no changes.
|
||||
|
||||
`main.go` imports `_ "time/tzdata"`, and it is load-bearing: `server/Dockerfile`
|
||||
builds on Alpine, which ships no zone database, so without it
|
||||
runs on `scratch`, which ships no zone database, so without it
|
||||
`time.LoadLocation("Europe/London")` fails and every schedule silently falls
|
||||
back to UTC — an hour wrong for half the year, in the direction nobody notices
|
||||
until a maintenance window lands in business hours. It works on a developer
|
||||
@@ -350,6 +350,12 @@ is where risk goes to be forgotten. An unsupported distribution reports
|
||||
unknown is the same lie as a silently stale database, which is why
|
||||
`vulndb_meta.pulled_at` is on screen rather than only in a log.
|
||||
|
||||
**`server/Dockerfile`'s runtime stage is `scratch`, so it carries an explicitly
|
||||
copied `/tmp`.** The scheduler unpacks the database to a temporary directory,
|
||||
and a scratch image has none — the failure is `vulnsched: temp dir: stat /tmp:
|
||||
no such file or directory`, logged once at boot while every other subsystem
|
||||
runs normally, so the only symptom is a fleet that never reports a finding.
|
||||
|
||||
Two environment variables: `VANTAGE_TRIVY_DB_REF` mirrors the artifact for
|
||||
air-gapped installs, and `VANTAGE_VULNDB_DISABLED` switches the puller and
|
||||
scheduler off entirely.
|
||||
|
||||
@@ -18,10 +18,19 @@ ARG VERSION=dev
|
||||
RUN cd server && CGO_ENABLED=0 GOOS=linux go build \
|
||||
-ldflags="-s -w -X main.Version=${VERSION}" -o /vantage-server ./cmd
|
||||
|
||||
# Staged so the scratch image below can have a /tmp. It cannot mkdir one
|
||||
# itself — scratch has no shell — and os.MkdirTemp fails outright without it.
|
||||
RUN mkdir -p /staging/tmp && chmod 1777 /staging/tmp
|
||||
|
||||
# Runtime stage
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
# vulndb unpacks the ~50MB trivy-db here. Without it the scheduler stops at
|
||||
# "temp dir: stat /tmp: no such file or directory" and no scanning happens,
|
||||
# while everything else in the process runs perfectly well.
|
||||
COPY --from=builder /staging/tmp /tmp
|
||||
COPY --from=builder /vantage-server /vantage-server
|
||||
|
||||
COPY default_steps/ /opt/default-steps/
|
||||
|
||||
+12
-19
@@ -40,10 +40,7 @@ export default function PricingPage() {
|
||||
>
|
||||
Pick a tier, then pay per server.
|
||||
</h1>
|
||||
<p className="lede">
|
||||
You pay for the number of servers you manage, and nothing else. Adding people costs nothing, and neither does adding keys, scripts, uptime checks or stored passwords. Every tier costs
|
||||
the same whether we host it or you do.
|
||||
</p>
|
||||
<p className="lede">You pay for the number of servers you manage, and nothing else. Adding people costs nothing, and neither does adding keys, scripts, uptime checks or stored passwords. Every tier costs the same whether we host it or you do.</p>
|
||||
|
||||
<div className="plans">
|
||||
<div className="plan">
|
||||
@@ -126,6 +123,13 @@ export default function PricingPage() {
|
||||
<p>Connect your own OIDC provider to an instance. £90 a year.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">£7 / MO</span>
|
||||
<div>
|
||||
<h3>Vulnerability Scanning</h3>
|
||||
<p>Agents will scan and report known Vulnerabilites. £70 a year.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="scroll__hint">Scroll the table sideways for all three tiers</p>
|
||||
@@ -180,39 +184,28 @@ export default function PricingPage() {
|
||||
<span className="spec__k">LIMITS</span>
|
||||
<div>
|
||||
<h3>Reaching your limit</h3>
|
||||
<p>
|
||||
Nothing gets deleted. A server past the limit still checks in and still shows up in your list, but access changes stop reaching it until you raise the limit or remove
|
||||
one.
|
||||
</p>
|
||||
<p>Nothing gets deleted. A server past the limit still checks in and still shows up in your list, but access changes stop reaching it until you raise the limit or remove one.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">CANCELLING</span>
|
||||
<div>
|
||||
<h3>What happens when you stop paying</h3>
|
||||
<p>
|
||||
You keep everything until the end of the period you paid for. After that it becomes read-only: your uptime checks carry on running, your alerts still arrive and your
|
||||
servers keep the access they have. You just can't change anything until you renew.
|
||||
</p>
|
||||
<p>You keep everything until the end of the period you paid for. After that it becomes read-only: your uptime checks carry on running, your alerts still arrive and your servers keep the access they have. You just can't change anything until you renew.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">SELF-HOSTED</span>
|
||||
<div>
|
||||
<h3>Why self-hosted is yearly only</h3>
|
||||
<p>
|
||||
When you host it yourself, your licence is a file your installation checks on its own. It never contacts us, which is the point but it also means we can't switch
|
||||
one off partway through, so we sell it a year at a time.
|
||||
</p>
|
||||
<p>When you host it yourself, your licence is a file your installation checks on its own. It never contacts us, which is the point but it also means we can't switch one off partway through, so we sell it a year at a time.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="spec">
|
||||
<span className="spec__k">EXIT</span>
|
||||
<div>
|
||||
<h3>Leaving</h3>
|
||||
<p>
|
||||
You can export your servers, keys, scripts and stored passwords at any time. Your servers keep the access they already have, so nobody gets locked out while you move.
|
||||
</p>
|
||||
<p>You can export your servers, keys, scripts and stored passwords at any time. Your servers keep the access they already have, so nobody gets locked out while you move.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { api, vulnerabilities, type FindingState, type Severity, type VulnFinding } from "@/lib/api";
|
||||
import { useAuth } from "@/components/AuthProvider";
|
||||
import { Button } from "@/components/ui";
|
||||
import { Button, Card } from "@/components/ui";
|
||||
import { AcceptDialog } from "@/components/vulnerabilities/AcceptDialog";
|
||||
import { DBFreshness } from "@/components/vulnerabilities/DBFreshness";
|
||||
import { FindingRow } from "@/components/vulnerabilities/FindingRow";
|
||||
@@ -78,12 +78,12 @@ export default function VulnerabilitiesPage() {
|
||||
const total = SEVERITY_ORDER.reduce((n, s) => n + (counts[s] ?? 0), 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="p-4 sm:p-6 lg:p-8">
|
||||
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-text-primary">Vulnerabilities</h1>
|
||||
<h1 className="text-2xl font-bold text-text-primary">Vulnerabilities</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">
|
||||
Installed packages matched against distribution security advisories.
|
||||
{total} open finding{total !== 1 ? "s" : ""} · installed packages matched against distribution security advisories
|
||||
</p>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
@@ -93,32 +93,36 @@ export default function VulnerabilitiesPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DBFreshness summary={summary.data} />
|
||||
<div className="mb-4">
|
||||
<DBFreshness summary={summary.data} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 rounded-lg border border-border bg-surface px-5 py-4">
|
||||
{/* Severity counts double as the filter. They are always the whole
|
||||
fleet's open counts, never the filtered view's, so switching
|
||||
state cannot make the fleet look better than it is. */}
|
||||
<div className="mb-4 flex flex-wrap gap-2 rounded-lg border border-border bg-surface px-4 py-3 sm:px-5">
|
||||
{SEVERITY_ORDER.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setSeverity(severity === s ? "" : s)}
|
||||
className={`flex items-center gap-2 rounded px-2 py-1 text-left transition-colors ${
|
||||
severity === s ? "bg-surface-2" : "hover:bg-surface-2"
|
||||
aria-pressed={severity === s}
|
||||
className={`flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-left transition-colors ${
|
||||
severity === s ? "border-accent bg-surface-2" : "border-transparent hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
<SeverityBadge severity={s} />
|
||||
<span className="font-mono text-lg font-semibold tabular-nums text-text-primary">{counts[s] ?? 0}</span>
|
||||
</button>
|
||||
))}
|
||||
<span className="ml-auto self-center font-mono text-[10px] uppercase tracking-[0.16em] text-text-tertiary">
|
||||
{total} open
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="mb-4 flex gap-2">
|
||||
{STATES.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setState(s)}
|
||||
className={`rounded border px-3 py-1.5 text-sm capitalize transition-colors ${
|
||||
aria-pressed={state === s}
|
||||
className={`rounded-lg border px-3 py-1.5 text-sm capitalize transition-colors ${
|
||||
state === s ? "border-accent text-accent" : "border-border text-text-secondary hover:text-text-primary"
|
||||
}`}
|
||||
>
|
||||
@@ -127,33 +131,42 @@ export default function VulnerabilitiesPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{groups.isLoading && <p className="text-sm text-text-secondary">Loading…</p>}
|
||||
{groups.error && <p className="text-sm text-danger">{(groups.error as Error).message}</p>}
|
||||
|
||||
{groups.data && groups.data.length === 0 && (
|
||||
<div className="rounded-lg border border-border bg-surface px-5 py-8 text-center">
|
||||
<p className="text-sm text-text-secondary">No {state} findings.</p>
|
||||
<p className="mt-1 text-xs text-text-tertiary">
|
||||
Servers report packages hourly. A server whose distribution has no advisory feed is reported as unsupported on its own
|
||||
page rather than counted here.
|
||||
</p>
|
||||
{groups.error && (
|
||||
<div className="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">
|
||||
{(groups.error as Error).message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{groups.data?.map((g) => (
|
||||
<FindingRow
|
||||
key={g.cve_id}
|
||||
group={g}
|
||||
serverName={serverName}
|
||||
canAct={isAdmin}
|
||||
onAccept={setAccepting}
|
||||
onUnaccept={(f) => unaccept.mutate(f.id)}
|
||||
onApplyUpdates={(serverId) => applyUpdates.mutate(serverId)}
|
||||
applying={applyUpdates.isPending ? (applyUpdates.variables as string) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Card padding={false}>
|
||||
{groups.isLoading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-accent" />
|
||||
</div>
|
||||
) : groups.data && groups.data.length > 0 ? (
|
||||
groups.data.map((g) => (
|
||||
<FindingRow
|
||||
key={g.cve_id}
|
||||
group={g}
|
||||
serverName={serverName}
|
||||
canAct={isAdmin}
|
||||
onAccept={setAccepting}
|
||||
onUnaccept={(f) => unaccept.mutate(f.id)}
|
||||
onApplyUpdates={(serverId) => applyUpdates.mutate(serverId)}
|
||||
applying={applyUpdates.isPending ? (applyUpdates.variables as string) : undefined}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="px-6 py-14 text-center">
|
||||
<p className="text-[15px] font-semibold text-text-primary">
|
||||
No {state} findings{severity ? ` at ${severity} severity` : ""}.
|
||||
</p>
|
||||
<p className="mx-auto mt-2 max-w-[52ch] text-sm text-text-secondary">
|
||||
Servers report their packages hourly. A server whose distribution has no advisory feed is reported as unsupported
|
||||
on its own page rather than counted as clean here.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{accepting && (
|
||||
<AcceptDialog
|
||||
|
||||
@@ -32,26 +32,32 @@ export function FindingRow({ group, serverName, canAct, onAccept, onUnaccept, on
|
||||
const anyFix = group.findings.some((f) => f.fixed_in);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-surface">
|
||||
// A row inside the page's one bordered container, not a card of its
|
||||
// own — the same stack idiom as the monitors and workflows lists.
|
||||
<div className="border-t border-border-soft first:border-t-0">
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-surface-2"
|
||||
className="flex w-full items-center gap-3 px-4 py-3.5 text-left transition-colors hover:bg-surface-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent sm:px-5"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span className="font-mono text-xs text-text-tertiary">{open ? "▾" : "▸"}</span>
|
||||
<SeverityBadge severity={group.severity} />
|
||||
<span className="font-mono text-sm font-medium text-text-primary">{group.cve_id}</span>
|
||||
{group.title && <span className="hidden truncate text-sm text-text-secondary sm:block">{group.title}</span>}
|
||||
<span className="ml-auto whitespace-nowrap text-xs text-text-secondary">
|
||||
<span className="ml-auto whitespace-nowrap font-mono text-[11px] text-text-tertiary">
|
||||
{group.server_count} {group.server_count === 1 ? "server" : "servers"}
|
||||
</span>
|
||||
{!anyFix && <span className="whitespace-nowrap text-xs text-text-tertiary">no fix published</span>}
|
||||
{!anyFix && (
|
||||
<span className="hidden whitespace-nowrap rounded-sm border border-border px-1.5 font-mono text-[10px] uppercase tracking-[0.1em] text-text-tertiary sm:block">
|
||||
no fix
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="border-t border-border">
|
||||
<div className="border-t border-border-soft bg-surface-2/40">
|
||||
{group.findings.map((f) => (
|
||||
<div key={f.id} className="flex flex-wrap items-center gap-x-4 gap-y-2 border-b border-border-soft px-4 py-3 last:border-b-0">
|
||||
<div key={f.id} className="flex flex-wrap items-center gap-x-4 gap-y-2 border-b border-border-soft px-4 py-3 last:border-b-0 sm:px-5">
|
||||
<Link href={`/servers/${f.server_id}`} className="text-sm text-accent hover:underline">
|
||||
{serverName(f.server_id)}
|
||||
</Link>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function ServerVulnerabilities({ serverId }: { serverId: string }) {
|
||||
<h2 className="text-lg font-semibold text-text-primary">Vulnerabilities</h2>
|
||||
<Link href="/vulnerabilities">
|
||||
<Button variant="ghost" size="sm">
|
||||
Fleet board →
|
||||
View All
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -52,18 +52,13 @@ export function ServerVulnerabilities({ serverId }: { serverId: string }) {
|
||||
{packages.isLoading || findings.isLoading ? (
|
||||
<p className="text-sm text-text-secondary">Loading…</p>
|
||||
) : !pkg ? (
|
||||
<p className="text-sm text-text-secondary">
|
||||
No package inventory yet. Agents report hourly, and only when vulnerability scanning is included in this instance's
|
||||
licence.
|
||||
</p>
|
||||
<p className="text-sm text-text-secondary">No package inventory yet. Agents report hourly, and only when vulnerability scanning is included in this instance's licence.</p>
|
||||
) : pkg.status === "unsupported" ? (
|
||||
<>
|
||||
<p className="text-sm text-warning">
|
||||
{pkg.os.family} {pkg.os.version_id} has no advisory feed, so this server cannot be scanned.
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-text-tertiary">
|
||||
This is not the same as having no vulnerabilities — it means we cannot answer the question for this distribution.
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-text-tertiary">This is not the same as having no vulnerabilities — it means we cannot answer the question for this distribution.</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -94,15 +89,11 @@ export function ServerVulnerabilities({ serverId }: { serverId: string }) {
|
||||
<span className="font-mono text-xs text-text-secondary">
|
||||
{f.package_name} {f.installed_version}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-text-tertiary">
|
||||
{f.fixed_in ? `→ ${f.fixed_in}` : "no fix published"}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-text-tertiary">{f.fixed_in ? `→ ${f.fixed_in}` : "no fix published"}</span>
|
||||
<StateBadge state={f.state} />
|
||||
</li>
|
||||
))}
|
||||
{open.length > 10 && (
|
||||
<li className="pt-1 text-xs text-text-tertiary">…and {open.length - 10} more on the fleet board.</li>
|
||||
)}
|
||||
{open.length > 10 && <li className="pt-1 text-xs text-text-tertiary">…and {open.length - 10} more on the fleet board.</li>}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user