From ac79f0f79a9d030a84e5f212b958ce5580a0d21c Mon Sep 17 00:00:00 2001 From: Sahiba Mittal Date: Mon, 23 Sep 2024 16:25:30 +0100 Subject: [PATCH 1/3] Include team name in audit trail for API-submitted audit changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Thomas Schauer-Köckeis <75749982+Gepardgame@users.noreply.github.com> --- .../resources/v1/AnalysisResource.java | 17 ++- .../resources/v1/AnalysisResourceTest.java | 107 ++++++++++++++---- 2 files changed, 97 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java b/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java index 7e295c10c..f2c588a0f 100644 --- a/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/AnalysisResource.java @@ -20,9 +20,8 @@ import alpine.common.validation.RegexSequence; import alpine.common.validation.ValidationTask; -import alpine.model.LdapUser; -import alpine.model.ManagedUser; -import alpine.model.OidcUser; +import alpine.model.ApiKey; +import alpine.model.Team; import alpine.model.UserPrincipal; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -58,6 +57,9 @@ import org.dependencytrack.util.AnalysisCommentUtil; import org.dependencytrack.util.NotificationUtil; +import java.util.ArrayList; +import java.util.List; + import static org.dependencytrack.util.AnalysisCommentFormatter.formatComment; /** @@ -169,8 +171,13 @@ public Response updateAnalysis(AnalysisRequest request) { } String commenter = null; - if (getPrincipal() instanceof LdapUser || getPrincipal() instanceof ManagedUser || getPrincipal() instanceof OidcUser) { - commenter = ((UserPrincipal) getPrincipal()).getUsername(); + if (getPrincipal() instanceof UserPrincipal principal) { + commenter = principal.getUsername(); + } else if (getPrincipal() instanceof ApiKey apiKey) { + List teams = apiKey.getTeams(); + List teamNames = new ArrayList<>(); + teams.forEach(team -> teamNames.add(team.getName())); + commenter = String.join(", ", teamNames); } boolean analysisStateChange = false; diff --git a/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java index e718a1ef3..112e18421 100644 --- a/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/AnalysisResourceTest.java @@ -18,9 +18,17 @@ */ package org.dependencytrack.resources.v1; +import alpine.model.ManagedUser; +import alpine.server.auth.JsonWebToken; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import alpine.server.filters.AuthorizationFilter; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import net.jcip.annotations.NotThreadSafe; import org.apache.http.HttpStatus; import org.dependencytrack.JerseyTestRule; @@ -47,12 +55,6 @@ import org.junit.ClassRule; import org.junit.Test; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.time.Duration; import java.util.List; import java.util.UUID; @@ -326,10 +328,71 @@ public void updateAnalysisCreateNewTest() throws Exception { assertThat(responseJson.getJsonArray("analysisComments")).hasSize(2); assertThat(responseJson.getJsonArray("analysisComments").getJsonObject(0)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Analysis: NOT_SET → NOT_AFFECTED")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); + assertThat(responseJson.getJsonArray("analysisComments").getJsonObject(1)) + .hasFieldOrPropertyWithValue("comment", Json.createValue("Analysis comment here")) + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); + assertThat(responseJson.getBoolean("isSuppressed")).isTrue(); + + assertConditionWithTimeout(() -> kafkaMockProducer.history().size() == 2, Duration.ofSeconds(5)); + final Notification projectNotification = deserializeValue(KafkaTopics.NOTIFICATION_PROJECT_CREATED, kafkaMockProducer.history().get(0)); + assertThat(projectNotification).isNotNull(); + final Notification notification = deserializeValue(KafkaTopics.NOTIFICATION_PROJECT_AUDIT_CHANGE, kafkaMockProducer.history().get(1)); + assertThat(notification).isNotNull(); + assertThat(notification.getScope()).isEqualTo(SCOPE_PORTFOLIO); + assertThat(notification.getGroup()).isEqualTo(GROUP_PROJECT_AUDIT_CHANGE); + assertThat(notification.getLevel()).isEqualTo(LEVEL_INFORMATIONAL); + assertThat(notification.getTitle()).isEqualTo(NotificationUtil.generateNotificationTitle(NotificationConstants.Title.ANALYSIS_DECISION_NOT_AFFECTED, project)); + assertThat(notification.getContent()).isEqualTo("An analysis decision was made to a finding affecting a project"); + } + + @Test + public void updateAnalysisCreateNewWithUserTest() throws Exception { + initializeWithPermissions(Permissions.VULNERABILITY_ANALYSIS); + + ManagedUser testUser = qm.createManagedUser("testuser", TEST_USER_PASSWORD_HASH); + String jwt = new JsonWebToken().createToken(testUser); + qm.addUserToTeam(testUser, team); + + final Project project = qm.createProject("Acme Example", null, "1.0", null, null, null, true, false); + + var component = new Component(); + component.setProject(project); + component.setName("Acme Component"); + component.setVersion("1.0"); + component = qm.createComponent(component, false); + + var vulnerability = new Vulnerability(); + vulnerability.setVulnId("INT-001"); + vulnerability.setSource(Vulnerability.Source.INTERNAL); + vulnerability.setSeverity(Severity.HIGH); + vulnerability.setComponents(List.of(component)); + vulnerability = qm.createVulnerability(vulnerability, false); + + final var analysisRequest = new AnalysisRequest(project.getUuid().toString(), component.getUuid().toString(), + vulnerability.getUuid().toString(), AnalysisState.NOT_AFFECTED, AnalysisJustification.CODE_NOT_REACHABLE, + AnalysisResponse.WILL_NOT_FIX, "Analysis details here", "Analysis comment here", true); + + final Response response = jersey.target(V1_ANALYSIS) + .request() + .header("Authorization", "Bearer " + jwt) + .put(Entity.entity(analysisRequest, MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + assertThat(response.getHeaderString(TOTAL_COUNT_HEADER)).isNull(); + + final JsonObject responseJson = parseJsonObject(response); + assertThat(responseJson).isNotNull(); + assertThat(responseJson.getString("analysisState")).isEqualTo(AnalysisState.NOT_AFFECTED.name()); + assertThat(responseJson.getString("analysisJustification")).isEqualTo(AnalysisJustification.CODE_NOT_REACHABLE.name()); + assertThat(responseJson.getString("analysisResponse")).isEqualTo(AnalysisResponse.WILL_NOT_FIX.name()); + assertThat(responseJson.getString("analysisDetails")).isEqualTo("Analysis details here"); + assertThat(responseJson.getJsonArray("analysisComments")).hasSize(2); + assertThat(responseJson.getJsonArray("analysisComments").getJsonObject(0)) + .hasFieldOrPropertyWithValue("comment", Json.createValue("Analysis: NOT_SET → NOT_AFFECTED")) + .hasFieldOrPropertyWithValue("commenter", Json.createValue("testuser")); assertThat(responseJson.getJsonArray("analysisComments").getJsonObject(1)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Analysis comment here")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("testuser")); assertThat(responseJson.getBoolean("isSuppressed")).isTrue(); assertConditionWithTimeout(() -> kafkaMockProducer.history().size() == 2, Duration.ofSeconds(5)); @@ -442,22 +505,22 @@ public void updateAnalysisUpdateExistingTest() throws Exception { .hasFieldOrPropertyWithValue("commenter", Json.createValue("Jane Doe")); assertThat(analysisComments.getJsonObject(1)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Analysis: NOT_AFFECTED → EXPLOITABLE")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(analysisComments.getJsonObject(2)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Justification: CODE_NOT_REACHABLE → NOT_SET")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(analysisComments.getJsonObject(3)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Vendor Response: WILL_NOT_FIX → UPDATE")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(analysisComments.getJsonObject(4)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Details: New analysis details here")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(analysisComments.getJsonObject(5)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Unsuppressed")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(analysisComments.getJsonObject(6)) .hasFieldOrPropertyWithValue("comment", Json.createValue("New analysis comment here")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(responseJson.getBoolean("isSuppressed")).isFalse(); assertConditionWithTimeout(() -> kafkaMockProducer.history().size() == 2, Duration.ofSeconds(5)); @@ -569,13 +632,13 @@ public void updateAnalysisUpdateExistingWithEmptyRequestTest() throws Exception .hasFieldOrPropertyWithValue("commenter", Json.createValue("Jane Doe")); assertThat(analysisComments.getJsonObject(1)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Analysis: NOT_AFFECTED → NOT_SET")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(analysisComments.getJsonObject(2)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Justification: CODE_NOT_REACHABLE → NOT_SET")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(analysisComments.getJsonObject(3)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Vendor Response: WILL_NOT_FIX → NOT_SET")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertConditionWithTimeout(() -> kafkaMockProducer.history().size() == 2, Duration.ofSeconds(5)); final Notification projectNotification = deserializeValue(KafkaTopics.NOTIFICATION_PROJECT_CREATED, kafkaMockProducer.history().get(0)); @@ -732,19 +795,19 @@ public void updateAnalysisIssue1409Test() throws InterruptedException { assertThat(analysisComments).hasSize(5); assertThat(analysisComments.getJsonObject(0)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Analysis: IN_TRIAGE → NOT_AFFECTED")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(analysisComments.getJsonObject(1)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Justification: NOT_SET → PROTECTED_BY_MITIGATING_CONTROL")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(analysisComments.getJsonObject(2)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Vendor Response: NOT_SET → UPDATE")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(analysisComments.getJsonObject(3)) .hasFieldOrPropertyWithValue("comment", Json.createValue("Details: New analysis details here")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(analysisComments.getJsonObject(4)) .hasFieldOrPropertyWithValue("comment", Json.createValue("New analysis comment here")) - .doesNotContainKey("commenter"); // Not set when authenticating via API key + .hasFieldOrPropertyWithValue("commenter", Json.createValue("Test Users")); assertThat(responseJson.getBoolean("isSuppressed")).isFalse(); assertConditionWithTimeout(() -> kafkaMockProducer.history().size() == 2, Duration.ofSeconds(5)); From 4c6c9932c581b54f95af0d1bfeb0d1557bb29280 Mon Sep 17 00:00:00 2001 From: Sahiba Mittal Date: Mon, 23 Sep 2024 16:31:03 +0100 Subject: [PATCH 2/3] Feat/systemwide language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Thomas Schauer-Köckeis <75749982+Gepardgame@users.noreply.github.com> --- .../org/dependencytrack/model/ConfigPropertyConstants.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index c8b77501c..6bb6d0b37 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -111,7 +111,8 @@ public enum ConfigPropertyConstants { CUSTOM_RISK_SCORE_LOW("risk-score", "weight.low", "1", PropertyType.INTEGER, "Low severity vulnerability weight (between 1-10)", ConfigPropertyAccessMode.READ_WRITE), CUSTOM_RISK_SCORE_UNASSIGNED("risk-score", "weight.unassigned", "5", PropertyType.INTEGER, "Unassigned severity vulnerability weight (between 1-10)", ConfigPropertyAccessMode.READ_WRITE), WELCOME_MESSAGE("general", "welcome.message.html", "%20%3Chtml%3E%3Ch1%3EYour%20Welcome%20Message%3C%2Fh1%3E%3C%2Fhtml%3E", PropertyType.STRING, "Custom HTML Code that is displayed before login", ConfigPropertyAccessMode.READ_WRITE, true), - IS_WELCOME_MESSAGE("general", "welcome.message.enabled", "false", PropertyType.BOOLEAN, "Bool that says wheter to show the welcome message or not", ConfigPropertyAccessMode.READ_WRITE, true); + IS_WELCOME_MESSAGE("general", "welcome.message.enabled", "false", PropertyType.BOOLEAN, "Bool that says wheter to show the welcome message or not", ConfigPropertyAccessMode.READ_WRITE, true), + DEFAULT_LANGUAGE("general", "default.locale", null, PropertyType.STRING, "Determine the default Language to use", true); private final String groupName; private final String propertyName; From f822da6edf1e71b7f84a1c438120a1aa650a0456 Mon Sep 17 00:00:00 2001 From: Sahiba Mittal Date: Mon, 23 Sep 2024 16:33:32 +0100 Subject: [PATCH 3/3] Update ConfigPropertyConstants.java --- .../java/org/dependencytrack/model/ConfigPropertyConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 6bb6d0b37..ca74fea14 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -112,7 +112,7 @@ public enum ConfigPropertyConstants { CUSTOM_RISK_SCORE_UNASSIGNED("risk-score", "weight.unassigned", "5", PropertyType.INTEGER, "Unassigned severity vulnerability weight (between 1-10)", ConfigPropertyAccessMode.READ_WRITE), WELCOME_MESSAGE("general", "welcome.message.html", "%20%3Chtml%3E%3Ch1%3EYour%20Welcome%20Message%3C%2Fh1%3E%3C%2Fhtml%3E", PropertyType.STRING, "Custom HTML Code that is displayed before login", ConfigPropertyAccessMode.READ_WRITE, true), IS_WELCOME_MESSAGE("general", "welcome.message.enabled", "false", PropertyType.BOOLEAN, "Bool that says wheter to show the welcome message or not", ConfigPropertyAccessMode.READ_WRITE, true), - DEFAULT_LANGUAGE("general", "default.locale", null, PropertyType.STRING, "Determine the default Language to use", true); + DEFAULT_LANGUAGE("general", "default.locale", null, PropertyType.STRING, "Determine the default Language to use", ConfigPropertyAccessMode.READ_WRITE); private final String groupName; private final String propertyName;