From d4347f2c046166e26927470e6cf28093bf907f43 Mon Sep 17 00:00:00 2001 From: Andrew Malkov Date: Fri, 12 Jan 2024 16:57:56 +0100 Subject: [PATCH] add benchmarks v3 --- src/Crisp.Core/Models/SecurityBenchmark.cs | 8 +- .../SecurityBenchmarksV11Repository.cs | 3 +- .../SecurityBenchmarksV2Repository.cs | 3 +- .../SecurityBenchmarksV3Repository.cs | 242 ++++++++++++++++++ .../Services/RecommendationsService.cs | 112 +++++++- .../ClientApp/src/components/Resources.css | 2 +- 6 files changed, 353 insertions(+), 17 deletions(-) create mode 100644 src/Crisp.Core/Repositories/SecurityBenchmarksV3Repository.cs diff --git a/src/Crisp.Core/Models/SecurityBenchmark.cs b/src/Crisp.Core/Models/SecurityBenchmark.cs index d9d3aab..ff1b22f 100644 --- a/src/Crisp.Core/Models/SecurityBenchmark.cs +++ b/src/Crisp.Core/Models/SecurityBenchmark.cs @@ -2,8 +2,14 @@ public record SecurityBenchmark( string Category, - string AzureId, string Title, string Description, + string? AzureId, + string? ControlId, + string? ControlTitle, + string? FeatureName, + string? FeatureDescription, + string? FeatureNotes, + string? FeatureReference, string? Responsibility ); diff --git a/src/Crisp.Core/Repositories/SecurityBenchmarksV11Repository.cs b/src/Crisp.Core/Repositories/SecurityBenchmarksV11Repository.cs index 5047519..bb2ebad 100644 --- a/src/Crisp.Core/Repositories/SecurityBenchmarksV11Repository.cs +++ b/src/Crisp.Core/Repositories/SecurityBenchmarksV11Repository.cs @@ -73,9 +73,10 @@ private static Task> GetAllSecurityBenchmarksAsyn } benchmarks.Add(new SecurityBenchmark( reader.GetValue(1)?.ToString() ?? "", - reader.GetValue(2)?.ToString() ?? "", title, description, + reader.GetValue(2)?.ToString(), + null, null, null, null, null, null, responsibility )); } diff --git a/src/Crisp.Core/Repositories/SecurityBenchmarksV2Repository.cs b/src/Crisp.Core/Repositories/SecurityBenchmarksV2Repository.cs index 1170ae5..f03df8a 100644 --- a/src/Crisp.Core/Repositories/SecurityBenchmarksV2Repository.cs +++ b/src/Crisp.Core/Repositories/SecurityBenchmarksV2Repository.cs @@ -63,9 +63,10 @@ private static Task> GetAllSecurityBenchmarksAsyn } benchmarks.Add(new SecurityBenchmark( reader.GetValue(1)?.ToString() ?? "", - reader.GetValue(2)?.ToString() ?? "", title, reader.GetValue(6)?.ToString() ?? "", + reader.GetValue(2)?.ToString(), + null, null, null, null, null, null, reader.GetValue(7)?.ToString() )); } diff --git a/src/Crisp.Core/Repositories/SecurityBenchmarksV3Repository.cs b/src/Crisp.Core/Repositories/SecurityBenchmarksV3Repository.cs new file mode 100644 index 0000000..45b187f --- /dev/null +++ b/src/Crisp.Core/Repositories/SecurityBenchmarksV3Repository.cs @@ -0,0 +1,242 @@ +using Crisp.Core.Models; +using ExcelDataReader; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Crisp.Core.Repositories; + +public class SecurityBenchmarksV3Repository : ISecurityBenchmarksRepository +{ + private const string SecurityBaselineVersion = "3"; + private const string SecurityBaselineFileSuffix = $"-azure-security-benchmark-v{SecurityBaselineVersion}-latest-security-baseline.xlsx"; + + public Task> GetAllResourceNamesAsync(string rootDirectoryPath) + { + return Task.Run>(() => + { + var benchmarksDirectory = GetBenchmarksDirectory(rootDirectoryPath); + if (!Directory.Exists(benchmarksDirectory)) + { + return Enumerable.Empty(); + } + + var resourceNames = Directory.GetFiles(benchmarksDirectory).Select(f => GetResourceNameFromSecurityBaselineFileName(f)).ToArray(); + return resourceNames; + }); + } + + public async Task> GetSecurityBenchmarksForResourceAsync(string resourceName, string rootDirectoryPath) + { + var fileFullName = GetFileFullNameForResource(rootDirectoryPath, resourceName); + if (!File.Exists(fileFullName)) + { + return Enumerable.Empty(); + } + + return await GetAllSecurityBenchmarksAsync(fileFullName); + } + + + private static Task> GetAllSecurityBenchmarksAsync(string fileFullName) + { + return Task.Run>(() => + { + var benchmarks = new List(); + // Register the code pages to support ExcelDataReader on non-Windows systems + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + using var stream = File.Open(fileFullName, FileMode.Open, FileAccess.Read); + using var reader = ExcelReaderFactory.CreateReader(stream); + // Skip the first sheet and move to the second one + reader.NextResult(); + // Skip the header row + reader.Read(); + while (reader.Read()) + { + var title = reader.GetValue(2)?.ToString(); + if (string.IsNullOrEmpty(title)) + { + break; + } + + var responsibility = reader.GetValue(4)?.ToString(); + if (string.Equals(responsibility, "Not Applicable", StringComparison.OrdinalIgnoreCase) || + string.Equals(responsibility, "Microsoft", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var description = reader.GetValue(3)?.ToString() ?? ""; + if (description.StartsWith("This feature is not applicable")) + { + continue; + } + + benchmarks.Add(new SecurityBenchmark( + reader.GetValue(0)?.ToString() ?? "", + title, + description, + null, + reader.GetValue(1)?.ToString(), + reader.GetValue(2)?.ToString(), + reader.GetValue(5)?.ToString(), + reader.GetValue(6)?.ToString(), + reader.GetValue(10)?.ToString(), + reader.GetValue(9)?.ToString(), + responsibility + )); + } + return benchmarks; + }); + } + + private static string GetFileFullNameForResource(string rootDirectoryPath, string resourceName) + { + return Path.Combine(GetBenchmarksDirectory(rootDirectoryPath), GetSecurityBaselineFileName(resourceName)); + } + + private static string GetBenchmarksDirectory(string rootDirectoryPath) + { + return Path.Combine(rootDirectoryPath, "Azure Offer Security Baselines", SecurityBaselineVersion + ".0"); + } + + private static string GetSecurityBaselineFileName(string resourceName) + { + var filePrefix = resourceName.Trim().ToLower().Replace(' ', '-'); + return $"{filePrefix}{SecurityBaselineFileSuffix}"; + } + + private static string GetResourceNameFromSecurityBaselineFileName(string fileName) + { + if (fileName.Contains(Path.DirectorySeparatorChar) || fileName.Contains(Path.AltDirectorySeparatorChar)) + { + fileName = Path.GetFileName(fileName); + } + + if (!fileName.Contains(SecurityBaselineFileSuffix)) + { + return ""; + } + + var filePrefix = fileName[..^SecurityBaselineFileSuffix.Length].Replace('-', ' ').Trim(); + var resourceName = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(filePrefix); + if (filePrefix.Contains("api ", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"api\b", "API", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" wan", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bwan", "WAN", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" iaas", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\biaas", "IaaS", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" pubsub", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bpubsub", "PubSub", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" signalr", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bsignalr", "SignalR", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains("(aro)", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\(aro\)", "(ARO)", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" openshift", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bopenshift", "OpenShift", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" openai", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bopenai", "OpenAI", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" netapp", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bnetapp", "NetApp", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" hci", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bhci", "HCI", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" aks", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\baks", "AKS", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" hpc", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bhpc", "HPC", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" devtest", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bdevtest", "DevTest", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" hsm", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bhsm", "HSM", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" ddos", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bddos", "DDoS", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" mysql", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bmysql", "MySQL", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" postgresql", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bpostgresql", "PostgreSQL", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" mariadb", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bmariadb", "MariaDB", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" sap", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bsap", "SAP", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" db", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bdb", "DB", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" iot", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\biot", "IoT", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains("iot ", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"iot\b", "IoT", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" ip", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bip", "IP", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" sql", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bsql", "SQL", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains("sql ", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"sql\b", "SQL", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" dns", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bdns", "DNS", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains(" nat", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"\bnat", "NAT", RegexOptions.IgnoreCase); + } + if (filePrefix.Contains("vpn", StringComparison.InvariantCultureIgnoreCase)) + { + resourceName = Regex.Replace(resourceName, @"vpn\b", "VPN", RegexOptions.IgnoreCase); + } + + return resourceName; + } +} diff --git a/src/Crisp.Core/Services/RecommendationsService.cs b/src/Crisp.Core/Services/RecommendationsService.cs index 65fb20d..201547f 100644 --- a/src/Crisp.Core/Services/RecommendationsService.cs +++ b/src/Crisp.Core/Services/RecommendationsService.cs @@ -1,7 +1,9 @@ using Crisp.Core.Models; using Crisp.Core.Repositories; +using DocumentFormat.OpenXml.Spreadsheet; using System.Security.Cryptography; using System.Text; +using System.Text.RegularExpressions; namespace Crisp.Core.Services; @@ -53,15 +55,61 @@ public async Task> GetResourcesAsync() private Category MapBenchmarksToCategory(string resourceName, IEnumerable benchmarks) { var categories = new List(); - foreach (var categoryName in benchmarks.Select(b => b.Category).Distinct()) + if (benchmarks.All(b => b.ControlId is null)) { - var recommendations = benchmarks.Where(b => b.Category.Equals(categoryName)).Select(b => MapSecurityBenchmarkToRecommendation(resourceName, b)).ToArray(); - categories.Add(new Category( - GenerateIdFor($"{resourceName}-{categoryName}"), - categoryName, - Description: null, - Children: Enumerable.Empty(), - recommendations)); + // old benchmarks - v1.1, v2 + foreach (var categoryName in benchmarks.Select(b => b.Category).Distinct()) + { + var recommendations = benchmarks.Where(b => b.Category.Equals(categoryName)).Select(b => MapSecurityBenchmarkToRecommendation(resourceName, b)).ToArray(); + categories.Add(new Category( + GenerateIdFor($"{resourceName}-{categoryName}"), + categoryName, + Description: null, + Children: Enumerable.Empty(), + recommendations)); + } + } else + { + // new benchmarks - v3 + var categoriesOrder = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + {"Network Security", 1}, + {"Identity Management", 2}, + {"Privileged Access", 3}, + {"Data Protection", 4}, + {"Asset Management", 5}, + {"Logging and Threat Detection", 6}, + {"Incident Response", 7}, + {"Posture and Vulnerability Management", 8}, + {"Endpoint Security", 9}, + {"Backup and Recovery", 10}, + {"DevOps Security", 11}, + {"Governance and Strategy", 12} + }; + + var sortedBenchmarksGroups = benchmarks.Where(b => categoriesOrder.ContainsKey(b.Category)).GroupBy(b => b.Category).OrderBy(g => categoriesOrder[g.Key]).ToList(); + + foreach (var group in sortedBenchmarksGroups) + { + var subCategories = new List(); + foreach (var subGroup in group.GroupBy(b => b.ControlId).OrderBy(g => g.Key)) + { + var recommendations = subGroup.Select(b => MapSecurityBenchmarkToRecommendation($"{resourceName}-{group.Key}-{subGroup.Key}", b)).ToArray(); + subCategories.Add(new Category( + GenerateIdFor($"{resourceName}-{group.Key}-{subGroup.Key}"), + subGroup.First().ControlTitle ?? "", + Description: null, + Children: Enumerable.Empty(), + recommendations)); + } + categories.Add(new Category( + GenerateIdFor($"{resourceName}-{group.Key}"), + group.Key, + Description: null, + Children: subCategories, + Recommendations: Enumerable.Empty())); + } + } return new Category( GenerateIdFor(resourceName), @@ -73,11 +121,49 @@ private Category MapBenchmarksToCategory(string resourceName, IEnumerable