From 4840da39ff383e0381a258645332e1fccfea7542 Mon Sep 17 00:00:00 2001 From: Andrew Malkov Date: Tue, 2 Apr 2024 14:56:29 +0200 Subject: [PATCH] align with the latest changes --- src/Crisp.Core.Tests/UnitTest1.cs | 12 +- .../Helpers/MarkdownReportHelper.cs | 2 +- src/Crisp.Core/Helpers/OpenXmlHelper.cs | 2 +- src/Crisp.Core/Models/Threat.cs | 29 ++ src/Crisp.Core/Models/ThreatModel.cs | 2 +- src/Crisp.Core/Models/ThreatRecommendation.cs | 8 + src/Crisp.Ui/ClientApp/package-lock.json | 274 ++++++++++++++++-- src/Crisp.Ui/ClientApp/package.json | 5 +- src/Crisp.Ui/ClientApp/src/AppRoutes.js | 4 +- .../src/components/Recommendations.js | 4 +- .../src/components/ThreatModelStatusBar.tsx | 68 +++++ .../ClientApp/src/components/ThreatModels.js | 54 ++-- .../threat-model/DataFlowAttributes.css | 4 + .../threat-model/DataFlowAttributes.js | 113 ++++++++ .../components/threat-model/ThreatModel.css | 0 .../components/threat-model/ThreatModel.js | 136 +++++++++ .../threat-model/ThreatModelImage.css | 5 + .../threat-model/ThreatModelImage.js | 38 +++ .../threat-model/ThreatModelResources.css | 6 + .../threat-model/ThreatModelResources.js | 45 +++ .../SelectCatalogRecommendations.js | 98 +++++++ .../SelectResourcesRecommendations.js | 120 ++++++++ .../SelectSecurityBenchmarkRecommendations.js | 97 +++++++ .../ThreatRecommendation.js | 44 +++ .../ThreatRecommendations.js | 236 +++++++++++++++ .../threat-model/threats/MarkdownEditor.js | 51 ++++ .../threat-model/threats/SelectThreats.js | 97 +++++++ .../components/threat-model/threats/Threat.js | 77 +++++ .../threat-model/threats/ThreatRisk.tsx | 50 ++++ .../threat-model/threats/ThreatStatus.tsx | 47 +++ .../threat-model/threats/Threats.js | 226 +++++++++++++++ src/Crisp.Ui/ClientApp/src/custom.css | 62 +++- .../ClientApp/src/fetchers/recommendations.js | 8 + src/Crisp.Ui/ClientApp/src/models/Threat.ts | 26 ++ .../src/models/ThreatRecommendation.ts | 6 + src/Crisp.Ui/ClientApp/tsconfig.json | 111 +++++++ src/Crisp.Ui/Crisp.Ui.csproj | 10 + .../Handlers/CreateThreatModelHandler.cs | 20 +- .../Handlers/GetThreatModelsHandler.cs | 24 +- .../Handlers/UpdateThreatModelHandler.cs | 20 +- .../Requests/CreateThreatModelRequest.cs | 23 +- 41 files changed, 2189 insertions(+), 75 deletions(-) create mode 100644 src/Crisp.Core/Models/Threat.cs create mode 100644 src/Crisp.Core/Models/ThreatRecommendation.cs create mode 100644 src/Crisp.Ui/ClientApp/src/components/ThreatModelStatusBar.tsx create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/DataFlowAttributes.css create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/DataFlowAttributes.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModel.css create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModel.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelImage.css create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelImage.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelResources.css create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelResources.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectCatalogRecommendations.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectResourcesRecommendations.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectSecurityBenchmarkRecommendations.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/ThreatRecommendation.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/ThreatRecommendations.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/threats/MarkdownEditor.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/threats/SelectThreats.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/threats/Threat.js create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/threats/ThreatRisk.tsx create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/threats/ThreatStatus.tsx create mode 100644 src/Crisp.Ui/ClientApp/src/components/threat-model/threats/Threats.js create mode 100644 src/Crisp.Ui/ClientApp/src/fetchers/recommendations.js create mode 100644 src/Crisp.Ui/ClientApp/src/models/Threat.ts create mode 100644 src/Crisp.Ui/ClientApp/src/models/ThreatRecommendation.ts create mode 100644 src/Crisp.Ui/ClientApp/tsconfig.json diff --git a/src/Crisp.Core.Tests/UnitTest1.cs b/src/Crisp.Core.Tests/UnitTest1.cs index c0eb92d..55e5a81 100644 --- a/src/Crisp.Core.Tests/UnitTest1.cs +++ b/src/Crisp.Core.Tests/UnitTest1.cs @@ -96,12 +96,12 @@ public async Task Test2() //[Fact] public async Task Test3() { - var recommendations = new List + var threats = new List { - new Recommendation("4", "test 1", "**Principle:** Confidentiality and Integrity \r\n**Affected Asset:** All services \r\n**Threat:** Secrets leaking into unsecured locations are an easy way for adversaries to gain access to a system. These secrets can be used to either spoof the owners of these secrets or, in the case of encryption keys, use them to decrypt data.\r\n\r\n**Mitigation:**\r\n\r\nProper storage and management of secrets is critical in protecting systems from compromises, in most cases, with severe impact.\r\n\r\n1. Never store secrets in code, configuration files or databases. Instead, use a vault or any secure container (such as encrypted variables) to store secrets.\r\n2. Separate application secrets by environment.\r\n3. Rotate all secrets before turning over the application to the customer.\r\n\r\n- Store all secrets, encryption keys and certificates in Key Vault.\r\n- You can use multiple Key Vaults to separate secrets for different and critical services to minimize secrets leaking\r\n- Define and implement secrets rotation strategy. All items in the vault should have expiration dates.", null), - new Recommendation("3", "test 1", "**Principle:** Confidentiality \r\n**Affected Asset:** All services \r\n**Threat:** Broken or non-existent authentication mechanisms may allow attackers to gain access to confidential information.\r\n\r\n**Mitigation:**\r\n\r\nAll services within the Azure Trust Boundary must authenticate all incoming requests, including requests coming from the same network. Proper authorizations should also be applied to prevent unnecessary privileges.\r\n\r\n1. Use Azure AD authentication for centralized identity management.\r\n2. Whenever available, use Azure Managed Identities to authenticate services. Service Principals may be used if Managed Identities are not supported.\r\n3. External users or services may use Username + Passwords, Tokens, or Certificates to authenticate, provided these are stored on Key Vault or any other vaulting solution.\r\n4. For authorization, use Azure RBAC to segregate duties and grant only the least amount of access to perform an action at a particular scope.\r\n5. Leverage AAD PIM for any administrative access.\r\n6. Avoid storing secrets in databases or configuration files.", null), - new Recommendation("2", "test 1", "this is **bold and *italic* and** but this is \\*\\*not\\*\\* this is `new block` and this is \\`not a block\\`", null), - new Recommendation("1", "test 1", "this is [link test](http://www.google.com) and now **in bold [google](http://www.google.com?q=test&t=now) *italic* and bold**", null), + new Threat("4", "test 1", "**Principle:** Confidentiality and Integrity \r\n**Affected Asset:** All services \r\n**Threat:** Secrets leaking into unsecured locations are an easy way for adversaries to gain access to a system. These secrets can be used to either spoof the owners of these secrets or, in the case of encryption keys, use them to decrypt data.\r\n\r\n**Mitigation:**\r\n\r\nProper storage and management of secrets is critical in protecting systems from compromises, in most cases, with severe impact.\r\n\r\n1. Never store secrets in code, configuration files or databases. Instead, use a vault or any secure container (such as encrypted variables) to store secrets.\r\n2. Separate application secrets by environment.\r\n3. Rotate all secrets before turning over the application to the customer.\r\n\r\n- Store all secrets, encryption keys and certificates in Key Vault.\r\n- You can use multiple Key Vaults to separate secrets for different and critical services to minimize secrets leaking\r\n- Define and implement secrets rotation strategy. All items in the vault should have expiration dates.", ThreatStatus.NotMitigated, ThreatRisk.High, 1, null, null), + new Threat("3", "test 1", "**Principle:** Confidentiality \r\n**Affected Asset:** All services \r\n**Threat:** Broken or non-existent authentication mechanisms may allow attackers to gain access to confidential information.\r\n\r\n**Mitigation:**\r\n\r\nAll services within the Azure Trust Boundary must authenticate all incoming requests, including requests coming from the same network. Proper authorizations should also be applied to prevent unnecessary privileges.\r\n\r\n1. Use Azure AD authentication for centralized identity management.\r\n2. Whenever available, use Azure Managed Identities to authenticate services. Service Principals may be used if Managed Identities are not supported.\r\n3. External users or services may use Username + Passwords, Tokens, or Certificates to authenticate, provided these are stored on Key Vault or any other vaulting solution.\r\n4. For authorization, use Azure RBAC to segregate duties and grant only the least amount of access to perform an action at a particular scope.\r\n5. Leverage AAD PIM for any administrative access.\r\n6. Avoid storing secrets in databases or configuration files.", ThreatStatus.PartiallyMitigated, ThreatRisk.Critical, 2, null, null), + new Threat("2", "test 1", "this is **bold and *italic* and** but this is \\*\\*not\\*\\* this is `new block` and this is \\`not a block\\`", ThreatStatus.Mitigated, ThreatRisk.Medium, 3, null, null), + new Threat("1", "test 1", "this is [link test](http://www.google.com) and now **in bold [google](http://www.google.com?q=test&t=now) *italic* and bold**", ThreatStatus.NotEvaluated, ThreatRisk.NotEvaluated, 4, null, null), }; var wordTemplate = File.ReadAllBytes("template.docx"); @@ -109,7 +109,7 @@ public async Task Test3() var stream = new MemoryStream(); stream.Write(wordTemplate, 0, wordTemplate.Length); - OpenXmlHelper.AddThreats(stream, recommendations, null); + OpenXmlHelper.AddThreats(stream, threats, null); File.WriteAllBytes("result2.docx", stream.ToArray()); } diff --git a/src/Crisp.Core/Helpers/MarkdownReportHelper.cs b/src/Crisp.Core/Helpers/MarkdownReportHelper.cs index 0128999..899361a 100644 --- a/src/Crisp.Core/Helpers/MarkdownReportHelper.cs +++ b/src/Crisp.Core/Helpers/MarkdownReportHelper.cs @@ -32,7 +32,7 @@ public static string GenerateThreatModelPropertiesSection(ThreatModel threatMode return section.ToString().TrimEnd(Environment.NewLine.ToCharArray()); } - public static string GenerateResourcesRecommendationsForThreat(Recommendation threat, + public static string GenerateResourcesRecommendationsForThreat(Threat threat, IDictionary>? benchmarks) { if (threat.BenchmarkIds is null || !threat.BenchmarkIds.Any() || benchmarks is null) diff --git a/src/Crisp.Core/Helpers/OpenXmlHelper.cs b/src/Crisp.Core/Helpers/OpenXmlHelper.cs index 8f45b10..c2f1de0 100644 --- a/src/Crisp.Core/Helpers/OpenXmlHelper.cs +++ b/src/Crisp.Core/Helpers/OpenXmlHelper.cs @@ -63,7 +63,7 @@ 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); diff --git a/src/Crisp.Core/Models/Threat.cs b/src/Crisp.Core/Models/Threat.cs new file mode 100644 index 0000000..15249a9 --- /dev/null +++ b/src/Crisp.Core/Models/Threat.cs @@ -0,0 +1,29 @@ +namespace Crisp.Core.Models; + +public enum ThreatStatus +{ + NotEvaluated, + NotMitigated, + PartiallyMitigated, + Mitigated +} + +public enum ThreatRisk +{ + NotEvaluated, + Critical, + High, + Medium, + Low +} + +public record Threat( + string Id, + string Title, + string Description, + ThreatStatus Status, + ThreatRisk Risk, + int OrderIndex, + IEnumerable? Recommendations, + IEnumerable? BenchmarkIds +); diff --git a/src/Crisp.Core/Models/ThreatModel.cs b/src/Crisp.Core/Models/ThreatModel.cs index 3d3e563..0435603 100644 --- a/src/Crisp.Core/Models/ThreatModel.cs +++ b/src/Crisp.Core/Models/ThreatModel.cs @@ -19,7 +19,7 @@ public record ThreatModel( DateTime? UpdatedAt, bool AddResourcesRecommendations, IEnumerable DataflowAttributes, - IEnumerable Threats, + IEnumerable Threats, IDictionary? Images, IEnumerable? Resources ) : IStorableItem; \ No newline at end of file diff --git a/src/Crisp.Core/Models/ThreatRecommendation.cs b/src/Crisp.Core/Models/ThreatRecommendation.cs new file mode 100644 index 0000000..ad3465d --- /dev/null +++ b/src/Crisp.Core/Models/ThreatRecommendation.cs @@ -0,0 +1,8 @@ +namespace Crisp.Core.Models; + +public record ThreatRecommendation( + string Id, + string Title, + string Description, + int OrderIndex +); diff --git a/src/Crisp.Ui/ClientApp/package-lock.json b/src/Crisp.Ui/ClientApp/package-lock.json index 04ae120..0fa00aa 100644 --- a/src/Crisp.Ui/ClientApp/package-lock.json +++ b/src/Crisp.Ui/ClientApp/package-lock.json @@ -8,12 +8,15 @@ "name": "crisp.ui", "version": "0.1.0", "dependencies": { + "@types/react": "^18.2.73", + "@types/react-dom": "^18.2.22", "bootstrap": "^5.2.0", "http-proxy-middleware": "^2.0.6", "jquery": "^3.6.0", "merge": "^2.1.1", "oidc-client": "^1.11.5", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-icons": "^4.7.1", "react-markdown": "^8.0.3", @@ -48,7 +51,7 @@ "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react": "^7.30.1", "nan": "^2.16.0", - "typescript": "^4.7.4" + "typescript": "^4.9.5" } }, "node_modules/@ampproject/remapping": { @@ -3565,6 +3568,15 @@ "@types/unist": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -3663,16 +3675,33 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "node_modules/@types/react": { - "version": "18.0.23", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.23.tgz", - "integrity": "sha512-R1wTULtCiJkudAN2DJGoYYySbGtOdzZyUWAACYinKdiQC8auxso4kLDUhQ7AJ2kh3F6A6z4v69U6tNY39hihVQ==", - "peer": true, + "version": "18.2.73", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.73.tgz", + "integrity": "sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.2.22", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz", + "integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -3686,12 +3715,6 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "node_modules/@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "peer": true - }, "node_modules/@types/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", @@ -5664,6 +5687,14 @@ "postcss": "^8.4" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-declaration-sorter": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.0.tgz", @@ -8276,6 +8307,14 @@ "@babel/runtime": "^7.7.6" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -11609,6 +11648,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/merge": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", @@ -14368,6 +14412,11 @@ "performance-now": "^2.1.0" } }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -14444,6 +14493,24 @@ "node": ">=14" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -14670,6 +14737,35 @@ } } }, + "node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -14886,6 +14982,14 @@ "node": "*" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -16359,6 +16463,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -16552,9 +16661,9 @@ } }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16815,6 +16924,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -20144,6 +20261,15 @@ "@types/unist": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -20242,16 +20368,33 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "@types/react": { - "version": "18.0.23", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.23.tgz", - "integrity": "sha512-R1wTULtCiJkudAN2DJGoYYySbGtOdzZyUWAACYinKdiQC8auxso4kLDUhQ7AJ2kh3F6A6z4v69U6tNY39hihVQ==", - "peer": true, + "version": "18.2.73", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.73.tgz", + "integrity": "sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==", "requires": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, + "@types/react-dom": { + "version": "18.2.22", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz", + "integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==", + "requires": { + "@types/react": "*" + } + }, + "@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -20265,12 +20408,6 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "peer": true - }, "@types/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", @@ -21706,6 +21843,14 @@ "postcss-selector-parser": "^6.0.9" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-declaration-sorter": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.0.tgz", @@ -23571,6 +23716,14 @@ "@babel/runtime": "^7.7.6" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -25982,6 +26135,11 @@ "fs-monkey": "^1.0.3" } }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "merge": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", @@ -27691,6 +27849,11 @@ "performance-now": "^2.1.0" } }, + "raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -27751,6 +27914,20 @@ "whatwg-fetch": "^3.6.2" } }, + "react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "requires": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -27915,6 +28092,26 @@ "match-sorter": "^6.0.2" } }, + "react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "dependencies": { + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + } + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -28078,6 +28275,14 @@ } } }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -29174,6 +29379,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -29320,9 +29530,9 @@ } }, "typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==" + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" }, "unbox-primitive": { "version": "1.0.2", @@ -29496,6 +29706,12 @@ "requires-port": "^1.0.0" } }, + "use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/src/Crisp.Ui/ClientApp/package.json b/src/Crisp.Ui/ClientApp/package.json index d4e9f2e..8b3e591 100644 --- a/src/Crisp.Ui/ClientApp/package.json +++ b/src/Crisp.Ui/ClientApp/package.json @@ -3,12 +3,15 @@ "version": "0.1.0", "private": true, "dependencies": { + "@types/react": "^18.2.73", + "@types/react-dom": "^18.2.22", "bootstrap": "^5.2.0", "http-proxy-middleware": "^2.0.6", "jquery": "^3.6.0", "merge": "^2.1.1", "oidc-client": "^1.11.5", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-icons": "^4.7.1", "react-markdown": "^8.0.3", @@ -43,7 +46,7 @@ "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react": "^7.30.1", "nan": "^2.16.0", - "typescript": "^4.7.4" + "typescript": "^4.9.5" }, "overrides": { "autoprefixer": "10.4.5" diff --git a/src/Crisp.Ui/ClientApp/src/AppRoutes.js b/src/Crisp.Ui/ClientApp/src/AppRoutes.js index f07b472..1afd31f 100644 --- a/src/Crisp.Ui/ClientApp/src/AppRoutes.js +++ b/src/Crisp.Ui/ClientApp/src/AppRoutes.js @@ -1,6 +1,6 @@ import Home from "./components/Home"; import ThreatModels from "./components/ThreatModels"; -import AddThreatModel from "./components/AddThreatModel"; +import ThreatModel from "./components/threat-model/ThreatModel"; import ThreatModelReport from "./components/ThreatModelReport"; import Recommendations from "./components/Recommendations"; import Resources from "./components/Resources"; @@ -17,7 +17,7 @@ const AppRoutes = [ }, { path: '/addthreatmodel', - element: + element: }, { path: '/threatmodelreport', diff --git a/src/Crisp.Ui/ClientApp/src/components/Recommendations.js b/src/Crisp.Ui/ClientApp/src/components/Recommendations.js index 09a3f71..d8bb8dd 100644 --- a/src/Crisp.Ui/ClientApp/src/components/Recommendations.js +++ b/src/Crisp.Ui/ClientApp/src/components/Recommendations.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Spinner, ListGroup, Alert, Button, Badge } from 'reactstrap'; import { useQuery } from 'react-query'; -import { fetchCategory } from '../fetchers/categories'; +import { fetchCatalogRecommendations } from '../fetchers/recommendations'; import Category from './Category'; import { useEffect } from 'react'; import './Recommendations.css'; @@ -9,7 +9,7 @@ import './Recommendations.css'; const Recommendations = () => { - const { isError, isLoading, data, error } = useQuery(['category'], fetchCategory, { staleTime: 24*60*60*1000 }); + const { isError, isLoading, data, error } = useQuery(['catalog-recommendations'], fetchCatalogRecommendations, { staleTime: 24*60*60*1000 }); const category = data; const [selectedList, setSelectedList] = useState([]); diff --git a/src/Crisp.Ui/ClientApp/src/components/ThreatModelStatusBar.tsx b/src/Crisp.Ui/ClientApp/src/components/ThreatModelStatusBar.tsx new file mode 100644 index 0000000..bdfa994 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/ThreatModelStatusBar.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import { Tooltip, Row, Col } from 'reactstrap'; +import ThreatStatus from './threat-model/threats/ThreatStatus'; +import { Threat } from '../models/Threat'; + +interface ThreatModelStatusBarProps { + id: string; + threats: Threat[]; +} + +const ThreatModelStatusBar: React.FC = ({ id, threats }) => { + + const orderedStatuses = [1, 2, 3, 0]; + + const [tooltipOpen, setTooltipOpen] = useState(false); + + const totalThreats = threats.length; + + const statusCounts = threats.reduce<{ [key: number]: number }>((acc, threat) => { + acc[threat.status] = (acc[threat.status] || 0) + 1; + return acc; + }, {}); + + const getStatusWidth = (status: number): number => { + return statusCounts[status] ? (statusCounts[status] / totalThreats) * 100 : 0; + }; + + const getStatusColor = (status: number): string => { + switch (status) { + case 1: return '#a4262c'; // Not mitigated + case 2: return '#db7500'; // Partially mitigated + case 3: return 'green'; // Mitigated + default: return '#a19f9d'; // Not evaluated + } + }; + + const toggleTooltip = () => setTooltipOpen(prev => !prev); + + return ( + <> +
+ {orderedStatuses.map((status) => ( +
+ ))} +
+ +
+ {orderedStatuses.map(s => statusCounts[s] + ? ( + + + {statusCounts[s]} + + ) : null + )} +
+
+ + ); +}; + +export default ThreatModelStatusBar; diff --git a/src/Crisp.Ui/ClientApp/src/components/ThreatModels.js b/src/Crisp.Ui/ClientApp/src/components/ThreatModels.js index ab296a7..bd2b7f0 100644 --- a/src/Crisp.Ui/ClientApp/src/components/ThreatModels.js +++ b/src/Crisp.Ui/ClientApp/src/components/ThreatModels.js @@ -1,16 +1,20 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Spinner, Alert, Button, Table } from 'reactstrap'; import { useQuery, useMutation, useQueryClient } from 'react-query'; import { fetchThreatModels, deleteThreatModel } from '../fetchers/threatmodels'; import { useNavigate } from 'react-router-dom'; import { FiEdit2, FiDownload, FiX, FiSearch, FiPlus } from "react-icons/fi"; +import ThreatModelStatusBar from './ThreatModelStatusBar'; const ThreatModels = () => { const navigate = useNavigate(); - const { isError, isLoading, data, error } = useQuery(['threatmodels'], fetchThreatModels, { staleTime: 1 * 60 * 60 * 1000 }); - const threatModels = data; + const threatModels = useQuery(['threatmodels'], fetchThreatModels, { staleTime: 1 * 60 * 60 * 1000 }); + + const sortedThreatModels = useMemo(() => { + return threatModels && threatModels.data ? [...threatModels.data].sort((a, b) => a.projectName > b.projectName ? 1 : -1) : []; + }, [threatModels]); const queryClient = useQueryClient(); @@ -22,8 +26,13 @@ const ThreatModels = () => { return `api/threatmodels/${id}/report/archive`; } - const deleteHandler = async (id) => { - const threatModel = threatModels.find(t => t.id === id); + const addUpdateThreatModel = (threatModel) => { + navigate('/addthreatmodel', threatModel ? { state: { threatModel: threatModel } } : {}); + } + + const deleteThreatModelHandler = async (e, id) => { + e.stopPropagation(); + const threatModel = threatModels.data.find(t => t.id === id); if (!threatModel || !window.confirm(`Do you want to delete security plan '${threatModel.projectName}' ?`)) { return; } @@ -35,7 +44,17 @@ const ThreatModels = () => { catch { } } - if (isLoading) { + const downloadReport = (e, id) => { + e.stopPropagation(); + window.open(getReportUrl(id), '_blank'); + } + + const navigateToReport = (e, id) => { + e.stopPropagation(); + navigate('/threatmodelreport', { state: { id: id } }); + } + + if (threatModels?.isLoading) { return (
@@ -45,41 +64,42 @@ const ThreatModels = () => { ); } - if (isError) { + if (threatModels?.isError) { return ( - {error.message} + {threatModels.error.message} ); } return ( <>
- +
- {!threatModels || threatModels.length === 0 ? ( + {!sortedThreatModels.length === 0 ? (

There are no security plans

) : ( + - {threatModels.sort((a, b) => a.projectName > b.projectName ? 1 : -1).map(t => ( - - + {sortedThreatModels.map(t => ( + addUpdateThreatModel(t)} className="cursor-pointer align-middle"> + + diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/DataFlowAttributes.css b/src/Crisp.Ui/ClientApp/src/components/threat-model/DataFlowAttributes.css new file mode 100644 index 0000000..d93c8e8 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/DataFlowAttributes.css @@ -0,0 +1,4 @@ +.tooltip-inner { + max-width: 750px; + text-align: start; +} diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/DataFlowAttributes.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/DataFlowAttributes.js new file mode 100644 index 0000000..780bcc0 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/DataFlowAttributes.js @@ -0,0 +1,113 @@ +import React, { useState } from 'react'; +import { Button, Label, Input, Row, Col, Tooltip } from 'reactstrap'; +import { FiPlus, FiInfo } from "react-icons/fi"; +import './DataFlowAttributes.css'; + +const DataFlowAttributes = ({ dataflowAttributes, setDataflowAttributes }) => { + + const [dataClassificationTooltipOpen, setDataClassificationTooltipOpen] = useState(false); + + const addDataflowAttribute = () => { + const nextIndex = dataflowAttributes.length > 0 + ? Math.max(...dataflowAttributes.map(a => parseInt(a.number, 10))) + 1 + : 1; + + const newAttribute = { + number: nextIndex.toString(), + transport: 'HTTPS/TLS 1.2', + dataClassification: 'Confidential', + authentication: 'Microsoft Entra ID', + authorization: 'RBAC', + notes: '' + }; + setDataflowAttributes(prev => [...prev, newAttribute]); + } + + const deleteDataflowAttribute = (index) => { + setDataflowAttributes(prev => prev.filter((_, attrIndex) => attrIndex !== index)); + } + + const onDataflowAttributeChange = (e, index) => { + const { name, value } = e.target; + setDataflowAttributes(prev => + prev.map((attribute, attrIndex) => + attrIndex === index ? { ...attribute, [name]: value } : attribute + ) + ); + } + + const onDataClassificationTooltipToggle = () => setDataClassificationTooltipOpen(!dataClassificationTooltipOpen); + + return ( +
+ + +
+ + + + + + + + + + + + + + + + + + + {dataflowAttributes.map((a, index) => ( + + + onDataflowAttributeChange(e, index)} /> + + + onDataflowAttributeChange(e, index)} /> + + + onDataflowAttributeChange(e, index)}> + + + + + + + + + onDataflowAttributeChange(e, index)} /> + + + onDataflowAttributeChange(e, index)} /> + + + + + onDataflowAttributeChange(e, index)} /> + + + + + + + + )) + } + +
    +
  • Sensitive
    Data that is to have the most limited access and requires a high degree of integrity. This is typically data that will do the most damage to the organization should it be disclosed. Personal data (including PII) falls into this category and includes any identifier, such as name, an identification number, location data, online identifier. This also includes data related to one or more factors specific to the physical, psychological, genetic, mental, economic, cultural, or social identity of an individual.
  • +
  • Confidential
    Data that might be less restrictive within the company but might cause damage if disclosed.
  • +
  • Private
    Private data is usually compartmental data that might not do the company damage but must be kept private for other reasons. Human resources data is one example of data that can be classified as private.
  • +
  • Proprietary
    Proprietary data is data that is disclosed outside the company on a limited basis or contains information that could reduce the company's competitive advantage, such as the technical specifications of a new product.
  • +
  • Public
    Public data is the least sensitive data used by the company and would cause the least harm if disclosed. This could be anything from data used for marketing to the number of employees in the company.
  • +
+
+ + ); +}; + +export default DataFlowAttributes; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModel.css b/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModel.css new file mode 100644 index 0000000..e69de29 diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModel.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModel.js new file mode 100644 index 0000000..da27268 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModel.js @@ -0,0 +1,136 @@ +import React, { useState, useEffect } from 'react'; +import { Spinner, Button, FormGroup, Label, Input,UncontrolledAlert} from 'reactstrap'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useQuery, useMutation, useQueryClient } from 'react-query'; +import { createThreatModel, fetchThreatModelFiles, updateThreatModel } from '../../fetchers/threatmodels'; +import { FiArrowLeft, FiCheck } from "react-icons/fi"; +import DataFlowAttributes from './DataFlowAttributes'; +import ThreatModelImage from './ThreatModelImage'; +import ThreatModelResources from './ThreatModelResources'; +import Threats from './threats/Threats'; +import './ThreatModel.css'; + +const ThreatModel = () => { + + const navigate = useNavigate(); + const { state } = useLocation(); + const oldThreatModel = state ? state.threatModel : null; + + const [projectName, setProjectName] = useState(oldThreatModel?.projectName ?? ''); + const [selectedResources, setSelectedResources] = useState(oldThreatModel?.resources ?? []); + const [dataflowAttributes, setDataflowAttributes] = useState(oldThreatModel?.dataflowAttributes ?? []); + const [addResourcesRecommendations, setAddResourcesRecommendations] = useState(oldThreatModel?.addResourcesRecommendations ?? false); + const [images, setImages] = useState([]); + const [threats, setThreats] = useState(oldThreatModel?.threats ?? []); + const [imagesDownloaded, setImagesDownloaded] = useState(false); + const [saveButtonDisabled, setSaveButtonDisabled] = useState(true); + + const queryClient = useQueryClient(); + const queryName = oldThreatModel ? `threatmodel-images-${oldThreatModel.id}` : 'threatmodel-images'; + useQuery([queryName], () => fetchThreatModelFiles(oldThreatModel?.id, oldThreatModel?.images), { + onSuccess: (data) => { + if (data.length > 0 && !imagesDownloaded) { + setImages(data.map(f => ({ + type: f.type, + fileName: f.name, + file: null, + url: URL.createObjectURL(f.content) + }))); + setImagesDownloaded(true); + } + } + }); + + const onProjectNameChange = (e) => { + setProjectName(e.target.value); + }; + + useEffect(() => { + setSaveButtonDisabled(!projectName || projectName.length === 0); + }, [projectName]); + + const createThreatModelMutation = useMutation(threatModel => { + return createThreatModel(threatModel, images); + }); + + const updateThreatModelMutation = useMutation(threatModel => { + return updateThreatModel(oldThreatModel.id, threatModel, images); + }); + + const saveThreatModel = async () => { + const threatModel = { + projectName: projectName, + dataflowAttributes: dataflowAttributes, + addResourcesRecommendations: addResourcesRecommendations, + threats: threats, + images: images.length > 0 ? images.map(i => ({ key: i.type, value: i.fileName })) : null, + resources: selectedResources + }; + try { + if (!oldThreatModel) { + await createThreatModelMutation.mutateAsync(threatModel); + } else { + await updateThreatModelMutation.mutateAsync(threatModel); + queryClient.invalidateQueries([`threatmodelreport.${oldThreatModel.id}`]); + queryClient.refetchQueries(`threatmodelreport.${oldThreatModel.id}`, { force: true }); + } + queryClient.invalidateQueries(['threatmodels']); + queryClient.refetchQueries('threatmodels', { force: true }); + navigate('/threatmodels'); + } + catch { } + } + + return ( + <> +
+ +
+ + + + + +
Architecture diagram
+ +
+ +
Resources
+ +
+ +
Data flow diagram
+ +
+ +
Data flow attributes
+ +
+ +
Threat map
+ +
+ +
Threats and Mitigations
+ +
+ + + {createThreatModelMutation.isLoading && + Loading... + } + {createThreatModelMutation.isError && + + {createThreatModelMutation.error.message} + + } + + + ); +}; + +export default ThreatModel; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelImage.css b/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelImage.css new file mode 100644 index 0000000..47af139 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelImage.css @@ -0,0 +1,5 @@ +img.diagram { + width: 100%; + border: 1px dashed #999999; + padding: 1em; +} diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelImage.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelImage.js new file mode 100644 index 0000000..508ad0f --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelImage.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { CloseButton } from 'reactstrap'; +import './ThreatModelImage.css'; + +const ThreatModelImage = ({ images, setImages, type }) => { + + function onImageChange(type, e) { + const newImages = images.filter(i => i.type != type); + if (e && e.target && e.target.files[0]) { + const file = e.target.files[0]; + newImages.push({ type: type, fileName: file.name, file: file, url: URL.createObjectURL(file) }); + } else { + const element = document.getElementById(`image-${type}`); + element.value = null; + } + setImages(newImages); + } + + return ( +
+ onImageChange(type, e)} + /> + {images.filter(i => i.type === type).map(i => ( +
+ {`${type} + onImageChange(type)} /> +
+ ))} +
+ ); +}; + +export default ThreatModelImage; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelResources.css b/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelResources.css new file mode 100644 index 0000000..7bf5510 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelResources.css @@ -0,0 +1,6 @@ +button.resource-small { + font-size: 0.85em; + width: 205px; + height: 50px; + margin: 0 !important; +} diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelResources.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelResources.js new file mode 100644 index 0000000..e68c6d2 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/ThreatModelResources.js @@ -0,0 +1,45 @@ +import React from 'react'; +import { Spinner, Button, Row, Col, Input, FormGroup } from 'reactstrap'; +import { useQuery} from 'react-query'; +import { fetchResources } from '../../fetchers/resources'; +import './ThreatModelResources.css'; + +const ThreatModelResources = ({ addResourcesRecommendations, setAddResourcesRecommendations, selectedResources, setSelectedResources }) => { + + const resources = useQuery([`fetchResources`], fetchResources, { staleTime: 1 * 60 * 60 * 1000 }); + + const onResourcesChange = (resourceName) => { + if (selectedResources.includes(resourceName)) { + setSelectedResources(selectedResources.filter(n => n !== resourceName)); + } else { + setSelectedResources([...selectedResources, resourceName]); + } + }; + + const resourceButtonColor = (resourceName) => { + return selectedResources.includes(resourceName) ? 'primary' : 'secondary' + } + + return ( + <> + + setAddResourcesRecommendations(!addResourcesRecommendations)} /> Add resources recommendations to threats + + {addResourcesRecommendations ? ( + + {resources.isLoading ? ( +
+ Loading... +
+ ) : resources.data.resources.map(r => ( +
+ + + ))} + + ) : null} + + ); +}; + +export default ThreatModelResources; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectCatalogRecommendations.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectCatalogRecommendations.js new file mode 100644 index 0000000..2112b48 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectCatalogRecommendations.js @@ -0,0 +1,98 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import { Spinner, ListGroup, Alert, Button, Badge } from 'reactstrap'; +import { useQuery } from 'react-query'; +import { fetchCatalogRecommendations } from '../../../fetchers/recommendations'; +import { FiPlus, FiArrowLeft } from "react-icons/fi"; +import Category from '../../Category'; + +const STALE_TIME = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + +const SelectCatalogRecommendations = ({ recommendations, onClose }) => { + + const recommendationsCategory = useQuery(['catalog-recommendations'], fetchCatalogRecommendations, { staleTime: STALE_TIME }); + + + const [selectedList, setSelectedList] = useState([]); + + const getChildrenIds = useCallback(category => { + let ids = [category.id]; + if (category.children) { + category.children.forEach(c => ids = [...ids, ...getChildrenIds(c)]); + } + if (category.recommendations) { + category.recommendations.forEach(r => ids = [...ids, r.id]); + } + return ids; + }, []); + + const toggleSelectability = useCallback(selectedCategory => { + setSelectedList(prev => { + const toggledIds = getChildrenIds(selectedCategory); + if (prev.includes(selectedCategory.id)) { + return prev.filter(id => !toggledIds.includes(id)); + } else { + return [...prev, ...toggledIds.filter(id => !prev.includes(id))]; + } + }); + }, [getChildrenIds]); + + const isSelected = useCallback(id => { + return selectedList.includes(id); + }, [selectedList]); + + const selectedRecommendations = useMemo(() => { + const getSelectedRecommendations = (category) => { + if (!category) { + return []; + } + let recommendations = category.recommendations?.filter(r => isSelected(r.id)) || []; + category.children?.forEach(c => recommendations = [...recommendations, ...getSelectedRecommendations(c)]); + return recommendations; + }; + + return recommendationsCategory?.data ? getSelectedRecommendations(recommendationsCategory.data) : []; + }, [recommendationsCategory, isSelected]); + + const selectedRecommendationsCount = selectedRecommendations.length; + + const addRecommendations = useCallback(() => { + onClose(selectedRecommendations); + }, [onClose, selectedRecommendations]); + + if (recommendationsCategory.isLoading) { + return
Loading...
; + } + + if (recommendationsCategory.isError) { + return {recommendationsCategory.error.message}; + } + + return ( + <> +
+ +
+
Select recommendations
+ {!recommendationsCategory.data ? ( +

There are no recommendations

+ ) : ( + <> + + + + + + + )} + + ); +}; + +const SelectRecommendationsActionBar = ({ onSave, selectedCount }) => ( +
+ {' '} + Selected recommendations {selectedCount} +
+); + +export default SelectCatalogRecommendations; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectResourcesRecommendations.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectResourcesRecommendations.js new file mode 100644 index 0000000..48d9017 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectResourcesRecommendations.js @@ -0,0 +1,120 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import { Spinner, ListGroup, Alert, Button, Badge } from 'reactstrap'; +import { useQuery } from 'react-query'; +import { fetchRecommendations } from '../../../fetchers/resources'; +import { FiPlus, FiArrowLeft } from "react-icons/fi"; +import Category from '../../Category'; + +const STALE_TIME = 24 * 60 * 60 * 1000; // 24 hours in milliseconds +const RECOMMENDATION_TITLE_DIVIDER = ' - '; + +const SelectResourcesRecommendations = ({ recommendations, resources, onClose }) => { + + const recommendationsCategory = useQuery(['resources-recommendations-category', ...resources], () => fetchRecommendations(resources), { staleTime: STALE_TIME }); + + const [selectedList, setSelectedList] = useState([]); + + const getChildrenIds = useCallback(category => { + let ids = [category.id]; + if (category.children) { + category.children.forEach(c => ids = [...ids, ...getChildrenIds(c)]); + } + if (category.recommendations) { + category.recommendations.forEach(r => ids = [...ids, r.id]); + } + return ids; + }, []); + + const toggleSelectability = useCallback(selectedCategory => { + setSelectedList(prev => { + const toggledIds = getChildrenIds(selectedCategory); + if (prev.includes(selectedCategory.id)) { + return prev.filter(id => !toggledIds.includes(id)); + } else { + return [...prev, ...toggledIds.filter(id => !prev.includes(id))]; + } + }); + }, [getChildrenIds]); + + const isSelected = useCallback(id => { + return selectedList.includes(id); + }, [selectedList]); + + const selectedRecommendations = useMemo(() => { + const getSelectedRecommendations = (category, titlePrefix = '', isRootCategory = true) => { + if (!category) { + return []; + } + + const updatedTitlePrefix = isRootCategory + ? titlePrefix + : titlePrefix ? `${titlePrefix}${RECOMMENDATION_TITLE_DIVIDER}${category.name}` : category.name; + + const parts = updatedTitlePrefix.split(RECOMMENDATION_TITLE_DIVIDER); + const resourceName = parts[0]; + const categoryNames = parts.slice(1).join(RECOMMENDATION_TITLE_DIVIDER); + + let recommendations = category.recommendations + .filter(r => isSelected(r.id)) + .map(r => { + const title = r.title === "No Related Feature" + ? updatedTitlePrefix + : updatedTitlePrefix ? `${updatedTitlePrefix}${RECOMMENDATION_TITLE_DIVIDER}${r.title}` : r.title; + const titleInDescription = r.title === "No Related Feature" + ? '' + : ` \n**Title:** ${r.title}`; + const description = updatedTitlePrefix + ? `**Resource:** ${resourceName} \n**Category:** ${categoryNames}${titleInDescription}\n\n${r.description}` + : r.description; + return { ...r, title, description }; + }); + category.children.forEach(c => recommendations = [...recommendations, ...getSelectedRecommendations(c, updatedTitlePrefix, false)]); + return recommendations; + } + + return recommendationsCategory?.data ? getSelectedRecommendations(recommendationsCategory.data) : []; + }, [recommendationsCategory, isSelected]); + + const selectedRecommendationsCount = selectedRecommendations.length; + + const addRecommendations = useCallback(() => { + onClose(selectedRecommendations); + }, [onClose, selectedRecommendations]); + + if (recommendationsCategory.isLoading) { + return
Loading...
; + } + + if (recommendationsCategory.isError) { + return {recommendationsCategory.error.message}; + } + + return ( + <> +
+ +
+
Select recommendations
+ {!recommendationsCategory.data ? ( +

There are no recommendations

+ ) : ( + <> + + + + + + + )} + + ); +}; + +const SelectRecommendationsActionBar = ({ onSave, selectedCount }) => ( +
+ {' '} + Selected recommendations {selectedCount} +
+); + +export default SelectResourcesRecommendations; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectSecurityBenchmarkRecommendations.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectSecurityBenchmarkRecommendations.js new file mode 100644 index 0000000..49d388d --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/SelectSecurityBenchmarkRecommendations.js @@ -0,0 +1,97 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import { Spinner, ListGroup, Alert, Button, Badge } from 'reactstrap'; +import { useQuery } from 'react-query'; +import { fetchBenchmarkControls } from '../../../fetchers/resources'; +import { FiPlus, FiArrowLeft } from "react-icons/fi"; +import Category from '../../Category'; + +const STALE_TIME = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + +const SelectSecurityBenchmarkRecommendations = ({ recommendations, onClose }) => { + + const recommendationsCategory = useQuery(['benchmark-controls-category'], fetchBenchmarkControls, { staleTime: STALE_TIME }); + + const [selectedList, setSelectedList] = useState([]); + + const getChildrenIds = useCallback(category => { + let ids = [category.id]; + if (category.children) { + category.children.forEach(c => ids = [...ids, ...getChildrenIds(c)]); + } + if (category.recommendations) { + category.recommendations.forEach(r => ids = [...ids, r.id]); + } + return ids; + }, []); + + const toggleSelectability = useCallback(selectedCategory => { + setSelectedList(prev => { + const toggledIds = getChildrenIds(selectedCategory); + if (prev.includes(selectedCategory.id)) { + return prev.filter(id => !toggledIds.includes(id)); + } else { + return [...prev, ...toggledIds.filter(id => !prev.includes(id))]; + } + }); + }, [getChildrenIds]); + + const isSelected = useCallback(id => { + return selectedList.includes(id); + }, [selectedList]); + + const selectedRecommendations = useMemo(() => { + const getSelectedRecommendations = (category) => { + if (!category) { + return []; + } + let recommendations = category.recommendations?.filter(r => isSelected(r.id)) || []; + category.children?.forEach(c => recommendations = [...recommendations, ...getSelectedRecommendations(c)]); + return recommendations; + }; + + return recommendationsCategory?.data ? getSelectedRecommendations(recommendationsCategory.data) : []; + }, [recommendationsCategory, isSelected]); + + const selectedRecommendationsCount = selectedRecommendations.length; + + const addRecommendations = useCallback(() => { + onClose(selectedRecommendations); + }, [onClose, selectedRecommendations]); + + if (recommendationsCategory.isLoading) { + return
Loading...
; + } + + if (recommendationsCategory.isError) { + return {recommendationsCategory.error.message}; + } + + return ( + <> +
+ +
+
Select recommendations
+ {!recommendationsCategory.data ? ( +

There are no recommendations

+ ) : ( + <> + + + + + + + )} + + ); +}; + +const SelectRecommendationsActionBar = ({ onSave, selectedCount }) => ( +
+ {' '} + Selected recommendations {selectedCount} +
+); + +export default SelectSecurityBenchmarkRecommendations; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/ThreatRecommendation.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/ThreatRecommendation.js new file mode 100644 index 0000000..38b2615 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/ThreatRecommendation.js @@ -0,0 +1,44 @@ +import React, { useState, useEffect } from 'react'; +import { Button, FormGroup, Label, Input } from 'reactstrap'; +import { FiArrowLeft, FiCheck } from "react-icons/fi"; +import MarkdownEditor from '../threats/MarkdownEditor'; + +const ThreatRecommendation = ({ recommendation, onClose }) => { + + const [recommendationLocal, setRecommendationLocal] = useState(recommendation); + const [saveButtonDisabled, setSaveButtonDisabled] = useState(false); + + const onModelPropertyChange = (e) => { + const { name, value } = e.target; + setRecommendationLocal(prev => ({ ...prev, [name]: value })); + } + + useEffect(() => { + const isDisabled = !recommendationLocal.title.trim() || !recommendationLocal.description.trim(); + setSaveButtonDisabled(isDisabled); + }, [recommendationLocal.title, recommendationLocal.description]); + + return ( + <> +
+ +
+
{recommendation && recommendation.id ? "Update recommendation" : "New recommendation"}
+ + + + + onModelPropertyChange({ target: { name: "description", value: value } })} + /> + + + + + ); +}; + +export default ThreatRecommendation; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/ThreatRecommendations.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/ThreatRecommendations.js new file mode 100644 index 0000000..9e1f159 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/threat-recommendations/ThreatRecommendations.js @@ -0,0 +1,236 @@ +import React, { useState, useMemo } from 'react'; +import { Button, Table, Dropdown, DropdownToggle, DropdownMenu, DropdownItem, Modal, ModalBody } from 'reactstrap'; +import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; +import { FiPlus, FiX } from "react-icons/fi"; +import { RxDragHandleDots2 } from "react-icons/rx"; +import SelectResourcesRecommendations from './SelectResourcesRecommendations'; +import ThreatRecommendation from './ThreatRecommendation'; +import SelectSecurityBenchmarkRecommendations from './SelectSecurityBenchmarkRecommendations'; +import SelectCatalogRecommendations from './SelectCatalogRecommendations'; + +const ThreatRecommendations = ({ recommendations, setRecommendations, resources }) => { + + const [addRecommendationMenuOpen, setAddRecommendationMenuOpen] = useState(false); + const [selectResourcesRecommendationsModalOpen, setSelectResourcesRecommendationsModalOpen] = useState(false); + const [selectSecurityBenchmarkRecommendationsModalOpen, setSelectSecurityBenchmarkRecommendationsModalOpen] = useState(false); + const [selectCatalogRecommendationsModalOpen, setSelectCatalogRecommendationsModalOpen] = useState(false); + const [recommendationModalOpen, setRecommendationModalOpen] = useState(false); + const [selectedRecommendation, setSelectedRecommendation] = useState(null); + + const sortedRecommendations = useMemo(() => { + return [...recommendations].sort((a, b) => a.orderIndex - b.orderIndex); + }, [recommendations]); + + const defaultRecommendation = { + title: '', + description: '', + orderIndex: getNextOrderIndex() + }; + + function getNextOrderIndex() { + return recommendations.length > 0 ? Math.max(...recommendations.map(t => t.orderIndex)) + 1 : 1; + } + + const addOrUpdateRecommendation = (recommendation) => { + setSelectedRecommendation(recommendation); + setRecommendationModalOpen(true); + } + + const deleteRecommendation = (e, recommendation) => { + e.stopPropagation(); + if (!window.confirm(`Do you want to delete recommendation '${recommendation.title}' ?`)) { + return; + } + setRecommendations(prev => prev.filter(t => t.id !== recommendation.id) + .map(t => t.orderIndex > recommendation.orderIndex ? { ...t, orderIndex: t.orderIndex - 1 } : t)); + } + + const addRecommendationsForResources = () => { + setSelectResourcesRecommendationsModalOpen(true); + } + + const addRecommendationsFromSecurityBenchmark = () => { + setSelectSecurityBenchmarkRecommendationsModalOpen(true); + } + + const addRecommendationsFromCatalog = () => { + setSelectCatalogRecommendationsModalOpen(true); + } + + const toggleAddRecommendationsMenu = () => setAddRecommendationMenuOpen((prevState) => !prevState); + + const toggleSelectResourcesRecommendationsModal = (selectedRecommendations) => { + if (selectedRecommendations) { + let orderIndex = getNextOrderIndex(); + const recommendationsToAdd = []; + selectedRecommendations.filter(r => !recommendations.find(recommendation => recommendation.id === r.id)).forEach(r => { + recommendationsToAdd.push({ + id: r.id, + title: r.title, + description: r.description, + orderIndex: orderIndex + }); + orderIndex++; + }); + setRecommendations(prev => [...prev, ...recommendationsToAdd]); + } + setSelectResourcesRecommendationsModalOpen(prev => !prev); + } + + const toggleSelectSecurityBenchmarkRecommendationsModal = (selectedRecommendations) => { + if (selectedRecommendations) { + let orderIndex = getNextOrderIndex(); + const recommendationsToAdd = []; + selectedRecommendations.filter(r => !recommendations.find(recommendation => recommendation.id === r.id)).forEach(r => { + recommendationsToAdd.push({ + id: r.id, + title: r.title, + description: r.description, + orderIndex: orderIndex + }); + orderIndex++; + }); + setRecommendations(prev => [...prev, ...recommendationsToAdd]); + } + setSelectSecurityBenchmarkRecommendationsModalOpen(prev => !prev); + } + + const toggleSelectCatalogRecommendationsModal = (selectedRecommendations) => { + if (selectedRecommendations) { + let orderIndex = getNextOrderIndex(); + const recommendationsToAdd = []; + selectedRecommendations.filter(r => !recommendations.find(recommendation => recommendation.id === r.id)).forEach(r => { + recommendationsToAdd.push({ + id: r.id, + title: r.title, + description: r.description, + orderIndex: orderIndex + }); + orderIndex++; + }); + setRecommendations(prev => [...prev, ...recommendationsToAdd]); + } + setSelectCatalogRecommendationsModalOpen(prev => !prev); + } + + const toggleRecommendationModal = (recommendation) => { + if (recommendation) { + if (recommendation.id) { + setRecommendations(prev => { + const index = prev.findIndex(r => r.id === recommendation.id); + prev[index] = recommendation; + return [...prev]; + }); + + setSelectedRecommendation(null); + } else { + recommendation.id = Math.floor(Date.now() / 1000).toString(); + console.log(recommendation.id); + setRecommendations(prev => [...prev, recommendation]); + } + } + setRecommendationModalOpen(prev => !prev); + } + + const onDragEnd = (result) => { + if (!result.destination || result.destination.index === result.source.index) { + return; + } + + const recommendationMovedForward = result.destination.index > result.source.index; + const newOrderIndex = result.destination.index + 1; + + setRecommendations(prev => prev.map(r => { + if (r.id === result.draggableId) { + return { ...r, orderIndex: newOrderIndex }; + } else if (recommendationMovedForward) { + if (r.orderIndex > result.source.index + 1 && r.orderIndex <= newOrderIndex) { + return { ...r, orderIndex: r.orderIndex - 1 }; + } + } else { + if (r.orderIndex >= newOrderIndex && r.orderIndex < result.source.index + 1) { + return { ...r, orderIndex: r.orderIndex + 1 }; + } + } + return r; + })); + }; + + return ( + <> +
+ + Add recommendation   + + addOrUpdateRecommendation(defaultRecommendation)}>New recommendation + For selected resources + From cloud security benchmark + From catalog + + +
+ {sortedRecommendations.length > 0 ? ( + + + {(provided) => ( +
Project nameStatus Created Updated
{t.projectName}
{(new Date(t.createdAt)).toLocaleDateString()} {t.updatedAt ? (new Date(t.updatedAt)).toLocaleDateString() : 'Never'}
- - - - + + +
+ + + + + + + + + {sortedRecommendations.map((r, index) => ( + + {(provided, snapshot) => ( + addOrUpdateRecommendation(r)} + className={`cursor-pointer align-middle ${snapshot.isDragging ? "dragging" : ""}`} + > + + + + + )} + + ))} + {provided.placeholder} + +
Title
{r.orderIndex} +
+ +
+
+ )} + + + ) : null} + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ThreatRecommendations; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/MarkdownEditor.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/MarkdownEditor.js new file mode 100644 index 0000000..5cc1b6a --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/MarkdownEditor.js @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import { FormGroup, Label, Input, Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap'; +import ReactMarkdown from 'react-markdown'; + +const MarkdownEditor = ({ label, fieldName, placeholder, value, setValue }) => { + const [activeTab, setActiveTab] = useState('1'); + + const toggleTab = (tab) => { + if (activeTab !== tab) setActiveTab(tab); + }; + + const onDescriptionChange = (e) => { + setValue(e.target.value); + }; + + return ( + + + + + + + + + {value} + + + + ); +}; + +export default MarkdownEditor; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/SelectThreats.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/SelectThreats.js new file mode 100644 index 0000000..fdc06b9 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/SelectThreats.js @@ -0,0 +1,97 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import { Spinner, ListGroup, Alert, Button, Badge } from 'reactstrap'; +import { useQuery } from 'react-query'; +import { fetchThreatModelCategory } from '../../../fetchers/threatmodels'; +import { FiPlus, FiArrowLeft } from "react-icons/fi"; +import Category from '../../Category'; + +const STALE_TIME = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + +const SelectThreats = ({ threats, onClose }) => { + + const threatsCategory = useQuery(['threatmodel-category'], fetchThreatModelCategory, { staleTime: STALE_TIME }); + + const [selectedList, setSelectedList] = useState([]); + + const getChildrenIds = useCallback(category => { + let ids = [category.id]; + if (category.children) { + category.children.forEach(c => ids.push(...getChildrenIds(c))); + } + if (category.recommendations) { + category.recommendations.forEach(r => ids.push(r.id)); + } + return ids; + }, []); + + const toggleSelectability = useCallback(selectedCategory => { + setSelectedList(prev => { + const toggledIds = getChildrenIds(selectedCategory); + if (prev.includes(selectedCategory.id)) { + return prev.filter(id => !toggledIds.includes(id)); + } else { + return [...prev, ...toggledIds.filter(id => !prev.includes(id))]; + } + }); + }, [getChildrenIds]); + + const isSelected = useCallback(id => { + return selectedList.includes(id); + }, [selectedList]); + + const selectedRecommendations = useMemo(() => { + const getSelectedRecommendations = (category) => { + if (!category) { + return []; + } + let recommendations = category.recommendations?.filter(r => isSelected(r.id)) || []; + category.children?.forEach(c => recommendations = [...recommendations, ...getSelectedRecommendations(c)]); + return recommendations; + }; + + return threatsCategory?.data ? getSelectedRecommendations(threatsCategory.data) : []; + }, [threatsCategory, isSelected]); + + const selectedRecommendationsCount = selectedRecommendations.length; + + const addThreats = useCallback(() => { + onClose(selectedRecommendations); + }, [onClose, selectedRecommendations]); + + if (threatsCategory.isLoading) { + return
Loading...
; + } + + if (threatsCategory.isError) { + return {threatsCategory.error.message}; + } + + return ( + <> +
+ +
+
Select threats
+ {!threatsCategory.data ? ( +

There are no threats

+ ) : ( + <> + + + + + + + )} + + ); +}; + +const SelectThreatsActionBar = ({ onSave, selectedCount }) => ( +
+ + Selected threats {selectedCount} +
+); + +export default SelectThreats; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/Threat.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/Threat.js new file mode 100644 index 0000000..01c2aa5 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/Threat.js @@ -0,0 +1,77 @@ +import React, { useState, useEffect } from 'react'; +import { Button, FormGroup, Label, Input } from 'reactstrap'; +import { FiArrowLeft, FiCheck } from "react-icons/fi"; +import MarkdownEditor from './MarkdownEditor'; +import ThreatRecommendations from '../threat-recommendations/ThreatRecommendations'; + +const Threat = ({ threat, onClose, resources }) => { + + const [threatLocal, setThreatLocal] = useState(threat); + const [recommendations, setRecommendations] = useState(threat?.recommendations || []); + const [saveButtonDisabled, setSaveButtonDisabled] = useState(false); + + const onModelPropertyChange = (e) => { + const { name, value } = e.target; + + const isNumericField = name === 'status' || name === 'risk'; + const typedValue = isNumericField ? parseInt(value, 10) : value; + + setThreatLocal(prev => ({ ...prev, [name]: typedValue })); + } + + useEffect(() => { + const isDisabled = !threatLocal.title.trim() || !threatLocal.description.trim(); + setSaveButtonDisabled(isDisabled); + }, [threatLocal.title, threatLocal.description]); + + useEffect(() => { + setThreatLocal(prev => ({ ...prev, recommendations })); + }, [recommendations]); + + return ( + <> +
+ +
+
{threat && threat.id ? "Update threat" : "New threat"}
+ + + + + onModelPropertyChange({ target: { name: "description", value: value } })} + /> + + + + + + + + + + + + + + + + + + + + +
Recommendations
+ +
+ + + + + ); +}; + +export default Threat; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/ThreatRisk.tsx b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/ThreatRisk.tsx new file mode 100644 index 0000000..118455e --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/ThreatRisk.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +const getRiskColor = (risk: number): string => { + switch (risk) { + case 1: // Critical + return '#a4262c'; + case 2: // High + return '#db7500'; + case 3: // Medium + return '#ffcb12'; + case 4: // Low + return '#0078d4'; + default: + return '#a19f9d'; + } +}; + +const getRiskText = (risk: number): string => { + switch (risk) { + case 1: + return 'Critical'; + case 2: + return 'High'; + case 3: + return 'Medium'; + case 4: + return 'Low'; + default: + return 'Not evaluated'; + } +} + +interface ThreatRiskProp { + risk: number; +} + +const ThreatRisk: React.FC = ({ risk }) => ( +
+ + {getRiskText(risk)} +
+); + +export default ThreatRisk; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/ThreatStatus.tsx b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/ThreatStatus.tsx new file mode 100644 index 0000000..b3b6bb8 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/ThreatStatus.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +const getStatusColor = (status: number): string => { + switch (status) { + case 1: // Not mitigated + return '#a4262c'; + case 2: // Partially mitigated + return '#db7500'; + case 3: // Mitigated + return 'green'; + default: + return '#a19f9d'; + } +}; + +const getStatusText = (status: number): string => { + switch (status) { + case 1: + return 'Not mitigated'; + case 2: + return 'Partially mitigated'; + case 3: + return 'Mitigated'; + default: + return 'Not evaluated'; + } +} + +interface ThreatStatusProps { + status: number; +} + +const ThreatStatus: React.FC = ({ status }) => ( +
+ + {getStatusText(status)} +
+); + +export default ThreatStatus; diff --git a/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/Threats.js b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/Threats.js new file mode 100644 index 0000000..5f3e391 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/components/threat-model/threats/Threats.js @@ -0,0 +1,226 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { Button, Table, Dropdown, DropdownToggle, DropdownMenu, DropdownItem, Modal, ModalBody } from 'reactstrap'; +import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; +import { FiPlus, FiX } from "react-icons/fi"; +import { RxDragHandleDots2 } from "react-icons/rx"; +import { fetchRecommendations } from '../../../fetchers/resources'; +import ThreatStatus from './ThreatStatus'; +import ThreatRisk from './ThreatRisk'; +import SelectThreats from './SelectThreats'; +import Threat from './Threat'; + +const Threats = ({ threats, setThreats, addResourcesRecommendations, resources }) => { + + const [addThreatMenuOpen, setAddThreatMenuOpen] = useState(false); + const [selectThreatsModalOpen, setSelectThreatsModalOpen] = useState(false); + const [threatModalOpen, setThreatModalOpen] = useState(false); + const [selectedThreat, setSelectedThreat] = useState(null); + //const [resourcesRecommendations, setResourcesRecommendations] = useState([]); + + const sortedThreats = useMemo(() => { + return [...threats].sort((a, b) => a.orderIndex - b.orderIndex); + }, [threats]); + + //useEffect(() => { + // const fetchAndSetRecommendations = async () => { + // const recommendations = await fetchRecommendations(resources); + // console.log(recommendations); + // setResourcesRecommendations(recommendations); + // } + // fetchAndSetRecommendations(); + //}, [resources]); + + const defaultDescription = `**Principle:** Confidentiality, Integrity, and Availability +**Affected Asset:** All services +**Threat:** [threat description]. + +**Mitigation:** + +[mitigation description] + +1. Mitigation step 1. +2. Mitigation step 2. +3. Mitigation step 3. +`; + + const defaultThreat = { + title: '', + description: defaultDescription, + status: 0, // Not evaluated + risk: 0, // Not evaluated + orderIndex: getNextOrderIndex(), + recommendations: [] + }; + + function getNextOrderIndex() { + return threats.length > 0 ? Math.max(...threats.map(t => t.orderIndex)) + 1 : 1; + } + + const addOrUpdateThreat = (threat) => { + setSelectedThreat(threat); + setThreatModalOpen(true); + } + + const deleteThreat = (e, threat) => { + e.stopPropagation(); + if (!window.confirm(`Do you want to delete threat '${threat.title}' ?`)) { + return; + } + setThreats(prev => prev.filter(t => t.id !== threat.id) + .map(t => t.orderIndex > threat.orderIndex ? { ...t, orderIndex: t.orderIndex - 1 } : t)); + } + + const addThreatsFromCatalog = () => { + setSelectThreatsModalOpen(true); + } + + const toggleAddThreatMenu = () => setAddThreatMenuOpen((prevState) => !prevState); + + const getRecommendationsForResources = (threat) => { + const recommendations = []; + if (!addResourcesRecommendations || resources.length === 0) { + return recommendations; + } + //console.log(threat); + return recommendations; + } + + const getRecommendations = (threat) => { + const recommendations = getRecommendationsForResources(threat); + return recommendations; + } + + const toggleSelectThreatsModal = (selectedThreats) => { + if (selectedThreats) { + let orderIndex = getNextOrderIndex(); + const threatsToAdd = []; + selectedThreats.filter(t => !threats.find(threat => threat.id === t.id)).forEach(t => { + threatsToAdd.push({ + id: t.id, + title: t.title, + description: t.description, + status: 0, // Not evaluated + risk: 0, // Not evaluated + orderIndex: orderIndex, + recommendations: getRecommendations(t) + }); + orderIndex++; + }); + setThreats(prev => [...prev, ...threatsToAdd]); + } + setSelectThreatsModalOpen(prev => !prev); + } + + const toggleThreatModal = (threat) => { + if (threat) { + if (threat.id) { + setThreats(prev => { + const index = prev.findIndex(t => t.id === threat.id); + prev[index] = threat; + return [...prev]; + }); + + setSelectedThreat(null); + } else { + threat.id = Math.floor(Date.now() / 1000).toString(); + console.log(threat.id); + setThreats(prev => [...prev, threat]); + } + } + setThreatModalOpen(prev => !prev); + } + + const onDragEnd = (result) => { + if (!result.destination || result.destination.index === result.source.index) { + return; + } + + const threatMovedForward = result.destination.index > result.source.index; + const newOrderIndex = result.destination.index + 1; + + setThreats(prev => prev.map(t => { + if (t.id === result.draggableId) { + return { ...t, orderIndex: newOrderIndex }; + } else if (threatMovedForward) { + if (t.orderIndex > result.source.index + 1 && t.orderIndex <= newOrderIndex) { + return { ...t, orderIndex: t.orderIndex - 1 }; + } + } else { + if (t.orderIndex >= newOrderIndex && t.orderIndex < result.source.index + 1) { + return { ...t, orderIndex: t.orderIndex + 1 }; + } + } + return t; + })); + }; + + return ( + <> +
+ + Add threat   + + addOrUpdateThreat(defaultThreat)}>New threat + From catalog + + +
+ {sortedThreats.length > 0 ? ( + + + {(provided) => ( + + + + + + + + + + + + {sortedThreats.map((threat, index) => ( + + {(provided, snapshot) => ( + addOrUpdateThreat(threat)} + className={`cursor-pointer align-middle ${snapshot.isDragging ? "dragging" : ""}`} + > + + + + + + + )} + + ))} + {provided.placeholder} + +
TitleStatusRisk
{threat.orderIndex} +
+ +
+
+ )} +
+
+ ) : null } + + + + + + + + + + + + ); +}; + +export default Threats; diff --git a/src/Crisp.Ui/ClientApp/src/custom.css b/src/Crisp.Ui/ClientApp/src/custom.css index a1f9798..8efb95f 100644 --- a/src/Crisp.Ui/ClientApp/src/custom.css +++ b/src/Crisp.Ui/ClientApp/src/custom.css @@ -1,12 +1,22 @@ /* Provide sufficient contrast against white background */ -a { +a, .btn-link { color: #0366d6; + text-decoration: none; +} + +a:hover, .btn-link:hover { + color: #0366d6; + text-decoration: underline; } .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; } +.btn-link { + padding: 0; +} + code { color: #E01A76; } @@ -18,10 +28,24 @@ code { } svg { - margin-top: -4px; + margin-top: -4px; +} + +.cursor-pointer { + cursor: pointer; } @media (prefers-color-scheme: dark) { + a, .btn-link { + color: #2899f5; + text-decoration: none; + } + + a:hover, .btn-link:hover { + color: #3aa0f3; + text-decoration: underline; + } + .table { --bs-table-hover-color: white; --bs-table-hover-bg: #111111; @@ -73,4 +97,38 @@ svg { color: #afafaf !important; border-color: #666666 !important; } + + input:focus, select:focus, textarea:focus { + background-color: #333333 !important; + color: #dddddd !important; + } + + .dropdown-menu { + background-color: #333333; + color: #afafaf; + border-color: #666666; + } + + .dropdown-item { + color: #afafaf; + } + + .disabled.dropdown-item { + color: #6f6f6f !important; + } + + .dropdown-item:hover { + color: white; + background-color: rgba(255, 255, 255, 0.15); + } + + .modal-content { + background-color: #1a1a1a; + } + + .modal-header, + .modal-body, + .modal-footer { + border-color: #454d55; + } } diff --git a/src/Crisp.Ui/ClientApp/src/fetchers/recommendations.js b/src/Crisp.Ui/ClientApp/src/fetchers/recommendations.js new file mode 100644 index 0000000..dca1299 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/fetchers/recommendations.js @@ -0,0 +1,8 @@ +export const fetchCatalogRecommendations = async () => { + const response = await fetch('api/categories'); + const result = await response.json(); + if (response.status !== 200) { + throw Error(result.detail); + } + return result; +} \ No newline at end of file diff --git a/src/Crisp.Ui/ClientApp/src/models/Threat.ts b/src/Crisp.Ui/ClientApp/src/models/Threat.ts new file mode 100644 index 0000000..0e54a19 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/models/Threat.ts @@ -0,0 +1,26 @@ +import { ThreatRecommendation } from "./ThreatRecommendation"; + +export enum ThreatStatus { + NotEvaluated, + NotMitigated, + PartiallyMitigated, + Mitigated +} + +export enum ThreatRisk { + NotEvaluated, + Critical, + High, + Medium, + Low +} +export interface Threat { + id: string; + title: string; + description: string; + status: ThreatStatus; + risk: ThreatRisk; + orderIndex: number; + recommendations?: ThreatRecommendation[]; + benchmarkIds?: string[]; +} diff --git a/src/Crisp.Ui/ClientApp/src/models/ThreatRecommendation.ts b/src/Crisp.Ui/ClientApp/src/models/ThreatRecommendation.ts new file mode 100644 index 0000000..5f95a52 --- /dev/null +++ b/src/Crisp.Ui/ClientApp/src/models/ThreatRecommendation.ts @@ -0,0 +1,6 @@ +export interface ThreatRecommendation { + id: string; + title: string; + description: string; + orderIndex: number; +} diff --git a/src/Crisp.Ui/ClientApp/tsconfig.json b/src/Crisp.Ui/ClientApp/tsconfig.json new file mode 100644 index 0000000..de205ab --- /dev/null +++ b/src/Crisp.Ui/ClientApp/tsconfig.json @@ -0,0 +1,111 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": [ "dom", "dom.iterable", "esnext" ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "jsx": "react-jsx", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "esnext", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + "removeComments": true, /* Disable emitting comments. */ + "noEmit": false, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": [ + "src/**/*" // Include all files in the src directory + ], + "exclude": [ + "node_modules", // Exclude the node_modules directory + "**/*.spec.ts", // Exclude test files + "**/*.test.ts" // Exclude test files + ] +} diff --git a/src/Crisp.Ui/Crisp.Ui.csproj b/src/Crisp.Ui/Crisp.Ui.csproj index e757841..3a9435c 100644 --- a/src/Crisp.Ui/Crisp.Ui.csproj +++ b/src/Crisp.Ui/Crisp.Ui.csproj @@ -27,10 +27,20 @@ + + + + + + + + + + diff --git a/src/Crisp.Ui/Handlers/CreateThreatModelHandler.cs b/src/Crisp.Ui/Handlers/CreateThreatModelHandler.cs index 4e3769a..bdde4fc 100644 --- a/src/Crisp.Ui/Handlers/CreateThreatModelHandler.cs +++ b/src/Crisp.Ui/Handlers/CreateThreatModelHandler.cs @@ -38,7 +38,7 @@ private static ThreatModel MapRequestToThreatModel(CreateThreatModelDto dto) null, dto.AddResourcesRecommendations, dto.DataflowAttributes.Select(MapDtoToDataflowAttribute).ToArray(), - dto.Threats.Select(MapDtoToRecommendation).ToArray(), + dto.Threats.Select(MapDtoToThreat).ToArray(), dto.Images?.ToDictionary(i => i.Key, i => i.Value), dto.Resources ); @@ -56,14 +56,28 @@ private static DataflowAttribute MapDtoToDataflowAttribute(DataflowAttributeDto ); } - private static Recommendation MapDtoToRecommendation(RecommendationDto dto) + private static Threat MapDtoToThreat(ThreatDto dto) { - return new Recommendation( + return new Threat( dto.Id, dto.Title, dto.Description, + dto.Status, + dto.Risk, + dto.OrderIndex, + dto.Recommendations?.Select(MapDtoToThreatRecommendation).ToArray(), dto.BenchmarkIds ); } + + private static ThreatRecommendation MapDtoToThreatRecommendation(ThreatRecommendationDto dto) + { + return new ThreatRecommendation( + dto.Id, + dto.Title, + dto.Description, + dto.OrderIndex + ); + } } } diff --git a/src/Crisp.Ui/Handlers/GetThreatModelsHandler.cs b/src/Crisp.Ui/Handlers/GetThreatModelsHandler.cs index a88dfd6..dd67601 100644 --- a/src/Crisp.Ui/Handlers/GetThreatModelsHandler.cs +++ b/src/Crisp.Ui/Handlers/GetThreatModelsHandler.cs @@ -23,7 +23,7 @@ public record ThreatModelDto( DateTime? UpdatedAt, bool AddResourcesRecommendations, IEnumerable DataflowAttributes, - IEnumerable Threats, + IEnumerable Threats, IEnumerable>? Images, IEnumerable? Resources ); @@ -68,7 +68,7 @@ public async Task Handle(GetThreatModelsRequest request, CancellationTo p.UpdatedAt, p.AddResourcesRecommendations, p.DataflowAttributes.Select(MapDataflowAttributeToDto).ToArray(), - p.Threats.Select(MapRecommendationToDto).ToArray(), + p.Threats.Select(MapThreatToDto).ToArray(), p.Images, p.Resources )).ToArray(); @@ -86,13 +86,27 @@ private static DataflowAttributeDto MapDataflowAttributeToDto(DataflowAttribute ); } - private static RecommendationDto MapRecommendationToDto(Recommendation recommendation) + private static ThreatDto MapThreatToDto(Threat threat) { - return new RecommendationDto( + return new ThreatDto( + threat.Id, + threat.Title, + threat.Description, + threat.Status, + threat.Risk, + threat.OrderIndex, + threat.Recommendations?.Select(MapThreatRecommendationToDto).ToArray(), + threat.BenchmarkIds + ); + } + + private static ThreatRecommendationDto MapThreatRecommendationToDto(ThreatRecommendation recommendation) + { + return new ThreatRecommendationDto( recommendation.Id, recommendation.Title, recommendation.Description, - recommendation.BenchmarkIds + recommendation.OrderIndex ); } } diff --git a/src/Crisp.Ui/Handlers/UpdateThreatModelHandler.cs b/src/Crisp.Ui/Handlers/UpdateThreatModelHandler.cs index 5f622bb..c1c8358 100644 --- a/src/Crisp.Ui/Handlers/UpdateThreatModelHandler.cs +++ b/src/Crisp.Ui/Handlers/UpdateThreatModelHandler.cs @@ -43,7 +43,7 @@ private static ThreatModel MapRequestToThreatModel(CreateThreatModelDto dto, Thr DateTime.Now, dto.AddResourcesRecommendations, dto.DataflowAttributes.Select(MapDtoToDataflowAttribute).ToArray(), - dto.Threats.Select(MapDtoToRecommendation).ToArray(), + dto.Threats.Select(MapDtoToThreat).ToArray(), dto.Images?.ToDictionary(i => i.Key, i => i.Value), dto.Resources ); @@ -61,14 +61,28 @@ private static DataflowAttribute MapDtoToDataflowAttribute(DataflowAttributeDto ); } - private static Recommendation MapDtoToRecommendation(RecommendationDto dto) + private static Threat MapDtoToThreat(ThreatDto dto) { - return new Recommendation( + return new Threat( dto.Id, dto.Title, dto.Description, + dto.Status, + dto.Risk, + dto.OrderIndex, + dto.Recommendations?.Select(MapDtoToThreatRecommendation).ToArray(), dto.BenchmarkIds ); } + + private static ThreatRecommendation MapDtoToThreatRecommendation(ThreatRecommendationDto dto) + { + return new ThreatRecommendation( + dto.Id, + dto.Title, + dto.Description, + dto.OrderIndex + ); + } } } diff --git a/src/Crisp.Ui/Requests/CreateThreatModelRequest.cs b/src/Crisp.Ui/Requests/CreateThreatModelRequest.cs index 457ab29..d4e1118 100644 --- a/src/Crisp.Ui/Requests/CreateThreatModelRequest.cs +++ b/src/Crisp.Ui/Requests/CreateThreatModelRequest.cs @@ -1,14 +1,33 @@ -using Crisp.Ui.Handlers; +using Crisp.Core.Models; +using Crisp.Ui.Handlers; using Microsoft.AspNetCore.Mvc; namespace Crisp.Ui.Requests; +public record ThreatRecommendationDto( + string Id, + string Title, + string Description, + int OrderIndex +); + +public record ThreatDto( + string Id, + string Title, + string Description, + ThreatStatus Status, + ThreatRisk Risk, + int OrderIndex, + IEnumerable? Recommendations, + IEnumerable? BenchmarkIds +); + public record CreateThreatModelDto( string ProjectName, string? Description, bool AddResourcesRecommendations, IEnumerable DataflowAttributes, - IEnumerable Threats, + IEnumerable Threats, IEnumerable>? Images, IEnumerable? Resources );