Skip to content

Asset Approval Workflow Issues

Discovery ran, and Inventory does not show what you expected. This page is organised by symptom, and the first one is by far the most common — and is not a fault at all.

The model this page assumes. An asset is one thing — a server, a switch, a bucket, a service — with a class, the identifiers it is known by, and the endpoints it listens on. A host exposing five services is one asset with five endpoints, not five assets. Queries below read assets, asset_identifiers, asset_endpoints and asset_history. If you arrived here with older notes, see What replaced what at the bottom.

Substitute your tenant's UUID for <tenant> throughout. Every query is written against a single tenant on purpose: every table here is row-level-security scoped, and a query without a tenant predicate is a query that will surprise you.


Symptom: discovery ran, but Inventory is empty

Sensors are reporting, batches are processed, external connections appear — and Inventory shows nothing, with certificates and crypto_implementations at zero rows.

This is the designed approval workflow, not a broken pipeline. New assets land with asset_status = 'pending_approval', and their certificates and crypto configurations are deliberately deferred — held with the pending asset — until it is approved in Discovery → Approvals. Only approval materializes them into the inventory tables. Auto-approval is per prefix and is off by default.

Confirm it in one query:

SELECT asset_status, count(*)
FROM assets
WHERE tenant_id = '<tenant>' AND deleted_at IS NULL
GROUP BY asset_status
ORDER BY asset_status;

A large pending_approval count and a small (or zero) monitoring count is the answer: the work is waiting for a person, in Discovery → Approvals.

To see what is being held with each pending asset — the certificates and crypto configurations that will appear the moment it is approved:

SELECT a.id,
       a.display_name,
       a.class_key,
       count(d.id) AS deferred
FROM assets a
LEFT JOIN deferred_crypto_findings d
       ON d.tenant_id = a.tenant_id AND d.asset_id = a.id AND d.replayed_at IS NULL
WHERE a.tenant_id = '<tenant>'
  AND a.asset_status = 'pending_approval'
  AND a.deleted_at IS NULL
GROUP BY a.id, a.display_name, a.class_key
ORDER BY deferred DESC
LIMIT 20;

deferred_crypto_findings is a holding pen, not an attribute of the thing: a row is replayed (replayed_at set) when its asset becomes monitoring, by whatever route. A pending asset with 0 deferred findings is normal — it means nothing cryptographic was observed on it yet. Under identity admission enforce, a finding waits on its observation instead (observation_id set, asset_id empty until it is replayed).

If an asset is monitoring and rows for it still show replayed_at empty with a last_error, the replay failed and is retried every five minutes; the inventory-service log names the asset.

Two things exist to make this self-evident before anyone reaches for SQL: the discovery-processor batch log reports internal findings split by monitoring/pending and notes the deferral rather than printing 0 asset findings, and the Inventory page shows a pending-approval banner with a count and a link to the queue.


Symptom: assets keep queuing when you expected auto-approval

Two independent gates decide this, and both are off until someone turns them on.

1. The right switch for the source. Sensor discoveries are auto-approved by a prefix's Auto-approve setting (Inventory → Networks, open the prefix). Cloud resources are not — they are auto-approved by their account: the "Auto-approve resources" switch on the integration under Discovery → Cloud (on by default).

-- Sensor discoveries: the prefix setting, and the rule it generates.
SELECT prefix, description, network_type, auto_approve_discoveries, auto_approve_sources
FROM prefixes
WHERE tenant_id = '<tenant>' AND vrf_id IS NULL AND status NOT IN ('rejected', 'archived')
ORDER BY prefix;

-- Cloud resources: the account switch, and the account each integration names.
SELECT integration_name, integration_type, account_id, auto_approve_resources
FROM platform_integrations
WHERE tenant_id = '<tenant>' AND deleted_at IS NULL
ORDER BY integration_name;

auto_approve_discoveries = false is the default for every prefix. For cloud, the usual answer when resources still queue is an empty account_id: an AWS integration learns its account on the first successful Test connection, and until then there is no account for a rule to be about. A resource whose own ARN names a different account than the integration's is approved only by that account's switch.

2. The tenant's auto-approval rules, which are queries now. A rule is a query-language string over the observation target, not a JSON object interpreted by a hand-written evaluator:

SELECT name, is_active, query
FROM discovery_auto_approval_rules
WHERE tenant_id = '<tenant>'
ORDER BY name;

Read the query column as the rule: class:server and environment:production approves exactly what it says. Two things worth knowing when one does not fire: an empty query matches every observation (a rule that auto-approves everything — deliberately expressible, and deliberately something a person has to type), and a query is validated when it is saved, so a rule that stored at all is a rule that parses. A rule that parses and never matches is a rule about the wrong thing, not a typo.


Symptom: the queue is full of merge proposals, and nothing new appears

Discovery → Approvals holds two kinds of work. Pending assets are one; the other is a merge proposal — the identification engine's report that a new sighting matched more than one existing asset, or matched one it is not confident enough to fold into. Nothing is ever merged for you: a proposal waits for a person to say "these are the same thing" or "keep them separate".

Merge proposals are rows in asset_history, not a table of their own:

SELECT seq,
       asset_id,
       created_at,
       changes_json ->> 'fingerprint' AS fingerprint,
       changes_json
FROM asset_history
WHERE tenant_id = '<tenant>'
  AND action = 'merge_proposed'
  AND COALESCE(changes_json ->> 'status', 'pending') = 'pending'
ORDER BY created_at DESC;

changes_json carries the candidate assets and the identifiers that matched them — which is what the Approvals row renders. Deciding a proposal writes status into it, so it drops out of the query above.

One proposal per question. A unique index on (tenant_id, changes_json->>'fingerprint') for pending proposals means a collector that re-observes the same conflict every fifteen minutes does not deposit 96 identical work items a day. If you see many proposals with DIFFERENT fingerprints for what looks like one machine, that is a real signal: something is producing observations that identify differently each time.

To see why the engine thinks a particular asset is ambiguous, read its identifiers — the scope is usually the answer, because a hostname or an IP identifies only within its prefix:

SELECT kind, value, scope, source_kind, source_ref, last_seen_at
FROM asset_identifiers
WHERE tenant_id = '<tenant>' AND asset_id = '<asset>'
ORDER BY kind;

And the asset's own story, in order:

SELECT seq, action, source, actor_user_id, created_at, changes_json
FROM asset_history
WHERE tenant_id = '<tenant>' AND asset_id = '<asset>'
ORDER BY seq;

Order by seq, never by created_at: several history rows are written inside one transaction and therefore carry the same timestamp, so ordering by time returns them in whatever order the planner chose.


Symptom: cloud discoveries are not reaching Inventory

Cloud discoveries go through the same pipeline as sensor discoveries:

  1. device-interrogation-service discovers the cloud resources (AWS, Azure, GCP).
  2. They are written to sensor_discoveries with metadata->>'discovery_method' = 'cloud_api'.
  3. discovery-processor-service is woken as soon as they are written and processes the batches (a 60-second poll catches anything it was not woken for).
  4. A cloud discovery is classified by its cloud_provider/cloud_region — the per-region cloud prefix — not by its address. Most cloud resources have no address at all, and an asset with no endpoint is a real answer rather than a gap: a bucket or a key store has nothing listening on a port.
  5. Assets land monitoring when their account's "Auto-approve resources" switch is on (the default), and pending_approval in Discovery → Approvals when it is off. Either way they hang off the account's Cloud Account asset.

Check that the discoveries arrived at all:

SELECT batch_id, count(*) AS rows, min(created_at) AS first_seen
FROM sensor_discoveries
WHERE metadata ->> 'discovery_method' = 'cloud_api'
GROUP BY batch_id
ORDER BY first_seen DESC;

Unprocessed rows:

SELECT id, batch_id, created_at
FROM sensor_discoveries
WHERE metadata ->> 'discovery_method' = 'cloud_api'
  AND processed_at IS NULL
ORDER BY created_at;

If rows are arriving and never processed, check the processor:

docker compose logs discovery-processor-service

Cloud discovery runs through the Platform Device Interrogation Agent system sensor — verify that sensor exists and is active for the tenant.


What replaced what

Older runbooks (and older habits) reach for tables and a screen that no longer exist. A query naming any of these fails at runtime with relation … does not exist, per query — which is exactly the error this codebase has historically swallowed behind a 200, so it is worth recognising.

If your notes say Read instead
network_assets (any form) assets — one row per thing, with class_key / class_path
an asset's ip_address / port asset_endpoints — one row per network face
an asset's asset_type assets.class_key, or class_path for a whole subtree
devices assets joined to asset_management (a device is an asset with management configured)
discovery_approval_queue sensor_discoveries + the approval state on assets — this table is dropped, not merely deprecated
"import the discovery results" Nothing to do: a job's findings reach inventory server-side. No API accepts a caller-supplied approval status.