Skip to content

Commit

Permalink
Merge pull request #2043 from akto-api-security/feature/teams_webhook…
Browse files Browse the repository at this point in the history
…_test_resuts

webhook for test run results in ms teams
  • Loading branch information
notshivansh authored Feb 3, 2025
2 parents 2432664 + eeb5597 commit d866396
Show file tree
Hide file tree
Showing 25 changed files with 640 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import javax.servlet.http.HttpServletRequest;

import static com.akto.utils.Utils.createDashboardUrlFromRequest;

import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -186,6 +187,6 @@ public void setTokenUtility(ApiToken.Utility tokenUtility) {

@Override
public void setServletRequest(HttpServletRequest request) {
this.dashboardUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
this.dashboardUrl = createDashboardUrlFromRequest(request);
}
}
74 changes: 72 additions & 2 deletions apps/dashboard/src/main/java/com/akto/action/WebhookAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Updates;
import com.opensymphony.xwork2.Action;

import org.apache.struts2.interceptor.ServletRequestAware;
import org.bson.conversions.Bson;

import static com.akto.utils.Utils.createDashboardUrlFromRequest;

import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class WebhookAction extends UserAction {
import javax.servlet.http.HttpServletRequest;

public class WebhookAction extends UserAction implements ServletRequestAware{

private int id;
private String webhookName;
Expand All @@ -42,6 +48,9 @@ public class WebhookAction extends UserAction {
private int batchSize;
private boolean sendInstantly;
private String webhookType;
private boolean webhookPresent;
private String webhookOption;
private String dashboardUrl;

private static final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

Expand Down Expand Up @@ -99,6 +108,7 @@ public String addCustomWebhook(){
}
customWebhook.setSendInstantly(sendInstantly);
customWebhook.setWebhookType(type);
customWebhook.setDashboardUrl(this.dashboardUrl);
CustomWebhooksDao.instance.insertOne(customWebhook);
fetchCustomWebhooks();
}
Expand Down Expand Up @@ -156,7 +166,8 @@ public String updateCustomWebhook(){
Updates.set(CustomWebhook.SELECTED_WEBHOOK_OPTIONS, selectedWebhookOptions),
Updates.set(CustomWebhook.NEW_ENDPOINT_COLLECTIONS, newEndpointCollections),
Updates.set(CustomWebhook.NEW_SENSITIVE_ENDPOINT_COLLECTIONS, newSensitiveEndpointCollections),
Updates.set(CustomWebhook.SEND_INSTANTLY, sendInstantly)
Updates.set(CustomWebhook.SEND_INSTANTLY, sendInstantly),
Updates.set(CustomWebhook.DASHBOARD_URL, this.dashboardUrl)
);

if (batchSize > 0) {
Expand Down Expand Up @@ -248,6 +259,52 @@ public String deleteCustomWebhook() {
return Action.SUCCESS.toUpperCase();
}

private enum ValidationDataType{
TYPE, OPTION
}

private boolean validationCheck(String data, ValidationDataType expectedType) {
if (data == null || data.isEmpty()) {
addActionError("webhook " + expectedType + " is invalid");
return false;
}
try {
switch (expectedType) {
case TYPE:
CustomWebhook.WebhookType.valueOf(data);
break;
case OPTION:
CustomWebhook.WebhookOptions.valueOf(data);
break;
default:
throw new Exception("Invalid " + expectedType);
}
} catch (Exception e) {
addActionError("webhook " + expectedType + " is invalid");
return false;
}
return true;
}

public String checkWebhook() {

if (!(validationCheck(this.webhookType, ValidationDataType.TYPE) &&
validationCheck(this.webhookOption, ValidationDataType.OPTION))) {
return ERROR.toUpperCase();
}

CustomWebhook webhook = CustomWebhooksDao.instance.findOne(
Filters.and(
Filters.eq(CustomWebhook.WEBHOOK_TYPE, webhookType),
Filters.in(CustomWebhook.SELECTED_WEBHOOK_OPTIONS, webhookOption)));

if (webhook != null) {
webhookPresent = true;
}

return Action.SUCCESS.toUpperCase();
}

public int getId() {
return id;
}
Expand Down Expand Up @@ -379,4 +436,17 @@ public boolean getSendInstantly() {
public void setSendInstantly(boolean sendInstantly) {
this.sendInstantly = sendInstantly;
}

@Override
public void setServletRequest(HttpServletRequest request) {
this.dashboardUrl = createDashboardUrlFromRequest(request);
}

public void setWebhookOption(String webhookOption) {
this.webhookOption = webhookOption;
}

public boolean getWebhookPresent() {
return webhookPresent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ private static List<ObjectId> getTestsWithSeverity(List<String> severities) {

private CallSource source;
private boolean sendSlackAlert = false;
private boolean sendMsTeamsAlert = false;

private TestingRun createTestingRun(int scheduleTimestamp, int periodInSeconds) {
User user = getSUser();
Expand Down Expand Up @@ -165,7 +166,7 @@ private TestingRun createTestingRun(int scheduleTimestamp, int periodInSeconds)

return new TestingRun(scheduleTimestamp, user.getLogin(),
testingEndpoints, testIdConfig, State.SCHEDULED, periodInSeconds, testName, this.testRunTime,
this.maxConcurrentRequests, this.sendSlackAlert);
this.maxConcurrentRequests, this.sendSlackAlert, this.sendMsTeamsAlert);
}

private List<String> selectedTests;
Expand Down Expand Up @@ -1200,6 +1201,12 @@ public String modifyTestingRunConfig(){
Updates.set(TestingRun.SEND_SLACK_ALERT, editableTestingRunConfig.getSendSlackAlert()));
}

if (existingTestingRun.getSendMsTeamsAlert() != editableTestingRunConfig.getSendMsTeamsAlert()) {
updates.add(
Updates.set(TestingRun.SEND_MS_TEAMS_ALERT,
editableTestingRunConfig.getSendMsTeamsAlert()));
}

int periodInSeconds = 0;
if (editableTestingRunConfig.getContinuousTesting()) {
periodInSeconds = -1;
Expand Down Expand Up @@ -1686,4 +1693,12 @@ public boolean getCleanUpTestingResources() {
public void setCleanUpTestingResources(boolean cleanUpTestingResources) {
this.cleanUpTestingResources = cleanUpTestingResources;
}

public boolean getSendMsTeamsAlert() {
return sendMsTeamsAlert;
}

public void setSendMsTeamsAlert(boolean sendMsTeamsAlert) {
this.sendMsTeamsAlert = sendMsTeamsAlert;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import com.akto.mixpanel.AktoMixpanel;
import com.akto.notifications.slack.DailyUpdate;
import com.akto.notifications.slack.TestSummaryGenerator;
import com.akto.notifications.webhook.WebhookSender;
import com.akto.parsers.HttpCallParser;
import com.akto.runtime.RuntimeUtil;
import com.akto.stigg.StiggReporterClient;
Expand Down Expand Up @@ -936,6 +937,18 @@ public static void webhookSenderUtil(CustomWebhook webhook) {
return;
}

/*
* TESTING_RUN_RESULTS type webhooks are
* triggered only on test complete not periodically.
*/

if (webhook.getSelectedWebhookOptions() != null &&
!webhook.getSelectedWebhookOptions().isEmpty()
&& webhook.getSelectedWebhookOptions()
.contains(CustomWebhook.WebhookOptions.TESTING_RUN_RESULTS)) {
return;
}

ChangesInfo ci = getChangesInfo(now - webhook.getLastSentTimestamp(), now - webhook.getLastSentTimestamp(), webhook.getNewEndpointCollections(), webhook.getNewSensitiveEndpointCollections(), true);

boolean sendApiThreats = false;
Expand Down Expand Up @@ -1182,29 +1195,7 @@ private static void actuallySendWebhook(CustomWebhook webhook, Map<String, Objec
errors.add("Failed to replace variables");
}

webhook.setLastSentTimestamp(now);
CustomWebhooksDao.instance.updateOne(Filters.eq("_id", webhook.getId()), Updates.set("lastSentTimestamp", now));

Map<String, List<String>> headers = OriginalHttpRequest.buildHeadersMap(webhook.getHeaderString());
OriginalHttpRequest request = new OriginalHttpRequest(webhook.getUrl(), webhook.getQueryParams(), webhook.getMethod().toString(), payload, headers, "");
OriginalHttpResponse response = null; // null response means api request failed. Do not use new OriginalHttpResponse() in such cases else the string parsing fails.

try {
response = ApiExecutor.sendRequest(request, true, null, false, new ArrayList<>());
loggerMaker.infoAndAddToDb("webhook request sent", LogDb.DASHBOARD);
} catch (Exception e) {
errors.add("API execution failed");
}

String message = null;
try {
message = convertOriginalReqRespToString(request, response);
} catch (Exception e) {
errors.add("Failed converting sample data");
}

CustomWebhookResult webhookResult = new CustomWebhookResult(webhook.getId(), webhook.getUserEmail(), now, message, errors);
CustomWebhooksResultDao.instance.insertOne(webhookResult);
WebhookSender.sendCustomWebhook(webhook, payload, errors, now, LogDb.DASHBOARD);
}

private static void createBodyForWebhook(CustomWebhook webhook) {
Expand Down
10 changes: 10 additions & 0 deletions apps/dashboard/src/main/java/com/akto/utils/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import static com.akto.dto.RawApi.convertHeaders;


Expand Down Expand Up @@ -626,4 +628,12 @@ public static List<String> getUniqueValuesOfList(List<String> input){
input.addAll(copySet);
return input;
}

public static String createDashboardUrlFromRequest(HttpServletRequest request) {
if (request == null) {
return "http://localhost:8080";
}
return request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
}

}
25 changes: 25 additions & 0 deletions apps/dashboard/src/main/resources/struts.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4847,6 +4847,31 @@
</result>
</action>

<action name="api/checkWebhook" class="com.akto.action.WebhookAction" method="checkWebhook">
<interceptor-ref name="json"/>
<interceptor-ref name="defaultStack" />
<interceptor-ref name="roleAccessInterceptor">
<param name="featureLabel">USER_ACTIONS</param>
<param name="accessType">READ</param>
</interceptor-ref>
<result name="FORBIDDEN" type="json">
<param name="statusCode">403</param>
<param name="ignoreHierarchy">false</param>
<param name="includeProperties">^actionErrors.*</param>
</result>
<result name="SUCCESS" type="json"/>
<result name="ERROR" type="json">
<param name="statusCode">422</param>
<param name="ignoreHierarchy">false</param>
<param name="includeProperties">^actionErrors.*</param>
</result>
<result name="UNAUTHORIZED" type="json">
<param name="statusCode">403</param>
<param name="ignoreHierarchy">false</param>
<param name="includeProperties">^actionErrors.*</param>
</result>
</action>

<action name="api/deleteScheduledWorkflowTests" class="com.akto.action.testing.StartTestAction" method="deleteScheduledWorkflowTests">
<interceptor-ref name="json"/>
<interceptor-ref name="defaultStack" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,18 @@ public void testValidURL() {
System.out.println(Utils.isValidURL("asdad"));
}

@Test
public void testValidationCheck(){
WebhookAction webhookAction = new WebhookAction();
webhookAction.setWebhookType(CustomWebhook.WebhookType.MICROSOFT_TEAMS.name());
webhookAction.setWebhookOption(CustomWebhook.WebhookOptions.TESTING_RUN_RESULTS.name());
assertEquals("SUCCESS", webhookAction.checkWebhook());

webhookAction.setWebhookType(CustomWebhook.WebhookType.MICROSOFT_TEAMS.name()+"1");
assertEquals("ERROR", webhookAction.checkWebhook());
webhookAction.setWebhookOption(CustomWebhook.WebhookOptions.TESTING_RUN_RESULTS.name() + "1");
webhookAction.setWebhookType(CustomWebhook.WebhookType.MICROSOFT_TEAMS.name());
assertEquals("ERROR", webhookAction.checkWebhook());

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -573,20 +573,20 @@ export default {
data: {}
})
},
scheduleTestForCollection(apiCollectionId, startTimestamp, recurringDaily, selectedTests, testName, testRunTime, maxConcurrentRequests, overriddenTestAppUrl, testRoleId, continuousTesting, sendSlackAlert, testConfigsAdvancedSettings, cleanUpTestingResources) {
scheduleTestForCollection(apiCollectionId, startTimestamp, recurringDaily, selectedTests, testName, testRunTime, maxConcurrentRequests, overriddenTestAppUrl, testRoleId, continuousTesting, sendSlackAlert, sendMsTeamsAlert, testConfigsAdvancedSettings, cleanUpTestingResources) {
return request({
url: '/api/startTest',
method: 'post',
data: { apiCollectionId, type: "COLLECTION_WISE", startTimestamp, recurringDaily, selectedTests, testName, testRunTime, maxConcurrentRequests, overriddenTestAppUrl, testRoleId, continuousTesting, sendSlackAlert, testConfigsAdvancedSettings, cleanUpTestingResources}
data: { apiCollectionId, type: "COLLECTION_WISE", startTimestamp, recurringDaily, selectedTests, testName, testRunTime, maxConcurrentRequests, overriddenTestAppUrl, testRoleId, continuousTesting, sendSlackAlert, sendMsTeamsAlert, testConfigsAdvancedSettings, cleanUpTestingResources}
}).then((resp) => {
return resp
})
},
scheduleTestForCustomEndpoints(apiInfoKeyList, startTimestamp, recurringDaily, selectedTests, testName, testRunTime, maxConcurrentRequests, overriddenTestAppUrl, source, testRoleId, continuousTesting, sendSlackAlert, testConfigsAdvancedSettings, cleanUpTestingResources) {
scheduleTestForCustomEndpoints(apiInfoKeyList, startTimestamp, recurringDaily, selectedTests, testName, testRunTime, maxConcurrentRequests, overriddenTestAppUrl, source, testRoleId, continuousTesting, sendSlackAlert, sendMsTeamsAlert, testConfigsAdvancedSettings, cleanUpTestingResources) {
return request({
url: '/api/startTest',
method: 'post',
data: {apiInfoKeyList, type: "CUSTOM", startTimestamp, recurringDaily, selectedTests, testName, testRunTime, maxConcurrentRequests, overriddenTestAppUrl, source, testRoleId, continuousTesting, sendSlackAlert, testConfigsAdvancedSettings, cleanUpTestingResources}
data: {apiInfoKeyList, type: "CUSTOM", startTimestamp, recurringDaily, selectedTests, testName, testRunTime, maxConcurrentRequests, overriddenTestAppUrl, source, testRoleId, continuousTesting, sendSlackAlert, sendMsTeamsAlert, testConfigsAdvancedSettings, cleanUpTestingResources}
}).then((resp) => {
return resp
})
Expand All @@ -613,6 +613,15 @@ export default {
return resp
},

async checkWebhook(webhookType, webhookOption) {
const resp = await request({
url: '/api/checkWebhook',
method: 'post',
data: { webhookType, webhookOption }
})
return resp
},

async fetchNewParametersTrend(startTimestamp, endTimestamp) {
const resp = await request({
url: '/api/fetchNewParametersTrend',
Expand Down
Loading

0 comments on commit d866396

Please sign in to comment.