Rules: remove the AutoLogon allow-list template ANALYZE-SEC-004

The ID stays retired; a test pins that it does not come back and that no rule
matches on the AutoLogon user name.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-11-11 22:05:18 +01:00
parent ccea1dacfe
commit e6191831bd
10 changed files with 26 additions and 219 deletions
-94
View File
@@ -1,94 +0,0 @@
{
"$schema": "../schema/analyze-rule.schema.json",
"ruleId": "ANALYZE-SEC-004",
"title": "AutoLogon enabled for an unexpected user",
"description": "Optional kiosk allow-list for AutoLogon. Fires when AutoLogon is enabled for a default user that is NOT on your configured list of approved kiosk / shared-device accounts. Disabled until configured because a normal Autopilot enrollment leaves Windows' own ESP auto-logon for the enrolling user (AutoLogon enabled, a default user, no plaintext password) — so AutoLogon-enabled alone is not graded by default; only a plaintext DefaultPassword on disk (ANALYZE-SEC-003) is. Enable this rule with your expected kiosk account name(s) if you want to surface AutoLogon configured for any other user.",
"severity": "warning",
"category": "security",
"version": "1.2.0",
"enabled": false,
"evaluateOn": [
"enrollment_end",
"whiteglove_sealed"
],
"trigger": "single",
"baseConfidence": 80,
"conditions": [
{
"signal": "autologon_enabled",
"source": "event_type",
"eventType": "autologon_analysis",
"dataField": "checks.autologon_enabled",
"operator": "equals",
"value": "true",
"required": true
},
{
"signal": "unexpected_autologon_user",
"source": "event_data",
"eventType": "autologon_analysis",
"dataField": "checks.default_user_name",
"operator": "not_in",
"value": "EXPECTED-KIOSK-USER-1,EXPECTED-KIOSK-USER-2",
"required": true
}
],
"confidenceThreshold": 40,
"explanation": "AutoLogon is enabled on this device for a user that is **not on your approved kiosk allow-list**. AutoLogon is legitimate for kiosk and shared-device scenarios, so this rule only warns when the configured default user differs from the account(s) you expect.\n\nThis rule is **off by default**. A normal user-driven Autopilot enrollment leaves Windows' own ESP auto-logon for the enrolling user (`AutoAdminLogon=1`, a `DefaultUserName`, an `AutoLogonSID`, and no plaintext password) — registry-indistinguishable from a kiosk AutoLogon — so AutoLogon-enabled alone is not graded. The only AutoLogon state graded by default is a plaintext `DefaultPassword` on disk (**ANALYZE-SEC-003**).\n\nTo use this rule: configure the allow-list with your approved kiosk account name(s) (comma-separated, case-insensitive exact match against the Winlogon `DefaultUserName`) and enable it. AutoLogon for any other user will then warn.\n\nNote: an AutoLogon with no explicit default user (e.g. ForceAutoLogon only) is treated as unexpected and will warn.",
"remediation": [
{
"title": "Confirm the AutoLogon user",
"steps": [
"Open the autologon_analysis event in the session timeline and check the default user/domain",
"If the user is an approved kiosk account, add it to this rule's allow-list",
"If the user is unexpected, treat it as a possible enrollment/OOBE manipulation and investigate what wrote the Winlogon keys"
]
},
{
"title": "Disable AutoLogon if not required",
"steps": [
"Set HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\AutoAdminLogon to 0",
"Remove DefaultPassword (and any Sysinternals LSA secret) if present",
"For kiosk scenarios, prefer Assigned Access / kiosk profiles over raw AutoLogon where possible"
]
}
],
"relatedDocs": [
{
"title": "Microsoft: Configure Windows to automatically sign in (AutoLogon)",
"url": "https://learn.microsoft.com/en-us/troubleshoot/windows-server/user-profiles-and-logon/turn-on-automatic-logon"
}
],
"presentation": {
"role": "advisory",
"summary": "AutoLogon is enabled for an unexpected user: {{unexpected_autologon_user.value}}",
"subject": {
"kind": "account",
"key": "{{unexpected_autologon_user.value}}"
},
"facts": [
{
"label": "AutoLogon user",
"value": "{{unexpected_autologon_user.value}}",
"format": "code"
}
]
},
"tags": [
"security",
"autologon",
"winlogon",
"kiosk",
"allow-list"
],
"templateVariables": [
{
"name": "allowed_autologon_users",
"label": "Allowed AutoLogon users (kiosk accounts)",
"description": "Comma-separated list of approved kiosk / shared-device account names that are expected to use AutoLogon (case-insensitive exact match against the Winlogon DefaultUserName, e.g. 'KioskUser' or 'Reception,LobbyPC'). AutoLogon for any other user raises a warning.",
"conditionIndex": 1,
"field": "value",
"placeholder": "EXPECTED-KIOSK-USER-1,EXPECTED-KIOSK-USER-2"
}
]
}
-93
View File
@@ -4926,99 +4926,6 @@
"plaintext-password"
]
},
{
"ruleId": "ANALYZE-SEC-004",
"title": "AutoLogon enabled for an unexpected user",
"description": "Optional kiosk allow-list for AutoLogon. Fires when AutoLogon is enabled for a default user that is NOT on your configured list of approved kiosk / shared-device accounts. Disabled until configured because a normal Autopilot enrollment leaves Windows' own ESP auto-logon for the enrolling user (AutoLogon enabled, a default user, no plaintext password) — so AutoLogon-enabled alone is not graded by default; only a plaintext DefaultPassword on disk (ANALYZE-SEC-003) is. Enable this rule with your expected kiosk account name(s) if you want to surface AutoLogon configured for any other user.",
"severity": "warning",
"category": "security",
"version": "1.2.0",
"enabled": false,
"evaluateOn": [
"enrollment_end",
"whiteglove_sealed"
],
"trigger": "single",
"baseConfidence": 80,
"conditions": [
{
"signal": "autologon_enabled",
"source": "event_type",
"eventType": "autologon_analysis",
"dataField": "checks.autologon_enabled",
"operator": "equals",
"value": "true",
"required": true
},
{
"signal": "unexpected_autologon_user",
"source": "event_data",
"eventType": "autologon_analysis",
"dataField": "checks.default_user_name",
"operator": "not_in",
"value": "EXPECTED-KIOSK-USER-1,EXPECTED-KIOSK-USER-2",
"required": true
}
],
"confidenceThreshold": 40,
"explanation": "AutoLogon is enabled on this device for a user that is **not on your approved kiosk allow-list**. AutoLogon is legitimate for kiosk and shared-device scenarios, so this rule only warns when the configured default user differs from the account(s) you expect.\n\nThis rule is **off by default**. A normal user-driven Autopilot enrollment leaves Windows' own ESP auto-logon for the enrolling user (`AutoAdminLogon=1`, a `DefaultUserName`, an `AutoLogonSID`, and no plaintext password) — registry-indistinguishable from a kiosk AutoLogon — so AutoLogon-enabled alone is not graded. The only AutoLogon state graded by default is a plaintext `DefaultPassword` on disk (**ANALYZE-SEC-003**).\n\nTo use this rule: configure the allow-list with your approved kiosk account name(s) (comma-separated, case-insensitive exact match against the Winlogon `DefaultUserName`) and enable it. AutoLogon for any other user will then warn.\n\nNote: an AutoLogon with no explicit default user (e.g. ForceAutoLogon only) is treated as unexpected and will warn.",
"remediation": [
{
"title": "Confirm the AutoLogon user",
"steps": [
"Open the autologon_analysis event in the session timeline and check the default user/domain",
"If the user is an approved kiosk account, add it to this rule's allow-list",
"If the user is unexpected, treat it as a possible enrollment/OOBE manipulation and investigate what wrote the Winlogon keys"
]
},
{
"title": "Disable AutoLogon if not required",
"steps": [
"Set HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\AutoAdminLogon to 0",
"Remove DefaultPassword (and any Sysinternals LSA secret) if present",
"For kiosk scenarios, prefer Assigned Access / kiosk profiles over raw AutoLogon where possible"
]
}
],
"relatedDocs": [
{
"title": "Microsoft: Configure Windows to automatically sign in (AutoLogon)",
"url": "https://learn.microsoft.com/en-us/troubleshoot/windows-server/user-profiles-and-logon/turn-on-automatic-logon"
}
],
"presentation": {
"role": "advisory",
"summary": "AutoLogon is enabled for an unexpected user: {{unexpected_autologon_user.value}}",
"subject": {
"kind": "account",
"key": "{{unexpected_autologon_user.value}}"
},
"facts": [
{
"label": "AutoLogon user",
"value": "{{unexpected_autologon_user.value}}",
"format": "code"
}
]
},
"tags": [
"security",
"autologon",
"winlogon",
"kiosk",
"allow-list"
],
"templateVariables": [
{
"name": "allowed_autologon_users",
"label": "Allowed AutoLogon users (kiosk accounts)",
"description": "Comma-separated list of approved kiosk / shared-device account names that are expected to use AutoLogon (case-insensitive exact match against the Winlogon DefaultUserName, e.g. 'KioskUser' or 'Reception,LobbyPC'). AutoLogon for any other user raises a warning.",
"conditionIndex": 1,
"field": "value",
"placeholder": "EXPECTED-KIOSK-USER-1,EXPECTED-KIOSK-USER-2"
}
]
},
{
"ruleId": "ANALYZE-SEC-005",
"title": "Unexpected Provisioning Package",
@@ -32,7 +32,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers
/// This analyzer reports facts ONLY, always at <see cref="EventSeverity.Info"/>. It never reads
/// or emits the <c>DefaultPassword</c> value — only its presence. Whether an AutoLogon is a
/// problem is judged by backend analyze-rules (ANALYZE-SEC-003 = plaintext DefaultPassword on
/// disk → escalated; ANALYZE-SEC-004 = optional kiosk allow-list template).
/// disk → escalated).
/// </para>
/// <para>
/// <b>Trigger</b>: NOT at agent start — the Winlogon keys are written later by a provisioning
@@ -247,8 +247,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers
/// <summary>
/// A sign-in address is reported as its domain only. Local account names (kiosk,
/// defaultuser0) and the provisioning placeholders stay readable — the allow-list rule
/// matches on them.
/// defaultuser0) and the provisioning placeholders stay readable.
/// </summary>
internal static string ReduceUserName(string? userName)
{
@@ -27,7 +27,7 @@ public class AnalyzeRulesFunctionAuthorEndpointTests
{
private const string TenantId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
private const string RuleId = "ANALYZE-CUSTOM-777";
private const string TemplateRuleId = "ANALYZE-SEC-004";
private const string TemplateRuleId = "ANALYZE-SEC-006";
private sealed class FakeHttpResponseData : HttpResponseData
{
@@ -177,7 +177,7 @@ public class AnalyzeRulesFunctionAuthorEndpointTests
var template = new AnalyzeRule
{
RuleId = TemplateRuleId,
Title = "AutoLogon enabled for an unexpected user",
Title = "Unexpected Provisioning Package (Custom Allow-List)",
Description = "Template",
Severity = "warning",
Category = "security",
@@ -186,7 +186,7 @@ public class AnalyzeRulesFunctionAuthorEndpointTests
Author = "Autopilot Monitor",
Conditions = new List<RuleCondition>
{
new() { Signal = "autologon", Source = "event_type", EventType = "autologon_analysis", Operator = "equals", Value = "PLACEHOLDER", Required = true }
new() { Signal = "unexpected_package", Source = "event_type", EventType = "provisioning_package_scan", Operator = "equals", Value = "PLACEHOLDER", Required = true }
},
TemplateVariables = new List<TemplateVariable>
{
@@ -194,7 +194,7 @@ public class AnalyzeRulesFunctionAuthorEndpointTests
},
};
var (function, stored) = BuildFunction(globalRule: template);
var req = BuildRequest(AlicePrincipal(), new { variables = new Dictionary<string, string> { ["allowed"] = "KIOSK-1" } });
var req = BuildRequest(AlicePrincipal(), new { variables = new Dictionary<string, string> { ["allowed"] = "Contoso.Recovery" } });
var response = await function.CreateFromTemplate(req, TemplateRuleId);
@@ -111,17 +111,12 @@ public class BuiltInRulesTests
Assert.Equal("high", sec003.Severity);
Assert.Contains(sec003.Conditions, c => c.DataField == "checks.default_password_present");
// SEC-004: kiosk allow-list TEMPLATE — disabled until configured.
var sec004 = rules.FirstOrDefault(r => r.RuleId == "ANALYZE-SEC-004");
Assert.NotNull(sec004);
Assert.False(sec004!.Enabled, "ANALYZE-SEC-004 is a template and must ship disabled");
Assert.NotNull(sec004.TemplateVariables);
var tv = Assert.Single(sec004.TemplateVariables);
Assert.Equal("allowed_autologon_users", tv.Name);
Assert.Equal(1, tv.ConditionIndex);
Assert.Equal("value", tv.Field);
Assert.Equal("not_in", sec004.Conditions[1].Operator);
Assert.Equal("checks.default_user_name", sec004.Conditions[1].DataField);
// SEC-004 (kiosk allow-list template) was removed. The ID stays retired, and no surviving
// rule may match on the AutoLogon user name: the event reports a sign-in address as its
// domain only, so an exact-match list no longer fits that field.
Assert.DoesNotContain(rules, r => r.RuleId == "ANALYZE-SEC-004");
Assert.DoesNotContain(rules, r =>
r.Conditions != null && r.Conditions.Any(c => c.DataField == "checks.default_user_name"));
}
[Fact]
@@ -68,7 +68,7 @@ public class RuleSubmissionServiceTests
MarkSessionAsFailed = true,
Notify = true,
NotifyChannelIds = new List<string> { "chan-1" },
DerivedFromTemplateRuleId = "ANALYZE-SEC-004",
DerivedFromTemplateRuleId = "ANALYZE-SEC-006",
Explanation = "The PAC file could not be fetched.",
Conditions = new List<RuleCondition>
{
@@ -89,7 +89,7 @@ public class RuleSubmissionServiceTests
Assert.Equal("ANALYZE-CUSTOM-003", node["ruleId"]!.GetValue<string>());
Assert.Equal("Alice Admin", node["author"]!.GetValue<string>());
Assert.Equal("ANALYZE-SEC-004", node["derivedFromTemplateRuleId"]!.GetValue<string>());
Assert.Equal("ANALYZE-SEC-006", node["derivedFromTemplateRuleId"]!.GetValue<string>());
Assert.Single(node["conditions"]!.AsArray());
}
@@ -430,8 +430,8 @@ public class RuleSubmissionServiceTests
Assert.Null(s.ContactEmail);
Assert.Equal("Proxy PAC unreachable", s.Title);
Assert.Equal("network", s.Category);
Assert.Equal("ANALYZE-SEC-004", s.DerivedFromTemplateRuleId);
Assert.Contains(s.ValidationFindings, f => f.Level == "info" && f.Message.Contains("ANALYZE-SEC-004"));
Assert.Equal("ANALYZE-SEC-006", s.DerivedFromTemplateRuleId);
Assert.Contains(s.ValidationFindings, f => f.Level == "info" && f.Message.Contains("ANALYZE-SEC-006"));
Assert.DoesNotContain(s.ValidationFindings, f => f.Level == "error");
Assert.Equal(5, s.SourceFireStats!.FireCount);
Assert.Equal(50, s.SourceFireStats.SessionsEvaluated);
@@ -27,14 +27,14 @@ public class RuleSubmissionsWireParityTests
AttributionName = "Community contribution",
SubmittedAt = T0,
Status = "approved",
ValidationFindings = new List<RuleSubmissionFinding> { new() { Level = "info", Message = "Derived from template ANALYZE-SEC-004" } },
ValidationFindings = new List<RuleSubmissionFinding> { new() { Level = "info", Message = "Derived from template ANALYZE-SEC-006" } },
SourceFireStats = new RuleSubmissionFireStats { Days = 30, FireCount = 42, SessionsEvaluated = 310, EvaluationCount = 312 },
ReviewedBy = "ga@example.com",
ReviewedAt = T0.AddDays(1),
ReviewComment = "Taken.",
WillBeAdapted = true,
PublishedRuleId = "ANALYZE-NET-002",
DerivedFromTemplateRuleId = "ANALYZE-SEC-004",
DerivedFromTemplateRuleId = "ANALYZE-SEC-006",
};
private static object ItemLiteral() => new
@@ -54,14 +54,14 @@ public class RuleSubmissionsWireParityTests
attributionName = "Community contribution",
submittedAt = T0,
status = "approved",
validationFindings = new List<object> { new { level = "info", message = "Derived from template ANALYZE-SEC-004" } },
validationFindings = new List<object> { new { level = "info", message = "Derived from template ANALYZE-SEC-006" } },
sourceFireStats = new { days = 30, fireCount = 42, sessionsEvaluated = 310, evaluationCount = 312 },
reviewedBy = "ga@example.com",
reviewedAt = T0.AddDays(1),
reviewComment = "Taken.",
willBeAdapted = true,
publishedRuleId = "ANALYZE-NET-002",
derivedFromTemplateRuleId = "ANALYZE-SEC-004",
derivedFromTemplateRuleId = "ANALYZE-SEC-006",
};
[Fact]
@@ -33,7 +33,7 @@ public class TableRuleSubmissionRepositoryTests
Status = RuleSubmissionStatuses.Approved,
ValidationFindings = new List<RuleSubmissionFinding>
{
new() { Level = "info", Message = "Derived from template ANALYZE-SEC-004" },
new() { Level = "info", Message = "Derived from template ANALYZE-SEC-006" },
},
SourceFireStats = new RuleSubmissionFireStats { Days = 30, FireCount = 42, SessionsEvaluated = 310, EvaluationCount = 312 },
ReviewedBy = "ga@example.com",
@@ -41,7 +41,7 @@ public class TableRuleSubmissionRepositoryTests
ReviewComment = "Taken, generalised for the community.",
WillBeAdapted = true,
PublishedRuleId = "ANALYZE-NET-002",
DerivedFromTemplateRuleId = "ANALYZE-SEC-004",
DerivedFromTemplateRuleId = "ANALYZE-SEC-006",
};
[Fact]
@@ -1009,9 +1009,9 @@ namespace AutopilotMonitor.Shared
// raw facts only (Winlogon registry indicators; DefaultPassword presence-only, never the
// value) at Info severity. AutoLogon-enabled alone is NOT graded — it is the fingerprint
// of Windows' own ESP auto-logon on every normal Autopilot enrollment. Backend analyze-
// rules grade only the unambiguous cases: a plaintext DefaultPassword on disk (ANALYZE-
// SEC-003, escalated) and an optional kiosk allow-list template (ANALYZE-SEC-004, off by
// default). AutoLogon may be legitimate (kiosk) OR an enrollment/OOBE manipulation vector.
// rules grade only the unambiguous case: a plaintext DefaultPassword on disk (ANALYZE-
// SEC-003, escalated). AutoLogon may be legitimate (kiosk) OR an enrollment/OOBE
// manipulation vector.
public const string AutoLogonAnalysis = "autologon_analysis";
// OOBE-console / Shift+F10 detection (per-tenant opt-OUT, AnalyzerConfiguration.
// EnableConsoleBypassDetection, default ON). Two complementary Warning signals and one count:
@@ -187,7 +187,7 @@ export const KNOWN_EVENT_TYPES: EventTypeEntry[] = [
{ value: "local_admin_analysis", label: "local_admin_analysis", category: "diagnostics",
description: "Local administrator / user-profile audit detecting pre-enrollment admin account creation (an Autopilot bypass technique); emitted at startup and shutdown for delta detection." },
{ value: "autologon_analysis", label: "autologon_analysis", category: "diagnostics",
description: "Winlogon AutoLogon facts (raw, Info-only): AutoAdminLogon/ForceAutoLogon, default user/domain, AutoLogon count, and presence-only of a plaintext DefaultPassword. AutoLogon-enabled alone is not graded (Windows' own ESP auto-logon looks identical on every normal enrollment); backend analyze-rules grade only a plaintext DefaultPassword on disk (ANALYZE-SEC-003) plus an optional kiosk allow-list template (ANALYZE-SEC-004). AutoLogon can be a legitimate kiosk or an enrollment/OOBE manipulation vector." },
description: "Winlogon AutoLogon facts (raw, Info-only): AutoAdminLogon/ForceAutoLogon, default user/domain (a sign-in address as its domain only), AutoLogon count, and presence-only of a plaintext DefaultPassword. AutoLogon-enabled alone is not graded (Windows' own ESP auto-logon looks identical on every normal enrollment); backend analyze-rules grade only a plaintext DefaultPassword on disk (ANALYZE-SEC-003). AutoLogon can be a legitimate kiosk or an enrollment/OOBE manipulation vector." },
{ value: "gather_result", label: "gather_result", category: "diagnostics",
description: "Output of another gather rule (can be chained)." },
{ value: "provisioning_package_scan", label: "provisioning_package_scan", category: "diagnostics",