Skip to content

Commit 0758451

Browse files
committed
added vulnerability endpoints for frontend integration
1 parent 1047783 commit 0758451

7 files changed

Lines changed: 433 additions & 2 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package org.frankframework.insights.common.entityconnection.releasevulnerability;
22

3+
import java.util.List;
34
import org.frankframework.insights.release.Release;
45
import org.springframework.data.jpa.repository.JpaRepository;
56
import org.springframework.stereotype.Repository;
67

78
@Repository
89
public interface ReleaseVulnerabilityRepository extends JpaRepository<ReleaseVulnerability, ReleaseVulnerabilityId> {
910
void deleteAllByRelease(Release release);
11+
List<ReleaseVulnerability> findAllByReleaseId(String releaseId);
1012
}

src/main/java/org/frankframework/insights/release/ReleaseController.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22

33
import java.util.Collections;
44
import java.util.Set;
5-
import lombok.extern.slf4j.Slf4j;
65
import org.springframework.http.HttpStatus;
76
import org.springframework.http.ResponseEntity;
87
import org.springframework.web.bind.annotation.GetMapping;
98
import org.springframework.web.bind.annotation.RequestMapping;
109
import org.springframework.web.bind.annotation.RestController;
1110

12-
@Slf4j
1311
@RestController
1412
@RequestMapping("/releases")
1513
public class ReleaseController {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.frankframework.insights.vulnerability;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.PathVariable;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
import java.util.Collections;
11+
import java.util.Set;
12+
13+
@RestController
14+
@RequestMapping("/vulnerabilities")
15+
public class VulnerabilityController {
16+
private final VulnerabilityService vulnerabilityService;
17+
18+
public VulnerabilityController(VulnerabilityService vulnerabilityService) {
19+
this.vulnerabilityService = vulnerabilityService;
20+
}
21+
22+
/**
23+
* Fetches all vulnerabilities associated with a given release ID.
24+
* @param releaseId The ID of the release to fetch vulnerabilities for
25+
* @return Set of vulnerabilities associated with the release
26+
*/
27+
@GetMapping("/release/{releaseId}")
28+
public ResponseEntity<Set<VulnerabilityResponse>> getVulnerabilitiesByReleaseId(@PathVariable String releaseId) {
29+
Set<VulnerabilityResponse> vulnerabilities = vulnerabilityService.getVulnerabilitiesByReleaseId(releaseId);
30+
if (vulnerabilities == null) vulnerabilities = Collections.emptySet();
31+
return ResponseEntity.status(HttpStatus.OK).body(vulnerabilities);
32+
}
33+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.frankframework.insights.vulnerability;
2+
3+
import org.frankframework.insights.common.enums.VulnerabilitySeverity;
4+
5+
import java.util.Set;
6+
7+
public record VulnerabilityResponse(String cveId, VulnerabilitySeverity severity, Double cvssScore, String description, Set<String> cwes) { }

src/main/java/org/frankframework/insights/vulnerability/VulnerabilityService.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.frankframework.insights.common.entityconnection.releasevulnerability.ReleaseVulnerability;
1414
import org.frankframework.insights.common.entityconnection.releasevulnerability.ReleaseVulnerabilityRepository;
1515
import org.frankframework.insights.common.enums.VulnerabilitySeverity;
16+
import org.frankframework.insights.common.mapper.Mapper;
1617
import org.frankframework.insights.common.properties.NVDProperties;
1718
import org.frankframework.insights.common.properties.OSSProperties;
1819
import org.frankframework.insights.release.Release;
@@ -34,6 +35,7 @@ public class VulnerabilityService {
3435
private final ReleaseVulnerabilityRepository releaseVulnerabilityRepository;
3536
private final ReleaseRepository releaseRepository;
3637
private final ReleaseArtifactService releaseArtifactService;
38+
private final Mapper mapper;
3739
private final String ossIndexUsername;
3840
private final String ossIndexToken;
3941
private final String nvdApiKey;
@@ -45,12 +47,14 @@ public VulnerabilityService(
4547
ReleaseVulnerabilityRepository releaseVulnerabilityRepository,
4648
ReleaseRepository releaseRepository,
4749
ReleaseArtifactService releaseArtifactService,
50+
Mapper mapper,
4851
OSSProperties ossProperties,
4952
NVDProperties nvdProperties) {
5053
this.vulnerabilityRepository = vulnerabilityRepository;
5154
this.releaseVulnerabilityRepository = releaseVulnerabilityRepository;
5255
this.releaseRepository = releaseRepository;
5356
this.releaseArtifactService = releaseArtifactService;
57+
this.mapper = mapper;
5458
this.ossIndexUsername = ossProperties.getUsername();
5559
this.ossIndexToken = ossProperties.getToken();
5660
this.nvdApiKey = nvdProperties.getKey();
@@ -418,4 +422,31 @@ private Double extractCvssV2Score(Vulnerability scannedVulnerability) {
418422
}
419423
return -1.0;
420424
}
425+
426+
/**
427+
* Retrieves all vulnerabilities associated with a specific release.
428+
* @param releaseId The ID of the release to fetch vulnerabilities for
429+
* @return Set of vulnerability responses for the given release
430+
*/
431+
public Set<VulnerabilityResponse> getVulnerabilitiesByReleaseId(String releaseId) {
432+
if (releaseId == null || releaseId.isBlank()) {
433+
log.warn("Attempted to fetch vulnerabilities with null or blank release ID");
434+
return Set.of();
435+
}
436+
437+
List<ReleaseVulnerability> releaseVulnerabilities = releaseVulnerabilityRepository.findAllByReleaseId(releaseId);
438+
439+
if (releaseVulnerabilities.isEmpty()) {
440+
log.info("No vulnerabilities found for release ID: {}", releaseId);
441+
return Set.of();
442+
}
443+
444+
Set<VulnerabilityResponse> responses = releaseVulnerabilities.stream()
445+
.map(ReleaseVulnerability::getVulnerability)
446+
.map(vulnerability -> mapper.toDTO(vulnerability, VulnerabilityResponse.class))
447+
.collect(Collectors.toSet());
448+
449+
log.info("Found {} vulnerabilities for release ID: {}", responses.size(), releaseId);
450+
return responses;
451+
}
421452
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package org.frankframework.insights.vulnerability;
2+
3+
import static org.mockito.Mockito.*;
4+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
5+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
6+
7+
import java.util.Collections;
8+
import java.util.Set;
9+
import org.frankframework.insights.common.enums.VulnerabilitySeverity;
10+
import org.junit.jupiter.api.BeforeEach;
11+
import org.junit.jupiter.api.Test;
12+
import org.springframework.beans.factory.annotation.Autowired;
13+
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
14+
import org.springframework.boot.test.context.TestConfiguration;
15+
import org.springframework.context.annotation.Bean;
16+
import org.springframework.http.MediaType;
17+
import org.springframework.test.web.servlet.MockMvc;
18+
19+
@WebMvcTest(VulnerabilityController.class)
20+
public class VulnerabilityControllerTest {
21+
22+
@Autowired
23+
private MockMvc mockMvc;
24+
25+
@Autowired
26+
private VulnerabilityService vulnerabilityService;
27+
28+
@TestConfiguration
29+
public static class TestConfig {
30+
@Bean
31+
public VulnerabilityService vulnerabilityService() {
32+
return mock(VulnerabilityService.class);
33+
}
34+
}
35+
36+
@BeforeEach
37+
public void resetMocks() {
38+
reset(vulnerabilityService);
39+
}
40+
41+
@Test
42+
public void getVulnerabilitiesByReleaseId_returnsOkWithVulnerabilities() throws Exception {
43+
String releaseId = "release-123";
44+
VulnerabilityResponse vuln1 = new VulnerabilityResponse(
45+
"CVE-2023-1234",
46+
VulnerabilitySeverity.HIGH,
47+
7.5,
48+
"SQL Injection vulnerability",
49+
Set.of("CWE-89")
50+
);
51+
VulnerabilityResponse vuln2 = new VulnerabilityResponse(
52+
"CVE-2023-5678",
53+
VulnerabilitySeverity.CRITICAL,
54+
9.8,
55+
"Remote Code Execution",
56+
Set.of("CWE-78", "CWE-502")
57+
);
58+
Set<VulnerabilityResponse> vulnerabilities = Set.of(vuln1, vuln2);
59+
60+
when(vulnerabilityService.getVulnerabilitiesByReleaseId(releaseId)).thenReturn(vulnerabilities);
61+
62+
mockMvc.perform(get("/api/vulnerabilities/release/{releaseId}", releaseId))
63+
.andExpect(status().isOk())
64+
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
65+
.andExpect(jsonPath("$").isArray())
66+
.andExpect(jsonPath("$.length()").value(2));
67+
68+
verify(vulnerabilityService, times(1)).getVulnerabilitiesByReleaseId(releaseId);
69+
}
70+
71+
@Test
72+
public void getVulnerabilitiesByReleaseId_returnsEmptySet() throws Exception {
73+
String releaseId = "release-no-vulns";
74+
when(vulnerabilityService.getVulnerabilitiesByReleaseId(releaseId)).thenReturn(Collections.emptySet());
75+
76+
mockMvc.perform(get("/api/vulnerabilities/release/{releaseId}", releaseId))
77+
.andExpect(status().isOk())
78+
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
79+
.andExpect(jsonPath("$").isArray())
80+
.andExpect(jsonPath("$.length()").value(0));
81+
82+
verify(vulnerabilityService, times(1)).getVulnerabilitiesByReleaseId(releaseId);
83+
}
84+
85+
@Test
86+
public void getVulnerabilitiesByReleaseId_serviceReturnsNull_treatedAsEmptySet() throws Exception {
87+
String releaseId = "release-null";
88+
when(vulnerabilityService.getVulnerabilitiesByReleaseId(releaseId)).thenReturn(null);
89+
90+
mockMvc.perform(get("/api/vulnerabilities/release/{releaseId}", releaseId))
91+
.andExpect(status().isOk())
92+
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
93+
.andExpect(jsonPath("$").isArray())
94+
.andExpect(jsonPath("$.length()").value(0));
95+
96+
verify(vulnerabilityService, times(1)).getVulnerabilitiesByReleaseId(releaseId);
97+
}
98+
99+
@Test
100+
public void getVulnerabilitiesByReleaseId_withSingleVulnerability() throws Exception {
101+
String releaseId = "release-single";
102+
VulnerabilityResponse vuln = new VulnerabilityResponse(
103+
"CVE-2024-0001",
104+
VulnerabilitySeverity.MEDIUM,
105+
5.3,
106+
"Information Disclosure",
107+
Set.of("CWE-200")
108+
);
109+
Set<VulnerabilityResponse> vulnerabilities = Set.of(vuln);
110+
111+
when(vulnerabilityService.getVulnerabilitiesByReleaseId(releaseId)).thenReturn(vulnerabilities);
112+
113+
mockMvc.perform(get("/api/vulnerabilities/release/{releaseId}", releaseId))
114+
.andExpect(status().isOk())
115+
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
116+
.andExpect(jsonPath("$").isArray())
117+
.andExpect(jsonPath("$.length()").value(1))
118+
.andExpect(jsonPath("$[0].cveId").value("CVE-2024-0001"))
119+
.andExpect(jsonPath("$[0].severity").value("MEDIUM"))
120+
.andExpect(jsonPath("$[0].cvssScore").value(5.3))
121+
.andExpect(jsonPath("$[0].description").value("Information Disclosure"));
122+
123+
verify(vulnerabilityService, times(1)).getVulnerabilitiesByReleaseId(releaseId);
124+
}
125+
126+
@Test
127+
public void getVulnerabilitiesByReleaseId_withoutReleaseIdParam_returnsNotFound() throws Exception {
128+
mockMvc.perform(get("/api/vulnerabilities/release/"))
129+
.andExpect(status().isNotFound());
130+
131+
verify(vulnerabilityService, never()).getVulnerabilitiesByReleaseId(any());
132+
}
133+
134+
@Test
135+
public void getVulnerabilitiesByReleaseId_withVulnerabilitiesOfDifferentSeverities() throws Exception {
136+
String releaseId = "release-mixed-severity";
137+
VulnerabilityResponse low = new VulnerabilityResponse(
138+
"CVE-2024-0010",
139+
VulnerabilitySeverity.LOW,
140+
3.1,
141+
"Low severity issue",
142+
Set.of("CWE-1")
143+
);
144+
VulnerabilityResponse medium = new VulnerabilityResponse(
145+
"CVE-2024-0020",
146+
VulnerabilitySeverity.MEDIUM,
147+
5.5,
148+
"Medium severity issue",
149+
Set.of("CWE-2")
150+
);
151+
VulnerabilityResponse high = new VulnerabilityResponse(
152+
"CVE-2024-0030",
153+
VulnerabilitySeverity.HIGH,
154+
7.8,
155+
"High severity issue",
156+
Set.of("CWE-3")
157+
);
158+
VulnerabilityResponse critical = new VulnerabilityResponse(
159+
"CVE-2024-0040",
160+
VulnerabilitySeverity.CRITICAL,
161+
9.9,
162+
"Critical severity issue",
163+
Set.of("CWE-4")
164+
);
165+
Set<VulnerabilityResponse> vulnerabilities = Set.of(low, medium, high, critical);
166+
167+
when(vulnerabilityService.getVulnerabilitiesByReleaseId(releaseId)).thenReturn(vulnerabilities);
168+
169+
mockMvc.perform(get("/api/vulnerabilities/release/{releaseId}", releaseId))
170+
.andExpect(status().isOk())
171+
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
172+
.andExpect(jsonPath("$").isArray())
173+
.andExpect(jsonPath("$.length()").value(4));
174+
175+
verify(vulnerabilityService, times(1)).getVulnerabilitiesByReleaseId(releaseId);
176+
}
177+
178+
@Test
179+
public void getVulnerabilitiesByReleaseId_withVulnerabilityWithMultipleCwes() throws Exception {
180+
String releaseId = "release-multi-cwe";
181+
VulnerabilityResponse vuln = new VulnerabilityResponse(
182+
"CVE-2024-9999",
183+
VulnerabilitySeverity.HIGH,
184+
8.2,
185+
"Multiple weakness types",
186+
Set.of("CWE-79", "CWE-89", "CWE-352", "CWE-434")
187+
);
188+
Set<VulnerabilityResponse> vulnerabilities = Set.of(vuln);
189+
190+
when(vulnerabilityService.getVulnerabilitiesByReleaseId(releaseId)).thenReturn(vulnerabilities);
191+
192+
mockMvc.perform(get("/api/vulnerabilities/release/{releaseId}", releaseId))
193+
.andExpect(status().isOk())
194+
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
195+
.andExpect(jsonPath("$").isArray())
196+
.andExpect(jsonPath("$.length()").value(1))
197+
.andExpect(jsonPath("$[0].cwes").isArray())
198+
.andExpect(jsonPath("$[0].cwes.length()").value(4));
199+
200+
verify(vulnerabilityService, times(1)).getVulnerabilitiesByReleaseId(releaseId);
201+
}
202+
203+
@Test
204+
public void getVulnerabilitiesByReleaseId_withVulnerabilityWithNoCwes() throws Exception {
205+
String releaseId = "release-no-cwe";
206+
VulnerabilityResponse vuln = new VulnerabilityResponse(
207+
"CVE-2024-1111",
208+
VulnerabilitySeverity.LOW,
209+
2.1,
210+
"Vulnerability without CWE classification",
211+
Collections.emptySet()
212+
);
213+
Set<VulnerabilityResponse> vulnerabilities = Set.of(vuln);
214+
215+
when(vulnerabilityService.getVulnerabilitiesByReleaseId(releaseId)).thenReturn(vulnerabilities);
216+
217+
mockMvc.perform(get("/api/vulnerabilities/release/{releaseId}", releaseId))
218+
.andExpect(status().isOk())
219+
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
220+
.andExpect(jsonPath("$").isArray())
221+
.andExpect(jsonPath("$.length()").value(1))
222+
.andExpect(jsonPath("$[0].cwes").isArray())
223+
.andExpect(jsonPath("$[0].cwes.length()").value(0));
224+
225+
verify(vulnerabilityService, times(1)).getVulnerabilitiesByReleaseId(releaseId);
226+
}
227+
}

0 commit comments

Comments
 (0)