AgentStack
SKILL unreviewed MIT Self-run

Siem Rules

skill-unitoneai-securityskills-siem-rules · by UnitOneAI

>

No reviews yet
0 installs
16 views
0.0% view→install

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

⚠ Flagged

1 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.

Are you the author of Siem Rules? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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:

  1. Baseline: Run the query in search mode for 7-30 days without alerting. Record the result count distribution.
  2. Statistical analysis: Calculate mean, median, and standard deviation of the daily/hourly result count.
  3. Threshold selection: Set the initial threshold at mean + 2 standard deviations to capture anomalous activity while filtering normal variance.
  4. Iterative tuning: After deployment, review alerts weekly for the first month. Adjust the threshold based on TP/FP ratio.
  5. 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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.