diff --git a/CLAUDE.md b/CLAUDE.md index 2d9521c..7f9b827 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,6 +83,11 @@ Required environment variables/arguments: 4. **Defensive Design**: All external API calls include proper resource management (try-with-resources), error handling, and logging 5. **Pagination Handling**: SDK extension methods handle pagination automatically (see `getLibraryObservations` for pattern) +### Coding Standards + +- **Prefer `var`** for local variables when the type is obvious from the right-hand side +- **Use `isEmpty()`** instead of `size() > 0` or `size() == 0` for collection checks + ### Security Considerations This codebase handles sensitive vulnerability data. The README contains critical warnings about data privacy when using with AI models. Never expose Contrast credentials or vulnerability data to untrusted AI services. @@ -162,3 +167,417 @@ This project is tracked in Jira under the **AIML** project. When creating Jira t - `Epic` - for large features with many dependent tasks (typically managed by Product Management) **Access**: Use the Atlassian MCP server to read or write Jira tickets programmatically. + +---- + +## AI Development Workflow + +This section defines the complete workflow for a Developer using AI agents working with beads and Jira tickets in this project. + +### Workflow Overview + +**Key Labels:** +- `stacked-branch` - Branch is based on another PR branch (not main) +- `pr-created` - Pull request has been created +- `in-review` - Pull request is ready for human review (not draft) + +**Decision Tree:** + +``` +Branch Creation: +├─ Based on main → No special label +└─ Based on another PR branch → Label with `stacked-branch` + +PR Creation: +├─ Has `stacked-branch` label? +│ └─ YES → Create DRAFT PR (Stacked PRs workflow) +│ - Base: parent PR's branch +│ - Labels: `pr-created` (NOT `in-review` yet) +│ - Add warning banner + dependency context +│ +└─ NO → Create ready PR (Moving to Review workflow) + - Base: main + - Labels: `pr-created`, `in-review` + - Standard PR description + +Promoting Stacked PR (after base PR merges): +└─ Rebase onto main, update base branch, remove warnings + Add `in-review` label, mark PR ready +``` + +### Starting Work on a Bead + +**1. Determine if a feature branch is needed:** + - **Bead has Jira ticket**: Create a new feature branch + - **Ask user which branch to base it off of** + - Show recently updated branches (sorted by most recent commits/PRs) + - User may be working with stacked branches where each new branch comes off the previous PR branch + - Name the branch with Jira ID prefix (e.g., `AIML-224-description`) + - **If based on another PR branch (not main)**: Label bead with `stacked-branch` + - **Bead is a child of Jira-linked bead**: Use the same branch as the parent bead + - **Bead has no Jira association and no parent**: + - **Ask user if it should have a Jira ticket** + - Most code changes need a Jira ticket and branch before merging + - Code changes should generally have a Jira ticket in scope + +**2. Update bead status and labels:** + - Set bead status to `in_progress` + - Record the branch name in the bead (so it's easily found later) + - **If this is a stacked branch** (based on another PR branch): + - Label the bead with `stacked-branch` + - Create parent-child dependency: the stacked bead depends on the bead of the branch it's based on + +**3. Update Jira (if applicable):** + - If bead has a linked Jira ticket: + - Update Jira status to "In Progress" using Atlassian MCP + - **Assign the ticket to the current user** (the authenticated Atlassian MCP user) + +**4. Enter plan mode and present approach:** + - Present a textual plan of what needs to be done + - Discuss the approach with the user + - **Tell user: "ASK ME TO GENERATE A PLAN WHEN YOU ARE READY"** + - Wait for user approval before generating full plan and proceeding + +### Creating Related Beads + +**When creating new beads from a Jira-linked bead:** +- Ask user if the new bead should be a child of the Jira-linked bead +- If yes, establish parent-child relationship using `bd dep add ` with `parent-child` dependency type +- Child beads work on the same branch as their parent + +### Managing Bead Dependencies + +**Command syntax:** `bd dep add ` + +Example: If B must be done after A completes, use `bd dep add B A` (not `bd dep add A B`). + +Verify with `bd show ` - dependent tasks show "Depends on", prerequisites show "Blocks". + +### During Development + +**Build and verify artifacts** as needed for testing: +- Build JAR for MCP server manual testing: `mvn clean package` +- Verify version logging to confirm correct build is running + +### Testing Requirements Before Moving to Review + +**CRITICAL: Before requesting review, you MUST:** +1. **Write tests for ALL code changes** - No exceptions +2. **Run unit tests** - `mvn test` must pass with 0 failures +3. **Run integration tests** - `mvn verify` must pass (requires credentials in `.env.integration-test`) + - If credentials unavailable, verify integration tests pass in CI/CD +4. **Verify new tests are included** - Ensure your tests ran and passed + +All code changes require corresponding test coverage. Do not move to review without tests. + +See INTEGRATION_TESTS.md for integration test setup and credentials. + +### Creating High-Quality PR Descriptions + +**This section defines the shared approach for creating PR descriptions used by both "Moving to Review" and "Stacked PRs" workflows.** + +Human review is the bottleneck in AI-assisted development. Creating exceptional PR descriptions that make review effortless is critical to development velocity. + +**Research Phase** - Gather comprehensive context from: +- All beads that have been worked on for this branch +- All git commits in the branch (`git log`, `git diff`) +- Voice notes that relate to this work +- Any related Jira tickets + +**PR Description Structure:** + +1. **Why**: Explain the problem or need that motivated this change + - What problem are we solving? + - What value does this provide? + - What was the business or technical driver? + +2. **What**: Describe what changes were made at a high level + - What components/files were modified? + - What new capabilities exist? + - What behavior changed? + +3. **How**: Explain how it was implemented + - Technical approach and architecture decisions + - Key implementation choices and trade-offs + - Design patterns used + - Integration points + +4. **Step-by-step walkthrough**: Guide the reviewer through the changes in logical order + - Walk through the diff in a sensible sequence + - Explain complex sections + - Call out important details + - Help reviewer understand the flow + +5. **Testing**: Summarize test coverage and results + - Unit test coverage added + - Integration test coverage added + - Test results (pass/fail counts) + - Manual testing performed + - Edge cases covered + +**Goal**: Make reviewing effortless by providing all information the reviewer needs to understand and evaluate the changes with confidence and speed. The reviewer should not need to ask clarifying questions. + +### Moving to Review + +**When user says "move to review" or "ready for review" for a bead WITHOUT the `stacked-branch` label:** + +This workflow creates a standard PR ready for immediate review, targeting the `main` branch. + +**1. Label the bead(s):** + - Create/apply labels: `pr-created` and `in-review` + - Apply to all beads worked on in this branch + +**2. Push to remote:** + - Push the feature branch to remote repository + +**3. Create or update Pull Request:** + - If PR doesn't exist, create it with base branch `main` + - If PR exists, update the description + - PR should be ready for review (NOT draft) + +**4. Generate comprehensive PR description:** + - Follow the **"Creating High-Quality PR Descriptions"** section above + - Use the standard structure: Why / What / How / Walkthrough / Testing + - No special warnings or dependency context needed + +### Stacked PRs (Ready for Draft Review) + +**When user says "ready for stacked PR", "ready for draft review", or when creating a PR for a bead WITH the `stacked-branch` label:** + +This workflow creates a draft PR that depends on another unmerged PR (stacked branches). + +**1. Identify the base PR:** + - Find the PR for the base branch using `gh pr list --head ` + - Note the PR number and URL + +**2. Label the bead(s):** + - Create/apply label `pr-created` to the bead + - **Do NOT add `in-review` label yet** (only added when promoted to ready-for-review) + - Apply to all beads worked on in this branch + +**3. Push to remote:** + - Push the feature branch: `git push -u origin ` + +**4. Create DRAFT Pull Request:** + - **Base branch**: Set to the parent PR's branch (NOT main) + - **Status**: MUST be draft + - **Title**: Include `[STACKED]` indicator + - **Body**: Start with prominent warning, then add dependency context, followed by standard PR description: + ``` + **⚠️ DO NOT MERGE - WAITING FOR ** + + This is a stacked PR based on #. + Please review and merge # first, + then rebase this PR onto `main` before merging. + + --- + + **Dependency Context:** + This PR builds on the work from #. [Briefly explain + what the base PR did and why this PR follows it] + + --- + ``` + - After the warning and dependency context, follow the **"Creating High-Quality PR Descriptions"** section + - Use the standard structure: Why / What / How / Walkthrough / Testing + +**5. Verify configuration:** + - Confirm PR is in draft status + - Confirm base branch is the parent PR's branch + - Confirm warning and dependency context are prominently displayed + +**Example command:** +```bash +gh pr create --draft \ + --base AIML-226-parent-branch \ + --title "AIML-228: Feature name [STACKED]" \ + --body "$(cat <<'EOF' +**⚠️ DO NOT MERGE - WAITING FOR https://github.com/org/repo/pull/27** + +This is a stacked PR based on #27. Please review and merge #27 first. + +--- + +## Summary +[Your PR description here] +EOF +)" +``` + +**IMPORTANT**: Stacked PRs must remain in draft until: +1. The base PR is merged +2. This PR is rebased onto main +3. CI/CD passes on the rebased code + +### Promoting Stacked PR to Ready for Review + +**When user says "move stacked PR to ready for review", "promote stacked PR", or "finalize stacked PR" for a bead WITH the `stacked-branch` label:** + +This workflow promotes a draft stacked PR to ready-for-review after its base PR has been merged to main. + +**Prerequisites:** +- Bead must have `stacked-branch` label +- Base PR must be merged to main +- All tests must be passing on the stacked branch +- PR is currently in draft status targeting the base PR's branch + +**Steps:** + +**1. Identify PR context:** + - Determine which PR to promote (by PR number or current branch) + - Identify the base PR it depends on + - Example: "Which PR should I promote?" or infer from current branch + +**2. Verify base PR is merged:** + ```bash + gh pr view --json state,mergedAt,baseRefName + ``` + - Must show `state: "MERGED"` + - Note when it was merged for reference + - If NOT merged, inform user and wait + +**3. Fetch and rebase onto main:** + ```bash + git fetch origin + git checkout + git rebase origin/main + ``` + - Handle conflicts if they arise (pause and ask user for guidance) + - Clean rebase expected for well-structured stacks + +**4. Force push safely:** + ```bash + git push --force-with-lease origin + ``` + - Use `--force-with-lease` (NOT `--force`) for safety + - Prevents overwriting if branch was updated elsewhere + +**5. Update PR base branch:** + ```bash + gh pr edit --base main + ``` + - Changes PR from targeting base branch to targeting main + - GitHub will update the diff automatically + +**6. Update PR description:** + - Remove "DO NOT MERGE" and dependency warnings + - Remove stacked PR notes about targeting other branches + - Add line confirming rebase: "Rebased onto main after # merged" + - Update test counts if they changed + - Keep all other content (Why/What/How/Testing) + + Example: + ```bash + # Create updated description in temp file + gh pr view --json body -q .body > /tmp/pr_body.txt + # Edit to remove warnings and add rebase note + gh pr edit --body-file /tmp/updated_pr_body.txt + ``` + +**7. Mark PR ready for review:** + ```bash + gh pr ready + ``` + - Removes draft status + - PR is now visible in review queue + - CI/CD should trigger automatically + +**8. Verify tests pass:** + ```bash + mvn clean verify + ``` + - Run full test suite to verify rebase didn't break anything + - Address any failures before proceeding + - Check CI status on GitHub + +**9. Update bead:** + - **Add `in-review` label to the bead** (PR is now truly ready for human review) + - Update bead notes with PR status: "Rebased onto main, ready for review" + - Keep bead in `in_progress` status + - Don't close until PR is merged + +**10. Confirm completion:** + - Provide PR URL to user + - Confirm tests passing + - Note CI status + - Summary: "PR # rebased onto main and ready for review" + +**Example full workflow:** +```bash +# Check base PR merged +gh pr view 24 --json state,mergedAt +# Output: {"state":"MERGED","mergedAt":"2025-11-12T21:44:33Z"} + +# Fetch and rebase +git fetch origin +git checkout AIML-224-consolidate-route-coverage +git rebase origin/main +# Output: Successfully rebased and updated refs/heads/AIML-224-consolidate-route-coverage + +# Force push +git push --force-with-lease origin AIML-224-consolidate-route-coverage + +# Update PR +gh pr edit 25 --base main +gh pr edit 25 --body-file /tmp/updated_description.txt +gh pr ready 25 + +# Verify +mvn clean verify +``` + +**Common Issues:** + +**Rebase conflicts:** +- Pause and show user the conflicts +- Ask for guidance on resolution +- Don't attempt to auto-resolve without user input + +**Base PR not merged:** +- Inform user: "Base PR # is not yet merged (status: )" +- Wait for user instruction +- Don't proceed with promotion + +**Tests fail after rebase:** +- Report failures to user +- Keep PR in draft until tests pass +- Investigate if rebase introduced issues + +**CI not triggering:** +- GitHub CI may take a few minutes to start +- Verify workflow configuration targets main branch +- Check `.github/workflows/` for PR triggers + +### Landing the Plane + +**When user says "let's land the plane":** + +This workflow is for ending the current session while preserving all state so work can continue seamlessly in a new session (due to context limits or time constraints). + +1. **Create follow-up beads:** + - Identify any remaining work that needs to be done + - Create child beads of the current bead for each follow-up task + - Use parent-child dependencies to maintain relationship + +2. **Update current bead with complete status:** + - Document everything done so far + - Record current state, blockers, decisions made + - Include any context the next AI will need to continue + - Update bead notes with progress details + +3. **Commit changes:** + - Stage and commit all work-in-progress changes + - Write appropriate commit message describing current state + +4. **Generate continuation prompt:** + - Create a prompt that will allow the user to resume work in next session + - User should be able to copy/paste this prompt to continue working on the bead + - Include bead ID, current status, and what needs to happen next + +### Closing Beads + +**IMPORTANT**: Always ask the user before closing a bead. + +**Cannot close parent beads** if they still have open children. Ensure all child beads are closed first. + +Beads typically remain `in_progress` (with `in-review` label) until the PR review is complete and merged. Only close beads when explicitly instructed by the user. diff --git a/src/main/java/com/contrast/labs/ai/mcp/contrast/AssessService.java b/src/main/java/com/contrast/labs/ai/mcp/contrast/AssessService.java index 29deeb1..40c4483 100644 --- a/src/main/java/com/contrast/labs/ai/mcp/contrast/AssessService.java +++ b/src/main/java/com/contrast/labs/ai/mcp/contrast/AssessService.java @@ -195,11 +195,12 @@ public List listVulnsByAppId( logger.info("Listing vulnerabilities for application ID: {}", appID); ContrastSDK contrastSDK = SDKHelper.getSDK(hostName, apiKey, serviceKey, userName, httpProxyHost, httpProxyPort); try { - // Use SDK native API with SESSION_METADATA expand + // Use SDK native API with SESSION_METADATA, SERVER_ENVIRONMENTS, and APPLICATION expand TraceFilterForm form = new TraceFilterForm(); form.setExpand(EnumSet.of( TraceFilterForm.TraceExpandValue.SESSION_METADATA, - TraceFilterForm.TraceExpandValue.SERVER_ENVIRONMENTS + TraceFilterForm.TraceExpandValue.SERVER_ENVIRONMENTS, + TraceFilterForm.TraceExpandValue.APPLICATION )); Traces traces = contrastSDK.getTraces(orgID, appID, form); @@ -277,7 +278,10 @@ public List listVulnsByAppIdForLatestSession( orgID, appID, filterBody, - EnumSet.of(TraceFilterForm.TraceExpandValue.SESSION_METADATA) + EnumSet.of( + TraceFilterForm.TraceExpandValue.SESSION_METADATA, + TraceFilterForm.TraceExpandValue.APPLICATION + ) ); List vulns = tracesResponse.getTraces().stream() @@ -492,7 +496,8 @@ public PaginatedResponse getAllVulnerabilities( filterForm.setOffset(pagination.offset()); filterForm.setExpand(EnumSet.of( TraceFilterForm.TraceExpandValue.SERVER_ENVIRONMENTS, - TraceFilterForm.TraceExpandValue.SESSION_METADATA + TraceFilterForm.TraceExpandValue.SESSION_METADATA, + TraceFilterForm.TraceExpandValue.APPLICATION )); // Try organization-level API (or app-specific if appId provided) diff --git a/src/main/java/com/contrast/labs/ai/mcp/contrast/data/VulnLight.java b/src/main/java/com/contrast/labs/ai/mcp/contrast/data/VulnLight.java index 84fa5be..14d1ebc 100644 --- a/src/main/java/com/contrast/labs/ai/mcp/contrast/data/VulnLight.java +++ b/src/main/java/com/contrast/labs/ai/mcp/contrast/data/VulnLight.java @@ -19,11 +19,31 @@ import java.util.List; +/** + * Lightweight vulnerability record for listing operations. + * Contains essential vulnerability information including application correlation data. + * + * @param title Vulnerability title/description + * @param type Vulnerability type/rule name (e.g., "sql-injection", "xss-reflected") + * @param vulnID Unique vulnerability identifier (UUID) + * @param severity Severity level (CRITICAL, HIGH, MEDIUM, LOW, NOTE) + * @param appID Application UUID that owns this vulnerability + * @param appName Application display name that owns this vulnerability + * @param sessionMetadata Session metadata tags associated with this vulnerability + * @param lastSeenAt ISO-8601 timestamp of last detection + * @param status Current vulnerability status (Reported, Confirmed, Remediated, etc.) + * @param firstSeenAt ISO-8601 timestamp of first detection + * @param closedAt ISO-8601 timestamp when closed (null if open) + * @param environments List of environments where vulnerability was seen (DEVELOPMENT, QA, PRODUCTION) + * @param tags User-defined tags applied to this vulnerability + */ public record VulnLight( String title, String type, String vulnID, String severity, + String appID, + String appName, List sessionMetadata, String lastSeenAt, String status, diff --git a/src/main/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapper.java b/src/main/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapper.java index 38c6539..6390608 100644 --- a/src/main/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapper.java +++ b/src/main/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapper.java @@ -41,16 +41,27 @@ public class VulnerabilityMapper { * Transform a Trace object into a lightweight vulnerability representation. * Used for list endpoints where full details aren't needed. * Session metadata is populated when expand=session_metadata is used in the SDK call. + * Application data is populated when expand=application is used in the SDK call. * * @param trace The trace object from Contrast SDK * @return VulnLight object with essential vulnerability information */ public VulnLight toVulnLight(Trace trace) { + // Extract application info safely, handling null cases + String appId = null; + String appName = null; + if (trace.getApplication() != null) { + appId = trace.getApplication().getId(); + appName = trace.getApplication().getName(); + } + return new VulnLight( trace.getTitle(), trace.getRule(), trace.getUuid(), trace.getSeverity(), + appId, + appName, trace.getSessionMetadata() != null ? trace.getSessionMetadata() : new ArrayList<>(), FilterHelper.formatTimestamp(trace.getLastTimeSeen()), trace.getStatus(), diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceIntegrationTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceIntegrationTest.java index 9e55cd4..d5873be 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceIntegrationTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/AssessServiceIntegrationTest.java @@ -199,11 +199,15 @@ void testVulnerabilitiesHaveBasicFields() throws IOException { assertNotNull(vuln.vulnID(), "VulnID should not be null"); assertNotNull(vuln.severity(), "Severity should not be null"); assertNotNull(vuln.status(), "Status should not be null"); + assertNotNull(vuln.appID(), "appID should not be null (APPLICATION expand should be included)"); + assertNotNull(vuln.appName(), "appName should not be null (APPLICATION expand should be included)"); + assertFalse(vuln.appID().isEmpty(), "appID should not be empty"); + assertFalse(vuln.appName().isEmpty(), "appName should not be empty"); - System.out.println("✓ " + vuln.vulnID() + ": " + vuln.title() + " (" + vuln.severity() + ")"); + System.out.println("✓ " + vuln.vulnID() + ": " + vuln.title() + " (" + vuln.severity() + ") - App: " + vuln.appName() + " (" + vuln.appID() + ")"); } - System.out.println("✓ All vulnerabilities have required fields"); + System.out.println("✓ All vulnerabilities have required fields including appID and appName"); } @Test diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapperTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapperTest.java index f4c1d1b..cb32994 100644 --- a/src/test/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapperTest.java +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/mapper/VulnerabilityMapperTest.java @@ -19,6 +19,7 @@ import com.contrast.labs.ai.mcp.contrast.data.VulnLight; import com.contrast.labs.ai.mcp.contrast.data.Vulnerability; import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.LibraryExtended; +import com.contrastsecurity.models.Application; import com.contrastsecurity.models.SessionMetadata; import com.contrastsecurity.models.Trace; import org.junit.jupiter.api.BeforeEach; @@ -60,6 +61,11 @@ void setUp() { void toVulnLight_BasicTrace_TransformsCorrectly() { // Arrange Trace trace = mock(Trace.class); + Application app = mock(Application.class); + when(app.getId()).thenReturn("app-123"); + when(app.getName()).thenReturn("Test Application"); + + when(trace.getApplication()).thenReturn(app); when(trace.getTitle()).thenReturn("SQL Injection"); when(trace.getRule()).thenReturn("sql-injection"); when(trace.getUuid()).thenReturn("vuln-123"); @@ -81,6 +87,8 @@ void toVulnLight_BasicTrace_TransformsCorrectly() { assertEquals("sql-injection", result.type()); assertEquals("vuln-123", result.vulnID()); assertEquals("CRITICAL", result.severity()); + assertEquals("app-123", result.appID()); + assertEquals("Test Application", result.appName()); assertEquals("Reported", result.status()); assertNotNull(result.lastSeenAt()); assertNotNull(result.firstSeenAt()); @@ -400,4 +408,69 @@ void toVulnLight_TraceWithEmptySessionMetadata_ReturnsEmptyList() { assertNotNull(result.sessionMetadata()); assertTrue(result.sessionMetadata().isEmpty()); } + + @Test + void toVulnLight_TraceWithNullApplication_HandlesGracefully() { + // Arrange - Test that mapper handles null application without crashing + Trace trace = mock(Trace.class); + + when(trace.getApplication()).thenReturn(null); // No application expanded + when(trace.getTitle()).thenReturn("Test Vuln"); + when(trace.getRule()).thenReturn("test-rule"); + when(trace.getUuid()).thenReturn("vuln-no-app"); + when(trace.getSeverity()).thenReturn("HIGH"); + when(trace.getStatus()).thenReturn("Reported"); + when(trace.getLastTimeSeen()).thenReturn(JAN_15_2025_10_30_UTC); + when(trace.getFirstTimeSeen()).thenReturn(JAN_14_2025_09_30_UTC); + when(trace.getClosedTime()).thenReturn(null); + when(trace.getServerEnvironments()).thenReturn(new ArrayList<>()); + when(trace.getTags()).thenReturn(new ArrayList<>()); + when(trace.getSessionMetadata()).thenReturn(null); + + // Act + VulnLight result = mapper.toVulnLight(trace); + + // Assert + assertNotNull(result); + assertNull(result.appID(), "appID should be null when application is not expanded"); + assertNull(result.appName(), "appName should be null when application is not expanded"); + assertEquals("vuln-no-app", result.vulnID()); + assertEquals("HIGH", result.severity()); + } + + @Test + void toVulnLight_TraceWithApplication_ExtractsAppData() { + // Arrange - Test that application data is correctly extracted when expanded + Trace trace = mock(Trace.class); + Application app = mock(Application.class); + when(app.getId()).thenReturn("app-abc-123"); + when(app.getName()).thenReturn("My Production App"); + + when(trace.getApplication()).thenReturn(app); + when(trace.getTitle()).thenReturn("XSS Vulnerability"); + when(trace.getRule()).thenReturn("xss-reflected"); + when(trace.getUuid()).thenReturn("vuln-with-app"); + when(trace.getSeverity()).thenReturn("CRITICAL"); + when(trace.getStatus()).thenReturn("Confirmed"); + when(trace.getLastTimeSeen()).thenReturn(JAN_15_2025_10_30_UTC); + when(trace.getFirstTimeSeen()).thenReturn(JAN_14_2025_09_30_UTC); + when(trace.getClosedTime()).thenReturn(null); + when(trace.getServerEnvironments()).thenReturn(Arrays.asList("PRODUCTION")); + when(trace.getTags()).thenReturn(Arrays.asList("high-priority")); + when(trace.getSessionMetadata()).thenReturn(new ArrayList<>()); + + // Act + VulnLight result = mapper.toVulnLight(trace); + + // Assert + assertNotNull(result); + assertEquals("app-abc-123", result.appID(), "appID should match application ID"); + assertEquals("My Production App", result.appName(), "appName should match application name"); + assertEquals("vuln-with-app", result.vulnID()); + assertEquals("xss-reflected", result.type()); + assertEquals("CRITICAL", result.severity()); + assertEquals(1, result.environments().size()); + assertEquals("PRODUCTION", result.environments().get(0)); + assertEquals(1, result.tags().size()); + } }