Install
$ agentstack add skill-unitoneai-securityskills-siem-rules Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Possible prompt-injection directive.
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
SIEM Detection Rule Development
> Framework: MITRE ATT&CK v16 > Role: SOC Analyst, Security Engineer > Time: 20-40 min per rule > Output: Production-ready KQL or SPL detection query, correlation rule logic, tuning parameters
1. When to Use
If a target is provided via arguments, focus the review on: $ARGUMENTS
Invoke this skill when any of the following conditions are met:
- SIEM rule authoring -- A new detection rule needs to be written in KQL (Microsoft Sentinel) or SPL (Splunk) for a specific threat scenario.
- Sigma rule conversion review -- A Sigma rule has been converted to KQL or SPL and needs manual review, optimization, or platform-specific tuning.
- Alert threshold tuning -- An existing rule is generating too many false positives or too few true positives and requires threshold or logic adjustments.
- Correlation rule design -- Multiple log sources need to be joined or correlated to produce a higher-fidelity detection.
- Detection rule lifecycle management -- Rules need to be reviewed, versioned, promoted, deprecated, or retired following a structured lifecycle.
- Query performance optimization -- A detection query is consuming excessive resources or timing out and requires optimization.
Do not use when: The task is writing platform-agnostic Sigma rules (use detection-engineering), performing alert triage on a fired alert (use alert-triage), or analyzing raw logs for forensic investigation (use log-analysis).
2. Context the Agent Needs
Before beginning, gather or confirm:
- [ ] Target SIEM platform: Microsoft Sentinel (KQL) or Splunk (SPL).
- [ ] Detection objective: What behavior or threat is being detected? Include ATT&CK technique ID if known.
- [ ] Available data tables/indexes: Which log tables (Sentinel) or indexes (Splunk) contain the relevant data?
- [ ] Environment baseline: Normal volume and patterns for the data source (e.g., average daily failed logon count, typical admin logon hours).
- [ ] Alert priority and response: Desired severity level and expected analyst response procedure.
- [ ] Performance constraints: Query time window, maximum execution time, and scheduled frequency.
- [ ] Existing rules: Any current rules covering similar detections that may overlap or conflict.
3. Process
Step 1: Detection Pattern Selection
Select the appropriate detection logic pattern based on the threat being detected.
Core detection patterns:
| Pattern | Use Case | Complexity | |---------|----------|------------| | Simple match | Known-bad indicators, specific event IDs | Low | | Threshold | Brute force, scanning, volume anomalies | Low-Medium | | Time window | Rapid successive events, timing-based attacks | Medium | | Aggregation | Group-by analysis, frequency counting | Medium | | Correlation | Multi-table joins, multi-stage attacks | High | | Behavioral baseline | Deviation from normal, first-seen analysis | High | | Impossible travel | Geographically implausible authentication | High |
Step 2: Write the Detection Query
KQL (Microsoft Sentinel) Syntax Reference
Common Sentinel tables:
| Table | Data Source | Key Fields | |-------|------------|------------| | SigninLogs | Azure AD interactive sign-ins | UserPrincipalName, ResultType, IPAddress, Location | | AADNonInteractiveUserSignInLogs | Azure AD non-interactive sign-ins | Same as SigninLogs | | SecurityEvent | Windows Security Event Log | EventID, Account, Computer, Activity | | Syslog | Linux syslog | SyslogMessage, ProcessName, Facility, SeverityLevel | | DeviceProcessEvents | Microsoft Defender for Endpoint | FileName, ProcessCommandLine, InitiatingProcessFileName | | DeviceNetworkEvents | MDE network events | RemoteIP, RemotePort, RemoteUrl | | AzureActivity | Azure control plane | OperationNameValue, Caller, ResourceGroup | | CommonSecurityLog | CEF-format logs (firewalls, proxies) | DeviceAction, SourceIP, DestinationIP | | ThreatIntelligenceIndicator | Threat intel feeds | NetworkIP, DomainName, Url, ExpirationDateTime | | OfficeActivity | Microsoft 365 audit logs | Operation, UserId, ClientIP |
Detection: Brute Force -- Password Spray (KQL)
ATT&CK: T1110.003 -- Brute Force: Password Spraying
// Password Spray Detection -- Multiple accounts, same source, failed logins
// ATT&CK: T1110.003 -- Brute Force: Password Spraying
// Sentinel Table: SigninLogs
// Threshold: 10+ distinct accounts with failed auth from same IP in 10 minutes
let threshold_accounts = 10;
let threshold_window = 10m;
SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType in ("50126", "50053", "50055", "50056") // Failed password, locked, expired, etc.
| summarize
DistinctAccounts = dcount(UserPrincipalName),
AttemptCount = count(),
TargetAccounts = make_set(UserPrincipalName, 50),
FirstAttempt = min(TimeGenerated),
LastAttempt = max(TimeGenerated)
by IPAddress, bin(TimeGenerated, threshold_window)
| where DistinctAccounts >= threshold_accounts
| extend AttackDuration = LastAttempt - FirstAttempt
| project
TimeGenerated,
IPAddress,
DistinctAccounts,
AttemptCount,
AttackDuration,
TargetAccounts
| sort by DistinctAccounts desc
Key ResultType values (Azure AD):
| ResultType | Meaning | |------------|---------| | 0 | Success | | 50126 | Invalid username or password | | 50053 | Account locked | | 50055 | Password expired | | 50056 | Invalid or null password | | 50057 | Account disabled | | 50074 | MFA required | | 50076 | MFA prompt not satisfied | | 53003 | Conditional access block |
Detection: Impossible Travel (KQL)
ATT&CK: T1078 -- Valid Accounts
// Impossible Travel Detection
// ATT&CK: T1078 -- Valid Accounts (compromised credentials)
// Detects successful logins from geographically distant locations within
// a time window that makes physical travel impossible
let travel_speed_kmh = 900; // Maximum plausible travel speed (commercial flight)
let min_distance_km = 500; // Minimum distance to flag (avoids VPN/proxy noise)
let time_window = 24h;
SigninLogs
| where TimeGenerated > ago(time_window)
| where ResultType == 0 // Successful logins only
| where isnotempty(LocationDetails.geoCoordinates.latitude)
| extend
Latitude = todouble(LocationDetails.geoCoordinates.latitude),
Longitude = todouble(LocationDetails.geoCoordinates.longitude),
City = tostring(LocationDetails.city),
Country = tostring(LocationDetails.countryOrRegion)
| sort by UserPrincipalName asc, TimeGenerated asc
| serialize
| extend
PrevLatitude = prev(Latitude, 1),
PrevLongitude = prev(Longitude, 1),
PrevTime = prev(TimeGenerated, 1),
PrevCity = prev(City, 1),
PrevCountry = prev(Country, 1),
PrevUser = prev(UserPrincipalName, 1)
| where UserPrincipalName == PrevUser
| extend
TimeDiffHours = datetime_diff('minute', TimeGenerated, PrevTime) / 60.0,
// Haversine formula for distance calculation
DistanceKm = 2 * 6371 * asin(sqrt(
sin(radians((Latitude - PrevLatitude) / 2)) * sin(radians((Latitude - PrevLatitude) / 2)) +
cos(radians(PrevLatitude)) * cos(radians(Latitude)) *
sin(radians((Longitude - PrevLongitude) / 2)) * sin(radians((Longitude - PrevLongitude) / 2))
))
| where DistanceKm >= min_distance_km
| extend RequiredSpeedKmh = iff(TimeDiffHours > 0, DistanceKm / TimeDiffHours, real(99999))
| where RequiredSpeedKmh > travel_speed_kmh
| project
TimeGenerated,
UserPrincipalName,
CurrentLocation = strcat(City, ", ", Country),
PreviousLocation = strcat(PrevCity, ", ", PrevCountry),
TimeDiffHours = round(TimeDiffHours, 1),
DistanceKm = round(DistanceKm, 0),
RequiredSpeedKmh = round(RequiredSpeedKmh, 0),
IPAddress
Detection: Privileged Account Usage Outside Business Hours (KQL)
ATT&CK: T1078.002 -- Valid Accounts: Domain Accounts
// Privileged Account Usage Outside Business Hours
// ATT&CK: T1078.002 -- Valid Accounts: Domain Accounts
// Detects privileged account logins outside defined business hours
let business_start = 7; // 7 AM
let business_end = 19; // 7 PM
let weekend_days = dynamic(["Saturday", "Sunday"]);
let privileged_patterns = dynamic(["admin", "svc-", "sa-", "break-glass", "emergency"]);
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == 0
| extend
HourOfDay = hourofday(TimeGenerated),
DayOfWeek = dayofweek(TimeGenerated),
DayName = case(
dayofweek(TimeGenerated) == 0d, "Sunday",
dayofweek(TimeGenerated) == 1d, "Monday",
dayofweek(TimeGenerated) == 2d, "Tuesday",
dayofweek(TimeGenerated) == 3d, "Wednesday",
dayofweek(TimeGenerated) == 4d, "Thursday",
dayofweek(TimeGenerated) == 5d, "Friday",
dayofweek(TimeGenerated) == 6d, "Saturday",
"Unknown")
| where HourOfDay = business_end
or DayName in (weekend_days)
| where UserPrincipalName has_any (privileged_patterns)
| project
TimeGenerated,
UserPrincipalName,
HourOfDay,
DayName,
IPAddress,
AppDisplayName,
LocationDetails.city,
LocationDetails.countryOrRegion,
ConditionalAccessStatus
SPL (Splunk) Syntax Reference
Common Splunk sourcetypes:
| Sourcetype | Data Source | Key Fields | |------------|------------|------------| | WinEventLog:Security | Windows Security Event Log | EventCode, AccountName, ComputerName | | WinEventLog:System | Windows System Event Log | EventCode, SourceName | | XmlWinEventLog:Microsoft-Windows-Sysmon/Operational | Sysmon | EventCode, Image, CommandLine, ParentImage | | linux_secure | /var/log/secure (RHEL/CentOS) | action, user, srcip | | linux_audit | auditd logs | type, uid, exe, key | | pan:traffic | Palo Alto firewall | srcip, destip, dest_port, action | | aws:cloudtrail | AWS CloudTrail | eventName, sourceIPAddress, userIdentity.arn | | o365:management:activity | Microsoft 365 | Operation, UserId, ClientIP |
Detection: Brute Force -- Password Spray (SPL)
ATT&CK: T1110.003 -- Brute Force: Password Spraying
`comment("Password Spray Detection -- ATT&CK T1110.003")`
`comment("Detects multiple distinct accounts with failed auth from same source IP")`
index=wineventlog sourcetype="WinEventLog:Security" EventCode=4625
| bin _time span=10m
| stats
dc(TargetUserName) as distinct_accounts,
count as attempt_count,
values(TargetUserName) as target_accounts,
earliest(_time) as first_attempt,
latest(_time) as last_attempt
by IpAddress, _time
| where distinct_accounts >= 10
| eval attack_duration_sec = last_attempt - first_attempt
| eval first_attempt = strftime(first_attempt, "%Y-%m-%d %H:%M:%S")
| eval last_attempt = strftime(last_attempt, "%Y-%m-%d %H:%M:%S")
| sort - distinct_accounts
| table _time, IpAddress, distinct_accounts, attempt_count, attack_duration_sec, target_accounts
Detection: Impossible Travel (SPL)
ATT&CK: T1078 -- Valid Accounts
`comment("Impossible Travel Detection -- ATT&CK T1078")`
`comment("Detects logins from geographically distant locations within implausible time")`
index=o365 sourcetype="o365:management:activity" Operation=UserLoggedIn
| iplocation ClientIP
| where isnotnull(lat) AND isnotnull(lon)
| sort 0 UserId _time
| streamstats current=f window=1
last(lat) as prev_lat,
last(lon) as prev_lon,
last(_time) as prev_time,
last(City) as prev_city,
last(Country) as prev_country,
last(ClientIP) as prev_ip
by UserId
| where isnotnull(prev_lat)
| eval time_diff_hours = (_time - prev_time) / 3600
| eval distance_km = 2 * 6371 * asin(sqrt(
pow(sin((lat - prev_lat) * pi() / 360), 2) +
cos(prev_lat * pi() / 180) * cos(lat * pi() / 180) *
pow(sin((lon - prev_lon) * pi() / 360), 2)
))
| where distance_km >= 500
| eval required_speed_kmh = if(time_diff_hours > 0, distance_km / time_diff_hours, 99999)
| where required_speed_kmh > 900
| eval current_location = City . ", " . Country
| eval previous_location = prev_city . ", " . prev_country
| table _time, UserId, current_location, previous_location,
time_diff_hours, distance_km, required_speed_kmh, ClientIP, prev_ip
Detection: Privileged Account Usage Outside Business Hours (SPL)
ATT&CK: T1078.002 -- Valid Accounts: Domain Accounts
`comment("Privileged Account Off-Hours Logon -- ATT&CK T1078.002")`
`comment("Detects privileged account logins outside business hours")`
index=wineventlog sourcetype="WinEventLog:Security" EventCode=4624
(TargetUserName="admin*" OR TargetUserName="svc-*" OR TargetUserName="sa-*")
| eval hour = strftime(_time, "%H")
| eval day_of_week = strftime(_time, "%A")
| where (hour = 19)
OR (day_of_week="Saturday" OR day_of_week="Sunday")
| stats
count as logon_count,
values(IpAddress) as source_ips,
values(WorkstationName) as workstations,
earliest(_time) as first_seen,
latest(_time) as last_seen
by TargetUserName, LogonType
| eval first_seen = strftime(first_seen, "%Y-%m-%d %H:%M:%S")
| eval last_seen = strftime(last_seen, "%Y-%m-%d %H:%M:%S")
| eval logon_type_desc = case(
LogonType=2, "Interactive",
LogonType=3, "Network",
LogonType=4, "Batch",
LogonType=5, "Service",
LogonType=7, "Unlock",
LogonType=8, "NetworkCleartext",
LogonType=9, "NewCredentials",
LogonType=10, "RemoteInteractive",
LogonType=11, "CachedInteractive",
true(), "Unknown"
)
| sort - logon_count
| table TargetUserName, logon_type_desc, logon_count, source_ips, workstations, first_seen, last_seen
Step 3: Correlation Rule Design
Correlation rules join data across multiple log sources or detect multi-stage attack sequences.
Correlation pattern: KQL join example -- Failed Logins Followed by Success
// Successful login preceded by multiple failures (credential guessing success)
// ATT&CK: T1110 -- Brute Force
let failure_threshold = 5;
let correlation_window = 15m;
let failures = SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType != 0
| summarize
FailureCount = count(),
FailureCodes = make_set(ResultType),
FirstFailure = min(TimeGenerated)
by UserPrincipalName, IPAddress;
let successes = SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType == 0
| project SuccessTime = TimeGenerated, UserPrincipalName, IPAddress,
AppDisplayName, LocationDetails;
failures
| where FailureCount >= failure_threshold
| join kind=inner (successes) on UserPrincipalName, IPAddress
| where SuccessTime > FirstFailure
| where SuccessTime - FirstFailure = 3
| sort - distinct_hosts
| table _time, TargetUserName, distinct_hosts, logon_count, target_hosts, source_ips
Step 4: Alert Threshold Tuning
Tuning methodology:
- Baseline: Run the query in search mode for 7-30 days without alerting. Record the result count distribution.
- Statistical analysis: Calculate mean, median, and standard deviation of the daily/hourly result count.
- Threshold selection: Set the initial threshold at mean + 2 standard deviations to capture anomalous activity while filtering normal variance.
- Iterative tuning: After deployment, review alerts weekly for the first month. Adjust the threshold based on TP/FP ratio.
- Exclusion management: Add exclusions for confirmed legitimate activity. Document each exclusion with a ticket reference and review date.
Threshold tuning parameters:
| Parameter | Purpose | Example | |-----------|---------|---------| | count threshold | Minimum event count to trigger | >= 10 failed logins | | distinct count threshold | Minimum unique values | >= 5 distinct accounts | | time window | Aggregation period | 10m, 1h, 24h | | lookback period | Historical data to evaluate | ago(1h), ago(24h) | | `frequ
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: UnitOneAI
- Source: UnitOneAI/SecuritySkills
- License: MIT
- Homepage: https://www.unitone.ai
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.