Skip to content

Commit ee61014

Browse files
Implementation for Banner
Signed-off-by: ElenaStroebele <elena.stroebele@rohde-schwarz.com>
1 parent 20d40b5 commit ee61014

File tree

4 files changed

+285
-0
lines changed

4 files changed

+285
-0
lines changed

src/main/java/org/dependencytrack/persistence/QueryManager.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import alpine.persistence.ScopedCustomization;
3232
import alpine.resources.AlpineRequest;
3333
import alpine.server.util.DbUtil;
34+
35+
import com.fasterxml.jackson.databind.ObjectMapper;
3436
import com.github.packageurl.PackageURL;
3537
import com.google.common.collect.Lists;
3638
import org.apache.commons.lang3.ClassUtils;
@@ -80,6 +82,7 @@
8082
import org.dependencytrack.model.VulnerableSoftware;
8183
import org.dependencytrack.notification.NotificationScope;
8284
import org.dependencytrack.notification.publisher.Publisher;
85+
import org.dependencytrack.resources.v1.vo.BannerConfig;
8386
import org.dependencytrack.resources.v1.vo.AffectedProject;
8487
import org.dependencytrack.resources.v1.vo.DependencyGraphResponse;
8588
import org.dependencytrack.tasks.scanners.AnalyzerIdentity;
@@ -1316,6 +1319,42 @@ public boolean isEnabled(final ConfigPropertyConstants configPropertyConstants)
13161319
return false;
13171320
}
13181321

1322+
public BannerConfig getBannerConfig() {
1323+
final ConfigProperty property = getConfigProperty("banner", "config");
1324+
if (property == null || property.getPropertyValue() == null || property.getPropertyValue().isBlank()){
1325+
return new BannerConfig();
1326+
}
1327+
try {
1328+
final ObjectMapper objectMapper = new ObjectMapper();
1329+
return objectMapper.readValue(property.getPropertyValue(), BannerConfig.class);
1330+
} catch (Exception e){
1331+
return new BannerConfig();
1332+
}
1333+
}
1334+
1335+
public BannerConfig setBannerConfig(final BannerConfig bannerConfig) {
1336+
final String json;
1337+
try {
1338+
final ObjectMapper objectMapper = new ObjectMapper();
1339+
json = objectMapper.writeValueAsString(bannerConfig);
1340+
} catch (Exception e){
1341+
throw new RuntimeException("Failed to serialize banner configuration", e);
1342+
}
1343+
1344+
runInTransaction(() -> {
1345+
ConfigProperty cp = getConfigProperty("banner", "config");
1346+
if (cp == null) {
1347+
cp = new ConfigProperty();
1348+
cp.setGroupName("banner");
1349+
cp.setPropertyName("config");
1350+
cp.setPropertyType(ConfigProperty.PropertyType.STRING);
1351+
}
1352+
cp.setPropertyValue(json);
1353+
pm.makePersistent(cp);
1354+
});
1355+
return bannerConfig;
1356+
}
1357+
13191358
public ComponentAnalysisCache getComponentAnalysisCache(ComponentAnalysisCache.CacheType cacheType, String targetHost, String targetType, String target) {
13201359
return getCacheQueryManager().getComponentAnalysisCache(cacheType, targetHost, targetType, target);
13211360
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* This file is part of Dependency-Track.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
package org.dependencytrack.resources.v1;
20+
21+
import org.dependencytrack.auth.Permissions;
22+
import org.dependencytrack.persistence.QueryManager;
23+
import org.dependencytrack.resources.v1.vo.BannerConfig;
24+
25+
import alpine.server.auth.PermissionRequired;
26+
import alpine.server.resources.AlpineResource;
27+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
28+
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
29+
import io.swagger.v3.oas.annotations.tags.Tag;
30+
import jakarta.ws.rs.Consumes;
31+
import jakarta.ws.rs.GET;
32+
import jakarta.ws.rs.POST;
33+
import jakarta.ws.rs.Path;
34+
import jakarta.ws.rs.Produces;
35+
import jakarta.ws.rs.core.MediaType;
36+
import jakarta.ws.rs.core.Response;
37+
import io.swagger.v3.oas.annotations.Operation;
38+
39+
@Path("/v1/banner")
40+
@Tag(name = "banner")
41+
@SecurityRequirements({
42+
@SecurityRequirement(name = "ApiKeyAuth"),
43+
@SecurityRequirement(name = "BearerAuth")
44+
})
45+
46+
public class BannerResource extends AlpineResource {
47+
48+
public static final String CFG_GROUP = "banner";
49+
public static final String CFG_NAME = "config";
50+
51+
@GET
52+
@Produces(MediaType.APPLICATION_JSON)
53+
@Operation(summary = "Fetch banner configuration.")
54+
55+
public Response getBannerConfiguration() {
56+
try (final QueryManager qm = new QueryManager(getAlpineRequest())) {
57+
final BannerConfig config = qm.getBannerConfig();
58+
return Response.ok(config).build();
59+
} catch (Exception e) {
60+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Failed to fetch banner configuration")
61+
.build();
62+
}
63+
64+
}
65+
66+
@POST
67+
@Consumes(MediaType.APPLICATION_JSON)
68+
@Produces(MediaType.APPLICATION_JSON)
69+
@Operation(summary = "Update banner configuration.")
70+
@PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT)
71+
72+
public Response updateBannerConfiguration(BannerConfig config) {
73+
if (config == null) {
74+
return Response.status(Response.Status.BAD_REQUEST).entity("Banner configuration is required").build();
75+
}
76+
if (config.activateBanner) {
77+
if (config.customMode) {
78+
if (config.html == null || config.html.trim().isEmpty()) {
79+
return Response.status(Response.Status.BAD_REQUEST)
80+
.entity("Banner HTML is required when banner is active in custom mode").build();
81+
}
82+
} else {
83+
if (config.message == null || config.message.trim().isEmpty()) {
84+
return Response.status(Response.Status.BAD_REQUEST)
85+
.entity("Banner message is required when banner is active").build();
86+
}
87+
}
88+
}
89+
try (final QueryManager qm = new QueryManager(getAlpineRequest())) {
90+
final BannerConfig saved = qm.setBannerConfig(config);
91+
return Response.ok(saved).build();
92+
} catch (Exception e) {
93+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
94+
.entity("Failed to update banner configuration").build();
95+
}
96+
}
97+
98+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* This file is part of Dependency-Track.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
package org.dependencytrack.resources.v1.vo;
20+
21+
public class BannerConfig {
22+
public boolean activateBanner;
23+
public boolean makeBannerDismissable;
24+
public String message;
25+
public String colorScheme;
26+
public boolean customMode;
27+
public String html;
28+
29+
public BannerConfig() {
30+
}
31+
32+
public BannerConfig(boolean activateBanner, boolean makeBannerDismissable, String message, String colorScheme,
33+
boolean customMode, String html) {
34+
this.activateBanner = activateBanner;
35+
this.makeBannerDismissable = makeBannerDismissable;
36+
this.message = message;
37+
this.colorScheme = colorScheme;
38+
this.customMode = customMode;
39+
this.html = html;
40+
}
41+
42+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package org.dependencytrack.resources.v1;
2+
3+
import alpine.server.filters.ApiFilter;
4+
import alpine.server.filters.AuthenticationFilter;
5+
import jakarta.json.JsonObject;
6+
import jakarta.ws.rs.client.Entity;
7+
import jakarta.ws.rs.core.MediaType;
8+
import jakarta.ws.rs.core.Response;
9+
10+
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
12+
13+
import org.dependencytrack.JerseyTestExtension;
14+
import org.dependencytrack.ResourceTest;
15+
import org.dependencytrack.auth.Permissions;
16+
import org.glassfish.jersey.server.ResourceConfig;
17+
import org.junit.jupiter.api.Test;
18+
import org.junit.jupiter.api.extension.RegisterExtension;
19+
import org.dependencytrack.resources.v1.vo.BannerConfig;
20+
21+
public class BannerResourceTest extends ResourceTest {
22+
@RegisterExtension
23+
public static JerseyTestExtension jersey = new JerseyTestExtension(
24+
() -> new ResourceConfig(BannerResource.class)
25+
.register(ApiFilter.class)
26+
.register(AuthenticationFilter.class));
27+
28+
@Test
29+
void getBannerConfigurationForPresetActive() {
30+
BannerConfig bannerConfiguration = new BannerConfig();
31+
bannerConfiguration.activateBanner = true;
32+
bannerConfiguration.customMode = false;
33+
bannerConfiguration.message = "Banner Test Preset";
34+
qm.setBannerConfig(bannerConfiguration);
35+
36+
Response response = jersey.target("/v1/banner").request().header(X_API_KEY, apiKey).get();
37+
assertEquals(200, response.getStatus());
38+
JsonObject bannerJson = parseJsonObject(response);
39+
assertEquals(true, bannerJson.getBoolean("activateBanner"));
40+
assertEquals(false, bannerJson.getBoolean("customMode"));
41+
assertEquals("Banner Test Preset", bannerJson.getString("message"));
42+
}
43+
44+
@Test
45+
void updateBannerConfigurationForCustomHTMLActive() {
46+
47+
initializeWithPermissions(Permissions.ACCESS_MANAGEMENT);
48+
49+
Response response = jersey.target("/v1/banner").request().header(X_API_KEY, apiKey)
50+
.post(Entity.entity(
51+
/* language=JSON */ """
52+
{"activateBanner": true, "customMode": true, "html": "<div style=\\\"position:relative;background:#321FDB;color:#fff;padding:${padding};border-bottom:1px solid rgba(0,0,0,.2);font-size:18px;line-height:1.4;text-align:center\\\"><strong>Example:</strong> <span>Your HTML-Banner Text</span>"}
53+
""",
54+
MediaType.APPLICATION_JSON));
55+
56+
assertEquals(200, response.getStatus());
57+
JsonObject bannerJson = parseJsonObject(response);
58+
assertTrue(bannerJson.getBoolean("activateBanner"));
59+
assertTrue(bannerJson.getBoolean("customMode"));
60+
final var expected = "<div style=\"position:relative;background:#321FDB;color:#fff;padding:${padding};border-bottom:1px solid rgba(0,0,0,.2);font-size:18px;line-height:1.4;text-align:center\"><strong>Example:</strong> <span>Your HTML-Banner Text</span>";
61+
assertEquals(
62+
expected,
63+
bannerJson.getString("html"));
64+
}
65+
66+
@Test
67+
void updateBannerConfigurationForCustomHTMLMissingHTML() {
68+
Response response = jersey.target("/v1/banner").request().header(X_API_KEY, apiKey)
69+
.post(Entity.entity(
70+
/* language=JSON */ """
71+
{"activateBanner": true, "customMode": true, "html": ""}
72+
""",
73+
MediaType.APPLICATION_JSON));
74+
75+
assertEquals(400, response.getStatus());
76+
assertEquals("Banner HTML is required when banner is active in custom mode", getPlainTextBody(response));
77+
}
78+
79+
@Test
80+
void updateBannerConfigurationForPresetMissingMessage() {
81+
Response response = jersey.target("/v1/banner").request().header(X_API_KEY, apiKey)
82+
.post(Entity.entity(
83+
/* language=JSON */ """
84+
{"activateBanner": true, "customMode": false, "message": ""}
85+
""",
86+
MediaType.APPLICATION_JSON));
87+
88+
assertEquals(400, response.getStatus());
89+
assertEquals("Banner message is required when banner is active", getPlainTextBody(response));
90+
}
91+
92+
@Test
93+
void setBannerConfigurationForPreset() {
94+
BannerConfig bannerConfiguration = new BannerConfig(false, true, "Test Banner", "blue", false, "");
95+
96+
Response post = jersey.target("/v1/banner").request().header(X_API_KEY, apiKey).post(Entity.entity(bannerConfiguration, MediaType.APPLICATION_JSON));
97+
assertEquals(200, post.getStatus());
98+
JsonObject bannerJson = parseJsonObject(post);
99+
assertEquals(false, bannerJson.getBoolean("activateBanner"));
100+
assertEquals(true, bannerJson.getBoolean("makeBannerDismissable"));
101+
assertEquals("Test Banner", bannerJson.getString("message"));
102+
assertEquals("blue", bannerJson.getString("colorScheme"));
103+
assertEquals(false, bannerJson.getBoolean("customMode"));
104+
assertEquals("", bannerJson.getString("html"));
105+
}
106+
}

0 commit comments

Comments
 (0)