diff --git a/apps/dashboard/src/main/java/com/akto/action/testing/StartTestAction.java b/apps/dashboard/src/main/java/com/akto/action/testing/StartTestAction.java index a975ad40b1..08513459dc 100644 --- a/apps/dashboard/src/main/java/com/akto/action/testing/StartTestAction.java +++ b/apps/dashboard/src/main/java/com/akto/action/testing/StartTestAction.java @@ -24,6 +24,7 @@ import com.akto.log.LoggerMaker; import com.akto.log.LoggerMaker.LogDb; import com.akto.util.Constants; +import com.akto.util.enums.GlobalEnums; import com.akto.util.enums.GlobalEnums.TestErrorSource; import com.akto.utils.DeleteTestRunUtils; import com.akto.utils.Utils; @@ -36,10 +37,12 @@ import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import java.nio.file.DirectoryStream.Filter; import java.time.Instant; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -509,23 +512,34 @@ public String fetchTestingRunResultSummary() { } } + private static Bson vulnerableFilter = Filters.and( + Filters.eq(TestingRunResult.VULNERABLE, true), + Filters.or( + Filters.exists(TestingRunResult.IS_IGNORED_RESULT, false), + Filters.eq(TestingRunResult.IS_IGNORED_RESULT, false) + ) + + ); + private List prepareTestRunResultsFilters(ObjectId testingRunResultSummaryId, QueryMode queryMode) { List filterList = new ArrayList<>(); filterList.add(Filters.eq(TestingRunResult.TEST_RUN_RESULT_SUMMARY_ID, testingRunResultSummaryId)); if(reportFilterList != null) { Bson filtersForTestingRunResults = com.akto.action.testing.Utils.createFiltersForTestingReport(reportFilterList); - if (!filtersForTestingRunResults.equals(Filters.empty())) filterList.add(filtersForTestingRunResults); + if (!filtersForTestingRunResults.equals(Filters.empty())) { + filterList.add(filtersForTestingRunResults); + } } if(queryMode == null) { if(fetchOnlyVulnerable) { - filterList.add(Filters.eq(TestingRunResult.VULNERABLE, true)); + filterList.add(vulnerableFilter); } } else { switch (queryMode) { case VULNERABLE: - filterList.add(Filters.eq(TestingRunResult.VULNERABLE, true)); + filterList.add(vulnerableFilter); break; case SKIPPED_EXEC_API_REQUEST_FAILED: filterList.add(Filters.eq(TestingRunResult.VULNERABLE, false)); @@ -1058,6 +1072,80 @@ public String modifyTestingRunConfig(){ return SUCCESS.toUpperCase(); } + public String handleRefreshTableCount(){ + if(this.testingRunResultSummaryHexId == null || this.testingRunResultSummaryHexId.isEmpty()){ + addActionError("Invalid summary id"); + return ERROR.toUpperCase(); + } + int accountId = Context.accountId.get(); + executorService.schedule( new Runnable() { + public void run() { + Context.accountId.set(accountId); + try { + ObjectId summaryObjectId = new ObjectId(testingRunResultSummaryHexId); + List testingRunResults = TestingRunResultDao.instance.findAll( + Filters.and( + Filters.eq(TestingRunResult.TEST_RUN_RESULT_SUMMARY_ID, summaryObjectId), + vulnerableFilter + ), + Projections.include(TestingRunResult.API_INFO_KEY, TestingRunResult.TEST_SUB_TYPE) + ); + + if(testingRunResults.isEmpty()){ + return; + } + + Set issuesIds = new HashSet<>(); + Map mapIssueToResultId = new HashMap<>(); + Set ignoredResults = new HashSet<>(); + for(TestingRunResult runResult: testingRunResults){ + TestingIssuesId issuesId = new TestingIssuesId(runResult.getApiInfoKey(), TestErrorSource.AUTOMATED_TESTING , runResult.getTestSubType()); + issuesIds.add(issuesId); + mapIssueToResultId.put(issuesId, runResult.getId()); + ignoredResults.add(runResult.getId()); + } + + List issues = TestingRunIssuesDao.instance.findAll( + Filters.and( + Filters.in(Constants.ID, issuesIds), + Filters.eq(TestingRunIssues.TEST_RUN_ISSUES_STATUS, GlobalEnums.TestRunIssueStatus.OPEN) + ), Projections.include(TestingRunIssues.KEY_SEVERITY) + ); + + Map totalCountIssues = new HashMap<>(); + totalCountIssues.put("HIGH", 0); + totalCountIssues.put("MEDIUM", 0); + totalCountIssues.put("LOW", 0); + + for(TestingRunIssues runIssue: issues){ + int initCount = totalCountIssues.getOrDefault(runIssue.getSeverity().name(), 0); + totalCountIssues.put(runIssue.getSeverity().name(), initCount + 1); + if(mapIssueToResultId.containsKey(runIssue.getId())){ + ObjectId resId = mapIssueToResultId.get(runIssue.getId()); + ignoredResults.remove(resId); + } + } + + // update testing run result summary + TestingRunResultSummariesDao.instance.updateOne( + Filters.eq(Constants.ID, summaryObjectId), + Updates.set(TestingRunResultSummary.COUNT_ISSUES, totalCountIssues) + ); + + // update testing run results, by setting them isIgnored true + TestingRunResultDao.instance.updateMany( + Filters.in(Constants.ID, ignoredResults), + Updates.set(TestingRunResult.IS_IGNORED_RESULT, true) + ); + } catch (Exception e) { + e.printStackTrace(); + } + } + }, 0 , TimeUnit.SECONDS); + + return SUCCESS.toUpperCase(); + } + public void setType(TestingEndpoints.Type type) { this.type = type; diff --git a/apps/dashboard/src/main/java/com/akto/action/testing_issues/IssuesAction.java b/apps/dashboard/src/main/java/com/akto/action/testing_issues/IssuesAction.java index 7f4e07a8ad..b58ee1005e 100644 --- a/apps/dashboard/src/main/java/com/akto/action/testing_issues/IssuesAction.java +++ b/apps/dashboard/src/main/java/com/akto/action/testing_issues/IssuesAction.java @@ -37,6 +37,7 @@ import com.akto.util.enums.GlobalEnums.Severity; import com.akto.util.enums.GlobalEnums.TestCategory; import com.akto.util.enums.GlobalEnums.TestRunIssueStatus; +import com.akto.utils.jobs.CleanInventory; import com.mongodb.BasicDBObject; import com.mongodb.client.MongoCursor; import com.mongodb.client.model.*; @@ -50,6 +51,9 @@ import org.slf4j.LoggerFactory; import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import static com.akto.util.Constants.ID; import static com.akto.util.Constants.ONE_DAY_TIMESTAMP; @@ -77,6 +81,9 @@ public class IssuesAction extends UserAction { long endTimeStamp; private Map> severityInfo = new HashMap<>(); + private static final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + + private Bson createFilters (boolean useFilterStatus) { Bson filters = Filters.empty(); if (useFilterStatus && filterStatus != null && !filterStatus.isEmpty()) { @@ -513,6 +520,8 @@ public String updateIssueStatus () { return SUCCESS.toUpperCase(); } + private Map testingRunResultHexIdsMap; + public String bulkUpdateIssueStatus () { if (issueIdArray == null || statusToBeUpdated == null || ignoreReason == null) { throw new IllegalStateException(); @@ -530,6 +539,42 @@ public String bulkUpdateIssueStatus () { update = Updates.combine(update, Updates.unset(TestingRunIssues.IGNORE_REASON)); } TestingRunIssuesDao.instance.updateMany(Filters.in(ID, issueIdArray), update); + + int accountId = Context.accountId.get(); + executorService.schedule( new Runnable() { + public void run() { + Context.accountId.set(accountId); + try { + + final Map countIssuesMap = new HashMap<>(); + countIssuesMap.put(Severity.HIGH.toString(), 0); + countIssuesMap.put(Severity.MEDIUM.toString(), 0); + countIssuesMap.put(Severity.LOW.toString(), 0); + + // update summaries accordingly with issues ignored + + Map mapSummaryToResultId = TestingRunResultDao.instance.mapSummaryIdToTestingResultHexId(testingRunResultHexIdsMap.keySet()); + Map> summaryWiseCountMap = new HashMap<>(); + + for(ObjectId summaryId: mapSummaryToResultId.keySet()){ + String resultHexId = mapSummaryToResultId.get(summaryId); + Map countMap = summaryWiseCountMap.getOrDefault(summaryId, countIssuesMap); + String severity = testingRunResultHexIdsMap.get(resultHexId); + int initialCount = countMap.getOrDefault(severity, 0); + countMap.put(severity, initialCount + 1); + summaryWiseCountMap.put(summaryId, countMap); + } + if(!summaryWiseCountMap.isEmpty()){ + TestingRunResultSummariesDao.instance.bulkUpdateTestingRunResultSummariesCount(summaryWiseCountMap); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + }, 0 , TimeUnit.SECONDS); + + return SUCCESS.toUpperCase(); } @@ -904,4 +949,8 @@ public Map> getSeverityInfo() { public void setSeverityInfo(Map> severityInfo) { this.severityInfo = severityInfo; } + + public void setTestingRunResultHexIdsMap(Map testingRunResultHexIdsMap) { + this.testingRunResultHexIdsMap = testingRunResultHexIdsMap; + } } diff --git a/apps/dashboard/src/main/resources/struts.xml b/apps/dashboard/src/main/resources/struts.xml index b210e8f748..a5d6881002 100644 --- a/apps/dashboard/src/main/resources/struts.xml +++ b/apps/dashboard/src/main/resources/struts.xml @@ -3513,6 +3513,27 @@ + + + + + TEST_RESULTS + READ + + + + 403 + false + ^actionErrors.* + + + + 422 + false + ^actionErrors.* + + + diff --git a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/issues/IssuesPage/IssuesPage.jsx b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/issues/IssuesPage/IssuesPage.jsx index 63de81c35b..0eeded355f 100644 --- a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/issues/IssuesPage/IssuesPage.jsx +++ b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/issues/IssuesPage/IssuesPage.jsx @@ -229,7 +229,7 @@ function IssuesPage() { } function ignoreAction(ignoreReason){ - api.bulkUpdateIssueStatus(items, "IGNORED", ignoreReason ).then((res) => { + api.bulkUpdateIssueStatus(items, "IGNORED", ignoreReason, {} ).then((res) => { setToast(true, false, `Issue${items.length==1 ? "" : "s"} ignored`) resetResourcesSelected() }) diff --git a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/issues/api.js b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/issues/api.js index 8cb617ba3e..7289caf117 100644 --- a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/issues/api.js +++ b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/issues/api.js @@ -23,11 +23,11 @@ export default { data: {issuesIds, issueStatusQuery} }) }, - bulkUpdateIssueStatus (issueIdArray, statusToBeUpdated, ignoreReason) { + bulkUpdateIssueStatus (issueIdArray, statusToBeUpdated, ignoreReason, testingRunResultHexIdsMap) { return request({ url: 'api/bulkUpdateIssueStatus', method: 'post', - data: {issueIdArray, statusToBeUpdated, ignoreReason} + data: {issueIdArray, statusToBeUpdated, ignoreReason, testingRunResultHexIdsMap} }) }, fetchTestingRunResult (issueId) { diff --git a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/SingleTestRunPage/SingleTestRunPage.js b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/SingleTestRunPage/SingleTestRunPage.js index 9a75c819a3..f21836ac5e 100644 --- a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/SingleTestRunPage/SingleTestRunPage.js +++ b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/SingleTestRunPage/SingleTestRunPage.js @@ -626,6 +626,13 @@ const editableConfigsComp = ( } } + const handleRefreshTableCount = async(summaryHexId) => { + await api.handleRefreshTableCount(summaryHexId).then((res) => { + func.setToast(true, false, "Re-calculating issues count") + setSecondaryPopover(false) + }) + } + const EmptyData = () => { return(
@@ -720,6 +727,11 @@ const editableConfigsComp = ( content: 'Edit testing config settings', icon: EditMajor, onAction: () => { setShowEditableSettings(true); handleAddSettings(); } + }, + { + content: 'Re-Calculate Issues Count', + icon: RefreshMajor, + onAction: () => {handleRefreshTableCount(currentSummary.hexId)} } ]}) const moreActionsComp = ( diff --git a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/TestRunResultPage/TestRunResultFlyout.jsx b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/TestRunResultPage/TestRunResultFlyout.jsx index 7ad8dbde32..891c962c7f 100644 --- a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/TestRunResultPage/TestRunResultFlyout.jsx +++ b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/testing/TestRunResultPage/TestRunResultFlyout.jsx @@ -15,7 +15,6 @@ import "./style.css" import ActivityTracker from '../../dashboard/components/ActivityTracker' import observeFunc from "../../observe/transform.js" import settingFunctions from '../../settings/module.js' -import DropdownSearch from '../../../components/shared/DropdownSearch.jsx' import JiraTicketCreationModal from '../../../components/shared/JiraTicketCreationModal.jsx' function TestRunResultFlyout(props) { @@ -66,7 +65,12 @@ function TestRunResultFlyout(props) { },[issueDetails?.id?.apiInfoKey]) function ignoreAction(ignoreReason){ - issuesApi.bulkUpdateIssueStatus([issueDetails.id], "IGNORED", ignoreReason ).then((res) => { + const severity = (selectedTestRunResult && selectedTestRunResult.vulnerable) ? issueDetails.severity : ""; + let obj = {} + if(issueDetails?.testRunIssueStatus !== "IGNORED"){ + obj = {[selectedTestRunResult.id]: severity.toUpperCase()} + } + issuesApi.bulkUpdateIssueStatus([issueDetails.id], "IGNORED", ignoreReason, obj ).then((res) => { func.setToast(true, false, `Issue ignored`) }) } 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 2b86e5b4d8..5d9c8b82f4 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 @@ -504,5 +504,12 @@ export default { method: 'post', data: {...filters, issueIds, endTimeStamp} }) + }, + handleRefreshTableCount(testingRunResultSummaryHexId) { + return request({ + url: '/api/handleRefreshTableCount', + method: 'post', + data: {testingRunResultSummaryHexId} + }) } } \ No newline at end of file diff --git a/apps/dashboard/web/src/apps/dashboard/views/issues/api.js b/apps/dashboard/web/src/apps/dashboard/views/issues/api.js index 2e94a46a1c..fb121d2d1b 100644 --- a/apps/dashboard/web/src/apps/dashboard/views/issues/api.js +++ b/apps/dashboard/web/src/apps/dashboard/views/issues/api.js @@ -23,13 +23,6 @@ export default { data: {issueId, statusToBeUpdated, ignoreReason} }) }, - bulkUpdateIssueStatus (issueIdArray, statusToBeUpdated, ignoreReason) { - return request({ - url: 'api/bulkUpdateIssueStatus', - method: 'post', - data: {issueIdArray, statusToBeUpdated, ignoreReason} - }) - }, fetchTestingRunResult (issueId) { return request({ url: 'api/fetchTestingRunResult', 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 cb915a4f0f..eed942bfea 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 @@ -23,7 +23,11 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; public class TestingRunResultDao extends AccountsContextDaoWithRbac { @@ -184,6 +188,27 @@ public List fetchLatestTestingRunResult(Bson filters, int limi return testingRunResults; } + public Map mapSummaryIdToTestingResultHexId(Set testingRunResultHexIds){ + Map finalMap = new HashMap<>(); + if(testingRunResultHexIds == null || testingRunResultHexIds.isEmpty()){ + return finalMap; + } + + List objectIdList = testingRunResultHexIds.stream() + .map(ObjectId::new) + .collect(Collectors.toList()); + + // doing only for 1000 results at a time + objectIdList = objectIdList.subList(0, 1000); + + List runResults = instance.findAll(Filters.in(Constants.ID, objectIdList), Projections.include(TestingRunResult.TEST_RUN_RESULT_SUMMARY_ID)); + for(TestingRunResult runResult: runResults){ + finalMap.put(runResult.getTestRunResultSummaryId(), runResult.getHexId()); + } + + return finalMap; + } + public void createIndicesIfAbsent() { String dbName = Context.accountId.get()+""; diff --git a/libs/dao/src/main/java/com/akto/dao/testing/TestingRunResultSummariesDao.java b/libs/dao/src/main/java/com/akto/dao/testing/TestingRunResultSummariesDao.java index e756ba1515..514c705abb 100644 --- a/libs/dao/src/main/java/com/akto/dao/testing/TestingRunResultSummariesDao.java +++ b/libs/dao/src/main/java/com/akto/dao/testing/TestingRunResultSummariesDao.java @@ -28,6 +28,10 @@ import com.mongodb.client.model.Indexes; import com.mongodb.client.model.Projections; import com.mongodb.client.model.Sorts; +import com.mongodb.client.model.UpdateOneModel; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.Updates; +import com.mongodb.client.model.WriteModel; public class TestingRunResultSummariesDao extends AccountsContextDao { @@ -100,6 +104,28 @@ public Map> fetchMetadataFilters(List filterKeys) { return ret; } + public void bulkUpdateTestingRunResultSummariesCount(Map> summaryWiseCountMap){ + + ArrayList> bulkUpdates = new ArrayList<>(); + for(ObjectId summaryId: summaryWiseCountMap.keySet()){ + + Map countIssuesMap = summaryWiseCountMap.get(summaryId); + + Bson update = Updates.combine( + Updates.inc("countIssues.HIGH", (-1 * countIssuesMap.get("HIGH"))), + Updates.inc("countIssues.MEDIUM", (-1 * countIssuesMap.get("MEDIUM"))), + Updates.inc("countIssues.LOW", (-1 * countIssuesMap.get("LOW"))) + ); + + bulkUpdates.add( + new UpdateOneModel<>(Filters.eq("_id",summaryId), update, new UpdateOptions().upsert(false)) + ); + } + + instance.getMCollection().bulkWrite(bulkUpdates); + + } + public void createIndicesIfAbsent() { String dbName = Context.accountId.get()+""; diff --git a/libs/dao/src/main/java/com/akto/dao/testing_run_findings/TestingRunIssuesDao.java b/libs/dao/src/main/java/com/akto/dao/testing_run_findings/TestingRunIssuesDao.java index e43fef603e..0c213ccebe 100644 --- a/libs/dao/src/main/java/com/akto/dao/testing_run_findings/TestingRunIssuesDao.java +++ b/libs/dao/src/main/java/com/akto/dao/testing_run_findings/TestingRunIssuesDao.java @@ -19,6 +19,7 @@ import com.akto.dao.test_editor.YamlTemplateDao; import com.akto.dto.test_editor.YamlTemplate; import com.akto.dto.test_run_findings.TestingIssuesId; +import com.akto.util.Constants; import com.akto.util.enums.GlobalEnums; import com.akto.util.enums.MongoDBEnums; import com.mongodb.BasicDBObject; @@ -60,6 +61,9 @@ public void createIndicesIfAbsent() { MCollection.createIndexIfAbsent(getDBName(), getCollName(), fieldNames, true); fieldNames = new String[] {TestingRunIssues.LATEST_TESTING_RUN_SUMMARY_ID}; MCollection.createIndexIfAbsent(getDBName(), getCollName(), fieldNames, true); + + fieldNames = new String[] {Constants.ID, TestingRunIssues.TEST_RUN_ISSUES_STATUS}; + MCollection.createIndexIfAbsent(getDBName(), getCollName(), fieldNames, true); } 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 777f352b9a..dadb433a56 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 @@ -46,6 +46,8 @@ public class TestingRunResult implements Comparable { public static final String TEST_RUN_RESULT_SUMMARY_ID = "testRunResultSummaryId"; private ObjectId testRunResultSummaryId; + public static final String IS_IGNORED_RESULT = "isIgnoredResult"; + private boolean isIgnoredResult ; public static final String ERRORS_LIST = "errorsList"; private List errorsList; @@ -343,4 +345,12 @@ public List getErrorsList() { public void setErrorsList(List errorsList) { this.errorsList = errorsList; } + + public boolean isIgnoredResult() { + return isIgnoredResult; + } + + public void setIgnoredResult(boolean isIgnoredResult) { + this.isIgnoredResult = isIgnoredResult; + } }