Detection rules › Kusto
Large number of AD objects accessed by user
This query detects a user accessing a large number of Group and User objects from Active Directory which is outside the baseline of normal behavior for that particular user.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Discovery | T1087.002 Account Discovery: Domain Account |
| Collection | T1119 Automated Collection |
References
Event coverage
| Provider | Event | Title |
|---|---|---|
| Security-Auditing | Event ID 4662 | An operation was performed on an object. |
Rule body kusto
let timeframe = 2*1d;
let RuleId = "0108";
let DedupFields = dynamic(["TimeGenerated"]);
let lookback_days=14d; // Look back this many days to calculate baseline for maximum number of objects accessed per day.
let min_suspicious_count = 20000; // Only consider users looking up at least this many objects during the timeframe.
let suspicious_factor = 50; // Consider as suspicious if requesting more than suspicious_factor*daily maximum in lookback period.
let ADObjectAccess=(
SecurityEvent
| where EventID == 4662
| where not(Account endswith "$")
| where ObjectType in~ (
"%{bf967aba-0de6-11d0-a285-00aa003049e2}", // User.
"%{bf967a9c-0de6-11d0-a285-00aa003049e2}" // Group.
)
);
let AccessBaseline=(
ADObjectAccess
| where TimeGenerated <= ago(timeframe)
| where TimeGenerated >= ago(timeframe + lookback_days)
| summarize BaselineObjectCount=count() by Account, bin(TimeGenerated,1d)
| summarize PreviousMaxObjectPerDay=max(BaselineObjectCount) by Account
);
ADObjectAccess
| where ingestion_time() >= ago(timeframe)
| summarize ObjectCount=count(),TimeGenerated=min(TimeGenerated) by Account
| where ObjectCount > min_suspicious_count
| join kind=leftouter AccessBaseline on Account
| extend PreviousMaxObjectPerDay=coalesce(PreviousMaxObjectPerDay,0)
// Calculate what is considered a suspicious number for the given user.
| extend SuspiciousThreshold=max_of(PreviousMaxObjectPerDay*suspicious_factor,min_suspicious_count)
| where ObjectCount > SuspiciousThreshold
| project TimeGenerated, Account, ObjectCount, PreviousMaxObjectPerDay, SuspiciousThreshold
| extend AccountName=iif(Account contains @"\",tostring(split(Account,@"\")[1]),Account),AccountDomain=iif(Account contains @"\",tostring(split(Account,@"\")[0]),"")
// Begin environment-specific filter.
// End environment-specific filter.
// Begin de-duplication logic.
| extend DedupFieldValues=pack_all()
| mv-apply e=DedupFields to typeof(string) on (
extend DedupValue=DedupFieldValues[tostring(e)]
| order by e // Sorting is required to ensure make_list is deterministic.
| summarize DedupValues=make_list(DedupValue)
)
| extend DedupEntity=strcat_array(DedupValues, "|")
| project-away DedupFieldValues, DedupValues
| join kind=leftanti (
SecurityAlert
| where AlertName has RuleId and ProviderName has "ASI"
| where TimeGenerated >= ago(timeframe)
| extend DedupEntity = tostring(parse_json(tostring(parse_json(ExtendedProperties)["Custom Details"])).DedupEntity[0])
| project DedupEntity
) on DedupEntity
// End de-duplication logic.
Stages and Predicates
Parameters
let timeframe = 2*1d;
let RuleId = "0108";
let DedupFields = dynamic(["TimeGenerated"]);
let lookback_days = 14d;
let min_suspicious_count = 20000;
let suspicious_factor = 50;
Let binding: AccessBaseline
let AccessBaseline = (
ADObjectAccess
| where TimeGenerated <= ago(timeframe)
| where TimeGenerated >= ago(timeframe + lookback_days)
| summarize BaselineObjectCount=count() by Account, bin(TimeGenerated,1d)
| summarize PreviousMaxObjectPerDay=max(BaselineObjectCount) by Account
);
Derived from timeframe, lookback_days, ADObjectAccess.
The stages below define let ADObjectAccess (the rule's main pipeline source).
Stage 1: source
let ADObjectAccess
Stage 2: source
let AccessBaseline
Stage 3: source
SecurityEvent
Stage 4: where
| where EventID == 4662
Stage 5: where
| where not(Account endswith "$")
Stage 6: where
| where ObjectType in~ (
"%{bf967aba-0de6-11d0-a285-00aa003049e2}",
"%{bf967a9c-0de6-11d0-a285-00aa003049e2}"
)
The stages below run on ADObjectAccess (the outer pipeline).
Stage 7: where
ADObjectAccess
| where ingestion_time() >= ago(timeframe)
Stage 8: summarize
| summarize ObjectCount=count(),TimeGenerated=min(TimeGenerated) by Account
Stage 9: where
| where ObjectCount > min_suspicious_count
Stage 10: join
| join kind=leftouter AccessBaseline on Account
Stage 11: extend
| extend PreviousMaxObjectPerDay=coalesce(PreviousMaxObjectPerDay,0)
Stage 12: extend
| extend SuspiciousThreshold=max_of(PreviousMaxObjectPerDay*suspicious_factor,min_suspicious_count)
Stage 13: where
| where ObjectCount > SuspiciousThreshold
Stage 14: project
| project TimeGenerated, Account, ObjectCount, PreviousMaxObjectPerDay, SuspiciousThreshold
Stage 15: extend
| extend AccountName=iif(Account contains @"\",tostring(split(Account,@"\")[1]),Account),AccountDomain=iif(Account contains @"\",tostring(split(Account,@"\")[0]),"")
Stage 16: extend
| extend DedupFieldValues=pack_all()
Stage 17: kusto:mv-apply
| mv-apply e=DedupFields to typeof(string) on (
extend DedupValue=DedupFieldValues[tostring(e)]
| order by e
| summarize DedupValues=make_list(DedupValue)
)
Stage 18: extend
| extend DedupEntity=strcat_array(DedupValues, "|")
Stage 19: project-away
| project-away DedupFieldValues, DedupValues
Stage 20: join (negated)
| join kind=leftanti (
SecurityAlert
| where AlertName has RuleId and ProviderName has "ASI"
| where TimeGenerated >= ago(timeframe)
| extend DedupEntity = tostring(parse_json(tostring(parse_json(ExtendedProperties)["Custom Details"])).DedupEntity[0])
| project DedupEntity
) on DedupEntity
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
Account | ends_with | $ |
Account | ends_with | $ |
AlertName | match | 0108 |
ProviderName | match | ASI |
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
EventID | eq |
|
ObjectCount | gt |
|
ObjectType | in |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
Account | project |
ObjectCount | project |
PreviousMaxObjectPerDay | project |
SuspiciousThreshold | project |
TimeGenerated | project |
AccountDomain | extend |
AccountName | extend |
DedupEntity | extend |