diff --git a/activities/fellowship of the ring/gandalf.yaml b/activities/fellowship of the ring/gandalf.yaml index 7d78b14..9627d9a 100644 --- a/activities/fellowship of the ring/gandalf.yaml +++ b/activities/fellowship of the ring/gandalf.yaml @@ -5,21 +5,10 @@ settings: application: "gandalf" # e.g. from deployment team: "fellowship of the ring" -# TODO optional fields results in null values in the output is good? +includes: + - infrastructure/infrastructure.yaml activities: - Source Control Protection: - components: - - url: "https://test1.com" - date: 2023-01-01 - - date: 2022-05-01 - url: "https://test1.com" - - date: 2021-05-01 - url: "https://test1.com" - - date: 2023-12-19 - url: "https://test1.com" - - Conduction of simple threat modeling on technical level: components: - date: "2008-11-01" @@ -42,4 +31,6 @@ activities: links: - title: "testtitle" url: "https://test1.com" - + Reduction of the attack surface: + components: + - date: "2024-01-01" diff --git a/activities/infrastructure/infrastructure.yaml b/activities/infrastructure/infrastructure.yaml new file mode 100644 index 0000000..6af60b2 --- /dev/null +++ b/activities/infrastructure/infrastructure.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: application +settings: + desired level: Level 2 + application: "kube" + team: "ops" + +activities: + Source Control Protection: + components: + - url: "https://test1.com" + date: 2023-01-01 + - date: 2022-05-01 + url: "https://test1.com" + - date: 2021-05-01 + url: "https://test1.com" + - date: 2023-12-19 + url: "https://test1.com" diff --git a/activities/infrastructure/team.yaml b/activities/infrastructure/team.yaml new file mode 100644 index 0000000..87adf50 --- /dev/null +++ b/activities/infrastructure/team.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: team + +settings: + team: "ops" + +activities: + Security requirements: + components: + - url: "https://test1.com" + date: 2023-01-01 + - date: 2022-05-01 + url: "https://test1.com" + - date: 2021-05-01 + url: "https://test1.com" + - date: 2023-12-19 + url: "https://test1.com" + + Data privacy requirements: + components: + - date: 2022-05-01 + - date: 2021-05-01 + - date: 2023-12-19 + + Security champion: + components: + - firstname: "Max" + lastname: "Mustermann" + date: "2020-11-01" + + Security Training: + components: + - date: "2020-11-01" + people: 2 + hours: 5 + - date: "2023-11-01" + people: 2 + hours: 5 diff --git a/activities/two towers/sauron.yaml b/activities/two towers/sauron.yaml index 0609971..0faf974 100644 --- a/activities/two towers/sauron.yaml +++ b/activities/two towers/sauron.yaml @@ -7,21 +7,32 @@ settings: application: "sauron" # e.g. from deployment team: "two towers" +includes: + - infrastructure/infrastructure.yaml + activities: Source Control Protection: components: - url: "https://test1.com" date: 2023-01-01 - - date: 2022-10-01 + - date: 2022-05-01 url: "https://test1.com" - - date: 2000-01-01 + - date: 2021-05-01 url: "https://test1.com" - date: 2023-12-19 url: "https://test1.com" + + Defined build process: + components: + - url: "https://test1.com" + date: 2023-01-01 - date: 2022-05-01 - url: "https://test2.com" + url: "https://test1.com" + - date: 2021-05-01 + url: "https://test1.com" - date: 2023-12-19 - url: "https://test2.com" + url: "https://test1.com" + Conduction of simple threat modeling on technical level: components: - date: "2009-11-01" @@ -44,10 +55,7 @@ activities: links: - title: "testtitle" url: "https://test1.com" - Reduction of the attack surface: - components: - - date: "2023-01-01" # sast: # confirmation: # - confirmed by: "Max Mustermann" -# confirmed date: "2020-01-01" \ No newline at end of file +# confirmed date: "2020-01-01" diff --git a/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/ApplicationDirector.java b/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/ApplicationDirector.java index d0f533d..8e36aaf 100644 --- a/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/ApplicationDirector.java +++ b/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/ApplicationDirector.java @@ -80,7 +80,7 @@ private List getDeserializeSkeletons() throws IOException, Git private List getDeserializedApplications(List skeletonActivities) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, GitAPIException { List applications = new ArrayList<>(); - YamlApplicationNodes yamlApplicationNodes = new YamlApplicationNodes(); + YamlApplicationNodes yamlApplicationNodes = new YamlApplicationNodes(yamlScanner.getYamlApplicationFolderPath()); HashMap> teamActivities = new HashMap<>(); for (File yamlApplicationFilePath : yamlScanner.getApplicationYamls()) { logger.info("yamlApplicationFilePath: " + yamlApplicationFilePath.getPath()); diff --git a/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlApplicationNodes.java b/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlApplicationNodes.java index d6c4fe0..4c971d5 100644 --- a/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlApplicationNodes.java +++ b/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlApplicationNodes.java @@ -4,6 +4,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -12,10 +13,17 @@ public class YamlApplicationNodes { // Key: Team name protected HashMap> nodes; + protected String yamlBasePath = ""; + public YamlApplicationNodes() { this.nodes = new HashMap>(); } + public YamlApplicationNodes(String yamlBasePath) { + this.yamlBasePath = yamlBasePath; + this.nodes = new HashMap>(); + } + public void addJsonNode(JsonNode node) { JsonNode settings = node.get("settings"); String team = settings.get("team").asText(); @@ -32,6 +40,22 @@ public HashMap> getNodes() { return nodes; } + public ArrayList getNodes(String team, String kind) throws IOException { + ArrayList applicationNodes = new ArrayList(); + + for (JsonNode node : getNodesForTeam(team)) { + if (node.get("kind").asText().equals(kind)) { + if (yamlBasePath != null && !yamlBasePath.isEmpty()) { + YamlScannerIncludes yamlScannerIncludes = new YamlScannerIncludes(node, yamlBasePath); + node = yamlScannerIncludes.getNode(); + } + applicationNodes.add(node); + } + } + + return applicationNodes; + } + public ArrayList getNodesForTeam(String team) { ArrayList allNodes = new ArrayList(); @@ -44,15 +68,4 @@ public ArrayList getNodesForTeam(String team) { } return allNodes; } - - public ArrayList getNodes(String team, String kind) { - ArrayList applicationNodes = new ArrayList(); - - for (JsonNode node : getNodesForTeam(team)) { - if (node.get("kind").asText().equals(kind)) { - applicationNodes.add(node); - } - } - return applicationNodes; - } } diff --git a/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlScanner.java b/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlScanner.java index 92c628c..2d211ae 100644 --- a/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlScanner.java +++ b/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlScanner.java @@ -143,8 +143,8 @@ private String getYamlSkeletonFilePath() { return yamlSkeletonFilePath; } - private String getYamlApplicationFolderPath() { - logger.info("yamlApplicationFolderPath() " + yamlApplicationFolderPath); + public String getYamlApplicationFolderPath() { + logger.debug("yamlApplicationFolderPath() " + yamlApplicationFolderPath); if (isGit()) { return yamlGitTargetPath + "/" + yamlApplicationFolderPath; } diff --git a/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlScannerIncludes.java b/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlScannerIncludes.java new file mode 100644 index 0000000..975554a --- /dev/null +++ b/src/main/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlScannerIncludes.java @@ -0,0 +1,71 @@ +package org.owasp.dsomm.metricca.analyzer.deserialization; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import java.io.File; +import java.io.IOException; + +public class YamlScannerIncludes { + private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + private final String yamlBasePath; + private final JsonNode applicationJsonNode; + + public YamlScannerIncludes(JsonNode applicationJsonNode, String basePath) { + this.applicationJsonNode = applicationJsonNode; + this.yamlBasePath = basePath; + } + + public JsonNode getNode() throws IOException { + processIncludes(applicationJsonNode); + return applicationJsonNode; + } + + private void processIncludes(JsonNode node) throws IOException { + if (node.has("includes")) { + ArrayNode includesArray = (ArrayNode) node.get("includes"); + for (JsonNode includeNode : includesArray) { + if (includeNode.isTextual()) { + String includePath = includeNode.asText(); + File includeFile = new File(yamlBasePath + File.separator + includePath); + JsonNode includeContent = mapper.readTree(includeFile); + if (includeContent.has("includes")) { + processIncludes(includeContent); + } + mergeJsonNodes(node, includeContent); + } + } + ((ObjectNode) node).remove("includes"); + } + + + } + + private void mergeJsonNodes(JsonNode destinationNode, JsonNode sourceNode) { + if (destinationNode instanceof ObjectNode destObjectNode && sourceNode instanceof ObjectNode srcObjectNode) { + + // Merge fields from source node to destination node + srcObjectNode.fieldNames().forEachRemaining(fieldName -> { + JsonNode srcFieldValue = srcObjectNode.get(fieldName); + JsonNode destFieldValue = destObjectNode.get(fieldName); + + if (destFieldValue != null && destFieldValue.isObject() && srcFieldValue.isObject()) { + // Recursive deep merge for nested objects + mergeJsonNodes(destFieldValue, srcFieldValue); + } else { + // If the field exists in the destination node but not in the source node, or if the field is null in the destination + // and exists in the source node, or if the included YAML is overriding the including YAML, override the value in the destination + if (destFieldValue == null || (!destObjectNode.has(fieldName) && srcObjectNode.has(fieldName))) { + destObjectNode.set(fieldName, srcFieldValue); + } + } + }); + } else { + throw new IllegalArgumentException("Both nodes must be ObjectNodes"); + } + } + +} diff --git a/src/main/java/org/owasp/dsomm/metricca/analyzer/exception/ApplicationNotFoundException.java b/src/main/java/org/owasp/dsomm/metricca/analyzer/exception/ApplicationNotFoundException.java index 969b4b5..fd3c755 100644 --- a/src/main/java/org/owasp/dsomm/metricca/analyzer/exception/ApplicationNotFoundException.java +++ b/src/main/java/org/owasp/dsomm/metricca/analyzer/exception/ApplicationNotFoundException.java @@ -7,7 +7,7 @@ public ApplicationNotFoundException(String applicationName, String teamName) { this.teamName = teamName; } - private String teamName; + private final String teamName; private String applicationName; public String getApplicationName() { diff --git a/src/main/resources/application-debugging.properties b/src/main/resources/application-debugging.properties index a357351..512dd09 100644 --- a/src/main/resources/application-debugging.properties +++ b/src/main/resources/application-debugging.properties @@ -1,4 +1,4 @@ logging.level.root=INFO metricCA.resource.path=/app/resources -spring.config.import: ${metricCA.resource.path}/application-prod.properties +spring.config.import= ${metricCA.resource.path}/application-prod.properties logging.level.org.eclipse.jgit=DEBUG diff --git a/src/main/resources/application-heroku.properties b/src/main/resources/application-heroku.properties index beb8298..320f1de 100644 --- a/src/main/resources/application-heroku.properties +++ b/src/main/resources/application-heroku.properties @@ -1,2 +1,2 @@ metricCA.resource.path=/app/src/main/resources/ -spring.config.import: ${metricCA.resource.path}/application-prod.properties +spring.config.import= ${metricCA.resource.path}/application-prod.properties diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index db49300..b5fcf80 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -1,7 +1,7 @@ logging.level.org.owasp=${LOGGING_LEVEL_OWASP} metricCA.git.targetPath=/tmp/metricCA metricCA.resource.path=${RESOURCE_PATH} -spring.config.import: ${metricCA.resource.path}/application.properties +spring.config.import= ${metricCA.resource.path}/application.properties metricCA.application.path=${metricCA.git.targetPath}/activities metricCA.teams.path=${metricCA.git.targetPath}/teams.yaml # only internal http://metric-analyzer:8080 diff --git a/src/test/java/org/owasp/dsomm/metricca/analyzer/deserialization/DatePeriodWithSecurityRequirementsTest.java b/src/test/java/org/owasp/dsomm/metricca/analyzer/deserialization/DatePeriodWithSecurityRequirementsTest.java index acb3280..4e1b061 100644 --- a/src/test/java/org/owasp/dsomm/metricca/analyzer/deserialization/DatePeriodWithSecurityRequirementsTest.java +++ b/src/test/java/org/owasp/dsomm/metricca/analyzer/deserialization/DatePeriodWithSecurityRequirementsTest.java @@ -70,9 +70,9 @@ public void testShowEndDateWithEndDateToShowForSingleDate() throws Exception { SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd"); isoFormat.setTimeZone(TimeZone.getTimeZone("Etc/UTC")); - Date existingDate = isoFormat.parse("2000-01-01"); + Date existingDate = isoFormat.parse("2023-12-19"); DatePeriod endDateForExistingDate = activity.getThresholdDatePeriodMap().get("Level 1").getDatePeriodForDate(existingDate); assertNotNull(endDateForExistingDate); assertTrue(endDateForExistingDate.getShowEndDate()); } -} \ No newline at end of file +} diff --git a/src/test/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlApplicationNodesTest.java b/src/test/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlApplicationNodesTest.java index 1dcc9c0..46c6e30 100644 --- a/src/test/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlApplicationNodesTest.java +++ b/src/test/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlApplicationNodesTest.java @@ -11,6 +11,7 @@ import org.owasp.dsomm.metricca.analyzer.deserialization.YamlApplicationNodes; import org.springframework.test.util.ReflectionTestUtils; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -91,7 +92,7 @@ void shouldGetNodesForTeam() { } @Test - void testGetNodesForTeam() throws JsonProcessingException { + void testGetNodesForTeam() throws IOException { HashMap> nodes = new HashMap<>(); ArrayList jsonNodes = new ArrayList<>(); jsonNodes.add(objectMapper.readTree(jsonNodeString)); diff --git a/src/test/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlScannerIncludesTest.java b/src/test/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlScannerIncludesTest.java new file mode 100644 index 0000000..e86333a --- /dev/null +++ b/src/test/java/org/owasp/dsomm/metricca/analyzer/deserialization/YamlScannerIncludesTest.java @@ -0,0 +1,72 @@ +package org.owasp.dsomm.metricca.analyzer.deserialization; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import java.nio.file.Paths; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class YamlScannerIncludesTest { + + @Test + void testSingleNodeInclude() throws Exception { + // Arrange + String basePath = Paths.get("src", "test", "resources", "include").toString(); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode applicationJsonNode = objectMapper.readTree("{ \"key\": \"value\", \"includes\": [\"include1.yaml\"] }"); + JsonNode expected = objectMapper.readTree("{ \"key\": \"value\", \"includedKey\": \"includedValue\" }"); + + // Act + YamlScannerIncludes yamlScannerIncludes = new YamlScannerIncludes(applicationJsonNode, basePath); + JsonNode result = yamlScannerIncludes.getNode(); + + // Assert + assertEquals(expected, result); + } + + @Test + void testMultipleNodeInclude() throws Exception { + // Arrange + String basePath = Paths.get("src", "test", "resources", "include").toString(); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode applicationJsonNode = objectMapper.readTree("{ \"key\": \"value\", \"includes\": [\"include2.yaml\"] }"); + JsonNode expected = objectMapper.readTree("{ \"key\": \"value\", \"includedKey2\": \"includedValue2\" }"); + + // Act + YamlScannerIncludes yamlScannerIncludes = new YamlScannerIncludes(applicationJsonNode, basePath); + JsonNode result = yamlScannerIncludes.getNode(); + + // Assert + assertEquals(expected, result); + } + + @Test + void testNestedIncludes() throws Exception { + // Arrange + String basePath = Paths.get("src", "test", "resources", "include").toString(); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode applicationJsonNode = objectMapper.readTree("{ \"key\": \"value\", \"includes\": [\"nestedInclude.yaml\"] }"); + JsonNode expected = objectMapper.readTree("{ \"key\": \"value\", \"includedKey1\": \"includedValue1\", \"nestedKey2\": \"nestedValue2\"}"); + // Act + YamlScannerIncludes yamlScannerIncludes = new YamlScannerIncludes(applicationJsonNode, basePath); + JsonNode result = yamlScannerIncludes.getNode(); + + // Assert + assertEquals(expected, result); + } + + @Test + void testIncludesWithActivities() throws Exception { + // Arrange + String basePath = Paths.get("src", "test", "resources", "include").toString(); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode applicationJsonNode = objectMapper.readTree("{ \"activities\": {\"key\": \"value\"}, \"includes\": [\"includeWithActivities.yaml\"]}"); + JsonNode expected = objectMapper.readTree("{ \"activities\": {\"key\": \"value\", \"includedKey\": \"includedValue\"}}"); + // Act + YamlScannerIncludes yamlScannerIncludes = new YamlScannerIncludes(applicationJsonNode, basePath); + JsonNode result = yamlScannerIncludes.getNode(); + + // Assert + assertEquals(expected, result); + } +} diff --git a/src/test/resources/include/application.yaml b/src/test/resources/include/application.yaml new file mode 100644 index 0000000..d328efb --- /dev/null +++ b/src/test/resources/include/application.yaml @@ -0,0 +1,5 @@ +includes: + - include1.yaml + - include2.yaml + - nestedInclude.yaml +key: value diff --git a/src/test/resources/include/include1.yaml b/src/test/resources/include/include1.yaml new file mode 100644 index 0000000..639bafb --- /dev/null +++ b/src/test/resources/include/include1.yaml @@ -0,0 +1 @@ +includedKey: includedValue diff --git a/src/test/resources/include/include2.yaml b/src/test/resources/include/include2.yaml new file mode 100644 index 0000000..b494136 --- /dev/null +++ b/src/test/resources/include/include2.yaml @@ -0,0 +1 @@ +includedKey2: includedValue2 diff --git a/src/test/resources/include/includeWithActivities.yaml b/src/test/resources/include/includeWithActivities.yaml new file mode 100644 index 0000000..743ea90 --- /dev/null +++ b/src/test/resources/include/includeWithActivities.yaml @@ -0,0 +1,2 @@ +activities: + includedKey: includedValue diff --git a/src/test/resources/include/nestedInclude.yaml b/src/test/resources/include/nestedInclude.yaml new file mode 100644 index 0000000..c8b6767 --- /dev/null +++ b/src/test/resources/include/nestedInclude.yaml @@ -0,0 +1,4 @@ +key: toBeOverriden +includedKey1: includedValue1 +includes: + - nestedInclude2.yaml diff --git a/src/test/resources/include/nestedInclude2.yaml b/src/test/resources/include/nestedInclude2.yaml new file mode 100644 index 0000000..d4e29eb --- /dev/null +++ b/src/test/resources/include/nestedInclude2.yaml @@ -0,0 +1 @@ +nestedKey2: nestedValue2