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):
- Static Assets - Manual asset inventory (highest priority)
- DHCP Logs - Windows DHCP Server or Infoblox (authoritative for IP assignments)
- 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
| Value | Lookup Direction | JOIN Column |
|---|---|---|
src_ip, dest_ip (any *_ip) | IP → identity | ip |
user, dest_user | User → IP/identity | user |
src_host, dest_host | Hostname → IP/identity | hostname |
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):
| Field | Description |
|---|---|
src_host | Filled with resolved hostname if originally empty |
src_mac | Filled with resolved MAC address if originally empty |
user | Filled with resolved user if originally empty |
User lookups (field=user, field=dest_user):
| Field | Description |
|---|---|
identity_ip | Resolved IP address for this user at event time |
src_host | Filled with resolved hostname if originally empty |
src_mac | Filled with resolved MAC address if originally empty |
Hostname lookups (field=src_host, field=dest_host):
| Field | Description |
|---|---|
identity_ip | Resolved IP address for this hostname at event time |
src_mac | Filled with resolved MAC address if originally empty |
user | Filled with resolved user if originally empty |
Provenance Fields (always added, all lookup types):
| Field | Description |
|---|---|
identity_confidence | Confidence level: high (<1h old), medium (<4h), low (<24h), stale (older), none (no match) |
identity_observed_at | Timestamp when the identity binding was observed |
identity_source | Source of the identity observation (static, windows_dhcp, defender_edr, etc.) |
identity_fqdn | Fully qualified domain name from identity observation |
is_nat_candidate | true 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
- Identity Collection: Multiple sources emit events containing IP + hostname + MAC + user bindings
- Priority Selection: For each IP/hour bucket, the highest-priority source is selected
- 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
- Confidence Scoring: Assigns confidence based on how recent the identity observation is relative to the event
- 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:
| Priority | Source | Rationale |
|---|---|---|
| 100 | Static Assets | User-defined, highest authority |
| 80 | DHCP (Windows/Infoblox) | Authoritative for IP assignment |
| 50 | EDR (Defender/Sysmon/CrowdStrike) | Observational, may have gaps |
| 30 | Other | Default 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
Option 1: Static Assets (Recommended for known infrastructure)
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 controllerIngest with the static asset script:
python scripts/ingest-static-assets.py --file assets.csv --vector http://localhost:8686Run this on a schedule (e.g., hourly cron) to keep static entries fresh.
Option 2: DHCP Logs (Recommended for dynamic workstations)
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)
Trace a user across multiple log sources (recommended)
src_ip="192.168.1.100"
| resolve_identity
| table timestamp, source_type, src_ip, src_host, user, action, identity_confidenceFilter 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_confidenceThis 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_confidenceResolve 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_confidenceOnly 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_sourceHunt 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_confidenceResolve 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_hostSee 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_confidenceResolve 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 > 3Find 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, answerFull 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_candidateInvestigate potentially suspicious outbound connections on port 4444 with full identity context.
Identity Sources
| Source | Type | Priority | Identity Fields |
|---|---|---|---|
| Static Assets | Manual | 100 | ip, hostname, mac, user |
| Windows DHCP | DHCP | 80 | IPAddress, HostName, MACAddress |
| Infoblox DHCP | DHCP | 80 | IP, hostname, MAC |
| Microsoft Defender | EDR | 50 | DeviceName, IPAddresses, MacAddress |
| Sysmon | EDR | 50 | SourceHostname, SourceIp |
| CrowdStrike | EDR | 50 | ComputerName, 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_hostCombine with source_type for focused queries
(source_type=squid_proxy OR source_type=defender_edr) src_ip="192.168.1.100"
| resolve_identityFilter 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_observationsIf 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_ageto 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=trueAlso check which source provided the identity:
src_ip="192.168.1.100" | resolve_identity | table identity_source, src_hostIf 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 10Ensure the migration 076_add_identity_source_priority.sql has been applied.
Related Commands
- 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