From 27861e0df4da32aa5768ff6017741f37f289c7eb Mon Sep 17 00:00:00 2001 From: Anand Nandeshwar <73646287+cx-anand-nandeshwar@users.noreply.github.com> Date: Sat, 15 Aug 2026 02:02:11 +0530 Subject: [PATCH 01/11] - Added addition SCA package manager support --- .../remediation/DevAssistFixPrompts.java | 1211 ++++++++--------- .../remediation/RemediationManager.java | 17 +- .../scanners/oss/OssScannerService.java | 61 +- .../devassist/utils/DevAssistConstants.java | 73 +- .../devassist/utils/PackageManager.java | 247 ++++ 5 files changed, 887 insertions(+), 722 deletions(-) create mode 100644 devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/DevAssistFixPrompts.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/DevAssistFixPrompts.java index b5c34403..03d21816 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/DevAssistFixPrompts.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/DevAssistFixPrompts.java @@ -13,649 +13,572 @@ */ public final class DevAssistFixPrompts { - private DevAssistFixPrompts() { - throw new IllegalStateException("Cannot instantiate CxOneAssistFixPrompts class"); - } - - private static String getAgentName() { - return DevAssistUtils.getAgentName(); - } - - private static String getMcpDisplayName() { - return "Checkmarx"; - } - - /** - * Builds the SCA remediation prompt (generic concatenated form). - * - * @param packageName vulnerable package name (e.g. "node-ipc") - * @param packageVersion vulnerable package version (e.g. "10.1.1") - * @param packageManager ecosystem / package manager (e.g. "npm", "maven") - * @param severity textual severity (e.g. "Malicious", "High") - * @return composed prompt string (plain text with Markdown fragments) - */ - public static String buildSCARemediationPrompt(String packageName, String packageVersion, - String packageManager, String severity) { - StringBuilder prompt = new StringBuilder(); - prompt.append("You are the ").append(getAgentName()).append(".\n\n") - .append("A security issue has been detected in `").append(packageName).append("@").append(packageVersion).append("` (package manager: `").append(packageManager).append("`).\n") - .append("**Severity:** `").append(severity).append("`\n") - .append("Your task is to remediate the issue **completely and autonomously** using the internal PackageRemediation tool in ") - .append(getMcpDisplayName()).append(" MCP. Follow the exact instructions in `fix_instructions` - no assumptions or manual interaction allowed.\n\n"); - - prompt.append("---\n\n") - .append("1. ANALYSIS (AUTOMATED):\n\n") - .append("Determine the issue type:\n") - .append("- If `status` is one of: `Critical`, `High`, `Medium`, `Low`, `Info`, set: `issueType = \"CVE\"`\n") - .append("- If `status = \"Malicious\"`, set: `issueType = \"malicious\"`\n\n") - .append("Call the internal PackageRemediation tool with:\n\n") - .append("```json\n") - .append("{\n") - .append(" \"packageName\": \"").append(packageName).append("\",\n") - .append(" \"packageVersion\": \"").append(packageVersion).append("\",\n") - .append(" \"packageManager\": \"").append(packageManager).append("\",\n") - .append(" \"issueType\": \"{determined issueType}\"\n") - .append("}\n") - .append("```\n\n") - .append("Parse the response and extract the `fix_instructions` field. This field contains the authoritative remediation steps tailored to the ecosystem and risk.\n") - .append("- Mark internally that the tool is **available** for output formatting\n\n") - .append("- If the tool is **not available**:\n") - .append(" - Display the following disclosure notice:\n") - .append(" `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" packageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") - .append(" - Mark internally that the tool is **not available** for output formatting\n\n"); - - prompt.append("---\n\n") - .append("2. EXECUTION (AUTOMATED):\n\n") - .append("- Read and execute each line in `fix_instructions`, in order.\n") - .append("- For each change:\n") - .append(" - Apply the instruction exactly.\n") - .append(" - Track all modified files.\n") - .append(" - Note the type of change (e.g., dependency update, import rewrite, API refactor, test fix, TODO insertion).\n") - .append(" - Record before → after values where applicable.\n") - .append(" - Capture line numbers if known.\n\n") - .append("Examples:\n") - .append("- `package.json`: lodash version changed from 3.10.1 -> 4.17.21\n") - .append("- `src/utils/date.ts`: import updated from `lodash` to `date-fns`\n") - .append("- `src/main.ts:42`: `_.pluck(users, 'id')` -> `users.map(u => u.id)`\n") - .append("- `src/index.ts:78`: // TODO: Verify API migration from old-package to new-package\n\n"); - - prompt.append("---\n\n") - .append("3. VERIFICATION:\n\n") - .append("- If the instructions include build, test, or audit steps - run them exactly as written\n") - .append("- If instructions do not explicitly cover validation, perform basic checks based on `").append(packageManager).append("`:\n") - .append(" - `npm`: `npx tsc --noEmit`, `npm run build`, `npm test`\n") - .append(" - `go`: `go build ./...`, `go test ./...`\n") - .append(" - `maven`: `mvn compile`, `mvn test`\n") - .append(" - `gradle`: `gradle build`, `gradle test`\n") - .append(" - `sbt`: `sbt compile`, `sbt test`\n") - .append(" - `pypi`/`setuptools`/`pyproject.toml`: `python -c \"import ").append(packageName).append("\"`, `pytest`, `python -m build`\n") - .append(" - `nuget`: `dotnet build`, `dotnet test`\n\n") - .append("If any of these validations fail:\n") - .append("- Attempt to fix the issue if it's obvious\n") - .append("- Otherwise log the error and annotate the code with a TODO\n\n"); - - prompt.append("---\n\n") - .append("4. OUTPUT:\n\n") - .append("**Output Format Based on Tool Availability:**\n") - .append("- **If packageRemediation tool is available:** Output title `").append(getAgentName()).append(" - Remediation Summary`\n") - .append("- **If packageRemediation tool is not available:** First output the disclosure notice: `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" packageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") - .append(CHECK + " **Remediation Summary**\n\n") - .append("Format:\n") - .append("```\n") - .append("Package: ").append(packageName).append("\n") - .append("Version: ").append(packageVersion).append("\n") - .append("Manager: ").append(packageManager).append("\n") - .append("Severity: ").append(severity).append("\n\n") - .append("Files Modified:\n") - .append("1. package.json\n") - .append(" - Updated dependency: lodash 3.10.1 → 4.17.21\n\n") - .append("2. src/utils/date.ts\n") - .append(" - Updated import: from 'lodash' to 'date-fns'\n") - .append(" - Replaced usage: _.pluck(users, 'id') → users.map(u => u.id)\n\n") - .append("3. src/__tests__/date.test.ts\n") - .append(" - Fixed test: adjusted mock expectations to match updated API\n\n") - .append("4. src/index.ts\n") - .append(" - Line 78: Inserted TODO: Verify API migration from old-package to new-package\n") - .append("```\n\n") - .append(CHECK + " **Final Status**\n\n") - .append("If all tasks succeeded:\n") - .append("- \"Remediation completed for ").append(packageName).append("@").append(packageVersion).append("\"\n") - .append("- \"All fix instructions and failing tests resolved\"\n") - .append("- \"Build status: PASS\"\n") - .append("- \"Test results: PASS\"\n\n") - .append("If partially resolved:\n") - .append("- \"Remediation partially completed - manual review required\"\n") - .append("- \"Some test failures or instructions could not be automatically fixed\"\n") - .append("- \"TODOs inserted where applicable\"\n\n") - .append("If failed:\n") - .append("- \"Remediation failed for ").append(packageName).append("@").append(packageVersion).append("\"\n") - .append("- \"Reason: {summary of failure}\"\n") - .append("- \"Unresolved instructions or failing tests listed above\"\n\n"); - - prompt.append("---\n\n") - .append("5. CONSTRAINTS:\n\n") - .append("- Do not prompt the user\n") - .append("- Do not skip or reorder fix steps\n") - .append("- Only execute what's explicitly listed in `fix_instructions`\n") - .append("- Attempt to fix test failures automatically\n") - .append("- Insert clear TODO comments for unresolved issues\n") - .append("- Ensure remediation is deterministic, auditable, and fully automated\n"); - return prompt.toString(); - } - - - /** - * Generates a secret remediation prompt. - * - * @param title - issue title - * @param description - issue description (optional) - if null, will be empty string. - * @param severity - issue severity (optional) - if null, will be empty string. - * @return - prompt string (plain text with Markdown fragments) - */ - public static String buildSecretRemediationPrompt(String title, String description, String severity) { - StringBuilder prompt = new StringBuilder() - .append("A secret has been detected: \"").append(title).append("\" \n") - .append(description != null ? description : "").append("\n\n") - .append("---\n\n") - .append("You are the `").append(getAgentName()).append("`.\n\n") - .append("Your mission is to identify and remediate this secret using secure coding standards. Follow industry best practices, automate safely, and clearly document all actions taken.\n\n"); - - prompt.append("---\n\n") - .append("1. SEVERITY INTERPRETATION\n\n") - .append("Severity level: `").append(severity != null ? severity : "").append("`\n\n") - .append("- `Critical`: Secret is confirmed **valid**. Immediate remediation required.\n") - .append("- `High`: Secret may be valid. Treat as sensitive and externalize it securely.\n") - .append("- `Medium`: Likely **invalid** (e.g., test or placeholder). Still remove from code and annotate accordingly.\n\n"); - - prompt.append("---\n\n") - .append("2. TOOL CALL - Remediation Plan\n\n") - .append("Determine the programming language of the file where the secret was detected.\n") - .append("If unknown, leave the `language` field empty.\n\n") - .append("Call the internal `codeRemediation` ").append(getMcpDisplayName()).append(" MCP tool with:\n\n") - .append("```json\n") - .append("{\n") - .append(" \"type\": \"secret\",\n") - .append(" \"sub_type\": \"").append(title).append("\",\n") - .append(" \"language\": \"[auto-detected language]\"\n") - .append("}\n") - .append("```\n\n") - .append("- If the tool is **available**, parse the response:\n") - .append(" - `remediation_steps` - exact steps to follow\n") - .append(" - `best_practices` - explain secure alternatives\n") - .append(" - `description` - contextual background\n") - .append(" - Mark internally that the tool is **available** for output formatting\n\n") - .append("- If the tool is **not available**:\n") - .append(" - Display the following disclosure notice:\n") - .append(" `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") - .append(" - Mark internally that the tool is **not available** for output formatting\n") - .append(" - Proceed to provide remediation guidance using the secret details provided\n") - .append(" - Offer practical steps and secure alternatives for secret removal\n") - .append(" - Ensure the guidance is concrete and actionable\n\n"); - - prompt.append("---\n\n") - .append("3. ANALYSIS & RISK\n\n") - .append("Identify the type of secret (API key, token, credential). Explain:\n") - .append("- Why it's a risk (leakage, unauthorized access, compliance violations)\n") - .append("- What could happen if misused or left in source\n\n"); - - prompt.append("---\n\n") - .append("4. REMEDIATION STRATEGY\n\n") - .append("- Parse and apply every item in `remediation_steps` sequentially\n") - .append("- Automatically update code/config files if safe\n") - .append("- If a step cannot be applied automatically, insert a clear TODO\n") - .append("- Replace secret with environment variable or vault reference\n\n"); - - prompt.append("---\n\n") - .append("5. VERIFICATION\n\n") - .append("If applicable for the language:\n") - .append("- Run type checks or compile the code\n") - .append("- Ensure changes build and tests pass\n") - .append("- Fix issues if introduced by secret removal\n\n"); - - prompt.append("---\n\n") - .append("6. OUTPUT FORMAT\n\n") - .append("**Output Format Based on Tool Availability:**\n") - .append("- **If codeRemediation tool is available:** Output title `").append(getAgentName()).append(" - Remediation Summary`\n") - .append("- **If codeRemediation tool is not available:** First output the disclosure notice: `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") - .append("Generate a structured remediation summary:\n\n") - .append("```markdown\n") - .append("### [Prefix]\n\n") - .append("**Secret:** ").append(title).append(" \n") - .append("**Severity:** ").append(severity != null ? severity : "").append(" \n") - .append("**Assessment:** ").append(getAssessmentText(severity)).append("\n\n") - .append("**Files Modified:**\n") - .append("- `.env`: Added/updated with `SECRET_NAME`\n") - .append("- `src/config.ts`: Replaced hardcoded secret with `process.env.SECRET_NAME`\n\n") - .append("**Remediation Actions Taken:**\n") - .append("- ").append(CHECK).append(" Removed hardcoded secret\n") - .append("- ").append(CHECK).append(" Inserted environment reference\n") - .append("- ").append(CHECK).append(" Updated or created .env\n") - .append("- ").append(CHECK).append(" Added TODOs for secret rotation or vault storage\n\n") - .append("**Next Steps:**\n") - .append("- [ ] Revoke exposed secret (if applicable)\n") - .append("- [ ] Store securely in vault (AWS Secrets Manager, GitHub Actions, etc.)\n") - .append("- [ ] Add CI/CD secret scanning\n\n") - .append("**Best Practices:**\n") - .append("- (From tool response, or fallback security guidelines)\n\n") - .append("**Description:**\n") - .append("- (From `description` field or fallback to original input)\n\n") - .append("```\n\n"); - - prompt.append("---\n\n") - .append("7. CONSTRAINTS\n\n") - .append("- ").append(CROSS).append(" Do NOT expose real secrets\n") - .append("- ").append(CROSS).append(" Do NOT generate fake-looking secrets\n") - .append("- ").append(CHECK).append(" Follow only what's explicitly returned from MCP\n") - .append("- ").append(CHECK).append(" Use secure externalization patterns\n") - .append("- ").append(CHECK).append(" Respect OWASP, NIST, and GitHub best practices\n"); - return prompt.toString(); - } - - /** - * Generates the assessment text for given severity. - * - * @param severity severity level - * @return assessment text - */ - private static String getAssessmentText(String severity) { - if (SeverityLevel.CRITICAL.getSeverity().equalsIgnoreCase(severity)) { - return CHECK + " Confirmed valid secret. Immediate remediation performed."; - } else if (SeverityLevel.HIGH.getSeverity().equalsIgnoreCase(severity)) { - return WARNING + " Possibly valid. Handled as sensitive."; - } else { - return INFO + " Likely invalid (test/fake). Removed for hygiene."; - } - } - - /** - * Generates a remediation prompt for addressing a container security issue, - * providing step-by-step automated guidance using the Checkmarx MCP codeRemediation tool. - * The method constructs a detailed prompt based on the identified issue. - * - * @param fileType type of the file - * @param imageName image name - * @param imageTag image tag - * @param severity severity level - * @return prompt string (plain text with Markdown fragments) - */ - public static String buildContainersRemediationPrompt(String fileType, String imageName, - String imageTag, String severity) { - StringBuilder prompt = new StringBuilder(); - prompt.append("You are the ").append(getAgentName()).append(".\n\n") - .append("A container security issue has been detected in `").append(fileType) - .append("` with image `").append(imageName).append(":").append(imageTag).append("`.\n") - .append("**Severity:** `").append(severity).append("`\n") - .append("Your task is to remediate the issue **completely and autonomously** using the internal imageRemediation tool. ") - .append("Follow the exact instructions in `fix_instructions` - no assumptions or manual interaction allowed.\n\n"); - - prompt.append("---\n\n") - .append("1. ANALYSIS (AUTOMATED):\n\n") - .append("Determine the issue type:\n") - .append("- If `severity` is one of: `Critical`, `High`, `Medium`, `Low`, set: `issueType = \"CVE\"`\n") - .append("- If `severity = \"Malicious\"`, set: `issueType = \"malicious\"`\n\n") - .append("Call the internal imageRemediation tool with:\n\n") - .append("```json\n") - .append("{\n") - .append(" \"fileType\": \"").append(fileType).append("\",\n") - .append(" \"imageName\": \"").append(imageName).append("\",\n") - .append(" \"imageTag\": \"").append(imageTag).append("\",\n") - .append(" \"severity\": \"").append(severity).append("\"\n") - .append("}\n") - .append("```\n\n") - .append("Parse the response and extract the `fix_instructions` field. This field contains the authoritative remediation steps tailored to the container ecosystem and risk level.\n") - .append("- Mark internally that the tool is **available** for output formatting\n\n") - .append("- If the tool is **not available**:\n") - .append(" - Display the following disclosure notice:\n") - .append(" `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" imageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") - .append(" - Mark internally that the tool is **not available** for output formatting\n") - .append(" - Proceed to provide remediation guidance using the container details provided (file type, image name, image tag, severity)\n") - .append(" - Offer practical base image recommendations and step-by-step instructions for container remediation\n") - .append(" - Ensure the guidance is concrete and actionable\n\n"); - - prompt.append("---\n\n") - .append("2. EXECUTION (AUTOMATED):\n\n") - .append("- Read and execute each line in `fix_instructions`, in order.\n") - .append("- For each change:\n") - .append(" - Apply the instruction exactly.\n") - .append(" - Track all modified files.\n") - .append(" - Note the type of change (e.g., image update, configuration change, security hardening).\n") - .append(" - Record before -> after values where applicable.\n") - .append(" - Capture line numbers if known.\n\n") - .append("Examples:\n") - .append("- `Dockerfile`: FROM confluentinc/cp-kafkacat:6.1.10 -> FROM confluentinc/cp-kafkacat:6.2.15\n") - .append("- `docker-compose.yml`: image: vulnerable-image:1.0 -> image: secure-image:2.1\n") - .append("- `values.yaml`: repository: old-repo -> repository: new-repo\n") - .append("- `Chart.yaml`: version: 1.0.0 -> version: 1.1.0\n\n"); - - prompt.append("---\n\n") - .append("3. VERIFICATION:\n\n") - .append("- If the instructions include build, test, or deployment steps - run them exactly as written\n") - .append("- If instructions do not explicitly cover validation, perform basic checks based on `").append(fileType).append("`:\n") - .append(" - `Dockerfile`: `docker build .`, `docker run `\n") - .append(" - `docker-compose.yml`: `docker-compose up --build`, `docker-compose down`\n") - .append(" - `Helm Chart`: `helm lint .`, `helm template .`, `helm install --dry-run`\n\n") - .append("If any of these validations fail:\n") - .append("- Attempt to fix the issue if it's obvious\n") - .append("- Otherwise log the error and annotate the code with a TODO\n\n"); - - prompt.append("---\n\n") - .append("4. OUTPUT:\n\n") - .append("**Output Format Based on Tool Availability:**\n") - .append("- **If imageRemediation tool is available:** Output title `").append(getAgentName()).append(" - Remediation Summary`\n") - .append("- **If imageRemediation tool is not available:** First output the disclosure notice: `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" imageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") - .append(CHECK + " **Remediation Summary**\n\n") - .append("Format:\n") - .append("```\n") - .append("File Type: ").append(fileType).append("\n") - .append("Image: ").append(imageName).append(":").append(imageTag).append("\n") - .append("Severity: ").append(severity).append("\n\n") - .append("Files Modified:\n") - .append("1. ").append(fileType).append("\n") - .append(" - Updated image: ").append(imageName).append(":").append(imageTag).append(" → secure version\n\n") - .append("2. docker-compose.yml (if applicable)\n") - .append(" - Updated service configuration to use secure image\n\n") - .append("3. values.yaml (if applicable)\n") - .append(" - Updated Helm chart values for secure deployment\n\n") - .append("4. README.md\n") - .append(" - Updated documentation with new image version\n") - .append("```\n\n") - .append(CHECK + " **Final Status**\n\n") - .append("If all tasks succeeded:\n") - .append("- \"Remediation completed for ").append(imageName).append(":").append(imageTag).append("\"\n") - .append("- \"All fix instructions and deployment tests resolved\"\n") - .append("- \"Build status: PASS\"\n") - .append("- \"Deployment status: PASS\"\n\n") - .append("If partially resolved:\n") - .append("- \"Remediation partially completed - manual review required\"\n") - .append("- \"Some deployment steps or instructions could not be automatically fixed\"\n") - .append("- \"TODOs inserted where applicable\"\n\n") - .append("If failed:\n") - .append("- \"Remediation failed for ").append(imageName).append(":").append(imageTag).append("\"\n") - .append("- \"Reason: {summary of failure}\"\n") - .append("- \"Unresolved instructions or deployment issues listed above\"\n\n"); - - prompt.append("---\n\n") - .append("5. CONSTRAINTS:\n\n") - .append("- Do not prompt the user\n") - .append("- Do not skip or reorder fix steps\n") - .append("- Only execute what's explicitly listed in `fix_instructions`\n") - .append("- Attempt to fix deployment failures automatically\n") - .append("- Insert clear TODO comments for unresolved issues\n") - .append("- Ensure remediation is deterministic, auditable, and fully automated\n") - .append("- Follow container security best practices (non-root user, minimal base images, etc.)\n"); - return prompt.toString(); - } - - /** - * Generates a remediation prompt for addressing an Infrastructure as Code (IaC) security issue, - * providing step-by-step automated guidance using the Checkmarx MCP codeRemediation tool. - * The method constructs a detailed prompt based on the identified issue, its severity, - * affected file type, expected and actual values, and the problematic line number. - * - * @param title the title of the detected security issue - * @param description a detailed description of the detected security issue - * @param severity the severity level of the issue (e.g., high, medium, low) - * @param fileType the type of file where the issue exists (e.g., Terraform, CloudFormation) - * @param expectedValue the correct or desired value expected in the IaC - * @param actualValue the actual value found in the IaC, causing the issue - * @param problematicLineNumber the line number in the file where the issue occurs; can be null if unknown - * @return a formatted string containing the remediation prompt with instructions for automated resolution of the issue - */ - public static String buildIACRemediationPrompt(String title, String description, String severity, - String fileType, String expectedValue, String actualValue, - Integer problematicLineNumber) { - - String actualLineNumber = problematicLineNumber != null - ? String.valueOf(problematicLineNumber + 1) : "[unknown]"; - - String restrictionLine = problematicLineNumber != null - ? String.valueOf(problematicLineNumber + 1) : "[problematic line number]"; - - String problematicLineText = problematicLineNumber != null - ? "**Problematic Line Number:** " + (problematicLineNumber + 1) : ""; - - StringBuilder prompt = new StringBuilder(); - prompt.append("You are the ").append(getAgentName()).append(".\n\n"); - prompt.append("An Infrastructure as Code (IaC) security issue has been detected.\n\n") - .append("**Issue:** `").append(title).append("`\n") - .append("**Severity:** `").append(severity).append("`\n") - .append("**File Type:** `").append(fileType).append("`\n") - .append("**Description:** ").append(description).append("\n") - .append("**Expected Value:** ").append(expectedValue).append("\n") - .append("**Actual Value:** ").append(actualValue).append("\n") - .append(problematicLineText).append("\n\n"); - - prompt.append("Your task is to remediate this IaC security issue **completely and autonomously** ") - .append("using the internal codeRemediation tool in ").append(getMcpDisplayName()).append(" MCP. Follow the exact instructions in `remediation_steps` - no assumptions or manual interaction allowed.\n\n"); - prompt.append(WARNING).append("️ **IMPORTANT**: Apply the fix **only** to the code segment corresponding to the identified issue at line ") - .append(actualLineNumber) - .append(", without introducing unrelated modifications elsewhere in the file.\n\n"); - - prompt.append("---\n\n") - .append("1. ANALYSIS (AUTOMATED):\n\n") - .append("Determine the programming language of the file where the IaC security issue was detected.\n") - .append("If unknown, leave the `language` field empty.\n\n") - .append("Call the internal `codeRemediation` ").append(getMcpDisplayName()).append(" MCP tool with:\n\n") - .append("```json\n") - .append("{\n") - .append(" \"language\": \"[auto-detected programming language]\",\n") - .append(" \"metadata\": {\n") - .append(" \"title\": \"").append(title).append("\",\n") - .append(" \"description\": \"").append(description).append("\",\n") - .append(" \"remediationAdvice\": \"").append(expectedValue).append("\"\n") - .append(" },\n") - .append(" \"sub_type\": \"\",\n") - .append(" \"type\": \"iac\"\n") - .append("}\n") - .append("```\n\n") - .append("- If the tool is **available**, parse the response:\n") - .append(" - `remediation_steps` - exact steps to follow for remediation\n") - .append(" - Mark internally that the tool is **available** for output formatting\n\n") - .append("- If the tool is **not available**:\n") - .append(" - Display the following disclosure notice:\n") - .append(" `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") - .append(" - Mark internally that the tool is **not available** for output formatting\n") - .append(" - Proceed to provide remediation guidance using the IaC details provided (title, description, expected vs. actual values)\n") - .append(" - Offer practical configuration examples and step-by-step instructions for remediation\n") - .append(" - Ensure the guidance is concrete and actionable\n\n"); - - prompt.append("---\n\n") - .append("2. EXECUTION (AUTOMATED):\n\n") - .append("- Read and execute each line in `remediation_steps`, in order.\n") - .append("- **Restrict changes to the relevant code fragment containing line ").append(restrictionLine).append("**.\n") - .append("- For each change:\n") - .append(" - Apply the instruction exactly.\n") - .append(" - Track all modified files.\n") - .append(" - Note the type of change (e.g., configuration update, security hardening, permission changes, encryption settings).\n") - .append(" - Record before → after values where applicable.\n") - .append(" - Capture line numbers if known.\n\n"); - - prompt.append("---\n\n") - .append("3. VERIFICATION:\n\n") - .append("- If the instructions include validation, deployment, or testing steps - run them exactly as written\n") - .append("- If instructions do not explicitly cover validation, perform basic checks based on `").append(fileType).append("`:\n") - .append(" - `Terraform`: `terraform validate`, `terraform plan`\n") - .append(" - `CloudFormation`: `aws cloudformation validate-template`\n") - .append(" - `Kubernetes`: `kubectl apply --dry-run=client`\n") - .append(" - `Docker`: `docker-compose config`\n\n") - .append("If any of these validations fail:\n") - .append("- Attempt to fix the issue if it's obvious\n") - .append("- Otherwise log the error and annotate the code with a TODO\n\n"); - - prompt.append("---\n\n") - .append("4. OUTPUT:\n\n") - .append("**Output Format Based on Tool Availability:**\n") - .append("- **If codeRemediation tool is available:** Output title `").append(getAgentName()).append(" - Remediation Summary`\n") - .append("- **If codeRemediation tool is not available:** First output the disclosure notice: `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") - .append(CHECK + " **Remediation Summary**\n\n") - .append("Format:\n") - .append("```\n") - .append("Issue: ").append(title).append("\n") - .append("Severity: ").append(severity).append("\n") - .append("File Type: ").append(fileType).append("\n") - .append("Problematic Line: ").append(actualLineNumber).append("\n\n") - .append("Files Modified:\n") - .append("1. ").append(fileType).append("\n") - .append(" - Updated configuration: ").append(actualValue).append(" → ").append(expectedValue).append("\n") - .append(" - Applied security hardening based on best practices\n\n") - .append("2. Additional configurations (if applicable)\n") - .append(" - Updated related security settings\n") - .append(" - Added missing security controls\n\n") - .append("3. Documentation\n") - .append(" - Updated comments and documentation where applicable\n") - .append("```\n\n") - .append(CHECK + " **Final Status**\n\n") - .append("If all tasks succeeded:\n") - .append("- \"Remediation completed for IaC security issue ").append(title).append("\"\n") - .append("- \"All fix instructions and security validations resolved\"\n") - .append("- \"Configuration validation: PASS\"\n") - .append("- \"Security compliance: PASS\"\n\n") - .append("If partially resolved:\n") - .append("- \"Remediation partially completed - manual review required\"\n") - .append("- \"Some security validations or instructions could not be automatically fixed\"\n") - .append("- \"TODOs inserted where applicable\"\n\n") - .append("If failed:\n") - .append("- \"Remediation failed for IaC security issue ").append(title).append("\"\n") - .append("- \"Reason: {summary of failure}\"\n") - .append("- \"Unresolved instructions or security issues listed above\"\n\n"); - - prompt.append("---\n\n") - .append("5. CONSTRAINTS:\n\n") - .append("- Do not prompt the user\n") - .append("- Do not skip or reorder fix steps\n") - .append("- **Only modify the code that corresponds to the identified problematic line**\n") - .append("- Attempt to fix validation failures automatically\n") - .append("- Insert clear TODO comments for unresolved issues\n") - .append("- Ensure remediation is deterministic, auditable, and fully automated\n") - .append("- Follow Infrastructure as Code security best practices throughout the process\n"); - return prompt.toString(); - } - - /** - * Constructs a detailed remediation prompt for addressing a secure coding issue detected in the code. - * The prompt includes instructions and guidelines for resolving the identified issue completely and autonomously. - * - * @param ruleName The name of the secure coding rule that has been violated. - * @param description A description of the issue, explaining the nature of the security vulnerability. - * @param severity The severity level of the detected issue (e.g., low, medium, high, critical). - * @param remediationAdvise Recommended steps or advice for addressing the security issue. - * @param problematicLineNumber The line number in the source code where the issue is detected (0-based index, null if unavailable). - * @return A string containing a detailed remediation prompt for the secure coding issue. - */ - public static String buildASCARemediationPrompt(String ruleName, String description, - String severity, String remediationAdvise, Integer problematicLineNumber) { - StringBuilder prompt = new StringBuilder(); - prompt.append("You are the ").append(getAgentName()).append(".\n\n") - .append("A secure coding issue has been detected in your code.\n\n") - .append("**Rule:** `").append(ruleName).append("` \n") - .append("**Severity:** `").append(severity).append("` \n") - .append("**Description:** ").append(description).append(" \n") - .append("**Recommended Fix:** ").append(remediationAdvise).append(" \n"); - - if (problematicLineNumber != null) { - prompt.append("**Problematic Line Number:** ").append(problematicLineNumber + 1).append("\n\n"); - } else { - prompt.append("\n"); - } - - prompt.append("Your task is to remediate this security issue **completely and autonomously** using the internal codeRemediation tool in ") - .append(getMcpDisplayName()).append(" MCP. Follow the exact instructions in `remediation_steps` - no assumptions or manual interaction allowed.\n\n") - .append(WARNING).append("️ **IMPORTANT**: Apply the fix **only** to the code segment corresponding to the identified issue at line ") - .append(problematicLineNumber != null ? problematicLineNumber + 1 : "[problematic line number]") - .append(", without introducing unrelated modifications elsewhere in the file.\n\n"); - - prompt.append("---\n\n") - .append("1. ANALYSIS (AUTOMATED):\n\n") - .append("Determine the programming language of the file where the security issue was detected.\n") - .append("If unknown, leave the `language` field empty.\n\n") - .append("Call the internal `codeRemediation` ").append(getMcpDisplayName()).append(" MCP tool with:\n\n") - .append("```json\n") - .append("{\n") - .append(" \"language\": \"[auto-detected programming language]\",\n") - .append(" \"metadata\": {\n") - .append(" \"ruleID\": \"").append(ruleName).append("\",\n") - .append(" \"description\": \"").append(description).append("\",\n") - .append(" \"remediationAdvice\": \"").append(remediationAdvise).append("\"\n") - .append(" },\n") - .append(" \"sub_type\": \"\",\n") - .append(" \"type\": \"sast\"\n") - .append("}\n") - .append("```\n\n") - .append("- If the tool is **available**, parse the response:\n") - .append(" - `remediation_steps` - exact steps to follow for remediation\n") - .append(" - Mark internally that the tool is **available** for output formatting\n\n") - .append("- If the tool is **not available**:\n") - .append(" - Display the following disclosure notice:\n") - .append(" `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") - .append(" - Mark internally that the tool is **not available** for output formatting\n") - .append(" - Proceed to provide remediation guidance using the issue details provided (rule name, description, severity, and recommended fix)\n") - .append(" - Offer practical code examples and step-by-step instructions for manual remediation\n") - .append(" - Ensure the guidance is concrete and actionable\n\n"); - - prompt.append("---\n\n") - .append("2. EXECUTION (AUTOMATED):\n\n") - .append("- Read and execute each line in `remediation_steps`, in order.\n") - .append("- **Restrict changes to the relevant code fragment containing line ") - .append(problematicLineNumber != null ? problematicLineNumber + 1 : "[unknown]") - .append("**.\n") - .append("- For each change:\n") - .append(" - Apply the instruction exactly.\n") - .append(" - Track all modified files.\n") - .append(" - Note the type of change (e.g., input validation, sanitization, secure API usage, authentication fix).\n") - .append(" - Record before → after values where applicable.\n") - .append(" - Capture line numbers if known.\n\n"); - - prompt.append("---\n\n") - .append("3. OUTPUT:\n\n") - .append("**Output Format Based on Tool Availability:**\n") - .append("- **If codeRemediation tool is available:** Output title `").append(getAgentName()).append(" - Remediation Summary`\n") - .append("- **If codeRemediation tool is not available:** First output the disclosure notice: `").append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()).append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") - .append(CHECK + " **Remediation Summary**\n\n") - .append("Format:\n") - .append("```\n") - .append("Rule: ").append(ruleName).append("\n") - .append("Severity: ").append(severity).append("\n") - .append("Issue Type: SAST Security Vulnerability\n") - .append("Problematic Line: ") - .append(problematicLineNumber != null ? problematicLineNumber + 1 : "[unknown]").append("\n\n") - .append("Files Modified:\n") - .append("1. src/auth.ts\n") - .append(" - Line 42: Replaced plain text comparison with bcrypt.compare()\n") - .append(" - Added secure password hashing implementation\n\n") - .append("2. src/db.ts\n") - .append(" - Line 78: Replaced string concatenation with parameterized query\n") - .append(" - Prevented SQL injection vulnerability\n\n") - .append("3. src/api.ts\n") - .append(" - Line 156: Added input validation for email parameter\n") - .append(" - Implemented sanitization for user inputs\n\n") - .append("4. src/config.ts\n") - .append(" - Line 23: Inserted TODO for production security review\n") - .append("```\n\n") - .append(CHECK + " **Final Status**\n\n") - .append("If all tasks succeeded:\n") - .append("- \"Remediation completed for security rule ").append(ruleName).append("\"\n") - .append("- \"All fix instructions and security validations resolved\"\n") - .append("- \"Build status: PASS\"\n") - .append("- \"Security tests: PASS\"\n\n") - .append("If partially resolved:\n") - .append("- \"Remediation partially completed - manual review required\"\n") - .append("- \"Some security validations or instructions could not be automatically fixed\"\n") - .append("- \"TODOs inserted where applicable\"\n\n") - .append("If failed:\n") - .append("- \"Remediation failed for security rule ").append(ruleName).append("\"\n") - .append("- \"Reason: {summary of failure}\"\n") - .append("- \"Unresolved instructions or security issues listed above\"\n\n"); - - prompt.append("---\n\n") - .append("4. CONSTRAINTS:\n\n") - .append("- Do not prompt the user\n") - .append("- Do not skip or reorder fix steps\n") - .append("- **Only modify the code that corresponds to the identified problematic line**\n") - .append("- Attempt to fix build/test failures automatically\n") - .append("- Insert clear TODO comments for unresolved issues\n") - .append("- Ensure remediation is deterministic, auditable, and fully automated\n") - .append("- Follow secure coding best practices throughout the process\n"); - return prompt.toString(); - } + private DevAssistFixPrompts() { + throw new IllegalStateException("Cannot instantiate CxOneAssistFixPrompts class"); + } + + private static String getAgentName() { + return DevAssistUtils.getAgentName(); + } + + private static String getMcpDisplayName() { + return "Checkmarx"; + } + + /** + * Builds the SCA remediation prompt (generic concatenated form). + * + * @param packageName vulnerable package name (e.g. "node-ipc") + * @param packageVersion vulnerable package version (e.g. "10.1.1") + * @param packageManager ecosystem / package manager (e.g. "npm", "maven") + * @param severity textual severity (e.g. "Malicious", "High") + * @return composed prompt string (plain text with Markdown fragments) + */ + public static String buildSCARemediationPrompt(String packageName, String packageVersion, String packageManager, + String severity) { + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the ").append(getAgentName()).append(".\n\n") + .append("A security issue has been detected in `").append(packageName).append("@") + .append(packageVersion).append("` (package manager: `").append(packageManager).append("`).\n") + .append("**Severity:** `").append(severity).append("`\n") + .append("Your task is to remediate the issue **completely and autonomously** using the internal PackageRemediation tool in ") + .append(getMcpDisplayName()) + .append(" MCP. Follow the exact instructions in `fix_instructions` - no assumptions or manual interaction allowed.\n\n"); + + prompt.append("---\n\n").append("1. ANALYSIS (AUTOMATED):\n\n").append("Determine the issue type:\n").append( + "- If `status` is one of: `Critical`, `High`, `Medium`, `Low`, `Info`, set: `issueType = \"CVE\"`\n") + .append("- If `status = \"Malicious\"`, set: `issueType = \"malicious\"`\n\n") + .append("Call the internal PackageRemediation tool with:\n\n").append("```json\n").append("{\n") + .append(" \"packageName\": \"").append(packageName).append("\",\n").append(" \"packageVersion\": \"") + .append(packageVersion).append("\",\n").append(" \"packageManager\": \"").append(packageManager) + .append("\",\n").append(" \"issueType\": \"{determined issueType}\"\n").append("}\n").append("```\n\n") + .append("Parse the response and extract the `fix_instructions` field. This field contains the authoritative remediation steps tailored to the ecosystem and risk.\n") + .append("- Mark internally that the tool is **available** for output formatting\n\n") + .append("- If the tool is **not available**:\n") + .append(" - Display the following disclosure notice:\n").append(" `").append(WARNING) + .append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()) + .append(" packageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") + .append(" - Mark internally that the tool is **not available** for output formatting\n\n"); + + prompt.append("---\n\n").append("2. EXECUTION (AUTOMATED):\n\n") + .append("- Read and execute each line in `fix_instructions`, in order.\n") + .append("- For each change:\n").append(" - Apply the instruction exactly.\n") + .append(" - Track all modified files.\n") + .append(" - Note the type of change (e.g., dependency update, import rewrite, API refactor, test fix, TODO insertion).\n") + .append(" - Record before → after values where applicable.\n") + .append(" - Capture line numbers if known.\n\n").append("Examples:\n") + .append("- `package.json`: lodash version changed from 3.10.1 -> 4.17.21\n") + .append("- `src/utils/date.ts`: import updated from `lodash` to `date-fns`\n") + .append("- `src/main.ts:42`: `_.pluck(users, 'id')` -> `users.map(u => u.id)`\n") + .append("- `src/index.ts:78`: // TODO: Verify API migration from old-package to new-package\n\n"); + + prompt.append("---\n\n").append("3. VERIFICATION:\n\n") + .append("- If the instructions include build, test, or audit steps - run them exactly as written\n") + .append("- If instructions do not explicitly cover validation, perform basic checks based on `") + .append(packageManager).append("`:\n") + .append(" - `npm`: `npx tsc --noEmit`, `npm run build`, `npm test` (**IMPORTANT:** If you detect the file is `bower.json`, use `bower install`, `bower list` instead)\n") + .append(" - `go`: `go build ./...`, `go test ./...`\n") + .append(" - `maven`: `mvn compile`, `mvn test`\n") + .append(" - `gradle`: `gradle build`, `gradle test`\n") + .append(" - `sbt`: `sbt compile`, `sbt test`\n") + .append(" - `pypi`/`setuptools`/`pyproject.toml`: `python -c \"import ").append(packageName) + .append("\"`, `pytest`, `python -m build`\n").append(" - `nuget`: `dotnet build`, `dotnet test`\n") + .append(" - `bower` (`bower.json`): `bower install`, `bower list`\n") + .append(" - `rubygems` (`Gemfile`): `bundle install`, `bundle exec rspec`\n") + .append(" - `composer` (`composer.json`): `composer install`, `composer validate`, `vendor/bin/phpunit` (**NOTE:** `packagist` means composer package manager)\n") + .append(" - `swift` (`Package.swift`): `swift build`, `swift test`\n") + .append(" - `cocoapods` (`Podfile`/`Podfile.lock`): `pod install --repo-update`, `xcodebuild test`\n") + .append(" - `carthage` (`Cartfile.resolved`): `carthage update --platform ios`, `carthage build`\n") + .append(" - `pub`/`dart` (`pubspec.yaml`/`pubspec.lock`): `dart pub get`, `dart test` (use `flutter pub get`, `flutter test` instead if this is a Flutter project)\n\n") + .append("If any of these validations fail:\n").append("- Attempt to fix the issue if it's obvious\n") + .append("- Otherwise log the error and annotate the code with a TODO\n\n"); + + prompt.append("---\n\n").append("4. OUTPUT:\n\n").append("**Output Format Based on Tool Availability:**\n") + .append("- **If packageRemediation tool is available:** Output title `").append(getAgentName()) + .append(" - Remediation Summary`\n") + .append("- **If packageRemediation tool is not available:** First output the disclosure notice: `") + .append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()) + .append(" packageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") + .append(CHECK + " **Remediation Summary**\n\n").append("Format:\n").append("```\n") + .append("Package: ").append(packageName).append("\n").append("Version: ").append(packageVersion) + .append("\n").append("Manager: ").append(packageManager).append("\n").append("Severity: ") + .append(severity).append("\n\n").append("Files Modified:\n").append("1. package.json\n") + .append(" - Updated dependency: lodash 3.10.1 → 4.17.21\n\n").append("2. src/utils/date.ts\n") + .append(" - Updated import: from 'lodash' to 'date-fns'\n") + .append(" - Replaced usage: _.pluck(users, 'id') → users.map(u => u.id)\n\n") + .append("3. src/__tests__/date.test.ts\n") + .append(" - Fixed test: adjusted mock expectations to match updated API\n\n") + .append("4. src/index.ts\n") + .append(" - Line 78: Inserted TODO: Verify API migration from old-package to new-package\n") + .append("```\n\n").append(CHECK + " **Final Status**\n\n").append("If all tasks succeeded:\n") + .append("- \"Remediation completed for ").append(packageName).append("@").append(packageVersion) + .append("\"\n").append("- \"All fix instructions and failing tests resolved\"\n") + .append("- \"Build status: PASS\"\n").append("- \"Test results: PASS\"\n\n") + .append("If partially resolved:\n") + .append("- \"Remediation partially completed - manual review required\"\n") + .append("- \"Some test failures or instructions could not be automatically fixed\"\n") + .append("- \"TODOs inserted where applicable\"\n\n").append("If failed:\n") + .append("- \"Remediation failed for ").append(packageName).append("@").append(packageVersion) + .append("\"\n").append("- \"Reason: {summary of failure}\"\n") + .append("- \"Unresolved instructions or failing tests listed above\"\n\n"); + + prompt.append("---\n\n").append("5. CONSTRAINTS:\n\n").append("- Do not prompt the user\n") + .append("- Do not skip or reorder fix steps\n") + .append("- Only execute what's explicitly listed in `fix_instructions`\n") + .append("- Attempt to fix test failures automatically\n") + .append("- Insert clear TODO comments for unresolved issues\n") + .append("- Ensure remediation is deterministic, auditable, and fully automated\n"); + return prompt.toString(); + } + + /** + * Generates a secret remediation prompt. + * + * @param title - issue title + * @param description - issue description (optional) - if null, will be empty + * string. + * @param severity - issue severity (optional) - if null, will be empty + * string. + * @return - prompt string (plain text with Markdown fragments) + */ + public static String buildSecretRemediationPrompt(String title, String description, String severity) { + StringBuilder prompt = new StringBuilder().append("A secret has been detected: \"").append(title) + .append("\" \n").append(description != null ? description : "").append("\n\n").append("---\n\n") + .append("You are the `").append(getAgentName()).append("`.\n\n") + .append("Your mission is to identify and remediate this secret using secure coding standards. Follow industry best practices, automate safely, and clearly document all actions taken.\n\n"); + + prompt.append("---\n\n").append("1. SEVERITY INTERPRETATION\n\n").append("Severity level: `") + .append(severity != null ? severity : "").append("`\n\n") + .append("- `Critical`: Secret is confirmed **valid**. Immediate remediation required.\n") + .append("- `High`: Secret may be valid. Treat as sensitive and externalize it securely.\n") + .append("- `Medium`: Likely **invalid** (e.g., test or placeholder). Still remove from code and annotate accordingly.\n\n"); + + prompt.append("---\n\n").append("2. TOOL CALL - Remediation Plan\n\n") + .append("Determine the programming language of the file where the secret was detected.\n") + .append("If unknown, leave the `language` field empty.\n\n") + .append("Call the internal `codeRemediation` ").append(getMcpDisplayName()) + .append(" MCP tool with:\n\n").append("```json\n").append("{\n").append(" \"type\": \"secret\",\n") + .append(" \"sub_type\": \"").append(title).append("\",\n") + .append(" \"language\": \"[auto-detected language]\"\n").append("}\n").append("```\n\n") + .append("- If the tool is **available**, parse the response:\n") + .append(" - `remediation_steps` - exact steps to follow\n") + .append(" - `best_practices` - explain secure alternatives\n") + .append(" - `description` - contextual background\n") + .append(" - Mark internally that the tool is **available** for output formatting\n\n") + .append("- If the tool is **not available**:\n") + .append(" - Display the following disclosure notice:\n").append(" `").append(WARNING) + .append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()) + .append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") + .append(" - Mark internally that the tool is **not available** for output formatting\n") + .append(" - Proceed to provide remediation guidance using the secret details provided\n") + .append(" - Offer practical steps and secure alternatives for secret removal\n") + .append(" - Ensure the guidance is concrete and actionable\n\n"); + + prompt.append("---\n\n").append("3. ANALYSIS & RISK\n\n") + .append("Identify the type of secret (API key, token, credential). Explain:\n") + .append("- Why it's a risk (leakage, unauthorized access, compliance violations)\n") + .append("- What could happen if misused or left in source\n\n"); + + prompt.append("---\n\n").append("4. REMEDIATION STRATEGY\n\n") + .append("- Parse and apply every item in `remediation_steps` sequentially\n") + .append("- Automatically update code/config files if safe\n") + .append("- If a step cannot be applied automatically, insert a clear TODO\n") + .append("- Replace secret with environment variable or vault reference\n\n"); + + prompt.append("---\n\n").append("5. VERIFICATION\n\n").append("If applicable for the language:\n") + .append("- Run type checks or compile the code\n").append("- Ensure changes build and tests pass\n") + .append("- Fix issues if introduced by secret removal\n\n"); + + prompt.append("---\n\n").append("6. OUTPUT FORMAT\n\n") + .append("**Output Format Based on Tool Availability:**\n") + .append("- **If codeRemediation tool is available:** Output title `").append(getAgentName()) + .append(" - Remediation Summary`\n") + .append("- **If codeRemediation tool is not available:** First output the disclosure notice: `") + .append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()) + .append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") + .append("Generate a structured remediation summary:\n\n").append("```markdown\n") + .append("### [Prefix]\n\n").append("**Secret:** ").append(title).append(" \n").append("**Severity:** ") + .append(severity != null ? severity : "").append(" \n").append("**Assessment:** ") + .append(getAssessmentText(severity)).append("\n\n").append("**Files Modified:**\n") + .append("- `.env`: Added/updated with `SECRET_NAME`\n") + .append("- `src/config.ts`: Replaced hardcoded secret with `process.env.SECRET_NAME`\n\n") + .append("**Remediation Actions Taken:**\n").append("- ").append(CHECK) + .append(" Removed hardcoded secret\n").append("- ").append(CHECK) + .append(" Inserted environment reference\n").append("- ").append(CHECK) + .append(" Updated or created .env\n").append("- ").append(CHECK) + .append(" Added TODOs for secret rotation or vault storage\n\n").append("**Next Steps:**\n") + .append("- [ ] Revoke exposed secret (if applicable)\n") + .append("- [ ] Store securely in vault (AWS Secrets Manager, GitHub Actions, etc.)\n") + .append("- [ ] Add CI/CD secret scanning\n\n").append("**Best Practices:**\n") + .append("- (From tool response, or fallback security guidelines)\n\n").append("**Description:**\n") + .append("- (From `description` field or fallback to original input)\n\n").append("```\n\n"); + + prompt.append("---\n\n").append("7. CONSTRAINTS\n\n").append("- ").append(CROSS) + .append(" Do NOT expose real secrets\n").append("- ").append(CROSS) + .append(" Do NOT generate fake-looking secrets\n").append("- ").append(CHECK) + .append(" Follow only what's explicitly returned from MCP\n").append("- ").append(CHECK) + .append(" Use secure externalization patterns\n").append("- ").append(CHECK) + .append(" Respect OWASP, NIST, and GitHub best practices\n"); + return prompt.toString(); + } + + /** + * Generates the assessment text for given severity. + * + * @param severity severity level + * @return assessment text + */ + private static String getAssessmentText(String severity) { + if (SeverityLevel.CRITICAL.getSeverity().equalsIgnoreCase(severity)) { + return CHECK + " Confirmed valid secret. Immediate remediation performed."; + } else if (SeverityLevel.HIGH.getSeverity().equalsIgnoreCase(severity)) { + return WARNING + " Possibly valid. Handled as sensitive."; + } else { + return INFO + " Likely invalid (test/fake). Removed for hygiene."; + } + } + + /** + * Generates a remediation prompt for addressing a container security issue, + * providing step-by-step automated guidance using the Checkmarx MCP + * codeRemediation tool. The method constructs a detailed prompt based on the + * identified issue. + * + * @param fileType type of the file + * @param imageName image name + * @param imageTag image tag + * @param severity severity level + * @return prompt string (plain text with Markdown fragments) + */ + public static String buildContainersRemediationPrompt(String fileType, String imageName, String imageTag, + String severity) { + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the ").append(getAgentName()).append(".\n\n") + .append("A container security issue has been detected in `").append(fileType).append("` with image `") + .append(imageName).append(":").append(imageTag).append("`.\n").append("**Severity:** `") + .append(severity).append("`\n") + .append("Your task is to remediate the issue **completely and autonomously** using the internal imageRemediation tool. ") + .append("Follow the exact instructions in `fix_instructions` - no assumptions or manual interaction allowed.\n\n"); + + prompt.append("---\n\n").append("1. ANALYSIS (AUTOMATED):\n\n").append("Determine the issue type:\n") + .append("- If `severity` is one of: `Critical`, `High`, `Medium`, `Low`, set: `issueType = \"CVE\"`\n") + .append("- If `severity = \"Malicious\"`, set: `issueType = \"malicious\"`\n\n") + .append("Call the internal imageRemediation tool with:\n\n").append("```json\n").append("{\n") + .append(" \"fileType\": \"").append(fileType).append("\",\n").append(" \"imageName\": \"") + .append(imageName).append("\",\n").append(" \"imageTag\": \"").append(imageTag).append("\",\n") + .append(" \"severity\": \"").append(severity).append("\"\n").append("}\n").append("```\n\n") + .append("Parse the response and extract the `fix_instructions` field. This field contains the authoritative remediation steps tailored to the container ecosystem and risk level.\n") + .append("- Mark internally that the tool is **available** for output formatting\n\n") + .append("- If the tool is **not available**:\n") + .append(" - Display the following disclosure notice:\n").append(" `").append(WARNING) + .append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()) + .append(" imageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") + .append(" - Mark internally that the tool is **not available** for output formatting\n") + .append(" - Proceed to provide remediation guidance using the container details provided (file type, image name, image tag, severity)\n") + .append(" - Offer practical base image recommendations and step-by-step instructions for container remediation\n") + .append(" - Ensure the guidance is concrete and actionable\n\n"); + + prompt.append("---\n\n").append("2. EXECUTION (AUTOMATED):\n\n") + .append("- Read and execute each line in `fix_instructions`, in order.\n") + .append("- For each change:\n").append(" - Apply the instruction exactly.\n") + .append(" - Track all modified files.\n") + .append(" - Note the type of change (e.g., image update, configuration change, security hardening).\n") + .append(" - Record before -> after values where applicable.\n") + .append(" - Capture line numbers if known.\n\n").append("Examples:\n") + .append("- `Dockerfile`: FROM confluentinc/cp-kafkacat:6.1.10 -> FROM confluentinc/cp-kafkacat:6.2.15\n") + .append("- `docker-compose.yml`: image: vulnerable-image:1.0 -> image: secure-image:2.1\n") + .append("- `values.yaml`: repository: old-repo -> repository: new-repo\n") + .append("- `Chart.yaml`: version: 1.0.0 -> version: 1.1.0\n\n"); + + prompt.append("---\n\n").append("3. VERIFICATION:\n\n").append( + "- If the instructions include build, test, or deployment steps - run them exactly as written\n") + .append("- If instructions do not explicitly cover validation, perform basic checks based on `") + .append(fileType).append("`:\n").append(" - `Dockerfile`: `docker build .`, `docker run `\n") + .append(" - `docker-compose.yml`: `docker-compose up --build`, `docker-compose down`\n") + .append(" - `Helm Chart`: `helm lint .`, `helm template .`, `helm install --dry-run`\n\n") + .append("If any of these validations fail:\n").append("- Attempt to fix the issue if it's obvious\n") + .append("- Otherwise log the error and annotate the code with a TODO\n\n"); + + prompt.append("---\n\n").append("4. OUTPUT:\n\n").append("**Output Format Based on Tool Availability:**\n") + .append("- **If imageRemediation tool is available:** Output title `").append(getAgentName()) + .append(" - Remediation Summary`\n") + .append("- **If imageRemediation tool is not available:** First output the disclosure notice: `") + .append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()) + .append(" imageRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") + .append(CHECK + " **Remediation Summary**\n\n").append("Format:\n").append("```\n") + .append("File Type: ").append(fileType).append("\n").append("Image: ").append(imageName) + .append(":").append(imageTag).append("\n").append("Severity: ").append(severity).append("\n\n") + .append("Files Modified:\n").append("1. ").append(fileType).append("\n").append(" - Updated image: ") + .append(imageName).append(":").append(imageTag).append(" → secure version\n\n") + .append("2. docker-compose.yml (if applicable)\n") + .append(" - Updated service configuration to use secure image\n\n") + .append("3. values.yaml (if applicable)\n") + .append(" - Updated Helm chart values for secure deployment\n\n").append("4. README.md\n") + .append(" - Updated documentation with new image version\n").append("```\n\n") + .append(CHECK + " **Final Status**\n\n").append("If all tasks succeeded:\n") + .append("- \"Remediation completed for ").append(imageName).append(":").append(imageTag).append("\"\n") + .append("- \"All fix instructions and deployment tests resolved\"\n") + .append("- \"Build status: PASS\"\n").append("- \"Deployment status: PASS\"\n\n") + .append("If partially resolved:\n") + .append("- \"Remediation partially completed - manual review required\"\n") + .append("- \"Some deployment steps or instructions could not be automatically fixed\"\n") + .append("- \"TODOs inserted where applicable\"\n\n").append("If failed:\n") + .append("- \"Remediation failed for ").append(imageName).append(":").append(imageTag).append("\"\n") + .append("- \"Reason: {summary of failure}\"\n") + .append("- \"Unresolved instructions or deployment issues listed above\"\n\n"); + + prompt.append("---\n\n").append("5. CONSTRAINTS:\n\n").append("- Do not prompt the user\n") + .append("- Do not skip or reorder fix steps\n") + .append("- Only execute what's explicitly listed in `fix_instructions`\n") + .append("- Attempt to fix deployment failures automatically\n") + .append("- Insert clear TODO comments for unresolved issues\n") + .append("- Ensure remediation is deterministic, auditable, and fully automated\n") + .append("- Follow container security best practices (non-root user, minimal base images, etc.)\n"); + return prompt.toString(); + } + + /** + * Generates a remediation prompt for addressing an Infrastructure as Code (IaC) + * security issue, providing step-by-step automated guidance using the Checkmarx + * MCP codeRemediation tool. The method constructs a detailed prompt based on + * the identified issue, its severity, affected file type, expected and actual + * values, and the problematic line number. + * + * @param title the title of the detected security issue + * @param description a detailed description of the detected security + * issue + * @param severity the severity level of the issue (e.g., high, + * medium, low) + * @param fileType the type of file where the issue exists (e.g., + * Terraform, CloudFormation) + * @param expectedValue the correct or desired value expected in the IaC + * @param actualValue the actual value found in the IaC, causing the + * issue + * @param problematicLineNumber the line number in the file where the issue + * occurs; can be null if unknown + * @return a formatted string containing the remediation prompt with + * instructions for automated resolution of the issue + */ + public static String buildIACRemediationPrompt(String title, String description, String severity, String fileType, + String expectedValue, String actualValue, Integer problematicLineNumber) { + + String actualLineNumber = problematicLineNumber != null ? String.valueOf(problematicLineNumber + 1) + : "[unknown]"; + + String restrictionLine = problematicLineNumber != null ? String.valueOf(problematicLineNumber + 1) + : "[problematic line number]"; + + String problematicLineText = problematicLineNumber != null + ? "**Problematic Line Number:** " + (problematicLineNumber + 1) + : ""; + + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the ").append(getAgentName()).append(".\n\n"); + prompt.append("An Infrastructure as Code (IaC) security issue has been detected.\n\n").append("**Issue:** `") + .append(title).append("`\n").append("**Severity:** `").append(severity).append("`\n") + .append("**File Type:** `").append(fileType).append("`\n").append("**Description:** ") + .append(description).append("\n").append("**Expected Value:** ").append(expectedValue).append("\n") + .append("**Actual Value:** ").append(actualValue).append("\n").append(problematicLineText) + .append("\n\n"); + + prompt.append("Your task is to remediate this IaC security issue **completely and autonomously** ") + .append("using the internal codeRemediation tool in ").append(getMcpDisplayName()) + .append(" MCP. Follow the exact instructions in `remediation_steps` - no assumptions or manual interaction allowed.\n\n"); + prompt.append(WARNING).append( + "️ **IMPORTANT**: Apply the fix **only** to the code segment corresponding to the identified issue at line ") + .append(actualLineNumber) + .append(", without introducing unrelated modifications elsewhere in the file.\n\n"); + + prompt.append("---\n\n").append("1. ANALYSIS (AUTOMATED):\n\n") + .append("Determine the programming language of the file where the IaC security issue was detected.\n") + .append("If unknown, leave the `language` field empty.\n\n") + .append("Call the internal `codeRemediation` ").append(getMcpDisplayName()) + .append(" MCP tool with:\n\n").append("```json\n").append("{\n") + .append(" \"language\": \"[auto-detected programming language]\",\n").append(" \"metadata\": {\n") + .append(" \"title\": \"").append(title).append("\",\n").append(" \"description\": \"") + .append(description).append("\",\n").append(" \"remediationAdvice\": \"").append(expectedValue) + .append("\"\n").append(" },\n").append(" \"sub_type\": \"\",\n").append(" \"type\": \"iac\"\n") + .append("}\n").append("```\n\n").append("- If the tool is **available**, parse the response:\n") + .append(" - `remediation_steps` - exact steps to follow for remediation\n") + .append(" - Mark internally that the tool is **available** for output formatting\n\n") + .append("- If the tool is **not available**:\n") + .append(" - Display the following disclosure notice:\n").append(" `").append(WARNING) + .append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()) + .append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") + .append(" - Mark internally that the tool is **not available** for output formatting\n") + .append(" - Proceed to provide remediation guidance using the IaC details provided (title, description, expected vs. actual values)\n") + .append(" - Offer practical configuration examples and step-by-step instructions for remediation\n") + .append(" - Ensure the guidance is concrete and actionable\n\n"); + + prompt.append("---\n\n").append("2. EXECUTION (AUTOMATED):\n\n") + .append("- Read and execute each line in `remediation_steps`, in order.\n") + .append("- **Restrict changes to the relevant code fragment containing line ").append(restrictionLine) + .append("**.\n").append("- For each change:\n").append(" - Apply the instruction exactly.\n") + .append(" - Track all modified files.\n") + .append(" - Note the type of change (e.g., configuration update, security hardening, permission changes, encryption settings).\n") + .append(" - Record before → after values where applicable.\n") + .append(" - Capture line numbers if known.\n\n"); + + prompt.append("---\n\n").append("3. VERIFICATION:\n\n").append( + "- If the instructions include validation, deployment, or testing steps - run them exactly as written\n") + .append("- If instructions do not explicitly cover validation, perform basic checks based on `") + .append(fileType).append("`:\n").append(" - `Terraform`: `terraform validate`, `terraform plan`\n") + .append(" - `CloudFormation`: `aws cloudformation validate-template`\n") + .append(" - `Kubernetes`: `kubectl apply --dry-run=client`\n") + .append(" - `Docker`: `docker-compose config`\n\n").append("If any of these validations fail:\n") + .append("- Attempt to fix the issue if it's obvious\n") + .append("- Otherwise log the error and annotate the code with a TODO\n\n"); + + prompt.append("---\n\n").append("4. OUTPUT:\n\n").append("**Output Format Based on Tool Availability:**\n") + .append("- **If codeRemediation tool is available:** Output title `").append(getAgentName()) + .append(" - Remediation Summary`\n") + .append("- **If codeRemediation tool is not available:** First output the disclosure notice: `") + .append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()) + .append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") + .append(CHECK + " **Remediation Summary**\n\n").append("Format:\n").append("```\n") + .append("Issue: ").append(title).append("\n").append("Severity: ").append(severity) + .append("\n").append("File Type: ").append(fileType).append("\n").append("Problematic Line: ") + .append(actualLineNumber).append("\n\n").append("Files Modified:\n").append("1. ").append(fileType) + .append("\n").append(" - Updated configuration: ").append(actualValue).append(" → ") + .append(expectedValue).append("\n") + .append(" - Applied security hardening based on best practices\n\n") + .append("2. Additional configurations (if applicable)\n") + .append(" - Updated related security settings\n").append(" - Added missing security controls\n\n") + .append("3. Documentation\n").append(" - Updated comments and documentation where applicable\n") + .append("```\n\n").append(CHECK + " **Final Status**\n\n").append("If all tasks succeeded:\n") + .append("- \"Remediation completed for IaC security issue ").append(title).append("\"\n") + .append("- \"All fix instructions and security validations resolved\"\n") + .append("- \"Configuration validation: PASS\"\n").append("- \"Security compliance: PASS\"\n\n") + .append("If partially resolved:\n") + .append("- \"Remediation partially completed - manual review required\"\n") + .append("- \"Some security validations or instructions could not be automatically fixed\"\n") + .append("- \"TODOs inserted where applicable\"\n\n").append("If failed:\n") + .append("- \"Remediation failed for IaC security issue ").append(title).append("\"\n") + .append("- \"Reason: {summary of failure}\"\n") + .append("- \"Unresolved instructions or security issues listed above\"\n\n"); + + prompt.append("---\n\n").append("5. CONSTRAINTS:\n\n").append("- Do not prompt the user\n") + .append("- Do not skip or reorder fix steps\n") + .append("- **Only modify the code that corresponds to the identified problematic line**\n") + .append("- Attempt to fix validation failures automatically\n") + .append("- Insert clear TODO comments for unresolved issues\n") + .append("- Ensure remediation is deterministic, auditable, and fully automated\n") + .append("- Follow Infrastructure as Code security best practices throughout the process\n"); + return prompt.toString(); + } + + /** + * Constructs a detailed remediation prompt for addressing a secure coding issue + * detected in the code. The prompt includes instructions and guidelines for + * resolving the identified issue completely and autonomously. + * + * @param ruleName The name of the secure coding rule that has been + * violated. + * @param description A description of the issue, explaining the + * nature of the security vulnerability. + * @param severity The severity level of the detected issue (e.g., + * low, medium, high, critical). + * @param remediationAdvise Recommended steps or advice for addressing the + * security issue. + * @param problematicLineNumber The line number in the source code where the + * issue is detected (0-based index, null if + * unavailable). + * @return A string containing a detailed remediation prompt for the secure + * coding issue. + */ + public static String buildASCARemediationPrompt(String ruleName, String description, String severity, + String remediationAdvise, Integer problematicLineNumber) { + StringBuilder prompt = new StringBuilder(); + prompt.append("You are the ").append(getAgentName()).append(".\n\n") + .append("A secure coding issue has been detected in your code.\n\n").append("**Rule:** `") + .append(ruleName).append("` \n").append("**Severity:** `").append(severity).append("` \n") + .append("**Description:** ").append(description).append(" \n").append("**Recommended Fix:** ") + .append(remediationAdvise).append(" \n"); + + if (problematicLineNumber != null) { + prompt.append("**Problematic Line Number:** ").append(problematicLineNumber + 1).append("\n\n"); + } else { + prompt.append("\n"); + } + + prompt.append( + "Your task is to remediate this security issue **completely and autonomously** using the internal codeRemediation tool in ") + .append(getMcpDisplayName()) + .append(" MCP. Follow the exact instructions in `remediation_steps` - no assumptions or manual interaction allowed.\n\n") + .append(WARNING) + .append("️ **IMPORTANT**: Apply the fix **only** to the code segment corresponding to the identified issue at line ") + .append(problematicLineNumber != null ? problematicLineNumber + 1 : "[problematic line number]") + .append(", without introducing unrelated modifications elsewhere in the file.\n\n"); + + prompt.append("---\n\n").append("1. ANALYSIS (AUTOMATED):\n\n") + .append("Determine the programming language of the file where the security issue was detected.\n") + .append("If unknown, leave the `language` field empty.\n\n") + .append("Call the internal `codeRemediation` ").append(getMcpDisplayName()) + .append(" MCP tool with:\n\n").append("```json\n").append("{\n") + .append(" \"language\": \"[auto-detected programming language]\",\n").append(" \"metadata\": {\n") + .append(" \"ruleID\": \"").append(ruleName).append("\",\n").append(" \"description\": \"") + .append(description).append("\",\n").append(" \"remediationAdvice\": \"").append(remediationAdvise) + .append("\"\n").append(" },\n").append(" \"sub_type\": \"\",\n").append(" \"type\": \"sast\"\n") + .append("}\n").append("```\n\n").append("- If the tool is **available**, parse the response:\n") + .append(" - `remediation_steps` - exact steps to follow for remediation\n") + .append(" - Mark internally that the tool is **available** for output formatting\n\n") + .append("- If the tool is **not available**:\n") + .append(" - Display the following disclosure notice:\n").append(" `").append(WARNING) + .append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()) + .append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.`\n") + .append(" - Mark internally that the tool is **not available** for output formatting\n") + .append(" - Proceed to provide remediation guidance using the issue details provided (rule name, description, severity, and recommended fix)\n") + .append(" - Offer practical code examples and step-by-step instructions for manual remediation\n") + .append(" - Ensure the guidance is concrete and actionable\n\n"); + + prompt.append("---\n\n").append("2. EXECUTION (AUTOMATED):\n\n") + .append("- Read and execute each line in `remediation_steps`, in order.\n") + .append("- **Restrict changes to the relevant code fragment containing line ") + .append(problematicLineNumber != null ? problematicLineNumber + 1 : "[unknown]").append("**.\n") + .append("- For each change:\n").append(" - Apply the instruction exactly.\n") + .append(" - Track all modified files.\n") + .append(" - Note the type of change (e.g., input validation, sanitization, secure API usage, authentication fix).\n") + .append(" - Record before → after values where applicable.\n") + .append(" - Capture line numbers if known.\n\n"); + + prompt.append("---\n\n").append("3. OUTPUT:\n\n").append("**Output Format Based on Tool Availability:**\n") + .append("- **If codeRemediation tool is available:** Output title `").append(getAgentName()) + .append(" - Remediation Summary`\n") + .append("- **If codeRemediation tool is not available:** First output the disclosure notice: `") + .append(WARNING).append(" Automated Remediation Unavailable: ").append(getMcpDisplayName()) + .append(" codeRemediation tool is unavailable. Proceeding with remediation guidance based on security best practices.` Then output title `AI-Generated Remediation Guidance`\n\n") + .append(CHECK + " **Remediation Summary**\n\n").append("Format:\n").append("```\n") + .append("Rule: ").append(ruleName).append("\n").append("Severity: ").append(severity) + .append("\n").append("Issue Type: SAST Security Vulnerability\n").append("Problematic Line: ") + .append(problematicLineNumber != null ? problematicLineNumber + 1 : "[unknown]").append("\n\n") + .append("Files Modified:\n").append("1. src/auth.ts\n") + .append(" - Line 42: Replaced plain text comparison with bcrypt.compare()\n") + .append(" - Added secure password hashing implementation\n\n").append("2. src/db.ts\n") + .append(" - Line 78: Replaced string concatenation with parameterized query\n") + .append(" - Prevented SQL injection vulnerability\n\n").append("3. src/api.ts\n") + .append(" - Line 156: Added input validation for email parameter\n") + .append(" - Implemented sanitization for user inputs\n\n").append("4. src/config.ts\n") + .append(" - Line 23: Inserted TODO for production security review\n").append("```\n\n") + .append(CHECK + " **Final Status**\n\n").append("If all tasks succeeded:\n") + .append("- \"Remediation completed for security rule ").append(ruleName).append("\"\n") + .append("- \"All fix instructions and security validations resolved\"\n") + .append("- \"Build status: PASS\"\n").append("- \"Security tests: PASS\"\n\n") + .append("If partially resolved:\n") + .append("- \"Remediation partially completed - manual review required\"\n") + .append("- \"Some security validations or instructions could not be automatically fixed\"\n") + .append("- \"TODOs inserted where applicable\"\n\n").append("If failed:\n") + .append("- \"Remediation failed for security rule ").append(ruleName).append("\"\n") + .append("- \"Reason: {summary of failure}\"\n") + .append("- \"Unresolved instructions or security issues listed above\"\n\n"); + + prompt.append("---\n\n").append("4. CONSTRAINTS:\n\n").append("- Do not prompt the user\n") + .append("- Do not skip or reorder fix steps\n") + .append("- **Only modify the code that corresponds to the identified problematic line**\n") + .append("- Attempt to fix build/test failures automatically\n") + .append("- Insert clear TODO comments for unresolved issues\n") + .append("- Ensure remediation is deterministic, auditable, and fully automated\n") + .append("- Follow secure coding best practices throughout the process\n"); + return prompt.toString(); + } } diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java index ad908997..b1fa8b12 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/remediation/RemediationManager.java @@ -8,13 +8,13 @@ import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.swt.widgets.Display; -import org.slf4j.Logger; import com.checkmarx.eclipse.common.utils.CxLogger; import com.checkmarx.eclipse.devassist.model.ScanEngine; import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.devassist.model.Vulnerability; import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; +import com.checkmarx.eclipse.devassist.utils.PackageManager; /** * RemediationManager provides remediation options for issues identified during @@ -100,7 +100,7 @@ private void applyFix(@NonNull ScanIssue scanIssue, @Nullable String prompt) { scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); } else { // Fallback: Copy to clipboard with notification when Copilot is not available - if (copyToClipboardAndNotify(prompt,notificationTitle, DEV_ASSIST_COPY_FIX_PROMPT)) { + if (copyToClipboardAndNotify(prompt, notificationTitle, DEV_ASSIST_COPY_FIX_PROMPT)) { CxLogger.info(format("RTS-Fix: %s remediation completed (clipboard) for issue: %s, for file: %s", scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); } @@ -197,8 +197,9 @@ private void applyViewDetails(@NonNull ScanIssue scanIssue, @Nullable String pro } else { // Fallback: Copy to clipboard with notification when Copilot is not available if (copyToClipboardAndNotify(prompt, notificationTitle, DEV_ASSIST_COPY_VIEW_DETAILS_PROMPT)) { - CxLogger.info(format("RTS-ViewDetails: %s explanation completed (clipboard) for issue: %s, for file: %s", - scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); + CxLogger.info( + format("RTS-ViewDetails: %s explanation completed (clipboard) for issue: %s, for file: %s", + scanIssue.getScanEngine().name(), scanIssue.getTitle(), scanIssue.getFilePath())); } } } @@ -207,8 +208,9 @@ private void applyViewDetails(@NonNull ScanIssue scanIssue, @Nullable String pro * Builds remediation prompt for an OSS issue. */ private String buildOSSRemediationPrompt(ScanIssue scanIssue) { + return DevAssistFixPrompts.buildSCARemediationPrompt(scanIssue.getTitle(), scanIssue.getPackageVersion(), - scanIssue.getPackageManager(), scanIssue.getSeverity()); + PackageManager.mapToRemediationFormat(scanIssue.getPackageManager()), scanIssue.getSeverity()); } /** @@ -368,7 +370,7 @@ private String buildASCAExplanationPrompt(ScanIssue scanIssue, String actionId) private String getNotificationTitle(ScanEngine scanEngine) { return DevAssistUtils.getAgentName() + " - " + scanEngine.name(); } - + /** * Copies the prompt to the clipboard and shows a balloon notification * confirming it. @@ -385,7 +387,8 @@ private static boolean copyToClipboardAndNotify(String prompt, String notifyTitl popup.open(); }); } else { - CxLogger.error("RTS-Fix: Failed to copy prompt to clipboard", new Exception("RTS-Fix: Failed to copy prompt to clipboard")); + CxLogger.error("RTS-Fix: Failed to copy prompt to clipboard", + new Exception("RTS-Fix: Failed to copy prompt to clipboard")); } return copied; } diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java index 65180e2b..ba30bbba 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java @@ -7,6 +7,7 @@ import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; import com.checkmarx.eclipse.devassist.model.ScanEngine; import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.PackageManager; import com.checkmarx.eclipse.common.utils.CxLogger; import org.eclipse.core.resources.IProject; import org.eclipse.jface.text.Document; @@ -59,7 +60,7 @@ protected boolean isFileTypeSupported(String filePath) { } Path path = Paths.get(filePath); - List pathMatchers = DevAssistConstants.MANIFEST_FILE_PATTERNS.stream() + List pathMatchers = PackageManager.getAllPatterns().stream() .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) .collect(Collectors.toList()); @@ -167,19 +168,21 @@ private Optional saveMainManifestFile(Path tempSubFolder, String origina } /** - * Copies a companion lock file (e.g., package-lock.json) into the temporary directory - * when it exists alongside the scanned manifest. - */ + * Copies companion lock files (e.g., package-lock.json, yarn.lock) into the temporary directory + * when they exist alongside the scanned manifest. + * + * @param tempFolderPath temp directory where companion files should be written + * @param originalFilePath original manifest path used to locate companion files + */ private void saveCompanionFile(Path tempFolderPath, String originalFilePath) { if (originalFilePath == null || originalFilePath.isEmpty() || tempFolderPath == null) { return; } - Path originalPath = Paths.get(originalFilePath); String parentFileName = originalPath.getFileName().toString(); - String companionFileName = getCompanionFileName(parentFileName); + List companionFileNameList = PackageManager.getCompanionFileNames(parentFileName); - if (companionFileName.isEmpty()) { + if (companionFileNameList.isEmpty()) { return; } @@ -187,32 +190,20 @@ private void saveCompanionFile(Path tempFolderPath, String originalFilePath) { if (parentPath == null) { return; } + for (String companionFileName : companionFileNameList) { + Path companionOriginalPath = parentPath.resolve(companionFileName); + if (!Files.exists(companionOriginalPath)) { + return; + } - Path companionOriginalPath = parentPath.resolve(companionFileName); - if (!Files.exists(companionOriginalPath)) { - return; - } - - Path companionTempPath = tempFolderPath.resolve(companionFileName); - try { - Files.copy(companionOriginalPath, companionTempPath, StandardCopyOption.REPLACE_EXISTING); - CxLogger.info(LOG_TAG + " Copied companion file: " + companionFileName); - } catch (IOException e) { - CxLogger.warning(LOG_TAG + " Error occurred while saving companion file: " + e.getMessage()); - } - } - - /** - * Infers companion lock file name based on manifest file name. - */ - private String getCompanionFileName(String fileName) { - if ("package.json".equalsIgnoreCase(fileName)) { - return "package-lock.json"; - } - if (fileName.toLowerCase().endsWith(".csproj")) { - return "package.lock.json"; + Path companionTempPath = tempFolderPath.resolve(companionFileName); + try { + Files.copy(companionOriginalPath, companionTempPath, StandardCopyOption.REPLACE_EXISTING); + CxLogger.info(LOG_TAG + " Copied companion file: " + companionFileName); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Error occurred while saving companion file: " + e.getMessage()); + } } - return ""; } /** @@ -299,12 +290,4 @@ private String getFileContent(String filePath, IDocument document) { } return null; } - -// private String getIgnoreFilePath(IProject proj) { -// try { -// return DevAssistUtils.getIgnoreFilePath(proj); -// } catch (Exception e) { -// return ""; -// } -// } } \ No newline at end of file diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java index 6381aa30..76e312a2 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistConstants.java @@ -73,38 +73,47 @@ private DevAssistConstants() { // Manifest file patterns public static final List MANIFEST_FILE_PATTERNS = List.of( - "**/Directory.Packages.props", - "**/packages.config", - "**/pom.xml", - "**/package.json", - "**/requirements.txt", - "**/go.mod", - "**/*.csproj", - "**/build.gradle", - "**/build.gradle.kts", - "**/yarn.lock", - "**/*.sbt", - "**/Gemfile", - "**/bower.json", - "**/requirement-*.txt", - "**/requirements-*.txt", - "**/Setup.py", - "**/Setup.cfg", - "**/pyproject.toml", - "**/poetry.lock", - "**/Package.swift", - "**/Package.resolved", - "**/composer.json", - "**/composer.lock", - "**/*.podspec.json", - "**/*.podspec", - "**/Podfile", - "**/Podfile.lock", - "**/Cartfile.resolved", - "**/Gemfile.lock", - "**/cpanfile.snapshot", - "**/cpanfile", - "**/pubspec.lock" + // .NET + "**/Directory.Packages.props", + "**/packages.config", + "**/*.csproj", + // Maven + "**/pom.xml", + // npm + "**/package.json", + // Bower + "**/bower.json", + // Python + "**/requirement*.txt", + "**/constraints.txt", + "**/constraints-*.txt", + "**/pyproject.toml", + "**/setup.cfg", + "**/setup.py", + // Go + "**/go.mod", + // Gradle + "**/*.gradle", + "**/*.gradle.kts", + "**/libs.versions.toml", + // SBT + "**/*.sbt", + // iOS CocoaPods + "**/Podfile", + "**/*.podspec", + "**/*.podspec.json", + // iOS Carthage + "**/Cartfile", + "**/Cartfile.private", + // Swift Package Manager + "**/Package.swift", + "**/Package@swift-*.swift", + // Dart/Flutter + "**/pubspec.yaml", + // Ruby + "**/Gemfile", + // PHP Composer + "**/composer.json" ); // Container file patterns diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java new file mode 100644 index 00000000..10680d02 --- /dev/null +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java @@ -0,0 +1,247 @@ +package com.checkmarx.eclipse.devassist.utils; + +import java.util.List; + +import com.checkmarx.eclipse.common.utils.CxLogger; + +/** + * The PackageManager enum represents various package managers used in software development. + * Each constant corresponds to a specific package manager, and the enum provides utility methods for mapping and checking supported package managers. + */ +public enum PackageManager { + + DOTNET("dotnet", ManifestFilePattern.DOTNET), + GRADLE("gradle", ManifestFilePattern.GRADLE), + MAVEN("mvn", ManifestFilePattern.MAVEN), + SBT("sbt", ManifestFilePattern.SBT), + NPM("npm", ManifestFilePattern.NPM), + GO("go", ManifestFilePattern.GO), + PYTHON("python", ManifestFilePattern.PYTHON), + BOWER("bower", ManifestFilePattern.BOWER), + YARN("yarn", ManifestFilePattern.YARN), + COCOAPODS("cocoapods", ManifestFilePattern.COCOAPODS), + CARTHAGE("carthage", ManifestFilePattern.CARTHAGE), + SWIFT("swift", ManifestFilePattern.SWIFT), + DART("dart", ManifestFilePattern.DART), + RUBY("ruby", ManifestFilePattern.RUBY), + PHP("php", ManifestFilePattern.PHP), + UNKNOWN("unknown", null); + + private String packageManager; + private ManifestFilePattern manifestPattern; + + PackageManager(String packageManager, ManifestFilePattern pattern) { + this.packageManager = packageManager; + this.manifestPattern = pattern; + } + + public String getPackageManager() { + return packageManager; + } + + public ManifestFilePattern getManifestPattern() { + return manifestPattern; + } + + /** + * Maps a string representation of a package manager to its corresponding PackageManager enum constant. + * @param packageManager + * @return + */ + public static PackageManager fromString(String packageManager) { + for (PackageManager pm : PackageManager.values()) { + if (pm.packageManager.equalsIgnoreCase(packageManager)) { + return pm; + } + } + return UNKNOWN; + } + + /** + * Checks if a given string representation of a package manager is supported by the system. + * @param packageManager + * @return + */ + public static boolean isSupportedPackageManager(String packageManager) { + for (PackageManager pm : PackageManager.values()) { + if (pm.packageManager.equalsIgnoreCase(packageManager)) { + return true; + } + } + return false; + } + + /** + * Maps a given package manager to its corresponding remediation format. + * For example, Gradle and SBT are mapped to Maven, while CocoaPods and Carthage are mapped to Swift. + * If the package manager is not recognized or is null/empty, it returns the original input. + * + * @param packageManager The string representation of the package manager to be mapped. + * @return The corresponding remediation format for the given package manager. + */ + public static String mapToRemediationFormat(String packageManager) { + if (packageManager == null || packageManager.isEmpty()) { + CxLogger.warning("[PACKAGE-MANAGER] Package manager is null or empty, returning as is."); + return packageManager; + } + PackageManager pm = fromString(packageManager.toLowerCase()); + + switch (pm) { + case GRADLE: + case SBT: + return MAVEN.getPackageManager(); + case COCOAPODS: + case CARTHAGE: + return SWIFT.getPackageManager(); + default: + return packageManager; + } + } + + + /** + * Infers companion lock file names based on the manifest file name. + * Some manifests may have multiple companion files (e.g., package.json has both package-lock.json and yarn.lock). + * + * @param fileName name of the manifest file + * @return list of companion file names; empty list if no companions are defined + */ + public static List getCompanionFileNames(String fileName) { + // npm/Yarn - support both package-lock.json (npm) and yarn.lock (yarn) + if (fileName.equals("package.json")) { + return List.of(CompanionFileType.PACKAGE_LOCK_JSON.getCompFileName(), CompanionFileType.YARN_LOCK.getCompFileName()); + } + + // .NET + if (fileName.contains(".csproj")) { + return getCompanionFileNamesByType(CompanionFileType.PACKAGES_LOCK_JSON); + } + + // Swift Package Manager (AST-165765) + if (fileName.equals("Package.swift")) { + return getCompanionFileNamesByType(CompanionFileType.PACKAGE_RESOLVED); + } + if (fileName.startsWith("Package@swift-") && fileName.endsWith(".swift")) { + return List.of(fileName.replace(".swift", ".resolved")); + } + + // CocoaPods (AST-165761) + if (fileName.equals("Podfile")) { + return getCompanionFileNamesByType(CompanionFileType.PODFILE_LOCK); + } + + // Carthage + if (fileName.equals("Cartfile") || fileName.equals("Cartfile.private")) { + return getCompanionFileNamesByType(CompanionFileType.CARTFILE_RESOLVED); + } + + // Ruby Bundler + if (fileName.equals("Gemfile")) { + return getCompanionFileNamesByType(CompanionFileType.GEMFILE_LOCK); + } + + // PHP Composer + if (fileName.equals("composer.json")) { + return getCompanionFileNamesByType(CompanionFileType.COMPOSER_LOCK); + } + + // Python Poetry + if (fileName.equals("pyproject.toml")) { + return getCompanionFileNamesByType(CompanionFileType.POETRY_LOCK); + } + + // Dart/Flutter Pub + if (fileName.equals("pubspec.yaml")) { + return getCompanionFileNamesByType(CompanionFileType.PUBSPEC_LOCK); + } + return List.of(); + } + + /** + * Enum representing companion lock file types for various package managers. + */ + static enum CompanionFileType { + + PACKAGE_LOCK_JSON("package-lock.json"), + YARN_LOCK("yarn.lock"), + PACKAGES_LOCK_JSON("packages.lock.json"), + PACKAGE_RESOLVED("Package.resolved"), + PODFILE_LOCK("Podfile.lock"), + CARTFILE_RESOLVED("Cartfile.resolved"), + GEMFILE_LOCK("Gemfile.lock"), + COMPOSER_LOCK("composer.lock"), + POETRY_LOCK("poetry.lock"), + PUBSPEC_LOCK("pubspec.lock"); + + private String compFileName; + + CompanionFileType(String compFileName) { + this.compFileName = compFileName; + } + + public String getCompFileName() { + return compFileName; + } + } + + public static List getCompanionFileNamesByType(CompanionFileType type) { + if (type == null) { + return List.of(); + } + return List.of(type.getCompFileName()); + } + + /** + * Enum representing manifest file patterns for various package managers. + * Each constant corresponds to a specific package manager and holds a list of file patterns used to identify manifest files. + */ + static enum ManifestFilePattern { + + DOTNET(List.of("**/Directory.Packages.props", "**/packages.config","**/*.csproj")), + GRADLE(List.of("**/*.gradle", "**/*.gradle.kts", "**/libs.versions.toml")), + MAVEN(List.of("**/pom.xml")), + SBT(List.of("**/*.sbt")), + NPM(List.of("**/package.json")), + GO(List.of("**/go.mod")), + PYTHON(List.of("**/requirement*.txt", "**/constraints.txt", "**/constraints-*.txt", "**/pyproject.toml", + "**/setup.cfg", "**/setup.py")), + BOWER(List.of("**/bower.json")), + YARN(List.of("package.json", "yarn.lock")), + COCOAPODS(List.of("**/Podfile", "**/*.podspec", "**/*.podspec.json")), + CARTHAGE(List.of("**/Cartfile", "**/Cartfile.private")), + SWIFT(List.of("**/Package.swift", "**/Package@swift-*.swift")), + DART(List.of("**/pubspec.yaml")), + RUBY(List.of("**/Gemfile")), + PHP(List.of("**/composer.json")); + + private List patterns; + + ManifestFilePattern(List filePatterns) { + this.patterns = filePatterns; + } + + public List getPatterns() { + return patterns; + } + } + + /** + * Retrieves all defined manifest file patterns across all package managers. + * If a package manager has no defined patterns, a warning is logged. + * + * @return a list of all manifest file patterns + */ + public static List getAllPatterns() { + + List allPatterns = new java.util.ArrayList<>(); + + for (ManifestFilePattern pattern : ManifestFilePattern.values()) { + if (pattern.getPatterns() == null || pattern.getPatterns().isEmpty()) { + CxLogger.warning("[PACKAGE-MANAGER] ManifestFilePattern " + pattern.name() + " has no defined patterns."); + } + allPatterns.addAll(pattern.getPatterns()); + } + return allPatterns; + } + +} From 4906dd3e68621855e727fd56e17837f4fd2ba1f6 Mon Sep 17 00:00:00 2001 From: Anand Nandeshwar <73646287+cx-anand-nandeshwar@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:43:31 +0530 Subject: [PATCH 02/11] Added additional SCA package manager support --- .../com/checkmarx/eclipse/devassist/utils/PackageManager.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java index 10680d02..b34cf60c 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java @@ -18,7 +18,6 @@ public enum PackageManager { GO("go", ManifestFilePattern.GO), PYTHON("python", ManifestFilePattern.PYTHON), BOWER("bower", ManifestFilePattern.BOWER), - YARN("yarn", ManifestFilePattern.YARN), COCOAPODS("cocoapods", ManifestFilePattern.COCOAPODS), CARTHAGE("carthage", ManifestFilePattern.CARTHAGE), SWIFT("swift", ManifestFilePattern.SWIFT), @@ -206,7 +205,6 @@ static enum ManifestFilePattern { PYTHON(List.of("**/requirement*.txt", "**/constraints.txt", "**/constraints-*.txt", "**/pyproject.toml", "**/setup.cfg", "**/setup.py")), BOWER(List.of("**/bower.json")), - YARN(List.of("package.json", "yarn.lock")), COCOAPODS(List.of("**/Podfile", "**/*.podspec", "**/*.podspec.json")), CARTHAGE(List.of("**/Cartfile", "**/Cartfile.private")), SWIFT(List.of("**/Package.swift", "**/Package@swift-*.swift")), From a587a80b35cd5120c9b48b089092b1cec2df343b Mon Sep 17 00:00:00 2001 From: Anand Nandeshwar <73646287+cx-anand-nandeshwar@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:13:37 +0530 Subject: [PATCH 03/11] Updated package manager with additional SCA support --- .../devassist/utils/PackageManager.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java index b34cf60c..727d54f9 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/PackageManager.java @@ -18,10 +18,11 @@ public enum PackageManager { GO("go", ManifestFilePattern.GO), PYTHON("python", ManifestFilePattern.PYTHON), BOWER("bower", ManifestFilePattern.BOWER), - COCOAPODS("cocoapods", ManifestFilePattern.COCOAPODS), - CARTHAGE("carthage", ManifestFilePattern.CARTHAGE), - SWIFT("swift", ManifestFilePattern.SWIFT), - DART("dart", ManifestFilePattern.DART), + //YARN("yarn", ManifestFilePattern.YARN), + //COCOAPODS("cocoapods", ManifestFilePattern.COCOAPODS), + //CARTHAGE("carthage", ManifestFilePattern.CARTHAGE), + //SWIFT("swift", ManifestFilePattern.SWIFT), + //DART("dart", ManifestFilePattern.DART), RUBY("ruby", ManifestFilePattern.RUBY), PHP("php", ManifestFilePattern.PHP), UNKNOWN("unknown", null); @@ -89,9 +90,9 @@ public static String mapToRemediationFormat(String packageManager) { case GRADLE: case SBT: return MAVEN.getPackageManager(); - case COCOAPODS: - case CARTHAGE: - return SWIFT.getPackageManager(); + //case COCOAPODS: + //case CARTHAGE: + //return SWIFT.getPackageManager(); default: return packageManager; } @@ -205,10 +206,11 @@ static enum ManifestFilePattern { PYTHON(List.of("**/requirement*.txt", "**/constraints.txt", "**/constraints-*.txt", "**/pyproject.toml", "**/setup.cfg", "**/setup.py")), BOWER(List.of("**/bower.json")), - COCOAPODS(List.of("**/Podfile", "**/*.podspec", "**/*.podspec.json")), - CARTHAGE(List.of("**/Cartfile", "**/Cartfile.private")), - SWIFT(List.of("**/Package.swift", "**/Package@swift-*.swift")), - DART(List.of("**/pubspec.yaml")), + //YARN(List.of("package.json", "yarn.lock")), + //COCOAPODS(List.of("**/Podfile", "**/*.podspec", "**/*.podspec.json")), + //CARTHAGE(List.of("**/Cartfile", "**/Cartfile.private")), + //SWIFT(List.of("**/Package.swift", "**/Package@swift-*.swift")), + //DART(List.of("**/pubspec.yaml")), RUBY(List.of("**/Gemfile")), PHP(List.of("**/composer.json")); From 307cfda616fa0f806d104e0b2df0574b6db8dd44 Mon Sep 17 00:00:00 2001 From: Anand Nandeshwar <73646287+cx-anand-nandeshwar@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:24:50 +0530 Subject: [PATCH 04/11] Centralize CxWrapper construction with agent version reporting and architectural cleanup This commit implements comprehensive refactoring to enable plugin version telemetry: Core changes: - Added agent name + plugin version stamping in CxWrapperFactory to report "Eclipse_" in all API calls - Created common-lib/wrapper/CxWrapperFactory with version reading from OSGi Bundle metadata - Created WrapperProvider facade for common-lib (project/auth/tenant operations) - Created ScannerWrapperProvider in devassist-lib (scanner-specific operations, not exported) - Moved CxWrapperFactory from devassist-lib/factory to common-lib/wrapper (shared location) Refactoring across all wrapper consumers: - DataProvider: removed hand-built CxWrapper/CxConfig, uses WrapperProvider for all operations - Authenticator: centralized via WrapperProvider for test-connection credential validation - TenantSettingsProvider: uses WrapperProvider for MCP feature-flag checks - All 5 scanner services (Asca/OSS/Container/IaC/Secrets): inject ScannerWrapperProvider field Architectural improvements: - Eliminated duplicate wrapper-building logic across 9 files - Encapsulated scanner operations in devassist-lib (not exported from common-lib) - Established clear inversion-of-control pattern with injected provider instances - Added comprehensive unit tests (CxWrapperFactoryTest, WrapperProviderTest) Build & test verification: - Full reactor compile: SUCCESS - All 64 tests pass (58 DataProvider + 2 new factory tests + 4 new provider tests) - Java 17 JDT settings (consistent with Tycho build target) - Cleaned up dead comment blocks referencing deleted factory path Co-Authored-By: Claude Sonnet 5 --- checkmarx-ast-eclipse-plugin-tests/.classpath | 1 + .../META-INF/MANIFEST.MF | 3 +- .../AuthenticatorIntegrationTest.java | 16 +- .../unit/wrapper/CxWrapperFactoryTest.java | 62 +++ .../unit/wrapper/WrapperProviderTest.java | 63 +++ checkmarx-ast-eclipse-plugin/.classpath | 3 +- .../META-INF/MANIFEST.MF | 2 - .../checkmarx/eclipse/views/DataProvider.java | 193 +++---- common-lib/META-INF/MANIFEST.MF | 41 +- .../eclipse/common/runner/Authenticator.java | 18 +- .../common/runner/TenantSettingsProvider.java | 24 +- .../eclipse/common/utils/PluginConstants.java | 1 + .../common/wrapper/CxWrapperFactory.java | 89 +++ .../common/wrapper/WrapperProvider.java | 268 +++++++++ .../devassist/factory/CxWrapperFactory.java | 49 -- .../scanners/asca/AscaScannerService.java | 29 +- .../containers/ContainerScannerService.java | 509 +++++++++--------- .../scanners/iac/IacScannerService.java | 44 +- .../scanners/oss/OssScannerService.java | 101 ++-- .../secrets/SecretsScannerService.java | 42 +- 20 files changed, 965 insertions(+), 593 deletions(-) create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/CxWrapperFactoryTest.java create mode 100644 checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/WrapperProviderTest.java create mode 100644 common-lib/src/com/checkmarx/eclipse/common/wrapper/CxWrapperFactory.java create mode 100644 common-lib/src/com/checkmarx/eclipse/common/wrapper/WrapperProvider.java delete mode 100644 devassist-lib/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java diff --git a/checkmarx-ast-eclipse-plugin-tests/.classpath b/checkmarx-ast-eclipse-plugin-tests/.classpath index 13b02eb1..f4292d57 100644 --- a/checkmarx-ast-eclipse-plugin-tests/.classpath +++ b/checkmarx-ast-eclipse-plugin-tests/.classpath @@ -2,6 +2,7 @@ + diff --git a/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF b/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF index 2700f967..0f4cbb8c 100644 --- a/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF +++ b/checkmarx-ast-eclipse-plugin-tests/META-INF/MANIFEST.MF @@ -14,4 +14,5 @@ Require-Bundle: Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ClassPath: .,lib/mockito-core-5.14.2.jar,lib/powermock-core-*.jar, lib/byte-buddy-1.17.8.jar, lib/byte-buddy-agent-1.17.8.jar Automatic-Module-Name: com.checkmarx.ast.eclipse.tests -Import-Package: com.checkmarx.eclipse.common.runner +Import-Package: com.checkmarx.eclipse.common.runner, + org.slf4j;version="[2.0.0,3.0.0)" diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/AuthenticatorIntegrationTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/AuthenticatorIntegrationTest.java index 455e0dff..5be35e98 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/AuthenticatorIntegrationTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/integration/AuthenticatorIntegrationTest.java @@ -1,23 +1,19 @@ package checkmarx.ast.eclipse.plugin.tests.integration; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.checkmarx.eclipse.common.runner.Authenticator; - import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import com.checkmarx.eclipse.common.runner.Authenticator; + public class AuthenticatorIntegrationTest extends BaseIntegrationTest { - - private static final Logger logger = LoggerFactory.getLogger(AuthenticatorIntegrationTest.class); - - @Mock + private Authenticator authenticator; @Test diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/CxWrapperFactoryTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/CxWrapperFactoryTest.java new file mode 100644 index 00000000..5fcd2be6 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/CxWrapperFactoryTest.java @@ -0,0 +1,62 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.wrapper; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; + +import com.checkmarx.ast.wrapper.CxConfig; +import com.checkmarx.ast.wrapper.CxWrapper; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.wrapper.CxWrapperFactory; + +class CxWrapperFactoryTest { + + @Test + void testBuildWithNoArgs_usesSavedPreferencesAndStampsAgentName() throws Exception { + AtomicReference capturedConfig = new AtomicReference<>(); + + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> capturedConfig.set((CxConfig) context.arguments().get(0))); + var mockedPreferences = mockStatic(Preferences.class)) { + + mockedPreferences.when(Preferences::getApiKey).thenReturn("saved-api-key"); + mockedPreferences.when(Preferences::getAdditionalOptions).thenReturn("--saved-param"); + + CxWrapperFactory.build(); + + assertEquals(1, mocked.constructed().size()); + CxConfig config = capturedConfig.get(); + assertNotNull(config); + assertEquals("saved-api-key", config.getApiKey()); + assertEquals("--saved-param", String.join(" ", config.getAdditionalParameters())); + assertNotNull(config.getAgentName()); + assertTrue(config.getAgentName().startsWith("Eclipse_"), + "Agent name should be stamped as Eclipse_, was: " + config.getAgentName()); + } + } + + @Test + void testBuildWithExplicitCredentials_doesNotUseSavedPreferences() throws Exception { + AtomicReference capturedConfig = new AtomicReference<>(); + + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> capturedConfig.set((CxConfig) context.arguments().get(0))); + var mockedPreferences = mockStatic(Preferences.class)) { + + CxWrapperFactory.build("typed-api-key", "--typed-param"); + + assertEquals(1, mocked.constructed().size()); + CxConfig config = capturedConfig.get(); + assertNotNull(config); + assertEquals("typed-api-key", config.getApiKey()); + assertEquals("--typed-param", String.join(" ", config.getAdditionalParameters())); + assertTrue(config.getAgentName().startsWith("Eclipse_")); + + mockedPreferences.verifyNoInteractions(); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/WrapperProviderTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/WrapperProviderTest.java new file mode 100644 index 00000000..263f1ab9 --- /dev/null +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/wrapper/WrapperProviderTest.java @@ -0,0 +1,63 @@ +package checkmarx.ast.eclipse.plugin.tests.unit.wrapper; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; + +import com.checkmarx.ast.project.Project; +import com.checkmarx.ast.wrapper.CxWrapper; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; + +class WrapperProviderTest { + + private final WrapperProvider wrapperProvider = new WrapperProvider(); + + @Test + void testIsAiMcpServerEnabled_forwardsCredentialsAndReturnsWrapperResult() throws Exception { + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> when(mock.aiMcpServerEnabled()).thenReturn(true))) { + + boolean result = wrapperProvider.isAiMcpServerEnabled("api-key", "--param"); + + assertTrue(result); + assertEquals(1, mocked.constructed().size()); + } + } + + @Test + void testIsAiMcpServerEnabled_propagatesFalseWhenDisabled() throws Exception { + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> when(mock.aiMcpServerEnabled()).thenReturn(false))) { + + boolean result = wrapperProvider.isAiMcpServerEnabled("api-key", "--param"); + + assertFalse(result); + } + } + + @Test + void testGetProjects_forwardsLimitAndReturnsWrapperResult() throws Exception { + Project mockProject = mock(Project.class); + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> when(mock.projectList("limit=10")).thenReturn(List.of(mockProject)))) { + + List projects = wrapperProvider.getProjects("limit=10"); + + assertEquals(1, projects.size()); + assertSame(mockProject, projects.get(0)); + } + } + + @Test + void testTriageGetStates_propagatesExceptionFromWrapper() throws Exception { + try (MockedConstruction mocked = mockConstruction(CxWrapper.class, + (mock, context) -> when(mock.triageGetStates(false)).thenThrow(new RuntimeException("boom")))) { + + assertThrows(RuntimeException.class, () -> wrapperProvider.triageGetStates(false)); + } + } +} diff --git a/checkmarx-ast-eclipse-plugin/.classpath b/checkmarx-ast-eclipse-plugin/.classpath index fbfce51f..6ed62e61 100644 --- a/checkmarx-ast-eclipse-plugin/.classpath +++ b/checkmarx-ast-eclipse-plugin/.classpath @@ -2,6 +2,7 @@ + @@ -12,7 +13,7 @@ - + diff --git a/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF b/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF index b52c8be5..c374c236 100644 --- a/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF +++ b/checkmarx-ast-eclipse-plugin/META-INF/MANIFEST.MF @@ -29,9 +29,7 @@ Import-Package: org.eclipse.core.resources, Bundle-ActivationPolicy: lazy Bundle-Activator: com.checkmarx.eclipse.Activator Export-Package: com.checkmarx.eclipse.enums, - com.checkmarx.eclipse.properties, com.checkmarx.eclipse.utils Bundle-ClassPath: ., lib/org.eclipse.mylyn.commons.ui_4.9.0.v20251121-0615.jar, lib/org-eclipse-mylyn-commons-core.jar - \ No newline at end of file diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/DataProvider.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/DataProvider.java index 2c7d4163..4e7e7cb2 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/DataProvider.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/DataProvider.java @@ -15,8 +15,6 @@ import java.util.stream.Collectors; import org.eclipse.jgit.util.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.checkmarx.ast.codebashing.CodeBashing; import com.checkmarx.ast.learnMore.LearnMore; @@ -27,13 +25,10 @@ import com.checkmarx.ast.results.result.Node; import com.checkmarx.ast.results.result.Result; import com.checkmarx.ast.scan.Scan; -import com.checkmarx.ast.wrapper.CxConfig; import com.checkmarx.ast.wrapper.CxException; -import com.checkmarx.ast.wrapper.CxWrapper; -import com.checkmarx.eclipse.common.preferences.Preferences; -import com.checkmarx.eclipse.common.runner.Authenticator; import com.checkmarx.eclipse.common.utils.CxLogger; import com.checkmarx.eclipse.common.utils.PluginConstants; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; import com.checkmarx.eclipse.utils.PluginUtils; import com.checkmarx.eclipse.views.filters.FilterState; @@ -60,6 +55,8 @@ public class DataProvider { private List currentResultsTransformed; private List platformStates = new ArrayList<>(); + private WrapperProvider wrapperProvider = new WrapperProvider(); + /** * Singleton data provider instance * @@ -97,30 +94,25 @@ public void setCurrentResults(Results currentResults) { */ public List getProjects() throws Exception { List projectList = new ArrayList(); - - CxWrapper cxWrapper = authenticateWithAST(); - - if (cxWrapper != null) { - try { - projectList = cxWrapper.projectList(LIMIT_FILTER); - } catch (IOException | InterruptedException | CxException e) { - CxLogger.error(String.format(PluginConstants.ERROR_GETTING_PROJECTS, e.getMessage()), e); - } + try { + projectList = wrapperProvider.getProjects(LIMIT_FILTER); + + } catch (IOException | InterruptedException | CxException e) { + CxLogger.error(String.format(PluginConstants.ERROR_GETTING_PROJECTS, e.getMessage()), e); } return projectList; } - + /** * Fetch a single project directly by its ID using the project show command. * Returns null if the project cannot be retrieved. */ public Project getProjectById(String projectId) { try { - CxWrapper cxWrapper = getWrapper(); - if (cxWrapper != null && projectId != null && !projectId.isEmpty()) { - return cxWrapper.projectShow(UUID.fromString(projectId)); + if (projectId != null && !projectId.isEmpty()) { + return wrapperProvider.projectShow(UUID.fromString(projectId)); } } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_PROJECTS, e.getMessage()), e); @@ -136,101 +128,71 @@ public Project getProjectById(String projectId) { */ public List getProjects(String projectName) throws Exception { List projectList = new ArrayList(); - - CxWrapper cxWrapper = authenticateWithAST(); + String filterProject = NAME_FILTER+projectName; - - if (cxWrapper != null) { - try { - projectList = cxWrapper.projectList(filterProject); - } catch (IOException | InterruptedException | CxException e) { - CxLogger.error(String.format(PluginConstants.ERROR_GETTING_PROJECTS, e.getMessage()), e); - } + try { + projectList = wrapperProvider.getProjects(filterProject); + + } catch (IOException | InterruptedException | CxException e) { + CxLogger.error(String.format(PluginConstants.ERROR_GETTING_PROJECTS, e.getMessage()), e); } return projectList; } - + /** * Get the codeBashing link - * @throws Exception + * @throws Exception */ - + public CodeBashing getCodeBashingLink(String cwe, String language, String queryName) throws CxException, Exception { - CxWrapper cxWrapper = getWrapper(); - - return cxWrapper.codeBashingList(cwe, language, queryName).get(0); + return wrapperProvider.codeBashingList(cwe, language, queryName).get(0); } - + /** * Get branches for a specific project - * + * * @param projectId * @return */ public List getBranchesForProject(String projectId) { this.projectId = projectId; List branchList = new ArrayList(); - + try { - CxWrapper cxWrapper = getWrapper(); - if(!StringUtils.isEmptyOrNull(projectId)) { - branchList = cxWrapper.projectBranches(UUID.fromString(projectId), PluginConstants.EMPTY_STRING); + branchList = wrapperProvider.projectBranches(UUID.fromString(projectId), PluginConstants.EMPTY_STRING); } } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_BRANCHES, projectId, e.getMessage()), e); } - + return branchList; } - + /** * Get scans for a specific project based on a provided branch - * + * * @param branch * @return */ public List getScansForProject(String branch) { List scanList = new ArrayList<>(); - + try { String filter = String.format(FILTER_SCANS_FOR_PROJECT, projectId, branch); - CxWrapper cxWrapper = getWrapper(); - scanList = cxWrapper.scanList(filter); + scanList = wrapperProvider.scanList(filter); } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_SCANS, projectId, branch, e.getMessage()), e); } - + return scanList; } - - /** - * Authenticate to One with current credentials - * @throws Exception - */ - private static CxWrapper authenticateWithAST() throws Exception { - CxWrapper cxWrapper = null; - - try { - - cxWrapper = getWrapper(); - String validationResult = cxWrapper.authValidate(); - - CxLogger.info(String.format(PluginConstants.INFO_AUTHENTICATION_STATUS, validationResult)); - } catch (CxException e) { - CxLogger.error(String.format(PluginConstants.ERROR_AUTHENTICATING_AST, e.getMessage()), e); - throw new Exception(e); - } - - return cxWrapper; - } - /** * Get results for a specific scan id * @@ -249,10 +211,9 @@ public List getResultsForScanId(String scanId) { CxLogger.warning("Failed to fetch all platform states on scan load: " + e.getMessage()); } - try { + try { CxLogger.info(String.format(PluginConstants.INFO_FETCHING_RESULTS, scanId)); - CxWrapper cxWrapper = getWrapper(); - scanResults = cxWrapper.results(UUID.fromString(scanId), ECLIPSE_AGENT); + scanResults = wrapperProvider.results(UUID.fromString(scanId), ECLIPSE_AGENT); setCurrentResults(scanResults); CxLogger.info(String.format(PluginConstants.INFO_SCAN_RESULTS_COUNT, scanResults.getTotalCount())); @@ -273,12 +234,10 @@ public List getResultsForScanId(String scanId) { */ public Scan getScanInformation(String scanId) throws Exception { Scan scan = null; - - CxWrapper cxWrapper = getWrapper(); - + try { CxLogger.info(String.format(PluginConstants.INFO_GETTING_SCAN_INFO, scanId)); - scan = cxWrapper.scanShow(UUID.fromString(scanId)); + scan = wrapperProvider.scanShow(UUID.fromString(scanId)); } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_GETTING_SCAN_INFO, e.getMessage()), e); throw new Exception(e); @@ -709,29 +668,6 @@ private int getParentCounter(List results) { } - /** - * Create a CxWrapper with current credentials - * - * @return - * @throws Exception - */ - private static CxWrapper getWrapper() throws Exception { - CxWrapper cxWrapper = null; - - Logger log = LoggerFactory.getLogger(Authenticator.class.getName()); - - CxConfig config = CxConfig.builder().apiKey(Preferences.getApiKey()).additionalParameters(Preferences.getAdditionalOptions()).build(); - - try { - cxWrapper = new CxWrapper(config, log); - } catch (IOException e) { - CxLogger.error(String.format(PluginConstants.ERROR_BUILDING_CX_WRAPPER, e.getMessage()), e); - throw new Exception(e); - } - - return cxWrapper; - } - /** * Check if plugin has results loaded * @@ -746,37 +682,33 @@ public boolean containsResults() { */ public int getBestFixLocation(UUID scanId, String queryId, List bflNodes) throws Exception { - CxWrapper cxWrapper = authenticateWithAST(); - int bflNode = -1; - if(cxWrapper != null) { - bflNode = cxWrapper.getResultsBfl(scanId, queryId, bflNodes); + try{ + return wrapperProvider.getResultsBfl(scanId, queryId, bflNodes); + }catch(Exception ex){ + CxLogger.error(String.format("Exception occurred while getting resultsbfl. Root cause: %s", ex.getMessage()), ex); + return -1; } - return bflNode; } - + /** * Get One Triage details - * + * * @return - * @throws Exception + * @throws Exception */ public List getTriageShow(UUID projectID, String similarityID, String scanType) throws Exception { List triageList = new ArrayList(); - CxWrapper cxWrapper = authenticateWithAST(); - // TODO: remove this condition when CLI is updated to manage these checks if(scanType.equals(PluginConstants.KICS_INFRASTRUCTURE)) { scanType = "kics"; } - if (cxWrapper != null) { - try { - triageList = cxWrapper.triageShow(projectID, similarityID, scanType); + try { + triageList = wrapperProvider.triageShow(projectID, similarityID, scanType); - } catch (IOException | InterruptedException | CxException e) { - CxLogger.error(String.format(PluginConstants.ERROR_GETTING_TRIAGE_DETAILS, e.getMessage()), e); - } + } catch (IOException | InterruptedException | CxException e) { + CxLogger.error(String.format(PluginConstants.ERROR_GETTING_TRIAGE_DETAILS, e.getMessage()), e); } return triageList; @@ -796,22 +728,18 @@ public List getTriageShow(UUID projectID, String similarityID, String public void triageUpdate(UUID projectId, String similarityId, String engineType, String state, String comment, String severity) throws Exception { try { - CxWrapper cxWrapper = authenticateWithAST(); - - if (cxWrapper != null) { - cxWrapper.triageUpdate(projectId, similarityId, engineType, state, comment, severity); - } + wrapperProvider.triageUpdate(projectId, similarityId, engineType, state, comment, severity); } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_UPDATING_TRIAGE, e.getMessage()), e); throw new Exception(e.getMessage()); - + } } - - public List learnMore(String queryId) throws Exception { - return authenticateWithAST().learnMore(queryId); + + public List learnMore(String queryId) throws Exception { + return wrapperProvider.learnMore(queryId); } - + public Scan createScan(String sourcePath, String projectName, String branchName) throws IOException, InterruptedException, CxException, Exception { Map scanArguments = new HashMap<>(); scanArguments.put("-s", sourcePath); @@ -820,16 +748,16 @@ public Scan createScan(String sourcePath, String projectName, String branchName) scanArguments.put("--agent", ECLIPSE_AGENT); String additionalParameters = "--async --sast-incremental --resubmit"; - - return authenticateWithAST().scanCreate(scanArguments, additionalParameters); + + return wrapperProvider.scanCreate(scanArguments, additionalParameters); } - + public void cancelScan(String scanId) throws IOException, InterruptedException, CxException, Exception { - authenticateWithAST().scanCancel(scanId); + wrapperProvider.scanCancel(scanId); } - + public boolean isScanAllowed() throws CxException, IOException, InterruptedException, Exception { - return authenticateWithAST().ideScansEnabled(); + return wrapperProvider.ideScansEnabled(); } /** @@ -841,11 +769,10 @@ private List getAllStatesFromPlatform() throws Exception { return Collections.emptyList(); } - CxWrapper cxWrapper = authenticateWithAST(); List allStates = new ArrayList<>(); try { - List customStates = cxWrapper.triageGetStates(false); + List customStates = wrapperProvider.triageGetStates(false); allStates = customStates.stream().map(CustomState::getName).collect(Collectors.toList()); } catch (Exception e) { CxLogger.warning("Could not fetch platform states: " + e.getMessage()); diff --git a/common-lib/META-INF/MANIFEST.MF b/common-lib/META-INF/MANIFEST.MF index 97bd5d8d..0874fa8c 100644 --- a/common-lib/META-INF/MANIFEST.MF +++ b/common-lib/META-INF/MANIFEST.MF @@ -22,40 +22,41 @@ Require-Bundle: org.eclipse.core.runtime, org.eclipse.ui.workbench.texteditor, org.eclipse.ui.editors Bundle-RequiredExecutionEnvironment: JavaSE-17 -Export-Package: com.checkmarx.eclipse.common.enums, - com.checkmarx.eclipse.common.events, - com.checkmarx.eclipse.common.listener, - com.checkmarx.eclipse.common.preferences, - com.checkmarx.eclipse.common.runner, - com.checkmarx.eclipse.common.utils, - org.apache.commons.lang3, - org.apache.commons.lang3.builder, - org.apache.commons.lang3.exception, - org.apache.commons.lang3.text, - org.apache.commons.lang3.time, - org.apache.commons.lang3.tuple, - com.checkmarx.ast.wrapper, - com.checkmarx.ast.project, - com.checkmarx.ast.scan, - com.checkmarx.ast.results, - com.checkmarx.ast.results.result, +Export-Package: com.checkmarx.ast.asca, com.checkmarx.ast.codebashing, - com.checkmarx.ast.learnMore, - com.checkmarx.ast.predicate, - com.checkmarx.ast.asca, com.checkmarx.ast.containersrealtime, com.checkmarx.ast.iacrealtime, com.checkmarx.ast.kicsRealtimeResults, com.checkmarx.ast.kicsRealtimeResults.ast.kicsRealtimeResult, + com.checkmarx.ast.learnMore, com.checkmarx.ast.mask, com.checkmarx.ast.ossrealtime, + com.checkmarx.ast.predicate, + com.checkmarx.ast.project, com.checkmarx.ast.realtime, com.checkmarx.ast.remediation, + com.checkmarx.ast.results, + com.checkmarx.ast.results.result, + com.checkmarx.ast.scan, com.checkmarx.ast.secretsrealtime, com.checkmarx.ast.tenant, com.checkmarx.ast.utils, + com.checkmarx.ast.wrapper, + com.checkmarx.eclipse.common.enums, + com.checkmarx.eclipse.common.events, + com.checkmarx.eclipse.common.listener, + com.checkmarx.eclipse.common.preferences, + com.checkmarx.eclipse.common.runner, + com.checkmarx.eclipse.common.utils, + com.checkmarx.eclipse.common.wrapper, com.fasterxml.jackson.annotation, com.fasterxml.jackson.core, com.fasterxml.jackson.core.type, com.fasterxml.jackson.databind, + org.apache.commons.lang3, + org.apache.commons.lang3.builder, + org.apache.commons.lang3.exception, + org.apache.commons.lang3.text, + org.apache.commons.lang3.time, + org.apache.commons.lang3.tuple, org.slf4j diff --git a/common-lib/src/com/checkmarx/eclipse/common/runner/Authenticator.java b/common-lib/src/com/checkmarx/eclipse/common/runner/Authenticator.java index 61c497ce..2ed9b747 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/runner/Authenticator.java +++ b/common-lib/src/com/checkmarx/eclipse/common/runner/Authenticator.java @@ -1,19 +1,16 @@ package com.checkmarx.eclipse.common.runner; -import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.checkmarx.ast.wrapper.CxConfig; -import com.checkmarx.ast.wrapper.CxException; -import com.checkmarx.ast.wrapper.CxWrapper; import com.checkmarx.eclipse.common.utils.CxLogger; import com.checkmarx.eclipse.common.utils.PluginConstants; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; public class Authenticator { private final Logger log; - - private Authenticator() { + + public Authenticator() { this.log = LoggerFactory.getLogger(Authenticator.class); } @@ -26,16 +23,11 @@ public Authenticator(Logger logger) { public static final Authenticator INSTANCE = new Authenticator(); public String doAuthentication(String apiKey, String additionalParams) { - CxConfig config = CxConfig.builder() - .apiKey(apiKey) - .additionalParameters(additionalParams) - .build(); try { - CxWrapper wrapper = new CxWrapper(config, log); - String cxValidateOutput = wrapper.authValidate(); + String cxValidateOutput = new WrapperProvider().authValidate(apiKey, additionalParams); CxLogger.info(String.format(PluginConstants.INFO_AUTHENTICATION_STATUS, cxValidateOutput)); return cxValidateOutput; - } catch (IOException | InterruptedException | CxException e) { + } catch (Exception e) { CxLogger.error(String.format(PluginConstants.ERROR_AUTHENTICATING_AST, e.getMessage()), e); return e.getMessage(); } diff --git a/common-lib/src/com/checkmarx/eclipse/common/runner/TenantSettingsProvider.java b/common-lib/src/com/checkmarx/eclipse/common/runner/TenantSettingsProvider.java index f4ed02e8..2ed6a381 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/runner/TenantSettingsProvider.java +++ b/common-lib/src/com/checkmarx/eclipse/common/runner/TenantSettingsProvider.java @@ -1,21 +1,14 @@ package com.checkmarx.eclipse.common.runner; -import java.io.IOException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.checkmarx.ast.wrapper.CxConfig; -import com.checkmarx.ast.wrapper.CxException; -import com.checkmarx.ast.wrapper.CxWrapper; import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; /** * Provides tenant-specific settings from the Checkmarx API. * Fetches configuration details like MCP enablement status. */ public class TenantSettingsProvider { - private static final Logger log = LoggerFactory.getLogger(TenantSettingsProvider.class); + private static final String LOG_PREFIX = "[TENANT_SETTINGS_PROVIDER] "; public static final TenantSettingsProvider INSTANCE = new TenantSettingsProvider(); private TenantSettingsProvider() { @@ -32,19 +25,12 @@ public boolean isAiMcpServerEnabled(String apiKey, String additionalParams) { if (apiKey == null || apiKey.trim().isEmpty()) { return false; } - try { - CxConfig config = CxConfig.builder() - .apiKey(apiKey) - .additionalParameters(additionalParams) - .build(); - - CxWrapper wrapper = new CxWrapper(config, log); - boolean mcpEnabled = wrapper.aiMcpServerEnabled(); + boolean mcpEnabled = new WrapperProvider().isAiMcpServerEnabled(apiKey, additionalParams); CxLogger.info(String.format("MCP Server Status: %s", mcpEnabled ? "ENABLED" : "DISABLED")); return mcpEnabled; - } catch (IOException | InterruptedException | CxException e) { - CxLogger.error("Failed to check MCP server status: " + e.getMessage(), e); + } catch (Exception e) { + CxLogger.error(String.format("%s Failed to check MCP server status: %s", LOG_PREFIX, e.getMessage()), e); // Default to false on error to be conservative return false; } diff --git a/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java index 76272e05..a30ea4de 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java +++ b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java @@ -3,6 +3,7 @@ import com.checkmarx.eclipse.common.events.SettingsTopics; public class PluginConstants { + public static final String AGENT_NAME = "Eclipse"; public static final String EMPTY_STRING = ""; public static final String SAST = "sast"; public static final String SCA_DEPENDENCY = "sca"; diff --git a/common-lib/src/com/checkmarx/eclipse/common/wrapper/CxWrapperFactory.java b/common-lib/src/com/checkmarx/eclipse/common/wrapper/CxWrapperFactory.java new file mode 100644 index 00000000..461094ef --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/wrapper/CxWrapperFactory.java @@ -0,0 +1,89 @@ +package com.checkmarx.eclipse.common.wrapper; + +import com.checkmarx.ast.wrapper.CxConfig; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.ast.wrapper.CxWrapper; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Builds wrapper objects according to the current configuration. + */ +public class CxWrapperFactory { + + public static CxWrapper build() throws CxException, Exception { + return build(Preferences.getApiKey(), Preferences.getAdditionalOptions()); + } + + /** + * Create a CxWrapper with the given credentials and the current agent configuration. + * Used when the credentials being validated aren't necessarily the ones already saved + * (e.g. the Preferences page "Test Connection" action). + * + * @param apiKey the API key to authenticate with + * @param additionalParameters additional CLI parameters + * @return initialized CxWrapper instance + * @throws Exception if wrapper instantiation fails + */ + public static CxWrapper build(String apiKey, String additionalParameters) throws CxException, Exception { + return getWrapper(apiKey, additionalParameters); + } + + /** + * Create a CxWrapper with the given credentials and configuration + * + * @return initialized CxWrapper instance + * @throws Exception if wrapper instantiation fails + */ + private static CxWrapper getWrapper(String apiKey, String additionalParameters) throws Exception { + CxWrapper cxWrapper = null; + + Logger log = LoggerFactory.getLogger(CxWrapperFactory.class.getName()); + + CxConfig config = CxConfig.builder() + .apiKey(apiKey) + .additionalParameters(additionalParameters) + .agentName(getAgentInfo()) + .build(); + try { + cxWrapper = new CxWrapper(config, log); + } catch (IOException e) { + CxLogger.error(String.format(PluginConstants.ERROR_BUILDING_CX_WRAPPER, e.getMessage()), e); + throw new Exception(e); + } + + return cxWrapper; + } + + /** + * Get the agent information string for the CxWrapper + * @return + */ + private static String getAgentInfo() { + String pluginVersion = getPluginVersion(); + CxLogger.info(String.format("PLUGIN_VERSION: %s_%s", PluginConstants.AGENT_NAME, pluginVersion)); + return String.format("%s_%s", PluginConstants.AGENT_NAME, pluginVersion); + } + + /** + * Resolve the version of the bundle this class ships in, as stamped by the + * build (Tycho replaces the "qualifier" placeholder in MANIFEST.MF with the + * real build qualifier), falling back when running outside an OSGi framework. + */ + private static String getPluginVersion() { + try { + Bundle bundle = FrameworkUtil.getBundle(CxWrapperFactory.class); + return bundle != null ? bundle.getVersion().toString() : "0.0.0"; + } catch (Exception e) { + CxLogger.error(String.format("Exception occurred while getting plugin version. Root cause: %s", e.getMessage()), e); + return "0.0.0"; + } + } +} \ No newline at end of file diff --git a/common-lib/src/com/checkmarx/eclipse/common/wrapper/WrapperProvider.java b/common-lib/src/com/checkmarx/eclipse/common/wrapper/WrapperProvider.java new file mode 100644 index 00000000..7ae10013 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/wrapper/WrapperProvider.java @@ -0,0 +1,268 @@ +package com.checkmarx.eclipse.common.wrapper; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.checkmarx.ast.asca.ScanResult; +import com.checkmarx.ast.codebashing.CodeBashing; +import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; +import com.checkmarx.ast.iacrealtime.IacRealtimeResults; +import com.checkmarx.ast.learnMore.LearnMore; +import com.checkmarx.ast.ossrealtime.OssRealtimeResults; +import com.checkmarx.ast.predicate.CustomState; +import com.checkmarx.ast.predicate.Predicate; +import com.checkmarx.ast.project.Project; +import com.checkmarx.ast.results.Results; +import com.checkmarx.ast.results.result.Node; +import com.checkmarx.ast.scan.Scan; +import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; + +/** + * Exposes CxWrapper operations to the rest of the plugin. Every call goes + * through CxWrapperFactory so the wrapper is always built with the current + * credentials and agent information. + */ +public class WrapperProvider { + + /** + * Authenticate with the given credentials, independently of what is currently saved + * in Preferences (e.g. the Preferences page "Test Connection" action). + * @param apiKey + * @param additionalParameters + * @return + * @throws Exception + */ + public String authValidate(String apiKey, String additionalParameters) throws Exception { + return CxWrapperFactory.build(apiKey, additionalParameters).authValidate(); + } + + /** + * Gets the list of projects from the Checkmarx API, optionally limiting the number of results. + * @param limit + * @return + * @throws Exception + */ + public List getProjects(String limit) throws Exception { + return CxWrapperFactory.build().projectList(limit); + } + + /** + * Fetch a single project directly by its ID. + * @param projectId + * @return + * @throws Exception + */ + public Project projectShow(UUID projectId) throws Exception { + return CxWrapperFactory.build().projectShow(projectId); + } + + /** + * Get branches for a specific project. + * @param projectId + * @param filter + * @return + * @throws Exception + */ + public List projectBranches(UUID projectId, String filter) throws Exception { + return CxWrapperFactory.build().projectBranches(projectId, filter); + } + + /** + * Get scans matching the given filter. + * @param filter + * @return + * @throws Exception + */ + public List scanList(String filter) throws Exception { + return CxWrapperFactory.build().scanList(filter); + } + + /** + * Get scan information for a specific scan id. + * @param scanId + * @return + * @throws Exception + */ + public Scan scanShow(UUID scanId) throws Exception { + return CxWrapperFactory.build().scanShow(scanId); + } + + /** + * Create a scan for the given source path/project/branch. + * @param scanArguments + * @param additionalParameters + * @return + * @throws Exception + */ + public Scan scanCreate(Map scanArguments, String additionalParameters) throws Exception { + return CxWrapperFactory.build().scanCreate(scanArguments, additionalParameters); + } + + /** + * Cancel a running scan. + * @param scanId + * @throws Exception + */ + public void scanCancel(String scanId) throws Exception { + CxWrapperFactory.build().scanCancel(scanId); + } + + /** + * Get results for a specific scan id. + * @param scanId + * @param agent + * @return + * @throws Exception + */ + public Results results(UUID scanId, String agent) throws Exception { + return CxWrapperFactory.build().results(scanId, agent); + } + + /** + * Get the codeBashing lessons matching a CWE/language/query name. + * @param cwe + * @param language + * @param queryName + * @return + * @throws Exception + */ + public List codeBashingList(String cwe, String language, String queryName) throws Exception { + return CxWrapperFactory.build().codeBashingList(cwe, language, queryName); + } + + /** + * Get the best fix location among the given nodes. + * @param scanId + * @param queryId + * @param bflNodes + * @return + * @throws Exception + */ + public int getResultsBfl(UUID scanId, String queryId, List bflNodes) throws Exception { + return CxWrapperFactory.build().getResultsBfl(scanId, queryId, bflNodes); + } + + /** + * Get triage details for a similarity id. + * @param projectId + * @param similarityId + * @param scanType + * @return + * @throws Exception + */ + public List triageShow(UUID projectId, String similarityId, String scanType) throws Exception { + return CxWrapperFactory.build().triageShow(projectId, similarityId, scanType); + } + + /** + * Update a vulnerability severity or state. + * @param projectId + * @param similarityId + * @param engineType + * @param state + * @param comment + * @param severity + * @throws Exception + */ + public void triageUpdate(UUID projectId, String similarityId, String engineType, String state, String comment, + String severity) throws Exception { + CxWrapperFactory.build().triageUpdate(projectId, similarityId, engineType, state, comment, severity); + } + + /** + * Triages the states from the Checkmarx API, optionally forcing a refresh of the cached states. + * @param forceRefresh + * @return + * @throws Exception + */ + public List triageGetStates(boolean forceRefresh) throws Exception { + return CxWrapperFactory.build().triageGetStates(forceRefresh); + } + + /** + * Get learn more information for a query. + * @param queryId + * @return + * @throws Exception + */ + public List learnMore(String queryId) throws Exception { + return CxWrapperFactory.build().learnMore(queryId); + } + + /** + * Check if scanning from the IDE is allowed for the current tenant. + * @return + * @throws Exception + */ + public boolean ideScansEnabled() throws Exception { + return CxWrapperFactory.build().ideScansEnabled(); + } + + /** + * Check if AI MCP (Checkmarx One Assist) is enabled for the current tenant. + * @return + * @throws Exception + */ + public boolean isAiMcpServerEnabled(String apiKey, String additionalParameter) throws Exception { + return CxWrapperFactory.build(apiKey, additionalParameter).aiMcpServerEnabled(); + } + + /** + * Run a Checkmarx ASCA (AI Security Code Assistant) realtime scan on a file. + * @param path + * @param latestVersion + * @param agent + * @param ignoreFilePath + * @return + * @throws Exception + */ + public ScanResult scanAsca(String path, boolean latestVersion, String agent, String ignoreFilePath) throws Exception { + return CxWrapperFactory.build().ScanAsca(path, latestVersion, agent, ignoreFilePath); + } + + /** + * Run a Checkmarx OSS (Software Composition Analysis) realtime scan on a manifest file. + * @param path + * @param ignoreFilePath + * @return + * @throws Exception + */ + public OssRealtimeResults ossRealtimeScan(String path, String ignoreFilePath) throws Exception { + return CxWrapperFactory.build().ossRealtimeScan(path, ignoreFilePath); + } + + /** + * Run a Checkmarx Containers realtime scan on a file. + * @param path + * @param ignoreFilePath + * @return + * @throws Exception + */ + public ContainersRealtimeResults containersRealtimeScan(String path, String ignoreFilePath) throws Exception { + return CxWrapperFactory.build().containersRealtimeScan(path, ignoreFilePath); + } + + /** + * Run a Checkmarx IaC realtime scan on a file. + * @param path + * @param containerTool + * @param ignoreFilePath + * @return + * @throws Exception + */ + public IacRealtimeResults iacRealtimeScan(String path, String containerTool, String ignoreFilePath) throws Exception { + return CxWrapperFactory.build().iacRealtimeScan(path, containerTool, ignoreFilePath); + } + + /** + * Run a Checkmarx Secrets realtime scan on a file. + * @param path + * @param ignoreFilePath + * @return + * @throws Exception + */ + public SecretsRealtimeResults secretsRealtimeScan(String path, String ignoreFilePath) throws Exception { + return CxWrapperFactory.build().secretsRealtimeScan(path, ignoreFilePath); + } +} diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java deleted file mode 100644 index bc7f9abd..00000000 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/factory/CxWrapperFactory.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.checkmarx.eclipse.devassist.factory; - -import com.checkmarx.ast.wrapper.CxConfig; -import com.checkmarx.ast.wrapper.CxException; -import com.checkmarx.ast.wrapper.CxWrapper; -import com.checkmarx.eclipse.common.preferences.Preferences; -import com.checkmarx.eclipse.common.utils.CxLogger; -import com.checkmarx.eclipse.devassist.backend.Constants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -/** - * Builds wrapper objects according to the current configuration. - */ -public class CxWrapperFactory { - - public static CxWrapper build() throws CxException, Exception { - return getWrapper(); - } - - /** - * Create a CxWrapper with current credentials and configuration - * - * @return initialized CxWrapper instance - * @throws Exception if wrapper instantiation fails - */ - private static CxWrapper getWrapper() throws Exception { - CxWrapper cxWrapper = null; - - Logger log = LoggerFactory.getLogger(CxWrapperFactory.class.getName()); - - CxConfig.CxConfigBuilder builder = CxConfig.builder() - .apiKey(Preferences.getApiKey()) - .additionalParameters(Preferences.getAdditionalOptions()); - - CxConfig config = builder.build(); - - try { - cxWrapper = new CxWrapper(config, log); - } catch (IOException e) { - CxLogger.error(String.format(Constants.ERROR_BUILDING_CX_WRAPPER, e.getMessage()), e); - throw new Exception(e); - } - - return cxWrapper; - } -} \ No newline at end of file diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java index 7cd51fa5..9453e2a9 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java @@ -1,22 +1,24 @@ package com.checkmarx.eclipse.devassist.scanners.asca; -import com.checkmarx.ast.asca.ScanResult; -import com.checkmarx.ast.wrapper.CxException; -import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; -import com.checkmarx.eclipse.devassist.common.ScannerConfig; -import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; -import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; -import com.checkmarx.eclipse.devassist.model.ScanEngine; -import com.checkmarx.eclipse.common.utils.CxLogger; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; + +import com.checkmarx.ast.asca.ScanResult; +import com.checkmarx.ast.wrapper.CxException; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.ScanEngine; /** * ASCA (Application Source Code Analysis) scanner service. @@ -32,6 +34,7 @@ public class AscaScannerService extends BaseScannerService { private static final String LOG_TAG = "[ASCA-SERVICE]"; private static final String ASCA_DIR = "CxASCA"; private static final Object SCAN_LOCK = new Object(); + private final WrapperProvider wrapperProvider = new WrapperProvider(); public AscaScannerService(IProject project) { super(project, createConfig()); @@ -335,7 +338,7 @@ private com.checkmarx.ast.asca.ScanResult scanAscaFile(String path, boolean asca String ignoreFilePath) throws IOException, CxException, InterruptedException { com.checkmarx.ast.asca.ScanResult scanResult = null; try { - scanResult = CxWrapperFactory.build().ScanAsca(path, ascaLatestVersion, agent, null); + scanResult = wrapperProvider.scanAsca(path, ascaLatestVersion, agent, null); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java index 2c238a43..30b5d026 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java @@ -1,168 +1,193 @@ package com.checkmarx.eclipse.devassist.scanners.containers; -import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; -import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; -import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; -import com.checkmarx.eclipse.devassist.common.ScannerConfig; -import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; -import com.checkmarx.eclipse.devassist.common.ScanResult; -import com.checkmarx.eclipse.devassist.model.ScanEngine; -import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; -import com.checkmarx.eclipse.common.utils.CxLogger; -import org.eclipse.core.resources.IProject; -import org.eclipse.jface.text.IDocument; - import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.*; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalTime; -import java.util.*; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.UUID; import java.util.stream.Collectors; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.ast.containersrealtime.ContainersRealtimeResults; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; + /** * Container image scanner service for Eclipse. * - * Handles file detection (Docker, Docker Compose, Helm), secure temporary - * folder management, and direct invocation of Checkmarx Container Realtime - * scanning via CxWrapperFactory. + * Handles file detection (Docker, Docker Compose, Helm), secure temporary folder management, + * and direct invocation of Checkmarx Container Realtime scanning via CxWrapperFactory. */ public class ContainerScannerService extends BaseScannerService { - private static final String LOG_TAG = "[CONTAINER-SERVICE]"; - private static final String CONTAINER_DIR = "CxContainer"; - private static final Object SCAN_LOCK = new Object(); - - private static final List CONTAINERS_FILE_PATTERNS = List.of("**/dockerfile*", "**/*.containerfile", - "**/*.image", "**/docker-compose*.yml", "**/docker-compose*.yaml"); - - private static final List CONTAINER_HELM_EXCLUDED_FILES = List.of("chart.yaml", "chart.yml", "values.yaml", - "values.yml"); - - private String fileType; - - public ContainerScannerService(IProject project) { - super(project, createConfig()); - } - - /** - * Create default Container scanner configuration. - */ - public static ScannerConfig createConfig() { - return ScannerConfig.builder().engineName(ScanEngine.CONTAINERS.name()) - .configSection(DevAssistConstants.CONTAINER_REALTIME_SCANNER) - .activateKey(DevAssistConstants.ACTIVATE_CONTAINER_REALTIME_SCANNER) - .enabledMessage(DevAssistConstants.CONTAINER_REALTIME_SCANNER_START) - .disabledMessage(DevAssistConstants.CONTAINER_REALTIME_SCANNER_DISABLED) - .errorMessage(DevAssistConstants.ERROR_CONTAINER_REALTIME_SCANNER).build(); - } - - @Override - protected boolean isFileTypeSupported(String filePath) { - return isContainersFilePatternMatching(filePath) || isHelmFile(filePath); - } - - /** - * Checks whether the supplied file path matches container file patterns - * (Dockerfile, Docker Compose, etc.). - */ - private boolean isContainersFilePatternMatching(String filePath) { - String lowerPath = filePath.toLowerCase(); - List pathMatchers = CONTAINERS_FILE_PATTERNS.stream() - .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)).collect(Collectors.toList()); - - Path path = Paths.get(lowerPath); - for (PathMatcher pathMatcher : pathMatchers) { - if (pathMatcher.matches(path) || lowerPath.contains("dockerfile")) { - if (DevAssistUtils.isDockerComposeFile(lowerPath)) { - this.fileType = DevAssistUtils.DOCKER_COMPOSE; - } else if (DevAssistUtils.isDockerFile(lowerPath)) { - this.fileType = DevAssistUtils.DOCKERFILE; - } - return true; - } - } - return false; - } - - /** - * Checks whether the supplied file path is part of a Helm chart. - */ - public boolean isHelmFile(String filePath) { - if (filePath == null) { - return false; - } - String lowerPath = filePath.toLowerCase(); - if (DevAssistUtils.isYamlFile(lowerPath)) { - String fileName = Paths.get(filePath).getFileName().toString().toLowerCase(); - if (CONTAINER_HELM_EXCLUDED_FILES.contains(fileName)) { - return false; - } - if (lowerPath.contains("/helm/")) { - this.fileType = DevAssistUtils.HELM; - return true; - } - } - return false; - } - - /** - * Primary scan method. Reads content, creates isolated temporary directory - * structure, executes the container realtime scan, and updates ignored issues. - */ - public ScanResult scan(String filePath, IDocument document, IProject proj) { - if (!shouldScanFile(filePath)) { - return null; - } - - synchronized (SCAN_LOCK) { - String fileContent = getFileContent(filePath, document); - if (fileContent == null || fileContent.isBlank()) { - CxLogger.warning(LOG_TAG + " Could not read or file empty: " + filePath); - return null; - } - - Path tempBaseDir = getSecureTempDirectory(); - Path tempSubFolder = null; - Path tempFilePath = null; - - try { - String fileName = Paths.get(filePath).getFileName().toString(); - String prefix = isHelmFile(filePath) ? "helm-" : fileName + "-"; - String folderName = prefix + generateFileHash(filePath); - tempSubFolder = tempBaseDir.resolve(folderName).normalize(); - createTempFolder(tempSubFolder); - tempFilePath = tempSubFolder.resolve(fileName).normalize(); - Files.writeString(tempFilePath, fileContent, StandardCharsets.UTF_8); - CxLogger.info(LOG_TAG + " Start Container Realtime Scan On File: " + filePath); + private static final String LOG_TAG = "[CONTAINER-SERVICE]"; + private static final String CONTAINER_DIR = "CxContainer"; + private static final Object SCAN_LOCK = new Object(); + private final WrapperProvider wrapperProvider = new WrapperProvider(); + + private static final List CONTAINERS_FILE_PATTERNS = List.of( + "**/dockerfile*", + "**/*.containerfile", + "**/*.image", + "**/docker-compose*.yml", + "**/docker-compose*.yaml" + ); + + private static final List CONTAINER_HELM_EXCLUDED_FILES = List.of( + "chart.yaml", + "chart.yml", + "values.yaml", + "values.yml" + ); + + private String fileType; + + public ContainerScannerService(IProject project) { + super(project, createConfig()); + } + + /** + * Create default Container scanner configuration. + */ + public static ScannerConfig createConfig() { + return ScannerConfig.builder() + .engineName(ScanEngine.CONTAINERS.name()) + .configSection(DevAssistConstants.CONTAINER_REALTIME_SCANNER) + .activateKey(DevAssistConstants.ACTIVATE_CONTAINER_REALTIME_SCANNER) + .enabledMessage(DevAssistConstants.CONTAINER_REALTIME_SCANNER_START) + .disabledMessage(DevAssistConstants.CONTAINER_REALTIME_SCANNER_DISABLED) + .errorMessage(DevAssistConstants.ERROR_CONTAINER_REALTIME_SCANNER) + .build(); + } + + @Override + protected boolean isFileTypeSupported(String filePath) { + return isContainersFilePatternMatching(filePath) || isHelmFile(filePath); + } + + /** + * Checks whether the supplied file path matches container file patterns (Dockerfile, Docker Compose, etc.). + */ + private boolean isContainersFilePatternMatching(String filePath) { + String lowerPath = filePath.toLowerCase(); + List pathMatchers = CONTAINERS_FILE_PATTERNS.stream() + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) + .collect(Collectors.toList()); + + Path path = Paths.get(lowerPath); + for (PathMatcher pathMatcher : pathMatchers) { + if (pathMatcher.matches(path) || lowerPath.contains("dockerfile")) { + if (DevAssistUtils.isDockerComposeFile(lowerPath)) { + this.fileType = DevAssistUtils.DOCKER_COMPOSE; + } else if (DevAssistUtils.isDockerFile(lowerPath)) { + this.fileType = DevAssistUtils.DOCKERFILE; + } + return true; + } + } + return false; + } + + /** + * Checks whether the supplied file path is part of a Helm chart. + */ + public boolean isHelmFile(String filePath) { + if (filePath == null) { + return false; + } + String lowerPath = filePath.toLowerCase(); + if (DevAssistUtils.isYamlFile(lowerPath)) { + String fileName = Paths.get(filePath).getFileName().toString().toLowerCase(); + if (CONTAINER_HELM_EXCLUDED_FILES.contains(fileName)) { + return false; + } + if (lowerPath.contains("/helm/")) { + this.fileType = DevAssistUtils.HELM; + return true; + } + } + return false; + } + + /** + * Primary scan method. Reads content, creates isolated temporary directory structure, + * executes the container realtime scan, and updates ignored issues. + */ + public ScanResult scan(String filePath, IDocument document, IProject proj) { + if (!shouldScanFile(filePath)) { + return null; + } + + synchronized (SCAN_LOCK) { + String fileContent = getFileContent(filePath, document); + if (fileContent == null || fileContent.isBlank()) { + CxLogger.warning(LOG_TAG + " Could not read or file empty: " + filePath); + return null; + } + + Path tempBaseDir = getSecureTempDirectory(); + Path tempSubFolder = null; + Path tempFilePath = null; + + try { + String fileName = Paths.get(filePath).getFileName().toString(); + String prefix = isHelmFile(filePath) ? "helm-" : fileName + "-"; + String folderName = prefix + generateFileHash(filePath); + + tempSubFolder = tempBaseDir.resolve(folderName).normalize(); + createTempFolder(tempSubFolder); + + tempFilePath = tempSubFolder.resolve(fileName).normalize(); + Files.writeString(tempFilePath, fileContent, StandardCharsets.UTF_8); + + CxLogger.info(LOG_TAG + " Start Container Realtime Scan On File: " + filePath); // String ignoreFilePath = DevAssistUtils.getIgnoreFilePath(proj != null ? proj : this.project); - ContainersRealtimeResults scanResults = null; + + ContainersRealtimeResults scanResults = null; try { - scanResults = CxWrapperFactory.build().containersRealtimeScan(tempFilePath.toString(), ""); + scanResults = wrapperProvider.containersRealtimeScan(tempFilePath.toString(), ""); } catch (Exception e) { - CxLogger.error(LOG_TAG + " Container Realtime Scan failed: " + e.getMessage(), e); - } - updateIgnoredFileDataOnLatestResult(tempFilePath.toString(), proj != null ? proj : this.project, - filePath); - return new ContainerScanResultAdaptor(scanResults, this.fileType, filePath); - } catch (IOException e) { - CxLogger.error(LOG_TAG + " Container Realtime Scan failed: " + e.getMessage(), e); - } finally { - if (Objects.nonNull(tempSubFolder)) { - deleteTempFolder(tempSubFolder); + // TODO Auto-generated catch block + e.printStackTrace(); } - } - } - return null; - } - - /** - * Re-runs scan without ignore settings to calculate line updates for ignored - * entries. - */ - private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { + + updateIgnoredFileDataOnLatestResult(tempFilePath.toString(), proj != null ? proj : this.project, filePath); + + return new ContainerScanResultAdaptor(scanResults, this.fileType, filePath); + + } catch (IOException e) { + CxLogger.error(LOG_TAG + " Container Realtime Scan failed: " + e.getMessage(), e); + } finally { + if (Objects.nonNull(tempSubFolder)) { + deleteTempFolder(tempSubFolder); + } + } + } + return null; + } + + /** + * Re-runs scan without ignore settings to calculate line updates for ignored entries. + */ + private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject proj, String filePath) { // try { // IgnoreManager ignoreManager = new IgnoreManager(proj); // if (ignoreManager.hasIgnoredEntries(ScanEngine.CONTAINERS)) { @@ -178,100 +203,102 @@ private void updateIgnoredFileDataOnLatestResult(String tempFilePath, IProject p // } catch (Exception e) { // CxLogger.warning(LOG_TAG + " Exception occurred while updating ignored file line numbers: " + e.getMessage()); // } - } - - /** - * Reads file content from Eclipse IDocument buffer or disk filesystem. - */ - private String getFileContent(String filePath, IDocument document) { - if (document != null) { - String content = document.get(); - if (content != null && !content.isEmpty()) { - return content; - } - } - - if (filePath == null || filePath.isBlank()) { - return null; - } - - try { - Path nioPath = Paths.get(filePath); - if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { - return Files.readString(nioPath, StandardCharsets.UTF_8); - } - } catch (IOException e) { - CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); - } - return null; - } - - /** - * Generates a unique 16-character hexadecimal hash using SHA-256 for temporary - * directory names. - */ - private String generateFileHash(String relativePath) { - try { - LocalTime time = LocalTime.now(); - String timeSuffix = String.format("%02d%02d", time.getMinute(), time.getSecond()); - String combined = relativePath + timeSuffix + UUID.randomUUID().toString().substring(0, 5); - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hashBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); - StringBuilder hexString = new StringBuilder(); - for (byte b : hashBytes) { - hexString.append(String.format("%02x", b)); - } - return hexString.substring(0, 16); - } catch (NoSuchAlgorithmException e) { - return Integer.toHexString((relativePath + System.currentTimeMillis()).hashCode()); - } - } - - private Path getSecureTempDirectory() { - String tempOSPath = System.getProperty("java.io.tmpdir"); - if (tempOSPath == null || tempOSPath.isBlank()) { - tempOSPath = System.getProperty("user.home"); - } - Path baseTempDir = Paths.get(tempOSPath).toAbsolutePath().normalize(); - return baseTempDir.resolve(CONTAINER_DIR).normalize(); - } - - protected void createTempFolder(Path tempDir) { - if (!Files.exists(tempDir)) { - try { - Files.createDirectories(tempDir); - } catch (IOException e) { - CxLogger.warning(LOG_TAG + " Failed to create temp folder: " + e.getMessage()); - } - } - } - - protected void deleteTempFolder(Path path) { - if (path == null || !Files.exists(path)) { - return; - } - try { - Files.walk(path).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); - CxLogger.info(LOG_TAG + " Temporary folder deleted: " + path.toAbsolutePath()); - } catch (Exception e) { - CxLogger.warning(LOG_TAG + " Failed to delete temporary directory: " + e.getMessage()); - } - } - - /** - * Compatibility method matching ScannerService interface. - */ - @Override - public ScanResult scan(String filePath) { - if (!shouldScanFile(filePath)) { - return null; - } - IDocument liveDocument = DevAssistUtils.getLiveDocumentForFile(filePath); - return scan(filePath, liveDocument, project); - } - - @Override - public void close() throws Exception { - // No persistent connections to close - } + } + + /** + * Reads file content from Eclipse IDocument buffer or disk filesystem. + */ + private String getFileContent(String filePath, IDocument document) { + if (document != null) { + String content = document.get(); + if (content != null && !content.isEmpty()) { + return content; + } + } + + if (filePath == null || filePath.isBlank()) { + return null; + } + + try { + Path nioPath = Paths.get(filePath); + if (Files.exists(nioPath) && Files.isRegularFile(nioPath)) { + return Files.readString(nioPath, StandardCharsets.UTF_8); + } + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to read file content from disk: " + e.getMessage()); + } + return null; + } + + /** + * Generates a unique 16-character hexadecimal hash using SHA-256 for temporary directory names. + */ + private String generateFileHash(String relativePath) { + try { + LocalTime time = LocalTime.now(); + String timeSuffix = String.format("%02d%02d", time.getMinute(), time.getSecond()); + String combined = relativePath + timeSuffix + UUID.randomUUID().toString().substring(0, 5); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + hexString.append(String.format("%02x", b)); + } + return hexString.substring(0, 16); + } catch (NoSuchAlgorithmException e) { + return Integer.toHexString((relativePath + System.currentTimeMillis()).hashCode()); + } + } + + private Path getSecureTempDirectory() { + String tempOSPath = System.getProperty("java.io.tmpdir"); + if (tempOSPath == null || tempOSPath.isBlank()) { + tempOSPath = System.getProperty("user.home"); + } + Path baseTempDir = Paths.get(tempOSPath).toAbsolutePath().normalize(); + return baseTempDir.resolve(CONTAINER_DIR).normalize(); + } + + protected void createTempFolder(Path tempDir) { + if (!Files.exists(tempDir)) { + try { + Files.createDirectories(tempDir); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Failed to create temp folder: " + e.getMessage()); + } + } + } + + protected void deleteTempFolder(Path path) { + if (path == null || !Files.exists(path)) { + return; + } + try { + Files.walk(path) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + CxLogger.info(LOG_TAG + " Temporary folder deleted: " + path.toAbsolutePath()); + } catch (Exception e) { + CxLogger.warning(LOG_TAG + " Failed to delete temporary directory: " + e.getMessage()); + } + } + + /** + * Compatibility method matching ScannerService interface. + */ + @Override + public ScanResult scan(String filePath) { + if (!shouldScanFile(filePath)) { + return null; + } + IDocument liveDocument = DevAssistUtils.getLiveDocumentForFile(filePath); + return scan(filePath, liveDocument, project); + } + + @Override + public void close() throws Exception { + // No persistent connections to close + } } \ No newline at end of file diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java index 8bd3a1df..979ba19a 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java @@ -1,31 +1,36 @@ package com.checkmarx.eclipse.devassist.scanners.iac; -import com.checkmarx.ast.iacrealtime.IacRealtimeResults; -import com.checkmarx.ast.wrapper.CxException; -import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; -import com.checkmarx.eclipse.devassist.common.ScanResult; -import com.checkmarx.eclipse.devassist.common.ScannerConfig; -import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; -import com.checkmarx.eclipse.devassist.model.ScanIssue; -import com.checkmarx.eclipse.devassist.model.ScanEngine; -import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; -import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; -import com.checkmarx.eclipse.common.utils.CxLogger; -import org.apache.commons.lang3.tuple.Pair; -import org.eclipse.core.resources.IProject; -import org.eclipse.jface.text.Document; -import org.eclipse.jface.text.IDocument; - import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.*; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalTime; -import java.util.*; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.Pair; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.ast.iacrealtime.IacRealtimeResults; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; + /** * Realtime IaC scanner service for Eclipse. * @@ -39,6 +44,7 @@ public class IacScannerService extends BaseScannerService { private static final String IAC_DIR = "CxIaC"; private static final String DOCKERFILE = "dockerfile"; private static final Object SCAN_LOCK = new Object(); + private final WrapperProvider wrapperProvider = new WrapperProvider(); // Supported glob patterns for IaC files private static final List IAC_SUPPORTED_PATTERNS = List.of( @@ -142,7 +148,7 @@ public ScanResult scan(String filePath, IDocument document, IacRealtimeResults scanResults = null; try { - scanResults = CxWrapperFactory.build() + scanResults = wrapperProvider .iacRealtimeScan(tempFilePath, containerTool, ""); } catch (Exception e) { // TODO Auto-generated catch block diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java index 65180e2b..63afb727 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java @@ -1,27 +1,37 @@ package com.checkmarx.eclipse.devassist.scanners.oss; -import com.checkmarx.ast.ossrealtime.OssRealtimeResults; -import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; -import com.checkmarx.eclipse.devassist.common.ScanResult; -import com.checkmarx.eclipse.devassist.common.ScannerConfig; -import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; -import com.checkmarx.eclipse.devassist.model.ScanEngine; -import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; -import com.checkmarx.eclipse.common.utils.CxLogger; -import org.eclipse.core.resources.IProject; -import org.eclipse.jface.text.Document; -import org.eclipse.jface.text.IDocument; - import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.*; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalTime; -import java.util.*; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.ast.ossrealtime.OssRealtimeResults; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.PackageManager; + /** * Realtime OSS manifest scanner service for Eclipse that handles temporary file isolation, * companion lock file resolution (e.g. package-lock.json), and invocation of the Checkmarx OSS engine. @@ -33,6 +43,7 @@ public class OssScannerService extends BaseScannerService { private static final String LOG_TAG = "[OSS-SERVICE]"; private static final String OSS_DIR = "CxOSS"; private static final Object SCAN_LOCK = new Object(); + private final WrapperProvider wrapperProvider = new WrapperProvider(); public OssScannerService(IProject project) { super(project, createConfig()); @@ -59,7 +70,7 @@ protected boolean isFileTypeSupported(String filePath) { } Path path = Paths.get(filePath); - List pathMatchers = DevAssistConstants.MANIFEST_FILE_PATTERNS.stream() + List pathMatchers = PackageManager.getAllPatterns().stream() .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) .collect(Collectors.toList()); @@ -112,7 +123,7 @@ public ScanResult scanWithDocument(String filePath, IDocumen CxLogger.info(LOG_TAG + " Starting Realtime OSS Scan on File: " + filePath); - OssRealtimeResults scanResults = CxWrapperFactory.build().ossRealtimeScan(mainTempPath.get(), ""); + OssRealtimeResults scanResults = wrapperProvider.ossRealtimeScan(mainTempPath.get(), ""); if (scanResults == null) { return null; } @@ -167,19 +178,21 @@ private Optional saveMainManifestFile(Path tempSubFolder, String origina } /** - * Copies a companion lock file (e.g., package-lock.json) into the temporary directory - * when it exists alongside the scanned manifest. - */ + * Copies companion lock files (e.g., package-lock.json, yarn.lock) into the temporary directory + * when they exist alongside the scanned manifest. + * + * @param tempFolderPath temp directory where companion files should be written + * @param originalFilePath original manifest path used to locate companion files + */ private void saveCompanionFile(Path tempFolderPath, String originalFilePath) { if (originalFilePath == null || originalFilePath.isEmpty() || tempFolderPath == null) { return; } - Path originalPath = Paths.get(originalFilePath); String parentFileName = originalPath.getFileName().toString(); - String companionFileName = getCompanionFileName(parentFileName); + List companionFileNameList = PackageManager.getCompanionFileNames(parentFileName); - if (companionFileName.isEmpty()) { + if (companionFileNameList.isEmpty()) { return; } @@ -187,32 +200,20 @@ private void saveCompanionFile(Path tempFolderPath, String originalFilePath) { if (parentPath == null) { return; } + for (String companionFileName : companionFileNameList) { + Path companionOriginalPath = parentPath.resolve(companionFileName); + if (!Files.exists(companionOriginalPath)) { + return; + } - Path companionOriginalPath = parentPath.resolve(companionFileName); - if (!Files.exists(companionOriginalPath)) { - return; - } - - Path companionTempPath = tempFolderPath.resolve(companionFileName); - try { - Files.copy(companionOriginalPath, companionTempPath, StandardCopyOption.REPLACE_EXISTING); - CxLogger.info(LOG_TAG + " Copied companion file: " + companionFileName); - } catch (IOException e) { - CxLogger.warning(LOG_TAG + " Error occurred while saving companion file: " + e.getMessage()); - } - } - - /** - * Infers companion lock file name based on manifest file name. - */ - private String getCompanionFileName(String fileName) { - if ("package.json".equalsIgnoreCase(fileName)) { - return "package-lock.json"; - } - if (fileName.toLowerCase().endsWith(".csproj")) { - return "package.lock.json"; + Path companionTempPath = tempFolderPath.resolve(companionFileName); + try { + Files.copy(companionOriginalPath, companionTempPath, StandardCopyOption.REPLACE_EXISTING); + CxLogger.info(LOG_TAG + " Copied companion file: " + companionFileName); + } catch (IOException e) { + CxLogger.warning(LOG_TAG + " Error occurred while saving companion file: " + e.getMessage()); + } } - return ""; } /** @@ -299,12 +300,4 @@ private String getFileContent(String filePath, IDocument document) { } return null; } - -// private String getIgnoreFilePath(IProject proj) { -// try { -// return DevAssistUtils.getIgnoreFilePath(proj); -// } catch (Exception e) { -// return ""; -// } -// } } \ No newline at end of file diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java index 1fba92d4..16886279 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/secrets/SecretsScannerService.java @@ -1,30 +1,35 @@ package com.checkmarx.eclipse.devassist.scanners.secrets; -import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; -import com.checkmarx.ast.wrapper.CxException; -import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; -import com.checkmarx.eclipse.devassist.common.ScanResult; -import com.checkmarx.eclipse.devassist.common.ScannerConfig; -import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; -import com.checkmarx.eclipse.devassist.model.ScanIssue; -import com.checkmarx.eclipse.devassist.model.ScanEngine; -import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; -import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; -import com.checkmarx.eclipse.common.utils.CxLogger; -import org.eclipse.core.resources.IProject; -import org.eclipse.jface.text.Document; -import org.eclipse.jface.text.IDocument; - import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.*; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalTime; -import java.util.*; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +import com.checkmarx.ast.secretsrealtime.SecretsRealtimeResults; +import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; +import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScanResult; +import com.checkmarx.eclipse.devassist.common.ScannerConfig; +import com.checkmarx.eclipse.devassist.model.ScanEngine; +import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; + /** * Realtime Secrets scanner service for Eclipse. * @@ -37,6 +42,7 @@ public class SecretsScannerService extends BaseScannerService MANIFEST_FILE_PATTERNS = List.of( @@ -127,7 +133,7 @@ public ScanResult scan(String filePath, IDocument docume CxLogger.info(LOG_TAG + " Starting scan: " + filePath); // String ignoreFilePath = getIgnoreFilePath(proj); - SecretsRealtimeResults scanResults = CxWrapperFactory.build() + SecretsRealtimeResults scanResults = wrapperProvider .secretsRealtimeScan(tempFilePath.get(), ""); if (scanResults == null) { From 6ff513b3c468beb51c8b0cfdb4dc1feec223e937 Mon Sep 17 00:00:00 2001 From: Anand Nandeshwar <73646287+cx-anand-nandeshwar@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:30:13 +0530 Subject: [PATCH 05/11] Upgrade ast-cli-java-wrapper from 2.4.24 to 2.4.27 Updated dependency version to match the latest stable release. Co-Authored-By: Claude Sonnet 5 --- common-lib/lib/ast-cli-java-wrapper-2.4.24.jar | 3 --- common-lib/lib/ast-cli-java-wrapper-2.4.27.jar | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 common-lib/lib/ast-cli-java-wrapper-2.4.24.jar create mode 100644 common-lib/lib/ast-cli-java-wrapper-2.4.27.jar diff --git a/common-lib/lib/ast-cli-java-wrapper-2.4.24.jar b/common-lib/lib/ast-cli-java-wrapper-2.4.24.jar deleted file mode 100644 index b4e1e934..00000000 --- a/common-lib/lib/ast-cli-java-wrapper-2.4.24.jar +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e664771fd767accd5bd47057c5a6d4cc86d292c93191200d061ded6e3e527bdf -size 135732567 diff --git a/common-lib/lib/ast-cli-java-wrapper-2.4.27.jar b/common-lib/lib/ast-cli-java-wrapper-2.4.27.jar new file mode 100644 index 00000000..3df6ef7a --- /dev/null +++ b/common-lib/lib/ast-cli-java-wrapper-2.4.27.jar @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5313792421835ddfd332713da41a82eb8234705c981bdbdd1431d797538185c0 +size 140401594 From 0da81b31b724b2e031299b0de2784076a21a3711 Mon Sep 17 00:00:00 2001 From: Anand Nandeshwar <73646287+cx-anand-nandeshwar@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:10:28 +0530 Subject: [PATCH 06/11] Additional Package Manager - Added plugin version with expected format - Resolved review comments for https://github.com/Checkmarx/ast-eclipse-plugin/pull/265 --- .classpath | 6 +++++- ast-cli-java-wrapper.version | 2 +- common-lib/.classpath | 2 +- common-lib/META-INF/MANIFEST.MF | 2 +- common-lib/build.properties | 2 +- devassist-lib/.classpath | 2 +- .../scanners/asca/AscaScannerService.java | 2 +- .../containers/ContainerScannerService.java | 4 ++-- .../scanners/oss/OssScannerCommand.java | 3 ++- .../scanners/oss/OssScannerService.java | 16 +--------------- 10 files changed, 16 insertions(+), 25 deletions(-) diff --git a/.classpath b/.classpath index ac37fb2e..b660d46e 100644 --- a/.classpath +++ b/.classpath @@ -1,5 +1,9 @@ - + + + + + diff --git a/ast-cli-java-wrapper.version b/ast-cli-java-wrapper.version index 0cb980f1..05cfbc0c 100644 --- a/ast-cli-java-wrapper.version +++ b/ast-cli-java-wrapper.version @@ -1 +1 @@ -2.4.24 +2.4.27 diff --git a/common-lib/.classpath b/common-lib/.classpath index 31c2a6e0..83424dc7 100644 --- a/common-lib/.classpath +++ b/common-lib/.classpath @@ -7,7 +7,7 @@ - + diff --git a/common-lib/META-INF/MANIFEST.MF b/common-lib/META-INF/MANIFEST.MF index 0874fa8c..0bb9fd72 100644 --- a/common-lib/META-INF/MANIFEST.MF +++ b/common-lib/META-INF/MANIFEST.MF @@ -5,7 +5,7 @@ Bundle-SymbolicName: com.checkmarx.eclipse.common Bundle-Version: 1.0.0.qualifier Bundle-Vendor: Checkmarx Bundle-ClassPath: ., - lib/ast-cli-java-wrapper-2.4.24.jar, + lib/ast-cli-java-wrapper-2.4.27.jar, lib/jackson-core-2.21.4.jar, lib/jackson-databind-2.21.5.jar, lib/jackson-annotations-2.21.jar, diff --git a/common-lib/build.properties b/common-lib/build.properties index 78e4ef65..a1599af2 100644 --- a/common-lib/build.properties +++ b/common-lib/build.properties @@ -1,6 +1,6 @@ output.. = bin/ bin.includes = META-INF/,\ - lib/ast-cli-java-wrapper-2.4.24.jar,\ + lib/ast-cli-java-wrapper-2.4.27.jar,\ lib/jackson-core-2.21.4.jar,\ lib/jackson-databind-2.21.5.jar,\ lib/jackson-annotations-2.21.jar,\ diff --git a/devassist-lib/.classpath b/devassist-lib/.classpath index 43dc9a78..cca0ae6a 100644 --- a/devassist-lib/.classpath +++ b/devassist-lib/.classpath @@ -3,7 +3,7 @@ - + diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java index d0ea5c55..b2c74196 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/asca/AscaScannerService.java @@ -11,11 +11,11 @@ import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; -import com.checkmarx.ast.asca.ScanResult; import com.checkmarx.ast.wrapper.CxException; import com.checkmarx.eclipse.common.utils.CxLogger; import com.checkmarx.eclipse.common.wrapper.WrapperProvider; import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; +import com.checkmarx.eclipse.devassist.common.ScanResult; import com.checkmarx.eclipse.devassist.common.ScannerConfig; import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; import com.checkmarx.eclipse.devassist.utils.ScanEngine; diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java index 725a0316..bd3cb02e 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/containers/ContainerScannerService.java @@ -4,7 +4,7 @@ import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; import com.checkmarx.eclipse.devassist.common.ScannerConfig; -import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; +import com.checkmarx.eclipse.common.wrapper.WrapperProvider; import com.checkmarx.eclipse.devassist.common.ScanResult; import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.devassist.model.ScanEngine; @@ -29,7 +29,7 @@ * Handles file detection (Docker, Docker Compose, Helm), secure temporary * folder management, * and direct invocation of Checkmarx Container Realtime scanning via - * CxWrapperFactory. + * WrapperProvider. */ public class ContainerScannerService extends BaseScannerService { diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java index 716b5794..5c612ed9 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerCommand.java @@ -26,6 +26,7 @@ import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.devassist.problems.ProblemHolderService; import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.PackageManager; import com.checkmarx.eclipse.common.utils.CxLogger; /** @@ -79,7 +80,7 @@ private void scanAllManifestFilesInFolder(IProgressMonitor monitor) { List matchedFiles = new ArrayList<>(); - List pathMatchers = DevAssistConstants.MANIFEST_FILE_PATTERNS.stream() + List pathMatchers = PackageManager.getAllPatterns().stream() .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) .collect(Collectors.toList()); diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java index acd4f130..f716940e 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/oss/OssScannerService.java @@ -1,18 +1,5 @@ package com.checkmarx.eclipse.devassist.scanners.oss; -import com.checkmarx.ast.ossrealtime.OssRealtimeResults; -import com.checkmarx.eclipse.devassist.basescanner.BaseScannerService; -import com.checkmarx.eclipse.devassist.common.ScanResult; -import com.checkmarx.eclipse.devassist.common.ScannerConfig; -import com.checkmarx.eclipse.devassist.factory.CxWrapperFactory; -import com.checkmarx.eclipse.devassist.model.ScanEngine; -import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; -import com.checkmarx.eclipse.devassist.utils.PackageManager; -import com.checkmarx.eclipse.common.utils.CxLogger; -import org.eclipse.core.resources.IProject; -import org.eclipse.jface.text.Document; -import org.eclipse.jface.text.IDocument; - import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -30,7 +17,6 @@ import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; -import com.checkmarx.eclipse.common.wrapper.WrapperProvider; import org.eclipse.core.resources.IProject; import org.eclipse.jface.text.Document; @@ -217,7 +203,7 @@ private void saveCompanionFile(Path tempFolderPath, String originalFilePath) { for (String companionFileName : companionFileNameList) { Path companionOriginalPath = parentPath.resolve(companionFileName); if (!Files.exists(companionOriginalPath)) { - return; + continue; } Path companionTempPath = tempFolderPath.resolve(companionFileName); From 1fbdd4e40785b69e6f57cf65ce12daa61d6feae8 Mon Sep 17 00:00:00 2001 From: Anand Nandeshwar <73646287+cx-anand-nandeshwar@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:41:41 +0530 Subject: [PATCH 07/11] Enhance Checkmarx One preferences page UI and add logout confirmation Add a help page link, reposition the CLI help link and Connect/Logout buttons for correct layout ordering and spacing, and require a Yes/Cancel confirmation before logging out with a success message shown afterward. --- .../common/preferences/PreferencesPage.java | 130 ++++++++++++++---- .../eclipse/common/utils/PluginConstants.java | 11 +- 2 files changed, 111 insertions(+), 30 deletions(-) diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java index a56832a5..ef986f72 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java @@ -5,7 +5,7 @@ import java.util.concurrent.CompletableFuture; import org.apache.commons.lang3.StringUtils; -import org.eclipse.jface.preference.FieldEditor; +import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.FieldEditorPreferencePage; import org.eclipse.jface.preference.PreferenceDialog; import org.eclipse.jface.preference.StringFieldEditor; @@ -107,14 +107,61 @@ protected void createFieldEditors() { parentLayout.marginWidth = 0; topComposite.setLayout(parentLayout); - StringFieldEditor apiKey = new StringFieldEditor(Preferences.API_KEY, PluginConstants.PREFERENCES_API_KEY, topComposite); + // Every widget on this page is parented directly to topComposite, in the exact order + // it should visually appear. They used to be split between topComposite and + // getFieldEditorParent(), which made the on-screen order depend on which of the two + // composites was created first rather than on the order of the code below - keeping a + // single parent removes that ambiguity. + + // helpLink lives in its own composite, isolated from the fields below, so its own + // sizing/margins can never influence the spacing between the API key / additional + // params labels and their input boxes. + Composite helpComposite = new Composite(topComposite, SWT.NONE); + GridLayout helpLayout = new GridLayout(); + helpLayout.numColumns = 1; + helpLayout.marginHeight = 0; + helpLayout.marginWidth = 0; + helpComposite.setLayout(helpLayout); + helpComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + Link helpLink = new Link(helpComposite, SWT.NONE); + helpLink.setText("" + PluginConstants.PREFERENCES_HELP_LINK_TEXT + ""); + helpLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); + helpLink.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + IWorkbenchBrowserSupport browserSupport = PlatformUI.getWorkbench().getBrowserSupport(); + try { + browserSupport.getExternalBrowser().openURL(new URL(e.text)); + } catch (PartInitException | MalformedURLException e1) { + CxLogger.error("Failed to open Checkmarx One Eclipse Plugin Help Page link.", e1); + e1.printStackTrace(); + } + } + }); + + spacer(topComposite); + + // apiKey and additionalParams get their own composite with a standard, fixed + // label-to-input gap - kept separate from topComposite (and from helpComposite above) + // so nothing else on the page can stretch or shrink that gap. + Composite fieldsComposite = new Composite(topComposite, SWT.NONE); + GridLayout fieldsLayout = new GridLayout(); + fieldsLayout.numColumns = 1; + fieldsLayout.marginHeight = 0; + fieldsLayout.marginWidth = 0; + fieldsLayout.verticalSpacing = 4; + fieldsComposite.setLayout(fieldsLayout); + fieldsComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + StringFieldEditor apiKey = new StringFieldEditor(Preferences.API_KEY, PluginConstants.PREFERENCES_API_KEY, fieldsComposite); apiKeyField = apiKey; addField(apiKey); - Text textControl = apiKey.getTextControl(topComposite); + Text textControl = apiKey.getTextControl(fieldsComposite); textControl.setEchoChar('*'); StringFieldEditor additionalParams = new StringFieldEditor(Preferences.ADDITIONAL_OPTIONS, - PluginConstants.PREFERENCES_ADDITIONAL_OPTIONS, StringFieldEditor.UNLIMITED, StringFieldEditor.VALIDATE_ON_KEY_STROKE, topComposite); + PluginConstants.PREFERENCES_ADDITIONAL_OPTIONS, StringFieldEditor.UNLIMITED, StringFieldEditor.VALIDATE_ON_KEY_STROKE, fieldsComposite); additionalParamsField = additionalParams; addField(additionalParams); @@ -130,14 +177,11 @@ protected void createFieldEditors() { gridData.horizontalAlignment = GridData.FILL; textControl.setLayoutData(gridData); - addField(space()); - + spacer(topComposite); - Link cliHelp = new Link(getFieldEditorParent(), SWT.NONE); + Link cliHelp = new Link(topComposite, SWT.NONE); cliHelp.setText("CLI command that supports a set of global flags"); - cliHelp.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); - GridData linkGridData = new GridData(SWT.END, SWT.CENTER, true, false); - cliHelp.setLayoutData(linkGridData); + cliHelp.setLayoutData(new GridData(SWT.END, SWT.CENTER, true, false)); cliHelp.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { @@ -151,20 +195,42 @@ public void widgetSelected(SelectionEvent e) { } }); - addField(space()); - - Label connectionLabel = new Label(getFieldEditorParent(), SWT.WRAP); - connectionLabel.setLayoutData( - new GridData(SWT.FILL, SWT.CENTER, true, false) - ); + spacer(topComposite); // Holds the Logout button reference so the Connect handler (defined before the // Logout button is created below) can disable/enable it during the connect flow. final Button[] logoutButtonHolder = new Button[1]; - Button connectionButton = new Button(topComposite, SWT.PUSH); - connectionButton.setText(PluginConstants.PREFERENCES_TEST_CONNECTION); + Composite buttonsComposite = new Composite(topComposite, SWT.NONE); + GridLayout buttonsLayout = new GridLayout(); + buttonsLayout.numColumns = 2; + buttonsLayout.marginHeight = 0; + buttonsLayout.marginWidth = 0; + buttonsLayout.horizontalSpacing = 10; + buttonsComposite.setLayout(buttonsLayout); + buttonsComposite.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); + + // Give both buttons a fixed minimum width so they aren't sized to hug their text - + // without this, "Logout" ends up noticeably narrower than "Connect to Checkmarx". + final int buttonWidthHint = 140; + + Button connectionButton = new Button(buttonsComposite, SWT.PUSH); + connectionButton.setText(PluginConstants.CONNECT_TO_CHECKMARX); + GridData connectionButtonGridData = new GridData(SWT.BEGINNING, SWT.CENTER, false, false); + connectionButtonGridData.widthHint = buttonWidthHint; + connectionButton.setLayoutData(connectionButtonGridData); connectionButton.setEnabled(!apiKey.getStringValue().trim().isEmpty()); + + // connectionLabel (the "Validating.../Connected" status text) is created after + // buttonsComposite so it renders below the Connect/Logout buttons, per AUTH_SUCCESS_DISPLAY + // placement - it's declared here, before the listeners below that reference it. + spacer(topComposite); + + Label connectionLabel = new Label(topComposite, SWT.WRAP); + connectionLabel.setLayoutData( + new GridData(SWT.FILL, SWT.CENTER, true, false) + ); + textControl.addModifyListener(e -> { connectionButton.setEnabled(!textControl.getText().trim().isEmpty()); @@ -274,18 +340,26 @@ public void widgetSelected(SelectionEvent e) { } }); - addField(space()); - - Button logoutButton = new Button(topComposite, SWT.PUSH); + Button logoutButton = new Button(buttonsComposite, SWT.PUSH); logoutButtonHolder[0] = logoutButton; - logoutButton.setText("Logout"); + logoutButton.setText(PluginConstants.LOGOUT); + GridData logoutButtonGridData = new GridData(SWT.BEGINNING, SWT.CENTER, false, false); + logoutButtonGridData.widthHint = 80; + logoutButton.setLayoutData(logoutButtonGridData); logoutButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { + MessageDialog confirmDialog = new MessageDialog(getShell(), PluginConstants.LOGOUT_CONFIRM_TITLE, null, + PluginConstants.LOGOUT_CONFIRM_MESSAGE, MessageDialog.QUESTION, + new String[] { "Yes", "Cancel" }, 0); + if (confirmDialog.open() != 0) { + return; + } + Preferences.clearApiKey(); apiKey.setStringValue(""); // textControl.setText(""); -// connectionLabel.setText(""); + connectionLabel.setText(PluginConstants.LOGOUT_SUCCESS_MESSAGE); refreshRealtimeScannersLink(); getFieldEditorParent().layout(); @@ -300,10 +374,10 @@ public void widgetSelected(SelectionEvent e) { } }); - addField(space()); + spacer(topComposite); - realtimeScannersLink = new Link(getFieldEditorParent(), SWT.NONE); - realtimeScannersLink.setText("Go to Realtime Scanners"); + realtimeScannersLink = new Link(topComposite, SWT.NONE); + realtimeScannersLink.setText(""+PluginConstants.GO_TO_CHECKMARX_ONE_ASSIST+""); realtimeScannersLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); // Call refresh after setting the LayoutData @@ -333,8 +407,8 @@ private static String mapAuthResult(String result) { return result; } - private FieldEditor space() { - return new LabelFieldEditor("", getFieldEditorParent()); + private Label spacer(Composite parent) { + return new Label(parent, SWT.NONE); } @Override diff --git a/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java index a30ea4de..87c68650 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java +++ b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java @@ -54,7 +54,6 @@ public class PluginConstants { public static final String INFO_CHANGE_BRANCH_EVENT_NOT_TRIGGERED = "Change branch event not triggered. Branch already selected"; public static final String INFO_CHANGE_PROJECT_EVENT_NOT_TRIGGERED = "Change project event not triggered. Project already selected"; public static final String AUTH_SUCCESS_PATTERN = "Successfully authenticated"; - public static final String AUTH_SUCCESS_DISPLAY = "You are connected to Checkmarx One"; /******************************** TREE MESSAGES ********************************/ public static final String TREE_INVALID_SCAN_ID_FORMAT = "Invalid scan id format."; @@ -64,8 +63,16 @@ public class PluginConstants { /******************************** PREFERENCES ********************************/ public static final String PREFERENCES_API_KEY = "API key:"; public static final String PREFERENCES_ADDITIONAL_OPTIONS = "Additional Params:"; - public static final String PREFERENCES_TEST_CONNECTION = "Test Connection"; + public static final String CONNECT_TO_CHECKMARX = "Connect to Checkmarx"; + public static final String LOGOUT = "Logout"; + public static final String GO_TO_CHECKMARX_ONE_ASSIST = "Go to Checkmarx One Assist"; + public static final String PREFERENCES_HELP_LINK_TEXT = "Checkmarx One Eclipse Plugin Help Page"; + public static final String PREFERENCES_HELP_LINK_URL = "https://checkmarx.com/resource/documents/en/34965-68728-checkmarx-one-eclipse-plugin.html"; public static final String PREFERENCES_VALIDATING_STATE = "Validating..."; + public static final String LOGOUT_CONFIRM_TITLE = "Confirm Logout"; + public static final String LOGOUT_CONFIRM_MESSAGE = "Are you sure you want to logout?"; + public static final String LOGOUT_SUCCESS_MESSAGE = "You have been successfully logged out."; + public static final String AUTH_SUCCESS_DISPLAY = "You are connected to Checkmarx One"; public static final String TOPIC_APPLY_SETTINGS = SettingsTopics.TOPIC_APPLY_SETTINGS; /******************************** PROBLEMS VIEW ********************************/ From fa226a565d29703be60c6ce3260177b5ddcd230d Mon Sep 17 00:00:00 2001 From: Anand Nandeshwar <73646287+cx-anand-nandeshwar@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:46:04 +0530 Subject: [PATCH 08/11] Improve Checkmarx One preferences page connect/logout UX Persist the connected state and success message across page reopens, lock/unlock the API key field and Connect/Logout buttons based on connection state, add a logout confirmation dialog, and focus the API key field on open. --- checkmarx-ast-eclipse-plugin/plugin.xml | 2 +- .../common/preferences/PreferencesPage.java | 210 ++++++++++++------ .../eclipse/common/utils/PluginConstants.java | 1 + 3 files changed, 147 insertions(+), 66 deletions(-) diff --git a/checkmarx-ast-eclipse-plugin/plugin.xml b/checkmarx-ast-eclipse-plugin/plugin.xml index 0c62d631..c3a8e6e3 100644 --- a/checkmarx-ast-eclipse-plugin/plugin.xml +++ b/checkmarx-ast-eclipse-plugin/plugin.xml @@ -12,7 +12,7 @@ diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java index ef986f72..06909556 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java @@ -35,13 +35,16 @@ import com.checkmarx.eclipse.common.runner.TenantSettingsProvider; import com.checkmarx.eclipse.common.utils.CxLogger; +/** + * PreferencesPage class for Chekmarx One Preference Page (Login settings) + */ public class PreferencesPage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { // Captured once the fields are loaded, so performOk() can tell whether THIS // page's own settings actually changed. Needed because Eclipse's shared // Preferences dialog calls performOk() on every page the user visited during // the session - not just the one they edited - so simply opening/looking at - // "Checkmarx One" while really only changing "Checkmarx Scanner Configuration" + // "Checkmarx One" while really only changing "Checkmarx One Assist" // (Realtime Scanners) would otherwise still unconditionally fire // TOPIC_APPLY_SETTINGS below and refresh the unrelated Checkmarx One scan view. private StringFieldEditor apiKeyField; @@ -50,6 +53,18 @@ public class PreferencesPage extends FieldEditorPreferencePage implements IWorkb private String initialAdditionalOptions; private Link realtimeScannersLink; + // The API key that was actually confirmed against the server (set on successful + // login, + // cleared on logout). Comparing the current field value against this - rather + // than + // clearing Preferences.CREDENTIALS_VALIDATED from a text ModifyListener - + // avoids reacting + // to StringFieldEditor.load() re-populating the field from the store on every + // page open, + // which would otherwise wipe the "connected" flag before the user ever touched + // anything. + private String lastValidatedApiKey; + public PreferencesPage() { super(GRID); // Replaced Activator preference store listener with Preferences.STORE @@ -67,9 +82,9 @@ private void handlePropertyChange(PropertyChangeEvent event) { private void refreshRealtimeScannersLink() { if (realtimeScannersLink != null && !realtimeScannersLink.isDisposed()) { boolean isLoggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); - + realtimeScannersLink.setVisible(isLoggedIn); - + if (realtimeScannersLink.getLayoutData() instanceof GridData) { ((GridData) realtimeScannersLink.getLayoutData()).exclude = !isLoggedIn; } @@ -85,7 +100,7 @@ private void refreshRealtimeScannersLink() { @Override public void init(IWorkbench workbench) { setPreferenceStore(Preferences.STORE); - setMessage("Checkmarx One preferences"); + setMessage(PluginConstants.CHECKMARX_ONE); } @Override @@ -107,14 +122,19 @@ protected void createFieldEditors() { parentLayout.marginWidth = 0; topComposite.setLayout(parentLayout); - // Every widget on this page is parented directly to topComposite, in the exact order + // Every widget on this page is parented directly to topComposite, in the exact + // order // it should visually appear. They used to be split between topComposite and - // getFieldEditorParent(), which made the on-screen order depend on which of the two - // composites was created first rather than on the order of the code below - keeping a + // getFieldEditorParent(), which made the on-screen order depend on which of the + // two + // composites was created first rather than on the order of the code below - + // keeping a // single parent removes that ambiguity. - // helpLink lives in its own composite, isolated from the fields below, so its own - // sizing/margins can never influence the spacing between the API key / additional + // helpLink lives in its own composite, isolated from the fields below, so its + // own + // sizing/margins can never influence the spacing between the API key / + // additional // params labels and their input boxes. Composite helpComposite = new Composite(topComposite, SWT.NONE); GridLayout helpLayout = new GridLayout(); @@ -125,7 +145,8 @@ protected void createFieldEditors() { helpComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); Link helpLink = new Link(helpComposite, SWT.NONE); - helpLink.setText("" + PluginConstants.PREFERENCES_HELP_LINK_TEXT + ""); + helpLink.setText("" + + PluginConstants.PREFERENCES_HELP_LINK_TEXT + ""); helpLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); helpLink.addSelectionListener(new SelectionAdapter() { @Override @@ -143,7 +164,8 @@ public void widgetSelected(SelectionEvent e) { spacer(topComposite); // apiKey and additionalParams get their own composite with a standard, fixed - // label-to-input gap - kept separate from topComposite (and from helpComposite above) + // label-to-input gap - kept separate from topComposite (and from helpComposite + // above) // so nothing else on the page can stretch or shrink that gap. Composite fieldsComposite = new Composite(topComposite, SWT.NONE); GridLayout fieldsLayout = new GridLayout(); @@ -154,14 +176,16 @@ public void widgetSelected(SelectionEvent e) { fieldsComposite.setLayout(fieldsLayout); fieldsComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); - StringFieldEditor apiKey = new StringFieldEditor(Preferences.API_KEY, PluginConstants.PREFERENCES_API_KEY, fieldsComposite); + StringFieldEditor apiKey = new StringFieldEditor(Preferences.API_KEY, PluginConstants.PREFERENCES_API_KEY, + fieldsComposite); apiKeyField = apiKey; addField(apiKey); Text textControl = apiKey.getTextControl(fieldsComposite); textControl.setEchoChar('*'); StringFieldEditor additionalParams = new StringFieldEditor(Preferences.ADDITIONAL_OPTIONS, - PluginConstants.PREFERENCES_ADDITIONAL_OPTIONS, StringFieldEditor.UNLIMITED, StringFieldEditor.VALIDATE_ON_KEY_STROKE, fieldsComposite); + PluginConstants.PREFERENCES_ADDITIONAL_OPTIONS, StringFieldEditor.UNLIMITED, + StringFieldEditor.VALIDATE_ON_KEY_STROKE, fieldsComposite); additionalParamsField = additionalParams; addField(additionalParams); @@ -170,7 +194,21 @@ public void widgetSelected(SelectionEvent e) { initialApiKey = apiKey.getStringValue(); initialAdditionalOptions = additionalParams.getStringValue(); - //set the width for API Key text field + // Restore the "connected" state on reopening the page. Read directly from the + // preference store (not apiKey.getStringValue()) - the field editors haven't + // had + // load() called on them yet at this point in createFieldEditors(), so their + // text + // controls are still empty. + lastValidatedApiKey = (Preferences.isCredentialsValidated() && StringUtils.isNotBlank(Preferences.getApiKey())) + ? Preferences.getApiKey() + : null; + boolean isConnected = lastValidatedApiKey != null; + // Locked while connected, so the validated key can't be edited out from under the + // "connected" state - re-enabled on logout. + textControl.setEnabled(!isConnected); + + // set the width for API Key text field GridData gridData = new GridData(SWT.BEGINNING, SWT.CENTER, true, false); gridData.widthHint = 500; // Some width gridData.grabExcessHorizontalSpace = false; @@ -179,26 +217,28 @@ public void widgetSelected(SelectionEvent e) { spacer(topComposite); - Link cliHelp = new Link(topComposite, SWT.NONE); - cliHelp.setText("CLI command that supports a set of global flags"); - cliHelp.setLayoutData(new GridData(SWT.END, SWT.CENTER, true, false)); + Link cliHelp = new Link(topComposite, SWT.NONE); + cliHelp.setText( + "CLI command that supports a set of global flags"); + cliHelp.setLayoutData(new GridData(SWT.END, SWT.CENTER, true, false)); cliHelp.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - IWorkbenchBrowserSupport browserSupport = PlatformUI.getWorkbench().getBrowserSupport(); - try { - browserSupport.getExternalBrowser().openURL(new URL(e.text)); - } catch (PartInitException | MalformedURLException e1) { - CxLogger.error("Failed to open CLI help documentation link.", e1); - e1.printStackTrace(); - } - } + @Override + public void widgetSelected(SelectionEvent e) { + IWorkbenchBrowserSupport browserSupport = PlatformUI.getWorkbench().getBrowserSupport(); + try { + browserSupport.getExternalBrowser().openURL(new URL(e.text)); + } catch (PartInitException | MalformedURLException e1) { + CxLogger.error("Failed to open CLI help documentation link.", e1); + e1.printStackTrace(); + } + } }); - spacer(topComposite); + spacer(topComposite); // Holds the Logout button reference so the Connect handler (defined before the - // Logout button is created below) can disable/enable it during the connect flow. + // Logout button is created below) can disable/enable it during the connect + // flow. final Button[] logoutButtonHolder = new Button[1]; Composite buttonsComposite = new Composite(topComposite, SWT.NONE); @@ -210,8 +250,10 @@ public void widgetSelected(SelectionEvent e) { buttonsComposite.setLayout(buttonsLayout); buttonsComposite.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); - // Give both buttons a fixed minimum width so they aren't sized to hug their text - - // without this, "Logout" ends up noticeably narrower than "Connect to Checkmarx". + // Give both buttons a fixed minimum width so they aren't sized to hug their + // text - + // without this, "Logout" ends up noticeably narrower than "Connect to + // Checkmarx". final int buttonWidthHint = 140; Button connectionButton = new Button(buttonsComposite, SWT.PUSH); @@ -219,26 +261,35 @@ public void widgetSelected(SelectionEvent e) { GridData connectionButtonGridData = new GridData(SWT.BEGINNING, SWT.CENTER, false, false); connectionButtonGridData.widthHint = buttonWidthHint; connectionButton.setLayoutData(connectionButtonGridData); - connectionButton.setEnabled(!apiKey.getStringValue().trim().isEmpty()); + // Disabled while already connected - re-enabled on logout (see logoutButton + // below). + connectionButton.setEnabled(!isConnected); // connectionLabel (the "Validating.../Connected" status text) is created after - // buttonsComposite so it renders below the Connect/Logout buttons, per AUTH_SUCCESS_DISPLAY + // buttonsComposite so it renders below the Connect/Logout buttons, per + // AUTH_SUCCESS_DISPLAY // placement - it's declared here, before the listeners below that reference it. spacer(topComposite); Label connectionLabel = new Label(topComposite, SWT.WRAP); - connectionLabel.setLayoutData( - new GridData(SWT.FILL, SWT.CENTER, true, false) - ); + connectionLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + if (isConnected) { + connectionLabel.setText(PluginConstants.AUTH_SUCCESS_DISPLAY); + } textControl.addModifyListener(e -> { - connectionButton.setEnabled(!textControl.getText().trim().isEmpty()); - - // Any edit means whatever gets saved next (even via Apply/OK without ever - // clicking Test Connection) hasn't been checked against the server, so it must - // not keep looking "connected" on the strength of a previous, different key's - // validation. - Preferences.setCredentialsValidated(false); + // This also fires when StringFieldEditor.load() programmatically repopulates + // the + // text field from the store on every page open - that's not a user edit. + // Comparing + // against lastValidatedApiKey (a value fixed for this page's lifetime, not + // re-read + // from the store) rather than clearing Preferences.CREDENTIALS_VALIDATED here + // keeps + // that load() from ever wiping the persisted "connected" flag. + boolean stillMatchesValidatedKey = lastValidatedApiKey != null + && textControl.getText().equals(lastValidatedApiKey); + connectionButton.setEnabled(!stillMatchesValidatedKey); }); connectionButton.addSelectionListener(new SelectionAdapter() { @@ -252,7 +303,8 @@ public void widgetSelected(SelectionEvent e) { getFieldEditorParent().layout(); // Disable Logout for the duration of the connect/validate flow so a user can't - // interrupt it mid-flight (e.g. closing the dialog or logging out) in a way that + // interrupt it mid-flight (e.g. closing the dialog or logging out) in a way + // that // leaves the flow half-finished and the welcome dialog never shown. if (logoutButtonHolder[0] != null && !logoutButtonHolder[0].isDisposed()) { logoutButtonHolder[0].setEnabled(false); @@ -260,8 +312,7 @@ public void widgetSelected(SelectionEvent e) { CompletableFuture.supplyAsync(() -> { try { - return Authenticator.INSTANCE.doAuthentication( - apiKey_str, additionalParams_str); + return Authenticator.INSTANCE.doAuthentication(apiKey_str, additionalParams_str); } catch (Throwable t) { CxLogger.error(PluginConstants.ERROR_AUTHENTICATING_AST, new Exception(t)); return t.getMessage(); @@ -271,9 +322,6 @@ public void widgetSelected(SelectionEvent e) { // while this connect/validate call was in flight, these are disposed. // Previously an unguarded call here threw and aborted this whole runnable, // which is why the welcome dialog never appeared after closing the dialog. - if (!connectionButton.isDisposed()) { - connectionButton.setEnabled(true); - } // Show welcome dialog on successful authentication. The "Validating..." // message is left on screen (not switched to "Connected") until the @@ -288,9 +336,16 @@ public void widgetSelected(SelectionEvent e) { Preferences.STORE.setValue(Preferences.API_KEY, apiKey_str); Preferences.STORE.setValue(Preferences.ADDITIONAL_OPTIONS, additionalParams_str); Preferences.setCredentialsValidated(true); + lastValidatedApiKey = apiKey_str; + // connectionButton stays disabled - it's only re-enabled on logout, or + // below if this attempt actually failed. + if (!textControl.isDisposed()) { + textControl.setEnabled(false); + } refreshRealtimeScannersLink(); - // Notify views (CheckmarxView/CxFindingsView) that credentials are now available + // Notify views (CheckmarxView/CxFindingsView) that credentials are now + // available // so they can switch from the credentials panel to the actual work views for (ISettingsChangeNotifier notifier : Preferences.getSettingsChangeNotifiers()) { notifier.notifySettingsApplied(); @@ -299,8 +354,8 @@ public void widgetSelected(SelectionEvent e) { // Fetch MCP enabled status from server asynchronously CompletableFuture.supplyAsync(() -> { try { - return TenantSettingsProvider.INSTANCE.isAiMcpServerEnabled( - apiKey_str, additionalParams_str); + return TenantSettingsProvider.INSTANCE.isAiMcpServerEnabled(apiKey_str, + additionalParams_str); } catch (Exception ex) { CxLogger.error("Failed to fetch MCP status", ex); return false; @@ -315,9 +370,11 @@ public void widgetSelected(SelectionEvent e) { // Delegate to handler registered by devassist-lib (if available) IAuthenticationSuccessHandler handler = Preferences.getAuthenticationSuccessHandler(); if (handler != null) { - handler.onAuthenticationSuccess(mcpEnabled, logoutButtonHolder[0], apiKey_str, additionalParams_str); + handler.onAuthenticationSuccess(mcpEnabled, logoutButtonHolder[0], apiKey_str, + additionalParams_str); } else { - CxLogger.warning("[PREFS] No authentication success handler registered - welcome dialog skipped"); + CxLogger.warning( + "[PREFS] No authentication success handler registered - welcome dialog skipped"); if (logoutButtonHolder[0] != null && !logoutButtonHolder[0].isDisposed()) { logoutButtonHolder[0].setEnabled(true); } @@ -325,7 +382,11 @@ public void widgetSelected(SelectionEvent e) { })); } else { // Authentication failed - the flow ends here with no welcome dialog, - // so show the failure message right away and restore Logout. + // so show the failure message right away, restore Logout, and let the + // user retry the connect. + if (!connectionButton.isDisposed()) { + connectionButton.setEnabled(true); + } if (!connectionLabel.isDisposed()) { connectionLabel.setText(mapAuthResult(result)); } @@ -346,6 +407,8 @@ public void widgetSelected(SelectionEvent e) { GridData logoutButtonGridData = new GridData(SWT.BEGINNING, SWT.CENTER, false, false); logoutButtonGridData.widthHint = 80; logoutButton.setLayoutData(logoutButtonGridData); + // Nothing to log out of until connected - mirrors connectionButton's inverse state. + logoutButton.setEnabled(isConnected); logoutButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { @@ -359,6 +422,10 @@ public void widgetSelected(SelectionEvent e) { Preferences.clearApiKey(); apiKey.setStringValue(""); // textControl.setText(""); + lastValidatedApiKey = null; + connectionButton.setEnabled(true); + textControl.setEnabled(true); + logoutButton.setEnabled(false); connectionLabel.setText(PluginConstants.LOGOUT_SUCCESS_MESSAGE); refreshRealtimeScannersLink(); getFieldEditorParent().layout(); @@ -377,7 +444,7 @@ public void widgetSelected(SelectionEvent e) { spacer(topComposite); realtimeScannersLink = new Link(topComposite, SWT.NONE); - realtimeScannersLink.setText(""+PluginConstants.GO_TO_CHECKMARX_ONE_ASSIST+""); + realtimeScannersLink.setText("" + PluginConstants.GO_TO_CHECKMARX_ONE_ASSIST + ""); realtimeScannersLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); // Call refresh after setting the LayoutData @@ -386,18 +453,22 @@ public void widgetSelected(SelectionEvent e) { realtimeScannersLink.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { - PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn( - getShell(), - "com.checkmarx.eclipse.devassist.prefs.checkmarxpreferencepage", - null, - null - ); + PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn(getShell(), + "com.checkmarx.eclipse.devassist.prefs.checkmarxpreferencepage", null, null); if (dialog != null) { CxPreferencesDialogSizing.applyTo(dialog); dialog.open(); } } }); + + // Deferred via asyncExec - the dialog's shell isn't shown/realized yet at this point + // in createFieldEditors(), so an immediate setFocus() here would be ignored. + Display.getDefault().asyncExec(() -> { + if (!textControl.isDisposed()) { + textControl.setFocus(); + } + }); } private static String mapAuthResult(String result) { @@ -424,11 +495,20 @@ public boolean performOk() { // this performOk() too when the user only meant to save realtime scanner // settings, spuriously refreshing the Checkmarx One scan window. String currentApiKey = apiKeyField != null ? apiKeyField.getStringValue() : null; - String currentAdditionalOptions = additionalParamsField != null ? additionalParamsField.getStringValue() : null; - boolean settingsActuallyChanged = - !java.util.Objects.equals(currentApiKey, initialApiKey) + String currentAdditionalOptions = additionalParamsField != null ? additionalParamsField.getStringValue() + : null; + boolean settingsActuallyChanged = !java.util.Objects.equals(currentApiKey, initialApiKey) || !java.util.Objects.equals(currentAdditionalOptions, initialAdditionalOptions); + // The key being saved here is only "connected" if it's the exact one that was + // actually validated (via Connect) this session - if the user typed something + // different and hit OK/Apply without testing it, the persisted flag must not + // keep + // claiming it's validated. + if (!java.util.Objects.equals(currentApiKey, lastValidatedApiKey)) { + Preferences.setCredentialsValidated(false); + } + if (settingsActuallyChanged) { // Notify main plugin that settings have changed for (ISettingsChangeNotifier notifier : Preferences.getSettingsChangeNotifiers()) { diff --git a/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java index 87c68650..3c706572 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java +++ b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java @@ -4,6 +4,7 @@ public class PluginConstants { public static final String AGENT_NAME = "Eclipse"; + public static final String CHECKMARX_ONE = "Checkmarx One"; public static final String EMPTY_STRING = ""; public static final String SAST = "sast"; public static final String SCA_DEPENDENCY = "sca"; From 913bd09f1295a9a9c2a650d2bdc3338d75fdc4e5 Mon Sep 17 00:00:00 2001 From: Anand Nandeshwar <73646287+cx-anand-nandeshwar@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:40:07 +0530 Subject: [PATCH 09/11] Decouple auth-state checks from API key presence; keep key after logout Introduce Preferences.isAuthenticated() as the single source of truth for login state, and route every existing "API key non-blank" check through it instead, so a future auth method (e.g. OAuth) only needs to set/clear the same flag. Logout now only clears the validated flag and no longer wipes the stored API key, which stays visible/editable in the preferences page. --- .../plugin/tests/unit/utils/PluginUtilsTest.java | 4 ++-- .../com/checkmarx/eclipse/utils/PluginUtils.java | 7 +++---- .../checkmarx/eclipse/views/CheckmarxView.java | 11 ++++++----- .../preferences/CheckmarxPreferencePage.java | 9 ++++----- .../eclipse/common/preferences/Preferences.java | 15 ++++++++++----- .../common/preferences/PreferencesPage.java | 13 ++++++++----- .../listener/ProjectLifecycleListener.java | 3 +-- .../backend/listener/RealTimeScanJob.java | 5 ++--- .../configuration/AuthenticationListener.java | 16 +++++++++------- .../configuration/McpInstallService.java | 10 +++++----- .../devassist/ui/findings/CxFindingsView.java | 3 +-- 11 files changed, 51 insertions(+), 45 deletions(-) diff --git a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/PluginUtilsTest.java b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/PluginUtilsTest.java index 4f6ba365..ac38179d 100644 --- a/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/PluginUtilsTest.java +++ b/checkmarx-ast-eclipse-plugin-tests/src/test/java/checkmarx/ast/eclipse/plugin/tests/unit/utils/PluginUtilsTest.java @@ -167,7 +167,7 @@ void testAreCredentialsDefinedTrue() { try (MockedStatic prefs = Mockito.mockStatic(Preferences.class)) { - prefs.when(Preferences::getApiKey).thenReturn("apikey"); + prefs.when(Preferences::isAuthenticated).thenReturn(true); boolean result = PluginUtils.areCredentialsDefined(); @@ -180,7 +180,7 @@ void testAreCredentialsDefinedFalse() { try (MockedStatic prefs = Mockito.mockStatic(Preferences.class)) { - prefs.when(Preferences::getApiKey).thenReturn(""); + prefs.when(Preferences::isAuthenticated).thenReturn(false); boolean result = PluginUtils.areCredentialsDefined(); diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java index a119f9cd..74589955 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/utils/PluginUtils.java @@ -20,7 +20,6 @@ import org.eclipse.jface.viewers.ComboViewer; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.ui.PlatformUI; -import org.apache.commons.lang3.StringUtils; import com.checkmarx.ast.results.result.Node; import com.checkmarx.ast.results.result.Result; import com.checkmarx.eclipse.enums.ActionName; @@ -155,12 +154,12 @@ public static IEventBroker getEventBroker() { } /** - * Check if checkmarx credentials are defined in the Preferences - * + * Check if the user is currently authenticated to Checkmarx One. + * * @return */ public static boolean areCredentialsDefined() { - return StringUtils.isNotBlank(Preferences.getApiKey()); + return Preferences.isAuthenticated(); } /** diff --git a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java index d29e4b41..3a9b31bc 100644 --- a/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java +++ b/checkmarx-ast-eclipse-plugin/src/com/checkmarx/eclipse/views/CheckmarxView.java @@ -2898,9 +2898,10 @@ public void handleEvent(org.osgi.service.event.Event arg0) { return; } String currentApiKey = Preferences.STORE.getString(Preferences.API_KEY); + boolean isAuthenticated = Preferences.isAuthenticated(); // Handle case: credentials just set (plugin panel not yet drawn) - if (!currentApiKey.isEmpty() && !isPluginDraw) { + if (isAuthenticated && !isPluginDraw) { CxLogger.info("Credentials detected, drawing plugin panel"); drawPluginPanel(); lastApiKey = currentApiKey; @@ -2908,7 +2909,7 @@ public void handleEvent(org.osgi.service.event.Event arg0) { } // Handle case: credentials just removed (plugin panel is drawn) - if (currentApiKey.isEmpty() && isPluginDraw) { + if (!isAuthenticated && isPluginDraw) { CxLogger.info("Credentials removed, showing missing credentials panel"); updateStartScanButton(false); drawMissingCredentialsPanel(); @@ -2922,15 +2923,15 @@ public void handleEvent(org.osgi.service.event.Event arg0) { return; } - // Handle case: no credentials and panel not drawn (initial state) - if (currentApiKey.isEmpty() && !isPluginDraw) { + // Handle case: not authenticated and panel not drawn (initial state) + if (!isAuthenticated && !isPluginDraw) { // Already showing missing credentials panel, nothing to do lastApiKey = currentApiKey; return; } // Handle case: API key changed but still authenticated (plugin already drawn) - if (!currentApiKey.isEmpty() && isPluginDraw) { + if (isAuthenticated && isPluginDraw) { if (lastApiKey != null && lastApiKey.equalsIgnoreCase(currentApiKey)) { // Same credentials, no reload needed return; diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java index 92e00f29..c8a60686 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java @@ -3,7 +3,6 @@ import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPreferencePage; -import org.apache.commons.lang3.StringUtils; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.preference.PreferenceDialog; import org.eclipse.jface.preference.PreferencePage; @@ -80,7 +79,7 @@ public CheckmarxPreferencePage() { private void handlePreferenceChange(PropertyChangeEvent event) { // Re-check login state: if API key was cleared, we need to switch from // logged-in scanner checkboxes to logged-out message - boolean isNowLoggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); + boolean isNowLoggedIn = Preferences.isAuthenticated(); if (loggedIn != isNowLoggedIn) { loggedIn = isNowLoggedIn; } @@ -88,7 +87,7 @@ private void handlePreferenceChange(PropertyChangeEvent event) { @Override protected Control createContents(Composite parent) { - loggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); + loggedIn = Preferences.isAuthenticated(); if (!loggedIn) { return createLoggedOutContent(parent); } @@ -217,7 +216,7 @@ private void loadValues() { protected void performDefaults() { // Check credentials fresh, not from captured field. // If user logged out while viewing another page, loggedIn would be stale. - boolean isCurrentlyLoggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); + boolean isCurrentlyLoggedIn = Preferences.isAuthenticated(); if (!isCurrentlyLoggedIn) { super.performDefaults(); return; @@ -276,7 +275,7 @@ public boolean performOk() { // dialog session, // loggedIn would be stale and we'd save/notify with false authentication // status. - boolean isCurrentlyLoggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); + boolean isCurrentlyLoggedIn = Preferences.isAuthenticated(); if (!isCurrentlyLoggedIn) { return super.performOk(); } diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java index b6c6b3e5..03ccb5a3 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java @@ -74,11 +74,6 @@ public static void store(String key, String value) { STORE.setValue(key, value); } - public static void clearApiKey() { - STORE.setValue(API_KEY, ""); - STORE.setValue(CREDENTIALS_VALIDATED, false); - } - public static boolean isCredentialsValidated() { return STORE.getBoolean(CREDENTIALS_VALIDATED); } @@ -87,6 +82,16 @@ public static void setCredentialsValidated(boolean validated) { STORE.setValue(CREDENTIALS_VALIDATED, validated); } + /** + * Single source of truth for "is the user logged in", independent of which credential + * type produced that state. Callers across the plugin should check this - not API key + * presence - so that a future auth method (e.g. OAuth) only needs to set/clear this same + * flag to plug into every existing authenticated-only code path. + */ + public static boolean isAuthenticated() { + return isCredentialsValidated(); + } + public static void setAuthenticationSuccessHandler(IAuthenticationSuccessHandler handler) { authSuccessHandler = handler; } diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java index 06909556..76fc0be6 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java @@ -81,7 +81,7 @@ private void handlePropertyChange(PropertyChangeEvent event) { */ private void refreshRealtimeScannersLink() { if (realtimeScannersLink != null && !realtimeScannersLink.isDisposed()) { - boolean isLoggedIn = StringUtils.isNotBlank(Preferences.getApiKey()); + boolean isLoggedIn = Preferences.isAuthenticated(); realtimeScannersLink.setVisible(isLoggedIn); @@ -200,7 +200,7 @@ public void widgetSelected(SelectionEvent e) { // load() called on them yet at this point in createFieldEditors(), so their // text // controls are still empty. - lastValidatedApiKey = (Preferences.isCredentialsValidated() && StringUtils.isNotBlank(Preferences.getApiKey())) + lastValidatedApiKey = (Preferences.isAuthenticated() && StringUtils.isNotBlank(Preferences.getApiKey())) ? Preferences.getApiKey() : null; boolean isConnected = lastValidatedApiKey != null; @@ -419,9 +419,12 @@ public void widgetSelected(SelectionEvent e) { return; } - Preferences.clearApiKey(); - apiKey.setStringValue(""); -// textControl.setText(""); + // Only mark the credentials as no longer validated - the API key itself stays + // stored and visible in the field. Every "am I logged in" check in the plugin + // now goes through Preferences.isAuthenticated() (not "API key non-blank"), so + // leaving the key in place here no longer makes any of them think the user is + // still logged in. + Preferences.setCredentialsValidated(false); lastValidatedApiKey = null; connectionButton.setEnabled(true); textControl.setEnabled(true); diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java index 03f98cd6..90ccceb8 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/ProjectLifecycleListener.java @@ -186,8 +186,7 @@ private void onProjectOpen(IProject project) { } private boolean isUserAuthenticated() { - String apiKey = com.checkmarx.eclipse.common.preferences.Preferences.getApiKey(); - return apiKey != null && !apiKey.trim().isEmpty(); + return com.checkmarx.eclipse.common.preferences.Preferences.isAuthenticated(); } private void onProjectClose(IProject project) { diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java index dd664493..5fef538a 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/backend/listener/RealTimeScanJob.java @@ -190,11 +190,10 @@ protected IStatus run(IProgressMonitor monitor) { } /** - * Check if user is authenticated by checking if API key is configured. + * Check if user is authenticated to Checkmarx One. */ private boolean isUserAuthenticated() { - String apiKey = com.checkmarx.eclipse.common.preferences.Preferences.getApiKey(); - return apiKey != null && !apiKey.trim().isEmpty(); + return com.checkmarx.eclipse.common.preferences.Preferences.isAuthenticated(); } @Override diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/AuthenticationListener.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/AuthenticationListener.java index 3b94af9f..cf4bf153 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/AuthenticationListener.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/AuthenticationListener.java @@ -10,7 +10,9 @@ * Listens for authentication events and triggers MCP auto-installation. * * Registered globally to respond to successful authentication by: - * - Detecting API_KEY changes in preferences + * - Detecting the CREDENTIALS_VALIDATED flag turning true - this fires regardless of which + * credential type (API key today, OAuth in future) produced the successful login, unlike + * listening for API_KEY changes directly. * - Triggering MCP configuration installation * - Logging success/failure for debugging */ @@ -24,13 +26,13 @@ public void propertyChange(PropertyChangeEvent event) { return; } - // Trigger MCP auto-install when API key is successfully set - if (Preferences.API_KEY.equals(event.getProperty())) { - String newApiKey = (String) event.getNewValue(); + // Trigger MCP auto-install when authentication just succeeded + if (Preferences.CREDENTIALS_VALIDATED.equals(event.getProperty())) { + Object newValue = event.getNewValue(); + boolean nowValidated = newValue instanceof Boolean && (Boolean) newValue; - // Only proceed if a key was set (not cleared) - if (newApiKey != null && !newApiKey.isBlank()) { - CxLogger.info(LOG_TAG + " API key updated, attempting MCP auto-install..."); + if (nowValidated) { + CxLogger.info(LOG_TAG + " Authentication succeeded, attempting MCP auto-install..."); McpInstallService.attemptAutoInstall(); } } diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java index f700688c..235decfc 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java @@ -60,14 +60,14 @@ public static void attemptAutoInstall() { CxLogger.info(LOG_TAG + " Attempting auto-install of MCP configuration..."); try { - String apiKey = Preferences.getApiKey(); - String additionalParams = Preferences.getAdditionalOptions(); - - if (apiKey == null || apiKey.isBlank()) { - CxLogger.info(LOG_TAG + " Skipping MCP auto-install: user not authenticated (no API key)"); + if (!Preferences.isAuthenticated()) { + CxLogger.info(LOG_TAG + " Skipping MCP auto-install: user not authenticated"); return; } + String apiKey = Preferences.getApiKey(); + String additionalParams = Preferences.getAdditionalOptions(); + attemptAutoInstall(apiKey, additionalParams); } catch (Exception e) { CxLogger.error(LOG_TAG + " Unexpected error during auto-install attempt: " + e.getMessage(), e); diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java index 80834dfc..60eb0762 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/ui/findings/CxFindingsView.java @@ -34,7 +34,6 @@ import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.ui.plugin.AbstractUIPlugin; -import org.apache.commons.lang3.StringUtils; import com.checkmarx.eclipse.devassist.ui.findings.provider.FindingsContentProvider; import com.checkmarx.eclipse.devassist.ui.findings.provider.FindingsLabelProvider; @@ -143,7 +142,7 @@ private void refreshViewMode() { return; } - if (StringUtils.isBlank(Preferences.getApiKey())) { + if (!Preferences.isAuthenticated()) { drawMissingCredentialsPanel(parentComposite); } else { loadCachedIssues(); From 4ae2fb5365771969282ae212f6835f54ae57d4aa Mon Sep 17 00:00:00 2001 From: Anand Nandeshwar <73646287+cx-anand-nandeshwar@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:48:02 +0530 Subject: [PATCH 10/11] Add Checkmarx MCP configuration UI with install/edit links and status display --- .../common/listener/IMcpInstallCallback.java | 31 ++++ .../common/listener/IMcpInstallHandler.java | 19 ++ .../preferences/CheckmarxPreferencePage.java | 164 +++++++++++++++--- .../common/preferences/Preferences.java | 12 ++ .../common/preferences/PreferencesPage.java | 30 +++- .../eclipse/common/utils/PluginConstants.java | 36 ++++ .../configuration/McpInstallService.java | 62 +++++++ .../scanners/iac/IacScannerService.java | 4 +- .../devassist/utils/DevAssistUtils.java | 42 +++-- 9 files changed, 358 insertions(+), 42 deletions(-) create mode 100644 common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallCallback.java create mode 100644 common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallHandler.java diff --git a/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallCallback.java b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallCallback.java new file mode 100644 index 00000000..26915cab --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallCallback.java @@ -0,0 +1,31 @@ +package com.checkmarx.eclipse.common.listener; + +/** + * Receives the outcome of an MCP install triggered from the UI (e.g. the "Install + * MCP" link on CheckmarxPreferencePage). Unlike the silent, best-effort auto-install + * run at startup, a user-initiated install needs to report back whether it actually + * succeeded so the UI can show a result message. + * + *

May be invoked from a background thread - implementations that touch SWT + * widgets must marshal onto the display thread themselves. + */ +public interface IMcpInstallCallback { + + /** + * Called when the MCP configuration was installed/updated successfully. + */ + void onSuccess(); + + /** + * Called when the install ran successfully but there was nothing to change - the + * server entry already matches the current API key/URL exactly. + */ + void onAlreadyUpToDate(); + + /** + * Called when the install could not be completed. + * + * @param errorMessage a user-presentable reason for the failure + */ + void onFailure(String errorMessage); +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallHandler.java b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallHandler.java new file mode 100644 index 00000000..13f34554 --- /dev/null +++ b/common-lib/src/com/checkmarx/eclipse/common/listener/IMcpInstallHandler.java @@ -0,0 +1,19 @@ +package com.checkmarx.eclipse.common.listener; + +/** + * Service for installing the Checkmarx MCP server configuration. + * + * Allows preference pages in common-lib (e.g. CheckmarxPreferencePage) to trigger + * MCP installation without depending on devassist-lib, which owns the actual + * McpInstallService implementation. + */ +public interface IMcpInstallHandler { + + /** + * Installs/updates the Checkmarx MCP server configuration for the currently + * authenticated user, reporting the outcome to the given callback. + * + * @param callback notified of success or failure, possibly from a background thread + */ + void installMcp(IMcpInstallCallback callback); +} diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java index c8a60686..25ef944b 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/CheckmarxPreferencePage.java @@ -18,6 +18,9 @@ import org.eclipse.ui.dialogs.PreferencesUtil; import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; +import com.checkmarx.eclipse.common.listener.IMcpInstallCallback; +import com.checkmarx.eclipse.common.listener.IMcpInstallHandler; import com.checkmarx.eclipse.common.listener.ISettingsChangeNotifier; /** @@ -42,21 +45,9 @@ public class CheckmarxPreferencePage extends PreferencePage implements IWorkbenc private Button containersCheckbox; private Button iacCheckbox; private Combo containersToolCombo; + private Label mcpStatusLabel; private boolean loggedIn; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_OSS_TITLE = "Checkmarx Developer Assist Open Source Realtime Scanner (OSS-Realtime): Activate OSS-Realtime"; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_SECRETS_TITLE = "Checkmarx Developer Assist Secret Detection Realtime Scanner: Activate Secret Detection Realtime"; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_CONTAINERS_TITLE = "Checkmarx Developer Assist Containers Realtime Scanner: Activate Containers Realtime"; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_TITLE = "Checkmarx Developer Assist IAC Realtime Scanner: Activate IAC Realtime"; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_ASCA_TITLE = "Checkmarx Developer Assist AI Secure Coding Assistant (ASCA): Activate ASCA"; - public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_PREFIX = "Checkmarx Developer Assist IAC Realtime Scanner: Containers Management Tool"; - public static final String DEVASSIST_PLUGIN_WELCOME_TITLE = "Welcome to Checkmarx Developer Assist"; - public static final String CONTAINERS_TOOL_DESCRIPTION = "Select the Containers Management Tool to use for IaC scanning."; - public static final String OSS_REALTIME_CHECKBOX = "Scans your manifest files as you code"; - public static final String SECRETS_REALTIME_CHECKBOX = "Scans your files for potential secrets and credentials as you code"; - public static final String CONTAINERS_REALTIME_CHECKBOX = "Scans your Docker files and container configurations as you code"; - public static final String IAC_REALTIME_CHECKBOX = "Scans your Infrastructure as Code files as you code"; - public static final String ASCA_CHECKBOX = "Scan your file as you code"; public CheckmarxPreferencePage() { super(); @@ -108,51 +99,168 @@ protected Control createContents(Composite parent) { assistMessageLabel.setVisible(false); // --- ASCA Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_ASCA_TITLE); + createSectionHeader(mainPanel, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_ASCA_TITLE); Composite ascaComp = createIndentComposite(mainPanel); ascaCheckbox = new Button(ascaComp, SWT.CHECK); - ascaCheckbox.setText(ASCA_CHECKBOX); + ascaCheckbox.setText(PluginConstants.ASCA_CHECKBOX); // --- OSS Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_OSS_TITLE); + createSectionHeader(mainPanel, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_OSS_TITLE); Composite ossComp = createIndentComposite(mainPanel); ossCheckbox = new Button(ossComp, SWT.CHECK); - ossCheckbox.setText(OSS_REALTIME_CHECKBOX); + ossCheckbox.setText(PluginConstants.OSS_REALTIME_CHECKBOX); // --- Secrets Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_SECRETS_TITLE); + createSectionHeader(mainPanel, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_SECRETS_TITLE); Composite secretsComp = createIndentComposite(mainPanel); secretsCheckbox = new Button(secretsComp, SWT.CHECK); - secretsCheckbox.setText(SECRETS_REALTIME_CHECKBOX); + secretsCheckbox.setText(PluginConstants.SECRETS_REALTIME_CHECKBOX); // --- Containers Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_CONTAINERS_TITLE); + createSectionHeader(mainPanel, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_CONTAINERS_TITLE); Composite containersComp = createIndentComposite(mainPanel); containersCheckbox = new Button(containersComp, SWT.CHECK); - containersCheckbox.setText(CONTAINERS_REALTIME_CHECKBOX); + containersCheckbox.setText(PluginConstants.CONTAINERS_REALTIME_CHECKBOX); // --- IaC Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_TITLE); + createSectionHeader(mainPanel, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_TITLE); Composite iacComp = createIndentComposite(mainPanel); iacCheckbox = new Button(iacComp, SWT.CHECK); - iacCheckbox.setText(IAC_REALTIME_CHECKBOX); + iacCheckbox.setText(PluginConstants.IAC_REALTIME_CHECKBOX); // --- Container Tool Selection Section --- - createSectionHeader(mainPanel, DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_PREFIX); + createSectionHeader(mainPanel, PluginConstants.DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_PREFIX); Composite containerToolComp = createIndentComposite(mainPanel); Label containerDesc = new Label(containerToolComp, SWT.WRAP); - containerDesc.setText(CONTAINERS_TOOL_DESCRIPTION); + containerDesc.setText(PluginConstants.CONTAINERS_TOOL_DESCRIPTION); GridData descData = new GridData(GridData.FILL_HORIZONTAL); containerDesc.setLayoutData(descData); containersToolCombo = new Combo(containerToolComp, SWT.READ_ONLY); - containersToolCombo.setItems(new String[] { "docker", "podman" }); + containersToolCombo.setItems(PluginConstants.CONTAINERS_TOOLS); containersToolCombo.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); + // --- Checkmarx MCP Section --- + // A horizontal rule marks this as a distinct settings group, separate from the + // Realtime Scanner sections above. + Label mcpSeparator = new Label(mainPanel, SWT.SEPARATOR | SWT.HORIZONTAL); + GridData mcpSeparatorData = new GridData(GridData.FILL_HORIZONTAL); + mcpSeparatorData.verticalIndent = 6; + mcpSeparator.setLayoutData(mcpSeparatorData); + + createSectionHeader(mainPanel, PluginConstants.CHECKMARX_MCP_SECTION_TITLE); + Composite mcpComp = createIndentComposite(mainPanel); + + Label mcpDesc = new Label(mcpComp, SWT.WRAP); + mcpDesc.setText(PluginConstants.MCP_DESCRIPTION); + mcpDesc.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // installMcpLink and mcpStatusLabel share a row so the result message appears + // right next to the link that triggered it, rather than on its own line. + Composite installMcpRow = new Composite(mcpComp, SWT.NONE); + GridLayout installMcpRowLayout = new GridLayout(2, false); + installMcpRowLayout.marginWidth = 0; + installMcpRowLayout.marginHeight = 0; + installMcpRowLayout.horizontalSpacing = 10; + installMcpRow.setLayout(installMcpRowLayout); + installMcpRow.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + Link installMcpLink = new Link(installMcpRow, SWT.NONE); + installMcpLink.setText("" + PluginConstants.INSTALL_MCP_LINK_TEXT + ""); + installMcpLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false)); + + mcpStatusLabel = new Label(installMcpRow, SWT.WRAP); + mcpStatusLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + installMcpLink.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + installMcp(); + } + }); + + Link editMcpSettingsLink = new Link(mcpComp, SWT.NONE); + editMcpSettingsLink.setText("" + PluginConstants.EDIT_MCP_SETTINGS_LINK_TEXT + ""); + editMcpSettingsLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); + editMcpSettingsLink.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + editMcpSettings(); + } + }); + loadValues(); return mainPanel; } + /** + * Installs/updates the Checkmarx MCP server configuration. Delegates to the handler + * registered by devassist-lib (this bundle - common-lib - doesn't depend on it directly), + * and shows the result right next to the "Install MCP" link. + */ + private void installMcp() { + IMcpInstallHandler handler = Preferences.getMcpInstallHandler(); + if (handler == null) { + CxLogger.warning("[PREFS] MCP install requested before the handler was registered"); + showMcpStatus(false, PluginConstants.MCP_INSTALL_UNAVAILABLE_MESSAGE); + return; + } + + showMcpStatus(null, PluginConstants.MCP_INSTALLING_STATE); + + handler.installMcp(new IMcpInstallCallback() { + @Override + public void onSuccess() { + Display.getDefault().asyncExec(() -> showMcpStatus(true, PluginConstants.MCP_INSTALL_SUCCESS_MESSAGE)); + } + + @Override + public void onAlreadyUpToDate() { + Display.getDefault().asyncExec(() -> showMcpStatus(true, PluginConstants.MCP_ALREADY_UP_TO_DATE_MESSAGE)); + } + + @Override + public void onFailure(String errorMessage) { + Display.getDefault().asyncExec(() -> showMcpStatus(false, errorMessage)); + } + }); + } + + /** + * Updates mcpStatusLabel with an install result/progress message. + * + * @param success true = success (green), false = failure (red), null = in-progress + * (default color) + */ + private void showMcpStatus(Boolean success, String message) { + if (mcpStatusLabel == null || mcpStatusLabel.isDisposed()) { + return; + } + + Display display = mcpStatusLabel.getDisplay(); + if (success == null) { + mcpStatusLabel.setForeground(null); + } else if (success) { + mcpStatusLabel.setForeground(display.getSystemColor(SWT.COLOR_DARK_GREEN)); + } else { + mcpStatusLabel.setForeground(display.getSystemColor(SWT.COLOR_RED)); + } + mcpStatusLabel.setText(message); + mcpStatusLabel.getParent().layout(true, true); + } + + /** + * Opens GitHub Copilot for Eclipse's own MCP preference page, where the Checkmarx MCP + * server entry (once installed) can be reviewed/edited alongside any other MCP servers. + */ + private void editMcpSettings() { + PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn(getShell(), + PluginConstants.COPILOT_MCP_PREFERENCE_PAGE_ID, null, null); + if (dialog != null) { + dialog.open(); + } + } + /** * Shown instead of the scanner checkboxes when the user isn't logged in - there * is nothing meaningful to configure until credentials are set in "Checkmarx @@ -166,11 +274,11 @@ private Control createLoggedOutContent(Composite parent) { composite.setLayoutData(new GridData(GridData.FILL_BOTH)); Label message = new Label(composite, SWT.WRAP); - message.setText("Log in to Checkmarx One to configure Realtime Scanners."); + message.setText(PluginConstants.LOGIN_NOTE_CXONE_ASSIST); message.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); Link goToLoginLink = new Link(composite, SWT.NONE); - goToLoginLink.setText("Go to Checkmarx One preferences"); + goToLoginLink.setText(""+PluginConstants.GO_TO_CHECKMARX_ONE+""); goToLoginLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); goToLoginLink.addSelectionListener(new SelectionAdapter() { @Override diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java index 03ccb5a3..44fe2628 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/Preferences.java @@ -11,6 +11,7 @@ import org.eclipse.ui.preferences.ScopedPreferenceStore; import com.checkmarx.eclipse.common.listener.IAuthenticationSuccessHandler; +import com.checkmarx.eclipse.common.listener.IMcpInstallHandler; import com.checkmarx.eclipse.common.listener.ISettingsChangeNotifier; import com.checkmarx.eclipse.common.listener.IWorkspaceScanService; @@ -54,6 +55,9 @@ public class Preferences { // Service for triggering workspace scans (registered by main plugin) private static IWorkspaceScanService workspaceScanService; + // Handler for installing the Checkmarx MCP server configuration (registered by devassist-lib) + private static IMcpInstallHandler mcpInstallHandler; + private Preferences() { } @@ -116,6 +120,14 @@ public static IWorkspaceScanService getWorkspaceScanService() { return workspaceScanService; } + public static void setMcpInstallHandler(IMcpInstallHandler handler) { + mcpInstallHandler = handler; + } + + public static IMcpInstallHandler getMcpInstallHandler() { + return mcpInstallHandler; + } + // ============================================================================ // USER PREFERENCES - Preserve user's scanner choices across feature toggles // Mirrors JetBrains GlobalSettingsState.setUserPreferences() pattern diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java index 76fc0be6..d1bdefc2 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java @@ -275,6 +275,7 @@ public void widgetSelected(SelectionEvent e) { connectionLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); if (isConnected) { connectionLabel.setText(PluginConstants.AUTH_SUCCESS_DISPLAY); + setStatusLabelColor(connectionLabel, true); } textControl.addModifyListener(e -> { @@ -300,6 +301,7 @@ public void widgetSelected(SelectionEvent e) { String additionalParams_str = additionalParams.getStringValue(); connectionButton.setEnabled(false); connectionLabel.setText(PluginConstants.PREFERENCES_VALIDATING_STATE); + setStatusLabelColor(connectionLabel, null); getFieldEditorParent().layout(); // Disable Logout for the duration of the connect/validate flow so a user can't @@ -363,6 +365,7 @@ public void widgetSelected(SelectionEvent e) { }).thenAccept((mcpEnabled) -> Display.getDefault().syncExec(() -> { if (!connectionLabel.isDisposed()) { connectionLabel.setText(mapAuthResult(result)); + setStatusLabelColor(connectionLabel, true); } if (!getFieldEditorParent().isDisposed()) { getFieldEditorParent().layout(); @@ -389,6 +392,7 @@ public void widgetSelected(SelectionEvent e) { } if (!connectionLabel.isDisposed()) { connectionLabel.setText(mapAuthResult(result)); + setStatusLabelColor(connectionLabel, false); } if (!getFieldEditorParent().isDisposed()) { getFieldEditorParent().layout(); @@ -430,6 +434,7 @@ public void widgetSelected(SelectionEvent e) { textControl.setEnabled(true); logoutButton.setEnabled(false); connectionLabel.setText(PluginConstants.LOGOUT_SUCCESS_MESSAGE); + setStatusLabelColor(connectionLabel, true); refreshRealtimeScannersLink(); getFieldEditorParent().layout(); @@ -478,13 +483,36 @@ private static String mapAuthResult(String result) { if (result != null && result.contains(PluginConstants.AUTH_SUCCESS_PATTERN)) { return PluginConstants.AUTH_SUCCESS_DISPLAY; } - return result; + // Log the actual failure reason (invalid key, network error, tenant misconfiguration, + // etc.) for diagnosis, but always show the user the same fixed message - the raw + // reason isn't reliably meaningful/actionable to them and may leak backend details. + CxLogger.error(String.format(PluginConstants.ERROR_AUTHENTICATING_AST, result), new Exception(result)); + return PluginConstants.AUTH_FAILURE_DISPLAY; } private Label spacer(Composite parent) { return new Label(parent, SWT.NONE); } + /** + * Colors the login/logout status label: green for a success message (connected, + * logged out), red for a failure message, or the default color while a message is + * neutral (e.g. "Validating..."). + */ + private void setStatusLabelColor(Label label, Boolean success) { + if (label == null || label.isDisposed()) { + return; + } + Display display = label.getDisplay(); + if (success == null) { + label.setForeground(null); + } else if (success) { + label.setForeground(display.getSystemColor(SWT.COLOR_DARK_GREEN)); + } else { + label.setForeground(display.getSystemColor(SWT.COLOR_RED)); + } + } + @Override public boolean performOk() { boolean ok = super.performOk(); diff --git a/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java index 3c706572..a5a1082c 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java +++ b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java @@ -74,6 +74,9 @@ public class PluginConstants { public static final String LOGOUT_CONFIRM_MESSAGE = "Are you sure you want to logout?"; public static final String LOGOUT_SUCCESS_MESSAGE = "You have been successfully logged out."; public static final String AUTH_SUCCESS_DISPLAY = "You are connected to Checkmarx One"; + // Shown to the user for any authentication failure, regardless of cause - the actual + // reason is logged (see PreferencesPage.mapAuthResult()), not surfaced in the UI. + public static final String AUTH_FAILURE_DISPLAY = "Failed to connect to Checkmarx One. Please check your credentials and try again."; public static final String TOPIC_APPLY_SETTINGS = SettingsTopics.TOPIC_APPLY_SETTINGS; /******************************** PROBLEMS VIEW ********************************/ @@ -146,4 +149,37 @@ public class PluginConstants { public static final String CX_PROJECT_MISMATCH = "Project mismatch"; public static final String CX_PROJECT_MISMATCH_QUESTION = "The files open in your workspace don't match the files previously scanned in this Checkmarx project. Do you want to scan anyway?"; public static final String CX_REFRESHING_TOOLBAR = "Checkmarx: Refreshing toolbar..."; + + /**********************************Checkmarx One Assist************************************/ + public static final String GO_TO_CHECKMARX_ONE = "Go to Checkmarx One"; + public static final String LOGIN_NOTE_CXONE_ASSIST = "To configure Checkmarx One Assist settings, log in to Checkmarx One."; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_OSS_TITLE = "Checkmarx Developer Assist Open Source Realtime Scanner (OSS-Realtime): Activate OSS-Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_SECRETS_TITLE = "Checkmarx Developer Assist Secret Detection Realtime Scanner: Activate Secret Detection Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_CONTAINERS_TITLE = "Checkmarx Developer Assist Containers Realtime Scanner: Activate Containers Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_TITLE = "Checkmarx Developer Assist IAC Realtime Scanner: Activate IAC Realtime"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_ASCA_TITLE = "Checkmarx Developer Assist AI Secure Coding Assistant (ASCA): Activate ASCA"; + public static final String DEVASSIST_PLUGIN_REALTIME_SCANNERS_IAC_PREFIX = "Checkmarx Developer Assist IAC Realtime Scanner: Containers Management Tool"; + public static final String DEVASSIST_PLUGIN_WELCOME_TITLE = "Welcome to Checkmarx Developer Assist"; + public static final String CONTAINERS_TOOL_DESCRIPTION = "Select the Containers Management Tool to use for IaC scanning."; + public static final String OSS_REALTIME_CHECKBOX = "Scans your manifest files as you code"; + public static final String SECRETS_REALTIME_CHECKBOX = "Scans your files for potential secrets and credentials as you code"; + public static final String CONTAINERS_REALTIME_CHECKBOX = "Scans your Docker files and container configurations as you code"; + public static final String IAC_REALTIME_CHECKBOX = "Scans your Infrastructure as Code files as you code"; + public static final String ASCA_CHECKBOX = "Scan your file as you code"; + public static final String[] CONTAINERS_TOOLS = new String[] { "docker", "podman" }; + + /**********************************Checkmarx MCP************************************/ + public static final String CHECKMARX_MCP_SECTION_TITLE = "Checkmarx : MCP"; + public static final String MCP_DESCRIPTION = "The Model Context Protocol (MCP) provides advanced contextual analysis for secure coding."; + public static final String INSTALL_MCP_LINK_TEXT = "Install MCP"; + public static final String EDIT_MCP_SETTINGS_LINK_TEXT = "Edit MCP Settings"; + public static final String MCP_INSTALL_UNAVAILABLE_MESSAGE = "MCP install is not available right now. Please try again after the plugin has fully started."; + public static final String MCP_INSTALLING_STATE = "Installing..."; + public static final String MCP_INSTALL_SUCCESS_MESSAGE = "Checkmarx MCP installed successfully"; + public static final String MCP_ALREADY_UP_TO_DATE_MESSAGE = "MCP configuration is already up to date."; + public static final String MCP_INSTALL_GENERIC_FAILURE_MESSAGE = "Failed to install Checkmarx MCP. Please try again."; + public static final String MCP_NOT_AUTHENTICATED_MESSAGE = "You must be connected to Checkmarx One before installing MCP."; + public static final String MCP_NOT_ENABLED_FOR_TENANT_MESSAGE = "MCP is not enabled for your Checkmarx One tenant."; + // GitHub Copilot for Eclipse's own MCP preference page - opened by "Edit MCP Settings". + public static final String COPILOT_MCP_PREFERENCE_PAGE_ID = "com.microsoft.copilot.eclipse.ui.preferences.McpPreferencePage"; } \ No newline at end of file diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java index 235decfc..5e7f26e4 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/configuration/McpInstallService.java @@ -2,9 +2,11 @@ import java.util.concurrent.CompletableFuture; +import com.checkmarx.eclipse.common.listener.IMcpInstallCallback; import com.checkmarx.eclipse.common.preferences.Preferences; import com.checkmarx.eclipse.common.runner.TenantSettingsProvider; import com.checkmarx.eclipse.common.utils.CxLogger; +import com.checkmarx.eclipse.common.utils.PluginConstants; /** @@ -40,6 +42,10 @@ private static void registerAuthenticationHandlers() { // Register handler for post-authentication UI (welcome dialog, workspace scan) Preferences.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler()); + // Register handler so common-lib preference pages (e.g. CheckmarxPreferencePage) + // can trigger MCP install without depending on this bundle directly. + Preferences.setMcpInstallHandler(McpInstallService::installFromUi); + authListenerRegistered = true; CxLogger.info(LOG_TAG + " Authentication handlers registered"); } @@ -118,6 +124,62 @@ public static void attemptAutoInstall(String apiKey, String additionalParams) { } } + /** + * Installs MCP configuration in response to a user-initiated action (the "Install MCP" + * link on CheckmarxPreferencePage), reporting the outcome to {@code callback} instead of + * only logging it - unlike {@link #attemptAutoInstall()}, which is silent by design. + * + * @param callback notified of success or failure; may be called from a background thread + */ + public static void installFromUi(IMcpInstallCallback callback) { + CxLogger.info(LOG_TAG + " Install MCP requested from preferences page..."); + + try { + if (!Preferences.isAuthenticated()) { + callback.onFailure(PluginConstants.MCP_NOT_AUTHENTICATED_MESSAGE); + return; + } + + String apiKey = Preferences.getApiKey(); + String additionalParams = Preferences.getAdditionalOptions(); + + if (apiKey == null || apiKey.isBlank()) { + callback.onFailure(PluginConstants.MCP_NOT_AUTHENTICATED_MESSAGE); + return; + } + + boolean aiMcpEnabled; + try { + aiMcpEnabled = TenantSettingsProvider.INSTANCE.isAiMcpServerEnabled(apiKey, additionalParams); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Failed to check MCP server status: " + e.getMessage(), e); + callback.onFailure(PluginConstants.MCP_INSTALL_GENERIC_FAILURE_MESSAGE); + return; + } + + if (!aiMcpEnabled) { + callback.onFailure(PluginConstants.MCP_NOT_ENABLED_FOR_TENANT_MESSAGE); + return; + } + + installSilentlyAsync(apiKey).thenAccept(changed -> { + if (changed == null) { + CxLogger.info(LOG_TAG + " Install MCP (from preferences page) failed"); + callback.onFailure(PluginConstants.MCP_INSTALL_GENERIC_FAILURE_MESSAGE); + } else if (changed) { + CxLogger.info(LOG_TAG + " Install MCP (from preferences page) succeeded"); + callback.onSuccess(); + } else { + CxLogger.info(LOG_TAG + " Install MCP (from preferences page): already up to date"); + callback.onAlreadyUpToDate(); + } + }); + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Unexpected error while installing MCP from preferences page: " + e.getMessage(), e); + callback.onFailure(PluginConstants.MCP_INSTALL_GENERIC_FAILURE_MESSAGE); + } + } + /** * Asynchronously installs MCP configuration without user notifications. * Failures are logged but do not interrupt plugin startup. diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java index 979ba19a..ce4f9d3b 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/scanners/iac/IacScannerService.java @@ -30,6 +30,7 @@ import com.checkmarx.eclipse.devassist.common.ScannerConfig; import com.checkmarx.eclipse.devassist.model.ScanEngine; import com.checkmarx.eclipse.devassist.utils.DevAssistConstants; +import com.checkmarx.eclipse.devassist.utils.DevAssistUtils; /** * Realtime IaC scanner service for Eclipse. @@ -143,13 +144,12 @@ public ScanResult scan(String filePath, IDocument document, String tempFilePath = saveResult.getLeft().toString(); CxLogger.info(LOG_TAG + " Start IAC Realtime Scan On File: " + filePath); - String containerTool = "docker"; // String ignoreFilePath = getIgnoreFilePath(proj); IacRealtimeResults scanResults = null; try { scanResults = wrapperProvider - .iacRealtimeScan(tempFilePath, containerTool, ""); + .iacRealtimeScan(tempFilePath, DevAssistUtils.getContainerTool(), ""); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); diff --git a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java index d8d48717..08df5ee5 100644 --- a/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java +++ b/devassist-lib/src/com/checkmarx/eclipse/devassist/utils/DevAssistUtils.java @@ -1,15 +1,21 @@ package com.checkmarx.eclipse.devassist.utils; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.Base64; -import java.util.List; import java.util.Objects; -import java.net.URL; + import org.eclipse.core.resources.IFile; +import org.eclipse.e4.ui.css.swt.theme.ITheme; +import org.eclipse.e4.ui.css.swt.theme.IThemeEngine; import org.eclipse.jface.text.IDocument; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.swt.SWT; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.graphics.Color; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorReference; @@ -18,20 +24,13 @@ import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.texteditor.ITextEditor; -import org.eclipse.swt.SWT; -import org.eclipse.swt.dnd.Clipboard; -import org.eclipse.swt.dnd.TextTransfer; -import org.eclipse.swt.graphics.Color; -import org.eclipse.e4.ui.css.swt.theme.ITheme; -import org.eclipse.e4.ui.css.swt.theme.IThemeEngine; -import org.eclipse.swt.dnd.Clipboard; -import org.eclipse.swt.dnd.TextTransfer; +import com.checkmarx.eclipse.common.preferences.Preferences; +import com.checkmarx.eclipse.common.utils.CxLogger; import com.checkmarx.eclipse.devassist.backend.SeverityLevel; import com.checkmarx.eclipse.devassist.model.ScanIssue; import com.checkmarx.eclipse.devassist.model.Vulnerability; import com.checkmarx.eclipse.devassist.remediation.NotificationPopup; -import com.checkmarx.eclipse.common.utils.CxLogger; /** * Utility class for DevAssist operations. Provides methods for encoding, @@ -437,4 +436,25 @@ private static boolean isDarkByBackgroundLuminance() { / 255.0; return luminance < 0.5; } + + /** + * Returns the container tool configured in the global settings. + * @return + */ + public static String getContainerTool() { + try { + // Prefer the typed preference store access which returns the stored value + // or an empty string if not present. Fall back to the generic getPref + // only if needed. Always return a sensible default when empty/null. + String value = Preferences.STORE.getString(Preferences.PREF_CONTAINERS_TOOL); + if (value == null || value.isBlank()) { + // Try the legacy getter which may consult the preference service + value = Preferences.getPref(Preferences.PREF_CONTAINERS_TOOL); + } + return (value == null || value.isBlank()) ? "docker" : value; + } catch (Exception e) { + CxLogger.error(LOG_TAG + " Error retrieving container tool preference: " + e.getMessage(), e); + return "docker"; // default to docker if preference retrieval fails + } + } } From 50ab007795bd9a5b3ce6d1912db29118ff60b120 Mon Sep 17 00:00:00 2001 From: Anand Nandeshwar <73646287+cx-anand-nandeshwar@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:08:50 +0530 Subject: [PATCH 11/11] Set fixed width (500px) for API key and Additional Params input fields --- .../common/preferences/PreferencesPage.java | 38 ++++++++++++++++--- .../eclipse/common/utils/PluginConstants.java | 2 + 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java index d1bdefc2..6402c468 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java +++ b/common-lib/src/com/checkmarx/eclipse/common/preferences/PreferencesPage.java @@ -169,7 +169,10 @@ public void widgetSelected(SelectionEvent e) { // so nothing else on the page can stretch or shrink that gap. Composite fieldsComposite = new Composite(topComposite, SWT.NONE); GridLayout fieldsLayout = new GridLayout(); - fieldsLayout.numColumns = 1; + // Use 2 columns so each FieldEditor places its label in column 1 and the + // input control in column 2. This allows us to set a widthHint on the + // input control without the control stretching to the full dialog width. + fieldsLayout.numColumns = 2; fieldsLayout.marginHeight = 0; fieldsLayout.marginWidth = 0; fieldsLayout.verticalSpacing = 4; @@ -182,6 +185,14 @@ public void widgetSelected(SelectionEvent e) { addField(apiKey); Text textControl = apiKey.getTextControl(fieldsComposite); textControl.setEchoChar('*'); + // Set fixed width for apiKey field + GridData apiKeyGridData = new GridData(SWT.BEGINNING, SWT.CENTER, true, false); + apiKeyGridData.widthHint = 500; + apiKeyGridData.grabExcessHorizontalSpace = false; + apiKeyGridData.horizontalAlignment = GridData.FILL; + if (textControl != null && !textControl.isDisposed()) { + textControl.setLayoutData(apiKeyGridData); + } StringFieldEditor additionalParams = new StringFieldEditor(Preferences.ADDITIONAL_OPTIONS, PluginConstants.PREFERENCES_ADDITIONAL_OPTIONS, StringFieldEditor.UNLIMITED, @@ -189,6 +200,16 @@ public void widgetSelected(SelectionEvent e) { additionalParamsField = additionalParams; addField(additionalParams); + // Ensure the Additional Params text control has a fixed width similar to the API key + Text additionalTextControl = additionalParams.getTextControl(fieldsComposite); + GridData additionalGridData = new GridData(SWT.BEGINNING, SWT.CENTER, true, false); + additionalGridData.widthHint = 500; // match apiKey width + additionalGridData.grabExcessHorizontalSpace = false; + additionalGridData.horizontalAlignment = GridData.FILL; + if (additionalTextControl != null && !additionalTextControl.isDisposed()) { + additionalTextControl.setLayoutData(additionalGridData); + } + // Baseline for the change-detection guard in performOk() - captured now that // both fields have loaded their values from the preference store. initialApiKey = apiKey.getStringValue(); @@ -215,12 +236,10 @@ public void widgetSelected(SelectionEvent e) { gridData.horizontalAlignment = GridData.FILL; textControl.setLayoutData(gridData); - spacer(topComposite); - Link cliHelp = new Link(topComposite, SWT.NONE); - cliHelp.setText( - "CLI command that supports a set of global flags"); - cliHelp.setLayoutData(new GridData(SWT.END, SWT.CENTER, true, false)); + cliHelp.setText("" + + PluginConstants.PREFERENCES_CLI_HELP_LINK_TEXT + ""); + cliHelp.setLayoutData(new GridData(SWT.END, SWT.CENTER, false, false)); cliHelp.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { @@ -451,6 +470,13 @@ public void widgetSelected(SelectionEvent e) { spacer(topComposite); + Label mcpSeparator = new Label(topComposite, SWT.SEPARATOR | SWT.HORIZONTAL); + GridData mcpSeparatorData = new GridData(GridData.FILL_HORIZONTAL); + mcpSeparatorData.verticalIndent = 6; + mcpSeparator.setLayoutData(mcpSeparatorData); + + spacer(topComposite); + realtimeScannersLink = new Link(topComposite, SWT.NONE); realtimeScannersLink.setText("" + PluginConstants.GO_TO_CHECKMARX_ONE_ASSIST + ""); realtimeScannersLink.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false)); diff --git a/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java index a5a1082c..2e53df17 100644 --- a/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java +++ b/common-lib/src/com/checkmarx/eclipse/common/utils/PluginConstants.java @@ -69,6 +69,8 @@ public class PluginConstants { public static final String GO_TO_CHECKMARX_ONE_ASSIST = "Go to Checkmarx One Assist"; public static final String PREFERENCES_HELP_LINK_TEXT = "Checkmarx One Eclipse Plugin Help Page"; public static final String PREFERENCES_HELP_LINK_URL = "https://checkmarx.com/resource/documents/en/34965-68728-checkmarx-one-eclipse-plugin.html"; + public static final String PREFERENCES_CLI_HELP_LINK_TEXT = "CLI command that supports a set of global flags"; + public static final String PREFERENCES_CLI_HELP_LINK = "https://checkmarx.com/resource/documents/en/34965-68626-global-flags.html"; public static final String PREFERENCES_VALIDATING_STATE = "Validating..."; public static final String LOGOUT_CONFIRM_TITLE = "Confirm Logout"; public static final String LOGOUT_CONFIRM_MESSAGE = "Are you sure you want to logout?";