Skip to content

Commit

Permalink
Merge pull request #922 from DependencyTrack/port-team-name-in-API-su…
Browse files Browse the repository at this point in the history
…bmitted-audit-changes

Port : Include team name in audit trail for API-submitted audit changes
  • Loading branch information
nscuro authored Sep 24, 2024
2 parents 6abf01f + f822da6 commit c87faef
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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", ConfigPropertyAccessMode.READ_WRITE);

private final String groupName;
private final String propertyName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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<Team> teams = apiKey.getTeams();
List<String> teamNames = new ArrayList<>();
teams.forEach(team -> teamNames.add(team.getName()));
commenter = String.join(", ", teamNames);
}

boolean analysisStateChange = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down

0 comments on commit c87faef

Please sign in to comment.