diff --git a/apps/dashboard/src/main/java/com/akto/action/test_editor/SaveTestEditorAction.java b/apps/dashboard/src/main/java/com/akto/action/test_editor/SaveTestEditorAction.java index aa2292b61b..b45a083aa6 100644 --- a/apps/dashboard/src/main/java/com/akto/action/test_editor/SaveTestEditorAction.java +++ b/apps/dashboard/src/main/java/com/akto/action/test_editor/SaveTestEditorAction.java @@ -29,6 +29,7 @@ import com.akto.dto.testing.GenericTestResult; import com.akto.dto.testing.MultiExecTestResult; import com.akto.dto.testing.TestResult; +import com.akto.dto.testing.TestingRunConfig; import com.akto.dto.testing.TestResult.Confidence; import com.akto.dto.testing.TestingRunResult; import com.akto.dto.testing.WorkflowNodeDetails; @@ -329,7 +330,8 @@ public String runTestForGivenTemplate() { List testLogs = new ArrayList<>(); int lastSampleIndex = sampleDataList.get(0).getSamples().size() - 1; - testingRunResult = executor.runTestNew(infoKey, null, testingUtil, null, testConfig, null, true, testLogs); + TestingRunConfig testingRunConfig = new TestingRunConfig(); + testingRunResult = executor.runTestNew(infoKey, null, testingUtil, null, testConfig, testingRunConfig, true, testLogs); if (testingRunResult == null) { testingRunResult = new TestingRunResult( new ObjectId(), infoKey, testConfig.getInfo().getCategory().getName(), testConfig.getInfo().getSubCategory() ,Collections.singletonList(new TestResult(null, sampleDataList.get(0).getSamples().get(lastSampleIndex), diff --git a/apps/dashboard/src/main/java/com/akto/action/testing/ScriptAction.java b/apps/dashboard/src/main/java/com/akto/action/testing/ScriptAction.java new file mode 100644 index 0000000000..da9eff7fe9 --- /dev/null +++ b/apps/dashboard/src/main/java/com/akto/action/testing/ScriptAction.java @@ -0,0 +1,82 @@ +package com.akto.action.testing; + +import java.util.UUID; + +import org.apache.commons.lang3.NotImplementedException; +import org.bson.conversions.Bson; + +import com.akto.action.UserAction; +import com.akto.dao.context.Context; +import com.akto.dao.testing.config.TestScriptsDao; +import com.akto.dto.testing.config.TestScript; +import com.akto.util.DashboardMode; +import com.mongodb.BasicDBObject; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.Updates; +import com.opensymphony.xwork2.Action; + +public class ScriptAction extends UserAction { + + @Override + public String execute() throws Exception { + throw new NotImplementedException(); + } + + private TestScript testScript; + + public String addScript() { + + if (!DashboardMode.isSaasDeployment()) { + return Action.ERROR.toUpperCase(); + } + if (this.testScript == null || this.testScript.getJavascript() == null) { + return Action.ERROR.toUpperCase(); + } + + TestScriptsDao.instance.insertOne( + new TestScript( + UUID.randomUUID().toString(), + this.testScript.getJavascript(), + TestScript.Type.PRE_REQUEST, + getSUser().getLogin(), + Context.now() + ) + ); + + return Action.SUCCESS.toUpperCase(); + } + + public String fetchScript() { + this.testScript = TestScriptsDao.instance.findOne(new BasicDBObject()); + return Action.SUCCESS.toUpperCase(); + } + + public String updateScript() { + + if (!DashboardMode.isSaasDeployment()) { + return Action.ERROR.toUpperCase(); + } + + if (this.testScript == null || this.testScript.getJavascript() == null) { + return Action.ERROR.toUpperCase(); + } + + Bson filterQ = Filters.eq("_id", testScript.getId()); + Bson updateQ = + Updates.combine( + Updates.set(TestScript.JAVASCRIPT, this.testScript.getJavascript()), + Updates.set(TestScript.AUTHOR, getSUser().getLogin()), + Updates.set(TestScript.LAST_UPDATED_AT, Context.now()) + ); + TestScriptsDao.instance.updateOne(filterQ, updateQ); + return Action.SUCCESS.toUpperCase(); + } + + public TestScript getTestScript() { + return testScript; + } + + public void setTestScript(TestScript testScript) { + this.testScript = testScript; + } +} diff --git a/apps/dashboard/src/main/resources/struts.xml b/apps/dashboard/src/main/resources/struts.xml index 88e79f7ada..5d35f6fe82 100644 --- a/apps/dashboard/src/main/resources/struts.xml +++ b/apps/dashboard/src/main/resources/struts.xml @@ -6028,6 +6028,80 @@ + + + + + USER_ACTIONS + READ_WRITE + + + TEST_PRE_SCRIPT + + + + 403 + false + ^actionErrors.* + + + + + 422 + false + ^actionErrors.* + + + + + + + + + USER_ACTIONS + READ_WRITE + + + + 403 + false + ^actionErrors.* + + + + + 422 + false + ^actionErrors.* + + + + + + + + USER_ACTIONS + READ_WRITE + + + TEST_PRE_SCRIPT + + + + 403 + false + ^actionErrors.* + + + + + 422 + false + ^actionErrors.* + + + + diff --git a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/api.js b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/api.js index f89449581b..03aeedae79 100644 --- a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/api.js +++ b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/api.js @@ -406,4 +406,25 @@ export default { data: {reportId, organizationName, reportDate, reportUrl} }) }, + fetchScript() { + return request({ + url: '/api/fetchScript', + method: 'post', + data: {} + }) + }, + addScript({javascript}) { + return request({ + url: '/api/addScript', + method: 'post', + data: {testScript:{javascript}} + }) + }, + updateScript(id, javascript) { + return request({ + url: '/api/updateScript', + method: 'post', + data: {testScript:{id, javascript}} + }) + } } \ No newline at end of file diff --git a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/user_config/UserConfig.jsx b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/user_config/UserConfig.jsx index 66c7872da2..e7c30c1e8b 100644 --- a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/user_config/UserConfig.jsx +++ b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/user_config/UserConfig.jsx @@ -1,4 +1,4 @@ -import { Box, Button, Collapsible, Divider, LegacyCard, LegacyStack, Text } from "@shopify/polaris" +import { TextField, Button, Collapsible, Divider, LegacyCard, LegacyStack, Text } from "@shopify/polaris" import { ChevronRightMinor, ChevronDownMinor } from '@shopify/polaris-icons'; import { useState } from "react"; import api from "../api" @@ -20,9 +20,14 @@ function UserConfig() { const [isLoading, setIsLoading] = useState(true) const [hardcodedOpen, setHardcodedOpen] = useState(true); const [initialLimit, setInitialLimit] = useState(0); + const [preRequestScript, setPreRequestScript] = useState({javascript: ""}); const handleToggleHardcodedOpen = () => setHardcodedOpen((prev) => !prev) + const handlePreRequestScriptChange = (value) => { + setPreRequestScript({...preRequestScript, javascript: value}) + } + async function fetchAuthMechanismData() { setIsLoading(true) const authMechanismDataResponse = await api.fetchAuthMechanismData() @@ -36,6 +41,12 @@ function UserConfig() { await settingRequests.fetchAdminSettings().then((resp)=> { setInitialLimit(resp.accountSettings.globalRateLimit); }) + + await api.fetchScript().then((resp)=> { + if (resp) { + setPreRequestScript(resp.testScript) + } + }); setIsLoading(false) } @@ -43,6 +54,14 @@ function UserConfig() { fetchAuthMechanismData() }, []) + async function addOrUpdateScript() { + if (preRequestScript.id) { + api.updateScript(preRequestScript.id, preRequestScript.javascript) + } else { + api.addScript(preRequestScript) + } + } + async function handleStopAllTests() { await api.stopAllTests() setToastConfig({ isActive: true, isError: false, message: "All tests stopped!" }) @@ -121,7 +140,6 @@ function UserConfig() {
- Allowed requests / min: ) - const components = [, rateLimit] + const preRequestScriptComponent = ( + {addOrUpdateScript()} + } + + }> + + +
+ + +
+
+ + +
+ ) + + const components = [, rateLimit, preRequestScriptComponent] return ( isLoading ? diff --git a/libs/dao/src/main/java/com/akto/dao/testing/config/TestScriptsDao.java b/libs/dao/src/main/java/com/akto/dao/testing/config/TestScriptsDao.java new file mode 100644 index 0000000000..103a64a667 --- /dev/null +++ b/libs/dao/src/main/java/com/akto/dao/testing/config/TestScriptsDao.java @@ -0,0 +1,26 @@ +package com.akto.dao.testing.config; + +import com.akto.dao.*; +import com.akto.dto.testing.config.TestScript; +import com.mongodb.BasicDBObject; + +public class TestScriptsDao extends AccountsContextDao { + + public static final TestScriptsDao instance = new TestScriptsDao(); + + private TestScriptsDao() {} + + public TestScript fetchTestScript() { + return TestScriptsDao.instance.findOne(new BasicDBObject()); + } + + @Override + public String getCollName() { + return "test_collection_properties"; + } + + @Override + public Class getClassT() { + return TestScript.class; + } +} diff --git a/libs/dao/src/main/java/com/akto/dto/OriginalHttpRequest.java b/libs/dao/src/main/java/com/akto/dto/OriginalHttpRequest.java index b9de1c4901..0425dbc6eb 100644 --- a/libs/dao/src/main/java/com/akto/dto/OriginalHttpRequest.java +++ b/libs/dao/src/main/java/com/akto/dto/OriginalHttpRequest.java @@ -4,6 +4,7 @@ import com.akto.util.HttpRequestResponseUtils; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; +import com.ctc.wstx.shaded.msv_core.util.Uri; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; import com.mongodb.BasicDBObject; @@ -223,6 +224,19 @@ public String getFullUrlIncludingDomain() throws Exception { return url; } + public String getUrlPath() throws Exception { + if (!url.startsWith("http")) { + return url; + } else { + try { + URI uri = new URI(url); + return uri.getPath(); + } catch (Exception e) { + return url; + } + } + } + public String getFullUrlWithParams() { return getFullUrlWithParams(this.url, this.queryParams); } diff --git a/libs/dao/src/main/java/com/akto/dto/testing/config/TestScript.java b/libs/dao/src/main/java/com/akto/dto/testing/config/TestScript.java new file mode 100644 index 0000000000..391829043c --- /dev/null +++ b/libs/dao/src/main/java/com/akto/dto/testing/config/TestScript.java @@ -0,0 +1,122 @@ +package com.akto.dto.testing.config; +import java.util.Objects; + +import org.bson.codecs.pojo.annotations.BsonId; + +import com.akto.dao.context.Context; + +public class TestScript { + + + public enum Type { + PRE_TEST, PRE_REQUEST, POST_REQUEST, POST_TEST + } + + public static final String ID = "_id"; + @BsonId + private String id; + + public static final String JAVASCRIPT = "javascript"; + private String javascript; + + private Type type; + + public static final String AUTHOR = "author"; + private String author; + + private int lastCreatedAt; + + public static final String LAST_UPDATED_AT = "lastUpdatedAt"; + private int lastUpdatedAt; + + + public TestScript() { + } + + public TestScript(String id, String javascript, Type type, String author, int lastUpdatedAt) { + this.id = id; + this.javascript = javascript; + this.type = type; + this.author = author; + this.lastCreatedAt = Context.now(); + this.lastUpdatedAt = lastUpdatedAt; + } + + public String getJavascript() { + return this.javascript; + } + + public void setJavascript(String javascript) { + this.javascript = javascript; + } + + public Type getType() { + return this.type; + } + + public void setType(Type type) { + this.type = type; + } + + public String getAuthor() { + return this.author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public int getLastCreatedAt() { + return this.lastCreatedAt; + } + + public void setLastCreatedAt(int lastCreatedAt) { + this.lastCreatedAt = lastCreatedAt; + } + + public int getLastUpdatedAt() { + return this.lastUpdatedAt; + } + + public void setLastUpdatedAt(int lastUpdatedAt) { + this.lastUpdatedAt = lastUpdatedAt; + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof TestScript)) { + return false; + } + TestScript testScript = (TestScript) o; + return Objects.equals(javascript, testScript.javascript) && Objects.equals(type, testScript.type) && Objects.equals(author, testScript.author) && lastCreatedAt == testScript.lastCreatedAt && lastUpdatedAt == testScript.lastUpdatedAt; + } + + @Override + public int hashCode() { + return Objects.hash(javascript, type, author, lastCreatedAt, lastUpdatedAt); + } + + @Override + public String toString() { + return "{" + + " id='" + getId() + "'" + + ", javascript='" + getJavascript() + "'" + + ", type='" + getType() + "'" + + ", author='" + getAuthor() + "'" + + ", lastCreatedAt='" + getLastCreatedAt() + "'" + + ", lastUpdatedAt='" + getLastUpdatedAt() + "'" + + "}"; + } + + +} diff --git a/libs/utils/src/main/java/com/akto/testing/ApiExecutor.java b/libs/utils/src/main/java/com/akto/testing/ApiExecutor.java index d5975b3fae..0332db794c 100644 --- a/libs/utils/src/main/java/com/akto/testing/ApiExecutor.java +++ b/libs/utils/src/main/java/com/akto/testing/ApiExecutor.java @@ -1,10 +1,12 @@ package com.akto.testing; import com.akto.dao.context.Context; +import com.akto.dao.testing.config.TestScriptsDao; import com.akto.dto.OriginalHttpRequest; import com.akto.dto.OriginalHttpResponse; import com.akto.dto.testing.TestingRunConfig; import com.akto.dto.testing.TestingRunResult; +import com.akto.dto.testing.config.TestScript; import com.akto.dto.testing.rate_limit.RateLimitHandler; import com.akto.dto.type.URLMethods; import com.akto.log.LoggerMaker; @@ -12,11 +14,11 @@ import com.akto.util.Constants; import com.akto.util.HttpRequestResponseUtils; import com.akto.util.grpc.ProtoBufUtils; + import kotlin.Pair; import okhttp3.*; import okio.BufferedSink; import org.apache.commons.lang3.StringUtils; - import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -24,11 +26,19 @@ import java.net.URL; import java.util.*; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.SimpleScriptContext; +import jdk.nashorn.api.scripting.ScriptObjectMirror; + public class ApiExecutor { private static final LoggerMaker loggerMaker = new LoggerMaker(ApiExecutor.class); // Load only first 1 MiB of response body into memory. private static final int MAX_RESPONSE_SIZE = 1024*1024; + private static Map lastFetchedMap = new HashMap<>(); + private static Map testScriptMap = new HashMap<>(); private static OriginalHttpResponse common(Request request, boolean followRedirects, boolean debug, List testLogs, boolean skipSSRFCheck, String requestProtocol) throws Exception { @@ -243,6 +253,9 @@ public static OriginalHttpResponse sendRequest(OriginalHttpRequest request, bool builder = builder.url(request.getFullUrlWithParams()); + boolean executeScript = testingRunConfig != null; + calculateHashAndAddAuth(request, executeScript); + OriginalHttpResponse response = null; switch (method) { case GET: @@ -315,7 +328,72 @@ public void writeTo(BufferedSink sink) throws IOException { } + private static void calculateHashAndAddAuth(OriginalHttpRequest originalHttpRequest, boolean executeScript) { + if (!executeScript) { + loggerMaker.infoAndAddToDb("invalid context for hash calculation, returning"); + return; + } + int accountId = Context.accountId.get(); + try { + String script; + TestScript testScript = testScriptMap.getOrDefault(accountId, null); + int lastTestScriptFetched = lastFetchedMap.getOrDefault(accountId, 0); + if (Context.now() - lastTestScriptFetched > 5 * 60) { + testScript = TestScriptsDao.instance.fetchTestScript(); + lastTestScriptFetched = Context.now(); + testScriptMap.put(accountId, testScript); + lastFetchedMap.put(accountId, Context.now()); + } + if (testScript != null && testScript.getJavascript() != null) { + script = testScript.getJavascript(); + } else { + loggerMaker.infoAndAddToDb("returning from calculateHashAndAddAuth, no test script present"); + return; + } + + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName("nashorn"); + + SimpleScriptContext sctx = ((SimpleScriptContext) engine.get("context")); + sctx.setAttribute("method", originalHttpRequest.getMethod(), ScriptContext.ENGINE_SCOPE); + sctx.setAttribute("headers", originalHttpRequest.getHeaders(), ScriptContext.ENGINE_SCOPE); + sctx.setAttribute("url", originalHttpRequest.getPath(), ScriptContext.ENGINE_SCOPE); + sctx.setAttribute("payload", originalHttpRequest.getBody(), ScriptContext.ENGINE_SCOPE); + sctx.setAttribute("queryParams", originalHttpRequest.getQueryParams(), ScriptContext.ENGINE_SCOPE); + engine.eval(script); + + String method = (String) sctx.getAttribute("method"); + Map headers = (Map) sctx.getAttribute("headers"); + String url = (String) sctx.getAttribute("url"); + String payload = (String) sctx.getAttribute("payload"); + String queryParams = (String) sctx.getAttribute("queryParams"); + + Map> hs = new HashMap<>(); + for (String key: headers.keySet()) { + try { + ScriptObjectMirror scm = ((ScriptObjectMirror) headers.get(key)); + List val = new ArrayList<>(); + for (int i = 0; i < scm.size(); i++) { + val.add((String) scm.get(Integer.toString(i))); + } + hs.put(key, val); + } catch (Exception e) { + hs.put(key, (List) headers.get(key)); + } + } + + originalHttpRequest.setBody(payload); + originalHttpRequest.setMethod(method); + originalHttpRequest.setUrl(url); + originalHttpRequest.setHeaders(hs); + originalHttpRequest.setQueryParams(queryParams); + } catch (Exception e) { + loggerMaker.errorAndAddToDb("error in calculateHashAndAddAuth " + e.getMessage() + " url " + originalHttpRequest.getUrl()); + e.printStackTrace(); + return; + } + } private static OriginalHttpResponse sendWithRequestBody(OriginalHttpRequest request, Request.Builder builder, boolean followRedirects, boolean debug, List testLogs, boolean skipSSRFCheck, String requestProtocol) throws Exception { Map> headers = request.getHeaders();