Skip to content

Commit b68689e

Browse files
authored
Merge pull request #1486 from utmstack/backlog/adversary-view-with-hierarchical-graph-adversary-alert-echoes
Backlog/adversary view with hierarchical graph adversary alert echoes
2 parents 56bf79d + d6b355f commit b68689e

27 files changed

Lines changed: 905 additions & 25 deletions

backend/src/main/java/com/park/utmstack/config/Constants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ public final class Constants {
140140
// ----------------------------------------------------------------------------------
141141
public static final String STATISTICS_INDEX_PATTERN = "v11-statistics-*";
142142
public static final String V11_API_ACCESS_LOGS = "v11-api-access-logs-*";
143+
public static final String V11_ALERTS_INDEX_PATTERN = "v11-alert-*";
143144

144145
// Logging
145146
public static final String TRACE_ID_KEY = "traceId";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.park.utmstack.service.dto.threat_management;
2+
3+
import com.park.utmstack.domain.shared_types.alert.Side;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
@Data
9+
@AllArgsConstructor
10+
@NoArgsConstructor
11+
public class Adversary {
12+
Side adversary;
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.park.utmstack.service.dto.threat_management;
2+
3+
import com.park.utmstack.domain.shared_types.alert.Side;
4+
import com.park.utmstack.domain.shared_types.alert.UtmAlert;
5+
import lombok.Data;
6+
import lombok.Builder;
7+
import java.util.List;
8+
9+
@Data
10+
@Builder
11+
public class AdversaryAlertsResponseDto {
12+
13+
private Side adversary;
14+
private List<AlertWithChildren> alerts;
15+
16+
@Data
17+
@Builder
18+
public static class AlertWithChildren {
19+
private UtmAlert alert;
20+
private List<UtmAlert> children;
21+
}
22+
}
23+
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.park.utmstack.service.threat_management;
2+
3+
4+
import com.park.utmstack.domain.chart_builder.types.query.FilterType;
5+
import com.park.utmstack.domain.shared_types.alert.Side;
6+
import com.park.utmstack.domain.shared_types.alert.UtmAlert;
7+
import com.park.utmstack.service.dto.threat_management.Adversary;
8+
import com.park.utmstack.service.dto.threat_management.AdversaryAlertsResponseDto;
9+
import com.park.utmstack.service.elasticsearch.ElasticsearchService;
10+
import com.park.utmstack.service.elasticsearch.SearchUtil;
11+
import lombok.RequiredArgsConstructor;
12+
import org.opensearch.client.json.JsonData;
13+
import org.opensearch.client.opensearch._types.SortOrder;
14+
import org.opensearch.client.opensearch.core.SearchRequest;
15+
import org.opensearch.client.opensearch.core.SearchResponse;
16+
import org.springframework.stereotype.Service;
17+
18+
import java.util.*;
19+
import java.util.stream.Collectors;
20+
21+
import static com.park.utmstack.config.Constants.V11_ALERTS_INDEX_PATTERN;
22+
23+
@Service
24+
@RequiredArgsConstructor
25+
public class AdversaryAlertsService {
26+
27+
private final ElasticsearchService elasticsearchService;
28+
29+
public List<AdversaryAlertsResponseDto> fetchAdversaryAlerts(List<FilterType> filters){
30+
SearchRequest request = SearchRequest.of(s -> s
31+
.index(V11_ALERTS_INDEX_PATTERN)
32+
.query(SearchUtil.toQuery(filters))
33+
.size(0)
34+
.aggregations("adversary", a -> a
35+
.terms(t -> t.field("adversary.host.keyword").size(100))
36+
.aggregations("adversary_obj", ag -> ag
37+
.topHits(th -> th
38+
.size(1)
39+
.sort(srt -> srt.field(f -> f.field("@timestamp")
40+
.order(SortOrder.Desc)))
41+
.source(src -> src.filter(f -> f.includes(List.of("adversary"))))
42+
)
43+
)
44+
.aggregations("alerts", ag -> ag
45+
.filter(flt -> flt
46+
.bool(b -> b
47+
.mustNot(mn -> mn.exists(e -> e.field("parentId")))
48+
)
49+
)
50+
.aggregations("alerts_hits", subAg -> subAg
51+
.topHits(th -> th
52+
.size(100)
53+
.sort(srt -> srt.field(f -> f.field("@timestamp")
54+
.order(SortOrder.Desc)))
55+
)
56+
)
57+
)
58+
59+
.aggregations("child_alerts", ag -> ag
60+
.terms(t -> t.field("parentId.keyword").size(50))
61+
.aggregations("child_hits", ch -> ch
62+
.topHits(th -> th
63+
.size(50)
64+
.sort(srt -> srt.field(f -> f.field("@timestamp")
65+
.order(SortOrder.Desc)))
66+
)
67+
)
68+
)
69+
)
70+
);
71+
72+
SearchResponse<JsonData> response = elasticsearchService.search(request, JsonData.class);
73+
74+
List<AdversaryAlertsResponseDto> groups = new ArrayList<>();
75+
76+
var adversaryBuckets = response.aggregations()
77+
.get("adversary")
78+
.sterms()
79+
.buckets()
80+
.array();
81+
82+
for (var bucket : adversaryBuckets) {
83+
var adversaryHit = bucket.aggregations().get("adversary_obj").topHits().hits().hits().get(0);
84+
85+
if (adversaryHit.source() != null) {
86+
Adversary adversaryWrapper = adversaryHit.source().to(Adversary.class);
87+
Side adversary = adversaryWrapper.getAdversary();
88+
89+
var topHitsAgg = bucket.aggregations().get("alerts").filter().aggregations()
90+
.get("alerts_hits").topHits();
91+
List<UtmAlert> alerts = topHitsAgg.hits().hits().stream()
92+
.filter(hit -> hit.source() != null)
93+
.map(hit -> hit.source().to(UtmAlert.class))
94+
.filter(alert -> Objects.isNull(alert.getParentId()))
95+
.toList();
96+
97+
Map<String, List<UtmAlert>> childMap = new HashMap<>();
98+
var childAgg = bucket.aggregations().get("child_alerts").sterms();
99+
100+
for (var cb : childAgg.buckets().array()) {
101+
var childHits = cb.aggregations().get("child_hits").topHits();
102+
List<UtmAlert> children = childHits.hits().hits().stream()
103+
.filter(hit -> hit.source() != null)
104+
.map(hit -> hit.source().to(UtmAlert.class))
105+
.toList();
106+
childMap.put(cb.key(), children);
107+
}
108+
109+
List<AdversaryAlertsResponseDto.AlertWithChildren> alertsWithChildren = alerts.stream()
110+
.map(alert -> AdversaryAlertsResponseDto.AlertWithChildren.builder()
111+
.alert(alert)
112+
.children(childMap.getOrDefault(alert.getId(), Collections.emptyList()))
113+
.build())
114+
.collect(Collectors.toList());
115+
116+
groups.add(AdversaryAlertsResponseDto.builder()
117+
.adversary(adversary)
118+
.alerts(alertsWithChildren)
119+
.build());
120+
}
121+
}
122+
123+
return groups;
124+
}
125+
126+
127+
}
128+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.park.utmstack.web.rest.threat_management;
2+
3+
import com.park.utmstack.domain.chart_builder.types.query.FilterType;
4+
import com.park.utmstack.service.dto.threat_management.AdversaryAlertsResponseDto;
5+
import com.park.utmstack.service.threat_management.AdversaryAlertsService;
6+
import io.swagger.v3.oas.annotations.Hidden;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.http.HttpStatus;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.*;
12+
13+
import java.util.Collections;
14+
import java.util.List;
15+
16+
@RestController
17+
@RequiredArgsConstructor
18+
@Slf4j
19+
@Hidden
20+
@RequestMapping("api/adversary")
21+
public class AdversaryAlertsResource {
22+
23+
private final AdversaryAlertsService adversaryAlertsService;
24+
25+
@PostMapping("/alerts")
26+
public ResponseEntity<List<AdversaryAlertsResponseDto>> search(@RequestBody(required = false) List<FilterType> filters) {
27+
List<AdversaryAlertsResponseDto> responseDto = adversaryAlertsService.fetchAdversaryAlerts(filters);
28+
29+
if (responseDto.isEmpty())
30+
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(Collections.emptyList());
31+
32+
return ResponseEntity.ok().body(responseDto);
33+
}
34+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<databaseChangeLog
3+
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
6+
7+
<changeSet id="20251118001" author="Manuel">
8+
<insert tableName="utm_menu">
9+
<column name="id" value="316"/>
10+
<column name="name" value="Adversary View"/>
11+
<column name="url" value="/data/adversary/view"/>
12+
<column name="parent_id" value="300"/>
13+
<column name="type" value="1"/>
14+
<column name="dashboard_id" valueNumeric="NULL"/>
15+
<column name="position" value="3"/>
16+
<column name="menu_active" valueBoolean="true"/>
17+
<column name="menu_action" valueBoolean="true"/>
18+
<column name="menu_icon" value="NULL"/>
19+
<column name="module_name_short" value="NULL"/>
20+
</insert>
21+
<insert tableName="utm_menu_authority">
22+
<column name="menu_id" value="316"/>
23+
<column name="authority_name" value="ROLE_ADMIN"/>
24+
</insert>
25+
<insert tableName="utm_menu_authority">
26+
<column name="menu_id" value="316"/>
27+
<column name="authority_name" value="ROLE_USER"/>
28+
</insert>
29+
</changeSet>
30+
31+
32+
</databaseChangeLog>

backend/src/main/resources/config/liquibase/master.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,5 +275,7 @@
275275

276276
<include file="/config/liquibase/changelog/20251201002_update_filter_google.xml" relativeToChangelogFile="false"/>
277277

278+
<include file="/config/liquibase/changelog/20251118001_add_adversary_view_menu.xml" relativeToChangelogFile="false"/>
279+
278280

279281
</databaseChangeLog>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div class="d-flex flex-column h-100 m-h-0 overflow-auto">
2+
<div class="flex-grow-1 h-100">
3+
<div echarts
4+
[options]="option"
5+
(chartInit)="onChartInit($event)"
6+
class="sankey-chart"
7+
[ngStyle]="{ height: chartHeight + 'px' }">
8+
</div>
9+
</div>
10+
</div>
11+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
:host {
2+
display: flex;
3+
flex-direction: column;
4+
flex: 1 1 auto;
5+
min-height: 0;
6+
height: 100%;
7+
}
8+
9+
.sankey-chart {
10+
min-height: 1200px;
11+
height: auto;
12+
}

0 commit comments

Comments
 (0)