diff --git a/BeyondTrustConnector/LicenseUsageUpdater.cs b/BeyondTrustConnector/LicenseUsageUpdater.cs new file mode 100644 index 0000000..0e000a6 --- /dev/null +++ b/BeyondTrustConnector/LicenseUsageUpdater.cs @@ -0,0 +1,52 @@ +using System; +using System.IO.Compression; +using BeyondTrustConnector.Model.Dto; +using BeyondTrustConnector.Service; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; + +namespace BeyondTrustConnector +{ + public class LicenseUsageUpdater(BeyondTrustService beyondTrustService, IngestionService ingestionService, ILogger logger) + { + [Function(nameof(LicenseUsageUpdater))] + public async Task Run([TimerTrigger("0 0 8 * * *", RunOnStartup = false)] TimerInfo myTimer) + { + var reportArchive = await beyondTrustService.GetEndpointLicenseUsageReportAsync(); + ZipArchive archive = new(new MemoryStream(reportArchive)); + var jumpItemReport = archive.GetEntry("jump_items.csv"); + if (jumpItemReport is null) + { + logger.LogError("Jump Items report not found in archive"); + throw new Exception("Jump Items report not found in archive"); + } + + using var jumpItemStream = jumpItemReport.Open(); + using var jumpItemReader = new StreamReader(jumpItemStream); + var csvLine = await jumpItemReader.ReadLineAsync(); + + var licenseEntries = new List(); + while ((csvLine = await jumpItemReader.ReadLineAsync()) != null){ + var fields = csvLine.Split(','); + var licenseEntry = new BeyondTrustLicenseEntryDto + { + JumpItemName = fields[0].Trim('\"'), + HostnameOrIp = fields[1].Trim('\"'), + Jumpoint = fields[2].Trim('\"'), + RemoteApplicationName = fields[3].Trim('\"'), + License = fields[4].Trim('\"') == "yes", + JumpMethod = fields[5].Trim('\"'), + JumpGroup = fields[6].Trim('\"'), + }; + licenseEntries.Add(licenseEntry); + } + if(licenseEntries.Count == 0) + { + logger.LogWarning("No license entries found in report"); + return; + } + + await ingestionService.IngestLicenseUsage(licenseEntries); + } + } +} diff --git a/BeyondTrustConnector/Model/Dto/BeyondTrustLicenseEntryDto.cs b/BeyondTrustConnector/Model/Dto/BeyondTrustLicenseEntryDto.cs new file mode 100644 index 0000000..6b19da2 --- /dev/null +++ b/BeyondTrustConnector/Model/Dto/BeyondTrustLicenseEntryDto.cs @@ -0,0 +1,12 @@ +namespace BeyondTrustConnector.Model.Dto; + +internal class BeyondTrustLicenseEntryDto +{ + public required string JumpItemName { get; set; } + public required string HostnameOrIp { get; set; } + public required string JumpMethod { get; set; } + public required string JumpGroup { get; set; } + public bool License { get; set; } + public required string Jumpoint { get; set; } + public string? RemoteApplicationName { get; set; } +} diff --git a/BeyondTrustConnector/Service/BeyondTrustService.cs b/BeyondTrustConnector/Service/BeyondTrustService.cs index 6f9ff9d..6c48870 100644 --- a/BeyondTrustConnector/Service/BeyondTrustService.cs +++ b/BeyondTrustConnector/Service/BeyondTrustService.cs @@ -1,4 +1,5 @@ using BeyondTrustConnector.Model; +using BeyondTrustConnector.Model.Dto; using Microsoft.Extensions.Logging; using System.Net.Http.Json; using System.Xml; @@ -9,7 +10,7 @@ namespace BeyondTrustConnector.Service { public class BeyondTrustService(IHttpClientFactory httpClientFactory, ILogger logger) { - public async Task GetAccessSessionReport(DateTime start, int reportPeriod = 0) + internal async Task GetAccessSessionReport(DateTime start, int reportPeriod = 0) { var client = httpClientFactory.CreateClient(nameof(BeyondTrustConnector)); var unixTime = ((DateTimeOffset)start).ToUnixTimeSeconds(); @@ -31,7 +32,7 @@ public async Task GetAccessSessionReport(DateTime start, int repor return sessionList ?? throw new Exception("Failed to deserialize report"); } - public async Task GetVaultActivityReport(DateTime start, int reportPeriod = 0) + internal async Task GetVaultActivityReport(DateTime start, int reportPeriod = 0) { var client = httpClientFactory.CreateClient(nameof(BeyondTrustConnector)); var unixTime = ((DateTimeOffset)start).ToUnixTimeSeconds() + 1; @@ -48,7 +49,7 @@ public async Task GetVaultActivityReport(DateTime start, int reportPe return XDocument.Parse(reportContent); } - public async Task DownloadReportAsync(string report) + internal async Task DownloadReportAsync(string report) { var client = httpClientFactory.CreateClient(nameof(BeyondTrustConnector)); var response = await client.GetAsync($"/api/reporting?generate_report={report}"); @@ -62,6 +63,21 @@ public async Task DownloadReportAsync(string report) return reportData; } + internal async Task GetEndpointLicenseUsageReportAsync() + { + var client = httpClientFactory.CreateClient(nameof(BeyondTrustConnector)); + var response = await client.GetAsync("/api/reporting?generate_report=EndpointLicenseUsage"); + if (!response.IsSuccessStatusCode) + { + var message = await response.Content.ReadAsStringAsync(); + logger.LogError("Failed to get license usage: {ErrorMessage}", message); + throw new Exception("Failed to get license usage"); + } + var reportArchive = await response.Content.ReadAsByteArrayAsync(); + + return reportArchive; + } + private async Task GetItem(string endpoint, string query) { var client = httpClientFactory.CreateClient(nameof(BeyondTrustConnector)); diff --git a/BeyondTrustConnector/Service/IngestionService.cs b/BeyondTrustConnector/Service/IngestionService.cs index 264f1ef..0beda27 100644 --- a/BeyondTrustConnector/Service/IngestionService.cs +++ b/BeyondTrustConnector/Service/IngestionService.cs @@ -19,6 +19,11 @@ internal async Task IngestAccessSessions(List sessi await Ingest("BeyondTrustAccessSession_CL", sessions); } + internal async Task IngestLicenseUsage(List licenseEntries) + { + await Ingest("BeyondTrustLicenseUsage_CL", licenseEntries); + } + internal async Task IngestVaultActivity(List vaultActivities) { await Ingest("BeyondTrustVaultActivity_CL", vaultActivities); diff --git a/modules/datacollection.bicep b/modules/datacollection.bicep index b691ec2..e5fcf2e 100644 --- a/modules/datacollection.bicep +++ b/modules/datacollection.bicep @@ -19,7 +19,48 @@ resource dataCollectionEndpoint 'Microsoft.Insights/dataCollectionEndpoints@2023 } } - +resource Custom_Table_BeyondTrustLicenseUsage_CL 'Microsoft.OperationalInsights/workspaces/tables@2022-10-01' = { + parent: law + name: 'BeyondTrustLicenseUsage_CL' + properties: { + totalRetentionInDays: 30 + plan: 'Analytics' + schema: { + name: 'BeyondTrustLicenseUsage_CL' + columns: [ + { + name: 'TimeGenerated' + type: 'datetime' + } + { + name: 'Name' + type: 'string' + } + { + name: 'HostnameOrIp' + type: 'string' + } + { + name: 'Jumpoint' + type: 'string' + } + { + name: 'License' + type: 'boolean' + } + { + name: 'JumpMethod' + type: 'string' + } + { + name: 'JumpGroup' + type: 'string' + } + ] + } + retentionInDays: 30 + } +} resource Custom_Table_BeyondTrustVaultActivity_CL 'Microsoft.OperationalInsights/workspaces/tables@2022-10-01' = { parent: law @@ -184,6 +225,38 @@ resource dataCollectionRule 'Microsoft.Insights/dataCollectionRules@2023-03-11' properties: { dataCollectionEndpointId: dataCollectionEndpoint.id streamDeclarations: { + 'Custom-BeyondTrustLicenseUsage_CL': { + columns: [ + { + name: 'TimeGenerated' + type: 'datetime' + } + { + name: 'Name' + type: 'string' + } + { + name: 'HostnameOrIp' + type: 'string' + } + { + name: 'Jumpoint' + type: 'string' + } + { + name: 'License' + type: 'boolean' + } + { + name: 'JumpMethod' + type: 'string' + } + { + name: 'JumpGroup' + type: 'string' + } + ] + } 'Custom-BeyondTrustEvents_CL': { columns: [ { @@ -319,6 +392,16 @@ resource dataCollectionRule 'Microsoft.Insights/dataCollectionRules@2023-03-11' ] } dataFlows: [ + { + streams: [ + 'Custom-BeyondTrustLicenseUsage_CL' + ] + destinations: [ + 'beyondTrustWorkspace' + ] + transformKql: 'source\n| extend TimeGenerated=now()\n' + outputStream: 'Custom-BeyondTrustLicenseUsage_CL' + } { streams: [ 'Custom-BeyondTrustEvents_CL'