Skip to content
114 changes: 108 additions & 6 deletions apps/database-abstractor/src/main/java/com/akto/action/DbAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<CustomDataTypeMapper> customDataTypes;
List<AktoDataType> aktoDataTypes;
Expand Down Expand Up @@ -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<BulkUpdates> getWritesForTestingRunIssues() {
return writesForTestingRunIssues;
Expand Down Expand Up @@ -1887,7 +1911,6 @@ public String insertTestingRunResultSummary() {

public String insertTestingRunResults() {
try {

Map<String, WorkflowNodeDetails> data = new HashMap<>();
try {
if (this.testingRunResult != null && this.testingRunResult.get("workflowTest") != null) {
Expand All @@ -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()));
}

Expand All @@ -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<String, Integer> apiErrors = new HashMap<>();
int cloudflareErrors = 0;
int rateLimit429 = 0;
int server5xx = 0;

try {
List<GenericTestResult> 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<String, WorkflowTestResult.NodeResult> nodeResultMap = multiResult
.getNodeResultMap();
List<String> 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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
11 changes: 11 additions & 0 deletions libs/dao/src/main/java/com/akto/dto/testing/TestingRunResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public class TestingRunResult implements Comparable<TestingRunResult> {
public static final String ERRORS_LIST = "errorsList";
private List<String> errorsList;

public static final String API_ERRORS = "apiErrors";
private Map<String, Integer> apiErrors;

public static final String REQUIRES_CONFIG = TEST_RESULTS + ".0." + TestResult.REQUIRES_CONFIG;

private WorkflowTest workflowTest;
Expand Down Expand Up @@ -383,4 +386,12 @@ public void setMultiExecTestResults(List<MultiExecTestResult> multiExecTestResul
this.multiExecTestResults = multiExecTestResults;
}

public Map<String, Integer> getApiErrors() {
return apiErrors;
}

public void setApiErrors(Map<String, Integer> apiErrors) {
this.apiErrors = apiErrors;
}

}