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

Webhook alert token and new user alerts #3275

Merged
merged 8 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
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("X-Api-Key", getToken(config));
nscuro marked this conversation as resolved.
Show resolved Hide resolved
}

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

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

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,7 @@ public interface Publisher {
String CONFIG_TEMPLATE_MIME_TYPE_KEY = "mimeType";

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

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

Expand Down Expand Up @@ -124,6 +126,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
47 changes: 47 additions & 0 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 @@ -409,6 +421,13 @@ public Response deleteLdapUser(LdapUser jsonUser) {
if (user != null) {
qm.delete(user);
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "LDAP user deleted: " + jsonUser.getUsername());
Notification.dispatch(new Notification()
.scope(NotificationScope.SYSTEM)
.group(NotificationGroup.USER_DELETED)
.title(NotificationConstants.Title.USER_DELETED)
.level(NotificationLevel.INFORMATIONAL)
.content("LDAP user deleted")
.subject(jsonUser));
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 +475,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 @@ -524,6 +550,13 @@ public Response deleteManagedUser(ManagedUser jsonUser) {
if (user != null) {
qm.delete(user);
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Managed user deleted: " + jsonUser.getUsername());
Notification.dispatch(new Notification()
.scope(NotificationScope.SYSTEM)
.group(NotificationGroup.USER_DELETED)
.title(NotificationConstants.Title.USER_DELETED)
.level(NotificationLevel.INFORMATIONAL)
.content("Managed user deleted")
.subject(jsonUser));
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 +588,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 @@ -581,6 +621,13 @@ public Response deleteOidcUser(final OidcUser jsonUser) {
if (user != null) {
qm.delete(user);
super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "OpenID Connect user deleted: " + jsonUser.getUsername());
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(jsonUser));
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
20 changes: 20 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,25 @@ 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.getId() != 0) {
userBuilder.add("id", user.getId());
}
nscuro marked this conversation as resolved.
Show resolved Hide resolved
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