Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5204,6 +5204,14 @@ components:
properties:
scan_id: {type: string, format: uuid}
host_id: {type: string, format: uuid}
# Human-friendly host label resolved from the hosts table for the
# scan-detail header (so it shows a hostname/IP, not a raw UUID).
# Populated on the detail endpoint (GET /scans/{id}); the list
# endpoint leaves them absent since the caller already has host
# context. hostname is the registered hostname (may be empty);
# ip_address is always present for a real host. Spec api-scans.
hostname: {type: string}
ip_address: {type: string}
status: {type: string, description: 'queued | running | completed | failed'}
trigger_source: {type: string, description: 'on_demand | scheduled'}
queued_at: {type: string, format: date-time}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/api/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3479,6 +3479,8 @@ export interface components {
scan_id: string;
/** Format: uuid */
host_id: string;
hostname?: string;
ip_address?: string;
/** @description queued | running | completed | failed */
status: string;
/** @description on_demand | scheduled */
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/pages/scans/ScanDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ export function ScanDetailPage() {
>
<Meta label="Host">
<Link to="/hosts/$hostId" params={{ hostId: scan.host_id }} style={{ color: 'var(--ow-link)', textDecoration: 'none' }}>
{scan.host_id.slice(0, 8)}
{/* Human-friendly label: hostname if registered, else IP, else
a short UUID as a last resort (api-scans resolves these). */}
{scan.hostname || scan.ip_address || scan.host_id.slice(0, 8)}
</Link>
</Meta>
<Meta label="Status">{scan.status}</Meta>
Expand Down
12 changes: 12 additions & 0 deletions frontend/tests/pages/scan-detail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,16 @@ describe('frontend-scan-detail', () => {
// No em-dash in copy.
expect(stripComments(PAGE_SRC)).not.toContain('—');
});

// @ac AC-08
test('frontend-scan-detail/AC-08 — Host field shows hostname || ip || short uuid, not a bare uuid', () => {
// The Host Meta uses the hostname-then-IP-then-short-UUID fallback.
expect(PAGE_SRC).toMatch(
/scan\.hostname \|\| scan\.ip_address \|\| scan\.host_id\.slice\(0, 8\)/,
);
// And it is NOT the old bare-UUID render (slice as the sole child).
expect(PAGE_SRC).not.toMatch(/>\s*\{scan\.host_id\.slice\(0, 8\)\}\s*</);
// Still a Link to the host detail page.
expect(PAGE_SRC).toContain('to="/hosts/$hostId"');
});
});
29 changes: 25 additions & 4 deletions internal/scanresult/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ func NewReader(pool *pgxpool.Pool) *Reader { return &Reader{pool: pool} }
// ScanSummary is the scan_runs metadata shown in the list and the scan
// detail header.
type ScanSummary struct {
ScanID uuid.UUID
HostID uuid.UUID
ScanID uuid.UUID
HostID uuid.UUID
// Hostname + IPAddress are the human-friendly host label, resolved
// from the hosts table by GetScan for the detail header. ListByHost
// leaves them empty (the list caller already has host context).
Hostname string
IPAddress string
Status string
TriggerSource string
QueuedAt time.Time
Expand Down Expand Up @@ -155,7 +160,11 @@ func (rd *Reader) ListByHost(ctx context.Context, hostID uuid.UUID, limit int, c
return out, next, nil
}

// GetScan returns a scan's metadata, or ErrScanNotFound.
// GetScan returns a scan's metadata, or ErrScanNotFound. It also resolves
// the host's hostname + ip_address from the hosts table so the detail
// header can show a human-friendly label instead of a raw UUID. A missing
// host row (the FK is ON DELETE RESTRICT, so this is unexpected) leaves the
// label fields empty rather than failing the scan read.
func (rd *Reader) GetScan(ctx context.Context, scanID uuid.UUID) (ScanSummary, error) {
run, err := scanruns.Get(ctx, rd.pool, scanID)
if errors.Is(err, scanruns.ErrNotFound) {
Expand All @@ -164,7 +173,19 @@ func (rd *Reader) GetScan(ctx context.Context, scanID uuid.UUID) (ScanSummary, e
if err != nil {
return ScanSummary{}, err
}
return summaryFromRun(run), nil
s := summaryFromRun(run)
var hostname, ip string
// host(ip_address) renders the inet as plain text (no /netmask), matching
// how internal/host formats it; COALESCE keeps a NULL hostname/IP as "".
err = rd.pool.QueryRow(ctx,
`SELECT COALESCE(hostname, ''), COALESCE(host(ip_address), '') FROM hosts WHERE id = $1`,
s.HostID).Scan(&hostname, &ip)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return ScanSummary{}, fmt.Errorf("scanresult: resolve host label: %w", err)
}
s.Hostname = hostname
s.IPAddress = ip
return s, nil
}

// ScanResults returns every rule's verdict for a scan, ordered by
Expand Down
Loading
Loading