Skip to content

Commit

Permalink
Merge pull request #3275 from fnxpt/3255
Browse files Browse the repository at this point in the history
Webhook alert token and new user alerts
  • Loading branch information
nscuro authored Feb 21, 2024
2 parents 33efa7a + 07d64b8 commit 012d03e
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 5 deletions.
44 changes: 43 additions & 1 deletion docs/_docs/integrations/notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ multiple levels, while others can only ever have a single level.
| SYSTEM | INDEXING_SERVICE | (Any) | Notifications generated as a result of performing maintenance on Dependency-Tracks internal index used for global searching |
| SYSTEM | FILE_SYSTEM | (Any) | Notifications generated as a result of a file system operation. These are typically only generated on error conditions |
| SYSTEM | REPOSITORY | (Any) | Notifications generated as a result of interacting with one of the supported repositories such as Maven Central, RubyGems, or NPM |
| SYSTEM | USER_CREATED | INFORMATIONAL | Notifications generated as a result of a user creation |
| SYSTEM | USER_DELETED | INFORMATIONAL | Notifications generated as a result of a user deletion |
| PORTFOLIO | NEW_VULNERABILITY | INFORMATIONAL | Notifications generated whenever a new vulnerability is identified |
| PORTFOLIO | NEW_VULNERABLE_DEPENDENCY | INFORMATIONAL | Notifications generated as a result of a vulnerable component becoming a dependency of a project |
| PORTFOLIO | GLOBAL_AUDIT_CHANGE | INFORMATIONAL | Notifications generated whenever an analysis or suppression state has changed on a finding from a component (global) |
Expand Down Expand Up @@ -365,6 +367,46 @@ This type of notification will always contain:
}
```

#### USER_CREATED

```json
{
"notification": {
"level": "INFORMATIONAL",
"scope": "SYSTEM",
"group": "USER_CREATED",
"timestamp": "2022-05-12T23:07:59.611303",
"title": "User Created",
"content": "LDAP user created",
"subject": {
"id": "user",
"username": "user",
"name": "User 1",
"email": "user@example.com",
}
}
}
```

#### USER_DELETED

```json
{
"notification": {
"level": "INFORMATIONAL",
"scope": "SYSTEM",
"group": "USER_CREATED",
"timestamp": "2022-05-12T23:07:59.611303",
"title": "User Deleted",
"content": "LDAP user deleted",
"subject": {
"username": "user",
}
}
}
```


### Override of default templates
Default publishers are installed in the database at startup using templates retrieved in Dependency-Track classpath. Those publishers are **read-only** by default.
Dependency-Track can be configured from the administrative page to allow an override of the default templates. This requires SYSTEM_CONFIGURATION permission.
Expand All @@ -376,7 +418,7 @@ Switch on enable default template override flag and provide a filesystem base di

![notification publisher general configuration](/images/screenshots/notifications-publisher-override-template.png)

> The default template override flag is switched off by default and can set at initial startup with environment variable `DEFAULT_TEMPLATES_OVERRIDE_ENABLED`.
> The default template override flag is switched off by default and can set at initial startup with environment variable `DEFAULT_TEMPLATES_OVERRIDE_ENABLED`.
> The default templates base directory is set to ${user.home} by default and can be set at initial startup with environment variable `DEFAULT_TEMPLATES_OVERRIDE_BASE_DIRECTORY`.
To override all default templates, you must have the following [Pebble Templates](https://pebbletemplates.io/) template files inside the configured base directory.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ 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 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_CONSUMED,
VEX_PROCESSED,
POLICY_VIOLATION,
PROJECT_CREATED
PROJECT_CREATED,
USER_CREATED,
USER_DELETED
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public void publish(final PublishContext ctx, final PebbleTemplate template, fin
} else {
request.addHeader("Authorization", "Bearer " + credentials.password);
}
} else if (getToken(config) != null) {
request.addHeader(getTokenHeader(config), getToken(config));
}

try {
Expand Down Expand Up @@ -107,6 +109,14 @@ protected AuthCredentials getAuthCredentials() {
return null;
}

protected String getToken(final JsonObject config) {
return config.getString(CONFIG_TOKEN, null);
}

protected String getTokenHeader(final JsonObject config) {
return config.getString(CONFIG_TOKEN_HEADER, "X-Api-Key");
}

protected record AuthCredentials(String user, String password) {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import alpine.common.util.UrlUtil;
import alpine.model.ConfigProperty;
import alpine.model.UserPrincipal;
import alpine.notification.Notification;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.template.PebbleTemplate;
Expand Down Expand Up @@ -52,6 +53,8 @@ public interface Publisher {
String CONFIG_TEMPLATE_MIME_TYPE_KEY = "mimeType";

String CONFIG_DESTINATION = "destination";
String CONFIG_TOKEN = "token";
String CONFIG_TOKEN_HEADER = "tokenHeader";

void inform(final PublishContext ctx, final Notification notification, final JsonObject config);

Expand Down Expand Up @@ -124,6 +127,11 @@ default String prepareTemplate(final Notification notification, final PebbleTemp
context.put("subject", subject);
context.put("subjectJson", NotificationUtil.toJson(subject));
}
} else if (NotificationScope.SYSTEM.name().equals(notification.getScope())) {
if (notification.getSubject() instanceof final UserPrincipal subject) {
context.put("subject", subject);
context.put("subjectJson", NotificationUtil.toJson(subject));
}
}
enrichTemplateContext(context);

Expand Down
56 changes: 53 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 @@ -45,6 +47,9 @@
import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.auth.Permissions;
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.owasp.security.logging.SecurityMarkers;

Expand Down Expand Up @@ -383,6 +388,13 @@ public Response createLdapUser(LdapUser jsonUser) {
if (user == null) {
user = qm.createLdapUser(jsonUser.getUsername());
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "LDAP user created: " + jsonUser.getUsername());
Notification.dispatch(new Notification()
.scope(NotificationScope.SYSTEM)
.group(NotificationGroup.USER_CREATED)
.title(NotificationConstants.Title.USER_CREATED)
.level(NotificationLevel.INFORMATIONAL)
.content("LDAP user created")
.subject(user));
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 +419,16 @@ 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);
Notification.dispatch(new Notification()
.scope(NotificationScope.SYSTEM)
.group(NotificationGroup.USER_DELETED)
.title(NotificationConstants.Title.USER_DELETED)
.level(NotificationLevel.INFORMATIONAL)
.content("LDAP user deleted")
.subject(detachedUser));
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 +476,13 @@ 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());
Notification.dispatch(new Notification()
.scope(NotificationScope.SYSTEM)
.group(NotificationGroup.USER_CREATED)
.title(NotificationConstants.Title.USER_CREATED)
.level(NotificationLevel.INFORMATIONAL)
.content("Managed user created")
.subject(user));
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 +549,16 @@ 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);
Notification.dispatch(new Notification()
.scope(NotificationScope.SYSTEM)
.group(NotificationGroup.USER_DELETED)
.title(NotificationConstants.Title.USER_DELETED)
.level(NotificationLevel.INFORMATIONAL)
.content("Managed user deleted")
.subject(detachedUser));
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 +590,13 @@ 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());
Notification.dispatch(new Notification()
.scope(NotificationScope.SYSTEM)
.group(NotificationGroup.USER_CREATED)
.title(NotificationConstants.Title.USER_CREATED)
.level(NotificationLevel.INFORMATIONAL)
.content("OpenID Connect user created")
.subject(user));
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 +621,16 @@ 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);
Notification.dispatch(new Notification()
.scope(NotificationScope.SYSTEM)
.group(NotificationGroup.USER_DELETED)
.title(NotificationConstants.Title.USER_DELETED)
.level(NotificationLevel.INFORMATIONAL)
.content("OpenID Connect user deleted")
.subject(detachedUser));
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
17 changes: 17 additions & 0 deletions src/main/java/org/dependencytrack/util/NotificationUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.dependencytrack.util;

import alpine.model.ConfigProperty;
import alpine.model.UserPrincipal;
import alpine.notification.Notification;
import alpine.notification.NotificationLevel;
import org.apache.commons.io.FileUtils;
Expand Down Expand Up @@ -272,6 +273,22 @@ public static JsonObject toJson(final Project project) {
return projectBuilder.build();
}

public static JsonObject toJson(final UserPrincipal user) {
final JsonObjectBuilder userBuilder = Json.createObjectBuilder();

userBuilder.add("username", user.getUsername());

if (user.getName() != null) {
userBuilder.add("name", user.getName());
}

if (user.getEmail() != null) {
userBuilder.add("email", user.getEmail());
}

return userBuilder.build();
}

public static JsonObject toJson(final Component component) {
final JsonObjectBuilder componentBuilder = Json.createObjectBuilder();
componentBuilder.add("uuid", component.getUuid().toString());
Expand Down

0 comments on commit 012d03e

Please sign in to comment.