mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
1c5f69a9fe
* w365: CloudPc device validator — cert-CN-bound fallback for Cloud PCs Windows 365 Cloud PCs are never Autopilot-registered, so the serial lookup (and the Corporate Identifier fallback) structurally reject them and Cloud PC first-connect enrollment could not be monitored. New stage-3 fallback in the device-validation chain: extract the Intune device id from the chain-validated MDM client certificate's Subject CN (field-verified: CN == managedDevice id == cloudPC.managedDeviceId) and require a matching cloudPC object in the claimed tenant (Graph v1.0 virtualEndpoint/cloudPCs, $filter=managedDeviceId eq). Only service-provisioned Cloud PCs have such an object — a regular enrolled non-Autopilot device still ends in the 403, and local W365 markers are never consulted for auth. - Shared: ValidatorType.CloudPc = 5, TenantConfiguration.ValidateCloudPcDevice (default false, tenant-admin writable), GraphAppPermissions.CloudPCReadAll, GraphFeatureCatalog feature W365CloudPcValidation -> CloudPC.Read.All - Backend: CloudPcDeviceValidator (AutopilotDeviceValidator sibling contract: 30/5 min cache, 2 attempts, transient->503; GUID gate before the OData filter; exact-match guard; Graph 403 = definitive "grant missing", not transient), SecurityValidator CN extraction + fallback stage + hard gate, wiring through the 5 agent endpoints, TableConfigRepository persistence - Consent script: W365CloudPcValidation in catalog/ValidateSet/help; NEW GraphFeatureCatalogSyncTests pins C# catalog <-> grant script (previously comment-only lock-step) - Bootstrap dev script v2.3-dev.3: guard 4 (uptime window) exempted while the Cloud-PC relax is active — Cloud PCs run headless for days before first connect; the fresh-profile window is the time anchor there (+ Pester tests) - Web: validation toggle + context plumbing, ValidatedBy "CloudPc" label, privacy-page validation claim generalized (Last updated bumped) - Docs: OKF concept doc backend/cloudpc-device-validation.md + index + log Verified: web tsc clean, ESLint 0 errors (99 pre-existing warnings = ratchet cap, none added). .NET build/tests and Pester run in CI — SDK download and pwsh are unavailable in this sandbox's network policy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqEFrcAHhWgCvgJgHGzMuv * w365: agent-side IsCloudPc flag + engine expectation "no Device-ESP phase" Companion to the CloudPc validator: the agent now classifies Windows 365 Cloud PCs locally with the bootstrap's field-verified marker AND (HKLM\SOFTWARE\Microsoft\Windows365 key + installed CloudManagedDesktopExtension service, probed via its Services registry key so net48 needs no System.ServiceProcess). SKIP-safe: any error resolves false. Never an auth input - backend admission stays exclusively the cert-CN-bound Graph lookup (ValidatedBy=CloudPc); the two signals are independent and may disagree. Two transport rails: - Session metadata: SessionRegistration.IsCloudPc -> Sessions table (sticky-true OR across re-registrations, IsSelfDeployingProfile pattern) -> SessionsIndex full mirror -> SessionSummary -> new isCloudPc search filter end to end (SearchSessions + QueryRawSessions + pagination fingerprint + all three in-memory predicates; MCP search_sessions / query_raw_sessions params + summary projection; web type, Cloud PC pill, SessionInfoCard enrollment label, CSV export appended column). - Decision engine: isCloudPc rides the EnrollmentFactsObserved payload, recorded set-once for BOTH values in EnrollmentScenarioObservations.CloudPc and annotates the scenario profile reason cloud_pc_first_connect:no_device_esp_expected - the engine-recorded expectation that Device-ESP already ran headless at provisioning and the session starts at Account Setup. Deliberately reason+observation only: no completion arm depends on DeviceSetup, Mode stays AccountSetup/IME- classified (Classic), EspConfig keeps configured-FirstSync semantics. Positive marker surfaces as cloud_pc_marker in the audit-trail census. Updater idempotency: the cloudPcAnnotates gate keeps the generic v1 reason branch from re-claiming the slot on repeat posts (would bump EvidenceOrdinal every post). Additive-nullable observation - no state schema bump (v4 convention), serializer roundtrip pinned. Tests: CloudPcDetector resolver (marker AND, throwing probe), registration body, facts-signal payload shape, updater annotation/idempotency/ self-deploying interplay, reducer observation both values + census fold, serializer roundtrip, index projection superset, sticky-true both directions. Suites: DecisionCore 500, Functions 3941, Agent V2 2154, vitest 632 - all green; tsc web+MCP clean; ESLint 0 errors (cap 99). Docs: cloudpc-device-validation.md follow-up section replaced with the shipped mechanism + log entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
104 lines
4.6 KiB
C#
104 lines
4.6 KiB
C#
using System;
|
|
using System.Net;
|
|
using System.Threading.Tasks;
|
|
using AutopilotMonitor.Functions.Services;
|
|
using AutopilotMonitor.Shared.Models;
|
|
using Microsoft.Azure.Functions.Worker.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace AutopilotMonitor.Functions.Security
|
|
{
|
|
/// <summary>
|
|
/// Extension methods for security validation on HTTP requests
|
|
/// </summary>
|
|
public static class SecurityValidationExtensions
|
|
{
|
|
/// <summary>
|
|
/// Validates request security and creates error response if validation fails
|
|
/// </summary>
|
|
/// <param name="req">HTTP request</param>
|
|
/// <param name="tenantId">Tenant ID</param>
|
|
/// <param name="configService">Tenant configuration service</param>
|
|
/// <param name="rateLimitService">Rate limit service</param>
|
|
/// <param name="logger">Logger</param>
|
|
/// <returns>Validation result with optional error response</returns>
|
|
public static async Task<(SecurityValidationResult validation, HttpResponseData? errorResponse)> ValidateSecurityAsync(
|
|
this HttpRequestData req,
|
|
string tenantId,
|
|
TenantConfigurationService configService,
|
|
AdminConfigurationService adminConfigService,
|
|
RateLimitService rateLimitService,
|
|
AutopilotDeviceValidator autopilotDeviceValidator,
|
|
CorporateIdentifierValidator corporateIdentifierValidator,
|
|
ILogger logger,
|
|
string? sessionId = null,
|
|
BootstrapSessionService? bootstrapSessionService = null,
|
|
DeviceAssociationValidator? deviceAssociationValidator = null,
|
|
CloudPcDeviceValidator? cloudPcDeviceValidator = null)
|
|
{
|
|
var validator = new SecurityValidator(configService, adminConfigService, rateLimitService, autopilotDeviceValidator, corporateIdentifierValidator, logger, bootstrapSessionService, deviceAssociationValidator, cloudPcDeviceValidator);
|
|
var validation = await validator.ValidateRequestAsync(req, tenantId, sessionId);
|
|
|
|
if (!validation.IsValid)
|
|
{
|
|
// Create appropriate error response
|
|
var response = req.CreateResponse(validation.StatusCode);
|
|
|
|
if (validation.StatusCode == HttpStatusCode.TooManyRequests)
|
|
{
|
|
// Rate limit error - include Retry-After header
|
|
if (validation.RateLimitResult?.RetryAfter.HasValue == true)
|
|
{
|
|
response.Headers.Add("Retry-After", ((int)validation.RateLimitResult.RetryAfter.Value.TotalSeconds).ToString());
|
|
}
|
|
|
|
await response.WriteAsJsonAsync(new
|
|
{
|
|
success = false,
|
|
message = validation.ErrorMessage,
|
|
rateLimitExceeded = true,
|
|
rateLimitInfo = new
|
|
{
|
|
requestsInWindow = validation.RateLimitResult?.RequestsInWindow,
|
|
maxRequests = validation.RateLimitResult?.MaxRequests,
|
|
windowDurationSeconds = validation.RateLimitResult?.WindowDuration.TotalSeconds,
|
|
retryAfterSeconds = validation.RateLimitResult?.RetryAfter?.TotalSeconds
|
|
}
|
|
});
|
|
}
|
|
else if (validation.StatusCode == HttpStatusCode.ServiceUnavailable)
|
|
{
|
|
// Transient device validation failure — tell agent to retry
|
|
if (validation.RetryAfterSeconds.HasValue)
|
|
{
|
|
response.Headers.Add("Retry-After", validation.RetryAfterSeconds.Value.ToString());
|
|
}
|
|
|
|
await response.WriteAsJsonAsync(new
|
|
{
|
|
success = false,
|
|
message = validation.ErrorMessage,
|
|
details = validation.Details,
|
|
retryAfterSeconds = validation.RetryAfterSeconds
|
|
});
|
|
}
|
|
else
|
|
{
|
|
// Other security errors
|
|
await response.WriteAsJsonAsync(new
|
|
{
|
|
success = false,
|
|
message = validation.ErrorMessage,
|
|
details = validation.Details
|
|
});
|
|
}
|
|
|
|
return (validation, response);
|
|
}
|
|
|
|
// Validation successful - no error response
|
|
return (validation, null);
|
|
}
|
|
}
|
|
}
|