Query Details
// =========================================================
// HUNT-12 | AD-GPO-Modification-Full-Audit-30d
// Description : Complete audit of all GPO-related changes
// (5136 on groupPolicyContainer objects),
// new GPO link events (5137), and GPO
// deletions (5141). Flags unauthorized
// editors, scope changes, and suspicious
// script/immediate task injections.
// Period : 30 days
// Use Case : GPO abuse detection, persistence via
// Group Policy, privilege escalation
// Tables : SecurityEvent
// =========================================================
let Period = 30d;
// Admin baseline: known accounts with legitimate GPO edit rights
let GpoAdmins = SecurityEvent
| where TimeGenerated > ago(90d)
| where EventID in (4728, 4732, 4756)
| where TargetUserName has_any ("Domain Admins", "Enterprise Admins",
"Group Policy Creator Owners",
"Administrators")
| summarize by GPOAdmin = tolower(tostring(column_ifexists("MemberName", "")));
// GPO attribute writes
let GPOWrites = SecurityEvent
| where TimeGenerated > ago(Period)
| where EventID == 5136
| where tostring(column_ifexists("ObjectClass", "")) =~ "groupPolicyContainer"
| extend
GPOName = extract(@"CN=\{([^}]+)\}", 0, ObjectName),
Actor = strcat(SubjectDomainName, "\\", SubjectUserName),
ActorNorm = tolower(SubjectUserName),
AttrName = tostring(column_ifexists("AttributeLDAPDisplayName", "")),
OldValue = OldValue,
NewValue = tostring(column_ifexists("AttributeValue", "")),
IsScriptPath = tostring(column_ifexists("AttributeValue", "")) has_any (".ps1", ".bat", ".cmd", ".vbs", ".js",
"scripts", "powershell", "cscript",
"wscript", "rundll32"),
IsOffHours = hourofday(TimeGenerated) < 7 or hourofday(TimeGenerated) >= 20;
// GPO object creations
let GPOCreations = SecurityEvent
| where TimeGenerated > ago(Period)
| where EventID == 5137
| where tostring(column_ifexists("ObjectClass", "")) =~ "groupPolicyContainer"
| extend
GPOName = extract(@"CN=\{([^}]+)\}", 0, ObjectName),
Actor = strcat(SubjectDomainName, "\\", SubjectUserName),
ActorNorm = tolower(SubjectUserName),
EventType = "GPOCreated";
// GPO deletions
let GPODeletions = SecurityEvent
| where TimeGenerated > ago(Period)
| where EventID == 5141
| where tostring(column_ifexists("ObjectClass", "")) =~ "groupPolicyContainer"
| extend
GPOName = extract(@"CN=\{([^}]+)\}", 0, ObjectName),
Actor = strcat(SubjectDomainName, "\\", SubjectUserName),
ActorNorm = tolower(SubjectUserName),
EventType = "GPODeleted";
// Identify unauthorized GPO editors
GPOWrites
| join kind=leftouter (GpoAdmins) on $left.ActorNorm == $right.GPOAdmin
| extend IsAuthorizedEditor = isnotempty(GPOAdmin)
| summarize
TotalEdits = count(),
UnauthorizedEdits = countif(not(IsAuthorizedEditor)),
ScriptPathChanges = countif(IsScriptPath),
OffHoursEdits = countif(IsOffHours),
UniqueEditors = make_set(Actor, 10),
UnauthorizedEditors = make_set_if(Actor, not(IsAuthorizedEditor), 10),
ChangedAttributes = make_set(AttrName, 20),
ScriptValues = make_set_if(NewValue, IsScriptPath, 10),
LastEdit = max(TimeGenerated),
FirstEdit = min(TimeGenerated)
by GPOName
| union (
GPOCreations
| join kind=leftouter (GpoAdmins) on $left.ActorNorm == $right.GPOAdmin
| extend IsAuthorizedEditor = isnotempty(GPOAdmin)
| summarize
TotalEdits = count(),
UnauthorizedEdits = countif(not(IsAuthorizedEditor)),
ScriptPathChanges = 0,
OffHoursEdits = 0,
UniqueEditors = make_set(Actor, 10),
UnauthorizedEditors = make_set_if(Actor, not(IsAuthorizedEditor), 10),
ChangedAttributes = pack_array("GPOCreated"),
ScriptValues = pack_array(""),
LastEdit = max(TimeGenerated),
FirstEdit = min(TimeGenerated)
by GPOName
)
| union (
GPODeletions
| join kind=leftouter (GpoAdmins) on $left.ActorNorm == $right.GPOAdmin
| extend IsAuthorizedEditor = isnotempty(GPOAdmin)
| summarize
TotalEdits = count(),
UnauthorizedEdits = countif(not(IsAuthorizedEditor)),
ScriptPathChanges = 0,
OffHoursEdits = 0,
UniqueEditors = make_set(Actor, 10),
UnauthorizedEditors = make_set_if(Actor, not(IsAuthorizedEditor), 10),
ChangedAttributes = pack_array("GPODeleted"),
ScriptValues = pack_array(""),
LastEdit = max(TimeGenerated),
FirstEdit = min(TimeGenerated)
by GPOName
)
| extend
RiskScore = (UnauthorizedEdits * 30)
+ (ScriptPathChanges * 40)
+ (OffHoursEdits * 10)
+ (TotalEdits * 2),
RiskLevel = case(
ScriptPathChanges >= 1 and UnauthorizedEdits >= 1,
"Critical - Script_Injected_By_Unauthorized_User",
ScriptPathChanges >= 1,
"High - Script_Injected_Into_GPO",
UnauthorizedEdits >= 1,
"High - Unauthorized_GPO_Modification",
OffHoursEdits >= 3,
"Medium - OffHours_GPO_Activity",
"Low"
)
| project
GPOName,
RiskLevel,
RiskScore,
TotalEdits,
UnauthorizedEdits,
ScriptPathChanges,
OffHoursEdits,
UnauthorizedEditors,
UniqueEditors,
ChangedAttributes,
ScriptValues,
FirstEdit,
LastEdit
| order by RiskScore desc
This query is designed to monitor and audit changes to Group Policy Objects (GPOs) over the past 30 days. It focuses on detecting unauthorized modifications, new GPO creations, and deletions, as well as identifying potentially suspicious activities such as script injections or changes made during off-hours. Here's a simple breakdown of what the query does:
Define the Monitoring Period: The query looks at events from the last 30 days.
Identify Authorized GPO Editors: It establishes a baseline of known accounts with legitimate rights to edit GPOs by looking at specific security events from the past 90 days.
Track GPO Changes:
Detect Unauthorized Activity: The query compares the actors making changes to the list of authorized editors to flag any unauthorized modifications.
Calculate Risk Scores: Each GPO is assigned a risk score based on the number and type of unauthorized edits, script injections, and off-hours activities. The risk level is categorized as Critical, High, Medium, or Low based on these factors.
Output: The results are presented in a table, sorted by risk score, showing details such as the GPO name, risk level, total edits, unauthorized edits, script changes, off-hours edits, and the list of unique and unauthorized editors involved.
This query is useful for detecting potential GPO abuse, persistence mechanisms, and privilege escalation attempts within an organization's network.

David Alonso
Released: March 24, 2026
Tables
Keywords
Operators