diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherRuleExecutor.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherRuleExecutor.cs index cc3e43e0..536c8e31 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherRuleExecutor.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/GatherRuleExecutor.cs @@ -667,7 +667,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors Regex pattern; try { - pattern = new Regex(patternStr, RegexOptions.Compiled); + pattern = new Regex(patternStr, RegexOptions.Compiled, TimeSpan.FromSeconds(1)); } catch (Exception ex) { @@ -711,7 +711,15 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors if (!CmTraceLogParser.TryParseLine(line, out entry)) continue; - var match = pattern.Match(entry.Message); + Match match; + try + { + match = pattern.Match(entry.Message); + } + catch (RegexMatchTimeoutException) + { + continue; + } if (!match.Success) continue; diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs index 86c133f8..2c1a5c8e 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs @@ -140,7 +140,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking { // Replace {GUID} placeholder with actual GUID capture regex var regexStr = pattern.Pattern.Replace("{GUID}", GuidPattern); - var regex = new Regex(regexStr, RegexOptions.IgnoreCase | RegexOptions.Compiled); + var regex = new Regex(regexStr, RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1)); var compiled = new CompiledPattern { @@ -439,11 +439,18 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking // Match against active patterns foreach (var pattern in _activePatterns) { - var match = pattern.Regex.Match(messageToMatch); - if (match.Success) + try { - WriteMatchLog(filePath, line, pattern.PatternId); - HandlePatternMatch(pattern, match, messageToMatch); + var match = pattern.Regex.Match(messageToMatch); + if (match.Success) + { + WriteMatchLog(filePath, line, pattern.PatternId); + HandlePatternMatch(pattern, match, messageToMatch); + } + } + catch (RegexMatchTimeoutException) + { + // Skip this pattern match to prevent ReDoS } } } diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/AnalyzeRulesFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/AnalyzeRulesFunction.cs index 96ae25ef..1f6f1426 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/AnalyzeRulesFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/AnalyzeRulesFunction.cs @@ -16,11 +16,19 @@ namespace AutopilotMonitor.Functions.Functions { private readonly ILogger _logger; private readonly AnalyzeRuleService _ruleService; + private readonly TenantAdminsService _tenantAdminsService; + private readonly GalacticAdminService _galacticAdminService; - public AnalyzeRulesFunction(ILogger logger, AnalyzeRuleService ruleService) + public AnalyzeRulesFunction( + ILogger logger, + AnalyzeRuleService ruleService, + TenantAdminsService tenantAdminsService, + GalacticAdminService galacticAdminService) { _logger = logger; _ruleService = ruleService; + _tenantAdminsService = tenantAdminsService; + _galacticAdminService = galacticAdminService; } [Function("GetAnalyzeRules")] @@ -54,6 +62,17 @@ namespace AutopilotMonitor.Functions.Functions return unauthorized; } + // Require Tenant Admin or Galactic Admin for rule modifications + var userIdentifier = TenantHelper.GetUserIdentifier(req); + var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier); + var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier); + if (!isGalacticAdmin && !isTenantAdmin) + { + var forbidden = req.CreateResponse(HttpStatusCode.Forbidden); + await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." }); + return forbidden; + } + if (req.Headers.TryGetValues("Content-Length", out var clValues) && long.TryParse(clValues.FirstOrDefault(), out var contentLength) && contentLength > 1_048_576) // 1 MB limit @@ -92,6 +111,17 @@ namespace AutopilotMonitor.Functions.Functions return unauthorized; } + // Require Tenant Admin or Galactic Admin for rule modifications + var userIdentifier = TenantHelper.GetUserIdentifier(req); + var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier); + var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier); + if (!isGalacticAdmin && !isTenantAdmin) + { + var forbidden = req.CreateResponse(HttpStatusCode.Forbidden); + await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." }); + return forbidden; + } + if (req.Headers.TryGetValues("Content-Length", out var clValues2) && long.TryParse(clValues2.FirstOrDefault(), out var contentLength2) && contentLength2 > 1_048_576) // 1 MB limit @@ -131,6 +161,17 @@ namespace AutopilotMonitor.Functions.Functions return unauthorized; } + // Require Tenant Admin or Galactic Admin for rule modifications + var userIdentifier = TenantHelper.GetUserIdentifier(req); + var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier); + var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier); + if (!isGalacticAdmin && !isTenantAdmin) + { + var forbidden = req.CreateResponse(HttpStatusCode.Forbidden); + await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." }); + return forbidden; + } + // Load the rule to determine its type (built-in/community vs. custom) var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId); var rule = rules.FirstOrDefault(r => r.RuleId == ruleId); diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/DeleteSessionFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/DeleteSessionFunction.cs index 98c39819..f3fe9de2 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/DeleteSessionFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/DeleteSessionFunction.cs @@ -11,11 +11,19 @@ namespace AutopilotMonitor.Functions.Functions { private readonly ILogger _logger; private readonly TableStorageService _storageService; + private readonly TenantAdminsService _tenantAdminsService; + private readonly GalacticAdminService _galacticAdminService; - public DeleteSessionFunction(ILogger logger, TableStorageService storageService) + public DeleteSessionFunction( + ILogger logger, + TableStorageService storageService, + TenantAdminsService tenantAdminsService, + GalacticAdminService galacticAdminService) { _logger = logger; _storageService = storageService; + _tenantAdminsService = tenantAdminsService; + _galacticAdminService = galacticAdminService; } [Function("DeleteSession")] @@ -43,6 +51,21 @@ namespace AutopilotMonitor.Functions.Functions var tenantId = TenantHelper.GetTenantId(req); var userIdentifier = TenantHelper.GetUserIdentifier(req); + // Require Tenant Admin or Galactic Admin for destructive operations + var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier); + var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier); + if (!isGalacticAdmin && !isTenantAdmin) + { + _logger.LogWarning($"Non-admin user {userIdentifier} attempted to delete session {sessionId}"); + var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden); + await forbiddenResponse.WriteAsJsonAsync(new + { + success = false, + message = "Access denied. Tenant Admin or Galactic Admin role required." + }); + return forbiddenResponse; + } + _logger.LogInformation($"Deleting session {sessionId} for tenant {tenantId} by user {userIdentifier}"); // Delete all related data for this session diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/GatherRulesFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/GatherRulesFunction.cs index faedf880..8e765da0 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/GatherRulesFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/GatherRulesFunction.cs @@ -16,11 +16,19 @@ namespace AutopilotMonitor.Functions.Functions { private readonly ILogger _logger; private readonly GatherRuleService _ruleService; + private readonly TenantAdminsService _tenantAdminsService; + private readonly GalacticAdminService _galacticAdminService; - public GatherRulesFunction(ILogger logger, GatherRuleService ruleService) + public GatherRulesFunction( + ILogger logger, + GatherRuleService ruleService, + TenantAdminsService tenantAdminsService, + GalacticAdminService galacticAdminService) { _logger = logger; _ruleService = ruleService; + _tenantAdminsService = tenantAdminsService; + _galacticAdminService = galacticAdminService; } [Function("GetGatherRules")] @@ -54,6 +62,17 @@ namespace AutopilotMonitor.Functions.Functions return unauthorized; } + // Require Tenant Admin or Galactic Admin for rule modifications + var userIdentifier = TenantHelper.GetUserIdentifier(req); + var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier); + var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier); + if (!isGalacticAdmin && !isTenantAdmin) + { + var forbidden = req.CreateResponse(HttpStatusCode.Forbidden); + await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." }); + return forbidden; + } + if (req.Headers.TryGetValues("Content-Length", out var clValues) && long.TryParse(clValues.FirstOrDefault(), out var contentLength) && contentLength > 1_048_576) // 1 MB limit @@ -92,6 +111,17 @@ namespace AutopilotMonitor.Functions.Functions return unauthorized; } + // Require Tenant Admin or Galactic Admin for rule modifications + var userIdentifier = TenantHelper.GetUserIdentifier(req); + var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier); + var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier); + if (!isGalacticAdmin && !isTenantAdmin) + { + var forbidden = req.CreateResponse(HttpStatusCode.Forbidden); + await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." }); + return forbidden; + } + if (req.Headers.TryGetValues("Content-Length", out var clValues2) && long.TryParse(clValues2.FirstOrDefault(), out var contentLength2) && contentLength2 > 1_048_576) // 1 MB limit @@ -131,6 +161,17 @@ namespace AutopilotMonitor.Functions.Functions return unauthorized; } + // Require Tenant Admin or Galactic Admin for rule modifications + var userIdentifier = TenantHelper.GetUserIdentifier(req); + var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier); + var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier); + if (!isGalacticAdmin && !isTenantAdmin) + { + var forbidden = req.CreateResponse(HttpStatusCode.Forbidden); + await forbidden.WriteAsJsonAsync(new { success = false, message = "Access denied. Tenant Admin or Galactic Admin role required." }); + return forbidden; + } + // Load the rule to determine its type (built-in/community vs. custom) var rules = await _ruleService.GetAllRulesForTenantAsync(tenantId); var rule = rules.FirstOrDefault(r => r.RuleId == ruleId); diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/MarkSessionFailedFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/MarkSessionFailedFunction.cs index 23256f67..fd9a29f9 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/MarkSessionFailedFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/MarkSessionFailedFunction.cs @@ -13,11 +13,19 @@ namespace AutopilotMonitor.Functions.Functions { private readonly ILogger _logger; private readonly TableStorageService _storageService; + private readonly TenantAdminsService _tenantAdminsService; + private readonly GalacticAdminService _galacticAdminService; - public MarkSessionFailedFunction(ILogger logger, TableStorageService storageService) + public MarkSessionFailedFunction( + ILogger logger, + TableStorageService storageService, + TenantAdminsService tenantAdminsService, + GalacticAdminService galacticAdminService) { _logger = logger; _storageService = storageService; + _tenantAdminsService = tenantAdminsService; + _galacticAdminService = galacticAdminService; } [Function("MarkSessionFailed")] @@ -45,6 +53,21 @@ namespace AutopilotMonitor.Functions.Functions string tenantId = TenantHelper.GetTenantId(req); string userIdentifier = TenantHelper.GetUserIdentifier(req); + // Require Tenant Admin or Galactic Admin for status modifications + var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier); + var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier); + if (!isGalacticAdmin && !isTenantAdmin) + { + _logger.LogWarning($"Non-admin user {userIdentifier} attempted to mark session {sessionId} as failed"); + var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden); + await forbiddenResponse.WriteAsJsonAsync(new + { + success = false, + message = "Access denied. Tenant Admin or Galactic Admin role required." + }); + return new MarkSessionFailedOutput { HttpResponse = forbiddenResponse }; + } + _logger.LogInformation($"Marking session {sessionId} as failed for tenant {tenantId} by user {userIdentifier}"); // Update session status to Failed with manual failure reason diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/PlatformUsageMetricsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/PlatformUsageMetricsFunction.cs index 535c9556..3d3b7a1f 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/PlatformUsageMetricsFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/PlatformUsageMetricsFunction.cs @@ -85,8 +85,7 @@ namespace AutopilotMonitor.Functions.Functions await errorResponse.WriteAsJsonAsync(new { success = false, - message = "Failed to compute usage metrics", - error = ex.Message + message = "Failed to compute usage metrics" }); return errorResponse; diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/ReseedFromGitHubFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/ReseedFromGitHubFunction.cs index 9115bbdf..14ef9aae 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/ReseedFromGitHubFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/ReseedFromGitHubFunction.cs @@ -115,7 +115,7 @@ namespace AutopilotMonitor.Functions.Functions await response.WriteAsJsonAsync(new { success = false, - message = $"Failed to fetch rules from GitHub: {ex.Message}. GitHub CDN may cache responses for up to 5 minutes after a merge." + message = "Failed to fetch rules from GitHub. GitHub CDN may cache responses for up to 5 minutes after a merge." }); return response; } diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/TenantOffboardFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/TenantOffboardFunction.cs index 4ce2af5f..1f594137 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/TenantOffboardFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/TenantOffboardFunction.cs @@ -1,6 +1,7 @@ using System.Net; using AutopilotMonitor.Functions.Extensions; using AutopilotMonitor.Functions.Helpers; +using AutopilotMonitor.Functions.Security; using AutopilotMonitor.Functions.Services; using AutopilotMonitor.Shared; using Azure.Data.Tables; @@ -75,6 +76,9 @@ public class TenantOffboardFunction return forbiddenResponse; } + // Validate tenantId format to prevent OData injection + SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId)); + _logger.LogWarning($"TENANT OFFBOARD initiated for tenant {tenantId} by {upn}"); var result = new OffboardResult { TenantId = tenantId, InitiatedBy = upn!, InitiatedAt = DateTime.UtcNow }; @@ -122,7 +126,7 @@ public class TenantOffboardFunction { _logger.LogError(ex, $"Tenant offboard failed for tenant {tenantId}"); result.Success = false; - result.Error = ex.Message; + result.Error = "Internal server error"; var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); await errorResponse.WriteAsJsonAsync(result); diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/TriggerMaintenanceFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/TriggerMaintenanceFunction.cs index 8c3bb779..db01ef3c 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/TriggerMaintenanceFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/TriggerMaintenanceFunction.cs @@ -121,7 +121,7 @@ namespace AutopilotMonitor.Functions.Functions await errorResponse.WriteAsJsonAsync(new { success = false, - error = ex.Message + message = "Internal server error" }); return errorResponse; diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/UpdateTenantConfigurationFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/UpdateTenantConfigurationFunction.cs index d1108e1b..1cb3bf8b 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/UpdateTenantConfigurationFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/UpdateTenantConfigurationFunction.cs @@ -16,13 +16,19 @@ namespace AutopilotMonitor.Functions.Functions { private readonly ILogger _logger; private readonly TenantConfigurationService _configService; + private readonly TenantAdminsService _tenantAdminsService; + private readonly GalacticAdminService _galacticAdminService; public UpdateTenantConfigurationFunction( ILogger logger, - TenantConfigurationService configService) + TenantConfigurationService configService, + TenantAdminsService tenantAdminsService, + GalacticAdminService galacticAdminService) { _logger = logger; _configService = configService; + _tenantAdminsService = tenantAdminsService; + _galacticAdminService = galacticAdminService; } [Function("UpdateTenantConfiguration")] @@ -62,6 +68,21 @@ namespace AutopilotMonitor.Functions.Functions return forbiddenResponse; } + // Require Tenant Admin or Galactic Admin for configuration changes + var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(userIdentifier); + var isTenantAdmin = await _tenantAdminsService.IsTenantAdminAsync(tenantId, userIdentifier); + if (!isGalacticAdmin && !isTenantAdmin) + { + _logger.LogWarning($"Non-admin user {userIdentifier} attempted to update configuration for tenant {tenantId}"); + var forbiddenResponse = req.CreateResponse(HttpStatusCode.Forbidden); + await forbiddenResponse.WriteAsJsonAsync(new + { + success = false, + message = "Access denied. Tenant Admin or Galactic Admin role required." + }); + return forbiddenResponse; + } + _logger.LogInformation($"UpdateTenantConfiguration: {tenantId} by user {userIdentifier}"); // Parse request body diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/UsageMetricsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/UsageMetricsFunction.cs index b164feef..641cff49 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/UsageMetricsFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/UsageMetricsFunction.cs @@ -69,8 +69,7 @@ namespace AutopilotMonitor.Functions.Functions await errorResponse.WriteAsJsonAsync(new { success = false, - message = "Failed to compute tenant usage metrics", - error = ex.Message + message = "Failed to compute tenant usage metrics" }); return errorResponse; diff --git a/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx b/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx index 4cf72af4..da493317 100644 --- a/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx +++ b/src/Web/autopilot-monitor-web/app/admin-configuration/page.tsx @@ -28,7 +28,7 @@ interface AdminConfiguration { export default function AdminConfigurationPage() { const router = useRouter(); - const { getAccessToken } = useAuth(); + const { getAccessToken, user } = useAuth(); const [galacticAdminMode, setGalacticAdminMode] = useState(false); // Diagnostics Log Paths state const [globalDiagPaths, setGlobalDiagPaths] = useState([]); @@ -53,16 +53,16 @@ export default function AdminConfigurationPage() { // Preview Whitelist state const [previewApproved, setPreviewApproved] = useState>(new Set()); - // Load galactic admin mode from localStorage + // Load galactic admin mode - use server-derived isGalacticAdmin as authoritative source useEffect(() => { - const galacticMode = localStorage.getItem("galacticAdminMode") === "true"; + const galacticMode = user?.isGalacticAdmin === true; setGalacticAdminMode(galacticMode); - // Redirect if not in galactic admin mode - if (!galacticMode) { + // Redirect if not a galactic admin + if (user && !galacticMode) { router.push("/dashboard"); } - }, [router]); + }, [router, user]); // Fetch admin configuration useEffect(() => { @@ -263,7 +263,7 @@ export default function AdminConfigurationPage() { } return ( - +
{/* Header */}