Skip to content

Commit ff6d2f0

Browse files
Move attack wave detection to WebResponseCollector to capture user info
1 parent 1d87e9e commit ff6d2f0

4 files changed

Lines changed: 108 additions & 52 deletions

File tree

agent_api/src/main/java/dev/aikido/agent_api/collectors/WebRequestCollector.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
package dev.aikido.agent_api.collectors;
22

33
import dev.aikido.agent_api.background.Endpoint;
4-
import dev.aikido.agent_api.background.cloud.api.events.DetectedAttackWave;
54
import dev.aikido.agent_api.context.Context;
65
import dev.aikido.agent_api.context.ContextObject;
76
import dev.aikido.agent_api.context.RouteMetadata;
87
import dev.aikido.agent_api.helpers.logging.LogManager;
98
import dev.aikido.agent_api.helpers.logging.Logger;
10-
import dev.aikido.agent_api.storage.AttackQueue;
119
import dev.aikido.agent_api.storage.ServiceConfigStore;
1210
import dev.aikido.agent_api.storage.ServiceConfiguration;
13-
import dev.aikido.agent_api.storage.attack_wave_detector.AttackWaveDetectorStore;
1411
import dev.aikido.agent_api.storage.statistics.StatisticsStore;
1512

1613
import java.util.List;
@@ -60,14 +57,6 @@ public static Res report(ContextObject newContext) {
6057
if (blockedUARes != null)
6158
return blockedUARes;
6259

63-
// Check for attack waves
64-
if (AttackWaveDetectorStore.check(newContext)) {
65-
AttackQueue.add(
66-
DetectedAttackWave.createAPIEvent(newContext)
67-
);
68-
StatisticsStore.incrementAttackWavesDetected();
69-
}
70-
7160
return null;
7261
}
7362

agent_api/src/main/java/dev/aikido/agent_api/collectors/WebResponseCollector.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package dev.aikido.agent_api.collectors;
22

33
import dev.aikido.agent_api.api_discovery.APISpec;
4+
import dev.aikido.agent_api.background.cloud.api.events.DetectedAttackWave;
45
import dev.aikido.agent_api.context.Context;
56
import dev.aikido.agent_api.context.ContextObject;
67
import dev.aikido.agent_api.context.RouteMetadata;
8+
import dev.aikido.agent_api.storage.AttackQueue;
9+
import dev.aikido.agent_api.storage.attack_wave_detector.AttackWaveDetectorStore;
710
import dev.aikido.agent_api.storage.routes.RoutesStore;
11+
import dev.aikido.agent_api.storage.statistics.StatisticsStore;
812

913
import static dev.aikido.agent_api.api_discovery.GetApiInfo.getApiInfo;
1014
import static dev.aikido.agent_api.helpers.url.IsUsefulRoute.isUsefulRoute;
@@ -23,6 +27,15 @@ public static void report(int statusCode) {
2327
if (statusCode <= 0 || context == null) {
2428
return; // Status code below or equal to zero: Invalid request
2529
}
30+
31+
// Check for attack waves (after request is complete and user has been set)
32+
if (AttackWaveDetectorStore.check(context)) {
33+
AttackQueue.add(
34+
DetectedAttackWave.createAPIEvent(context)
35+
);
36+
StatisticsStore.incrementAttackWavesDetected();
37+
}
38+
2639
RouteMetadata routeMetadata = context.getRouteMetadata();
2740
if (routeMetadata == null || !isUsefulRoute(statusCode, context.getRoute(), context.getMethod())) {
2841
return;
@@ -37,4 +50,10 @@ public static void report(int statusCode) {
3750
RoutesStore.updateApiSpec(routeMetadata, apiSpec);
3851
}
3952
}
40-
}
53+
}
54+
//do we run @Marian Popovici’s tests already on Ruby?
55+
// we do, but most of them fail because there are some differences in the implementation, like sending post intead of POST in event, or /test_ratelimiting_1(.:format) instead of /test_ratelimiting_1
56+
57+
// Other porbless:
58+
// path traversal bypas - https://aikido-security.slack.com/archives/C07F482T4JG/p1754669410362449
59+
//

agent_api/src/test/java/collectors/WebRequestCollectorTest.java

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
import dev.aikido.agent_api.background.Endpoint;
44
import dev.aikido.agent_api.background.cloud.api.APIResponse;
55
import dev.aikido.agent_api.background.cloud.api.ReportingApi;
6-
import dev.aikido.agent_api.background.cloud.api.events.DetectedAttackWave;
76
import dev.aikido.agent_api.collectors.WebRequestCollector;
87
import dev.aikido.agent_api.context.Context;
9-
import dev.aikido.agent_api.context.ContextObject;
108
import dev.aikido.agent_api.storage.AttackQueue;
119
import dev.aikido.agent_api.storage.ServiceConfigStore;
1210
import dev.aikido.agent_api.storage.statistics.StatisticsStore;
@@ -15,7 +13,6 @@
1513
import utils.EmptySampleContextObject;
1614

1715
import java.util.List;
18-
import java.util.Map;
1916

2017
import static dev.aikido.agent_api.helpers.UnixTimeMS.getUnixTimeMS;
2118
import static org.junit.jupiter.api.Assertions.*;
@@ -261,41 +258,4 @@ void testReport_ipNotAllowedUsingLists_Ip_Bypassed() {
261258
assertNull(Context.get());
262259
}
263260

264-
@Test
265-
void testReport_WithAttackWaveContext() throws InterruptedException {
266-
ContextObject attackWaveCtx = new EmptySampleContextObject("/wp-config.php", "BADMETHOD", Map.of());
267-
268-
WebRequestCollector.Res response = WebRequestCollector.report(attackWaveCtx);
269-
assertNull(response);
270-
assertEquals(0, AttackQueue.getSize());
271-
assertEquals(0, StatisticsStore.getStatsRecord().requests().attackWaves().total());
272-
273-
// 2...14
274-
WebRequestCollector.report(attackWaveCtx);
275-
WebRequestCollector.report(attackWaveCtx);
276-
WebRequestCollector.report(attackWaveCtx);
277-
WebRequestCollector.report(attackWaveCtx);
278-
WebRequestCollector.report(attackWaveCtx);
279-
WebRequestCollector.report(attackWaveCtx);
280-
WebRequestCollector.report(attackWaveCtx);
281-
WebRequestCollector.report(attackWaveCtx);
282-
WebRequestCollector.report(attackWaveCtx);
283-
WebRequestCollector.report(attackWaveCtx);
284-
WebRequestCollector.report(attackWaveCtx);
285-
WebRequestCollector.report(attackWaveCtx);
286-
WebRequestCollector.report(attackWaveCtx);
287-
288-
WebRequestCollector.Res response2 = WebRequestCollector.report(attackWaveCtx);
289-
assertNull(response2);
290-
assertEquals(1, AttackQueue.getSize());
291-
DetectedAttackWave.DetectedAttackWaveEvent event = (DetectedAttackWave.DetectedAttackWaveEvent) AttackQueue.get();
292-
assertEquals("192.168.1.1", event.request().ipAddress());
293-
assertEquals("web", event.request().source());
294-
assertEquals(null, event.request().userAgent());
295-
assertEquals("detected_attack_wave", event.type());
296-
assertEquals(null, event.attack().user());
297-
assertEquals(0, event.attack().metadata().size());
298-
// check stats changed
299-
assertEquals(1, StatisticsStore.getStatsRecord().requests().attackWaves().total());
300-
}
301261
}

agent_api/src/test/java/collectors/WebResponseCollectorTest.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
package collectors;
22

3+
import dev.aikido.agent_api.background.cloud.api.events.DetectedAttackWave;
4+
import dev.aikido.agent_api.collectors.WebRequestCollector;
35
import dev.aikido.agent_api.collectors.WebResponseCollector;
46
import dev.aikido.agent_api.context.Context;
57
import dev.aikido.agent_api.context.ContextObject;
68
import dev.aikido.agent_api.context.RouteMetadata;
9+
import dev.aikido.agent_api.context.User;
10+
import dev.aikido.agent_api.storage.AttackQueue;
11+
import dev.aikido.agent_api.storage.ServiceConfigStore;
712
import dev.aikido.agent_api.storage.routes.RoutesStore;
13+
import dev.aikido.agent_api.storage.statistics.StatisticsStore;
814
import org.junit.jupiter.api.*;
915
import org.junit.jupiter.api.Test;
16+
import utils.EmptySampleContextObject;
1017

1118
import java.sql.SQLException;
1219
import java.util.HashMap;
20+
import java.util.Map;
1321

1422
import static org.junit.jupiter.api.Assertions.*;
23+
import static utils.EmptyAPIResponses.emptyAPIListsResponse;
24+
import static utils.EmptyAPIResponses.emptyAPIResponse;
1525

1626

1727
public class WebResponseCollectorTest {
@@ -43,6 +53,8 @@ public static void clean() {
4353
public void tearDown() throws SQLException {
4454
Context.set(null);
4555
RoutesStore.clear();
56+
AttackQueue.clear();
57+
StatisticsStore.clear();
4658
}
4759

4860
@Test
@@ -92,5 +104,81 @@ public void testResponseCollectorWithInvalidMethodOrStatusCode() throws SQLExcep
92104
WebResponseCollector.report(-200);
93105
assertEquals(0, RoutesStore.getRoutesAsList().length);
94106
}
107+
108+
@Test
109+
public void testAttackWaveDetectionWithUserSet() throws SQLException, InterruptedException {
110+
// Setup
111+
ServiceConfigStore.updateFromAPIResponse(emptyAPIResponse);
112+
ServiceConfigStore.updateFromAPIListsResponse(emptyAPIListsResponse);
113+
114+
// Create attack wave context (unusual route/method that triggers attack wave detection)
115+
ContextObject attackWaveCtx = new EmptySampleContextObject("/wp-config.php", "BADMETHOD", Map.of());
116+
117+
// Set a user in the context (simulating SetUser.setUser being called during request)
118+
User testUser = new User("user123", "John Doe", "192.168.1.1", System.currentTimeMillis());
119+
attackWaveCtx.setUser(testUser);
120+
121+
// Simulate the request flow: first WebRequestCollector, then trigger attack wave detection
122+
// We need to trigger attack waves by making multiple requests
123+
for (int i = 0; i < 15; i++) {
124+
Context.set(attackWaveCtx);
125+
WebRequestCollector.report(attackWaveCtx);
126+
}
127+
128+
// Now call WebResponseCollector which should detect the attack wave WITH user info
129+
Context.set(attackWaveCtx);
130+
WebResponseCollector.report(200);
131+
132+
// Verify attack wave was detected
133+
assertTrue(AttackQueue.getSize() > 0, "Attack wave should be detected");
134+
assertEquals(1, StatisticsStore.getStatsRecord().requests().attackWaves().total());
135+
136+
// Get the attack wave event and verify user information is captured
137+
DetectedAttackWave.DetectedAttackWaveEvent event =
138+
(DetectedAttackWave.DetectedAttackWaveEvent) AttackQueue.get();
139+
140+
assertNotNull(event);
141+
assertEquals("detected_attack_wave", event.type());
142+
assertEquals("192.168.1.1", event.request().ipAddress());
143+
assertEquals("web", event.request().source());
144+
145+
// The key assertion: user information should be present
146+
assertNotNull(event.attack().user(), "User should be captured in attack wave event");
147+
assertEquals("user123", event.attack().user().id());
148+
assertEquals("John Doe", event.attack().user().name());
149+
}
150+
151+
@Test
152+
public void testAttackWaveDetectionWithoutUser() throws SQLException, InterruptedException {
153+
// Setup
154+
ServiceConfigStore.updateFromAPIResponse(emptyAPIResponse);
155+
ServiceConfigStore.updateFromAPIListsResponse(emptyAPIListsResponse);
156+
157+
// Create attack wave context without user
158+
ContextObject attackWaveCtx = new EmptySampleContextObject("/wp-config.php", "BADMETHOD", Map.of());
159+
160+
// Trigger attack waves by making multiple requests
161+
for (int i = 0; i < 15; i++) {
162+
Context.set(attackWaveCtx);
163+
WebRequestCollector.report(attackWaveCtx);
164+
}
165+
166+
// Call WebResponseCollector which should detect the attack wave without user info
167+
Context.set(attackWaveCtx);
168+
WebResponseCollector.report(200);
169+
170+
// Verify attack wave was detected
171+
assertTrue(AttackQueue.getSize() > 0, "Attack wave should be detected");
172+
173+
// Get the attack wave event
174+
DetectedAttackWave.DetectedAttackWaveEvent event =
175+
(DetectedAttackWave.DetectedAttackWaveEvent) AttackQueue.get();
176+
177+
assertNotNull(event);
178+
assertEquals("detected_attack_wave", event.type());
179+
180+
// User should be null when not set
181+
assertNull(event.attack().user(), "User should be null when not set during request");
182+
}
95183
}
96184

0 commit comments

Comments
 (0)