Risk-Based Alerting
Score events and entities with the risk command, track accumulated entity risk, and write detection rules over the risk dataset.
Risk-Based Alerting
Risk-based alerting assigns numerical risk scores to security events and the entities involved. Scoring threats numerically lets you prioritize responses, track cumulative risk per entity over time, and build risk rules — detection rules that fire when an entity's accumulated risk exceeds a threshold.
Overview
The risk system operates on a 0-100 scale where higher scores indicate greater security risk:
- 0-30: Low risk (blue)
- 31-50: Medium risk (yellow)
- 51-70: High risk (orange)
- 71-100: Critical risk (red)
Risk scores are calculated using:
- In-query
| riskcommand with static or dynamic scores - Conditional expressions for context-aware scoring (
if(), arithmetic) - Global weight multiplier for system-wide tuning (can be overridden per-query)
- Entity extraction for tracking risk per IP, user, hostname, etc.
How Risk Scoring Works
Risk Scores in Queries
Risk scores are set directly in detection queries using the | risk command:
// Static score
| risk score=50 entity=user factor="Suspicious activity"
// Dynamic score based on event count
| stats count() as hits | risk score=hits*5 entity=src_ip
// Conditional score
| risk score=if(is_admin, 80, 40) entity=user factor="Admin access"
// With weight override
| risk score=70 entity=user weight=0.5Severity-Based Default Scores
When using severity to guide risk scores, use this scale as a reference:
| Severity | Suggested Score |
|---|---|
| Critical | 90 |
| High | 70 |
| Medium | 50 |
| Low | 30 |
| Informational | 10 |
Dynamic Risk Scoring
Risk scores can be dynamically calculated based on event conditions using the | risk command with expressions:
// Failed Login Attempts - score scales with attempt count
event_type=authentication AND status=failure
| stats count() as attempts by src_ip
| risk score=if(attempts > 50, 90, if(attempts > 10, 70, 40)) entity=src_ip factor="Failed logins"Supported Score Expressions
The score= parameter supports full eval expressions:
- Literal values:
score=50 - Field references:
score=severity_level - Arithmetic:
score=attempts*5,score=count/10 - Conditionals:
score=if(count > 10, 70, 40) - Functions:
score=min(attempts*2, 100)
All dynamic scores are automatically clamped to the 0-100 range.
Entity Extraction
Risk scores are associated with entities (the "who" or "what" being scored). The system automatically extracts entities using:
- Explicit field: If
risk_entity_fieldis specified, extract from that field - Automatic detection: Try common fields in priority order:
- IP addresses:
src_ip,dest_ip,dvc_ip - Hostnames:
src_host,dest_host,hostname - Users:
src_user,dest_user,user - File hashes:
file_hash,process_hash,service_hash
- IP addresses:
Global Weight Multiplier
The global weight (0.0-1.0) acts as a system-wide risk multiplier:
- 1.0: Full risk scoring (default)
- 0.5: Conservative scoring (50% of calculated risk)
- 0.0: Risk scoring disabled
Final score = round(raw_score × global_weight)
Using Risk in Detection Rules
Risk scores are now set directly in the query using the | risk command. This approach is more flexible and allows for dynamic scoring based on event data.
Basic Risk Configuration
// Suspicious Process Execution
event_type=process_creation AND process_name=powershell.exe
| risk score=75 entity=src_host factor="PowerShell execution"Dynamic Risk Scoring
Score based on aggregated counts or field values:
// Network Anomaly - score increases with connection count
event_type=network_connection
| stats count() as connection_count by src_ip
| risk score=if(connection_count > 1000, 85, if(connection_count > 100, 60, 30)) entity=src_ip factor="Connection volume"Conditional Scoring
Use if() expressions for context-aware scoring:
// Higher score for external admin access
event_type=authentication AND user_type=admin
| risk score=if(src_ip=/^(10\.|192\.168\.)/, 40, 80) entity=user factor="Admin login"Arithmetic Score Expressions
// Score based on multiple factors
event_type=network
| eval risk_points = if(bytes_out > 100000000, 30, 0) +
if(dest_port != 80 AND dest_port != 443, 20, 0) +
if(hour < 6 OR hour > 22, 25, 0)
| risk score=risk_points entity=src_ip factor="Data transfer risk"Weight Override
Override the global risk weight for specific rules:
// Reduce impact of noisy detection during tuning
event_type=file_access
| risk score=30 entity=user factor="File access" weight=0.5Risk Commands in Queries
The risk command allows dynamic risk scoring within search queries:
Basic Risk Command
-- Assign risk score of 50 to all matching events
event_type="malware_detected" | risk score=50Risk with Entity Specification
-- Score by source IP with custom factor description
suspicious_activity=true
| risk score=75 entity=src_ip factor="Suspicious behavior detected"Additive Risk Scoring
Multiple risk commands in a query are additive (capped at 100). Here are practical examples:
1. Suspicious Login Activity
-- Comprehensive login risk assessment
event_type="authentication"
| risk score=10 entity=user factor="Login attempt"
| where auth_result="failure"
| risk score=20 factor="Failed login"
| where src_ip != /^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)/
| risk score=25 factor="External IP"
| eval hour = tonumber(strftime(timestamp, "%H"))
| where hour < 6 OR hour > 22
| risk score=20 factor="Off-hours access"
| where risk_score >= 50
| table timestamp, user, src_ip, auth_result, risk_score, risk_factors2. Rare Process with Network Activity
-- Detect uncommon processes making network connections
event_type="process_start" dest_port > 0
| risk score=15 entity=dest_host factor="Process with network"
| prevalence hash_prevalence < 3
| risk score=35 factor="Rare process hash"
| where dest_port != 80 AND dest_port != 443
| risk score=20 factor="Non-standard port"
| where risk_score >= 50
| table timestamp, dest_host, process_name, file_hash, dest_ip, dest_port, risk_score3. Data Exfiltration Indicators
-- Identify potential data exfiltration patterns
event_type="network" bytes_out > 10000000
| risk score=20 entity=src_ip factor="Large outbound transfer"
| where dest_port != 80 AND dest_port != 443
| risk score=15 factor="Non-web port"
| where dest_ip != /^(10\.|192\.168\.)/
| risk score=20 factor="External destination"
| eval hour = tonumber(strftime(timestamp, "%H"))
| where hour < 6 OR hour > 20
| risk score=25 factor="Off-hours transfer"
| where risk_score >= 60
| table timestamp, src_ip, dest_ip, dest_port, bytes_out, risk_score, risk_factors4. Suspicious Script Execution
-- Detect potentially malicious script execution
event_type="process_start"
| risk score=10 entity=user factor="Process execution"
| where process_name="wscript.exe" OR process_name="cscript.exe" OR process_name="mshta.exe"
| risk score=30 factor="Script interpreter"
| where command_line = /http:|https:|ftp:/
| risk score=35 factor="Remote script reference"
| where risk_score >= 50
| table timestamp, user, dest_host, process_name, command_line, risk_scoreRisk rules (the risk dataset)
Accumulated entity risk is a queryable nPL dataset: dataset=risk. A risk query returns one row per entity, with the same decayed 24-hour and 7-day windows shown on the Risk Analytics page:
| Column | Description |
|---|---|
entity | The scored entity (IP, user, hostname, …) |
entity_type | Entity type |
score_24h / score_7d | Decayed accumulated risk over the window |
raw_score_24h / raw_score_7d | Accumulated risk without TTL decay |
findings_24h / findings_7d | Number of contributing findings |
distinct_rules_24h / distinct_rules_7d | Distinct detection rules that contributed |
distinct_tactics_24h / distinct_tactics_7d | Distinct MITRE tactics represented |
last_finding_at | Timestamp of the most recent finding |
last_rule_name | Most recent rule that fired |
last_severity | Severity of the most recent finding |
Risk rules are detection rules
A risk rule is an ordinary scheduled detection rule whose dataset is risk. A minimal threshold rule:
// Dataset: risk — fire when an entity's 24h decayed score exceeds 100
* | where score_24h > 100The risk dataset returns one row per entity, so a bare where on the score columns is all a threshold rule needs — no stats or by grouping.
The real power is combining the threshold with the breadth columns — an entity that tripped multiple rules across multiple tactics is far stronger signal than one noisy rule re-firing:
// High-signal rule: risk built from at least 3 tactics and 2 distinct rules
* | where score_24h > 100
| where distinct_tactics_24h >= 3 and distinct_rules_24h >= 2// Slow-burn rule: sustained 7-day accumulation across multiple rules
* | where score_7d > 300
| where distinct_rules_7d >= 3Conditioning on distinct_tactics_* / distinct_rules_* cuts false positives dramatically compared to a bare score threshold. Project the columns you want on the alert with a trailing | table — the default rule uses | table entity, entity_type, score_24h, score_7d, distinct_rules_7d, last_rule_name.
The default risk rule
A default rule — "Accumulated risk threshold exceeded" (score_24h > 500 or score_7d > 1000) — ships out of the box.
The default rule ships in Live (non-alerting) mode: it evaluates and records matches, but does not page anyone. When you're ready for accumulated-risk alerts, flip it to Alerting — from Settings → Risk rules (see Risk Scoring Settings) or directly in the rule editor.
Alert cooldown
Detection rules support a per-rule alert cooldown (in minutes): once a rule alerts on an entity, it won't re-alert on it until the cooldown elapses — so a risky entity that stays above threshold doesn't page every evaluation cycle. The default risk rule ships with a 240-minute cooldown.
Constraints
A dataset=risk rule may not itself contribute risk: its risk_score is forced to 0, and the | risk command is rejected in its body. This prevents a feedback loop where a rule's own findings feed its input.
Under the hood, accumulated risk is computed from the findings stored in ClickHouse — the same findings you can search with source_type=findings. The risk dataset is the supported, decay-aware way to build risk rules on top of them.
Risk Analytics Dashboard
Overview Metrics
The Risk Analytics page shows:
- Total Entities: Count of entities with risk scores
- Risk Level Distribution: Breakdown by critical/high/medium/low
- Average Risk Score: Mean risk across all entities
- Finding Volume: Total number of risk-generating detections
Entity Risk Tracking
View detailed entity risk information:
- Risk Score: Current cumulative score
- Finding Count: Number of contributing detections
- Last Detection: Most recent rule that fired
- Last Seen: Timestamp of latest activity
- Risk Level: Color-coded classification
Time Window Analysis
Compare risk patterns across different time periods:
- 24-hour view: Recent high-priority threats
- 7-day view: Persistent or escalating risks
- Entity type filtering: Focus on IPs, users, or hosts
TTL Decay (Time-Based Scoring)
nano implements Google SecOps-style TTL (Time-To-Live) decay for entity risk scores. This ensures that recent security findings contribute more to an entity's risk score than older findings, prioritizing current threats over stale activity.
How TTL Decay Works
Risk scores are calculated from findings stored in the last 7 days. Each finding's contribution to the total score is multiplied by a decay factor based on its age:
| Finding Age | Decay Factor | Effect |
|---|---|---|
| 0-24 hours | 1.0 (100%) | Full weight - recent findings are most important |
| 1-3 days | 0.7 (70%) | Slight decay - still highly relevant |
| 3-5 days | 0.4 (40%) | Moderate decay - reduced contribution |
| 5-7 days | 0.2 (20%) | Heavy decay - minimal contribution |
| >7 days | 0.0 (0%) | Excluded - findings too old to be actionable |
Decay Calculation Example
Consider an entity with the following findings:
| Finding | Age | Raw Score | Decay Factor | Decayed Score |
|---|---|---|---|---|
| Finding A | 2 hours ago | 50 | 1.0 | 50 |
| Finding B | 12 hours ago | 30 | 1.0 | 30 |
| Finding C | 2 days ago | 80 | 0.7 | 56 |
| Finding D | 4 days ago | 60 | 0.4 | 24 |
| Finding E | 6 days ago | 40 | 0.2 | 8 |
24h Window Results:
- Raw Score: 50 + 30 = 80
- Decayed Score: 50 + 30 = 80 (same, all findings have 1.0 factor)
- Finding Count: 2
7d Window Results:
- Raw Score: 50 + 30 + 80 + 60 + 40 = 260
- Decayed Score: 50 + 30 + 56 + 24 + 8 = 168
- Finding Count: 5
Understanding the Risk Table
The Risk Analytics table displays both 24-hour and 7-day metrics:
| Column | Description |
|---|---|
| Entity | The IP, user, hostname, or other entity being tracked |
| Type | Entity type (ip, user, hostname, email) |
| 24h Score | Decayed risk score from findings in the last 24 hours |
| 7d Score | Decayed risk score from findings in the last 7 days |
| Findings | Finding count shown as "24h / 7d" format |
| Last Detection | Most recent detection rule that fired |
| Last Seen | Timestamp of the most recent finding |
Important Notes:
- If 24h Score = 0 but 7d Score > 0, it means all findings are older than 24 hours
- The 7d Score will always be ≥ the 24h Score (7d includes 24h findings plus older ones)
- Findings older than 7 days are automatically excluded from all calculations
Why Use TTL Decay?
- Prioritize Active Threats: Entities with recent activity appear at the top
- Automatic Aging: Old incidents don't permanently inflate risk scores
- Reduce False Positives: Stale findings have less impact on current risk assessment
- No Manual Cleanup: Scores naturally decay without intervention
Configuring Decay Factors
Decay factors are configurable in Settings > Risk Scoring > TTL Decay:
- Navigate to the Risk Scoring settings page
- Adjust the four decay sliders:
- 0-24 hours: Default 1.0 (full weight for recent findings)
- 1-3 days: Default 0.7 (slight decay)
- 3-5 days: Default 0.4 (moderate decay)
- 5-7 days: Default 0.2 (heavy decay)
- Click "Save Decay Settings"
Tuning Recommendations:
| Environment | Decay Strategy |
|---|---|
| High-velocity SOC | Aggressive decay (lower values) to focus on real-time threats |
| Compliance-focused | Conservative decay (higher values) to maintain longer visibility |
| Default | Balanced settings work well for most environments |
API Endpoints for Decay
# Get current decay configuration
curl -X GET "/api/settings/risk-decay"
# Update decay configuration
curl -X PUT "/api/settings/risk-decay" \
-H "Content-Type: application/json" \
-d '{
"decay_0_24h": 1.0,
"decay_1_3d": 0.7,
"decay_3_5d": 0.4,
"decay_5_7d": 0.2
}'
# Get time-windowed risk scores with decay applied
curl -X GET "/api/risk/time-windowed?limit=100"Decay vs. Raw Scores
The system tracks both decayed and raw scores:
- Decayed Scores (primary): Used for sorting, display, and alerting
- Raw Scores (available via tooltip): Sum of all finding scores without decay applied
Hover over any score in the Risk table to see both values in the tooltip.
Configuration and Settings
Global Risk Weight
Adjust the global risk multiplier in Settings > Risk Scoring:
- 1.0: Full risk scoring (aggressive)
- 0.75: Moderate risk scoring
- 0.5: Conservative risk scoring
- 0.25: Minimal risk scoring
- 0.0: Risk scoring disabled
Risk Score Validation
All risk scores are validated to ensure data integrity:
- Range: Must be 0-100 (inclusive)
- Type: Must be integers
- Modifiers: Conditions must use valid operators
- Weights: Global weight must be 0.0-1.0
API Integration
Risk Entities Endpoint
Query risky entities programmatically:
# Get top 50 risky entities in last 24h
curl -X GET "/api/risk/entities?window=24h&limit=50"
# Filter by entity type and minimum score
curl -X GET "/api/risk/entities?entity_type=src_ip&min_score=60"Risk Overview Endpoint
Get risk analytics summary:
# Get overview statistics
curl -X GET "/api/risk/overview"Clear Risk Scores
Reset entity risk scores when appropriate:
# Clear specific entity
curl -X POST "/api/risk/clear" \
-H "Content-Type: application/json" \
-d '{"entity": "192.168.1.100", "entity_type": "src_ip", "reason": "False positive"}'
# Clear all risk scores (admin only)
curl -X POST "/api/risk/clear-all"Time-Windowed Risk Queries
Get accurate time-based risk calculations:
# Get entities with high 24h or 7d risk
curl -X GET "/api/risk/time-windowed?min_score_24h=80&min_score_7d=100"
# Get entities exceeding thresholds (for alerting)
curl -X GET "/api/risk/thresholds?threshold_24h=100&threshold_7d=150"Best Practices
Risk Score Design
- Start Conservative: Begin with lower base scores and adjust based on false positive rates
- Use Modifiers Strategically: Focus on conditions that genuinely increase threat likelihood
- Consider Context: Account for normal business operations and expected behaviors
- Test Thoroughly: Validate risk logic with historical data before production deployment
Entity Selection
- Choose Meaningful Entities: Select fields that represent actionable security contexts
- Prioritize by Impact: IP addresses for network threats, users for insider threats, hosts for endpoint security
- Avoid Over-Granularity: Don't track risk for fields that change frequently (like timestamps)
Threshold Tuning
- Monitor Alert Volume: Adjust thresholds to maintain manageable alert rates
- Account for Environment Size: Larger environments may need higher thresholds
- Consider Time Windows: Shorter windows need higher thresholds to avoid noise
- Review Regularly: Risk patterns change as threats evolve
Operational Workflow
- Daily Review: Check high-risk entities each day
- Weekly Analysis: Look for trending risks and persistent threats
- Monthly Tuning: Adjust thresholds and modifiers based on observed patterns
- Incident Integration: Use risk scores to prioritize incident response
Advanced Use Cases
Risk-Based SOAR Integration
Integrate risk scores with Security Orchestration platforms:
# Example: Escalate high-risk entities to SOAR
def check_risk_escalation():
high_risk_entities = api.get_risk_entities(min_score=80, window="24h")
for entity in high_risk_entities:
if entity.risk_score > 90:
soar.create_high_priority_case(entity)
elif entity.risk_score > 80:
soar.create_medium_priority_case(entity)Dynamic Blocking
Automatically act on high-risk entities by pairing a risk rule with your automation:
// Dataset: risk — aggressive threshold for automated response
* | where score_24h > 150Route the resulting alerts to your firewall or SOAR automation via a notification channel (e.g. a generic HMAC-signed webhook).
Risk Trending Analysis
Track risk evolution over time:
-- Weekly risk trend analysis
source_type=findings
| bin span=1d
| stats sum(risk_score) as daily_risk by risk_entity, _time
| sort risk_entity, _time
| eval risk_trend = daily_risk - lag(daily_risk, 1)
| where risk_trend > 50 -- Entities with increasing riskCompliance Reporting
Generate risk-based compliance reports:
-- Monthly risk summary for compliance
source_type=findings
| where _time >= relative_time(now(), "-30d")
| stats sum(risk_score) as total_risk,
avg(risk_score) as avg_risk,
max(risk_score) as peak_risk,
count() as incident_count by risk_entity
| eval risk_category = case(
total_risk > 500, "High Risk",
total_risk > 200, "Medium Risk",
total_risk > 50, "Low Risk",
1=1, "Minimal Risk"
)
| stats count() by risk_category