mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
added local admin analyze rule,
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"$schema": "../schema/analyze-rule.schema.json",
|
||||
"ruleId": "ANALYZE-ID-002",
|
||||
"title": "Unexpected Local Admin Accounts Detected",
|
||||
"description": "Detects when the LocalAdminAnalyzer flags unexpected local user accounts or profile directories on the device. This is a known Autopilot bypass technique where admin accounts are created before or during enrollment to gain persistent local admin access.",
|
||||
"severity": "high",
|
||||
"category": "identity",
|
||||
"version": "1.0.0",
|
||||
"author": "Autopilot Monitor",
|
||||
"enabled": true,
|
||||
"trigger": "single",
|
||||
"baseConfidence": 60,
|
||||
"conditions": [
|
||||
{
|
||||
"signal": "unexpected_admins_found",
|
||||
"source": "event_type",
|
||||
"eventType": "local_admin_analysis",
|
||||
"dataField": "finding",
|
||||
"operator": "contains",
|
||||
"value": "unexpected",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"signal": "high_confidence",
|
||||
"source": "event_data",
|
||||
"eventType": "local_admin_analysis",
|
||||
"dataField": "confidence_score",
|
||||
"operator": "gte",
|
||||
"value": "60",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"signal": "error_severity",
|
||||
"source": "event_data",
|
||||
"eventType": "local_admin_analysis",
|
||||
"dataField": "severity",
|
||||
"operator": "equals",
|
||||
"value": "error",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"confidenceFactors": [
|
||||
{
|
||||
"signal": "high_confidence",
|
||||
"condition": "exists",
|
||||
"weight": 15
|
||||
},
|
||||
{
|
||||
"signal": "error_severity",
|
||||
"condition": "exists",
|
||||
"weight": 15
|
||||
}
|
||||
],
|
||||
"confidenceThreshold": 50,
|
||||
"explanation": "The agent detected **unexpected local user accounts or profile directories** on this device that are not in the built-in or tenant-configured allowed list.\n\nThis is a security-relevant finding. Creating local admin accounts before or during Autopilot enrollment is a known bypass technique (e.g. via BypassNRO or Shift+F10 during OOBE) that grants persistent local administrator access.\n\nCheck the `local_admin_analysis` event details to see:\n- **unexpected_accounts** — local user accounts not on the allowed list\n- **unexpected_profiles** — C:\\Users profile directories not on the allowed list\n- **bypass_nro** — whether the BypassNRO registry flag was set\n- **confidence_score** — the agent's confidence in the finding (0–100)\n\nIf these accounts are expected for your environment (e.g. a service account or helpdesk account), add them to the allowed list in Tenant Settings.",
|
||||
"remediation": [
|
||||
{
|
||||
"title": "Review the unexpected accounts",
|
||||
"steps": [
|
||||
"Open the session timeline and locate the local_admin_analysis event",
|
||||
"Check the 'unexpected_accounts' and 'unexpected_profiles' lists in the event data",
|
||||
"Verify whether these accounts are legitimate for your environment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Add expected accounts to the allowed list (false positive)",
|
||||
"steps": [
|
||||
"Go to Settings → Agent Analyzers section",
|
||||
"Under 'Allowed Admin Accounts', add the account names that are expected in your environment",
|
||||
"Save the settings — future enrollments will no longer flag these accounts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Investigate a potential security issue",
|
||||
"steps": [
|
||||
"Check if the BypassNRO registry flag was set (indicates OOBE was bypassed)",
|
||||
"Review the enrollment timeline for Shift+F10 or other OOBE escape indicators",
|
||||
"Check whether the unexpected account has admin group membership",
|
||||
"Consider re-imaging the device if unauthorized admin access is confirmed"
|
||||
]
|
||||
}
|
||||
],
|
||||
"relatedDocs": [
|
||||
{
|
||||
"title": "Windows Autopilot known issues",
|
||||
"url": "https://learn.microsoft.com/en-us/autopilot/known-issues"
|
||||
}
|
||||
],
|
||||
"tags": ["identity", "security", "local-admin", "bypass-detection"]
|
||||
}
|
||||
+94
@@ -1021,6 +1021,100 @@
|
||||
"machine-store",
|
||||
"subject-check"
|
||||
]
|
||||
},
|
||||
{
|
||||
"ruleId": "ANALYZE-ID-002",
|
||||
"title": "Unexpected Local Admin Accounts Detected",
|
||||
"description": "Detects when the LocalAdminAnalyzer flags unexpected local user accounts or profile directories on the device. This is a known Autopilot bypass technique where admin accounts are created before or during enrollment to gain persistent local admin access.",
|
||||
"severity": "high",
|
||||
"category": "identity",
|
||||
"version": "1.0.0",
|
||||
"author": "Autopilot Monitor",
|
||||
"enabled": true,
|
||||
"trigger": "single",
|
||||
"baseConfidence": 60,
|
||||
"conditions": [
|
||||
{
|
||||
"signal": "unexpected_admins_found",
|
||||
"source": "event_type",
|
||||
"eventType": "local_admin_analysis",
|
||||
"dataField": "finding",
|
||||
"operator": "contains",
|
||||
"value": "unexpected",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"signal": "high_confidence",
|
||||
"source": "event_data",
|
||||
"eventType": "local_admin_analysis",
|
||||
"dataField": "confidence_score",
|
||||
"operator": "gte",
|
||||
"value": "60",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"signal": "error_severity",
|
||||
"source": "event_data",
|
||||
"eventType": "local_admin_analysis",
|
||||
"dataField": "severity",
|
||||
"operator": "equals",
|
||||
"value": "error",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"confidenceFactors": [
|
||||
{
|
||||
"signal": "high_confidence",
|
||||
"condition": "exists",
|
||||
"weight": 15
|
||||
},
|
||||
{
|
||||
"signal": "error_severity",
|
||||
"condition": "exists",
|
||||
"weight": 15
|
||||
}
|
||||
],
|
||||
"confidenceThreshold": 50,
|
||||
"explanation": "The agent detected **unexpected local user accounts or profile directories** on this device that are not in the built-in or tenant-configured allowed list.\n\nThis is a security-relevant finding. Creating local admin accounts before or during Autopilot enrollment is a known bypass technique (e.g. via BypassNRO or Shift+F10 during OOBE) that grants persistent local administrator access.\n\nCheck the `local_admin_analysis` event details to see:\n- **unexpected_accounts** — local user accounts not on the allowed list\n- **unexpected_profiles** — C:\\Users profile directories not on the allowed list\n- **bypass_nro** — whether the BypassNRO registry flag was set\n- **confidence_score** — the agent's confidence in the finding (0–100)\n\nIf these accounts are expected for your environment (e.g. a service account or helpdesk account), add them to the allowed list in Tenant Settings.",
|
||||
"remediation": [
|
||||
{
|
||||
"title": "Review the unexpected accounts",
|
||||
"steps": [
|
||||
"Open the session timeline and locate the local_admin_analysis event",
|
||||
"Check the 'unexpected_accounts' and 'unexpected_profiles' lists in the event data",
|
||||
"Verify whether these accounts are legitimate for your environment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Add expected accounts to the allowed list (false positive)",
|
||||
"steps": [
|
||||
"Go to Settings → Agent Analyzers section",
|
||||
"Under 'Allowed Admin Accounts', add the account names that are expected in your environment",
|
||||
"Save the settings — future enrollments will no longer flag these accounts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Investigate a potential security issue",
|
||||
"steps": [
|
||||
"Check if the BypassNRO registry flag was set (indicates OOBE was bypassed)",
|
||||
"Review the enrollment timeline for Shift+F10 or other OOBE escape indicators",
|
||||
"Check whether the unexpected account has admin group membership",
|
||||
"Consider re-imaging the device if unauthorized admin access is confirmed"
|
||||
]
|
||||
}
|
||||
],
|
||||
"relatedDocs": [
|
||||
{
|
||||
"title": "Windows Autopilot known issues",
|
||||
"url": "https://learn.microsoft.com/en-us/autopilot/known-issues"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"identity",
|
||||
"security",
|
||||
"local-admin",
|
||||
"bypass-detection"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
480
|
||||
481
|
||||
|
||||
@@ -517,14 +517,17 @@ export default function DiagnosisPage() {
|
||||
|
||||
{/* Evidence Detail */}
|
||||
{showEvidence === primaryResult.ruleId && (
|
||||
<div className="mt-4 p-4 bg-gray-900 rounded-lg">
|
||||
<pre className="text-xs text-gray-100 font-mono overflow-x-auto whitespace-pre-wrap">
|
||||
{JSON.stringify(
|
||||
primaryResult.matchedConditions,
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
<div className="mt-4">
|
||||
<EvidenceEventLinks matchedConditions={primaryResult.matchedConditions} sessionId={sessionId} />
|
||||
<div className="p-4 bg-gray-900 rounded-lg">
|
||||
<pre className="text-xs text-gray-100 font-mono overflow-x-auto whitespace-pre-wrap">
|
||||
{JSON.stringify(
|
||||
primaryResult.matchedConditions,
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -637,14 +640,17 @@ export default function DiagnosisPage() {
|
||||
: "Show Evidence"}
|
||||
</button>
|
||||
{showEvidence === result.ruleId && (
|
||||
<div className="mt-2 p-3 bg-gray-900 rounded text-xs text-gray-100 font-mono overflow-x-auto">
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
result.matchedConditions,
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
<div className="mt-2">
|
||||
<EvidenceEventLinks matchedConditions={result.matchedConditions} sessionId={sessionId} />
|
||||
<div className="p-3 bg-gray-900 rounded text-xs text-gray-100 font-mono overflow-x-auto">
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
result.matchedConditions,
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -741,3 +747,34 @@ export default function DiagnosisPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function EvidenceEventLinks({ matchedConditions, sessionId }: { matchedConditions: Record<string, any>; sessionId: string }) {
|
||||
const eventLinks: { signal: string; eventId: string; eventType?: string }[] = [];
|
||||
|
||||
for (const [signal, evidence] of Object.entries(matchedConditions)) {
|
||||
if (signal.startsWith("factor_")) continue;
|
||||
if (evidence && typeof evidence === "object" && evidence.eventId) {
|
||||
eventLinks.push({ signal, eventId: evidence.eventId, eventType: evidence.eventType });
|
||||
}
|
||||
}
|
||||
|
||||
if (eventLinks.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{eventLinks.map(({ signal, eventId, eventType }) => (
|
||||
<a
|
||||
key={signal}
|
||||
href={`/sessions/${sessionId}#event-${eventId}`}
|
||||
className="inline-flex items-center space-x-1 px-2 py-1 text-xs font-medium text-amber-700 bg-amber-50 hover:bg-amber-100 border border-amber-200 rounded transition-colors"
|
||||
title={`View event in timeline: ${eventId}`}
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
<span>{eventType || signal}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+41
@@ -204,6 +204,7 @@ function AnalysisResultCard({ result }: { result: RuleResult }) {
|
||||
{result.matchedConditions && Object.keys(result.matchedConditions).length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-1">Evidence</h4>
|
||||
<EvidenceEventLinks matchedConditions={result.matchedConditions} />
|
||||
<div className="p-2 bg-gray-900 rounded text-xs text-gray-100 font-mono overflow-x-auto">
|
||||
<pre>{JSON.stringify(result.matchedConditions, null, 2)}</pre>
|
||||
</div>
|
||||
@@ -214,3 +215,43 @@ function AnalysisResultCard({ result }: { result: RuleResult }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function scrollToEvent(eventId: string) {
|
||||
const el = document.getElementById(`event-${eventId}`);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
el.classList.add("ring-2", "ring-amber-400", "ring-offset-1");
|
||||
setTimeout(() => el.classList.remove("ring-2", "ring-amber-400", "ring-offset-1"), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function EvidenceEventLinks({ matchedConditions }: { matchedConditions: Record<string, any> }) {
|
||||
const eventLinks: { signal: string; eventId: string; eventType?: string }[] = [];
|
||||
|
||||
for (const [signal, evidence] of Object.entries(matchedConditions)) {
|
||||
if (signal.startsWith("factor_")) continue;
|
||||
if (evidence && typeof evidence === "object" && evidence.eventId) {
|
||||
eventLinks.push({ signal, eventId: evidence.eventId, eventType: evidence.eventType });
|
||||
}
|
||||
}
|
||||
|
||||
if (eventLinks.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{eventLinks.map(({ signal, eventId, eventType }) => (
|
||||
<button
|
||||
key={signal}
|
||||
onClick={() => scrollToEvent(eventId)}
|
||||
className="inline-flex items-center space-x-1 px-2 py-1 text-xs font-medium text-amber-700 bg-amber-50 hover:bg-amber-100 border border-amber-200 rounded transition-colors"
|
||||
title={`Jump to event ${eventId}`}
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
<span>{eventType || signal}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ function EventRow({ event, isGalacticAdmin, showScriptOutput }: { event: Enrollm
|
||||
const hasDetails = isTruncated || (detailData && Object.keys(detailData).length > 0);
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 rounded-lg p-3 hover:bg-gray-100 transition-colors">
|
||||
<div id={`event-${event.eventId}`} className="bg-gray-50 rounded-lg p-3 hover:bg-gray-100 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -547,7 +547,7 @@ function RawEventRow({ event }: { event: EnrollmentEvent }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-1.5 text-xs font-mono">
|
||||
<div id={`event-${event.eventId}`} className="py-1.5 text-xs font-mono">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-gray-400 w-8 text-right flex-shrink-0">{event.sequence}</span>
|
||||
<span className="text-gray-500 flex-shrink-0">{new Date(event.timestamp).toLocaleTimeString()}</span>
|
||||
|
||||
@@ -740,6 +740,25 @@ export default function SessionDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle #event-{eventId} hash fragment (e.g. from diagnosis page links)
|
||||
useEffect(() => {
|
||||
if (loading || events.length === 0) return;
|
||||
const hash = window.location.hash;
|
||||
if (!hash.startsWith("#event-")) return;
|
||||
// Expand all phases so the event element is in the DOM
|
||||
expandAll();
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById(hash.slice(1));
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
el.classList.add("ring-2", "ring-amber-400", "ring-offset-1");
|
||||
setTimeout(() => el.classList.remove("ring-2", "ring-amber-400", "ring-offset-1"), 3000);
|
||||
}
|
||||
}, 100);
|
||||
// Clear hash so it doesn't re-trigger
|
||||
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
||||
}, [loading, events.length]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const togglePhase = (phaseName: string) => {
|
||||
setExpandedPhases(prev => {
|
||||
const newExpanded = new Set(prev);
|
||||
|
||||
@@ -123,6 +123,7 @@ export const COMMAND_CATEGORIES: readonly GuardrailCategory[] = [
|
||||
category: "Certificate",
|
||||
items: [
|
||||
"certutil -store My",
|
||||
"$c = Get-ChildItem Cert:\\LocalMachine\\My; if ($c) { $c | Select-Object Subject, Thumbprint, Issuer, NotBefore, NotAfter, HasPrivateKey | ConvertTo-Json } else { '{\"CertificateCount\": 0}' }",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -200,6 +201,7 @@ export const ALLOWED_COMMANDS_LIST: readonly string[] = [
|
||||
"nltest /dsgetdc:",
|
||||
"dsregcmd /status",
|
||||
"certutil -store My",
|
||||
"$c = Get-ChildItem Cert:\\LocalMachine\\My; if ($c) { $c | Select-Object Subject, Thumbprint, Issuer, NotBefore, NotAfter, HasPrivateKey | ConvertTo-Json } else { '{\"CertificateCount\": 0}' }",
|
||||
"Get-HotFix | Select-Object -First 10 HotFixID, InstalledOn, Description",
|
||||
];
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user