diff --git a/apps/dashboard/src/main/java/com/akto/action/AzureBoardsIntegrationAction.java b/apps/dashboard/src/main/java/com/akto/action/AzureBoardsIntegrationAction.java index 9b33aeefbf..a2551fd7c0 100644 --- a/apps/dashboard/src/main/java/com/akto/action/AzureBoardsIntegrationAction.java +++ b/apps/dashboard/src/main/java/com/akto/action/AzureBoardsIntegrationAction.java @@ -173,6 +173,7 @@ private boolean fetchAzureBoardsWorkItems(String personalAuthToken, String proje private String description; private String endpoint; // endpoint path for title formatting private String azureBoardsWorkItemUrl; + private String originalMessage; // Request/response message for attachment (for threat activity, contains orig with both request and response) public String createWorkItem() { AzureBoardsIntegration azureBoardsIntegration = AzureBoardsIntegrationDao.instance.findOne(new BasicDBObject()); @@ -215,36 +216,8 @@ public String createWorkItem() { azureBoardsPayloadCreator(testingRunResult, testName, testDescription, attachmentUrl, customABWorkItemFieldsPayload, reqPayload); logger.infoAndAddToDb("Azure board payload: " + reqPayload.toString(), LoggerMaker.LogDb.DASHBOARD); - String url = azureBoardsIntegration.getBaseUrl() + "/" + azureBoardsIntegration.getOrganization() + "/" + projectName + "/_apis/wit/workitems/$" + workItemType + "?api-version=" + version; - logger.infoAndAddToDb("Azure board final url: " + url, LoggerMaker.LogDb.DASHBOARD); - - Map> headers = new HashMap<>(); - headers.put("Authorization", Collections.singletonList("Basic " + azureBoardsIntegration.getPersonalAuthToken())); - headers.put("content-type", Collections.singletonList("application/json-patch+json")); - logger.infoAndAddToDb("Azure board headers: " + headers.toString(), LoggerMaker.LogDb.DASHBOARD); - OriginalHttpRequest request = new OriginalHttpRequest(url, "", "POST", reqPayload.toString(), headers, ""); try { - OriginalHttpResponse response = ApiExecutor.sendRequest(request, true, null, false, new ArrayList<>()); - logger.infoAndAddToDb("Status and Response from the createWorkItem API: " + response.getStatusCode() + " | " + response.getBody()); - String responsePayload = response.getBody(); - if (response.getStatusCode() > 201 || responsePayload == null) { - return Action.ERROR.toUpperCase(); - } - - BasicDBObject respPayloadObj = BasicDBObject.parse(responsePayload); - - String workItemUrl; - try { - Object linksObj = respPayloadObj.get("_links"); - BasicDBObject links = BasicDBObject.parse(linksObj.toString()); - Object htmlObj = links.get("html"); - BasicDBObject html = BasicDBObject.parse(htmlObj.toString()); - Object href = html.get("href"); - workItemUrl = href.toString(); - } catch (Exception e) { - workItemUrl = respPayloadObj.get("url").toString(); - } - + String workItemUrl = createAndSendWorkItemRequest(azureBoardsIntegration, projectName, workItemType, reqPayload); if(workItemUrl == null) { return Action.ERROR.toUpperCase(); } @@ -268,12 +241,9 @@ private void azureBoardsPayloadCreator(TestingRunResult testingRunResult, String String endpointPath = getEndpointPath(fullUrl); String title = "Akto Report - " + testName + " (" + method + " - " + endpointPath + ")"; - BasicDBObject titleDBObject = new BasicDBObject(); - titleDBObject.put("op", AzureBoardsOperations.ADD.name().toLowerCase()); - titleDBObject.put("path", "/fields/System.Title"); - titleDBObject.put("value", title); - reqPayload.add(titleDBObject); + addTitleField(reqPayload, title); + // Format description with host, endpoint, and dashboard link try { URL url = new URL(fullUrl); String hostname = url.getHost(); @@ -284,74 +254,36 @@ private void azureBoardsPayloadCreator(TestingRunResult testingRunResult, String e.printStackTrace(); } + addDescriptionField(reqPayload, testDescription); + addAttachment(reqPayload, attachmentUrl); + addCustomFields(reqPayload, customABWorkItemFieldsPayload); + } - BasicDBObject descriptionDBObject = new BasicDBObject(); - descriptionDBObject.put("op", AzureBoardsOperations.ADD.name().toLowerCase()); - descriptionDBObject.put("path", "/fields/System.Description"); - descriptionDBObject.put("value", testDescription); - reqPayload.add(descriptionDBObject); - - if(attachmentUrl != null && !attachmentUrl.isEmpty()) { - BasicDBObject attachmentsDBObject = new BasicDBObject(); - attachmentsDBObject.put("op", AzureBoardsOperations.ADD.name().toLowerCase()); - attachmentsDBObject.put("path", "/relations/-"); - BasicDBObject valueDBObject = new BasicDBObject(); - valueDBObject.put("rel", "AttachedFile"); - valueDBObject.put("url", attachmentUrl); - valueDBObject.put("attributes", new BasicDBObject().put("comment", "Request and Response sample data.")); - - attachmentsDBObject.put("value", valueDBObject); - reqPayload.add(attachmentsDBObject); - } + private String getAttachmentUrl(String originalMessage, String message, AzureBoardsIntegration azureBoardsIntegration) { + return getAttachmentUrl(originalMessage, message, azureBoardsIntegration, false); + } - // Adding custom fields data - if (customABWorkItemFieldsPayload != null) { - for (BasicDBObject field: customABWorkItemFieldsPayload) { - try { - String fieldReferenceName = field.getString("referenceName"); - String fieldValue = field.getString("value"); - String fieldType = field.getString("type"); - - BasicDBObject customFieldDBObject = new BasicDBObject(); - customFieldDBObject.put("op", AzureBoardsOperations.ADD.name().toLowerCase()); - customFieldDBObject.put("path", "/fields/" + fieldReferenceName); - - switch (fieldType) { - case "integer": - int intValue = Integer.parseInt(fieldValue); - customFieldDBObject.put("value", intValue); - break; - case "double": - double doubleValue = Double.parseDouble(fieldValue); - customFieldDBObject.put("value", doubleValue); - break; - case "boolean": - boolean booleanValue = Boolean.parseBoolean(fieldValue); - customFieldDBObject.put("value", booleanValue); - break; - default: - customFieldDBObject.put("value", fieldValue); - } - - reqPayload.add(customFieldDBObject); - } catch (Exception e) { - continue; - } - } + private String getAttachmentUrl(String originalMessage, String message, AzureBoardsIntegration azureBoardsIntegration, boolean isThreatActivity) { + File requestComparisonFile = createRequestFile(originalMessage, message, isThreatActivity); + if (requestComparisonFile == null) { + return null; } + return uploadAttachmentFile(requestComparisonFile, azureBoardsIntegration); } - private String getAttachmentUrl(String originalMessage, String message, AzureBoardsIntegration azureBoardsIntegration) { - File requestComparisonFile = createRequestFile(originalMessage, message); - if (requestComparisonFile == null) { + /** + * Uploads an attachment file to Azure DevOps and returns the attachment URL + */ + private String uploadAttachmentFile(File attachmentFile, AzureBoardsIntegration azureBoardsIntegration) { + if (attachmentFile == null) { return null; } try { - String uploadUrl = azureBoardsIntegration.getBaseUrl() + "/" + azureBoardsIntegration.getOrganization() + "/" + projectName + "/_apis/wit/attachments?fileName=" + URLEncoder.encode(requestComparisonFile.getName(), "UTF-8") + "&api-version=" + version; + String uploadUrl = azureBoardsIntegration.getBaseUrl() + "/" + azureBoardsIntegration.getOrganization() + "/" + projectName + "/_apis/wit/attachments?fileName=" + URLEncoder.encode(attachmentFile.getName(), "UTF-8") + "&api-version=" + version; - byte[] fileBytes = Files.readAllBytes(requestComparisonFile.toPath()); + byte[] fileBytes = Files.readAllBytes(attachmentFile.toPath()); Map> headers = new HashMap<>(); headers.put("Authorization", Collections.singletonList("Basic " + azureBoardsIntegration.getPersonalAuthToken())); @@ -370,7 +302,7 @@ private String getAttachmentUrl(String originalMessage, String message, AzureBoa } catch (Exception e) { e.printStackTrace(); } finally { - requestComparisonFile.delete(); + attachmentFile.delete(); } return null; @@ -465,156 +397,37 @@ public String createGeneralAzureBoardsWorkItem() { String workItemTitle = this.title; String workItemDescription = this.description; - // If templateId is provided, fetch threat policy info and use it + // Enrich with threat policy info if templateId is provided if (this.templateId != null && !this.templateId.isEmpty()) { - try { - YamlTemplate threatPolicyTemplate = FilterYamlTemplateDao.instance.findOne( - Filters.eq(Constants.ID, this.templateId) - ); - - if (threatPolicyTemplate != null && threatPolicyTemplate.getContent() != null) { - FilterConfig filterConfig = FilterConfigYamlParser.parseTemplate( - threatPolicyTemplate.getContent(), false - ); - - if (filterConfig != null && filterConfig.getInfo() != null) { - Info policyInfo = filterConfig.getInfo(); - - // Format title as "Policy Name - Endpoint" - if (policyInfo.getName() != null && !policyInfo.getName().isEmpty()) { - if (this.endpoint != null && !this.endpoint.isEmpty()) { - workItemTitle = policyInfo.getName() + " - " + this.endpoint; - } else { - workItemTitle = policyInfo.getName(); - } - } - - // Build description: keep original description and append Description, Details, Impact from threat policy - StringBuilder descriptionBuilder = new StringBuilder(); - - // Keep the original description (Template ID, Severity, Attack Count, Host, Endpoint, Reference URL) - if (workItemDescription != null && !workItemDescription.isEmpty()) { - descriptionBuilder.append(workItemDescription); - descriptionBuilder.append("\n\n"); - } - - // Append Description, Details, and Impact from threat policy - if (policyInfo.getDescription() != null && !policyInfo.getDescription().isEmpty()) { - descriptionBuilder.append("Description:\n"); - descriptionBuilder.append(policyInfo.getDescription()); - descriptionBuilder.append("\n\n"); - } - - if (policyInfo.getDetails() != null && !policyInfo.getDetails().isEmpty()) { - descriptionBuilder.append("Details:\n"); - descriptionBuilder.append(policyInfo.getDetails()); - descriptionBuilder.append("\n\n"); - } - - if (policyInfo.getImpact() != null && !policyInfo.getImpact().isEmpty()) { - descriptionBuilder.append("Impact:\n"); - descriptionBuilder.append(policyInfo.getImpact()); - } - - // Update description with combined content - if (descriptionBuilder.length() > 0) { - workItemDescription = descriptionBuilder.toString(); - } - - logger.infoAndAddToDb("Using threat policy info for work item: " + this.templateId, LoggerMaker.LogDb.DASHBOARD); - } - } - } catch (Exception e) { - logger.errorAndAddToDb("Error fetching threat policy template: " + e.getMessage(), LoggerMaker.LogDb.DASHBOARD); - // Continue with original title and description if template fetch fails - } + ThreatPolicyInfo enriched = enrichWithThreatPolicyInfo(this.templateId, workItemTitle, workItemDescription, this.endpoint); + workItemTitle = enriched.title; + workItemDescription = enriched.description; } - // Add title - BasicDBObject titleDBObject = new BasicDBObject(); - titleDBObject.put("op", AzureBoardsOperations.ADD.name().toLowerCase()); - titleDBObject.put("path", "/fields/System.Title"); - titleDBObject.put("value", workItemTitle); - reqPayload.add(titleDBObject); - - // Add description with HTML formatting - String formattedDescription = workItemDescription.replace("\n", "
"); - BasicDBObject descriptionDBObject = new BasicDBObject(); - descriptionDBObject.put("op", AzureBoardsOperations.ADD.name().toLowerCase()); - descriptionDBObject.put("path", "/fields/System.Description"); - descriptionDBObject.put("value", formattedDescription); - reqPayload.add(descriptionDBObject); - - // Add custom fields if provided - if (customABWorkItemFieldsPayload != null) { - for (BasicDBObject field: customABWorkItemFieldsPayload) { - try { - String fieldReferenceName = field.getString("referenceName"); - String fieldValue = field.getString("value"); - String fieldType = field.getString("type"); - - BasicDBObject customFieldDBObject = new BasicDBObject(); - customFieldDBObject.put("op", AzureBoardsOperations.ADD.name().toLowerCase()); - customFieldDBObject.put("path", "/fields/" + fieldReferenceName); - - switch (fieldType) { - case "integer": - int intValue = Integer.parseInt(fieldValue); - customFieldDBObject.put("value", intValue); - break; - case "double": - double doubleValue = Double.parseDouble(fieldValue); - customFieldDBObject.put("value", doubleValue); - break; - case "boolean": - boolean booleanValue = Boolean.parseBoolean(fieldValue); - customFieldDBObject.put("value", booleanValue); - break; - default: - customFieldDBObject.put("value", fieldValue); - } - - reqPayload.add(customFieldDBObject); - } catch (Exception e) { - logger.errorAndAddToDb("Error processing custom field: " + e.getMessage(), LoggerMaker.LogDb.DASHBOARD); - continue; - } + addTitleField(reqPayload, workItemTitle); + addDescriptionField(reqPayload, workItemDescription); + + // Add attachment if request/response data is provided + // For threat activity, originalMessage contains the orig field with both request and response + String attachmentUrl = null; + if (this.originalMessage != null && !this.originalMessage.isEmpty()) { + // For threat activity: originalMessage contains everything, explicitly mark as threat activity + attachmentUrl = getAttachmentUrl(this.originalMessage, this.originalMessage, azureBoardsIntegration, true); + if (attachmentUrl != null) { + logger.infoAndAddToDb("Successfully created attachment for threat activity work item", LoggerMaker.LogDb.DASHBOARD); + } else { + logger.errorAndAddToDb("Failed to create attachment for threat activity work item", LoggerMaker.LogDb.DASHBOARD); } } - - String url = azureBoardsIntegration.getBaseUrl() + "/" + azureBoardsIntegration.getOrganization() + "/" + projectName + "/_apis/wit/workitems/$" + workItemType + "?api-version=" + version; - logger.infoAndAddToDb("Azure board final url: " + url, LoggerMaker.LogDb.DASHBOARD); - - Map> headers = new HashMap<>(); - headers.put("Authorization", Collections.singletonList("Basic " + azureBoardsIntegration.getPersonalAuthToken())); - headers.put("content-type", Collections.singletonList("application/json-patch+json")); - OriginalHttpRequest request = new OriginalHttpRequest(url, "", "POST", reqPayload.toString(), headers, ""); - OriginalHttpResponse response = ApiExecutor.sendRequest(request, true, null, false, new ArrayList<>()); - logger.infoAndAddToDb("Status and Response from the createGeneralAzureBoardsWorkItem API: " + response.getStatusCode() + " | " + response.getBody()); + addAttachment(reqPayload, attachmentUrl); + addCustomFields(reqPayload, customABWorkItemFieldsPayload); - String responsePayload = response.getBody(); - if (response.getStatusCode() > 201 || responsePayload == null) { - addActionError("Error while creating Azure Boards work item"); - return Action.ERROR.toUpperCase(); - } - - BasicDBObject respPayloadObj = BasicDBObject.parse(responsePayload); - - String workItemUrl; - try { - Object linksObj = respPayloadObj.get("_links"); - BasicDBObject links = BasicDBObject.parse(linksObj.toString()); - Object htmlObj = links.get("html"); - BasicDBObject html = BasicDBObject.parse(htmlObj.toString()); - Object href = html.get("href"); - workItemUrl = href.toString(); - } catch (Exception e) { - workItemUrl = respPayloadObj.get("url").toString(); - } + logger.infoAndAddToDb("Azure Boards work item payload (before sending): " + reqPayload.toString(), LoggerMaker.LogDb.DASHBOARD); + String workItemUrl = createAndSendWorkItemRequest(azureBoardsIntegration, projectName, workItemType, reqPayload); if(workItemUrl == null) { - addActionError("Failed to extract work item URL from response"); + addActionError("Failed to create Azure Boards work item"); return Action.ERROR.toUpperCase(); } @@ -638,6 +451,210 @@ public String createGeneralAzureBoardsWorkItem() { } } + /** + * Helper class to hold enriched threat policy information + */ + private static class ThreatPolicyInfo { + String title; + String description; + + ThreatPolicyInfo(String title, String description) { + this.title = title; + this.description = description; + } + } + + /** + * Enriches title and description with threat policy information + */ + private ThreatPolicyInfo enrichWithThreatPolicyInfo(String templateId, String originalTitle, String originalDescription, String endpoint) { + try { + YamlTemplate threatPolicyTemplate = FilterYamlTemplateDao.instance.findOne( + Filters.eq(Constants.ID, templateId) + ); + + if (threatPolicyTemplate == null || threatPolicyTemplate.getContent() == null) { + return new ThreatPolicyInfo(originalTitle, originalDescription); + } + + FilterConfig filterConfig = FilterConfigYamlParser.parseTemplate( + threatPolicyTemplate.getContent(), false + ); + + if (filterConfig == null || filterConfig.getInfo() == null) { + return new ThreatPolicyInfo(originalTitle, originalDescription); + } + + Info policyInfo = filterConfig.getInfo(); + String enrichedTitle = originalTitle; + String enrichedDescription = originalDescription; + + // Format title as "Policy Name - Endpoint" + if (policyInfo.getName() != null && !policyInfo.getName().isEmpty()) { + if (endpoint != null && !endpoint.isEmpty()) { + enrichedTitle = policyInfo.getName() + " - " + endpoint; + } else { + enrichedTitle = policyInfo.getName(); + } + } + + // Build description: append Description, Details, Impact from threat policy + StringBuilder descriptionBuilder = new StringBuilder(); + if (originalDescription != null && !originalDescription.isEmpty()) { + descriptionBuilder.append(originalDescription).append("\n\n"); + } + + if (policyInfo.getDescription() != null && !policyInfo.getDescription().isEmpty()) { + descriptionBuilder.append("Description:\n").append(policyInfo.getDescription()).append("\n\n"); + } + + if (policyInfo.getDetails() != null && !policyInfo.getDetails().isEmpty()) { + descriptionBuilder.append("Details:\n").append(policyInfo.getDetails()).append("\n\n"); + } + + if (policyInfo.getImpact() != null && !policyInfo.getImpact().isEmpty()) { + descriptionBuilder.append("Impact:\n").append(policyInfo.getImpact()); + } + + if (descriptionBuilder.length() > 0) { + enrichedDescription = descriptionBuilder.toString(); + } + + logger.infoAndAddToDb("Using threat policy info for work item: " + templateId, LoggerMaker.LogDb.DASHBOARD); + return new ThreatPolicyInfo(enrichedTitle, enrichedDescription); + + } catch (Exception e) { + logger.errorAndAddToDb("Error fetching threat policy template: " + e.getMessage(), LoggerMaker.LogDb.DASHBOARD); + return new ThreatPolicyInfo(originalTitle, originalDescription); + } + } + + /** + * Adds title field to the request payload + */ + private void addTitleField(BasicDBList reqPayload, String title) { + BasicDBObject titleDBObject = new BasicDBObject(); + titleDBObject.put("op", AzureBoardsOperations.ADD.name().toLowerCase()); + titleDBObject.put("path", "/fields/System.Title"); + titleDBObject.put("value", title); + reqPayload.add(titleDBObject); + } + + /** + * Adds description field to the request payload with HTML formatting + */ + private void addDescriptionField(BasicDBList reqPayload, String description) { + String formattedDescription = description != null ? description.replace("\n", "
") : ""; + BasicDBObject descriptionDBObject = new BasicDBObject(); + descriptionDBObject.put("op", AzureBoardsOperations.ADD.name().toLowerCase()); + descriptionDBObject.put("path", "/fields/System.Description"); + descriptionDBObject.put("value", formattedDescription); + reqPayload.add(descriptionDBObject); + } + + /** + * Adds attachment to the request payload if attachment URL is provided + */ + private void addAttachment(BasicDBList reqPayload, String attachmentUrl) { + if (attachmentUrl != null && !attachmentUrl.isEmpty()) { + BasicDBObject attachmentsDBObject = new BasicDBObject(); + attachmentsDBObject.put("op", AzureBoardsOperations.ADD.name().toLowerCase()); + attachmentsDBObject.put("path", "/relations/-"); + BasicDBObject valueDBObject = new BasicDBObject(); + valueDBObject.put("rel", "AttachedFile"); + valueDBObject.put("url", attachmentUrl); + valueDBObject.put("attributes", new BasicDBObject().put("comment", "Request and Response sample data.")); + attachmentsDBObject.put("value", valueDBObject); + reqPayload.add(attachmentsDBObject); + logger.infoAndAddToDb("Added attachment to payload with URL: " + attachmentUrl, LoggerMaker.LogDb.DASHBOARD); + } else { + logger.infoAndAddToDb("No attachment URL provided, skipping attachment", LoggerMaker.LogDb.DASHBOARD); + } + } + + /** + * Adds custom fields to the request payload + */ + private void addCustomFields(BasicDBList reqPayload, List customFields) { + if (customFields == null) { + return; + } + + for (BasicDBObject field : customFields) { + try { + String fieldReferenceName = field.getString("referenceName"); + String fieldValue = field.getString("value"); + String fieldType = field.getString("type"); + + BasicDBObject customFieldDBObject = new BasicDBObject(); + customFieldDBObject.put("op", AzureBoardsOperations.ADD.name().toLowerCase()); + customFieldDBObject.put("path", "/fields/" + fieldReferenceName); + + switch (fieldType) { + case "integer": + customFieldDBObject.put("value", Integer.parseInt(fieldValue)); + break; + case "double": + customFieldDBObject.put("value", Double.parseDouble(fieldValue)); + break; + case "boolean": + customFieldDBObject.put("value", Boolean.parseBoolean(fieldValue)); + break; + default: + customFieldDBObject.put("value", fieldValue); + } + + reqPayload.add(customFieldDBObject); + } catch (Exception e) { + logger.errorAndAddToDb("Error processing custom field: " + e.getMessage(), LoggerMaker.LogDb.DASHBOARD); + } + } + } + + /** + * Creates and sends work item request, returns work item URL or null on error + */ + private String createAndSendWorkItemRequest(AzureBoardsIntegration azureBoardsIntegration, String projectName, String workItemType, BasicDBList reqPayload) { + String url = azureBoardsIntegration.getBaseUrl() + "/" + azureBoardsIntegration.getOrganization() + "/" + projectName + "/_apis/wit/workitems/$" + workItemType + "?api-version=" + version; + logger.infoAndAddToDb("Azure board final url: " + url, LoggerMaker.LogDb.DASHBOARD); + + Map> headers = new HashMap<>(); + headers.put("Authorization", Collections.singletonList("Basic " + azureBoardsIntegration.getPersonalAuthToken())); + headers.put("content-type", Collections.singletonList("application/json-patch+json")); + + OriginalHttpRequest request = new OriginalHttpRequest(url, "", "POST", reqPayload.toString(), headers, ""); + try { + OriginalHttpResponse response = ApiExecutor.sendRequest(request, true, null, false, new ArrayList<>()); + logger.infoAndAddToDb("Status and Response from Azure Boards work item API: " + response.getStatusCode() + " | " + response.getBody()); + + String responsePayload = response.getBody(); + if (response.getStatusCode() > 201 || responsePayload == null) { + return null; + } + + try { + BasicDBObject respPayloadObj = BasicDBObject.parse(responsePayload); + try { + Object linksObj = respPayloadObj.get("_links"); + BasicDBObject links = BasicDBObject.parse(linksObj.toString()); + Object htmlObj = links.get("html"); + BasicDBObject html = BasicDBObject.parse(htmlObj.toString()); + Object href = html.get("href"); + return href.toString(); + } catch (Exception e) { + Object urlObj = respPayloadObj.get("url"); + return urlObj != null ? urlObj.toString() : null; + } + } catch (Exception e) { + logger.errorAndAddToDb("Error parsing work item response: " + e.getMessage(), LoggerMaker.LogDb.DASHBOARD); + return null; + } + } catch (Exception e) { + logger.errorAndAddToDb("Error sending work item request: " + e.getMessage(), LoggerMaker.LogDb.DASHBOARD); + return null; + } + } + public String getAzureBoardsBaseUrl() { return azureBoardsBaseUrl; } @@ -785,4 +802,13 @@ public String getEndpoint() { public void setEndpoint(String endpoint) { this.endpoint = endpoint; } + + public String getOriginalMessage() { + return originalMessage; + } + + public void setOriginalMessage(String originalMessage) { + this.originalMessage = originalMessage; + } + } \ No newline at end of file diff --git a/apps/dashboard/src/main/java/com/akto/utils/Utils.java b/apps/dashboard/src/main/java/com/akto/utils/Utils.java index 70aacdcab8..c58e7a88e3 100644 --- a/apps/dashboard/src/main/java/com/akto/utils/Utils.java +++ b/apps/dashboard/src/main/java/com/akto/utils/Utils.java @@ -670,14 +670,27 @@ public static String createDashboardUrlFromRequest(HttpServletRequest request) { } public static File createRequestFile(String originalMessage, String message) { - if (originalMessage == null || message == null) { + return createRequestFile(originalMessage, message, false); + } + + public static File createRequestFile(String originalMessage, String message, boolean isThreatActivity) { + if (originalMessage == null) { return null; } try { + // If explicitly marked as threat activity, use threat activity format + if (isThreatActivity) { + LoggerMaker logger = new LoggerMaker(Utils.class, LogDb.DASHBOARD); + logger.infoAndAddToDb("Creating request file for threat activity", LogDb.DASHBOARD); + return createThreatActivityRequestFile(originalMessage); + } + + // Original format for testing scenarios - requires both parameters with different structures + String origCurl = CurlUtils.getCurl(originalMessage); String testCurl = CurlUtils.getCurl(message); - HttpResponseParams origObj = HttpCallParser.parseKafkaMessage(originalMessage); + HttpResponseParams origObjParsed = HttpCallParser.parseKafkaMessage(originalMessage); BasicDBObject testRespObj = BasicDBObject.parse(message); BasicDBObject testPayloadObj = BasicDBObject.parse(testRespObj.getString("response")); String testResp = testPayloadObj.getString("body"); @@ -687,7 +700,7 @@ public static File createRequestFile(String originalMessage, String message) { FileUtils.writeStringToFile(tmpOutputFile, "Original Curl ----- \n\n", (String) null); FileUtils.writeStringToFile(tmpOutputFile, origCurl + "\n\n", (String) null, true); FileUtils.writeStringToFile(tmpOutputFile, "Original Api Response ----- \n\n", (String) null, true); - FileUtils.writeStringToFile(tmpOutputFile, origObj.getPayload() + "\n\n", (String) null, true); + FileUtils.writeStringToFile(tmpOutputFile, origObjParsed.getPayload() + "\n\n", (String) null, true); FileUtils.writeStringToFile(tmpOutputFile, "Test Curl ----- \n\n", (String) null, true); FileUtils.writeStringToFile(tmpOutputFile, testCurl + "\n\n", (String) null, true); @@ -701,6 +714,107 @@ public static File createRequestFile(String originalMessage, String message) { } } + /** + * Creates a request file for threat activity data format + * Uses parseKafkaMessage to parse the orig field which contains the same format as Kafka messages + */ + private static File createThreatActivityRequestFile(String origJson) { + LoggerMaker logger = new LoggerMaker(Utils.class, LogDb.DASHBOARD); + try { + logger.infoAndAddToDb("Creating threat activity request file from orig data", LogDb.DASHBOARD); + + // Parse using parseKafkaMessage since threat activity format matches Kafka message format + HttpResponseParams responseParams = HttpCallParser.parseKafkaMessage(origJson); + HttpRequestParams requestParams = responseParams.getRequestParams(); + + File tmpOutputFile = File.createTempFile("threat_activity", ".txt"); + + // Write Request section + FileUtils.writeStringToFile(tmpOutputFile, "Request ----- \n\n", (String) null); + + // Write request line: METHOD PATH HTTP/1.1 + String method = requestParams.getMethod() != null ? requestParams.getMethod() : "GET"; + String path = requestParams.getURL() != null ? requestParams.getURL() : "/"; + String type = requestParams.type != null ? requestParams.type : "HTTP/1.1"; + String requestLine = method + " " + path + " " + type; + FileUtils.writeStringToFile(tmpOutputFile, requestLine + "\n", (String) null, true); + + // Write request headers + Map> requestHeaders = requestParams.getHeaders(); + if (requestHeaders != null) { + for (Map.Entry> header : requestHeaders.entrySet()) { + String headerValue = String.join(", ", header.getValue()); + FileUtils.writeStringToFile(tmpOutputFile, header.getKey() + ": " + headerValue + "\n", (String) null, true); + } + } + + // Write request body if present + String requestPayload = requestParams.getPayload(); + if (requestPayload != null && !requestPayload.trim().isEmpty()) { + FileUtils.writeStringToFile(tmpOutputFile, "\n" + requestPayload + "\n\n", (String) null, true); + } else { + FileUtils.writeStringToFile(tmpOutputFile, "\n", (String) null, true); + } + + // Write Response section + FileUtils.writeStringToFile(tmpOutputFile, "Response ----- \n\n", (String) null, true); + + // Write status line: HTTP/1.1 STATUS_CODE STATUS_TEXT + int statusCode = responseParams.getStatusCode(); + String statusText = getStatusText(String.valueOf(statusCode)); + String responseType = responseParams.type != null ? responseParams.type : "HTTP/1.1"; + FileUtils.writeStringToFile(tmpOutputFile, responseType + " " + statusCode + " " + statusText + "\n", (String) null, true); + + // Write response headers + Map> responseHeaders = responseParams.getHeaders(); + if (responseHeaders != null) { + for (Map.Entry> header : responseHeaders.entrySet()) { + String headerValue = String.join(", ", header.getValue()); + FileUtils.writeStringToFile(tmpOutputFile, header.getKey() + ": " + headerValue + "\n", (String) null, true); + } + } + + // Write response body if present + String responsePayload = responseParams.getPayload(); + if (responsePayload != null && !responsePayload.trim().isEmpty()) { + FileUtils.writeStringToFile(tmpOutputFile, "\n" + responsePayload + "\n", (String) null, true); + } else { + FileUtils.writeStringToFile(tmpOutputFile, "\n", (String) null, true); + } + + logger.infoAndAddToDb("Successfully created threat activity request file: " + tmpOutputFile.getName(), LogDb.DASHBOARD); + return tmpOutputFile; + } catch (Exception e) { + logger.errorAndAddToDb("Error creating threat activity request file: " + e.getMessage(), LogDb.DASHBOARD); + e.printStackTrace(); + return null; + } + } + + private static String getStatusText(String statusCode) { + if (statusCode == null) return "OK"; + int code = 200; + try { + code = Integer.parseInt(statusCode); + } catch (NumberFormatException e) { + return "OK"; + } + + switch (code) { + case 200: return "OK"; + case 201: return "Created"; + case 204: return "No Content"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 500: return "Internal Server Error"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + default: return "OK"; + } + } + public static TestResult getTestResultFromTestingRunResult(TestingRunResult testingRunResult) { TestResult testResult; try { diff --git a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/threat_detection/components/SampleDetails.jsx b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/threat_detection/components/SampleDetails.jsx index fb5e6b702f..f7f223cefb 100644 --- a/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/threat_detection/components/SampleDetails.jsx +++ b/apps/dashboard/web/polaris_web/web/src/apps/dashboard/pages/threat_detection/components/SampleDetails.jsx @@ -367,6 +367,12 @@ Reference URL: ${window.location.href}`.trim(); func.setToast(true, false, "Creating Azure Boards Work Item"); + // Extract originalMessage from first attempt data for attachment + // For threat activity, orig contains both request and response + const originalMessage = data && data.length > 0 && data[0]?.orig + ? (typeof data[0].orig === 'string' ? data[0].orig : JSON.stringify(data[0].orig)) + : null; + // Call createGeneralAzureBoardsWorkItem API const response = await issuesApi.createGeneralAzureBoardsWorkItem({ title: workItemTitle, @@ -377,7 +383,8 @@ Reference URL: ${window.location.href}`.trim(); templateId: moreInfoData?.templateId, // Pass templateId (filterId) from threat policy endpoint: endPointStr, // Pass endpoint for title formatting aktoDashboardHostName: window.location.origin, - customABWorkItemFieldsPayload + customABWorkItemFieldsPayload, + originalMessage: originalMessage // Pass request/response data for attachment }); if (response?.errorMessage) {