Behavioral Detection¶
Overview¶
LimaCharlie supports behavioral detection patterns using D&R rules and the suppression system. These patterns detect anomalous behavior — like a user logging in from a new country or a host resolving an unusual domain — without requiring external analytics infrastructure.
This page covers:
- First-Seen Detection — alert the first time a specific combination of entity + behavior is observed
- Cardinality Detection — alert when an entity exceeds a threshold of unique values (e.g., unique domains, unique hosts)
- Volume Detection — alert when a cumulative metric (e.g., bytes transferred) crosses a threshold
- Multi-Signal Aggregation — combine multiple detection signals into a composite risk indicator
All patterns use the existing D&R rules engine and suppression system.
First-Seen Detection¶
Suppression with max_count: 1 fires an action exactly once per unique key combination per time window. This makes it a first-seen detector: the first time a (entity, value) pair is observed, the action fires. For the rest of the window, it is suppressed.
First-Seen with Event Fields¶
When the value you want to track is directly in the event, a single rule is sufficient.
First time a host resolves a domain (within 30 days):
detect:
event: DNS_REQUEST
op: exists
path: event/DOMAIN_NAME
respond:
- action: report
name: new-domain-for-host
suppression:
max_count: 1
period: 720h
is_global: false
keys:
- 'first-domain'
- '{{ .event.DOMAIN_NAME }}'
The suppression key combines a constant label with the domain name, scoped per-sensor (is_global: false). The first DNS request for a given domain on a given sensor fires the report. Subsequent requests for the same domain on the same sensor are suppressed for 30 days.
First time a process hash runs on a host:
detect:
event: NEW_PROCESS
op: exists
path: event/HASH
respond:
- action: report
name: new-process-hash-on-host
suppression:
max_count: 1
period: 720h
is_global: false
keys:
- 'first-process'
- '{{ .event.HASH }}'
First time a user logs in from a new source IP (org-wide):
detect:
event: USER_LOGIN
op: exists
path: event/USER_NAME
respond:
- action: report
name: new-login-source-for-user
suppression:
max_count: 1
period: 720h
is_global: true
keys:
- 'first-login-src'
- '{{ .event.USER_NAME }}'
- '{{ .event.SOURCE_IP }}'
Using is_global: true means the suppression is org-wide — the counter is shared across all sensors. This is important for user-scoped detections where the user may log in from different sensors.
First-Seen with Lookup Metadata¶
When the value you want to track is derived from a lookup (e.g., a GeoIP country from an IP address), the lookup metadata can be referenced in suppression key templates using the .mtd namespace.
The .mtd namespace contains the metadata returned by the detection's lookup operator. The key name is the resource name with special characters replaced by underscores. For the IP Geolocation lookup (lcr://api/ip-geo), the metadata is available under .mtd.lcr___api_ip_geo.
First time a user logs in from a new country:
detect:
event: USER_LOGIN
op: lookup
path: event/SOURCE_IP
resource: lcr://api/ip-geo
respond:
- action: report
name: first-login-from-country
suppression:
max_count: 1
period: 720h
is_global: true
keys:
- 'first-country'
- '{{ .event.USER_NAME }}'
- '{{ .mtd.lcr___api_ip_geo.country.iso_code }}'
This rule:
- Matches every
USER_LOGINevent - Looks up the
SOURCE_IPvia the GeoIP API - Generates a suppression key from the user name and the resolved country ISO code
- Reports once per unique
(user, country)combination per 30 days
First time a user logs in from a new ASN:
detect:
event: USER_LOGIN
op: lookup
path: event/SOURCE_IP
resource: lcr://api/ip-geo
respond:
- action: report
name: first-login-from-asn
suppression:
max_count: 1
period: 720h
is_global: true
keys:
- 'first-asn'
- '{{ .event.USER_NAME }}'
- '{{ .mtd.lcr___api_ip_geo.autonomous_system.number }}'
First time a threat-intel-matched hash appears on a host:
detect:
event: NEW_PROCESS
op: lookup
path: event/HASH
resource: hive://lookup/threat-intel-hashes
respond:
- action: report
name: first-ti-match-on-host
suppression:
max_count: 1
period: 720h
is_global: false
keys:
- 'first-ti-hash'
- '{{ .event.HASH }}'
- '{{ .mtd.threat_intel_hashes.category }}'
Metadata Key Naming
The
.mtdkey name is derived from the lookup resource name with/and:replaced by_. For example:
lcr://api/ip-geobecomes.mtd.lcr___api_ip_geohive://lookup/my-listbecomes.mtd.my_list
Combining First-Seen with Other Operators¶
First-seen detection composes naturally with all D&R operators using and/or:
First time a rare domain is resolved on a VIP host:
detect:
op: and
rules:
- event: DNS_REQUEST
op: lookup
path: event/DOMAIN_NAME
resource: hive://lookup/rare-domains
- op: is tagged
tag: vip
respond:
- action: report
name: rare-domain-on-vip
priority: 1
suppression:
max_count: 1
period: 720h
is_global: false
keys:
- 'first-rare-domain'
- '{{ .event.DOMAIN_NAME }}'
Cardinality Detection¶
To detect when an entity accumulates too many unique values (e.g., a host resolving an unusual number of unique domains), use a two-rule chaining pattern:
- Rule 1 (dedup): Reports once per unique value using
max_count: 1 - Rule 2 (count): Targets the detection from Rule 1 and counts using
min_count: N
Example: DGA / C2 Beaconing Detection¶
Detect a host resolving more than 100 unique domains in 1 hour:
# Rule 1: Deduplicate — report once per unique domain per sensor per hour
detect:
event: DNS_REQUEST
op: exists
path: event/DOMAIN_NAME
respond:
- action: report
name: dns-domain-observed
suppression:
max_count: 1
period: 1h
is_global: false
keys:
- 'dns-dedup'
- '{{ .event.DOMAIN_NAME }}'
# Rule 2: Count — fire when unique domains exceed threshold
detect:
event: dns-domain-observed
target: detection
op: exists
path: detect
respond:
- action: report
name: excessive-dns-diversity
suppression:
min_count: 100
max_count: 100
period: 1h
is_global: false
keys:
- 'dns-diversity-count'
Rule 1 fires once per unique domain per sensor per hour (deduplication). Rule 2 chains on the detection target, counting how many unique domains triggered Rule 1. When the count reaches 100, Rule 2 fires exactly once.
Example: Lateral Movement Detection¶
Detect a user accessing more than 5 unique hosts in 6 hours:
# Rule 1: Deduplicate per (user, host)
detect:
event: USER_LOGIN
op: exists
path: event/USER_NAME
respond:
- action: report
name: user-host-access-observed
suppression:
max_count: 1
period: 6h
is_global: true
keys:
- 'lateral-dedup'
- '{{ .event.USER_NAME }}'
- '{{ .routing.hostname }}'
# Rule 2: Count unique hosts per user
detect:
event: user-host-access-observed
target: detection
op: exists
path: detect
respond:
- action: report
name: possible-lateral-movement
suppression:
min_count: 5
max_count: 5
period: 6h
is_global: true
keys:
- 'lateral-count'
- '{{ .detect.event.USER_NAME }}'
Example: Excessive External Connections¶
Detect a host connecting to more than 50 unique external IPs in 1 hour:
# Rule 1: Deduplicate unique external destination IPs per sensor
detect:
event: NEW_TCP4_CONNECTION
op: is public address
path: event/IP_ADDRESS
respond:
- action: report
name: external-conn-observed
suppression:
max_count: 1
period: 1h
is_global: false
keys:
- 'ext-conn-dedup'
- '{{ .event.IP_ADDRESS }}'
# Rule 2: Count unique destinations per sensor
detect:
event: external-conn-observed
target: detection
op: exists
path: detect
respond:
- action: report
name: excessive-external-connections
suppression:
min_count: 50
max_count: 50
period: 1h
is_global: false
keys:
- 'ext-conn-count'
Volume Detection¶
The count_path suppression parameter increments the counter by a value extracted from the event instead of by 1. This enables threshold detection on cumulative metrics like bytes transferred.
Example: Data Exfiltration Threshold¶
Alert when a host uploads more than 1 GB to external IPs in 24 hours:
detect:
event: USP_NETFLOW
op: is public address
path: event/dst_ip
respond:
- action: report
name: high-egress-volume
suppression:
min_count: 1073741824
max_count: 1073741824
period: 24h
is_global: false
count_path: event/bytes_out
keys:
- 'egress-volume'
The counter increments by the value at event/bytes_out for each matching event. When the cumulative bytes reach 1 GB (1,073,741,824 bytes), the report fires exactly once.
Multi-Signal Aggregation¶
Multiple detection rules can feed into a shared suppression counter to create a composite risk indicator. When independent detections all report with a shared key, the counter accumulates across them.
Example: Risk Score Aggregation¶
Individual indicator rules each generate a detection:
# Rule A: Suspicious DNS resolution
detect:
event: DNS_REQUEST
op: lookup
path: event/DOMAIN_NAME
resource: hive://lookup/suspicious-domains
respond:
- action: report
name: indicator-hit
# Rule B: Sensitive process access
detect:
event: SENSITIVE_PROCESS_ACCESS
op: exists
path: event/TARGET/FILE_PATH
respond:
- action: report
name: indicator-hit
Aggregation rule — fires when 5 indicators accumulate on a single host in 1 hour:
detect:
event: indicator-hit
target: detection
op: exists
path: detect
respond:
- action: report
name: high-risk-host
priority: 1
suppression:
min_count: 5
max_count: 5
period: 1h
is_global: false
keys:
- 'risk-aggregation'
Since both Rule A and Rule B report the same detection name (indicator-hit), the aggregation rule counts them together. Different types of suspicious activity on the same host contribute to the same counter.
Suppression Parameter Reference¶
| Parameter | Type | Description |
|---|---|---|
max_count |
integer | Maximum action executions per period per key. Use 1 for first-seen. |
min_count |
integer | Minimum activations before the action fires. Must be used with max_count. |
period |
string | Time window. Formats: s, m, h. Range: 1s to 720h (30 days). |
is_global |
boolean | true = org-wide counter. false (default) = per-sensor counter. |
keys |
list | Template strings that form the uniqueness key. Supports {{ .event.* }}, {{ .routing.* }}, and {{ .mtd.* }}. |
count_path |
string | Path to an integer in the event to use as the increment value instead of 1. |
Template Namespaces in Keys¶
| Namespace | Source | Example |
|---|---|---|
.event.* |
Raw event payload | {{ .event.FILE_PATH }} |
.routing.* |
Event routing metadata | {{ .routing.hostname }} |
.mtd.* |
Detection metadata from lookup operators | {{ .mtd.lcr___api_ip_geo.country.iso_code }} |
Limitations¶
- Static thresholds only. The thresholds (count values, periods) are user-defined constants. There is no adaptive baseline that learns "normal" from historical data.
- Fixed time windows. The suppression period is a fixed window that resets on expiry, not a rolling/sliding window.
- Maximum period: 30 days. Suppression counters reset after the period expires. "First seen within 30 days" is the longest tracking window.
- No statistical comparison. These patterns detect "above N" or "first occurrence" — they cannot detect "unusual compared to historical baseline."
- Cardinality detection requires two rules. The dedup+count pattern needs rule chaining via the
detectiontarget.