Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/end2end.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
- { name: JavalinMySQLKotlin, test_file: end2end/javalin_mysql_kotlin.py, db: mysql_database }
- { name: SpringBoot2.7Postgres, test_file: end2end/spring_boot_2.7_postgres.py, db: postgres_database }
- { name: SpringBootHyperSQL, test_file: end2end/spring_boot_hypersql.py, db: "" }
- { name: RingClojurePostgres, test_file: end2end/ring_clojure_postgres.py, db: postgres_database }
java-version: [17, 18, 19, 20, 21, 24, 25]
distribution: ['adopt', 'corretto', 'oracle']
exclude:
Expand Down Expand Up @@ -82,6 +83,12 @@ jobs:
- name: Install Python dependencies
run: python -m pip install -r end2end/requirements.txt

- name: Install Leiningen
if: matrix.app.name == 'RingClojurePostgres'
run: |
curl -sSL -o /usr/local/bin/lein https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein
chmod +x /usr/local/bin/lein

- name: Build Application (ensures cache)
working-directory: ./sample-apps/${{ matrix.app.name }}
run: |
Expand Down
1 change: 1 addition & 0 deletions agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
compileOnly 'io.projectreactor.netty:reactor-netty-http:1.2.1' // For Spring Webflux
compileOnly 'io.javalin:javalin:6.4.0'
compileOnly 'org.springframework:spring-web:5.3.20'
compileOnly 'com.google.code.gson:gson:2.11.0'
}

shadowJar {
Expand Down
6 changes: 5 additions & 1 deletion agent/src/main/java/dev/aikido/agent/Wrappers.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import dev.aikido.agent.wrappers.file.FileConstructorSingleArgumentWrapper;
import dev.aikido.agent.wrappers.javalin.*;
import dev.aikido.agent.wrappers.jdbc.*;
import dev.aikido.agent.wrappers.ring.RingJettyServletWrapper;
import dev.aikido.agent.wrappers.ring.RingRequestBodyWrapper;
import dev.aikido.agent.wrappers.spring.SpringMVCJavaxWrapper;
import dev.aikido.agent.wrappers.spring.SpringWebfluxWrapper;
import dev.aikido.agent.wrappers.spring.SpringControllerWrapper;
Expand Down Expand Up @@ -42,6 +44,8 @@ private Wrappers() {}
new JavalinDataWrapper(),
new JavalinContextClearWrapper(),
new SQLiteWrapper(),
new HyperSQLWrapper()
new HyperSQLWrapper(),
new RingJettyServletWrapper(),
new RingRequestBodyWrapper()
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package dev.aikido.agent.wrappers.ring;

import dev.aikido.agent.wrappers.Wrapper;
import dev.aikido.agent_api.collectors.WebRequestCollector;
import dev.aikido.agent_api.collectors.WebResponseCollector;
import dev.aikido.agent_api.context.ContextObject;
import dev.aikido.agent_api.context.RingContextObject;
import dev.aikido.agent_api.helpers.logging.LogManager;
import dev.aikido.agent_api.helpers.logging.Logger;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

import java.lang.reflect.Executable;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;

import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
import static net.bytebuddy.matcher.ElementMatchers.*;

public class RingJettyServletWrapper implements Wrapper {
public static final Logger logger = LogManager.getLogger(RingJettyServletWrapper.class);

@Override
public String getName() {
return RingJettyAdvice.class.getName();
}

@Override
public ElementMatcher<? super MethodDescription> getMatcher() {
return named("doHandle").and(takesArguments(4));
}

@Override
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
return hasSuperType(named("org.eclipse.jetty.ee9.servlet.ServletHandler"));
}

public static class RingJettyAdvice {
public record SkipOnWrapper(HttpServletResponse response) {}

@Advice.OnMethodEnter(skipOn = SkipOnWrapper.class, suppress = Throwable.class)
public static Object interceptOnEnter(
@Advice.Origin Executable method,
@Advice.Argument(value = 2, typing = DYNAMIC, optional = true) HttpServletRequest request,
@Advice.Argument(value = 3, typing = DYNAMIC, optional = true) HttpServletResponse response) throws Throwable {
if (request == null) {
return response;
}

HashMap<String, Enumeration<String>> headersMap = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames != null && headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
headersMap.put(headerName, request.getHeaders(headerName));
}

HashMap<String, List<String>> cookiesMap = new HashMap<>();
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (!cookiesMap.containsKey(cookie.getName())) {
cookiesMap.put(cookie.getName(), new ArrayList<>());
}
cookiesMap.get(cookie.getName()).add(cookie.getValue());
}
}

ContextObject contextObject = new RingContextObject(
request.getMethod(), request.getRequestURL(), request.getRemoteAddr(),
request.getParameterMap(), cookiesMap, headersMap, request.getQueryString()
);

WebRequestCollector.Res res = WebRequestCollector.report(contextObject);
if (res != null) {
logger.trace("Writing a new response");
response.setStatus(res.status());
response.setContentType("text/plain");
response.getWriter().write(res.msg());
return new SkipOnWrapper(response);
}
return response;
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void interceptOnExit(@Advice.Enter Object response) {
if (response instanceof HttpServletResponse httpServletResponse) {
WebResponseCollector.report(httpServletResponse.getStatus());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package dev.aikido.agent.wrappers.ring;

import com.google.gson.Gson;
import dev.aikido.agent.wrappers.Wrapper;
import dev.aikido.agent_api.context.Context;
import dev.aikido.agent_api.context.ContextObject;
import dev.aikido.agent_api.helpers.logging.LogManager;
import dev.aikido.agent_api.helpers.logging.Logger;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;

import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
import static net.bytebuddy.matcher.ElementMatchers.*;

public class RingRequestBodyWrapper implements Wrapper {
public static final Logger logger = LogManager.getLogger(RingRequestBodyWrapper.class);

@Override
public String getName() {
return RingRequestBodyAdvice.class.getName();
}

@Override
public ElementMatcher<? super MethodDescription> getMatcher() {
return named("getInputStream").and(takesArguments(0));
}

@Override
public ElementMatcher<? super TypeDescription> getTypeMatcher() {
return nameContains("org.eclipse.jetty.ee9.nested")
.and(hasSuperType(named("jakarta.servlet.http.HttpServletRequest")));
}

public static class BufferedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream backing;

public BufferedServletInputStream(byte[] body) {
this.backing = new ByteArrayInputStream(body);
}

@Override
public boolean isFinished() {
return backing.available() == 0;
}

@Override
public boolean isReady() {
return true;
}

@Override
public void setReadListener(ReadListener readListener) {
}

@Override
public int read() {
return backing.read();
}
}

public static class RingRequestBodyAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void interceptOnExit(
@Advice.Return(readOnly = false, typing = DYNAMIC) ServletInputStream returnValue) throws Throwable {
if (returnValue == null) {
return;
}
byte[] bodyBytes = returnValue.readAllBytes();

ContextObject ctx = Context.get();
if (ctx != null && bodyBytes.length > 0) {
try {
Object parsedBody = new Gson().fromJson(new String(bodyBytes, StandardCharsets.UTF_8), Object.class);
ctx.setBody(parsedBody);
} catch (Throwable t) {
logger.debug("RingRequestBodyWrapper failed to parse JSON body: %s", t.getMessage());
}
}

returnValue = new BufferedServletInputStream(bodyBytes);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package dev.aikido.agent_api.context;

import java.util.*;

import static dev.aikido.agent_api.helpers.net.ProxyForwardedParser.getIpFromRequest;
import static dev.aikido.agent_api.helpers.url.BuildRouteFromUrl.buildRouteFromUrl;

public class RingContextObject extends ContextObject {
public RingContextObject(
String method, StringBuffer url, String rawIp, Map<String, String[]> queryParams,
HashMap<String, List<String>> cookies, HashMap<String, Enumeration<String>> headers, String queryString
) {
this.method = method;
if (url != null) {
this.url = url.toString();
if (queryString != null && !queryString.isEmpty()) {
this.url = this.url + "?" + queryString;
}
}
this.query = extractQueryParameters(queryParams);
this.cookies = cookies;
this.headers = extractHeaders(headers);
this.route = buildRouteFromUrl(this.url);
this.remoteAddress = getIpFromRequest(rawIp, this.headers);
this.source = "Ring";
this.redirectStartNodes = new ArrayList<>();
}

private static HashMap<String, List<String>> extractHeaders(HashMap<String, Enumeration<String>> headers) {
HashMap<String, List<String>> extractedHeaders = new HashMap<>();
for (Map.Entry<String, Enumeration<String>> entry : headers.entrySet()) {
List<String> values = new ArrayList<>();
Enumeration<String> valuesEnum = entry.getValue();
while (valuesEnum.hasMoreElements()) {
values.add(valuesEnum.nextElement());
}
extractedHeaders.put(entry.getKey().toLowerCase(), values);
}
return extractedHeaders;
}

private static HashMap<String, List<String>> extractQueryParameters(Map<String, String[]> parameterMap) {
HashMap<String, List<String>> query = new HashMap<>();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
query.put(entry.getKey(), Arrays.asList(entry.getValue()));
}
return query;
}
}
88 changes: 88 additions & 0 deletions agent_api/src/test/java/context/RingContextObjectTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package context;

import dev.aikido.agent_api.context.RingContextObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.*;

import static org.junit.jupiter.api.Assertions.*;

class RingContextObjectTest {

private RingContextObject contextObject;

@BeforeEach
void setUp() {
contextObject = new RingContextObject(
"GET", new StringBuffer("http://localhost/test"), "192.168.1.1", Map.of(), new HashMap<>(), new HashMap<>(), null
);
}

@Test
void testConstructor() {
assertEquals("GET", contextObject.getMethod());
assertEquals("http://localhost/test", contextObject.getUrl());
assertEquals("192.168.1.1", contextObject.getRemoteAddress());
assertEquals("Ring", contextObject.getSource());
}

@Test
void testGetRouteWithSlash() {
contextObject = new RingContextObject(
"GET", new StringBuffer("http://localhost/test"), "192.168.1.1", Map.of(), new HashMap<>(), new HashMap<>(), "a=b"
);

assertEquals("http://localhost/test?a=b", contextObject.getUrl());
assertEquals("/test", contextObject.getRoute());
}

@Test
void testGetRouteWithNumbers() {
contextObject = new RingContextObject(
"GET", new StringBuffer("http://localhost/api/dog/28632"), "192.168.1.1", Map.of(), new HashMap<>(), new HashMap<>(), ""
);

assertEquals("http://localhost/api/dog/28632", contextObject.getUrl());
assertEquals("/api/dog/:number", contextObject.getRoute());
}

@Test
void testQueryParametersExtraction() {
Map<String, String[]> queryParams = new HashMap<>();
queryParams.put("param1", new String[]{"value1"});

contextObject = new RingContextObject(
"GET", new StringBuffer("http://localhost/test"), "192.168.1.1", queryParams, new HashMap<>(), new HashMap<>(), null
);

assertEquals(1, contextObject.getQuery().size());
assertEquals("value1", contextObject.getQuery().get("param1").get(0));
}

@Test
void testCookiesExtraction() {
HashMap<String, List<String>> cookies = new HashMap<>();
cookies.put("sessionId", List.of("abc123"));

contextObject = new RingContextObject(
"GET", new StringBuffer("http://localhost/test"), "192.168.1.1", Map.of(), cookies, new HashMap<>(), null
);

assertEquals(1, contextObject.getCookies().size());
assertEquals("abc123", contextObject.getCookies().get("sessionId").get(0));
}

@Test
void testHeadersExtraction() {
Vector<String> contentTypeValues = new Vector<>(List.of("application/json"));
HashMap<String, Enumeration<String>> headers = new HashMap<>();
headers.put("Content-Type", contentTypeValues.elements());

contextObject = new RingContextObject(
"GET", new StringBuffer("http://localhost/test"), "192.168.1.1", Map.of(), new HashMap<>(), headers, null
);

assertEquals("application/json", contextObject.getHeader("content-type"));
}
}
10 changes: 10 additions & 0 deletions end2end/ring_clojure_postgres.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from utils import App, Request

ring_clojure_postgres_app = App(8102)

ring_clojure_postgres_app.add_payload("sql",
safe_request=Request("/api/create", body={"name": "Bobby"}),
unsafe_request=Request("/api/create", body={"name": "Malicious Pet', 'Gru from the Minions') -- "})
)

ring_clojure_postgres_app.test_all_payloads()
Loading
Loading