nano SIEM
Search Commands

resolve_identity

resolve_identity

Enrich events with identity data using bidirectional lookups — resolve IPs to hostnames/users, or reverse-resolve users and hostnames back to IPs.

:::caution[Prerequisites Required] Identity resolution requires at least one identity data source to be configured. Without identity sources, this command returns no enrichment.

Supported identity sources (in priority order):

  1. Static Assets - Manual asset inventory (highest priority)
  2. DHCP Logs - Windows DHCP Server or Infoblox (authoritative for IP assignments)
  3. EDR Telemetry - Defender, Sysmon, CrowdStrike (observational)

See Setting Up Identity Sources below. :::

Description

The resolve_identity command performs bidirectional identity lookups using temporal identity observations from multiple sources. It uses ClickHouse's ASOF JOIN to find the best identity observation at or before the event timestamp, with source priority ensuring authoritative sources take precedence.

Forward lookups (IP → identity): Resolve IP addresses to hostnames, MAC addresses, and users. Useful for enriching network-only data sources (firewall, proxy, DNS) with endpoint identity — "which user was behind this blocked connection?"

Reverse lookups (user/hostname → IP): Resolve users or hostnames back to their IP addresses. Useful for threat hunting by identity — "what IPs did jsmith use?" or "what IP was WORKSTATION-42 on at the time?"

Syntax

... | resolve_identity [field=<field>] [max_age=<duration>]

Optional Arguments

field The field to resolve. Determines the lookup direction. Default: src_ip

ValueLookup DirectionJOIN Column
src_ip, dest_ip (any *_ip)IP → identityip
user, dest_userUser → IP/identityuser
src_host, dest_hostHostname → IP/identityhostname

max_age Maximum age of identity observation to consider valid. Older observations are filtered out. Default: 24h

Output Fields

The output fields depend on the lookup direction. The command fills UDM fields if they're empty (never overwrites existing values), and adds provenance metadata.

IP lookups (field=src_ip, field=dest_ip — default):

FieldDescription
src_hostFilled with resolved hostname if originally empty
src_macFilled with resolved MAC address if originally empty
userFilled with resolved user if originally empty

User lookups (field=user, field=dest_user):

FieldDescription
identity_ipResolved IP address for this user at event time
src_hostFilled with resolved hostname if originally empty
src_macFilled with resolved MAC address if originally empty

Hostname lookups (field=src_host, field=dest_host):

FieldDescription
identity_ipResolved IP address for this hostname at event time
src_macFilled with resolved MAC address if originally empty
userFilled with resolved user if originally empty

Provenance Fields (always added, all lookup types):

FieldDescription
identity_confidenceConfidence level: high (<1h old), medium (<4h), low (<24h), stale (older), none (no match)
identity_observed_atTimestamp when the identity binding was observed
identity_sourceSource of the identity observation (static, windows_dhcp, defender_edr, etc.)
identity_fqdnFully qualified domain name from identity observation
is_nat_candidatetrue if the IP had 3+ unique hostnames in the same hour (possible NAT/shared IP)

Note: Existing values in src_host, src_mac, and user are preserved — the command only fills empty fields.

How It Works

  1. Identity Collection: Multiple sources emit events containing IP + hostname + MAC + user bindings
  2. Priority Selection: For each IP/hour bucket, the highest-priority source is selected
  3. ASOF JOIN: For each event being enriched, finds the most recent identity observation where the lookup field matches and the observation timestamp is before or equal to the event timestamp
  4. Confidence Scoring: Assigns confidence based on how recent the identity observation is relative to the event
  5. NAT Detection: Flags IPs that appear to be behind NAT (multiple hostnames per IP per hour)

For reverse lookups (field=user or field=src_host), the ASOF JOIN matches on the user or hostname column of the identity_observations table instead of ip, and adds identity_ip to the output so you can see which IP the user or host was on at that time.

Source Priority

When multiple sources provide identity for the same IP, nano uses this priority order:

PrioritySourceRationale
100Static AssetsUser-defined, highest authority
80DHCP (Windows/Infoblox)Authoritative for IP assignment
50EDR (Defender/Sysmon/CrowdStrike)Observational, may have gaps
30OtherDefault for unknown sources

Within the same priority level, the most recent observation is used.

Example: If DHCP says 192.168.1.100 = WORKSTATION01 and EDR says 192.168.1.100 = WORKSTATION02 (perhaps due to a detection error), DHCP wins because it's authoritative for IP assignments.

Setting Up Identity Sources

Create a CSV file with your known assets (servers, printers, network gear):

ip,hostname,mac,user,description
192.168.1.1,ROUTER01,00:00:5e:00:01:01,,Core router
192.168.1.10,PRINTER01,00:11:22:33:44:01,,Floor 1 printer
192.168.1.20,DC01,00:11:22:33:44:02,SYSTEM,Domain controller

Ingest with the static asset script:

python scripts/ingest-static-assets.py --file assets.csv --vector http://localhost:8686

Run this on a schedule (e.g., hourly cron) to keep static entries fresh.

Windows DHCP Server: Configure log forwarding from your Windows DHCP servers. Logs will be parsed with source_type=windows_dhcp.

Infoblox DHCP: Forward syslog from Infoblox appliances. Logs will be parsed with source_type=infoblox_dhcp.

Option 3: EDR Telemetry (Default)

If you have Microsoft Defender, Sysmon, or CrowdStrike configured, identity observations are collected automatically from:

  • Defender DeviceNetworkInfo events
  • Sysmon Event ID 3 (Network Connection)
  • CrowdStrike NetworkConnect events

Examples

Forward Lookups (IP → Identity)

src_ip="192.168.1.100"
| resolve_identity
| table timestamp, source_type, src_ip, src_host, user, action, identity_confidence

Filter by src_ip first, then enrich with identity. This is the most efficient way to correlate a user's activity across EDR, proxy, firewall, and other log sources. The IP filter uses indexed lookups, then resolve_identity adds hostname and user context.

Basic identity resolution for firewall logs

source_type=firewall action=block
| resolve_identity
| table timestamp, src_ip, src_host, user, dest_ip, dest_port, identity_confidence

This enriches blocked firewall connections with the hostname and user behind each source IP.

Resolve destination IPs in proxy logs

source_type=squid_proxy
| resolve_identity field=dest_ip
| table timestamp, src_ip, dest_ip, dest_host, url, identity_confidence

Resolve the destination IP to a hostname for internal proxy traffic.

Stricter time window for high-confidence resolution

source_type=firewall action=block earliest=-1h
| resolve_identity max_age=4h
| where identity_confidence IN ("high", "medium")
| table timestamp, src_ip, src_host, user, identity_confidence

Only include events with high or medium confidence identity resolution.

Reverse Lookups (User/Hostname → IP)

Find what IPs a user was on

user="jsmith"
| resolve_identity field=user
| table timestamp, identity_ip, src_host, identity_confidence, identity_source

Hunt for all IPs associated with a specific user. The identity_ip field shows which IP the user was on at the time of each event.

Find what IP a hostname resolved to

src_host="WORKSTATION-42"
| resolve_identity field=src_host
| table timestamp, identity_ip, user, identity_confidence

Resolve a hostname back to its IP address at event time. Useful when investigating alerts that reference a hostname but you need the IP for network-level correlation.

Correlate a user's network activity

user="jsmith"
| resolve_identity field=user
| where identity_confidence IN ("high", "medium")
| stats count, dc(identity_ip) as unique_ips, values(identity_ip) as ips by src_host

See how many IPs a user was associated with and from which hosts — helps identify shared accounts or lateral movement.

Investigate a destination hostname

dest_host="suspicious-server.internal"
| resolve_identity field=dest_host
| table timestamp, identity_ip, src_ip, user, action, identity_confidence

Resolve a destination hostname to its IP and correlate with source activity.

General Examples

Investigate NAT candidates

source_type=firewall
| resolve_identity
| where is_nat_candidate=true
| stats dc(src_host) as unique_hosts, values(src_host) as hosts by src_ip
| where unique_hosts > 3

Find IPs that appear to be NAT gateways with many hosts behind them.

Filter out stale identity observations

source_type=dns
| resolve_identity
| where identity_confidence != "stale" AND identity_confidence != "none"
| table timestamp, src_ip, src_host, query, answer

Full investigation workflow

source_type=firewall dest_port=4444 action=allow
| resolve_identity
| table timestamp, src_ip, src_host, user, dest_ip, dest_port, identity_confidence, identity_source, is_nat_candidate

Investigate potentially suspicious outbound connections on port 4444 with full identity context.

Identity Sources

SourceTypePriorityIdentity Fields
Static AssetsManual100ip, hostname, mac, user
Windows DHCPDHCP80IPAddress, HostName, MACAddress
Infoblox DHCPDHCP80IP, hostname, MAC
Microsoft DefenderEDR50DeviceName, IPAddresses, MacAddress
SysmonEDR50SourceHostname, SourceIp
CrowdStrikeEDR50ComputerName, LocalIP

Best Practices

Forward lookups: filter by IP first

When using forward lookups (IP → identity), filter by src_ip before resolve_identity:

# Good - efficient, uses indexed IP lookup
src_ip="192.168.1.100" | resolve_identity

# Bad - scans everything, then filters after enrichment
* | resolve_identity | where src_host LIKE 'WORKSTATION%'

Why? Network-only sources (proxy, firewall, DNS) don't have src_host until after enrichment. Filtering by hostname in the base search misses these logs entirely. Filtering by IP catches all sources, then resolve_identity fills in the hostname.

Reverse lookups: filter by user/hostname first

When using reverse lookups, filter by the lookup field in the base search:

# Good - filter by user, then resolve to IP
user="jsmith" | resolve_identity field=user

# Good - filter by hostname, then resolve to IP
src_host="WORKSTATION-42" | resolve_identity field=src_host

Combine with source_type for focused queries

(source_type=squid_proxy OR source_type=defender_edr) src_ip="192.168.1.100"
| resolve_identity

Filter enriched fields with | where

To filter on resolved identity fields, use | where after enrichment:

src_ip="192.168.1.*"
| resolve_identity
| where identity_confidence IN ("high", "medium")
| where src_host LIKE 'WORKSTATION%'

Usage Notes

Performance: The ASOF JOIN is optimized for the (ip, observed_at) ordering in the identity_observations table. Forward lookups (IP fields) benefit from the primary key ordering. Reverse lookups (user/hostname) use bloom filter indexes to skip granules, which works well when the left side is filtered (e.g., user="jsmith" | resolve_identity field=user). Avoid unfiltered reverse lookups like * | resolve_identity field=user — always filter by the lookup field first.

Private IPs only: Identity resolution is most useful for private IP addresses. Public IPs typically won't have identity observations.

NAT awareness: The is_nat_candidate flag helps identify cases where identity resolution may be unreliable due to multiple hosts sharing an IP (NAT gateway, VPN concentrator, shared jump host).

Confidence interpretation:

  • high - Identity observed within 1 hour of event (very reliable)
  • medium - Identity observed within 4 hours (reliable for most DHCP environments)
  • low - Identity observed within 24 hours (use with caution)
  • stale - Identity observation older than max_age (included but flagged)
  • none - No identity observation found for this IP

TTL: Identity observations are retained for 7 days by default.

Troubleshooting

"No identity enrichment" - all fields empty

Check that you have identity sources configured:

| stats count by source from identity_observations

If this returns no results, you need to configure at least one identity source.

Low confidence scores

If most results show identity_confidence=low or stale:

  • Check that identity sources are actively sending data
  • Consider enabling DHCP logging for more authoritative, frequent observations
  • Reduce max_age to filter out stale observations: | resolve_identity max_age=4h

Wrong hostname resolved

If identity seems incorrect, check for NAT:

src_ip="192.168.1.100" | resolve_identity | where is_nat_candidate=true

Also check which source provided the identity:

src_ip="192.168.1.100" | resolve_identity | table identity_source, src_host

If EDR is providing wrong data and you have DHCP, DHCP should take priority. If static assets are defined, they should always take priority.

Priority not working as expected

Verify priority values in the identity_observations table:

SELECT ip, hostname, source, priority, observed_at
FROM identity_observations
WHERE ip = '192.168.1.100'
ORDER BY priority DESC, observed_at DESC
LIMIT 10

Ensure the migration 076_add_identity_source_priority.sql has been applied.

  • asset - Full asset investigation view with identity resolution, activity timeline, and prevalence
  • lookup - Enrich with static lookup table data
  • prevalence - Filter or enrich based on artifact prevalence
  • tree - Visualize hierarchical relationships
On this page

On this page