diff --git a/src/Agent/AutopilotMonitor.Agent/AutopilotMonitorService.cs b/src/Agent/AutopilotMonitor.Agent/AutopilotMonitorService.cs
index 1062e045..58f21115 100644
--- a/src/Agent/AutopilotMonitor.Agent/AutopilotMonitorService.cs
+++ b/src/Agent/AutopilotMonitor.Agent/AutopilotMonitorService.cs
@@ -68,7 +68,8 @@ namespace AutopilotMonitor.Agent
return new AgentConfiguration
{
- ApiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? "http://localhost:7071",
+ //ApiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? "http://localhost:7071",
+ ApiBaseUrl = Environment.GetEnvironmentVariable("AUTOPILOT_MONITOR_API") ?? "https://autopilotmonitor-func.azurewebsites.net",
SessionId = sessionId,
TenantId = "default-tenant",
SpoolDirectory = Environment.ExpandEnvironmentVariables(@"%ProgramData%\AutopilotMonitor\Spool"),
diff --git a/src/Agent/AutopilotMonitor.Agent/Program.cs b/src/Agent/AutopilotMonitor.Agent/Program.cs
index 0a08e082..d1d1b96e 100644
--- a/src/Agent/AutopilotMonitor.Agent/Program.cs
+++ b/src/Agent/AutopilotMonitor.Agent/Program.cs
@@ -45,7 +45,10 @@ namespace AutopilotMonitor.Agent
var logDir = Environment.ExpandEnvironmentVariables(config.LogDirectory);
var logger = new AgentLogger(logDir, enableDebug: true);
- logger.Info("Agent starting in console mode");
+ logger.Info("");
+ logger.Info("============================================================================================================");
+ logger.Info("====================================== Agent starting in console mode ======================================");
+ logger.Info("============================================================================================================");
Console.WriteLine($"Session ID: {config.SessionId}");
Console.WriteLine($"Tenant ID: {config.TenantId}");
Console.WriteLine($"API URL: {config.ApiBaseUrl}");
@@ -334,7 +337,8 @@ namespace AutopilotMonitor.Agent
}
// Defaults (fallback values if no config file exists)
- string apiBaseUrl = "http://localhost:7071";
+ //string apiBaseUrl = "http://localhost:7071";
+ string apiBaseUrl = "https://autopilotmonitor-func.azurewebsites.net";
int uploadIntervalSeconds = 30;
bool cleanupOnExit = true;
bool selfDestructOnComplete = true;
diff --git a/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj b/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj
index 5b47be84..abea1170 100644
--- a/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj
+++ b/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj
@@ -28,4 +28,9 @@
+
+
+
+
+
diff --git a/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs b/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs
index 4800179b..b4502e49 100644
--- a/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Security/CertificateValidator.cs
@@ -1,6 +1,9 @@
using System;
using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
using System.Linq;
+using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
@@ -18,17 +21,23 @@ namespace AutopilotMonitor.Functions.Security
/// - Prevents self-signed certificate attacks
/// - Validates Enhanced Key Usage for Client Authentication
/// - In-memory cache for validated certificates (5 minute TTL)
+ /// - Intune CA certificates embedded as resources (required on Linux/Azure Functions flex plan
+ /// where the OS trust store does not contain Microsoft Intune CAs)
///
public static class CertificateValidator
{
private static readonly ConcurrentDictionary _validationCache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
+ // Lazily loaded Intune CA certs from embedded resources
+ private static readonly Lazy _intuneCaCerts = new(LoadIntuneCaCertificates);
+
private class CachedValidationResult
{
public CertificateValidationResult Result { get; set; } = null!;
public DateTime ExpiresAt { get; set; }
}
+
///
/// Validates a client certificate from HTTP request header
///
@@ -77,25 +86,33 @@ namespace AutopilotMonitor.Functions.Security
var now = DateTime.UtcNow;
if (now < certificate.NotBefore || now > certificate.NotAfter)
{
- var result = new CertificateValidationResult
+ return new CertificateValidationResult
{
IsValid = false,
ErrorMessage = $"Certificate expired or not yet valid (NotBefore={certificate.NotBefore}, NotAfter={certificate.NotAfter})",
Thumbprint = thumbprint
};
-
- // Don't cache failures
- return result;
}
// 2. Validate certificate chain (without revocation check - Intune certs have no CRL/OCSP)
+ //
+ // On Linux (Azure Functions flex consumption plan) the OS trust store does not contain
+ // Microsoft Intune CAs. We load them from embedded PEM resources into ExtraStore and set
+ // AllowUnknownCertificateAuthority so chain.Build() can resolve the issuer without the OS
+ // store. The issuer identity is still enforced in step 3.
using var chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
- chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
+ chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
chain.ChainPolicy.VerificationTime = DateTime.UtcNow;
chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromSeconds(10);
+ // Load embedded Intune CA certs into ExtraStore
+ var caCerts = _intuneCaCerts.Value;
+ logger?.LogDebug("Loading {Count} embedded Intune CA certificate(s) into chain ExtraStore", caCerts.Length);
+ foreach (var caCert in caCerts)
+ chain.ChainPolicy.ExtraStore.Add(caCert);
+
var chainValid = chain.Build(certificate);
if (!chainValid)
@@ -103,15 +120,12 @@ namespace AutopilotMonitor.Functions.Security
var chainErrors = string.Join(", ", chain.ChainStatus.Select(s => s.StatusInformation));
logger?.LogWarning($"Certificate chain validation failed: {chainErrors}");
- var result = new CertificateValidationResult
+ return new CertificateValidationResult
{
IsValid = false,
ErrorMessage = $"Certificate chain validation failed: {chainErrors}",
Thumbprint = thumbprint
};
-
- // Don't cache failures
- return result;
}
// 3. Verify issuer chain contains Microsoft Intune CA
@@ -128,15 +142,12 @@ namespace AutopilotMonitor.Functions.Security
var chainInfo = string.Join(" -> ", chain.ChainElements.Cast().Select(e => e.Certificate.Subject));
logger?.LogWarning($"Certificate chain does not contain Microsoft Intune CA: {chainInfo}");
- var result = new CertificateValidationResult
+ return new CertificateValidationResult
{
IsValid = false,
ErrorMessage = $"Certificate not issued by Microsoft Intune CA",
Thumbprint = thumbprint
};
-
- // Don't cache failures
- return result;
}
// 4. Check Enhanced Key Usage for Client Authentication (1.3.6.1.5.5.7.3.2)
@@ -148,15 +159,12 @@ namespace AutopilotMonitor.Functions.Security
if (!hasClientAuth)
{
- var result = new CertificateValidationResult
+ return new CertificateValidationResult
{
IsValid = false,
ErrorMessage = "Certificate does not have Client Authentication EKU",
Thumbprint = thumbprint
};
-
- // Don't cache failures
- return result;
}
// All checks passed - create and cache success result
@@ -190,6 +198,52 @@ namespace AutopilotMonitor.Functions.Security
};
}
}
+
+ ///
+ /// Loads Intune CA certificates from embedded PEM resources.
+ /// Files: Security/Certificates/intune-intermediate-ca.pem
+ /// Security/Certificates/intune-root-ca.pem
+ ///
+ private static X509Certificate2[] LoadIntuneCaCertificates()
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ var resourceNames = new[]
+ {
+ "AutopilotMonitor.Functions.Security.Certificates.intune-intermediate-ca.pem",
+ "AutopilotMonitor.Functions.Security.Certificates.intune-root-ca.pem"
+ };
+
+ List certs = [];
+
+ foreach (var resourceName in resourceNames)
+ {
+ using var stream = assembly.GetManifestResourceStream(resourceName);
+ if (stream == null)
+ {
+ // Resource not found - not fatal, chain validation will fall back to OS store
+ continue;
+ }
+
+ using var reader = new StreamReader(stream);
+ var pem = reader.ReadToEnd();
+
+ // Skip placeholder files that haven't been filled with a real cert yet
+ if (pem.Contains("PLACEHOLDER_REPLACE_WITH_BASE64"))
+ continue;
+
+ try
+ {
+ var cert = X509Certificate2.CreateFromPem(pem);
+ certs.Add(cert);
+ }
+ catch
+ {
+ // Malformed PEM - skip silently, chain validation continues with OS store
+ }
+ }
+
+ return certs.ToArray();
+ }
}
///
diff --git a/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-intermediate-ca.pem b/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-intermediate-ca.pem
new file mode 100644
index 00000000..b822133f
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-intermediate-ca.pem
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIEfTCCAmWgAwIBAgIQQt4O1ZNyWaBPqw1mUq1F8jANBgkqhkiG9w0BAQsF
+ADA4MTYwNAYDVQQDEy1NaWNyb3NvZnQgSW50dW5lIFJvb3QgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwHhcNMjMwNTA1MDAwMDAwWhcNMjYwNTA1MDAwMDAw
+WjApMScwJQYDVQQDEx5NaWNyb3NvZnQgSW50dW5lIE1ETSBEZXZpY2UgQ0Ew
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDYOy/IV8f+BmH5y9/z
+JUv5oROn3MEQua39r03K7fgAqwEsAFtU/4ZWnFbriFSx0xZsDkZxxUy9BlOQ
+Z+2sr/eJ2j6uI6CocR4woRktJR6pQ5EXdsJb9lZOfV9+q6PwLnAIBQTTpUq+
+XtE8T1MwwGQ5NWW1JqQMDPo3nR0Th14tDQNrLtCPe7c2f+qaG2x+uRvWGMgT
+mxtGpYiipQB8upnInAZZ5wVdhu+/GMZgTSYJvn+3Px33NOB+0mpVanq7Cwzy
+XA+dqGio8sZJDfOS1fPYDKqPj52l3rct3doa2o98gPHWaDs7Rj+GiAKeidUk
+N3uhQUyNfi8+YJEHb8OVGjEFAgMBAAGjgZEwgY4wHQYDVR0OBBYEFC5aTr/l
+v8IEGDqYYmHaaj3AsHw4MB8GA1UdIwQYMBaAFCtJzrEHJUyd5gqT85C5MPfm
+RJ+SMBAGCSsGAQQBgjcVAQQDAgEAMA4GA1UdDwEB/wQEAwIBhjAWBgNVHSUB
+Af8EDDAKBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMA0GCSqGSIb3
+DQEBCwUAA4ICAQCoci1EyidjS6xqi3S37/TJdIGqM0OcvTsi12WPIVxAMFRa
+F638dgu9FrRpTDCquc06lBHtIfBWCU7VFkGIGGIZ54L4en/dGi0ZBXjv4vfp
+RQ+LsoJZnYtCF3MbvlPXfQr3nk1k6m7c1utsuSzL0VgeEJJ++5KXlMRHrFdl
+XXm5N8MJNw78I/e7sbwNZxvA4UdxnEpqHeKDj1+nppANJDKrobIp9WzmYCFA
+8Qu3bX/Ng9OnxLnpGi8mCIvPCkPu3oo1RtHxjf/tQtdSE4V1/8mggYLNNftj
+Wk6ElqA+gRGYgJCssJrfG/bRYk+9ZQjavu3oG0B+SD7j3UeAOQqoOaPi8faF
+95tpgpVfJqQ6bm9wqBdODWfE4TYIPd0QG5nsYr86JThEtsL36U9j74wC33+4
+YHV4UviUiycyQiQQCSIFxSY2u9cLpFO4rjM+Qe/n8KWQURTW8i3wUFReVrMp
+9l+y70GklU/HHkXv8SDtFi6Q0+aK2duUhbHr+8P2nPuPNetfryVsdE+RVL4p
+FWEfIvbOKOI5EQj3Bh4/XkbhQtZk+FR7VkUIm5A9Kp+OzsQADkYhWE1N4xL/
+ysfRWmOcjB1MT1m7Zfie3YsR7j+rY4NLb+snuHqqpYRDSZpngZAJgRYHI1xS
+UxOk+zJ8XyHG+eyeZdfwlrWLKuMJEFw4pczs0Q==
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-root-ca.pem b/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-root-ca.pem
new file mode 100644
index 00000000..4f637ee3
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/Security/Certificates/intune-root-ca.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFaTCCA1GgAwIBAgIQWDldDibkr6lFMz6rqUVDRDANBgkqhkiG9w0BAQsF
+ADA4MTYwNAYDVQQDEy1NaWNyb3NvZnQgSW50dW5lIFJvb3QgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwHhcNMjUwOTE1MDAwMDAwWhcNMzAwOTE1MDAwMDAw
+WjA4MTYwNAYDVQQDEy1NaWNyb3NvZnQgSW50dW5lIFJvb3QgQ2VydGlmaWNh
+dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+AQDpcRMWOOO5oDbbLnOV2SnaqN5NuQoEaE7nIUuH/3dKtqL/ZSRqvpNgKUyI
+SfM/hpuq8V8JlYW/cutwg+B80WGXLkFHMgYhJGYxgDiwNbfZ0tk64zUpidzY
+gj1PeNWkKOVIspvVBiMj3co92LQlyK8CL0V4oSeD1LpR8fncdiZtYyWKZssD
+E1fSG/TH/wTAQHywZJ4cwKV2itgVr1QwF5yShozUerrwmQ+QnEb/5gthJzKl
+eb8lUW1FtHCaMWiEVhflUpQCDJzYgvZJOhFQ6l0CD+eR1Oz6scDj/UxJPBEv
+NKB4G5m1b93gYKZ10mtK0a3Qi4df5A+8P44AhDI6zaG2GrmF7EX6vLFOVdPC
+1d3MKFoApJ3ylMeknwqRlzZj0HB/SjhzyvVbLI+BPVDy8AvM+w8cJAtV3Ety
+rflpmc+dEzS8I1RTO1soEuMZ6/8osg2iPAH7rV6KwbLitQvo4thwVSw0x+hE
+Rl+M4vKnq7Vs789yC5Nb1zZ6MGTXp3tRiuG40BBOY+6FdBS0aQAQ+UmsnRxS
+MTrOC9CKCYFuFYxwDn9J+VvzMNhJ4VlR6e/R7KoJNVV7KWF+OfDcsxxIqLw3
+RcD1dsKKW+kl33K5XW7Q0Ra7KcMiLsvqHffhrYsgmllTGtpCgDLGl1H1vP9/
+TM31bLEc6udQX5xOdcht0VmewQIDAQABo28wbTAdBgNVHQ4EFgQUsj/fiIB6
+Rv1I2uifIZIKWggJF+0wDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYB
+Af8CAQEwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwEAYJKwYBBAGCNxUBBAMC
+AQAwDQYJKoZIhvcNAQELBQADggIBAJjQPngeOpKjhd0eouyDSlWzYMyTLEgr
+QwKc3XGL4el/AjB1bOTaSPs8Y7jLUQ62Za51qyijacWSHBbrOh77We5kUf4U
+vFcCOKl7KdgBfgqEKJKM3/r6SIFMouc0DhWO82Yjs2IOwOuEoKXagZRHfhHG
+MHFvnj6ebb1uGaPRvxdMVi70kEoPysCavQbDahVTKuFySo1iK2BtRDYKJYUZ
+RLTON3NfuVb6AaIUCQpAB8+0qDdRVGV0nblmecLJ+pyCmfrWKFoRHiqV69Wu
+AweiCX6qUxpokzwpM78fuaFnDFzihxzoFuLbv+79MiupYSsC73OrF9ABNyc2
+15jhmkM1NNNYtEH7Dj0JZz5o5GQ/ROH4Q2jU2mSbeB0EOpiW1tiUzt7/R/wR
+cYvg6XSlwfapmE0EjrW2iUwywkU2+qGHvNQeuoIwILZ2eSBiaYZW97P49GNB
+psliPGmpv9N5I35132b8CfwXpSBmqTTcN0UMBUKgZAV2AUqjNml8B7bZw8h++
+CybzMdHOy+8uNnwO/MWsozqXz5SIeRW6YeWqyHTwbfmkJzMVKuSBFrSgP9pe
+UmzmpMFiqZ0yb9Rp5AhKx+kBEBAJAq4UEIV9Dxcw1hgiz00g7NewBokEfeVK
+pYAyVLPiMvkT/HNZ0mnKeTES1xYONSV27UVzQHhQ6CxAKXiPS8Y
+-----END CERTIFICATE-----
\ No newline at end of file
diff --git a/src/Backend/AutopilotMonitor.Functions/host.json b/src/Backend/AutopilotMonitor.Functions/host.json
index 41d45508..ced44c96 100644
--- a/src/Backend/AutopilotMonitor.Functions/host.json
+++ b/src/Backend/AutopilotMonitor.Functions/host.json
@@ -4,6 +4,7 @@
"logLevel": {
"default": "Warning",
"Function": "Information",
+ "AutopilotMonitor.Functions.Security": "Debug",
"Host": "Error",
"Host.Aggregator": "Error",
"Host.Results": "Error",
diff --git a/src/Web/autopilot-monitor-web/staticwebapp.config.README.json b/src/Web/autopilot-monitor-web/staticwebapp.config.json.README.txt
similarity index 100%
rename from src/Web/autopilot-monitor-web/staticwebapp.config.README.json
rename to src/Web/autopilot-monitor-web/staticwebapp.config.json.README.txt