diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/DataAccessServiceExtensions.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/DataAccessServiceExtensions.cs new file mode 100644 index 00000000..71b3877d --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/DataAccessServiceExtensions.cs @@ -0,0 +1,58 @@ +using AutopilotMonitor.Shared.DataAccess; +using AutopilotMonitor.Functions.DataAccess.TableStorage; +using Microsoft.Extensions.DependencyInjection; + +namespace AutopilotMonitor.Functions.DataAccess +{ + /// + /// Extension methods for registering Data Access Layer services. + /// Currently registers Azure Table Storage implementations. + /// To switch to Cosmos DB, replace the implementation registrations here. + /// + public static class DataAccessServiceExtensions + { + /// + /// Registers all Data Access Layer services with Table Storage implementations. + /// + public static IServiceCollection AddTableStorageDataAccess(this IServiceCollection services) + { + // Event publisher (no-op by default; replace with Event Hub/Service Bus implementation) + services.AddSingleton(); + + // Storage initializer + services.AddSingleton(); + + // Repository implementations (delegate to existing TableStorageService) + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Note: IConfigRepository, IAdminRepository, IDeviceSecurityRepository, + // IBootstrapRepository, and INotificationRepository implementations will be + // added as existing services are migrated to use the DAL interfaces. + // Currently those services (TenantConfigurationService, GlobalAdminService, etc.) + // manage their own Table Storage connections directly. + + return services; + } + + /// + /// Replaces the default NullDataEventPublisher with a custom implementation. + /// Call this AFTER AddTableStorageDataAccess() to override. + /// Example: services.AddEventStreaming<EventHubPublisher>(); + /// + public static IServiceCollection AddEventStreaming(this IServiceCollection services) + where TPublisher : class, IDataEventPublisher + { + // Remove existing registration and replace + var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IDataEventPublisher)); + if (descriptor != null) + services.Remove(descriptor); + + services.AddSingleton(); + return services; + } + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMaintenanceRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMaintenanceRepository.cs new file mode 100644 index 00000000..d4492572 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMaintenanceRepository.cs @@ -0,0 +1,63 @@ +using AutopilotMonitor.Shared.DataAccess; +using AutopilotMonitor.Shared.Models; +using AutopilotMonitor.Functions.Services; + +namespace AutopilotMonitor.Functions.DataAccess.TableStorage +{ + /// + /// Table Storage implementation of IMaintenanceRepository. + /// Delegates to existing TableStorageService for backwards compatibility. + /// + public class TableMaintenanceRepository : IMaintenanceRepository + { + private readonly TableStorageService _storage; + + public TableMaintenanceRepository(TableStorageService storage) + { + _storage = storage; + } + + public Task LogAuditEntryAsync(string tenantId, string action, string entityType, + string entityId, string performedBy, Dictionary? details = null) + => _storage.LogAuditEntryAsync(tenantId, action, entityType, entityId, performedBy, details); + + public Task> GetAuditLogsAsync(string tenantId, int maxResults = 100) + => _storage.GetAuditLogsAsync(tenantId, maxResults); + + public Task> GetAllAuditLogsAsync(int maxResults = 100) + => _storage.GetAllAuditLogsAsync(maxResults); + + public Task> GetSessionsOlderThanAsync(string tenantId, DateTime cutoffDate) + => _storage.GetSessionsOlderThanAsync(tenantId, cutoffDate); + + public Task> GetSessionsByDateRangeAsync(DateTime startDate, DateTime endDate, string? tenantId = null) + => _storage.GetSessionsByDateRangeAsync(startDate, endDate, tenantId); + + public Task> GetStalledSessionsAsync(string tenantId, DateTime cutoffTime) + => _storage.GetStalledSessionsAsync(tenantId, cutoffTime); + + public Task> GetExcessiveDataSendersAsync(string tenantId, DateTime windowCutoff, int maxSessionWindowHours) + => _storage.GetExcessiveDataSendersAsync(tenantId, windowCutoff, maxSessionWindowHours); + + public Task> GetAllTenantIdsAsync() + => _storage.GetAllTenantIdsAsync(); + + public Task DeleteSessionEventsAsync(string tenantId, string sessionId) + => _storage.DeleteSessionEventsAsync(tenantId, sessionId); + + public Task DeleteSessionRuleResultsAsync(string tenantId, string sessionId) + => _storage.DeleteSessionRuleResultsAsync(tenantId, sessionId); + + public Task DeleteSessionAppInstallSummariesAsync(string tenantId, string sessionId) + => _storage.DeleteSessionAppInstallSummariesAsync(tenantId, sessionId); + + public Task BackfillSessionIndexAsync() + => _storage.BackfillSessionIndexAsync(); + + public Task CleanupGhostSessionIndexEntriesAsync() + => _storage.CleanupGhostSessionIndexEntriesAsync(); + + public Task IsSessionIndexEmptyAsync() + => _storage.IsSessionIndexEmptyAsync(); + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMetricsRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMetricsRepository.cs new file mode 100644 index 00000000..63d2943f --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableMetricsRepository.cs @@ -0,0 +1,65 @@ +using AutopilotMonitor.Shared.DataAccess; +using AutopilotMonitor.Shared.Models; +using AutopilotMonitor.Functions.Services; + +namespace AutopilotMonitor.Functions.DataAccess.TableStorage +{ + /// + /// Table Storage implementation of IMetricsRepository. + /// Delegates to existing TableStorageService for backwards compatibility. + /// + public class TableMetricsRepository : IMetricsRepository + { + private readonly TableStorageService _storage; + private readonly IDataEventPublisher _publisher; + + public TableMetricsRepository(TableStorageService storage, IDataEventPublisher publisher) + { + _storage = storage; + _publisher = publisher; + } + + public Task SaveUsageMetricsSnapshotAsync(UsageMetricsSnapshot metrics) + => _storage.SaveUsageMetricsSnapshotAsync(metrics); + + public Task> GetUsageMetricsSnapshotAsync( + string? tenantId = null, string? startDate = null, string? endDate = null, int maxResults = 100) + => _storage.GetUsageMetricsSnapshotAsync(tenantId, startDate, endDate, maxResults); + + public Task HasUsageMetricsSnapshotAsync(string date) + => _storage.HasUsageMetricsSnapshotAsync(date); + + public Task StoreAppInstallSummaryAsync(AppInstallSummary summary) + => _storage.StoreAppInstallSummaryAsync(summary); + + public Task> GetAppInstallSummariesByTenantAsync(string tenantId) + => _storage.GetAppInstallSummariesByTenantAsync(tenantId); + + public Task> GetAllAppInstallSummariesAsync() + => _storage.GetAllAppInstallSummariesAsync(); + + public Task GetPlatformStatsAsync() + => _storage.GetPlatformStatsAsync(); + + public Task SavePlatformStatsAsync(PlatformStats stats) + => _storage.SavePlatformStatsAsync(stats); + + public Task IncrementPlatformStatAsync(string field, long amount = 1) + => _storage.IncrementPlatformStatAsync(field, amount); + + public Task RecordUserLoginAsync(string tenantId, string upn, string? displayName, string? objectId) + => _storage.RecordUserLoginAsync(tenantId, upn, displayName, objectId); + + public Task GetUserActivityMetricsAsync(string tenantId) + => _storage.GetUserActivityMetricsAsync(tenantId); + + public Task GetAllUserActivityMetricsAsync() + => _storage.GetAllUserActivityMetricsAsync(); + + public Task<(int uniqueUsers, int loginCount)> GetUserActivityForDateAsync(string? tenantId, DateTime date) + => _storage.GetUserActivityForDateAsync(tenantId, date); + + public Task> GetMetricsSummaryAsync(string? tenantId) + => _storage.GetMetricsSummaryAsync(tenantId); + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableRuleRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableRuleRepository.cs new file mode 100644 index 00000000..1e885547 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableRuleRepository.cs @@ -0,0 +1,69 @@ +using AutopilotMonitor.Shared.DataAccess; +using AutopilotMonitor.Shared.Models; +using AutopilotMonitor.Functions.Services; + +namespace AutopilotMonitor.Functions.DataAccess.TableStorage +{ + /// + /// Table Storage implementation of IRuleRepository. + /// Delegates to existing TableStorageService for backwards compatibility. + /// + public class TableRuleRepository : IRuleRepository + { + private readonly TableStorageService _storage; + private readonly IDataEventPublisher _publisher; + + public TableRuleRepository(TableStorageService storage, IDataEventPublisher publisher) + { + _storage = storage; + _publisher = publisher; + } + + public async Task StoreRuleResultAsync(RuleResult result) + { + var success = await _storage.StoreRuleResultAsync(result); + if (success) + await _publisher.PublishAsync("rule.evaluated", result, result.TenantId); + return success; + } + + public Task> GetRuleResultsAsync(string tenantId, string sessionId) + => _storage.GetRuleResultsAsync(tenantId, sessionId); + + public Task StoreGatherRuleAsync(GatherRule rule, string tenantId = "global") + => _storage.StoreGatherRuleAsync(rule, tenantId); + + public Task> GetGatherRulesAsync(string partitionKey) + => _storage.GetGatherRulesAsync(partitionKey); + + public Task DeleteGatherRuleAsync(string tenantId, string ruleId) + => _storage.DeleteGatherRuleAsync(tenantId, ruleId); + + public Task StoreRuleStateAsync(string tenantId, string ruleId, bool enabled) + => _storage.StoreRuleStateAsync(tenantId, ruleId, enabled); + + public Task> GetRuleStatesAsync(string tenantId) + => _storage.GetRuleStatesAsync(tenantId); + + public Task DeleteRuleStateAsync(string tenantId, string ruleId) + => _storage.DeleteRuleStateAsync(tenantId, ruleId); + + public Task StoreAnalyzeRuleAsync(AnalyzeRule rule, string tenantId = "global") + => _storage.StoreAnalyzeRuleAsync(rule, tenantId); + + public Task> GetAnalyzeRulesAsync(string partitionKey) + => _storage.GetAnalyzeRulesAsync(partitionKey); + + public Task DeleteAnalyzeRuleAsync(string tenantId, string ruleId) + => _storage.DeleteAnalyzeRuleAsync(tenantId, ruleId); + + public Task StoreImeLogPatternAsync(ImeLogPattern pattern, string tenantId = "global") + => _storage.StoreImeLogPatternAsync(pattern, tenantId); + + public Task> GetImeLogPatternsAsync(string partitionKey) + => _storage.GetImeLogPatternsAsync(partitionKey); + + public Task DeleteImeLogPatternAsync(string tenantId, string patternId) + => _storage.DeleteImeLogPatternAsync(tenantId, patternId); + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableSessionRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableSessionRepository.cs new file mode 100644 index 00000000..5eb89389 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableSessionRepository.cs @@ -0,0 +1,102 @@ +using AutopilotMonitor.Shared.DataAccess; +using AutopilotMonitor.Shared.Models; +using AutopilotMonitor.Functions.Services; + +namespace AutopilotMonitor.Functions.DataAccess.TableStorage +{ + /// + /// Table Storage implementation of ISessionRepository. + /// Delegates to existing TableStorageService for backwards compatibility. + /// + public class TableSessionRepository : ISessionRepository + { + private readonly TableStorageService _storage; + private readonly IDataEventPublisher _publisher; + + public TableSessionRepository(TableStorageService storage, IDataEventPublisher publisher) + { + _storage = storage; + _publisher = publisher; + } + + public async Task StoreSessionAsync(SessionRegistration registration) + { + var result = await _storage.StoreSessionAsync(registration); + if (result) + await _publisher.PublishAsync("session.created", registration, registration.TenantId); + return result; + } + + public Task GetSessionAsync(string tenantId, string sessionId) + => _storage.GetSessionAsync(tenantId, sessionId); + + public Task GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null) + => _storage.GetSessionsAsync(tenantId, maxResults, cursor); + + public Task GetAllSessionsAsync(int maxResults = 100, string? cursor = null) + => _storage.GetAllSessionsAsync(maxResults, cursor); + + public Task DeleteSessionAsync(string tenantId, string sessionId) + => _storage.DeleteSessionAsync(tenantId, sessionId); + + public Task UpdateSessionStatusAsync( + string tenantId, string sessionId, SessionStatus status, + EnrollmentPhase? currentPhase = null, string? failureReason = null, + DateTime? completedAt = null, DateTime? earliestEventTimestamp = null, + DateTime? latestEventTimestamp = null, bool? isPreProvisioned = null, + bool? isUserDriven = null, DateTime? resumedAt = null) + => _storage.UpdateSessionStatusAsync(tenantId, sessionId, status, + currentPhase, failureReason, completedAt, earliestEventTimestamp, + latestEventTimestamp, isPreProvisioned, isUserDriven, resumedAt); + + public Task IncrementSessionEventCountAsync( + string tenantId, string sessionId, int increment, + DateTime? earliestEventTimestamp = null, DateTime? latestEventTimestamp = null, + EnrollmentPhase? currentPhase = null) + => _storage.IncrementSessionEventCountAsync(tenantId, sessionId, increment, + earliestEventTimestamp, latestEventTimestamp, currentPhase); + + public Task UpdateSessionDiagnosticsBlobAsync(string tenantId, string sessionId, string blobName) + => _storage.UpdateSessionDiagnosticsBlobAsync(tenantId, sessionId, blobName); + + public Task SetSessionPreProvisionedAsync(string tenantId, string sessionId, bool isPreProvisioned, + SessionStatus? status = null, bool? isUserDriven = null) + => _storage.SetSessionPreProvisionedAsync(tenantId, sessionId, isPreProvisioned, status, isUserDriven); + + public Task UpdateSessionGeoAsync(string tenantId, string sessionId, + string? country, string? region, string? city, string? loc) + => _storage.UpdateSessionGeoAsync(tenantId, sessionId, country, region, city, loc); + + public Task UpdateSessionImeAgentVersionAsync(string tenantId, string sessionId, string version) + => _storage.UpdateSessionImeAgentVersionAsync(tenantId, sessionId, version); + + public async Task StoreEventAsync(EnrollmentEvent evt) + { + var result = await _storage.StoreEventAsync(evt); + if (result) + await _publisher.PublishAsync("event.ingested", evt, evt.TenantId); + return result; + } + + public Task> SearchSessionsAsync(string? tenantId, SessionSearchFilter filter) + => _storage.SearchSessionsAsync(tenantId, filter); + + public Task> SearchSessionsByEventAsync( + string? tenantId, string eventType, string? source, string? severity, + string? phase, int limit = 50) + => _storage.SearchSessionsByEventAsync(tenantId, eventType, source, severity, phase, limit); + + public Task> SearchSessionsByCveAsync( + string? tenantId, string cveId, double? minCvssScore, string? overallRisk, int limit = 50) + => _storage.SearchSessionsByCveAsync(tenantId, cveId, minCvssScore, overallRisk, limit); + + public Task UpsertEventTypeIndexBatchAsync(string tenantId, string sessionId, IEnumerable eventTypes) + => _storage.UpsertEventTypeIndexBatchAsync(tenantId, sessionId, eventTypes); + + public Task UpsertDeviceSnapshotAsync(string tenantId, string sessionId, IEnumerable events) + => _storage.UpsertDeviceSnapshotAsync(tenantId, sessionId, events); + + public Task UpsertCveIndexEntriesAsync(string tenantId, string sessionId, List> findings) + => _storage.UpsertCveIndexEntriesAsync(tenantId, sessionId, findings); + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableStorageInitializer.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableStorageInitializer.cs new file mode 100644 index 00000000..107e8e59 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableStorageInitializer.cs @@ -0,0 +1,22 @@ +using AutopilotMonitor.Shared.DataAccess; +using AutopilotMonitor.Functions.Services; + +namespace AutopilotMonitor.Functions.DataAccess.TableStorage +{ + /// + /// Table Storage implementation of IStorageInitializer. + /// Delegates to existing TableStorageService for backwards compatibility. + /// + public class TableStorageInitializer : IStorageInitializer + { + private readonly TableStorageService _storage; + + public TableStorageInitializer(TableStorageService storage) + { + _storage = storage; + } + + public Task InitializeAsync() + => _storage.InitializeTablesAsync(); + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableVulnerabilityRepository.cs b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableVulnerabilityRepository.cs new file mode 100644 index 00000000..07d395a9 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/DataAccess/TableStorage/TableVulnerabilityRepository.cs @@ -0,0 +1,43 @@ +using AutopilotMonitor.Shared.DataAccess; +using AutopilotMonitor.Functions.Services; + +namespace AutopilotMonitor.Functions.DataAccess.TableStorage +{ + /// + /// Table Storage implementation of IVulnerabilityRepository. + /// Delegates to existing TableStorageService for backwards compatibility. + /// + public class TableVulnerabilityRepository : IVulnerabilityRepository + { + private readonly TableStorageService _storage; + private readonly IDataEventPublisher _publisher; + + public TableVulnerabilityRepository(TableStorageService storage, IDataEventPublisher publisher) + { + _storage = storage; + _publisher = publisher; + } + + public async Task StoreVulnerabilityReportAsync(string tenantId, string sessionId, Dictionary reportData) + { + await _storage.StoreVulnerabilityReportAsync(tenantId, sessionId, reportData); + await _publisher.PublishAsync("vulnerability.reported", new { tenantId, sessionId, reportData }, tenantId); + } + + public Task?> GetVulnerabilityReportAsync(string tenantId, string sessionId) + => _storage.GetVulnerabilityReportAsync(tenantId, sessionId); + + public Task UpsertSoftwareInventoryAsync(string tenantId, List> inventoryItems, + string sessionId, Dictionary? cpeMappings = null) + => _storage.UpsertSoftwareInventoryAsync(tenantId, inventoryItems, sessionId, cpeMappings); + + public Task>> GetSoftwareInventoryAsync(string tenantId) + => _storage.GetSoftwareInventoryAsync(tenantId); + + public Task ImportCpeMappingSeedFromJsonAsync(string json) + => _storage.ImportCpeMappingSeedFromJsonAsync(json); + + public Task ImportCpeCommunityMappingsFromJsonAsync(string json) + => _storage.ImportCpeCommunityMappingsFromJsonAsync(json); + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/Program.cs b/src/Backend/AutopilotMonitor.Functions/Program.cs index 6afbb024..3c73116e 100644 --- a/src/Backend/AutopilotMonitor.Functions/Program.cs +++ b/src/Backend/AutopilotMonitor.Functions/Program.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; +using AutopilotMonitor.Functions.DataAccess; using AutopilotMonitor.Functions.Functions.Config; using AutopilotMonitor.Functions.Functions.Ingest; using AutopilotMonitor.Functions.Functions.Sessions; @@ -100,6 +101,11 @@ builder.Services.AddMemoryCache(); builder.Services.AddHttpClient(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); // Initialize all tables at startup + +// Data Access Layer — repository interfaces backed by Table Storage. +// To switch to Cosmos DB: replace AddTableStorageDataAccess() with AddCosmosDataAccess(). +// To add event streaming: chain .AddEventStreaming() after this call. +builder.Services.AddTableStorageDataAccess(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IAdminRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IAdminRepository.cs new file mode 100644 index 00000000..d05446fe --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IAdminRepository.cs @@ -0,0 +1,64 @@ +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Repository for admin and tenant member management. + /// Covers: GlobalAdmins, TenantAdmins, ApiKeys tables. + /// + public interface IAdminRepository + { + // --- Global Admins --- + Task IsGlobalAdminAsync(string upn); + Task> GetAllGlobalAdminsAsync(); + Task AddGlobalAdminAsync(string upn, string addedBy); + Task RemoveGlobalAdminAsync(string upn); + Task DisableGlobalAdminAsync(string upn); + + // --- Tenant Members --- + Task> GetTenantMembersAsync(string tenantId); + Task AddTenantMemberAsync(string tenantId, string upn, string addedBy, string role, bool canManageBootstrapTokens = false); + Task RemoveTenantMemberAsync(string tenantId, string upn); + Task UpdateMemberPermissionsAsync(string tenantId, string upn, string role, bool canManageBootstrapTokens); + Task GetTenantMemberAsync(string tenantId, string upn); + Task IsTenantAdminAsync(string tenantId, string upn); + Task IsTenantMemberAsync(string tenantId, string upn); + + // --- API Keys --- + Task ValidateApiKeyAsync(string apiKeyHash); + Task StoreApiKeyAsync(string tenantId, ApiKeyEntry entry); + Task> GetApiKeysAsync(string tenantId); + Task RevokeApiKeyAsync(string tenantId, string keyId); + } + + public class GlobalAdminEntry + { + public string Upn { get; set; } = string.Empty; + public bool IsEnabled { get; set; } = true; + public DateTime AddedAt { get; set; } + public string AddedBy { get; set; } = string.Empty; + } + + public class TenantMember + { + public string Upn { get; set; } = string.Empty; + public string TenantId { get; set; } = string.Empty; + public string Role { get; set; } = "Admin"; + public bool IsEnabled { get; set; } = true; + public bool CanManageBootstrapTokens { get; set; } + public DateTime AddedAt { get; set; } + public string AddedBy { get; set; } = string.Empty; + } + + public class ApiKeyEntry + { + public string KeyId { get; set; } = string.Empty; + public string TenantId { get; set; } = string.Empty; + public string KeyHash { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Scope { get; set; } = "tenant"; + public string CreatedBy { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } + public DateTime? ExpiresAt { get; set; } + public bool IsActive { get; set; } = true; + public long RequestCount { get; set; } + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IBootstrapRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IBootstrapRepository.cs new file mode 100644 index 00000000..efedf96a --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IBootstrapRepository.cs @@ -0,0 +1,18 @@ +using AutopilotMonitor.Shared.Models; + +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Repository for bootstrap session management. + /// Covers: BootstrapSessions table. + /// + public interface IBootstrapRepository + { + Task CreateBootstrapSessionAsync(BootstrapSession session); + Task GetBootstrapSessionByCodeAsync(string shortCode); + Task ValidateBootstrapTokenAsync(string token); + Task> GetBootstrapSessionsAsync(string tenantId); + Task RevokeBootstrapSessionAsync(string shortCode); + Task IncrementBootstrapUsageAsync(string shortCode); + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IConfigRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IConfigRepository.cs new file mode 100644 index 00000000..e0cab840 --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IConfigRepository.cs @@ -0,0 +1,30 @@ +using AutopilotMonitor.Shared.Models; + +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Repository for tenant and admin configuration. + /// Covers: TenantConfiguration, AdminConfiguration, PreviewWhitelist, PreviewConfig tables. + /// + public interface IConfigRepository + { + // --- Tenant Configuration --- + Task GetTenantConfigurationAsync(string tenantId); + Task SaveTenantConfigurationAsync(TenantConfiguration config); + Task> GetAllTenantConfigurationsAsync(); + + // --- Admin Configuration --- + Task GetAdminConfigurationAsync(); + Task SaveAdminConfigurationAsync(AdminConfiguration config); + + // --- Preview Whitelist --- + Task IsInPreviewWhitelistAsync(string tenantId); + Task AddToPreviewWhitelistAsync(string tenantId, string addedBy); + Task RemoveFromPreviewWhitelistAsync(string tenantId); + Task> GetPreviewWhitelistAsync(); + + // --- Preview Config --- + Task> GetPreviewConfigAsync(); + Task SavePreviewConfigAsync(string key, string value); + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IDataEventPublisher.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IDataEventPublisher.cs new file mode 100644 index 00000000..a7e71fb1 --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IDataEventPublisher.cs @@ -0,0 +1,27 @@ +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Publishes data change events for downstream consumers (Event Hub, Service Bus, etc.). + /// Default implementation is NullDataEventPublisher (no-op). + /// + public interface IDataEventPublisher + { + /// + /// Publishes a data change event. + /// + /// Domain event type (e.g. "session.created", "event.ingested", "rule.evaluated") + /// The event payload (typically the domain object) + /// Optional tenant context for routing + Task PublishAsync(string eventType, object payload, string? tenantId = null); + } + + /// + /// No-op implementation of IDataEventPublisher. + /// Used when event streaming is not configured. + /// + public class NullDataEventPublisher : IDataEventPublisher + { + public Task PublishAsync(string eventType, object payload, string? tenantId = null) + => Task.CompletedTask; + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IDeviceSecurityRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IDeviceSecurityRepository.cs new file mode 100644 index 00000000..bececb33 --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IDeviceSecurityRepository.cs @@ -0,0 +1,44 @@ +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Repository for device and version blocking. + /// Covers: BlockedDevices, BlockedVersions tables. + /// + public interface IDeviceSecurityRepository + { + // --- Blocked Devices --- + Task<(bool isBlocked, DateTime? unblockAt, string action)> IsDeviceBlockedAsync(string tenantId, string serialNumber); + Task> GetBlockedDevicesAsync(string tenantId); + Task> GetAllBlockedDevicesAsync(); + Task BlockDeviceAsync(string tenantId, string serialNumber, int durationHours, + string blockedByEmail, string? reason = null, string action = "Block"); + Task UnblockDeviceAsync(string tenantId, string serialNumber); + + // --- Blocked Versions --- + Task<(bool isBlocked, string action, string? matchedPattern)> IsVersionBlockedAsync(string agentVersion); + Task> GetBlockedVersionsAsync(); + Task BlockVersionAsync(string versionPattern, string action, string createdByEmail, string? reason = null); + Task UnblockVersionAsync(string versionPattern); + } + + public class BlockedDeviceEntry + { + public string TenantId { get; set; } = string.Empty; + public string SerialNumber { get; set; } = string.Empty; + public DateTime BlockedAt { get; set; } + public DateTime? UnblockAt { get; set; } + public string? BlockedByEmail { get; set; } + public int DurationHours { get; set; } + public string? Reason { get; set; } + public string Action { get; set; } = "Block"; + } + + public class BlockedVersionEntry + { + public string VersionPattern { get; set; } = string.Empty; + public string Action { get; set; } = "Block"; + public string CreatedByEmail { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } + public string? Reason { get; set; } + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IMaintenanceRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IMaintenanceRepository.cs new file mode 100644 index 00000000..8f0240dd --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IMaintenanceRepository.cs @@ -0,0 +1,36 @@ +using AutopilotMonitor.Shared.Models; + +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Repository for audit logs and data retention/maintenance operations. + /// Covers: AuditLogs table + maintenance queries across Sessions/Events/RuleResults. + /// + public interface IMaintenanceRepository + { + // --- Audit Logs --- + Task LogAuditEntryAsync(string tenantId, string action, string entityType, + string entityId, string performedBy, Dictionary? details = null); + Task> GetAuditLogsAsync(string tenantId, int maxResults = 100); + Task> GetAllAuditLogsAsync(int maxResults = 100); + + // --- Data Retention Queries --- + Task> GetSessionsOlderThanAsync(string tenantId, DateTime cutoffDate); + Task> GetSessionsByDateRangeAsync(DateTime startDate, DateTime endDate, string? tenantId = null); + Task> GetStalledSessionsAsync(string tenantId, DateTime cutoffTime); + Task> GetExcessiveDataSendersAsync(string tenantId, DateTime windowCutoff, int maxSessionWindowHours); + + // --- Tenant Discovery --- + Task> GetAllTenantIdsAsync(); + + // --- Cleanup --- + Task DeleteSessionEventsAsync(string tenantId, string sessionId); + Task DeleteSessionRuleResultsAsync(string tenantId, string sessionId); + Task DeleteSessionAppInstallSummariesAsync(string tenantId, string sessionId); + + // --- Index Maintenance --- + Task BackfillSessionIndexAsync(); + Task CleanupGhostSessionIndexEntriesAsync(); + Task IsSessionIndexEmptyAsync(); + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IMetricsRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IMetricsRepository.cs new file mode 100644 index 00000000..c0cad9fd --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IMetricsRepository.cs @@ -0,0 +1,36 @@ +using AutopilotMonitor.Shared.Models; + +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Repository for usage metrics, platform stats, and user activity tracking. + /// Covers: UsageMetrics, PlatformStats, UserActivity, AppInstallSummaries tables. + /// + public interface IMetricsRepository + { + // --- Usage Metrics Snapshots --- + Task SaveUsageMetricsSnapshotAsync(UsageMetricsSnapshot metrics); + Task> GetUsageMetricsSnapshotAsync( + string? tenantId = null, string? startDate = null, string? endDate = null, int maxResults = 100); + Task HasUsageMetricsSnapshotAsync(string date); + + // --- App Install Summaries --- + Task StoreAppInstallSummaryAsync(AppInstallSummary summary); + Task> GetAppInstallSummariesByTenantAsync(string tenantId); + Task> GetAllAppInstallSummariesAsync(); + + // --- Platform Stats --- + Task GetPlatformStatsAsync(); + Task SavePlatformStatsAsync(PlatformStats stats); + Task IncrementPlatformStatAsync(string field, long amount = 1); + + // --- User Activity --- + Task RecordUserLoginAsync(string tenantId, string upn, string? displayName, string? objectId); + Task GetUserActivityMetricsAsync(string tenantId); + Task GetAllUserActivityMetricsAsync(); + Task<(int uniqueUsers, int loginCount)> GetUserActivityForDateAsync(string? tenantId, DateTime date); + + // --- Metrics Summary (Agent API) --- + Task> GetMetricsSummaryAsync(string? tenantId); + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/INotificationRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/INotificationRepository.cs new file mode 100644 index 00000000..5a8b9044 --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/INotificationRepository.cs @@ -0,0 +1,34 @@ +using AutopilotMonitor.Shared.Models; + +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Repository for global notifications and session reports. + /// Covers: GlobalNotifications, SessionReports tables. + /// + public interface INotificationRepository + { + // --- Global Notifications --- + Task AddNotificationAsync(GlobalNotification notification); + Task> GetNotificationsAsync(int maxResults = 50); + Task DismissNotificationAsync(string notificationId, string dismissedBy); + + // --- Session Reports --- + Task StoreSessionReportMetadataAsync(SessionReportMetadata metadata); + Task> GetSessionReportsAsync(string? tenantId = null, int maxResults = 50); + Task GetSessionReportAsync(string reportId); + } + + public class GlobalNotification + { + public string NotificationId { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + public string Severity { get; set; } = "info"; + public DateTime CreatedAt { get; set; } + public string CreatedBy { get; set; } = string.Empty; + public bool IsDismissed { get; set; } + public string? DismissedBy { get; set; } + public DateTime? DismissedAt { get; set; } + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IRuleRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IRuleRepository.cs new file mode 100644 index 00000000..a490782d --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IRuleRepository.cs @@ -0,0 +1,35 @@ +using AutopilotMonitor.Shared.Models; + +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Repository for rule definitions, results, and patterns. + /// Covers: RuleResults, GatherRules, AnalyzeRules, ImeLogPatterns, RuleStates tables. + /// + public interface IRuleRepository + { + // --- Rule Results --- + Task StoreRuleResultAsync(RuleResult result); + Task> GetRuleResultsAsync(string tenantId, string sessionId); + + // --- Gather Rules --- + Task StoreGatherRuleAsync(GatherRule rule, string tenantId = "global"); + Task> GetGatherRulesAsync(string partitionKey); + Task DeleteGatherRuleAsync(string tenantId, string ruleId); + + // --- Rule States --- + Task StoreRuleStateAsync(string tenantId, string ruleId, bool enabled); + Task> GetRuleStatesAsync(string tenantId); + Task DeleteRuleStateAsync(string tenantId, string ruleId); + + // --- Analyze Rules --- + Task StoreAnalyzeRuleAsync(AnalyzeRule rule, string tenantId = "global"); + Task> GetAnalyzeRulesAsync(string partitionKey); + Task DeleteAnalyzeRuleAsync(string tenantId, string ruleId); + + // --- IME Log Patterns --- + Task StoreImeLogPatternAsync(ImeLogPattern pattern, string tenantId = "global"); + Task> GetImeLogPatternsAsync(string partitionKey); + Task DeleteImeLogPatternAsync(string tenantId, string patternId); + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/ISessionRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/ISessionRepository.cs new file mode 100644 index 00000000..96dad8aa --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/ISessionRepository.cs @@ -0,0 +1,52 @@ +using AutopilotMonitor.Shared.Models; + +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Repository for session and event data. + /// Covers: Sessions, SessionsIndex, Events tables. + /// + public interface ISessionRepository + { + // --- Session CRUD --- + Task StoreSessionAsync(SessionRegistration registration); + Task GetSessionAsync(string tenantId, string sessionId); + Task GetSessionsAsync(string tenantId, int maxResults = 100, string? cursor = null); + Task GetAllSessionsAsync(int maxResults = 100, string? cursor = null); + Task DeleteSessionAsync(string tenantId, string sessionId); + + // --- Session Updates --- + Task UpdateSessionStatusAsync( + string tenantId, string sessionId, SessionStatus status, + EnrollmentPhase? currentPhase = null, string? failureReason = null, + DateTime? completedAt = null, DateTime? earliestEventTimestamp = null, + DateTime? latestEventTimestamp = null, bool? isPreProvisioned = null, + bool? isUserDriven = null, DateTime? resumedAt = null); + Task IncrementSessionEventCountAsync( + string tenantId, string sessionId, int increment, + DateTime? earliestEventTimestamp = null, DateTime? latestEventTimestamp = null, + EnrollmentPhase? currentPhase = null); + Task UpdateSessionDiagnosticsBlobAsync(string tenantId, string sessionId, string blobName); + Task SetSessionPreProvisionedAsync(string tenantId, string sessionId, bool isPreProvisioned, + SessionStatus? status = null, bool? isUserDriven = null); + Task UpdateSessionGeoAsync(string tenantId, string sessionId, + string? country, string? region, string? city, string? loc); + Task UpdateSessionImeAgentVersionAsync(string tenantId, string sessionId, string version); + + // --- Events --- + Task StoreEventAsync(EnrollmentEvent evt); + + // --- Search --- + Task> SearchSessionsAsync(string? tenantId, SessionSearchFilter filter); + Task> SearchSessionsByEventAsync( + string? tenantId, string eventType, string? source, string? severity, + string? phase, int limit = 50); + Task> SearchSessionsByCveAsync( + string? tenantId, string cveId, double? minCvssScore, string? overallRisk, int limit = 50); + + // --- Agent Indexes --- + Task UpsertEventTypeIndexBatchAsync(string tenantId, string sessionId, IEnumerable eventTypes); + Task UpsertDeviceSnapshotAsync(string tenantId, string sessionId, IEnumerable events); + Task UpsertCveIndexEntriesAsync(string tenantId, string sessionId, List> findings); + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IStorageInitializer.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IStorageInitializer.cs new file mode 100644 index 00000000..34b7f646 --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IStorageInitializer.cs @@ -0,0 +1,11 @@ +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Initializes the storage backend (creates tables, containers, collections, etc.). + /// Each storage provider implements this for its specific setup needs. + /// + public interface IStorageInitializer + { + Task InitializeAsync(); + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/IVulnerabilityRepository.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/IVulnerabilityRepository.cs new file mode 100644 index 00000000..d9c5905a --- /dev/null +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/IVulnerabilityRepository.cs @@ -0,0 +1,17 @@ +namespace AutopilotMonitor.Shared.DataAccess +{ + /// + /// Repository for vulnerability data (CPE mappings, CVE reports, software inventory). + /// Covers: VulnerabilityCache, VulnerabilityReports, SoftwareInventory, CveIndex tables. + /// + public interface IVulnerabilityRepository + { + Task StoreVulnerabilityReportAsync(string tenantId, string sessionId, Dictionary reportData); + Task?> GetVulnerabilityReportAsync(string tenantId, string sessionId); + Task UpsertSoftwareInventoryAsync(string tenantId, List> inventoryItems, + string sessionId, Dictionary? cpeMappings = null); + Task>> GetSoftwareInventoryAsync(string tenantId); + Task ImportCpeMappingSeedFromJsonAsync(string json); + Task ImportCpeCommunityMappingsFromJsonAsync(string json); + } +} diff --git a/tasks/todo.md b/tasks/todo.md index 7958cf89..aa34177e 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -1,16 +1,74 @@ -# Tenant Config Report (Global Admin) +# Data Access Layer — Migration Plan -## Goal -New global-admin-only page that shows a read-only report of a tenant's full configuration, -including admin-set parameters and computed runtime (agent) parameters. +## Phase 1: Foundation ✅ +- [x] Define 10 Repository Interfaces in `Shared/DataAccess/` +- [x] Define `IDataEventPublisher` + `NullDataEventPublisher` +- [x] Define `IStorageInitializer` +- [x] Create 5 core Table Storage implementations (delegate to `TableStorageService`) +- [x] Create `DataAccessServiceExtensions` with `AddTableStorageDataAccess()` +- [x] Register DAL in `Program.cs` -## Plan -- [x] Write plan -- [ ] Create `/app/tenant-config-report/page.tsx` — global admin only, read-only report - - Tenant selector (reuse pattern from gather-rules page) - - Fetch config via `GET /api/config/{tenantId}` (existing endpoint, GA cross-tenant) - - Display all TenantConfiguration fields grouped by section - - Show computed runtime parameters (AgentConfigResponse equivalents with defaults) - - Highlight non-default values for quick scanning -- [ ] Add nav item to global admin section in `globalNavConfig.tsx` -- [ ] Commit and push to feature branch +## Phase 2: Migrate Core Functions to Use Interfaces +These functions currently inject `TableStorageService` directly. Migrate to inject repository interfaces instead. + +### Session Functions +- [ ] `GetSessionsFunction` → `ISessionRepository` +- [ ] `GetSessionFunction` → `ISessionRepository` +- [ ] `DeleteSessionFunction` → `ISessionRepository` +- [ ] `RegisterSessionFunction` → `ISessionRepository` +- [ ] `IngestEventsFunction` → `ISessionRepository` +- [ ] `SearchSessionsFunction` → `ISessionRepository` +- [ ] `SearchSessionsByEventFunction` → `ISessionRepository` +- [ ] `SearchSessionsByCveFunction` → `ISessionRepository` + +### Rule Functions +- [ ] `GetRuleResultsFunction` → `IRuleRepository` +- [ ] `RunRulesFunction` → `IRuleRepository` +- [ ] Rule management functions → `IRuleRepository` + +### Metrics Functions +- [ ] `GetMetricsFunction` → `IMetricsRepository` +- [ ] `GetHistoricalMetricsFunction` → `IMetricsRepository` +- [ ] `GetPlatformStatsFunction` → `IMetricsRepository` + +### Maintenance Functions +- [ ] `GetAuditLogsFunction` → `IMaintenanceRepository` +- [ ] `MaintenanceFunction` → `IMaintenanceRepository` + +### Vulnerability Functions +- [ ] All vulnerability functions → `IVulnerabilityRepository` + +## Phase 3: Migrate "Bypass" Services +These services create their own `TableServiceClient` instances. Create proper repository implementations and migrate. + +- [ ] `TenantConfigurationService` → `IConfigRepository` +- [ ] `AdminConfigurationService` → `IConfigRepository` +- [ ] `PreviewWhitelistService` → `IConfigRepository` +- [ ] `GlobalAdminService` → `IAdminRepository` +- [ ] `TenantAdminsService` → `IAdminRepository` +- [ ] `BootstrapSessionService` → `IBootstrapRepository` +- [ ] `GlobalNotificationService` → `INotificationRepository` +- [ ] `SessionReportService` → `INotificationRepository` +- [ ] `BlockedDeviceService` → `IDeviceSecurityRepository` +- [ ] `BlockedVersionService` → `IDeviceSecurityRepository` +- [ ] `ApiKeyMiddleware` → `IAdminRepository` + +## Phase 4: Remove Legacy Code +- [ ] Remove `GetTableClient()` and `GetTableServiceClient()` from `TableStorageService` +- [ ] Move `TableStorageService` logic into repository implementations +- [ ] Delete `TableStorageService` partial files once fully migrated +- [ ] Remove direct `TableServiceClient` creation from all services + +## Phase 5: Event Streaming (When Ready) +- [ ] Implement `EventHubPublisher` (or `ServiceBusPublisher`) +- [ ] Register via `services.AddEventStreaming()` +- [ ] Define event schemas for key domain events +- [ ] Add event consumers for downstream processing + +## Phase 6: Cosmos DB Migration (When Ready) +- [ ] Create `CosmosSessionRepository`, `CosmosRuleRepository`, etc. +- [ ] Create `AddCosmosDataAccess()` extension method +- [ ] Replace inverted-tick indexing with native ORDER BY DESC +- [ ] Replace denormalized indexes with Cosmos queries +- [ ] Handle 2MB document limit vs. 64KB Table Storage limit +- [ ] Swap `AddTableStorageDataAccess()` → `AddCosmosDataAccess()` in Program.cs