diff --git a/src/Crisp.Core.Tests/Crisp.Core.Tests.csproj b/src/Crisp.Core.Tests/Crisp.Core.Tests.csproj index a3e2215..8277976 100644 --- a/src/Crisp.Core.Tests/Crisp.Core.Tests.csproj +++ b/src/Crisp.Core.Tests/Crisp.Core.Tests.csproj @@ -9,14 +9,14 @@ - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Crisp.Core/Crisp.Core.csproj b/src/Crisp.Core/Crisp.Core.csproj index 2a8ddad..a7ac1f7 100644 --- a/src/Crisp.Core/Crisp.Core.csproj +++ b/src/Crisp.Core/Crisp.Core.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Crisp.Core/Models/SecurityBenchmarkControl.cs b/src/Crisp.Core/Models/SecurityBenchmarkControl.cs new file mode 100644 index 0000000..407ad5f --- /dev/null +++ b/src/Crisp.Core/Models/SecurityBenchmarkControl.cs @@ -0,0 +1,11 @@ +namespace Crisp.Core.Models; + +public record SecurityBenchmarkControl( + string Id, + string Domain, + string Title, + string? Description, + string? Azure, + string? Aws, + string? Gcp +); diff --git a/src/Crisp.Core/Repositories/ISecurityBenchmarksRepository.cs b/src/Crisp.Core/Repositories/ISecurityBenchmarksRepository.cs index 9bb8e20..d842692 100644 --- a/src/Crisp.Core/Repositories/ISecurityBenchmarksRepository.cs +++ b/src/Crisp.Core/Repositories/ISecurityBenchmarksRepository.cs @@ -6,4 +6,5 @@ public interface ISecurityBenchmarksRepository { Task> GetAllResourceNamesAsync(string rootDirectoryPath); Task> GetSecurityBenchmarksForResourceAsync(string resourceName, string rootDirectoryPath); + Task> GetSecurityBenchmarkControlsAsync(string rootDirectoryPath); } \ No newline at end of file diff --git a/src/Crisp.Core/Repositories/SecurityBenchmarksV11Repository.cs b/src/Crisp.Core/Repositories/SecurityBenchmarksV11Repository.cs index 402c979..6906b51 100644 --- a/src/Crisp.Core/Repositories/SecurityBenchmarksV11Repository.cs +++ b/src/Crisp.Core/Repositories/SecurityBenchmarksV11Repository.cs @@ -38,6 +38,11 @@ public async Task> GetSecurityBenchmarksForResour return await GetAllSecurityBenchmarksAsync(fileFullName); } + public Task> GetSecurityBenchmarkControlsAsync(string rootDirectoryPath) + { + throw new NotImplementedException(); + } + private static Task> GetAllSecurityBenchmarksAsync(string fileFullName) { diff --git a/src/Crisp.Core/Repositories/SecurityBenchmarksV2Repository.cs b/src/Crisp.Core/Repositories/SecurityBenchmarksV2Repository.cs index 9216c5c..f997ab0 100644 --- a/src/Crisp.Core/Repositories/SecurityBenchmarksV2Repository.cs +++ b/src/Crisp.Core/Repositories/SecurityBenchmarksV2Repository.cs @@ -38,6 +38,11 @@ public async Task> GetSecurityBenchmarksForResour return await GetAllSecurityBenchmarksAsync(fileFullName); } + public Task> GetSecurityBenchmarkControlsAsync(string rootDirectoryPath) + { + throw new NotImplementedException(); + } + private static Task> GetAllSecurityBenchmarksAsync(string fileFullName) { diff --git a/src/Crisp.Core/Repositories/SecurityBenchmarksV3Repository.cs b/src/Crisp.Core/Repositories/SecurityBenchmarksV3Repository.cs index 615093b..b58c2c2 100644 --- a/src/Crisp.Core/Repositories/SecurityBenchmarksV3Repository.cs +++ b/src/Crisp.Core/Repositories/SecurityBenchmarksV3Repository.cs @@ -11,6 +11,7 @@ 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>(() => @@ -37,6 +38,18 @@ public async Task> GetSecurityBenchmarksForResour return await GetAllSecurityBenchmarksAsync(fileFullName); } + public async Task> GetSecurityBenchmarkControlsAsync(string rootDirectoryPath) + { + var benchmarkControlsFileName = Path.Combine(rootDirectoryPath, "Microsoft Cloud Security Benchmark", "Microsoft_cloud_security_benchmark_v1.xlsx"); + if (!File.Exists(benchmarkControlsFileName)) + { + return Enumerable.Empty(); + } + + var benchmarkControls = await GetAllSecurityBenchmarkControlsAsync(benchmarkControlsFileName); + return benchmarkControls; + } + private static Task> GetAllSecurityBenchmarksAsync(string fileFullName) { @@ -47,10 +60,8 @@ private static Task> GetAllSecurityBenchmarksAsyn 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(); + reader.NextResult(); // Skip the first sheet and move to the second one + reader.Read(); // Skip the header row while (reader.Read()) { var title = reader.GetValue(2)?.ToString(); @@ -89,6 +100,49 @@ private static Task> GetAllSecurityBenchmarksAsyn }); } + private static Task> GetAllSecurityBenchmarkControlsAsync(string fileFullName) + { + return Task.Run>(() => { + var controls = new List(); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); // Register the code pages to support ExcelDataReader on non-Windows systems + using var stream = File.Open(fileFullName, FileMode.Open, FileAccess.Read); + using var reader = ExcelReaderFactory.CreateReader(stream); + reader.NextResult(); // Skip the first sheet and move to the second one + do { + reader.Read(); // Skip the header row + while (reader.Read()) + { + var fieldCount = reader.FieldCount; + var azureGuidance = reader.GetValue(8)?.ToString(); + var azureImplementation = reader.GetValue(9)?.ToString(); + var awsGuidance = (string)null;// reader.GetValue(10)?.ToString(); + var awsImplementation = (string)null;// reader.GetValue(11)?.ToString(); + + var azure = azureGuidance is null && azureImplementation is null + ? null + : $"{azureGuidance}{((azureGuidance is not null && azureImplementation is not null) ? Environment.NewLine + Environment.NewLine : "")}{azureImplementation}"; + + var aws = awsGuidance is null && awsImplementation is null + ? null + : $"{awsGuidance}{((awsGuidance is not null && awsImplementation is not null) ? Environment.NewLine + Environment.NewLine : "")}{awsImplementation}"; + + controls.Add(new SecurityBenchmarkControl( + reader.GetValue(0)?.ToString() ?? "", + reader.GetValue(1)?.ToString() ?? "", + reader.GetValue(6)?.ToString() ?? "", + reader.GetValue(7)?.ToString(), + azure, + aws, + null + )); + } + } + while (reader.NextResult()); + return controls; + }); + } + + private static string GetFileFullNameForResource(string rootDirectoryPath, string resourceName) { return Path.Combine(GetBenchmarksDirectory(rootDirectoryPath), GetSecurityBaselineFileName(resourceName)); diff --git a/src/Crisp.Core/Services/IRecommendationsService.cs b/src/Crisp.Core/Services/IRecommendationsService.cs index 895fc58..c62c8c4 100644 --- a/src/Crisp.Core/Services/IRecommendationsService.cs +++ b/src/Crisp.Core/Services/IRecommendationsService.cs @@ -7,4 +7,5 @@ public interface IRecommendationsService Task> GetResourcesAsync(); Task GetRecommendationsAsync(IEnumerable resources); Task> GetBenchmarksAsync(string resourceName); + Task GetBenchmarkControlsAsync(); } \ No newline at end of file diff --git a/src/Crisp.Core/Services/RecommendationsService.cs b/src/Crisp.Core/Services/RecommendationsService.cs index 604e45f..b9df5a2 100644 --- a/src/Crisp.Core/Services/RecommendationsService.cs +++ b/src/Crisp.Core/Services/RecommendationsService.cs @@ -49,7 +49,6 @@ public async Task GetRecommendationsAsync(IEnumerable resource var repositoryDirectoryPath = await gitHubRepository.CloneAsync(GitHubAccountName, GitHubRepositoryName); var benchmarks = await securityBenchmarksRepository.GetSecurityBenchmarksForResourceAsync(resourceName, repositoryDirectoryPath); - return benchmarks; } @@ -59,6 +58,12 @@ public async Task> GetResourcesAsync() return await securityBenchmarksRepository.GetAllResourceNamesAsync(repositoryDirectoryPath); } + public async Task GetBenchmarkControlsAsync() + { + var repositoryDirectoryPath = await gitHubRepository.CloneAsync(GitHubAccountName, GitHubRepositoryName); + return MapBenchmarkControlsToCategory(await securityBenchmarksRepository.GetSecurityBenchmarkControlsAsync(repositoryDirectoryPath)); + } + private Category MapBenchmarksToCategory(string resourceName, IEnumerable benchmarks) { @@ -126,6 +131,45 @@ private Category MapBenchmarksToCategory(string resourceName, IEnumerable()); } + private Category MapBenchmarkControlsToCategory(IEnumerable controls) + { + var domainsOrder = 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 sortedControlsGroups = controls.Where(b => domainsOrder.ContainsKey(b.Domain)).GroupBy(b => b.Domain).OrderBy(g => domainsOrder[g.Key]).ToList(); + var categories = new List(); + foreach (var group in sortedControlsGroups) + { + var recommendations = group.Select(MapSecurityBenchmarkControlToRecommendation).ToArray(); + categories.Add(new Category( + GenerateIdFor(group.Key), + group.Key, + Description: null, + Children: Enumerable.Empty(), + recommendations)); + } + + return new Category( + GenerateIdFor("Cloud Security Benchmark"), + "Cloud Security Benchmark", + Description: null, + categories, + Recommendations: Enumerable.Empty()); + } + private Recommendation MapSecurityBenchmarkToRecommendation(string resourceName, SecurityBenchmark benchmark) { if (benchmark.ControlTitle is null) @@ -175,6 +219,39 @@ private Recommendation MapSecurityBenchmarkToRecommendation(string resourceName, } } + private Recommendation MapSecurityBenchmarkControlToRecommendation(SecurityBenchmarkControl benchmarkControl) + { + var description = ""; + if (benchmarkControl.Azure is not null) + { + if (benchmarkControl.Description is not null && benchmarkControl.Description.ToLower().Trim() != "n/a") + { + description = $"{benchmarkControl.Description}{Environment.NewLine}{Environment.NewLine}**Azure Guidance:**{Environment.NewLine}{Environment.NewLine}"; + } + description += benchmarkControl.Azure; + } + else + { + description = $"{benchmarkControl.Description}"; + } + + if (!string.IsNullOrWhiteSpace(description)) + { + var match = Regex.Matches(description, "https://[^\\s]+"); + foreach (Match m in match) + { + var url = m.Value.Trim(); + description = description.Replace(m.Value, $" [{url}]({url})"); + } + } + return new Recommendation( + benchmarkControl.Id, + $"{benchmarkControl.Id}: {benchmarkControl.Title!}", + description, + null + ); + } + private static string GenerateIdFor(string text) { return string.Join("", (SHA1.HashData(Encoding.UTF8.GetBytes(text))).Select(b => b.ToString("x2"))); diff --git a/src/Crisp.Ui/ClientApp/src/AppRoutes.js b/src/Crisp.Ui/ClientApp/src/AppRoutes.js index 3b1b934..f07b472 100644 --- a/src/Crisp.Ui/ClientApp/src/AppRoutes.js +++ b/src/Crisp.Ui/ClientApp/src/AppRoutes.js @@ -4,6 +4,7 @@ import AddThreatModel from "./components/AddThreatModel"; import ThreatModelReport from "./components/ThreatModelReport"; import Recommendations from "./components/Recommendations"; import Resources from "./components/Resources"; +import ThreatsMapping from "./components/ThreatMapping"; const AppRoutes = [ { @@ -29,6 +30,10 @@ const AppRoutes = [ { path: '/resources', element: + }, + { + path: '/map', + element: } ]; diff --git a/src/Crisp.Ui/ClientApp/src/components/AddThreatModel.js b/src/Crisp.Ui/ClientApp/src/components/AddThreatModel.js index 332dcc7..80bd959 100644 --- a/src/Crisp.Ui/ClientApp/src/components/AddThreatModel.js +++ b/src/Crisp.Ui/ClientApp/src/components/AddThreatModel.js @@ -229,7 +229,7 @@ const AddThreatModel = () => { {selectedRecommendationsCount > 0 ? ( - setAddResourcesRecommendations(!addResourcesRecommendations)} /> Add resources recommendations to threats (preview) + setAddResourcesRecommendations(!addResourcesRecommendations)} /> Add resources recommendations to threats ) : null} {addResourcesRecommendations ? ( diff --git a/src/Crisp.Ui/ClientApp/src/components/Category.js b/src/Crisp.Ui/ClientApp/src/components/Category.js index 13c4ba6..3990bd8 100644 --- a/src/Crisp.Ui/ClientApp/src/components/Category.js +++ b/src/Crisp.Ui/ClientApp/src/components/Category.js @@ -1,82 +1,40 @@ -import React, { useState, useRef, useEffect, useImperativeHandle, forwardRef } from 'react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; import { ListGroupItem, Badge, Input, Button } from 'reactstrap'; import Recommendation from './Recommendation'; import { BsArrowsAngleContract, BsArrowsAngleExpand } from "react-icons/bs"; -const Category = forwardRef(({ category, level, isSelected, toggleSelectability }, ref) => { - +const Category = ({ category, level = 0, isSelected, toggleSelectability, recommendationsExpanded = false }) => { const [isCollapsed, setIsCollapsed] = useState(false); - const [recommendationsExpanded, setRecommendationsExpanded] = useState(false); - const [recommendationsRefs, setRecommendationsRefs] = useState([]); - const [categoriesRefs, setCategoriesRefs] = useState([]); - - if (!level) { - level = 0; - } + const [recommendationsExpandedLocal, setRecommendationsExpandedLocal] = useState(recommendationsExpanded); useEffect(() => { - setRecommendationsRefs(recommendationsRefs => ( - Array(category.recommendations.length).fill().map((_, i) => recommendationsRefs[i] || React.createRef()) - )); - }, [category.recommendations.length]); + setRecommendationsExpandedLocal(recommendationsExpanded); + }, [recommendationsExpanded]); - useEffect(() => { - setCategoriesRefs(categoriesRefs => ( - Array(category.children.length).fill().map((_, i) => categoriesRefs[i] || React.createRef()) - )); - }, [category.children.length]); + const paddingLeft = useMemo(() => (level * 30) + 15, [level]); - const getPaddingLeft = () => { - return (level * 30) + 15; - } - - const calculateRecommendationsCount = (category, onlySelected) => { + const calculateRecommendationsCount = useCallback((cat, onlySelected) => { let count = onlySelected - ? category.recommendations.filter(r => isSelected(r.id)).length - : category.recommendations.length; - category.children.forEach(c => count += calculateRecommendationsCount(c, onlySelected)); + ? cat.recommendations.filter(r => isSelected(r.id)).length + : cat.recommendations.length; + cat.children?.forEach(c => count += calculateRecommendationsCount(c, onlySelected)); return count; - } + }, [isSelected]); - const toggleIsCollapsed = (e) => { - if (e.target.tagName === "INPUT") { - return; + const toggleIsCollapsed = useCallback((e) => { + if (e.target.tagName !== "INPUT") { + setIsCollapsed(!isCollapsed); } - setIsCollapsed(!isCollapsed); - } + }, [isCollapsed]); - const toggleIsSelect = (category) => { + const toggleIsSelect = useCallback(() => { toggleSelectability(category); - } - - const expandAllRecommendations = () => { - setRecommendationsExpanded(true); - recommendationsRefs.forEach(r => r.current?.open()); - categoriesRefs.forEach(c => c.current?.expandAllRecommendations()); - } + }, [category, toggleSelectability]); - const collapseAllRecommendations = () => { - setRecommendationsExpanded(false); - recommendationsRefs.forEach(r => r.current?.close()); - categoriesRefs.forEach(c => c.current?.collapseAllRecommendations()); - } - - useImperativeHandle(ref, () => ({ - expandAllRecommendations, collapseAllRecommendations - })); - - const expandAllHandler = (e) => { - console.log('expandAll'); - expandAllRecommendations(); + const toggleRecommendationsExpanded = useCallback((e) => { + setRecommendationsExpandedLocal(exp => !exp); e.stopPropagation(); - } - - const collapseAllHandler = (e) => { - console.log('collapseAll'); - collapseAllRecommendations(); - e.stopPropagation(); - } - + }, []); const categorySelected = isSelected(category.id); const recommendationsCount = calculateRecommendationsCount(category, false); @@ -84,31 +42,30 @@ const Category = forwardRef(({ category, level, isSelected, toggleSelectability return ( <> - +
- toggleIsSelect(category)} /> + {category.name} - {!recommendationsExpanded - ? - : - } +
{categorySelected ? selectedRecommendationsCount : recommendationsCount}
- {!isCollapsed ? ( + {!isCollapsed && ( <> - {categoriesRefs.map((ref, i) => ( - + {category.children?.map(child => ( + ))} - {recommendationsRefs.map((ref, i) => ( - + {category.recommendations.map(recommendation => ( + ))} - ) : null} + )} - ) -}); + ); +}; -export default Category; \ No newline at end of file +export default Category; diff --git a/src/Crisp.Ui/ClientApp/src/components/Recommendation.js b/src/Crisp.Ui/ClientApp/src/components/Recommendation.js index 054b814..456a3d9 100644 --- a/src/Crisp.Ui/ClientApp/src/components/Recommendation.js +++ b/src/Crisp.Ui/ClientApp/src/components/Recommendation.js @@ -1,57 +1,41 @@ -import React, { useState, useImperativeHandle, forwardRef } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { ListGroupItem, Input } from 'reactstrap'; import { FcFile } from "react-icons/fc"; -import ReactMarkdown from 'react-markdown' +import ReactMarkdown from 'react-markdown'; -const Recommendation = forwardRef(({ recommendation, level, isSelected, toggleSelectability }, ref) => { +const Recommendation = ({ recommendation, level = 0, isSelected, toggleSelectability, isOpen = false }) => { + const [isOpenLocal, setIsOpenLocal] = useState(isOpen); - const [isOpen, setIsOpen] = useState(false); + useEffect(() => { + setIsOpenLocal(isOpen); + }, [isOpen]); - if (!level) { - level = 0; - } - - const getPaddingLeft = () => { - return level * 30; - } - - const toggleIsOpen = (e) => { - if (e.target.tagName === "INPUT") { - return; + const toggleIsOpen = useCallback((e) => { + if (e.target.tagName !== "INPUT") { + setIsOpenLocal(!isOpenLocal); } - setIsOpen(!isOpen); - } - - const toggleIsSelect = (category) => { - toggleSelectability(category); - } - - const open = () => { - setIsOpen(true); - } + }, [isOpenLocal]); - const close = () => { - setIsOpen(false); - } + const toggleIsSelect = useCallback(() => { + toggleSelectability(recommendation); + }, [recommendation, toggleSelectability]); - useImperativeHandle(ref, () => ({ - open, close - })); + const paddingLeft = level * 30; return ( <> - - toggleIsSelect(recommendation)} /> {recommendation.title} + + {recommendation.title} - {isOpen ? ( - + {isOpenLocal && ( +
{recommendation.description}
- ) : null} + )} - ) -}); + ); +}; -export default Recommendation; \ No newline at end of file +export default Recommendation; diff --git a/src/Crisp.Ui/ClientApp/src/components/Recommendations.js b/src/Crisp.Ui/ClientApp/src/components/Recommendations.js index 9c89b5b..09a3f71 100644 --- a/src/Crisp.Ui/ClientApp/src/components/Recommendations.js +++ b/src/Crisp.Ui/ClientApp/src/components/Recommendations.js @@ -82,7 +82,6 @@ const Recommendations = () => { {selectedRecommendationsCount > 0 ? (
Selected recommendations {selectedRecommendationsCount} -
) : null} diff --git a/src/Crisp.Ui/ClientApp/src/components/ThreatMapping.css b/src/Crisp.Ui/ClientApp/src/components/ThreatMapping.css new file mode 100644 index 0000000..bcb1cc8 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/ThreatMapping.css @@ -0,0 +1,9 @@ +.selected-recommendation { + color: #0366d6; +} + +@media (prefers-color-scheme: dark) { + .selected-recommendation { + color: white; + } +} diff --git a/src/Crisp.Ui/ClientApp/src/components/ThreatMapping.js b/src/Crisp.Ui/ClientApp/src/components/ThreatMapping.js new file mode 100644 index 0000000..4ed0372 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/ThreatMapping.js @@ -0,0 +1,175 @@ +import React, { useState } from 'react'; +import { fetchThreatModelCategory } from '../fetchers/threatmodels'; +import { Spinner, Alert, Input, Button, Collapse, ListGroup } from 'reactstrap'; +import { useQuery } from 'react-query'; +import ReactMarkdown from 'react-markdown' +import { fetchBenchmarkControls } from '../fetchers/resources'; +import Category from './Category'; + +import './ThreatMapping.css'; + +const CategoryItem = ({ category, onRecommendationSelect, selectedRecommendation }) => { + return ( + <> +
{category.name}
+
+ {/* Render Sub-Categories */} + {category.children && category.children.length > 0 && ( +
+ {category.children.map((subCategory, index) => ( + + ))} +
+ )} + {/* Render Recommendations */} + {category.recommendations && category.recommendations.length > 0 && ( +
+ {category.recommendations.map((recommendation, index) => ( +
onRecommendationSelect(recommendation)} + className={`${selectedRecommendation && selectedRecommendation.id === recommendation.id ? 'selected-recommendation' : ''}`} + style={{ + cursor: 'pointer', + }} + > + {selectedRecommendation && selectedRecommendation.title === recommendation.title ? "> " : ""}{recommendation.title} +
+ ))} +
+ )} +
+ + ); +} + +const DescriptionPanel = ({ recommendation }) => { + if (!recommendation) { + return
Select a recommendation to see its details here.
; + } + + return ( + <> +

{recommendation.title}

+
+ {recommendation.description} +
+ + ); +}; + +const ThreatMappting = () => { + + const threats = useQuery(['threatmodel-category'], fetchThreatModelCategory, { staleTime: 24 * 60 * 60 * 1000 }); + const benchmarkControls = useQuery(['benchmark-controls-category'], fetchBenchmarkControls, { staleTime: 24 * 60 * 60 * 1000 }); + + const [selectedRecommendation, setSelectedRecommendation] = useState(null); + const [checkedBenchmarkControlIds, setCheckedBenchmarkControlIds] = useState([]); + + const [isThreatsOpen, setIsThreatsOpen] = useState(true); + + const handleRecommendationSelect = (recommendation) => { + if (checkedBenchmarkControlIds.length > 0) { + if (!window.confirm("Are you sure you want to change the recommendation? All selected benchmark controls will be lost.")) { + return; + } + } + setSelectedRecommendation(recommendation); + setCheckedBenchmarkControlIds(recommendation.benchmarkIds || []); + toggleThreats(); + }; + + const toggleThreats = () => { + setIsThreatsOpen(!isThreatsOpen); + } + + const getChildrenIds = item => { + const ids = [item.id]; + if (item.children) { + item.children.forEach(c => ids.push(...getChildrenIds(c))); + } + if (item.recommendations) { + item.recommendations.forEach(r => ids.push(r.id)); + } + return ids; + } + + const toggleSelectability = selectedItem => { + const toggledIds = getChildrenIds(selectedItem); + setCheckedBenchmarkControlIds(prev => { + if (prev.includes(selectedItem.id)) { + return prev.filter(id => !toggledIds.includes(id)); + } + return [...prev, ...toggledIds.filter(id => !prev.includes(id))]; + }); + } + + const isSelected = id => { + return checkedBenchmarkControlIds.includes(id); + } + function getCsvForIds(ids) { + return ids.filter(id => id.length <= 10).join(', '); + } + + if ((threats && threats.isLoading) || (benchmarkControls && benchmarkControls.isLoading)) { + return ( +
+ + Loading... + +
+ ); + } + + if ((threats && threats.isError) || (benchmarkControls && benchmarkControls.isError)) { + return ( + {threats.error.message} + ); + } + + return ( + <> +
+

Threats

+ +
+ +
+ +
+
+
+ +
+ {selectedRecommendation ? ( + <> +
+

Cloud Security Benchmark

+
+ + + +
+

Selected Benchmark Controls

+
+
+