<img height="1" width="1" style="display:none;" alt="" src="https://px.ads.linkedin.com/collect/?pid=7178634&amp;fmt=gif">
uControl Book a demo
Docs  ›  Discovery

API Reference

Last updated 2026-05-28

API Reference

uControl Insight provides a REST API for programmatic access to asset data. The API is available at /api/v1/ relative to the application context path.

Authentication

The API supports three authentication methods:

MethodHeaderExample
Integration Key (master)Authorization: Bearer ucm_... or X-Integration-Api-Key: ucm_...Service-to-service integration, full access
User API TokenAuthorization: Bearer uci_...Per-user tokens, inherits user roles
Session CookieStandard browser sessionUsed by the web UI

Generating Tokens

  • Integration Keys — sidebar → Integration Keys (ADMINISTRATOR only)
  • User API Tokens — Profile page (requires API_USER role)

Response Format

All API responses use a standard envelope:

{
  "data": [ ... ],
  "meta": {
    "totalCount": 42,
    "page": 0,
    "size": 50
  }
}

Endpoints

Assets

MethodEndpointDescription
GET/api/v1/assetsSearch and list assets
GET/api/v1/assets/{id}Get asset detail
GET/api/v1/assets/{id}/relationshipsGet asset relationships

Asset Search Parameters

ParameterTypeDescription
qstringSearch by hostname, FQDN, or display name
typestringFilter by asset type. Supports comma-separated OR: type=VM,PHYSICAL_HOST
categorystringFilter by coarse asset category (Type). Supports comma-separated OR: category=HOST,STORAGE
statusstringFilter by status. Supports comma-separated OR: status=ACTIVE,STALE
sitestringFilter by site. Supports comma-separated OR
environmentstringFilter by environment. Supports comma-separated OR
pageintPage number (0-based, default: 0)
sizeintPage size (default: 50)

Asset Response Fields

Each asset carries both a coarse category (the "Type" shown in the UI) and a fine-grained sub type (the historical assetType value), plus a virtual flag for host-like assets:

FieldTypeDescription
assetTypestringFine-grained type (e.g. VM, PHYSICAL_HOST, DATASTORE). Unchanged — retained for backwards compatibility.
assetCategorystringCoarse category (e.g. HOST, STORAGE, CLOUD). See Asset Categories below.
assetCategoryLabelstringHuman label for the category, e.g. HOST → "Host".
assetSubtypestringSub type — currently mirrors assetType.
assetSubtypeLabelstringHuman label for the sub type, e.g. PHYSICAL_HOST → "Physical Host".
virtualboolean / nulltrue for VMs and containers, false for physical hosts / hypervisors / servers, null for non-host categories.

The same fields appear in the CSV / JSON exports (/api/v1/export/assets.csv, /api/v1/export/assets.json): columns assetCategory, assetSubtype, virtual were added alongside assetType.

Example Requests

# List all VMs and physical hosts
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://server/uControlInsight/api/v1/assets?type=VM,PHYSICAL_HOST"

# List every Host-category asset (physical, VM, hypervisor, container, server)
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://server/uControlInsight/api/v1/assets?category=HOST"

# Search by name
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://server/uControlInsight/api/v1/assets?q=webserver&type=VM"

# Get asset detail
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://server/uControlInsight/api/v1/assets/abc123-def456"

Topology

MethodEndpointDescription
GET/api/v1/topology/{assetId}/neighborsGet topology graph for an asset

Query parameter: depth (1-5, default 2) — number of relationship hops to traverse.

Discovery Runs

MethodEndpointDescription
GET/api/v1/discovery/runsList recent discovery runs
POST/api/v1/discovery/targets/{id}/scanTrigger a discovery scan
GET/api/v1/runs/{id}/diagnosisAI diagnosis of a run (status: OK | ERROR | NONE | DISABLED | NOT_APPLICABLE)
POST/api/v1/runs/{id}/diagnosisGenerate / refresh the AI diagnosis for a FAILED / COMPLETED_WITH_ERRORS run

AI diagnosis (requires ucontrol.ai.enabled=true on the server) returns a short explanation of why a scan had errors / failed, with likely causes and recommended actions:

{
  "data": {
    "runId": "…",
    "status": "OK",
    "summary": "SSH authentication fails on 10.0.2.0/24 — no configured credential matches that subnet.",
    "likelyCauses": ["The only SSH credential has match pattern 10.0.1.* which does not cover 10.0.2.*"],
    "recommendedActions": ["Add an SSH credential matching 10.0.2.*", "Re-run the scan and check the per-IP scan summary"],
    "confidence": "HIGH",
    "model": "claude-haiku-4-5",
    "createdAt": "2026-05-12T19:40:00"
  }
}

When AI is disabled, status is DISABLED; for a run that did not fail it is NOT_APPLICABLE; if no diagnosis has been generated yet it is NONE (POST to generate). The diagnosis is also generated automatically when a scan finishes with errors, and is shown on the Discovery Runs page and included in the Microsoft Teams alert.

Credentials

MethodEndpointDescription
GET/api/v1/credentialsList credentials (metadata only, no secrets)
POST/api/v1/credentialsCreate a credential
PUT/api/v1/credentials/{id}Update a credential
DELETE/api/v1/credentials/{id}Delete a credential

Asset Categories & Types

Valid values for the category filter (coarse "Type") and the fine-grained types (assetType / sub type) they group:

Category (category)LabelFine types (assetType)
HOSTHostPHYSICAL_HOST, HYPERVISOR, VM, CONTAINER, SERVER
NETWORK_DEVICENetwork DeviceNETWORK_DEVICE
PRINTERPrinterPRINTER
IOT_DEVICEIoT DeviceIOT_DEVICE
VIRTUALIZATION_MANAGERVirtualization ManagerVIRTUALIZATION_MANAGER
CLUSTERClusterCLUSTER
STORAGEStorageDATASTORE, STORAGE_POOL
NETWORKNetworkNETWORK, VSWITCH, PORT_GROUP, INTERFACE
SOFTWARESoftwareSOFTWARE, SERVICE, SOFTWARE_PACKAGE
CLOUDCloudCLOUD_ACCOUNT, CLOUD_REGION, CLOUD_DATABASE, CLOUD_STORAGE, CLOUD_LOAD_BALANCER, CLOUD_STACK
FLOW_EXPORTERFlow ExporterFLOW_EXPORTER
ENDPOINTEndpointUNKNOWN_ENDPOINT

The type filter still accepts any of the fine-grained values above.

Swagger UI

Interactive API documentation is available at /swagger-ui.html relative to the application context path.

Asset Lifecycle Actions (May 2026)

Administrator endpoints for the asset-lifecycle workflow. Both require DISCOVERY_ADMIN or ADMINISTRATOR.

MethodEndpointDescription
POST/api/v1/assets/{id}/reactivate Flip a STALE / INACTIVE / DECOMMISSIONED asset back to ACTIVE. Resets the consecutive-failure counter and clears stale_since.
POST/api/v1/assets/{id}/decommission Mark the asset DECOMMISSIONED (end of life). The daily aging sweeper never touches DECOMMISSIONED assets.

See the Asset Aging & Lifecycle page for the full lifecycle model.

Asset Response Fields (May 2026 additions)

FieldTypeDescription
consecutiveScanFailuresinteger Number of consecutive failed discovery runs targeting this asset (0 after any successful scan).
lastScanOutcomestring SUCCESS / FAILED / PARTIAL — result of the most recent run that targeted this asset.
lastScanAttimestampWhen that run completed.

Pagination & navigation links (May 2026)

List endpoints with page and size query parameters now return absolute navigation URLs in a links object so client integrations don't have to construct the next URL by hand. The links preserve every other query parameter the caller sent (filters, sort, custom size) so following next just works.

The change is additive — existing clients that ignore links continue to work.

Shape

{
  "data": [ ... ],
  "total": 446,
  "page": 0,
  "pageSize": 50,
  "links": {
    "first": "https://insight.example/api/v1/assets?status=ACTIVE&size=50&page=0",
    "next":  "https://insight.example/api/v1/assets?status=ACTIVE&size=50&page=1",
    "last":  "https://insight.example/api/v1/assets?status=ACTIVE&size=50&page=8"
  }
}

Each link is independently optional:

  • page 0 → no prev
  • last page → no next (consumer signal to stop following)
  • single page → only first (with first == last)

Endpoints that emit full nav (first / prev / next / last)

MethodEndpoint
GET/api/v1/assets
GET/api/v1/discovery/runs

Endpoints that emit only first (top-N, no offset)

These return up to a limit capped result set and have no “next page” concept — they always show only first in the links block:

  • /api/v1/flow/top-talkers
  • /api/v1/flow/conversations
  • /api/v1/insights/… (AhaAi insight feeds)

Suggested integration pattern

response = http.get("/api/v1/assets?status=ACTIVE&size=200")
while True:
    process(response.data)
    next_url = response.links.get("next")
    if not next_url:
        break
    response = http.get(next_url)

Client doesn't need to track page manually or recompute URLs — just follow next until it's absent.

Bulk relationships (May 2026)

Avoids the per-asset N+1 pattern when an importer needs the full edge list. Each row carries both endpoints' display names + types so the importer doesn't need a second lookup against /api/v1/assets just to humanise the IDs.

MethodEndpointNotes
GET/api/v1/relationships Paginated edge list with optional filters. Query params:
  • type — comma-separated relationship types (RUNS_ON, HOSTS_VM, CONTAINS, COMMUNICATES_WITH, …)
  • sourceCategory / targetCategory — comma-separated asset categories (HOST, SOFTWARE, …)
  • activeOnly — default true
  • page — default 0
  • size — default 200, capped at 1000
Returns rows of {id, relationshipType, active, sourceAssetId, sourceDisplayName, sourceAssetType, sourceAssetCategory, targetAssetId, …}. Follows the same links.next pagination contract as /api/v1/assets.

Software → Host export — one call:

GET /api/v1/relationships?type=RUNS_ON&size=500

Returns every SOFTWARE→HOST edge in a single page (for any reasonable estate).

Computed root parent on every asset (May 2026)

Every /api/v1/assets response (list and detail) now includes a derived parent block when the asset is a child of another asset — so a 3rd-party importer can read the host id off a software row directly, without joining via /api/v1/relationships at all.

Shape:

{
  "id": "abc-123",
  "assetType": "SOFTWARE_PACKAGE",
  "displayName": "nginx",
  "assetCategory": "SOFTWARE",
  …
  "parent": {
    "id": "host-456",
    "displayName": "web01",
    "assetType": "SERVER",
    "assetCategory": "HOST",
    "via": "RUNS_ON"
  }
}

Root assets (PHYSICAL_HOST, NETWORK_DEVICE, PRINTER, CLOUD_ACCOUNT, VIRTUALIZATION_MANAGER, CLUSTER, FLOW_EXPORTER, UNKNOWN_ENDPOINT) have no parent field — the JSON omits it entirely (@JsonInclude(NON_NULL)).

Resolution rules (focal asset type → parent):

Focal asset typeEdge typeDirectionParent category
VMHOSTS_VMincomingHYPERVISOR
CONTAINERHOSTS_CONTAINERincomingHOST
HYPERVISORMANAGED_BYoutgoingVIRTUALIZATION_MANAGER
SOFTWARE_PACKAGE / SOFTWARE / SERVICERUNS_ONoutgoingHOST
DATASTORE / STORAGE_POOLUSES_DATASTORE (or CONTAINS)incomingHOST / HYPERVISOR
VSWITCH / PORT_GROUP / NETWORK / INTERFACECONTAINS (or HAS_INTERFACE)incomingHOST / HYPERVISOR
CLOUD_REGION / CLOUD_DATABASE / CLOUD_STORAGE / CLOUD_LOAD_BALANCER / CLOUD_STACKCONTAINSincomingCLOUD_ACCOUNT / CLOUD_REGION

Performance: bulk-resolved per page. A page of N assets always costs exactly 2 extra SQL queries (one for the edges, one for the parent asset rows) regardless of N — no N+1.

Schema introspection — available attributes per asset type (May 2026)

Importers can fetch the list of fields and observed customAttributes keys for every asset type without trial-and-error against the data. Two endpoints:

  • GET /api/v1/schema/asset-types — schema for every asset type
  • GET /api/v1/schema/asset-types/{type} — schema for one asset type (e.g. /api/v1/schema/asset-types/SOFTWARE_PACKAGE)

Each entry contains:

FieldDescription
type / category / label Identifiers and human-readable labels for the asset type and its category.
isRoot True for types that are never children (PHYSICAL_HOST, NETWORK_DEVICE, etc.) and so have no parent block on individual assets.
virtual / cloud Tri-state flags driven by the type taxonomy.
parent Resolution rule used to compute the per-asset parent block: edge type, direction (OUT/IN), and the typical parent category.
typeFields Ordered list of { name, type, description } entries: the common fields followed by the fields typically populated for this asset type.
customAttributeKeys Distinct top-level keys observed in custom_attributes for this asset type in the current database (data-driven, not a static list).
childTypes Types commonly discovered downstream — e.g. HYPERVISORVM, CONTAINER, DATASTORE, VSWITCH, PORT_GROUP.

Example fragment:

{
  "type": "SOFTWARE_PACKAGE",
  "category": "SOFTWARE",
  "label": "Software Package",
  "isRoot": false,
  "parent": { "via": "RUNS_ON", "direction": "OUT", "parentCategory": "HOST" },
  "typeFields": [
    { "name": "id",          "type": "string",  "description": "Stable UUID asset identifier" },
    { "name": "displayName", "type": "string",  "description": "Human-friendly label" },
    { "name": "vendor",      "type": "string?", "description": "Software vendor / publisher" },
    "..."
  ],
  "customAttributeKeys": ["install_date", "package_manager", "version"]
}

Use it when: building or maintaining a 3rd-party importer, generating ORM models from the API, or auditing which connector-specific keys are present in your environment (the keys list reflects your current dataset).

Filter by category: add ?category=HOST (or SOFTWARE, NETWORK, STORAGE, CLOUD, …) to scope the response to a single category. Case-insensitive. Unknown values return HTTP 400.

GET /api/v1/schema/asset-types?category=HOST
GET /api/v1/schema/asset-types?category=SOFTWARE

Bulk Export Endpoints

For exporting data into BI tools, ITSM CMDBs, spreadsheets, or for backup, the platform exposes paginated, streaming export endpoints. All responses include a Content-Disposition attachment header so browsers download rather than render. CSV fields follow RFC 4180 quoting; JSON is a single array streamed record-by-record so memory stays bounded for large estates.

EndpointFormatReturns
GET /api/v1/export/assets.csvCSVAll asset records (one row per asset)
GET /api/v1/export/assets.jsonJSONSame data as a JSON array
GET /api/v1/export/software.csvCSVInstalled software packages, joined to parent asset
GET /api/v1/export/software.jsonJSONSame as JSON
GET /api/v1/export/processes.csvCSVRunning processes, joined to parent asset
GET /api/v1/export/processes.jsonJSONSame as JSON

Installed Software

Each row carries assetId, assetDisplayName and assetHostname so the parent asset can be looked up without a second API call. Sorted by asset name, then package name, for stable, diffable output.

FieldTypeNotes
assetIdstring (UUID)Parent asset id — joins to /api/v1/assets/{id}
assetDisplayNamestringAsset's display name (hostname, FQDN, or fallback)
assetHostnamestring?Hostname when known
packageNamestringe.g. openssh-server, Microsoft Office 365
versionstring?e.g. 8.9p1-3, 16.0.17029.20132
publisherstring?Software vendor (Microsoft, Adobe, Canonical, etc.)
installDatedate?ISO-8601 date (YYYY-MM-DD) when available
packageManagerstring?dpkg, rpm, snap, brew, msi, winget etc.

Filters (all optional, combinable):

  • ?assetId={uuid} — only rows for that asset
  • ?publisher={name} — exact match, e.g. ?publisher=Microsoft
  • ?packageManager={name} — e.g. ?packageManager=dpkg
# Full inventory (CSV)
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://insight.example.com/uControlInsight/api/v1/export/software.csv" \
  -o software.csv

# Just one asset (JSON)
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://insight.example.com/uControlInsight/api/v1/export/software.json?assetId=3eadf3af-6683-4a6a-aa9e-ea4eff6e755a"

# Everything Microsoft-published, CSV
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://insight.example.com/uControlInsight/api/v1/export/software.csv?publisher=Microsoft"

Example CSV output (header + two rows):

assetId,assetDisplayName,assetHostname,packageName,version,publisher,installDate,packageManager
3eadf3af-6683-4a6a-aa9e-ea4eff6e755a,esx01.example.com,esx01,openssh-server,8.9p1-3,Canonical,2024-11-12,dpkg
3eadf3af-6683-4a6a-aa9e-ea4eff6e755a,esx01.example.com,esx01,nginx,1.24.0-2ubuntu7.1,Canonical,2024-11-12,dpkg

Running Processes

Snapshot of processes captured at the most recent connectorised scan of each asset. Same parent-linking convention. Sorted by asset name, then PID.

FieldTypeNotes
assetIdstring (UUID)Parent asset
assetDisplayNamestringAsset display name
assetHostnamestring?Hostname when known
pidintegerProcess id at the time of the scan
ppidinteger?Parent process id
processNamestringExecutable name (e.g. nginx, java)
commandLinestring?Full argv, truncated to fit storage column
usernamestring?Owning user (root, SYSTEM, etc.)
statestring?OS-reported run state (Linux R/S/D, Windows Running etc.)
memoryKbinteger?Resident memory in KB
cpuPercentdecimal?CPU % at sample time
sourceConnectorstring?Connector that captured this row (SSH, WINRM, etc.)
softwareAssetIdstring (UUID)?If the process was matched to a SOFTWARE-category asset (cross-link), that asset's id

Filters (all optional, combinable):

  • ?assetId={uuid} — only rows for that asset
  • ?username={name} — exact match, e.g. ?username=root
# Every process on every asset, JSON
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://insight.example.com/uControlInsight/api/v1/export/processes.json"

# Just root processes on one host, CSV
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://insight.example.com/uControlInsight/api/v1/export/processes.csv?assetId=3eadf3af-6683-4a6a-aa9e-ea4eff6e755a&username=root"

Performance & pagination

All four endpoints stream results in batches of 500 rows over the response body — memory stays bounded regardless of estate size. For estates with hundreds of thousands of software rows, prefer JSON over CSV (smaller encoding) and pipe directly to a file or parser rather than buffering in memory client-side.

Auth

Standard API key (Authorization: Bearer ucm_…) — same as every other /api/v1/* endpoint. Requires the user to have the API_USER, DISCOVERY_ADMIN or ADMINISTRATOR role.

Synthesized asset types (CMDB-style)

Three asset types are returned by GET /api/v1/assets?type=… on demand from sub-record tables, so CMDB integrators get a uniform asset-shaped response without the platform needing to migrate that data into the main assets table:

TypeSource tableParent link
SOFTWARE_PACKAGEsoftware_packagesRUNS_ON the host asset
RUNNING_PROCESSrunning_processesRUNS_ON the host asset
DISCOVERED_SERVICEservicesRUNS_ON the host asset

Each synthesized row is a standard AssetSummaryResponse with:

  • id — the sub-record UUID (stable across re-scans)
  • assetType / assetCategory — the synthesized type / category
  • displayName — the package / process / service name
  • customAttributes — JSON bag of type-specific fields (install_date, pid, listen_ports, etc.)
  • parent — {@link ParentRef} pointing to the host asset, with via=RUNS_ON so the relationship graph stays consistent

Filterable via the standard ?q= free-text search (matches against the package / process / service name, the asset display name, and the publisher / command line). Pagination via ?page= + ?size= mirrors the rest of the assets API.

# All software packages, page 1
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://insight.example.com/uControlInsight/api/v1/assets?type=SOFTWARE_PACKAGE&page=0&size=50"

# All Microsoft software across the estate
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://insight.example.com/uControlInsight/api/v1/assets?type=SOFTWARE_PACKAGE&q=Microsoft"

# Processes named nginx
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://insight.example.com/uControlInsight/api/v1/assets?type=RUNNING_PROCESS&q=nginx"

# Discovered services
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://insight.example.com/uControlInsight/api/v1/assets?type=DISCOVERED_SERVICE"

Example response (truncated):

{
  "data": [
    {
      "id": "8b2c1d3e-...",
      "assetType": "SOFTWARE_PACKAGE",
      "assetCategory": "SOFTWARE",
      "displayName": "openssh-server",
      "vendor": "Canonical",
      "model": "8.9p1-3",
      "customAttributes": "{\"install_date\":\"2024-11-12\",\"package_manager\":\"dpkg\"}",
      "status": "ACTIVE",
      "parent": {
        "id": "3eadf3af-6683-4a6a-aa9e-ea4eff6e755a",
        "displayName": "esx01.example.com",
        "assetType": "PHYSICAL_HOST",
        "assetCategory": "HOST",
        "via": "RUNS_ON"
      }
    }
  ],
  "page": 0, "size": 50, "totalElements": 1247
}

When to use which:

  • Bulk dump for ETL / data warehouse → the dedicated /api/v1/export/software.csv et al. Streaming, lighter payload, simpler shape.
  • CMDB sync / interactive queries / pagination/api/v1/assets?type=…. Same response shape as every other asset, parent relationship baked in.

Discovered Services export

Mirror of the software / processes export pattern.

EndpointFormat
GET /api/v1/export/services.csvCSV
GET /api/v1/export/services.jsonJSON
FieldTypeNotes
assetIdstring (UUID)Parent asset
assetDisplayNamestringAsset display name
assetHostnamestring?Hostname when known
serviceNamestringUnit / service name (e.g. nginx.service, Spooler)
displayNamestring?Friendly label when the OS provides one
statestring?running / stopped / active / etc.
startModestring?auto, manual, disabled, …
listenPortsarrayJSON array of {protocol, port} objects when the service is bound
sourceConnectorstring?SSH / WINRM / etc.

Filters: ?assetId={uuid}, ?state={state}.

# All services, JSON
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://insight.example.com/uControlInsight/api/v1/export/services.json"

# Only running services on one asset
cu​rl -H "Authorization: Bearer ucm_yourkey" \
  "https://insight.example.com/uControlInsight/api/v1/export/services.csv?assetId=3eadf3af-6683-4a6a-aa9e-ea4eff6e755a&state=running"

Retention & aging behaviour for sub-records

The three sub-record types persist with different lifecycle rules — the API surface is identical (export + synthesized assets), but how rows accumulate and age out differs by type:

TypePersistence modelWhat first_seen_at meansRetention
SOFTWARE_PACKAGE Upsert keyed by (asset_id, package_name) The first scan that ever reported this package on this host. Stays stable across re-scans and version upgrades. 30 days since last observation. Configurable via ucontrol.retention.sub-asset-days.
DISCOVERED_SERVICE Upsert keyed by (asset_id, service_name) The first scan that ever reported this service. Stays stable as the service starts/stops/restarts. 30 days since last observation. Same setting.
RUNNING_PROCESS Wipe-and-replace per scan The most recent scan's timestamp (no stable identity per process across scans — PIDs rotate). Implicit — the table is rebuilt every credentialed scan of the asset.

The behavioural difference matters for what you can ask of the data:

  • Packages & services have stable identity per host. You can track "when was this package first installed?", "this service has been running uninterrupted for 90 days", or "a new package appeared on this server overnight". The row's id is stable across scans, so a CMDB integrator can subscribe to a specific row and have it stay valid.
  • Processes are a snapshot. PIDs rotate, processes start and exit between scans, and there's no stable identity to age. The export and synthesized-asset views always reflect only the most recent scan. Asking "was nginx running yesterday?" is not a question the platform can answer for processes — by design.

The retention sweep runs daily at 02:15 UTC and once shortly after each application restart. Tunable via application.yml:

ucontrol:
  retention:
    sub-asset-days: 30        # how long to keep packages/services not observed
    cron: "0 15 2 * * *"      # when the sweep runs (UTC)

INTERFACE as a first-class CI

Interface rows can be queried directly as if they were assets, with the host attached as the parent CI. Same synthesis pattern used for SOFTWARE_PACKAGE / RUNNING_PROCESS / DISCOVERED_SERVICE — no data migration; the rows are mapped from the interfaces table on demand.

EndpointReturns
GET /api/v1/assets?type=INTERFACE One AssetSummaryResponse per interface, sorted by host name then ifIndex. parent populated with the host CI, via=HAS_INTERFACE.
GET /api/v1/assets?type=INTERFACE&q=eth0 Free-text matches if_name, MAC, host display name.

Interface-to-interface connections

Cross-asset edges between interfaces are derived in InterfaceTopologyStage of the normalization pipeline from two evidence sources:

SourceHowConfidence
LLDP / CDP The connector returns each neighbour with both localPort and remotePort. We resolve each side to its interfaces row by (asset_id, if_name). 1.00 — precise port-to-port observation
FDB For each switch_port_macs row, JOIN to the interfaces table by MAC to find the host NIC plugged into that switch port. 0.90 — FDB proves L2 reachability but not direct cable

Stored in the new interface_connections table with unique key (source_interface_id, target_interface_id, discovered_via) so the same edge confirmed via two evidence sources is kept as two rows — operators can see both pieces of corroboration.

Surfaced in the Interfaces tab on every asset detail page in a new Connected to column: each cell links to the neighbour asset and shows its port name (e.g. switch-core-1:Gi1/0/12), with the discovery source + VLAN in the tooltip.

Next phase (not yet shipped): render port-to-port edges on the asset topology graph instead of just chassis-to-chassis. The data is now there; the visualisation work remains.