From 95cc665f6df813873d8fb2413f3c4721848d316a Mon Sep 17 00:00:00 2001 From: Andrew Malkov Date: Wed, 31 Jan 2024 19:22:38 +0100 Subject: [PATCH] add benchmark recommendations to threats in report (#8) --- src/Crisp.Core.Tests/UnitTest1.cs | 2 +- .../Helpers/MarkdownReportHelper.cs | 85 +++++++++++++++++++ src/Crisp.Core/Helpers/OpenXmlHelper.cs | 11 ++- .../Services/IRecommendationsService.cs | 1 + .../Services/RecommendationsService.cs | 9 ++ .../Services/ThreatModelsService.cs | 56 ++++++------ .../src/components/Recommendation.js | 2 +- .../Handlers/CreateThreatModelHandler.cs | 2 +- src/Crisp.Ui/Handlers/GetCategoriesHandler.cs | 6 +- .../GetThreatModelCategoriesHandler.cs | 3 +- .../Handlers/GetThreatModelsHandler.cs | 3 +- .../Handlers/UpdateThreatModelHandler.cs | 2 +- 12 files changed, 140 insertions(+), 42 deletions(-) create mode 100644 src/Crisp.Core/Helpers/MarkdownReportHelper.cs diff --git a/src/Crisp.Core.Tests/UnitTest1.cs b/src/Crisp.Core.Tests/UnitTest1.cs index f2fc246..c0eb92d 100644 --- a/src/Crisp.Core.Tests/UnitTest1.cs +++ b/src/Crisp.Core.Tests/UnitTest1.cs @@ -109,7 +109,7 @@ public async Task Test3() var stream = new MemoryStream(); stream.Write(wordTemplate, 0, wordTemplate.Length); - OpenXmlHelper.AddThreats(stream, recommendations); + OpenXmlHelper.AddThreats(stream, recommendations, null); File.WriteAllBytes("result2.docx", stream.ToArray()); } diff --git a/src/Crisp.Core/Helpers/MarkdownReportHelper.cs b/src/Crisp.Core/Helpers/MarkdownReportHelper.cs new file mode 100644 index 0000000..0128999 --- /dev/null +++ b/src/Crisp.Core/Helpers/MarkdownReportHelper.cs @@ -0,0 +1,85 @@ +using Crisp.Core.Models; +using System.Text; + +namespace Crisp.Core.Helpers; + +public static class MarkdownReportHelper +{ + public static string GenerateThreatModelPropertiesSection(ThreatModel threatModel, + IDictionary>? benchmarks) + { + var section = new StringBuilder(); + var index = 1; + foreach (var threat in threatModel.Threats) + { + if (index > 1) + { + section.AppendLine(); + } + section.AppendLine("---"); + section.AppendLine($"**Threat #:** {index} "); + section.AppendLine(threat.Description.Trim()); + if (threatModel.AddResourcesRecommendations) + { + var resourcesRecommendations = GenerateResourcesRecommendationsForThreat(threat, benchmarks); + if (!string.IsNullOrEmpty(resourcesRecommendations)) + { + section.AppendLine(resourcesRecommendations); + } + } + index++; + } + return section.ToString().TrimEnd(Environment.NewLine.ToCharArray()); + } + + public static string GenerateResourcesRecommendationsForThreat(Recommendation threat, + IDictionary>? benchmarks) + { + if (threat.BenchmarkIds is null || !threat.BenchmarkIds.Any() || benchmarks is null) + { + return ""; + } + + var section = new StringBuilder(); + section.AppendLine(); + section.AppendLine($"**Recommendations for resources:**"); + section.AppendLine(); + foreach (var resourceName in benchmarks.Keys) + { + var resourceBenchmarks = benchmarks[resourceName]; + if (resourceBenchmarks is null) + { + continue; + } + section.AppendLine($"**{resourceName}:**"); + section.AppendLine(); + var index = 1; + foreach (var benchmarkId in threat.BenchmarkIds) + { + var benchmark = resourceBenchmarks.FirstOrDefault(b => b.Id == benchmarkId); + if (benchmark is null) + { + continue; + } + section.AppendLine($"**Recommendation #:** {index}"); + section.AppendLine(); + section.AppendLine(benchmark.Description); + section.AppendLine(); + index++; + } + } + section.AppendLine(); + + return section.ToString(); + } + + public static string GenerateDataflowAttributeSection(ThreatModel threatModel) + { + var section = new StringBuilder(); + foreach (var a in threatModel.DataflowAttributes) + { + section.AppendLine($"| {a.Number.Trim()} | {a.Transport.Trim()} | {a.DataClassification.Trim()} | {a.Authentication.Trim()} | {a.Authorization.Trim()} | {a.Notes.Trim()} |"); + } + return section.ToString().TrimEnd(Environment.NewLine.ToCharArray()); + } +} diff --git a/src/Crisp.Core/Helpers/OpenXmlHelper.cs b/src/Crisp.Core/Helpers/OpenXmlHelper.cs index 6178a50..8f45b10 100644 --- a/src/Crisp.Core/Helpers/OpenXmlHelper.cs +++ b/src/Crisp.Core/Helpers/OpenXmlHelper.cs @@ -63,7 +63,8 @@ public static void AddDataflowAttributes(Stream stream, IEnumerable threats) + public static void AddThreats(Stream stream, IEnumerable threats, + IDictionary>? benchmarks) { using var document = WordprocessingDocument.Open(stream, isEditable: true); var body = document.MainDocumentPart.Document.Body; @@ -88,7 +89,13 @@ public static void AddThreats(Stream stream, IEnumerable threats new Run(new Text($" {threatIndex}") { Space = SpaceProcessingModeValues.Preserve }) ) }; - paragraphs.AddRange(GetParagraphsFromMarkdown(threat.Description)); + + var description = threat.Description + + (benchmarks is not null + ? MarkdownReportHelper.GenerateResourcesRecommendationsForThreat(threat, benchmarks).Replace("\n", Environment.NewLine) + : ""); + paragraphs.AddRange(GetParagraphsFromMarkdown(description)); + foreach (var paragraph in paragraphs.ToArray().Reverse()) { var hyperlinks = paragraph.Descendants(); diff --git a/src/Crisp.Core/Services/IRecommendationsService.cs b/src/Crisp.Core/Services/IRecommendationsService.cs index 2620658..895fc58 100644 --- a/src/Crisp.Core/Services/IRecommendationsService.cs +++ b/src/Crisp.Core/Services/IRecommendationsService.cs @@ -6,4 +6,5 @@ public interface IRecommendationsService { Task> GetResourcesAsync(); Task GetRecommendationsAsync(IEnumerable resources); + Task> GetBenchmarksAsync(string resourceName); } \ No newline at end of file diff --git a/src/Crisp.Core/Services/RecommendationsService.cs b/src/Crisp.Core/Services/RecommendationsService.cs index 78ecc96..604e45f 100644 --- a/src/Crisp.Core/Services/RecommendationsService.cs +++ b/src/Crisp.Core/Services/RecommendationsService.cs @@ -44,6 +44,15 @@ public async Task GetRecommendationsAsync(IEnumerable resource Recommendations: Enumerable.Empty()); } + public async Task?> GetBenchmarksAsync(string resourceName) + { + var repositoryDirectoryPath = await gitHubRepository.CloneAsync(GitHubAccountName, GitHubRepositoryName); + + var benchmarks = await securityBenchmarksRepository.GetSecurityBenchmarksForResourceAsync(resourceName, repositoryDirectoryPath); + + return benchmarks; + } + public async Task> GetResourcesAsync() { var repositoryDirectoryPath = await gitHubRepository.CloneAsync(GitHubAccountName, GitHubRepositoryName); diff --git a/src/Crisp.Core/Services/ThreatModelsService.cs b/src/Crisp.Core/Services/ThreatModelsService.cs index 13086a7..7472586 100644 --- a/src/Crisp.Core/Services/ThreatModelsService.cs +++ b/src/Crisp.Core/Services/ThreatModelsService.cs @@ -6,6 +6,8 @@ using Category = Crisp.Core.Models.Category; using Crisp.Core.Helpers; using System.Text.RegularExpressions; +using System.Dynamic; +using System.Runtime.InteropServices; namespace Crisp.Core.Services; @@ -30,15 +32,18 @@ public class ThreatModelsService : IThreatModelsService private readonly IThreatModelCategoriesRepository _threatModelCategoriesRepository; private readonly IMemoryCache _memoryCache; private readonly IReportsRepository _reportsRepository; + private readonly IRecommendationsService _recommendationsService; public ThreatModelsService(IGitHubRepository gitHubRepository, IThreatModelsRepository threatModelsRepository, - IThreatModelCategoriesRepository threatModelCategoriesRepository, IMemoryCache memoryCache, IReportsRepository reportsRepository) + IThreatModelCategoriesRepository threatModelCategoriesRepository, IMemoryCache memoryCache, IReportsRepository reportsRepository, + IRecommendationsService recommendationsService) { _gitHubRepository = gitHubRepository; _threatModelsRepository = threatModelsRepository; _threatModelCategoriesRepository = threatModelCategoriesRepository; _memoryCache = memoryCache; _reportsRepository = reportsRepository; + _recommendationsService = recommendationsService; } @@ -169,10 +174,18 @@ public async Task DeleteAsync(string id) } mdReport = mdReport.Replace(ProjectNamePlaceholder, threatModel.ProjectName); - var dataflowAttributeSection = GenerateDataflowAttributeSection(threatModel); + + var dataflowAttributeSection = MarkdownReportHelper.GenerateDataflowAttributeSection(threatModel); mdReport = mdReport.Replace(DataflowAttributesPlaceholder, dataflowAttributeSection); - var threatModelPropertiesSection = GenerateThreatModelPropertiesSection(threatModel); + + IDictionary>? benchmarks = threatModel.AddResourcesRecommendations && threatModel.Resources is not null + ? (await Task.WhenAll( + threatModel.Resources.Select(async r => new KeyValuePair>(r, await _recommendationsService.GetBenchmarksAsync(r))) + )).ToDictionary(pair => pair.Key, pair => pair.Value) + : null; + var threatModelPropertiesSection = MarkdownReportHelper.GenerateThreatModelPropertiesSection(threatModel, benchmarks); mdReport = mdReport.Replace(ThreatPropertiesPlaceholder, threatModelPropertiesSection); + if (threatModel.Images is not null) { mdReport = RemoveHeadersForUnusedImages(mdReport, threatModel.Images); @@ -258,7 +271,14 @@ private static string RemoveHeadersForUnusedImages(string report, IDictionary>? benchmarks = threatModel.AddResourcesRecommendations && threatModel.Resources is not null + ? (await Task.WhenAll( + threatModel.Resources.Select(async r => new KeyValuePair>(r, await _recommendationsService.GetBenchmarksAsync(r))) + )).ToDictionary(pair => pair.Key, pair => pair.Value) + : null; + OpenXmlHelper.AddThreats(stream, threatModel.Threats, benchmarks); + if (threatModel.Images is not null) { OpenXmlHelper.RemoveParagraphForUnusedImages(stream, threatModel.Images); @@ -276,34 +296,6 @@ private static string RemoveHeadersForUnusedImages(string report, IDictionary 1) - { - section.AppendLine(); - } - section.AppendLine("---"); - section.AppendLine($"**Threat #:** {index} "); - section.AppendLine(threat.Description.Trim()); - index++; - } - return section.ToString().TrimEnd(Environment.NewLine.ToCharArray()); - } - - private static string GenerateDataflowAttributeSection(ThreatModel threatModel) - { - var section = new StringBuilder(); - foreach (var a in threatModel.DataflowAttributes) - { - section.AppendLine($"| {a.Number.Trim()} | {a.Transport.Trim()} | {a.DataClassification.Trim()} | {a.Authentication.Trim()} | {a.Authorization.Trim()} | {a.Notes.Trim()} |"); - } - return section.ToString().TrimEnd(Environment.NewLine.ToCharArray()); - } - private async Task GetRecommendationsFromGitHubAsync() { var directory = await _gitHubRepository.GetContentAsync(GitHubAccountName, GitHubRepositoryName, GitHubThreatModelFolderName); diff --git a/src/Crisp.Ui/ClientApp/src/components/Recommendation.js b/src/Crisp.Ui/ClientApp/src/components/Recommendation.js index 4e2b6e1..054b814 100644 --- a/src/Crisp.Ui/ClientApp/src/components/Recommendation.js +++ b/src/Crisp.Ui/ClientApp/src/components/Recommendation.js @@ -25,7 +25,7 @@ const Recommendation = forwardRef(({ recommendation, level, isSelected, toggleSe const toggleIsSelect = (category) => { toggleSelectability(category); } - + const open = () => { setIsOpen(true); } diff --git a/src/Crisp.Ui/Handlers/CreateThreatModelHandler.cs b/src/Crisp.Ui/Handlers/CreateThreatModelHandler.cs index 7dd7c9b..4e3769a 100644 --- a/src/Crisp.Ui/Handlers/CreateThreatModelHandler.cs +++ b/src/Crisp.Ui/Handlers/CreateThreatModelHandler.cs @@ -62,7 +62,7 @@ private static Recommendation MapDtoToRecommendation(RecommendationDto dto) dto.Id, dto.Title, dto.Description, - null + dto.BenchmarkIds ); } } diff --git a/src/Crisp.Ui/Handlers/GetCategoriesHandler.cs b/src/Crisp.Ui/Handlers/GetCategoriesHandler.cs index a963c97..9ff3ce1 100644 --- a/src/Crisp.Ui/Handlers/GetCategoriesHandler.cs +++ b/src/Crisp.Ui/Handlers/GetCategoriesHandler.cs @@ -9,7 +9,8 @@ namespace Crisp.Ui.Handlers public record RecommendationDto( string Id, string Title, - string Description + string Description, + IEnumerable? BenchmarkIds ); public record CategoryDto( @@ -62,7 +63,8 @@ private static RecommendationDto MapRecommendationToDto(Recommendation recommend return new RecommendationDto( recommendation.Id, recommendation.Title, - recommendation.Description + recommendation.Description, + recommendation.BenchmarkIds ); } } diff --git a/src/Crisp.Ui/Handlers/GetThreatModelCategoriesHandler.cs b/src/Crisp.Ui/Handlers/GetThreatModelCategoriesHandler.cs index 5df8a0c..72600e5 100644 --- a/src/Crisp.Ui/Handlers/GetThreatModelCategoriesHandler.cs +++ b/src/Crisp.Ui/Handlers/GetThreatModelCategoriesHandler.cs @@ -48,7 +48,8 @@ private static RecommendationDto MapRecommendationToDto(Recommendation recommend return new RecommendationDto( recommendation.Id, recommendation.Title, - recommendation.Description + recommendation.Description, + recommendation.BenchmarkIds ); } } diff --git a/src/Crisp.Ui/Handlers/GetThreatModelsHandler.cs b/src/Crisp.Ui/Handlers/GetThreatModelsHandler.cs index 738ab24..a88dfd6 100644 --- a/src/Crisp.Ui/Handlers/GetThreatModelsHandler.cs +++ b/src/Crisp.Ui/Handlers/GetThreatModelsHandler.cs @@ -91,7 +91,8 @@ private static RecommendationDto MapRecommendationToDto(Recommendation recommend return new RecommendationDto( recommendation.Id, recommendation.Title, - recommendation.Description + recommendation.Description, + recommendation.BenchmarkIds ); } } diff --git a/src/Crisp.Ui/Handlers/UpdateThreatModelHandler.cs b/src/Crisp.Ui/Handlers/UpdateThreatModelHandler.cs index 0163f7c..5f622bb 100644 --- a/src/Crisp.Ui/Handlers/UpdateThreatModelHandler.cs +++ b/src/Crisp.Ui/Handlers/UpdateThreatModelHandler.cs @@ -67,7 +67,7 @@ private static Recommendation MapDtoToRecommendation(RecommendationDto dto) dto.Id, dto.Title, dto.Description, - null + dto.BenchmarkIds ); } }