diff --git a/apps/database-abstractor/src/main/java/com/akto/action/DbAction.java b/apps/database-abstractor/src/main/java/com/akto/action/DbAction.java index 46b06f18c6..0f5a3c3324 100644 --- a/apps/database-abstractor/src/main/java/com/akto/action/DbAction.java +++ b/apps/database-abstractor/src/main/java/com/akto/action/DbAction.java @@ -57,6 +57,7 @@ import com.akto.dto.usage.MetricTypes; import com.akto.log.LoggerMaker; import com.akto.log.LoggerMaker.LogDb; +import com.akto.dao.testing.TestingRunResultDao; import com.akto.util.enums.GlobalEnums.TestErrorSource; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -77,9 +78,11 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; public class DbAction extends ActionSupport { static final ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); + static final ScheduledExecutorService apiErrorsService = Executors.newScheduledThreadPool(10); long count; List customDataTypes; List aktoDataTypes; @@ -115,6 +118,27 @@ public class DbAction extends ActionSupport { private ModuleInfo moduleInfo; private static final LoggerMaker loggerMaker = new LoggerMaker(DbAction.class, LogDb.DB_ABS); + public static final String REGEX_429 = "\"statusCode\"\\s*:\\s*429"; + public static final String REGEX_5XX = "\"statusCode\"\\s*:\\s*5[0-9][0-9]"; + public static final String REGEX_CLOUDFLARE = + "(?i)\"responsePayload\"\\s*:\\s*.*(" + + // ==== CLOUDFLARE BRANDED BLOCKING PAGES ==== + // Reference: https://developers.cloudflare.com/fundamentals/reference/under-attack-mode/ + // Matches official CF blocking page titles/messages + "attention\\s+required.*cloudflare|" + + "cloudflare.*security\\s+check|" + + + // ==== WAF EXPLICIT BLOCKING ==== + // Reference: https://developers.cloudflare.com/waf/ + // Only matches explicit WAF blocking messages, not normal errors + "blocked\\s+by\\s+cloudflare\\s+waf|" + + "cloudflare\\s+waf.*blocked" + + ")"; + + // Precompile regex patterns to avoid recompiling on every check + private static final Pattern PATTERN_CLOUDFLARE = Pattern.compile(REGEX_CLOUDFLARE, Pattern.CASE_INSENSITIVE); + private static final Pattern PATTERN_429 = Pattern.compile(REGEX_429); + private static final Pattern PATTERN_5XX = Pattern.compile(REGEX_5XX); public List getWritesForTestingRunIssues() { return writesForTestingRunIssues; @@ -1887,7 +1911,6 @@ public String insertTestingRunResultSummary() { public String insertTestingRunResults() { try { - Map data = new HashMap<>(); try { if (this.testingRunResult != null && this.testingRunResult.get("workflowTest") != null) { @@ -1902,23 +1925,26 @@ public String insertTestingRunResults() { } } } catch (Exception e) { - loggerMaker.errorAndAddToDb(e, "Error in insertTestingRunResults mapNodeIdToWorkflowNodeDetails" + e.toString()); + loggerMaker.errorAndAddToDb(e, + "Error in insertTestingRunResults mapNodeIdToWorkflowNodeDetails" + e.toString()); e.printStackTrace(); } - TestingRunResult testingRunResult = objectMapper.readValue(this.testingRunResult.toJson(), TestingRunResult.class); + TestingRunResult testingRunResult = objectMapper.readValue(this.testingRunResult.toJson(), + TestingRunResult.class); try { if (!data.isEmpty()) { testingRunResult.getWorkflowTest().setMapNodeIdToWorkflowNodeDetails(data); } } catch (Exception e) { - loggerMaker.errorAndAddToDb(e, "Error in insertTestingRunResults mapNodeIdToWorkflowNodeDetails2" + e.toString()); + loggerMaker.errorAndAddToDb(e, + "Error in insertTestingRunResults mapNodeIdToWorkflowNodeDetails2" + e.toString()); e.printStackTrace(); } - if(testingRunResult.getSingleTestResults()!=null){ + if (testingRunResult.getSingleTestResults() != null) { testingRunResult.setTestResults(new ArrayList<>(testingRunResult.getSingleTestResults())); - }else if(testingRunResult.getMultiExecTestResults() !=null){ + } else if (testingRunResult.getMultiExecTestResults() != null) { testingRunResult.setTestResults(new ArrayList<>(testingRunResult.getMultiExecTestResults())); } @@ -1932,7 +1958,83 @@ public String insertTestingRunResults() { testingRunResult.setTestRunResultSummaryId(id); } + // insert immediately without apiErrors to keep the insert fast and keep the field absent when not needed DbLayer.insertTestingRunResults(testingRunResult); + + // submit async job to compute apiErrors and update only if any error flag > 0 + final TestingRunResult trrForAsync = testingRunResult; + final int capturedAccountId = (Context.accountId.get() != null) ? Context.accountId.get() : 0; + if (capturedAccountId != 0) { + apiErrorsService.submit(() -> { + Context.accountId.set(capturedAccountId); + try { + Map apiErrors = new HashMap<>(); + int cloudflareErrors = 0; + int rateLimit429 = 0; + int server5xx = 0; + + try { + List trs = trrForAsync.getTestResults(); + if (trs != null && !trs.isEmpty()) { + GenericTestResult last = trs.get(trs.size() - 1); + String message = null; + try { + if (last instanceof TestResult) { + message = ((TestResult) last).getMessage(); + } else if (last instanceof MultiExecTestResult) { + MultiExecTestResult multiResult = (MultiExecTestResult) last; + Map nodeResultMap = multiResult + .getNodeResultMap(); + List executionOrder = multiResult.getExecutionOrder(); + if (nodeResultMap != null && executionOrder != null && !executionOrder.isEmpty()) { + String lastNodeId = executionOrder.get(executionOrder.size() - 1); + WorkflowTestResult.NodeResult lastNodeResult = nodeResultMap.get(lastNodeId); + if (lastNodeResult != null) { + message = lastNodeResult.getMessage(); + } + } + } + } catch (Exception ig) { + loggerMaker.errorAndAddToDb(ig, "Error while extracting message from test result (async): " + ig.toString()); + } + if (message != null) { + if (PATTERN_CLOUDFLARE.matcher(message).find()) { + cloudflareErrors = 1; + } + if (PATTERN_429.matcher(message).find()) { + rateLimit429 = 1; + } + if (PATTERN_5XX.matcher(message).find()) { + server5xx = 1; + } + } + } + } catch (Exception ig) { + loggerMaker.errorAndAddToDb(ig, "Error while analyzing test results for apiErrors (async): " + ig.toString()); + } + + apiErrors.put("cloudflare", cloudflareErrors); + apiErrors.put("429", rateLimit429); + apiErrors.put("5xx", server5xx); + + // only update if any error flag is > 0 (keeps field absent when not needed, allows $exists queries) + if (cloudflareErrors + rateLimit429 + server5xx > 0) { + try { + // set apiErrors field on the existing document + org.bson.conversions.Bson filter = Filters.eq(Constants.ID, trrForAsync.getId()); + org.bson.conversions.Bson update = Updates.set(TestingRunResult.API_ERRORS, apiErrors); + TestingRunResultDao.instance.getMCollection().updateOne(filter, update); + } catch (Exception updateEx) { + loggerMaker.errorAndAddToDb(updateEx, "Error updating apiErrors asynchronously: " + updateEx.toString()); + } + } + } catch (Exception t) { + loggerMaker.errorAndAddToDb(t, "Unexpected error in async apiErrors computation: " + t.toString()); + } finally { + try { Context.accountId.remove(); } catch (Exception ignore) {} + } + }); + } } catch (Exception e) { loggerMaker.errorAndAddToDb(e, "Error in insertTestingRunResults " + e.toString()); if (kafkaUtils.isWriteEnabled()) { diff --git a/libs/dao/src/main/java/com/akto/dao/testing/TestingRunResultDao.java b/libs/dao/src/main/java/com/akto/dao/testing/TestingRunResultDao.java index 6f39a8a548..220e0a1169 100644 --- a/libs/dao/src/main/java/com/akto/dao/testing/TestingRunResultDao.java +++ b/libs/dao/src/main/java/com/akto/dao/testing/TestingRunResultDao.java @@ -104,6 +104,9 @@ public void createIndicesIfAbsent() { MCollection.createIndexIfAbsent(getDBName(), getCollName(), new String[]{TestingRunResult.END_TIMESTAMP}, false); + // add index for apiErrors map field for querying error flags/counts + MCollection.createIndexIfAbsent(getDBName(), getCollName(), new String[]{TestingRunResult.API_ERRORS}, false); + String[] fieldNames = new String[]{TestingRunResult.END_TIMESTAMP, TestResult.TEST_RESULTS_ERRORS}; MCollection.createIndexIfAbsent(getDBName(), getCollName(), fieldNames, false); diff --git a/libs/dao/src/main/java/com/akto/dto/testing/TestingRunResult.java b/libs/dao/src/main/java/com/akto/dto/testing/TestingRunResult.java index ec1a83b39a..c2fceb0de9 100644 --- a/libs/dao/src/main/java/com/akto/dto/testing/TestingRunResult.java +++ b/libs/dao/src/main/java/com/akto/dto/testing/TestingRunResult.java @@ -58,6 +58,9 @@ public class TestingRunResult implements Comparable { public static final String ERRORS_LIST = "errorsList"; private List errorsList; + public static final String API_ERRORS = "apiErrors"; + private Map apiErrors; + public static final String REQUIRES_CONFIG = TEST_RESULTS + ".0." + TestResult.REQUIRES_CONFIG; private WorkflowTest workflowTest; @@ -383,4 +386,12 @@ public void setMultiExecTestResults(List multiExecTestResul this.multiExecTestResults = multiExecTestResults; } + public Map getApiErrors() { + return apiErrors; + } + + public void setApiErrors(Map apiErrors) { + this.apiErrors = apiErrors; + } + }