Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port: Webhook alert token and new user alerts #742

Merged
merged 2 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@
import org.dependencytrack.proto.notification.v1.PolicyViolationSubject;
import org.dependencytrack.proto.notification.v1.Project;
import org.dependencytrack.proto.notification.v1.ProjectVulnAnalysisCompleteSubject;
import org.dependencytrack.proto.notification.v1.UserSubject;
import org.dependencytrack.proto.notification.v1.VexConsumedOrProcessedSubject;
import org.dependencytrack.proto.notification.v1.VulnerabilityAnalysisDecisionChangeSubject;
import org.dependencytrack.proto.repometaanalysis.v1.AnalysisCommand;
import org.dependencytrack.proto.vulnanalysis.v1.Component;
import org.dependencytrack.proto.vulnanalysis.v1.ScanCommand;
import org.dependencytrack.proto.vulnanalysis.v1.ScanKey;

Expand All @@ -55,7 +57,7 @@
import static org.apache.commons.lang3.ObjectUtils.requireNonEmpty;

/**
* Utility class to convert {@link alpine.event.framework.Event}s and {@link alpine.notification.Notification}s
* Utility class to convert {@link Event}s and {@link alpine.notification.Notification}s
* to {@link KafkaEvent}s.
*/
public final class KafkaEventConverter {
Expand Down Expand Up @@ -103,7 +105,7 @@ private KafkaEventConverter() {
}

static KafkaEvent<ScanKey, ScanCommand> convert(final ComponentVulnerabilityAnalysisEvent event) {
final var componentBuilder = org.dependencytrack.proto.vulnanalysis.v1.Component.newBuilder()
final var componentBuilder = Component.newBuilder()
.setUuid(event.uuid().toString());
Optional.ofNullable(event.cpe()).ifPresent(componentBuilder::setCpe);
Optional.ofNullable(event.purl()).ifPresent(componentBuilder::setPurl);
Expand Down Expand Up @@ -181,6 +183,7 @@ private static Topic<String, Notification> extractDestinationTopic(final Notific
case GROUP_PROJECT_VULN_ANALYSIS_COMPLETE -> KafkaTopics.NOTIFICATION_PROJECT_VULN_ANALYSIS_COMPLETE;
case GROUP_REPOSITORY -> KafkaTopics.NOTIFICATION_REPOSITORY;
case GROUP_VEX_CONSUMED, GROUP_VEX_PROCESSED -> KafkaTopics.NOTIFICATION_VEX;
case GROUP_USER_CREATED, GROUP_USER_DELETED -> KafkaTopics.NOTIFICATION_USER;
case GROUP_UNSPECIFIED, UNRECOGNIZED -> throw new IllegalArgumentException("""
Unable to determine destination topic because the notification does not \
specify a notification group: %s""".formatted(notification.getGroup()));
Expand Down Expand Up @@ -245,6 +248,11 @@ private static String extractEventKey(final Notification notification) throws In
final var subject = notification.getSubject().unpack(VexConsumedOrProcessedSubject.class);
yield requireNonEmpty(subject.getProject().getUuid());
}
case GROUP_USER_CREATED, GROUP_USER_DELETED -> {
requireSubjectOfTypeAnyOf(notification, List.of(UserSubject.class));
final var subject = notification.getSubject().unpack(UserSubject.class);
yield requireNonEmpty(subject.getUsername());
sahibamittal marked this conversation as resolved.
Show resolved Hide resolved
}
case GROUP_ANALYZER, GROUP_CONFIGURATION, GROUP_DATASOURCE_MIRRORING,
GROUP_FILE_SYSTEM, GROUP_INTEGRATION, GROUP_REPOSITORY -> null;
case GROUP_UNSPECIFIED, UNRECOGNIZED -> throw new IllegalArgumentException("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public final class KafkaTopics {
public static final Topic<String, Notification> NOTIFICATION_PROJECT_CREATED;
public static final Topic<String, Notification> NOTIFICATION_REPOSITORY;
public static final Topic<String, Notification> NOTIFICATION_VEX;
public static final Topic<String, Notification> NOTIFICATION_USER;
public static final Topic<String, String> VULNERABILITY_MIRROR_COMMAND;
public static final Topic<String, Bom> NEW_VULNERABILITY;
public static final Topic<String, AnalysisCommand> REPO_META_ANALYSIS_COMMAND;
Expand Down Expand Up @@ -82,6 +83,7 @@ public final class KafkaTopics {
VULN_ANALYSIS_RESULT_PROCESSED = new Topic<>("dtrack.vuln-analysis.result.processed", Serdes.String(), new KafkaProtobufSerde<>(ScanResult.parser()));
NOTIFICATION_PROJECT_VULN_ANALYSIS_COMPLETE = new Topic<>("dtrack.notification.project-vuln-analysis-complete", Serdes.String(), NOTIFICATION_SERDE);
NEW_EPSS = new Topic<>("dtrack.epss", Serdes.String(), new KafkaProtobufSerde<>(EpssItem.parser()));
NOTIFICATION_USER = new Topic<>("dtrack.notification.user", Serdes.String(), NOTIFICATION_SERDE);
}

public record Topic<K, V>(String name, Serde<K> keySerde, Serde<V> valueSerde) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ public static class Title {
public static final String VEX_CONSUMED = "Vulnerability Exploitability Exchange (VEX) Consumed";
public static final String VEX_PROCESSED = "Vulnerability Exploitability Exchange (VEX) Processed";
public static final String PROJECT_CREATED = "Project Added";

public static final String PROJECT_VULN_ANALYSIS_COMPLETE = "Project vulnerability analysis complete";
public static final String USER_CREATED = "User Created";
public static final String USER_DELETED = "User Deleted";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@ public enum NotificationGroup {
VEX_PROCESSED,
POLICY_VIOLATION,
PROJECT_CREATED,
PROJECT_VULN_ANALYSIS_COMPLETE
PROJECT_VULN_ANALYSIS_COMPLETE,
USER_CREATED,
USER_DELETED
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
import com.google.protobuf.Any;
import com.google.protobuf.ByteString;
import com.google.protobuf.Timestamp;
import org.dependencytrack.model.Analysis;
import org.dependencytrack.model.Bom;
import org.dependencytrack.model.Cwe;
import org.dependencytrack.model.Tag;
import org.dependencytrack.model.ViolationAnalysis;
import org.dependencytrack.notification.NotificationGroup;
import org.dependencytrack.notification.NotificationScope;
import org.dependencytrack.notification.vo.AnalysisDecisionChange;
Expand Down Expand Up @@ -53,6 +56,7 @@
import org.dependencytrack.proto.notification.v1.PolicyViolationSubject;
import org.dependencytrack.proto.notification.v1.Project;
import org.dependencytrack.proto.notification.v1.Scope;
import org.dependencytrack.proto.notification.v1.UserSubject;
import org.dependencytrack.proto.notification.v1.VexConsumedOrProcessedSubject;
import org.dependencytrack.proto.notification.v1.Vulnerability;
import org.dependencytrack.proto.notification.v1.VulnerabilityAnalysis;
Expand Down Expand Up @@ -81,6 +85,8 @@
import static org.dependencytrack.proto.notification.v1.Group.GROUP_PROJECT_VULN_ANALYSIS_COMPLETE;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_REPOSITORY;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_UNSPECIFIED;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_USER_CREATED;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_USER_DELETED;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_VEX_CONSUMED;
import static org.dependencytrack.proto.notification.v1.Group.GROUP_VEX_PROCESSED;
import static org.dependencytrack.proto.notification.v1.Level.LEVEL_ERROR;
Expand Down Expand Up @@ -160,10 +166,12 @@ private static Group convertGroup(final String group) {
case POLICY_VIOLATION -> GROUP_POLICY_VIOLATION;
case PROJECT_CREATED -> GROUP_PROJECT_CREATED;
case PROJECT_VULN_ANALYSIS_COMPLETE -> GROUP_PROJECT_VULN_ANALYSIS_COMPLETE;
case USER_CREATED -> GROUP_USER_CREATED;
case USER_DELETED -> GROUP_USER_DELETED;
};
}

private static Optional<com.google.protobuf.Any> convert(final Object subject) {
private static Optional<Any> convert(final Object subject) {
if (subject instanceof final NewVulnerabilityIdentified nvi) {
return Optional.of(Any.pack(convert(nvi)));
} else if (subject instanceof final NewVulnerableDependency nvd) {
Expand All @@ -182,6 +190,8 @@ private static Optional<com.google.protobuf.Any> convert(final Object subject) {
return Optional.of(Any.pack(convert(pvi)));
} else if (subject instanceof final org.dependencytrack.model.Project p) {
return Optional.of(Any.pack(convert(p)));
} else if (subject instanceof final UserSubject p) {
return Optional.of(Any.pack(p));
}

return Optional.empty();
Expand Down Expand Up @@ -381,7 +391,7 @@ private static Policy convert(final org.dependencytrack.model.Policy policy) {
.build();
}

private static VulnerabilityAnalysis convert(final org.dependencytrack.model.Analysis analysis) {
private static VulnerabilityAnalysis convert(final Analysis analysis) {
final VulnerabilityAnalysis.Builder builder = VulnerabilityAnalysis.newBuilder()
.setComponent(convert(analysis.getComponent()))
.setProject(convert(analysis.getProject()))
Expand All @@ -393,7 +403,7 @@ private static VulnerabilityAnalysis convert(final org.dependencytrack.model.Ana
return builder.build();
}

private static PolicyViolationAnalysis convert(final org.dependencytrack.model.ViolationAnalysis analysis) {
private static PolicyViolationAnalysis convert(final ViolationAnalysis analysis) {
final PolicyViolationAnalysis.Builder builder = PolicyViolationAnalysis.newBuilder()
.setComponent(convert(analysis.getComponent()))
.setProject(convert(analysis.getComponent().getProject()))
Expand All @@ -405,7 +415,7 @@ private static PolicyViolationAnalysis convert(final org.dependencytrack.model.V
return builder.build();
}

private static Vulnerability.Cwe convert(final org.dependencytrack.model.Cwe cwe) {
private static Vulnerability.Cwe convert(final Cwe cwe) {
return Vulnerability.Cwe.newBuilder()
.setCweId(cwe.getCweId())
.setName(cwe.getName())
Expand Down
50 changes: 47 additions & 3 deletions src/main/java/org/dependencytrack/resources/v1/UserResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import alpine.model.Permission;
import alpine.model.Team;
import alpine.model.UserPrincipal;
import alpine.notification.Notification;
import alpine.notification.NotificationLevel;
import alpine.security.crypto.KeyManager;
import alpine.server.auth.AlpineAuthenticationException;
import alpine.server.auth.AuthenticationNotRequired;
Expand All @@ -44,8 +46,13 @@
import io.swagger.annotations.ResponseHeader;
import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.event.kafka.KafkaEventDispatcher;
import org.dependencytrack.model.IdentifiableObject;
import org.dependencytrack.notification.NotificationConstants;
import org.dependencytrack.notification.NotificationGroup;
import org.dependencytrack.notification.NotificationScope;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.proto.notification.v1.UserSubject;
import org.owasp.security.logging.SecurityMarkers;

import javax.ws.rs.Consumes;
Expand All @@ -61,6 +68,7 @@
import javax.ws.rs.core.Response;
import java.security.Principal;
import java.util.List;
import java.util.Optional;

/**
* JAX-RS resources for processing users.
Expand All @@ -74,6 +82,8 @@ public class UserResource extends AlpineResource {

private static final Logger LOGGER = Logger.getLogger(UserResource.class);

private final KafkaEventDispatcher eventDispatcher = new KafkaEventDispatcher();

@POST
@Path("login")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
Expand Down Expand Up @@ -383,6 +393,7 @@ public Response createLdapUser(LdapUser jsonUser) {
if (user == null) {
user = qm.createLdapUser(jsonUser.getUsername());
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "LDAP user created: " + jsonUser.getUsername());
dispatchUserCreatedNotification("LDAP user created", buildUserSubject(jsonUser.getUsername(), jsonUser.getEmail()));
return Response.status(Response.Status.CREATED).entity(user).build();
} else {
return Response.status(Response.Status.CONFLICT).entity("A user with the same username already exists. Cannot create new user.").build();
Expand All @@ -407,8 +418,10 @@ public Response deleteLdapUser(LdapUser jsonUser) {
try (QueryManager qm = new QueryManager()) {
final LdapUser user = qm.getLdapUser(jsonUser.getUsername());
if (user != null) {
final LdapUser detachedUser = qm.getPersistenceManager().detachCopy(user);
qm.delete(user);
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "LDAP user deleted: " + jsonUser.getUsername());
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "LDAP user deleted: " + detachedUser);
dispatchUserDeletedNotification("LDAP user deleted", buildUserSubject(detachedUser.getUsername(), detachedUser.getEmail()));
return Response.status(Response.Status.NO_CONTENT).build();
} else {
return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build();
Expand Down Expand Up @@ -456,6 +469,7 @@ public Response createManagedUser(ManagedUser jsonUser) {
String.valueOf(PasswordService.createHash(jsonUser.getNewPassword().toCharArray())),
jsonUser.isForcePasswordChange(), jsonUser.isNonExpiryPassword(), jsonUser.isSuspended());
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Managed user created: " + jsonUser.getUsername());
dispatchUserCreatedNotification("Managed user created", buildUserSubject(jsonUser.getUsername(), jsonUser.getEmail()));
return Response.status(Response.Status.CREATED).entity(user).build();
} else {
return Response.status(Response.Status.CONFLICT).entity("A user with the same username already exists. Cannot create new user.").build();
Expand Down Expand Up @@ -522,8 +536,10 @@ public Response deleteManagedUser(ManagedUser jsonUser) {
try (QueryManager qm = new QueryManager()) {
final ManagedUser user = qm.getManagedUser(jsonUser.getUsername());
if (user != null) {
final ManagedUser detachedUser = qm.getPersistenceManager().detachCopy(user);
qm.delete(user);
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Managed user deleted: " + jsonUser.getUsername());
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Managed user deleted: " + detachedUser);
dispatchUserDeletedNotification("Managed user deleted", buildUserSubject(detachedUser.getUsername(), detachedUser.getEmail()));
return Response.status(Response.Status.NO_CONTENT).build();
} else {
return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build();
Expand Down Expand Up @@ -555,6 +571,7 @@ public Response createOidcUser(final OidcUser jsonUser) {
if (user == null) {
user = qm.createOidcUser(jsonUser.getUsername());
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "OpenID Connect user created: " + jsonUser.getUsername());
dispatchUserCreatedNotification("OpenID Connect user created", buildUserSubject(jsonUser.getUsername(), jsonUser.getEmail()));
return Response.status(Response.Status.CREATED).entity(user).build();
} else {
return Response.status(Response.Status.CONFLICT).entity("A user with the same username already exists. Cannot create new user.").build();
Expand All @@ -579,8 +596,10 @@ public Response deleteOidcUser(final OidcUser jsonUser) {
try (QueryManager qm = new QueryManager()) {
final OidcUser user = qm.getOidcUser(jsonUser.getUsername());
if (user != null) {
final OidcUser detachedUser = qm.getPersistenceManager().detachCopy(user);
qm.delete(user);
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "OpenID Connect user deleted: " + jsonUser.getUsername());
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "OpenID Connect user deleted: " + detachedUser);
dispatchUserDeletedNotification("OpenID Connect user deleted", buildUserSubject(detachedUser.getUsername(), detachedUser.getEmail()));
return Response.status(Response.Status.NO_CONTENT).build();
} else {
return Response.status(Response.Status.NOT_FOUND).entity("The user could not be found.").build();
Expand Down Expand Up @@ -668,4 +687,29 @@ public Response removeTeamFromUser(
}
}

private void dispatchUserCreatedNotification(final String content, final UserSubject subject) {
eventDispatcher.dispatchNotification(new Notification()
.scope(NotificationScope.SYSTEM)
.group(NotificationGroup.USER_CREATED)
.level(NotificationLevel.INFORMATIONAL)
.title(NotificationConstants.Title.USER_CREATED)
.content(content)
.subject(subject));
}

private void dispatchUserDeletedNotification(final String content, final UserSubject subject) {
eventDispatcher.dispatchNotification(new Notification()
.scope(NotificationScope.SYSTEM)
.group(NotificationGroup.USER_DELETED)
.level(NotificationLevel.INFORMATIONAL)
.title(NotificationConstants.Title.USER_DELETED)
.content(content)
.subject(subject));
}

private UserSubject buildUserSubject(final String username, final String email) {
var userBuilder = UserSubject.newBuilder().setUsername(username);
Optional.ofNullable(email).ifPresent(userBuilder::setEmail);
return userBuilder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ enum Group {
GROUP_PROJECT_CREATED = 16;
GROUP_BOM_PROCESSING_FAILED = 17;
GROUP_PROJECT_VULN_ANALYSIS_COMPLETE = 18;
GROUP_USER_CREATED = 19;
GROUP_USER_DELETED = 20;

// Indexing service has been removed as of
// https://github.com/DependencyTrack/hyades/issues/661
Expand Down Expand Up @@ -216,6 +218,11 @@ message ProjectVulnAnalysisCompleteSubject {
string token = 4;
}

message UserSubject {
string username = 1;
optional string email = 2;
}

enum ProjectVulnAnalysisStatus {
PROJECT_VULN_ANALYSIS_STATUS_UNSPECIFIED = 0;
PROJECT_VULN_ANALYSIS_STATUS_FAILED = 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import org.junit.Assert;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;

public class NotificationConstantsTest {

Expand All @@ -42,5 +43,7 @@ public void testConstants() {
Assert.assertEquals("Analysis Decision: Finding Suppressed", NotificationConstants.Title.ANALYSIS_DECISION_SUPPRESSED);
Assert.assertEquals("Analysis Decision: Finding UnSuppressed", NotificationConstants.Title.ANALYSIS_DECISION_UNSUPPRESSED);
Assert.assertEquals("Analysis Decision: Finding Resolved", NotificationConstants.Title.ANALYSIS_DECISION_RESOLVED);
Assertions.assertEquals("User Created", NotificationConstants.Title.USER_CREATED);
Assertions.assertEquals("User Deleted", NotificationConstants.Title.USER_DELETED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,7 @@ public void testEnums() {
//Assert.assertEquals("FIXED_OUTDATED", NotificationGroup.FIXED_OUTDATED.name());
//Assert.assertEquals("GLOBAL_AUDIT_CHANGE", NotificationGroup.GLOBAL_AUDIT_CHANGE.name());
Assert.assertEquals("PROJECT_AUDIT_CHANGE", NotificationGroup.PROJECT_AUDIT_CHANGE.name());
Assert.assertEquals("USER_CREATED", NotificationGroup.USER_CREATED.name());
Assert.assertEquals("USER_DELETED", NotificationGroup.USER_DELETED.name());
}
}
Loading