From bb52fa371d2410dd40d8232b2418943bb45eca6f Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 17 Nov 2025 14:56:27 +0100 Subject: [PATCH 1/4] If context is null dont try and create a request data object --- .../cloud/api/events/DetectedAttack.java | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/events/DetectedAttack.java b/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/events/DetectedAttack.java index 15d82822..0865a2e8 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/events/DetectedAttack.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/events/DetectedAttack.java @@ -5,7 +5,6 @@ import dev.aikido.agent_api.context.User; import dev.aikido.agent_api.vulnerabilities.Attack; -import java.util.List; import java.util.Map; import static dev.aikido.agent_api.background.cloud.GetManagerInfo.getManagerInfo; @@ -46,7 +45,20 @@ public record AttackData ( public static DetectedAttackEvent createAPIEvent(Attack attack, ContextObject context) { boolean blocking = getConfig().isBlockingEnabled(); - RequestData requestData = new RequestData( + return new DetectedAttackEvent( + "detected_attack", // type + buildRequestData(context), // request + buildAttackData(attack, blocking), // attack + getManagerInfo(), // agent + getUnixTimeMS() // time + ); + } + + private static RequestData buildRequestData(ContextObject context) { + if (context == null) { + return null; + } + return new RequestData( context.getMethod(), context.getRemoteAddress(), context.getHeader("user-agent"), @@ -54,16 +66,20 @@ public static DetectedAttackEvent createAPIEvent(Attack attack, ContextObject co context.getSource(), context.getRoute() ); - AttackData attackData = new AttackData( - attack.kind, attack.operation, attack.source, attack.pathToPayload, attack.payload, attack.metadata, - "module", blocking, attack.stack, attack.user - ); - return new DetectedAttackEvent( - "detected_attack", // type - requestData, // request - attackData, // attack - getManagerInfo(), // agent - getUnixTimeMS() // time + } + + private static AttackData buildAttackData(Attack attack, boolean blocking) { + return new AttackData( + attack.kind, + attack.operation, + attack.source, + attack.pathToPayload, + attack.payload, + attack.metadata, + "module", + blocking, + attack.stack, + attack.user ); } } From 40ccf1038ba2f6cb059e901b2975bcb369a824da Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 17 Nov 2025 15:02:37 +0100 Subject: [PATCH 2/4] Add tests to DetectedAttack with context set to null --- .../cloud/api/DetectedAttackTest.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 agent_api/src/test/java/background/cloud/api/DetectedAttackTest.java diff --git a/agent_api/src/test/java/background/cloud/api/DetectedAttackTest.java b/agent_api/src/test/java/background/cloud/api/DetectedAttackTest.java new file mode 100644 index 00000000..c82f7043 --- /dev/null +++ b/agent_api/src/test/java/background/cloud/api/DetectedAttackTest.java @@ -0,0 +1,94 @@ +package background.cloud.api; + +import dev.aikido.agent_api.background.cloud.api.events.DetectedAttack; +import dev.aikido.agent_api.context.ContextObject; +import dev.aikido.agent_api.vulnerabilities.Attack; +import dev.aikido.agent_api.vulnerabilities.Vulnerabilities; +import org.junit.jupiter.api.Test; +import utils.EmptySampleContextObject; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class DetectedAttackTest { + + @Test + void createAPIEvent_WithValidContextAndAttack_ReturnsDetectedAttackEvent() { + // Arrange + ContextObject context = new EmptySampleContextObject("test", "/api/resource", "POST"); + + Map metadata = new HashMap<>(); + metadata.put("key", "value"); + + Attack attack = new Attack( + "read", + new Vulnerabilities.SQLInjectionVulnerability(), + "web", + "/api/resource", + metadata, + "test_payload", + "stack_trace", + null + ); + + // Act + DetectedAttack.DetectedAttackEvent event = DetectedAttack.createAPIEvent(attack, context); + + // Assert + assertNotNull(event); + assertEquals("detected_attack", event.type()); + assertNotNull(event.request()); + assertEquals("POST", event.request().method()); + assertEquals("web", event.request().source()); + assertEquals("/api/resource", event.request().route()); + assertNotNull(event.attack()); + assertEquals("sql_injection", event.attack().kind()); + assertEquals("read", event.attack().operation()); + assertEquals("web", event.attack().source()); + assertEquals("/api/resource", event.attack().path()); + assertEquals("test_payload", event.attack().payload()); + assertEquals(metadata, event.attack().metadata()); + assertEquals("module", event.attack().module()); + assertEquals("stack_trace", event.attack().stack()); + assertNull(event.attack().user()); + assertNotNull(event.agent()); + assertTrue(event.time() > 0); + } + + @Test + void createAPIEvent_WithNullContext_ReturnsDetectedAttackEventWithNullRequest() { + // Arrange + Attack attack = new Attack( + "read", + new Vulnerabilities.SQLInjectionVulnerability(), + "web", + "/api/resource", + Map.of("key", "value"), + "test_payload", + "stack_trace", + null + ); + + // Act + DetectedAttack.DetectedAttackEvent event = DetectedAttack.createAPIEvent(attack, null); + + // Assert + assertNotNull(event); + assertEquals("detected_attack", event.type()); + assertNull(event.request()); + assertNotNull(event.attack()); + assertEquals("sql_injection", event.attack().kind()); + assertEquals("read", event.attack().operation()); + assertEquals("web", event.attack().source()); + assertEquals("/api/resource", event.attack().path()); + assertEquals("test_payload", event.attack().payload()); + assertEquals(Map.of("key", "value"), event.attack().metadata()); + assertEquals("module", event.attack().module()); + assertEquals("stack_trace", event.attack().stack()); + assertNull(event.attack().user()); + assertNotNull(event.agent()); + assertTrue(event.time() > 0); + } +} From 95c72aa048a0a0d3c1458cb4d4a2f21892dd1bd8 Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Mon, 17 Nov 2025 15:07:45 +0100 Subject: [PATCH 3/4] Add extra test case for IMDS but with no context set --- .../collectors/DNSRecordCollectorTest.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java index 70908b11..95814a3d 100644 --- a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java +++ b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java @@ -1,11 +1,14 @@ package collectors; +import dev.aikido.agent_api.background.cloud.api.events.DetectedAttack; import dev.aikido.agent_api.collectors.DNSRecordCollector; import dev.aikido.agent_api.context.Context; import dev.aikido.agent_api.context.ContextObject; +import dev.aikido.agent_api.storage.AttackQueue; import dev.aikido.agent_api.storage.Hostnames; import dev.aikido.agent_api.storage.HostnamesStore; import dev.aikido.agent_api.storage.ServiceConfigStore; +import dev.aikido.agent_api.vulnerabilities.Attack; import dev.aikido.agent_api.vulnerabilities.ssrf.SSRFException; import dev.aikido.agent_api.vulnerabilities.ssrf.StoredSSRFException; import org.junit.jupiter.api.*; @@ -29,12 +32,14 @@ void setup() throws UnknownHostException { inetAddress1 = InetAddress.getByName("1.1.1.1"); inetAddress2 = InetAddress.getByName("127.0.0.1"); imdsAddress1 = InetAddress.getByName("169.254.169.254"); + AttackQueue.clear(); } @AfterEach public void cleanup() { HostnamesStore.clear(); Context.set(null); + AttackQueue.clear(); } @Test @@ -129,4 +134,31 @@ public void testHostnameSameWithContextAsAStoredSSRFAttack() { }); }); } + + @Test + public void testStoredSSRFWithNoContext() throws InterruptedException { + ServiceConfigStore.updateBlocking(true); + + Context.set(null); + + Exception exception = assertThrows(StoredSSRFException.class, () -> { + DNSRecordCollector.report("dev.aikido", new InetAddress[]{ + imdsAddress1, inetAddress2 + }); + }); + DetectedAttack.DetectedAttackEvent event = (DetectedAttack.DetectedAttackEvent) AttackQueue.get(); + assertEquals("stored_ssrf", event.attack().kind()); + assertNull(event.request()); + + assertEquals("Aikido Zen has blocked a stored server-side request forgery", exception.getMessage()); + + assertDoesNotThrow(() -> { + DNSRecordCollector.report("metadata.goog", new InetAddress[]{ + imdsAddress1, inetAddress2 + }); + DNSRecordCollector.report("metadata.google.internal", new InetAddress[]{ + imdsAddress1, inetAddress2 + }); + }); + } } From 6fa50aa98f98f8b8052bcfd7bfdddfb042d6a88b Mon Sep 17 00:00:00 2001 From: BitterPanda Date: Fri, 21 Nov 2025 11:12:53 +0100 Subject: [PATCH 4/4] format: fix spacing in creation of DetectedAttackEvent --- .../agent_api/background/cloud/api/events/DetectedAttack.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/events/DetectedAttack.java b/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/events/DetectedAttack.java index 0865a2e8..a118c7a7 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/events/DetectedAttack.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/background/cloud/api/events/DetectedAttack.java @@ -46,7 +46,7 @@ public record AttackData ( public static DetectedAttackEvent createAPIEvent(Attack attack, ContextObject context) { boolean blocking = getConfig().isBlockingEnabled(); return new DetectedAttackEvent( - "detected_attack", // type + "detected_attack", // type buildRequestData(context), // request buildAttackData(attack, blocking), // attack getManagerInfo(), // agent