Files
mrhid6 84dfcfeac7 feat: vulnerability findings UI
Fleet board grouped by CVE, a per-server section on server detail, and
alert rules beside the channels they consume.

The server detail page has no tab pattern despite the plan saying to
follow one, so this adds a section in the existing vertical stack.

Three states are kept visually distinct because they are identical if
handled carelessly and only one is good news: never reported, no advisory
feed for the distribution, and scanned-and-clean. Database freshness sits
with the findings rather than in settings for the same reason.
2026-08-06 14:40:37 +01:00

1078 lines
30 KiB
TypeScript

export type ServerStatus = "pending" | "active" | "offline";
export type KeySource = "uploaded" | "generated";
export interface PackageUpdate {
name: string;
current_version?: string;
new_version: string;
}
export interface Inventory {
cpu: { model?: string; cores?: number; usage_pct: number; load1?: number };
memory: { total_bytes: number; used_bytes: number };
swap_total_bytes: number;
swap_used_bytes: number;
partitions?: { device: string; mountpoint: string; fstype?: string; total_bytes: number; used_bytes: number }[];
kernel?: string;
metrics_at?: string;
static_at?: string;
}
export interface Server {
id: string;
server_id: string;
hostname: string;
ip_address: string;
os_info: string;
status: ServerStatus;
agent_version?: string;
last_seen: string;
created_at: string;
available_updates?: PackageUpdate[];
updates_checked_at?: string;
console_protocols?: string[];
inventory?: Inventory;
tags?: Record<string, string>;
}
export type MonitorType = "http" | "tcp" | "icmp" | "tls";
export type MonitorStatus = "up" | "down" | "pending";
export interface MonitorTarget {
url?: string;
host?: string;
port?: number;
method?: string;
expected_status?: number;
keyword?: string;
tls_warn_days?: number;
insecure?: boolean;
}
export interface MonitorState {
status: MonitorStatus;
last_check_at?: string;
latency_ms: number;
message?: string;
cert_expiry_at?: string;
fails: number;
}
export interface Monitor {
monitor_id: string;
name: string;
type: MonitorType;
target: MonitorTarget;
interval_sec: number;
runner: string; // "server" or a server_id
retries: number;
enabled: boolean;
channel_ids?: string[];
state: MonitorState;
created_at: string;
}
export interface MonitorInput {
name: string;
type: MonitorType;
target: MonitorTarget;
interval_sec: number;
runner: string;
retries: number;
enabled: boolean;
channel_ids?: string[];
}
export interface Incident {
incident_id: string;
monitor_id: string;
started_at: string;
resolved_at?: string;
cause?: string;
}
export interface Rollup {
monitor_id: string;
period_start: string;
checks: number;
up_count: number;
sum_latency: number;
}
export type ChannelType = "webhook" | "smtp" | "discord" | "slack" | "telegram";
export interface NotificationChannel {
channel_id: string;
name: string;
type: ChannelType;
config: Record<string, string>;
enabled: boolean;
created_at: string;
}
export interface ChannelInput {
name: string;
type: ChannelType;
config: Record<string, string>;
enabled: boolean;
}
export interface ConsoleConnectRequest {
server_id: string;
protocol: string;
key_id?: string;
rdp_username?: string;
rdp_password?: string;
ssh_username?: string;
}
export interface ConsoleConnectResponse {
session_id: string;
token: string;
ws_path: string;
}
export interface Key {
id: string;
key_id: string;
label: string;
public_key: string;
fingerprint: string;
source: KeySource;
generated_by_server_id?: string;
has_private_key: boolean;
has_passphrase?: boolean;
created_at: string;
assigned_count?: number;
}
export interface Assignment {
id: string;
key_id: string;
server_id: string;
assigned_at: string;
revoked_at: string | null;
}
export interface AuditEvent {
id: string;
event_type: string;
actor: string;
server_id?: string;
key_id?: string;
details: string;
created_at: string;
}
export interface AlertSettings {
offline_threshold_minutes: number;
offline_channel_ids: string[] | null;
}
export interface SecretsSettings {
read_token_set: boolean;
rotated_at?: string;
}
export interface Settings {
alerts: AlertSettings;
secrets: SecretsSettings;
workflow_log_retention_days?: number | null;
local_login_enabled?: boolean;
}
export interface SecretGroupSummary {
group: string;
key_count: number;
updated_at: string;
}
export interface Secret {
group: string;
key: string;
updated_at: string;
}
export interface NewServerResponse {
server_id: string;
pre_reg_token: string;
install_command: string;
install_command_ps: string;
}
export interface GenerateKeyOptions {
label: string;
key_type: "ed25519" | "rsa" | "ecdsa";
key_size?: number;
passphrase?: string;
comment?: string;
}
export interface KeyWithAssignments extends Key {
assignments: (Assignment & { server: Server })[];
}
export interface ServerWithKeys extends Server {
keys: (Assignment & { key: Key })[];
}
export interface InputParam {
name: string;
default: string;
description: string;
}
export interface WorkflowStep {
step_id: string;
name: string;
description: string;
interpreter: "bash" | "powershell";
script: string;
declared_outputs: string[];
declared_inputs: InputParam[];
secret_refs: string[];
source?: "user" | "default";
slug?: string;
}
export interface WorkflowStepRef {
step_id?: string;
inline?: WorkflowStep;
order: number;
on_failure: "stop" | "continue" | "retry";
max_retries: number;
overrides?: { script?: string; secret_refs?: string[] };
inputs?: Record<string, string>;
}
export interface Workflow {
workflow_id: string;
name: string;
target_server_ids: string[];
target_tags?: Record<string, string>;
steps: WorkflowStepRef[];
schedule?: Schedule;
next_run_at?: string;
last_run_at?: string;
last_skipped?: Skip;
}
export interface StepRun {
order: number;
name: string;
status: string;
attempts: number;
exit_code: number;
log_offset: number;
output_env: Record<string, string>;
started_at?: string;
finished_at?: string;
}
export interface ServerRun {
server_id: string;
hostname: string;
status: string;
run_env: Record<string, string>;
steps: StepRun[];
started_at?: string;
finished_at?: string;
}
export interface WorkflowRun {
run_id: string;
workflow_id: string;
name: string;
status: string;
triggered_by: string;
started_at: string;
finished_at?: string;
server_runs: ServerRun[];
}
export type Role = "owner" | "admin" | "member";
/** The session as returned by GET /auth/me mirrors auth.Session on the server. */
export interface SessionUser {
user_id: string;
instance_id: string;
role: Role;
email: string;
name: string;
}
export interface Instance {
instance_id: string;
name: string;
slug: string;
created_at: string;
}
export interface MeResponse {
user: SessionUser;
instance: Instance | null;
}
export interface BootstrapStatus {
needs_setup: boolean;
instance_name?: string;
}
export interface BootstrapResponse {
instance: Instance;
slug: string;
instance_id: string;
}
export interface InstanceUser {
user_id: string;
instance_id: string;
email: string;
role: Role;
// "hq" means the row was projected from a Vantage HQ account. Its role,
// password and existence belong to HQ; this instance refuses to change them.
auth_source: "local" | "oidc" | "hq";
hq_user_id?: string;
created_at: string;
last_login?: string;
}
export interface OrgUserInput {
email: string;
password: string;
role: Role;
}
export interface PublicProvider {
id: string;
name: string;
preset: string;
}
export interface ProvidersResponse {
local_enabled: boolean;
providers: PublicProvider[];
}
export interface AuthProvider {
provider_id: string;
instance_id: string;
name: string;
kind: "oidc" | "oauth2";
preset: string;
issuer: string;
client_id: string;
scopes: string[];
enabled: boolean;
callback_notice: boolean;
order: number;
created_at: string;
updated_at: string;
client_secret_set: boolean;
callback_url: string;
}
export interface AuthPreset {
id: string;
label: string;
kind: "oidc" | "oauth2";
input_label: string;
input_hint: string;
}
export interface AuthProviderInput {
name: string;
preset: string;
issuer_input?: string;
client_id: string;
client_secret: string;
enabled: boolean;
}
export interface AuthProviderUpdate {
name?: string;
issuer_input?: string;
client_id?: string;
client_secret?: string;
enabled?: boolean;
order?: number;
}
class ApiError extends Error {
constructor(
public status: number,
message: string,
) {
super(message);
this.name = "ApiError";
}
}
async function request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`/api${path}`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
...options?.headers,
},
...options,
});
if (!res.ok) {
const text = await res.text().catch(() => "");
let message = text || res.statusText || `HTTP ${res.status}`;
try {
const body = JSON.parse(text);
if (body?.error) message = body.error;
} catch {}
throw new ApiError(res.status, message);
}
if (res.status === 204) {
return undefined as T;
}
return res.json();
}
async function authRequest<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(path, {
credentials: "include",
headers: { "Content-Type": "application/json", ...options?.headers },
...options,
});
if (!res.ok) {
let message = `HTTP ${res.status}`;
try {
const body = await res.json();
if (body?.error) message = body.error;
} catch {}
throw new ApiError(res.status, message);
}
if (res.status === 204) {
return undefined as T;
}
return res.json();
}
export const auth = {
bootstrapStatus(): Promise<BootstrapStatus> {
return authRequest<BootstrapStatus>("/auth/bootstrap-status");
},
bootstrap(input: { instance_name: string; email: string; password: string }): Promise<BootstrapResponse> {
return authRequest<BootstrapResponse>("/auth/bootstrap", {
method: "POST",
body: JSON.stringify(input),
});
},
login(email: string, password: string): Promise<{ ok: boolean }> {
return authRequest<{ ok: boolean }>("/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
});
},
logout(): Promise<void> {
return authRequest<void>("/auth/logout", { method: "POST" });
},
me(): Promise<MeResponse> {
return authRequest<MeResponse>("/auth/me");
},
/** Unauthenticated: what the login page draws itself from. */
providers(): Promise<ProvidersResponse> {
return authRequest<ProvidersResponse>("/auth/providers");
},
/** Where a provider button sends the browser. */
ssoStartUrl(providerId: string): string {
return `/auth/oidc/${providerId}/start`;
},
};
export const api = {
listInstanceUsers(): Promise<InstanceUser[]> {
return request<InstanceUser[]>("/instance/users");
},
createInstanceUser(input: OrgUserInput): Promise<InstanceUser> {
return request<InstanceUser>("/instance/users", { method: "POST", body: JSON.stringify(input) });
},
updateInstanceUserRole(userId: string, role: Role): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(`/instance/users/${userId}/role`, {
method: "PUT",
body: JSON.stringify({ role }),
});
},
deleteInstanceUser(userId: string): Promise<void> {
return request<void>(`/instance/users/${userId}`, { method: "DELETE" });
},
listAuthPresets(): Promise<AuthPreset[]> {
return request<AuthPreset[]>("/auth/presets");
},
listAuthProviders(): Promise<AuthProvider[]> {
return request<AuthProvider[]>("/auth/providers");
},
createAuthProvider(input: AuthProviderInput): Promise<AuthProvider> {
return request<AuthProvider>("/auth/providers", { method: "POST", body: JSON.stringify(input) });
},
updateAuthProvider(id: string, input: AuthProviderUpdate): Promise<{ saved: boolean }> {
return request<{ saved: boolean }>(`/auth/providers/${id}`, { method: "PUT", body: JSON.stringify(input) });
},
deleteAuthProvider(id: string): Promise<{ deleted: boolean }> {
return request<{ deleted: boolean }>(`/auth/providers/${id}`, { method: "DELETE" });
},
testAuthProvider(id: string): Promise<{ ok: boolean; message: string }> {
return request<{ ok: boolean; message: string }>(`/auth/providers/${id}/test`, { method: "POST" });
},
ackAuthProviderNotice(id: string): Promise<{ acknowledged: boolean }> {
return request<{ acknowledged: boolean }>(`/auth/providers/${id}/ack-notice`, { method: "POST" });
},
listServers(tags?: Record<string, string>): Promise<Server[]> {
const params = Object.entries(tags ?? {}).map(([k, v]) => `tag=${encodeURIComponent(`${k}:${v}`)}`);
return request<Server[]>(`/servers${params.length ? `?${params.join("&")}` : ""}`);
},
listKnownTags(): Promise<Record<string, string[]>> {
return request<Record<string, string[]>>("/servers/tags");
},
setServerTags(serverId: string, tags: Record<string, string>): Promise<{ tags: Record<string, string> }> {
return request<{ tags: Record<string, string> }>(`/servers/${serverId}/tags`, {
method: "PUT",
body: JSON.stringify({ tags }),
});
},
getServer(serverId: string): Promise<ServerWithKeys> {
return request<ServerWithKeys>(`/servers/${serverId}`);
},
createServer(): Promise<NewServerResponse> {
return request<NewServerResponse>("/servers/new", { method: "POST" });
},
deleteServer(serverId: string): Promise<void> {
return request<void>(`/servers/${serverId}`, { method: "DELETE" });
},
generateKeyForServer(serverId: string, opts: GenerateKeyOptions): Promise<{ command_id: string }> {
return request<{ command_id: string }>(`/servers/${serverId}/generate-key`, {
method: "POST",
body: JSON.stringify(opts),
});
},
getUpdateCommand(osInfo?: string): string {
if (osInfo && osInfo.toLowerCase().includes("windows")) {
return `irm "${window.location.origin}/update.ps1" | iex`;
}
return `curl -fsSL "${window.location.origin}/update" | bash`;
},
listMonitors(): Promise<Monitor[]> {
return request<Monitor[]>("/monitors");
},
getMonitor(monitorId: string): Promise<Monitor> {
return request<Monitor>(`/monitors/${monitorId}`);
},
createMonitor(input: MonitorInput): Promise<Monitor> {
return request<Monitor>("/monitors", { method: "POST", body: JSON.stringify(input) });
},
updateMonitor(monitorId: string, input: Partial<MonitorInput>): Promise<void> {
return request<void>(`/monitors/${monitorId}`, { method: "PUT", body: JSON.stringify(input) });
},
deleteMonitor(monitorId: string): Promise<void> {
return request<void>(`/monitors/${monitorId}`, { method: "DELETE" });
},
getMonitorIncidents(monitorId: string): Promise<Incident[]> {
return request<Incident[]>(`/monitors/${monitorId}/incidents`);
},
getMonitorUptime(monitorId: string): Promise<Rollup[]> {
return request<Rollup[]>(`/monitors/${monitorId}/uptime`);
},
listChannels(): Promise<NotificationChannel[]> {
return request<NotificationChannel[]>("/channels");
},
createChannel(input: ChannelInput): Promise<NotificationChannel> {
return request<NotificationChannel>("/channels", { method: "POST", body: JSON.stringify(input) });
},
updateChannel(channelId: string, input: Partial<ChannelInput>): Promise<void> {
return request<void>(`/channels/${channelId}`, { method: "PUT", body: JSON.stringify(input) });
},
deleteChannel(channelId: string): Promise<void> {
return request<void>(`/channels/${channelId}`, { method: "DELETE" });
},
testChannel(channelId: string): Promise<{ status: string }> {
return request<{ status: string }>(`/channels/${channelId}/test`, { method: "POST" });
},
getLatestAgentVersion(): Promise<{ version: string }> {
return request<{ version: string }>("/agent/latest-version");
},
updateAgent(serverId: string): Promise<{ message: string; version: string }> {
return request<{ message: string; version: string }>(`/servers/${serverId}/update-agent`, {
method: "POST",
});
},
applyUpdates(serverId: string): Promise<{ message: string }> {
return request<{ message: string }>(`/servers/${serverId}/apply-updates`, {
method: "POST",
});
},
listAuditEvents(limit?: number): Promise<AuditEvent[]> {
const qs = limit ? `?limit=${limit}` : "";
return request<AuditEvent[]>(`/audit${qs}`);
},
getSettings(): Promise<Settings> {
return request<Settings>("/settings");
},
saveSettings(settings: { alerts: AlertSettings; workflow_log_retention_days?: number | null; local_login_enabled?: boolean }): Promise<{ saved: boolean }> {
return request<{ saved: boolean }>("/settings", {
method: "PUT",
body: JSON.stringify(settings),
});
},
rotateSecretsToken(): Promise<{ token: string }> {
return request<{ token: string }>("/settings/secrets-token", { method: "POST" });
},
listSecretGroups(): Promise<SecretGroupSummary[]> {
return request<SecretGroupSummary[]>("/secrets");
},
createSecretGroup(group: string, values: Record<string, string>): Promise<{ group: string }> {
return request<{ group: string }>("/secrets", {
method: "POST",
body: JSON.stringify({ group, values }),
});
},
getSecretGroup(group: string): Promise<{ group: string; secrets: Secret[] }> {
return request<{ group: string; secrets: Secret[] }>(`/secrets/${encodeURIComponent(group)}`);
},
putSecrets(group: string, values: Record<string, string>): Promise<{ saved: boolean }> {
return request<{ saved: boolean }>(`/secrets/${encodeURIComponent(group)}`, {
method: "PUT",
body: JSON.stringify(values),
});
},
revealSecret(group: string, key: string): Promise<{ value: string }> {
return request<{ value: string }>(`/secrets/${encodeURIComponent(group)}/reveal`, {
method: "POST",
body: JSON.stringify({ key }),
});
},
deleteSecret(group: string, key: string): Promise<void> {
return request<void>(`/secrets/${encodeURIComponent(group)}/${encodeURIComponent(key)}`, {
method: "DELETE",
});
},
deleteSecretGroup(group: string): Promise<void> {
return request<void>(`/secrets/${encodeURIComponent(group)}`, { method: "DELETE" });
},
listKeys(): Promise<Key[]> {
return request<Key[]>("/keys");
},
getKey(keyId: string): Promise<KeyWithAssignments> {
return request<KeyWithAssignments>(`/keys/${keyId}`);
},
uploadKey(label: string, public_key: string, private_key?: string, passphrase?: string): Promise<Key> {
return request<Key>("/keys", {
method: "POST",
body: JSON.stringify({
label,
public_key,
private_key: private_key || undefined,
passphrase: passphrase || undefined,
}),
});
},
getPrivateKey(keyId: string): Promise<{ private_key: string }> {
return request<{ private_key: string }>(`/keys/${keyId}/private-key`);
},
deleteKey(keyId: string): Promise<void> {
return request<void>(`/keys/${keyId}`, { method: "DELETE" });
},
assignKey(keyId: string, serverId: string): Promise<Assignment> {
return request<Assignment>(`/keys/${keyId}/assign`, {
method: "POST",
body: JSON.stringify({ server_id: serverId }),
});
},
revokeKey(keyId: string, serverId: string): Promise<void> {
return request<void>(`/keys/${keyId}/assign/${serverId}`, {
method: "DELETE",
});
},
connectConsole(body: ConsoleConnectRequest): Promise<ConsoleConnectResponse> {
return request<ConsoleConnectResponse>("/console/connect", {
method: "POST",
body: JSON.stringify(body),
});
},
listSteps(): Promise<WorkflowStep[]> {
return request<WorkflowStep[]>("/steps");
},
createStep(s: Partial<WorkflowStep>): Promise<WorkflowStep> {
return request<WorkflowStep>("/steps", {
method: "POST",
body: JSON.stringify(s),
});
},
updateStep(stepId: string, s: Partial<WorkflowStep>): Promise<WorkflowStep> {
return request<WorkflowStep>(`/steps/${stepId}`, {
method: "PUT",
body: JSON.stringify(s),
});
},
deleteStep(stepId: string): Promise<void> {
return request<void>(`/steps/${stepId}`, { method: "DELETE" });
},
exportStepUrl(stepId: string): string {
return `/api/steps/${stepId}/export`;
},
importStep(doc: unknown): Promise<WorkflowStep> {
return request<WorkflowStep>("/steps/import", {
method: "POST",
body: JSON.stringify(doc),
});
},
parseStep(doc: unknown): Promise<WorkflowStep> {
return request<WorkflowStep>("/steps/parse", {
method: "POST",
body: JSON.stringify(doc),
});
},
seedDefaults(): Promise<{ created: number; updated: number }> {
return request<{ created: number; updated: number }>("/steps/seed-defaults", {
method: "POST",
});
},
stepUsage(): Promise<Record<string, number>> {
return request<Record<string, number>>("/steps/usage");
},
listWorkflows(): Promise<Workflow[]> {
return request<Workflow[]>("/workflows");
},
getWorkflow(workflowId: string): Promise<Workflow> {
return request<Workflow>(`/workflows/${workflowId}`);
},
createWorkflow(w: Partial<Workflow>): Promise<Workflow> {
return request<Workflow>("/workflows", {
method: "POST",
body: JSON.stringify(w),
});
},
updateWorkflow(workflowId: string, w: Partial<Workflow>): Promise<Workflow> {
return request<Workflow>(`/workflows/${workflowId}`, {
method: "PUT",
body: JSON.stringify(w),
});
},
deleteWorkflow(workflowId: string): Promise<void> {
return request<void>(`/workflows/${workflowId}`, { method: "DELETE" });
},
runWorkflow(workflowId: string): Promise<{ run_id: string }> {
return request<{ run_id: string }>(`/workflows/${workflowId}/run`, {
method: "POST",
});
},
listRuns(workflowId: string): Promise<WorkflowRun[]> {
return request<WorkflowRun[]>(`/workflows/${workflowId}/runs`);
},
getRun(runId: string): Promise<WorkflowRun> {
return request<WorkflowRun>(`/runs/${runId}`);
},
cancelRun(runId: string): Promise<void> {
return request<void>(`/runs/${runId}/cancel`, { method: "POST" });
},
async getServerRunLog(runId: string, serverId: string): Promise<string> {
const res = await fetch(`/api/runs/${runId}/servers/${serverId}/logs`, {
credentials: "include",
});
if (!res.ok) throw new Error("no logs");
return res.text();
},
serverRunLogStreamUrl(runId: string, serverId: string): string {
return `/api/runs/${runId}/servers/${serverId}/logs/stream`;
},
setWorkflowSchedule(workflowId: string, schedule: Schedule): Promise<{ schedule: Schedule; next_run_at: string | null }> {
return request(`/workflows/${workflowId}/schedule`, { method: "PUT", body: JSON.stringify(schedule) });
},
previewSchedule(workflowId: string, cron: string, tz: string): Promise<{ occurrences: string[] }> {
return request(`/workflows/${workflowId}/schedule/preview?cron=${encodeURIComponent(cron)}&tz=${encodeURIComponent(tz)}`);
},
};
export type LicenseState = "valid" | "expired" | "invalid";
export interface LicenseInfo {
instance_id: string;
state: LicenseState;
reason?: string;
tier?: string;
support_level?: string;
expires_at?: string;
days_remaining?: number;
limits: {
max_servers: number;
max_monitors: number;
max_secret_groups: number;
max_channels: number;
audit_retention_days: number;
};
features: Record<string, boolean>;
usage: {
servers: number;
monitors: number;
secret_groups: number;
channels: number;
};
source: string;
/** "cloud" | "self_hosted". A cloud instance's licence is managed in HQ. */
deployment: string;
}
export type Severity = "critical" | "high" | "medium" | "low" | "unknown";
export type FindingState = "open" | "fixed" | "accepted";
export interface Acceptance {
by: string;
reason: string;
until: string;
at: string;
}
export interface VulnFinding {
id: string;
server_id: string;
cve_id: string;
package_name: string;
installed_version: string;
/** Absent means no vendor fix is published — a real state, not missing data. */
fixed_in?: string;
severity: Severity;
cvss_score?: number;
title?: string;
references?: string[];
state: FindingState;
first_seen: string;
last_seen: string;
fixed_at?: string;
accepted?: Acceptance;
}
/** One CVE across every server it affects. The board groups by CVE because the
* same CVE on forty servers is one decision, not forty rows. */
export interface VulnGroup {
cve_id: string;
severity: Severity;
title?: string;
server_count: number;
findings: VulnFinding[];
}
export interface VulnSummary {
counts: Partial<Record<Severity, number>>;
db_version?: number;
pulled_at?: string;
last_full_scan_at?: string;
last_error?: string;
}
export interface InstalledPackage {
name: string;
version: string;
epoch?: number;
arch: string;
source_name?: string;
}
export interface ServerPackages {
server_id: string;
os: { family: string; version_id: string; arch: string };
hash: string;
packages: InstalledPackage[];
collected_at: string;
scan_pending: boolean;
scanned_at?: string;
/** "ok" | "unsupported". Unsupported must never read as "clean". */
status: string;
db_version: number;
}
export interface PackageHit {
server_id: string;
name: string;
version: string;
}
export interface VulnAlertRule {
id: string;
name: string;
enabled: boolean;
min_severity: Severity;
tags?: Record<string, string>;
channel_ids: string[];
created_at: string;
updated_at: string;
}
export interface VulnAlertRuleInput {
name: string;
enabled: boolean;
min_severity: Severity;
tags?: Record<string, string>;
channel_ids: string[];
}
export const vulnerabilities = {
list(params?: { severity?: string; state?: string; server?: string; tags?: Record<string, string> }): Promise<VulnGroup[]> {
const q = new URLSearchParams();
if (params?.severity) q.set("severity", params.severity);
if (params?.state) q.set("state", params.state);
if (params?.server) q.set("server", params.server);
for (const [k, v] of Object.entries(params?.tags ?? {})) q.append("tag", `${k}:${v}`);
const qs = q.toString();
return request<VulnGroup[]>(`/vulnerabilities${qs ? `?${qs}` : ""}`);
},
summary(): Promise<VulnSummary> {
return request<VulnSummary>("/vulnerabilities/summary");
},
rescan(): Promise<{ queued: number }> {
return request<{ queued: number }>("/vulnerabilities/rescan", { method: "POST" });
},
accept(id: string, reason: string, until: string): Promise<VulnFinding> {
return request<VulnFinding>(`/vulnerabilities/${id}/accept`, {
method: "POST",
body: JSON.stringify({ reason, until }),
});
},
unaccept(id: string): Promise<VulnFinding> {
return request<VulnFinding>(`/vulnerabilities/${id}/accept`, { method: "DELETE" });
},
forServer(serverId: string): Promise<VulnFinding[]> {
return request<VulnFinding[]>(`/servers/${serverId}/vulnerabilities`);
},
packagesForServer(serverId: string): Promise<ServerPackages | { reported: false }> {
return request<ServerPackages | { reported: false }>(`/servers/${serverId}/packages`);
},
searchPackages(name: string): Promise<PackageHit[]> {
return request<PackageHit[]>(`/packages/search?name=${encodeURIComponent(name)}`);
},
listRules(): Promise<VulnAlertRule[]> {
return request<VulnAlertRule[]>("/vuln-rules");
},
createRule(input: VulnAlertRuleInput): Promise<VulnAlertRule> {
return request<VulnAlertRule>("/vuln-rules", { method: "POST", body: JSON.stringify(input) });
},
updateRule(id: string, input: VulnAlertRuleInput): Promise<{ status: string }> {
return request<{ status: string }>(`/vuln-rules/${id}`, { method: "PUT", body: JSON.stringify(input) });
},
deleteRule(id: string): Promise<{ status: string }> {
return request<{ status: string }>(`/vuln-rules/${id}`, { method: "DELETE" });
},
};
// `request` already prefixes /api, so these paths do not repeat it.
export const licence = {
get(): Promise<LicenseInfo> {
return request<LicenseInfo>("/license");
},
put(blob: string): Promise<{ state: LicenseState; tier: string; expires_at?: string }> {
return request("/license", { method: "POST", body: JSON.stringify({ blob }) });
},
};
export interface Schedule {
enabled: boolean;
cron: string;
tz: string;
}
export interface Skip {
reason: string;
due: string;
at: string;
}