Skip to content

Commit 40076cd

Browse files
committed
[backend] feat: fix feedbacks pr
1 parent 35fc46b commit 40076cd

2 files changed

Lines changed: 284 additions & 1 deletion

File tree

openaev-api/src/main/java/io/openaev/rest/finding/FindingService.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,67 @@ public void createFindings(
218218
@NotNull final List<Finding> findings, @NotBlank final String injectId) {
219219
Inject inject = injectService.inject(injectId);
220220
findings.forEach(finding -> finding.setInject(inject));
221-
findingRepository.saveAll(findings);
221+
List<Finding> deduplicatedFindings = deduplicateFindings(findings);
222+
findingRepository.saveAll(deduplicatedFindings);
223+
}
224+
225+
/**
226+
* Deduplicates a list of findings based on the unique constraint keys: value, type, and field.
227+
* When duplicates are found, their assets, teams and users are merged into the first occurrence
228+
* finding_field)}.
229+
*
230+
* @param findings the raw list of findings, potentially containing duplicates
231+
* @return a deduplicated list with associations merged
232+
*/
233+
private List<Finding> deduplicateFindings(@NotNull final List<Finding> findings) {
234+
Map<String, Finding> seen = new java.util.LinkedHashMap<>();
235+
for (Finding finding : findings) {
236+
String key = finding.getValue() + "|" + finding.getType() + "|" + finding.getField();
237+
Finding existing = seen.get(key);
238+
if (existing == null) {
239+
seen.put(key, finding);
240+
} else {
241+
log.debug(
242+
"Duplicate finding detected (value={}, type={}, field={}): merging associations",
243+
finding.getValue(),
244+
finding.getType(),
245+
finding.getField());
246+
if (finding.getAssets() != null) {
247+
List<Asset> merged =
248+
new ArrayList<>(existing.getAssets() != null ? existing.getAssets() : List.of());
249+
finding
250+
.getAssets()
251+
.forEach(
252+
a -> {
253+
if (!merged.contains(a)) merged.add(a);
254+
});
255+
existing.setAssets(merged);
256+
}
257+
if (finding.getTeams() != null) {
258+
List<Team> merged =
259+
new ArrayList<>(existing.getTeams() != null ? existing.getTeams() : List.of());
260+
finding
261+
.getTeams()
262+
.forEach(
263+
t -> {
264+
if (!merged.contains(t)) merged.add(t);
265+
});
266+
existing.setTeams(merged);
267+
}
268+
if (finding.getUsers() != null) {
269+
List<User> merged =
270+
new ArrayList<>(existing.getUsers() != null ? existing.getUsers() : List.of());
271+
finding
272+
.getUsers()
273+
.forEach(
274+
u -> {
275+
if (!merged.contains(u)) merged.add(u);
276+
});
277+
existing.setUsers(merged);
278+
}
279+
}
280+
}
281+
return new ArrayList<>(seen.values());
222282
}
223283

224284
public List<Finding> buildFindings(

openaev-api/src/test/java/io/openaev/rest/inject/InjectApiTest.java

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,6 +1400,229 @@ void given_targetedAsset_should_linkFindingToIt() throws Exception {
14001400
assertEquals(endpointSaved.getId(), findings.getLast().getAssets().getFirst().getId());
14011401
}
14021402

1403+
// Deduplication
1404+
1405+
@Test
1406+
@DisplayName(
1407+
"Should consolidate duplicate CVE findings when structured output contains multiple entries with the same id")
1408+
void shouldConsolidateDuplicateCveFindingsWhenStructuredOutputContainsDuplicates()
1409+
throws Exception {
1410+
// -- PREPARE --
1411+
InjectExecutionInput input = new InjectExecutionInput();
1412+
input.setMessage("Duplicate CVE findings test");
1413+
input.setOutputStructured(
1414+
"""
1415+
{
1416+
"cve": [
1417+
{"id": "CVE-2025-99999", "host": "192.168.1.10", "severity": "critical"},
1418+
{"id": "CVE-2025-99999", "host": "192.168.1.20", "severity": "critical"}
1419+
]
1420+
}
1421+
""");
1422+
input.setAction(InjectExecutionAction.complete);
1423+
input.setStatus("SUCCESS");
1424+
1425+
Inject inject = getPendingInjectWithAssets();
1426+
Injector injector = InjectorFixture.createDefaultPayloadInjector();
1427+
injectTestHelper.forceSaveInjector(injector);
1428+
1429+
ObjectNode convertedContent =
1430+
(ObjectNode)
1431+
mapper.readTree(
1432+
"""
1433+
{
1434+
"outputs": [
1435+
{
1436+
"field": "cve",
1437+
"isFindingCompatible": true,
1438+
"isMultiple": true,
1439+
"labels": [],
1440+
"type": "cve"
1441+
}
1442+
]
1443+
}
1444+
""");
1445+
convertedContent.set(CONTRACT_CONTENT_FIELDS, objectMapper.valueToTree(List.of()));
1446+
InjectorContract injectorContract =
1447+
InjectorContractFixture.createInjectorContract(convertedContent);
1448+
injectorContract.setInjector(injector);
1449+
InjectorContract injectorContractSaved =
1450+
injectTestHelper.forceSaveInjectorContract(injectorContract);
1451+
inject.setInjectorContract(injectorContractSaved);
1452+
inject.setContent(convertedContent);
1453+
injectTestHelper.forceSaveInject(inject);
1454+
1455+
// -- EXECUTE --
1456+
performAgentlessCallbackRequest(inject.getId(), input);
1457+
1458+
// -- ASSERT --
1459+
Awaitility.await()
1460+
.atMost(15, TimeUnit.SECONDS)
1461+
.with()
1462+
.pollInterval(1, TimeUnit.SECONDS)
1463+
.until(() -> !injectTestHelper.findFindingsByInjectId(inject.getId()).isEmpty());
1464+
1465+
List<Finding> findings = findingRepository.findAllByInjectId(inject.getId());
1466+
assertEquals(
1467+
1,
1468+
findings.size(),
1469+
"Duplicate CVE findings with the same id must be consolidated into one");
1470+
assertEquals(ContractOutputType.CVE, findings.getFirst().getType());
1471+
assertEquals("CVE-2025-99999", findings.getFirst().getValue());
1472+
}
1473+
1474+
@Test
1475+
@DisplayName(
1476+
"Should consolidate duplicate Port findings when two scanned hosts both have the same port open")
1477+
void shouldConsolidateDuplicatePortFindingsWhenTwoHostsHaveSamePortOpen() throws Exception {
1478+
// -- PREPARE --
1479+
InjectExecutionInput input = new InjectExecutionInput();
1480+
input.setMessage("nmap TCP connect scan");
1481+
input.setOutputStructured(
1482+
"""
1483+
{
1484+
"ports": [22, 8080, 8080]
1485+
}
1486+
""");
1487+
input.setAction(InjectExecutionAction.complete);
1488+
input.setStatus("SUCCESS");
1489+
1490+
Inject inject = getPendingInjectWithAssets();
1491+
Injector injector = InjectorFixture.createDefaultPayloadInjector();
1492+
injectTestHelper.forceSaveInjector(injector);
1493+
1494+
ObjectNode convertedContent =
1495+
(ObjectNode)
1496+
mapper.readTree(
1497+
"""
1498+
{
1499+
"outputs": [
1500+
{
1501+
"field": "ports",
1502+
"isFindingCompatible": true,
1503+
"isMultiple": true,
1504+
"labels": ["scan"],
1505+
"type": "port"
1506+
}
1507+
]
1508+
}
1509+
""");
1510+
convertedContent.set(CONTRACT_CONTENT_FIELDS, objectMapper.valueToTree(List.of()));
1511+
InjectorContract injectorContract =
1512+
InjectorContractFixture.createInjectorContract(convertedContent);
1513+
injectorContract.setInjector(injector);
1514+
InjectorContract injectorContractSaved =
1515+
injectTestHelper.forceSaveInjectorContract(injectorContract);
1516+
inject.setInjectorContract(injectorContractSaved);
1517+
inject.setContent(convertedContent);
1518+
injectTestHelper.forceSaveInject(inject);
1519+
1520+
// -- EXECUTE --
1521+
performAgentlessCallbackRequest(inject.getId(), input);
1522+
1523+
// -- ASSERT --
1524+
Awaitility.await()
1525+
.atMost(15, TimeUnit.SECONDS)
1526+
.with()
1527+
.pollInterval(1, TimeUnit.SECONDS)
1528+
.until(() -> injectTestHelper.findFindingsByInjectId(inject.getId()).size() >= 2);
1529+
1530+
List<Finding> findings = findingRepository.findAllByInjectId(inject.getId());
1531+
assertEquals(
1532+
2,
1533+
findings.size(),
1534+
"Port 22 and port 8080 (deduplicated) must produce exactly 2 findings");
1535+
assertTrue(
1536+
findings.stream().anyMatch(f -> f.getValue().equals("22")),
1537+
"Expected finding for port 22");
1538+
assertTrue(
1539+
findings.stream().anyMatch(f -> f.getValue().equals("8080")),
1540+
"Expected deduplicated finding for port 8080");
1541+
findings.forEach(f -> assertEquals(ContractOutputType.Port, f.getType()));
1542+
}
1543+
1544+
@Test
1545+
@DisplayName(
1546+
"Should merge assets of duplicate PortsScan findings when two assets expose the same host/port/service")
1547+
void shouldMergeAssetsOfDuplicatePortScanFindingsWhenTwoAssetsHaveSameHostPortService()
1548+
throws Exception {
1549+
// -- PREPARE --
1550+
Endpoint endpointA = EndpointFixture.createEndpoint();
1551+
Endpoint endpointASaved = injectTestHelper.forceSaveEndpoint(endpointA);
1552+
Endpoint endpointB = EndpointFixture.createEndpoint();
1553+
Endpoint endpointBSaved = injectTestHelper.forceSaveEndpoint(endpointB);
1554+
1555+
InjectExecutionInput input = new InjectExecutionInput();
1556+
input.setMessage("nmap scan_results two assets both expose 192.168.1.10:8080/http");
1557+
input.setOutputStructured(
1558+
String.format(
1559+
"""
1560+
{
1561+
"scan_results": [
1562+
{"asset_id": "%s", "host": "192.168.1.10", "port": "8080", "service": "http"},
1563+
{"asset_id": "%s", "host": "192.168.1.10", "port": "8080", "service": "http"}
1564+
]
1565+
}
1566+
""",
1567+
endpointASaved.getId(), endpointBSaved.getId()));
1568+
input.setAction(InjectExecutionAction.complete);
1569+
input.setStatus("SUCCESS");
1570+
1571+
Inject inject = getPendingInjectWithAssets();
1572+
Injector injector = InjectorFixture.createDefaultPayloadInjector();
1573+
injectTestHelper.forceSaveInjector(injector);
1574+
1575+
ObjectNode convertedContent =
1576+
(ObjectNode)
1577+
mapper.readTree(
1578+
"""
1579+
{
1580+
"outputs": [
1581+
{
1582+
"field": "scan_results",
1583+
"isFindingCompatible": true,
1584+
"isMultiple": true,
1585+
"labels": ["scan"],
1586+
"type": "portscan"
1587+
}
1588+
]
1589+
}
1590+
""");
1591+
convertedContent.set(CONTRACT_CONTENT_FIELDS, objectMapper.valueToTree(List.of()));
1592+
InjectorContract injectorContract =
1593+
InjectorContractFixture.createInjectorContract(convertedContent);
1594+
injectorContract.setInjector(injector);
1595+
InjectorContract injectorContractSaved =
1596+
injectTestHelper.forceSaveInjectorContract(injectorContract);
1597+
inject.setInjectorContract(injectorContractSaved);
1598+
inject.setContent(convertedContent);
1599+
injectTestHelper.forceSaveInject(inject);
1600+
1601+
// -- EXECUTE --
1602+
performAgentlessCallbackRequest(inject.getId(), input);
1603+
1604+
// -- ASSERT --
1605+
Awaitility.await()
1606+
.atMost(15, TimeUnit.SECONDS)
1607+
.with()
1608+
.pollInterval(1, TimeUnit.SECONDS)
1609+
.until(() -> !injectTestHelper.findFindingsByInjectId(inject.getId()).isEmpty());
1610+
1611+
List<Finding> findings = findingRepository.findAllByInjectId(inject.getId());
1612+
assertEquals(
1613+
1,
1614+
findings.size(),
1615+
"Two PortsScan entries with same host/port/service must be consolidated into one finding");
1616+
Finding merged = findings.getFirst();
1617+
assertEquals(ContractOutputType.PortsScan, merged.getType());
1618+
assertEquals("192.168.1.10:8080 (http)", merged.getValue());
1619+
List<String> assetIds = merged.getAssets().stream().map(Asset::getId).toList();
1620+
assertTrue(
1621+
assetIds.contains(endpointASaved.getId()), "Merged finding must be linked to asset A");
1622+
assertTrue(
1623+
assetIds.contains(endpointBSaved.getId()), "Merged finding must be linked to asset B");
1624+
}
1625+
14031626
// CVE
14041627

14051628
@Test

0 commit comments

Comments
 (0)