Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,44 @@ This codebase handles sensitive vulnerability data. The README contains critical
- Console logging is minimal by design for MCP protocol compatibility
- Debug mode buffers API responses for logging (memory impact with large datasets)

## Beads Workflow Requirements

This project uses Beads (bd) for issue tracking. See the MCP resource `beads://quickstart` for usage details.

### Bead Status Management

**IMPORTANT: Update bead status as you work:**

1. **When starting work on a bead**: Immediately set status to `in_progress`
```
bd update <bead-id> status=in_progress
```
Or use the MCP tool:
```
mcp__plugin_beads_beads__update(issue_id="<bead-id>", status="in_progress")
```

2. **While working**: Keep the bead `in_progress` until all work is complete, tested, and ready to close

3. **When work is complete**: Close the bead only after all acceptance criteria are met
```
bd close <bead-id>
```

**Status lifecycle:**
- `open` → Task not yet started
- `in_progress` → Actively working on task (SET THIS WHEN YOU START!)
- `closed` → Task complete, tested, and merged

### Managing Bead Dependencies

**Command syntax:** `bd dep add <dependent-task> <prerequisite-task>`

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 <task-id>` - dependent tasks show "Depends on", prerequisites show "Blocks".

### Testing Requirements Before Closing Beads
### Troubleshooting

For common issues (SSL certificates, proxy configuration, debug logging), see the "Common Issues" and "Proxy Configuration" sections in [README.md](README.md).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@

import com.contrast.labs.ai.mcp.contrast.sdkexstension.SDKExtension;
import com.contrast.labs.ai.mcp.contrast.sdkexstension.SDKHelper;
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.application.Application;
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.routecoverage.Route;
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.routecoverage.RouteCoverageBySessionIDAndMetadataRequestExtended;
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.routecoverage.RouteCoverageResponse;
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.routecoverage.RouteDetailsResponse;
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.sessionmetadata.SessionMetadataResponse;
import com.contrastsecurity.models.RouteCoverageBySessionIDAndMetadataRequest;
import com.contrastsecurity.models.RouteCoverageMetadataLabelValues;
import com.contrastsecurity.sdk.ContrastSDK;
import org.slf4j.Logger;
Expand All @@ -18,15 +16,12 @@
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.Optional;

@Service
public class RouteCoverageService {

private static final Logger logger = LoggerFactory.getLogger(RouteCoverageService.class);



@Value("${contrast.host-name:${CONTRAST_HOST_NAME:}}")
private String hostName;

Expand All @@ -48,150 +43,94 @@ public class RouteCoverageService {
@Value("${http.proxy.port:${http_proxy_port:}}")
private String httpProxyPort;



@Tool(name = "get_application_route_coverage", description = "takes a application name and return the route coverage data for that application. " +
"If a route/endpoint is DISCOVERED, it means it has been found by Assess but that route has had no inbound http requests. If it is EXERCISED, it means it has had atleast one inbound http request to that route/endpoint.")
public RouteCoverageResponse getRouteCoverage(String app_name) throws IOException {
logger.info("Retrieving route coverage for application by name: {}", app_name);
ContrastSDK contrastSDK = SDKHelper.getSDK(hostName, apiKey, serviceKey, userName,httpProxyHost, httpProxyPort);
SDKExtension sdkExtension = new SDKExtension(contrastSDK);
logger.debug("Searching for application ID matching name: {}", app_name);

Optional<Application> application = SDKHelper.getApplicationByName(app_name, orgID, contrastSDK);

if (!application.isPresent()) {
logger.error("Application not found: {}", app_name);
throw new IOException("Application not found: " + app_name);
}

logger.debug("Fetching route coverage data for application ID: {}", application.get().getAppId());
RouteCoverageResponse response = sdkExtension.getRouteCoverage(orgID, application.get().getAppId(), null);
logger.debug("Found {} routes for application", response.getRoutes().size());

logger.debug("Retrieving route details for each route");
for(Route route : response.getRoutes()) {
logger.trace("Fetching details for route: {}", route.getSignature());
RouteDetailsResponse routeDetailsResponse = sdkExtension.getRouteDetails(orgID, application.get().getAppId(), route.getRouteHash());
route.setRouteDetailsResponse(routeDetailsResponse);
/**
* Retrieves route coverage data for an application with optional filtering.
*
* Routes can have two statuses:
* - DISCOVERED: Found by Contrast Assess but has not received any HTTP requests
* - EXERCISED: Has received at least one HTTP request
*
* @param appId Required - The application ID to retrieve route coverage for
* @param sessionMetadataName Optional - Filter by session metadata field name (e.g., "branch").
* Empty strings are treated as null (no filter).
* @param sessionMetadataValue Optional - Filter by session metadata field value (e.g., "main").
* Required if sessionMetadataName is provided. Empty strings are treated as null.
* @param useLatestSession Optional - If true, only return routes from the latest session
* @return RouteCoverageResponse containing route coverage data with details for each route
* @throws IOException If an error occurs while retrieving data from Contrast
* @throws IllegalArgumentException If sessionMetadataName is provided without sessionMetadataValue
*/
@Tool(name = "get_route_coverage",
description = "Retrieves route coverage data for an application. Routes can be DISCOVERED (found but not exercised) " +
"or EXERCISED (received HTTP traffic). All filter parameters are truly optional - if none provided (null or empty strings), " +
"returns all routes across all sessions. Parameters: appId (required), sessionMetadataName (optional), " +
"sessionMetadataValue (optional - required if sessionMetadataName provided), useLatestSession (optional).")
public RouteCoverageResponse getRouteCoverage(
String appId,
String sessionMetadataName,
String sessionMetadataValue,
Boolean useLatestSession) throws IOException {

logger.info("Retrieving route coverage for application ID: {}", appId);

// Validate parameters - treat empty strings as null
if (sessionMetadataName != null && !sessionMetadataName.isEmpty() &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the Spring StringUtils library or add this dependency that gives many String helper API's:
https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/StringUtils.html
So this could be:

if (StringUtils.hasText(sessionMetadataName) && !StringUtils.hasText(sessionMetadataValue)) {
    var errorMsg = "sessionMetadataValue is required when sessionMetadataName is provided";
    logger.error(errorMsg);
    throw new IllegalArgumentException(errorMsg);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will incorporate this into the project downstream and use it going forward.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix this downstream as it requires adding the library and updating all the places it could be used.

(sessionMetadataValue == null || sessionMetadataValue.isEmpty())) {
String errorMsg = "sessionMetadataValue is required when sessionMetadataName is provided";
logger.error(errorMsg);
throw new IllegalArgumentException(errorMsg);
}

logger.info("Successfully retrieved route coverage for application: {}", app_name);
return response;
}

@Tool(name = "get_application_route_coverage_by_app_id", description = "takes a application id and return the route coverage data for that application. " +
"If a route/endpoint is DISCOVERED, it means it has been found by Assess but that route has had no inbound http requests. If it is EXERCISED, it means it has had atleast one inbound http request to that route/endpoint.")
public RouteCoverageResponse getRouteCoverageByAppID(String app_id) throws IOException {
logger.info("Retrieving route coverage for application by ID: {}", app_id);
ContrastSDK contrastSDK = SDKHelper.getSDK(hostName, apiKey, serviceKey, userName,httpProxyHost, httpProxyPort);
SDKExtension sdkExtension = new SDKExtension(contrastSDK);

logger.debug("Fetching route coverage data for application ID: {}", app_id);
RouteCoverageResponse response = sdkExtension.getRouteCoverage(orgID, app_id, null);
logger.debug("Found {} routes for application", response.getRoutes().size());

logger.debug("Retrieving route details for each route");
for(Route route : response.getRoutes()) {
logger.trace("Fetching details for route: {}", route.getSignature());
RouteDetailsResponse routeDetailsResponse = sdkExtension.getRouteDetails(orgID, app_id, route.getRouteHash());
route.setRouteDetailsResponse(routeDetailsResponse);
}

logger.info("Successfully retrieved route coverage for application ID: {}", app_id);
return response;
}

@Tool(name = "get_application_route_coverage_by_app_name_and_session_metadata", description = "takes a application name and return the route coverage data for that application for the specified session metadata name and value. " +
"If a route/endpoint is DISCOVERED, it means it has been found by Assess but that route has had no inbound http requests. If it is EXERCISED, it means it has had at least one inbound http request to that route/endpoint.")
public RouteCoverageResponse getRouteCoverageByAppNameAndSessionMetadata(String app_name, String session_Metadata_Name, String session_Metadata_Value) throws IOException {
logger.info("Retrieving route coverage for application by Name: {}", app_name);
ContrastSDK contrastSDK = SDKHelper.getSDK(hostName, apiKey, serviceKey, userName,httpProxyHost, httpProxyPort);
logger.debug("Searching for application ID matching name: {}", app_name);

Optional<Application> application = SDKHelper.getApplicationByName(app_name, orgID, contrastSDK);
if (!application.isPresent()) {
logger.error("Application not found: {}", app_name);
throw new IOException("Application not found: " + app_name);
}
return getRouteCoverageByAppIDAndSessionMetadata(application.get().getAppId(), session_Metadata_Name, session_Metadata_Value);
}

@Tool(name = "get_application_route_coverage_by_app_id_and_session_metadata", description = "takes a application id and return the route coverage data for that application for the specified session metadata name and value. " +
"If a route/endpoint is DISCOVERED, it means it has been found by Assess but that route has had no inbound http requests. If it is EXERCISED, it means it has had at least one inbound http request to that route/endpoint.")
public RouteCoverageResponse getRouteCoverageByAppIDAndSessionMetadata(String app_id, String session_Metadata_Name, String session_Metadata_Value) throws IOException {
logger.info("Retrieving route coverage for application by ID: {}", app_id);
ContrastSDK contrastSDK = SDKHelper.getSDK(hostName, apiKey, serviceKey, userName,httpProxyHost, httpProxyPort);
// Initialize SDK
ContrastSDK contrastSDK = SDKHelper.getSDK(hostName, apiKey, serviceKey, userName, httpProxyHost, httpProxyPort);
SDKExtension sdkExtension = new SDKExtension(contrastSDK);
RouteCoverageBySessionIDAndMetadataRequestExtended requestExtended = new RouteCoverageBySessionIDAndMetadataRequestExtended();
RouteCoverageMetadataLabelValues metadataLabelValue = new RouteCoverageMetadataLabelValues();
metadataLabelValue.setLabel(session_Metadata_Name);
metadataLabelValue.getValues().add(String.valueOf(session_Metadata_Value));
requestExtended.getValues().add(metadataLabelValue);
logger.debug("Fetching route coverage data for application ID: {}", app_id);
RouteCoverageResponse response = sdkExtension.getRouteCoverage(orgID, app_id, requestExtended);
logger.debug("Found {} routes for application", response.getRoutes().size());

logger.debug("Retrieving route details for each route");
for(Route route : response.getRoutes()) {
logger.trace("Fetching details for route: {}", route.getSignature());
RouteDetailsResponse routeDetailsResponse = sdkExtension.getRouteDetails(orgID, app_id, route.getRouteHash());
route.setRouteDetailsResponse(routeDetailsResponse);
}

logger.info("Successfully retrieved route coverage for application ID: {}", app_id);
return response;
}

@Tool(name = "get_application_route_coverage_by_app_name_latest_session", description = "takes a application name and return the route coverage data for that application from the latest session. " +
"If a route/endpoint is DISCOVERED, it means it has been found by Assess but that route has had no inbound http requests. If it is EXERCISED, it means it has had atleast one inbound http request to that route/endpoint.")
public RouteCoverageResponse getRouteCoverageByAppNameLatestSession(String app_name) throws IOException {
logger.info("Retrieving route coverage for application by Name: {}", app_name);
ContrastSDK contrastSDK = SDKHelper.getSDK(hostName, apiKey, serviceKey, userName, httpProxyHost, httpProxyPort);
Optional<Application> application = SDKHelper.getApplicationByName(app_name, orgID, contrastSDK);
if (application.isEmpty()) {
logger.error("Application not found: {}", app_name);
throw new IOException("Application not found: " + app_name);
// Build request based on parameters
RouteCoverageBySessionIDAndMetadataRequestExtended requestExtended = null;

if (useLatestSession != null && useLatestSession) {
// Filter by latest session
logger.debug("Fetching latest session metadata for application ID: {}", appId);
SessionMetadataResponse latest = sdkExtension.getLatestSessionMetadata(orgID, appId);

if (latest == null || latest.getAgentSession() == null) {
logger.error("No session metadata found for application ID: {}", appId);
RouteCoverageResponse noRouteCoverageResponse = new RouteCoverageResponse();
noRouteCoverageResponse.setSuccess(false);
logger.debug("No Agent session found in latest session metadata response for application ID: {}", appId);
return noRouteCoverageResponse;
}

requestExtended = new RouteCoverageBySessionIDAndMetadataRequestExtended();
requestExtended.setSessionId(latest.getAgentSession().getAgentSessionId());
logger.debug("Using latest session ID: {}", latest.getAgentSession().getAgentSessionId());

} else if (sessionMetadataName != null && !sessionMetadataName.isEmpty()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (sessionMetadataName != null && !sessionMetadataName.isEmpty()) {
} else if (StringUtils.hasText(sessionMetadataName)) {

// Filter by session metadata
logger.debug("Filtering by session metadata: {}={}", sessionMetadataName, sessionMetadataValue);
requestExtended = new RouteCoverageBySessionIDAndMetadataRequestExtended();
RouteCoverageMetadataLabelValues metadataLabelValue = new RouteCoverageMetadataLabelValues();
metadataLabelValue.setLabel(sessionMetadataName);
metadataLabelValue.getValues().add(sessionMetadataValue);
requestExtended.getValues().add(metadataLabelValue);
} else {
logger.debug("No filters applied - retrieving all route coverage");
}
return getRouteCoverageByAppIDLatestSession(application.get().getAppId());
}


@Tool(name = "get_application_route_coverage_by_app_id_latest_session", description = "takes a application id and return the route coverage data for that application from the latest session. " +
"If a route/endpoint is DISCOVERED, it means it has been found by Assess but that route has had no inbound http requests. If it is EXERCISED, it means it has had atleast one inbound http request to that route/endpoint.")
public RouteCoverageResponse getRouteCoverageByAppIDLatestSession(String app_id) throws IOException {
logger.info("Retrieving route coverage for application by ID: {}", app_id);
ContrastSDK contrastSDK = SDKHelper.getSDK(hostName, apiKey, serviceKey, userName,httpProxyHost, httpProxyPort);
SDKExtension sdkExtension = new SDKExtension(contrastSDK);
SDKExtension extension = new SDKExtension(contrastSDK);
SessionMetadataResponse latest = extension.getLatestSessionMetadata(orgID,app_id);
if (latest == null || latest.getAgentSession() == null) {
logger.error("No session metadata found for application ID: {}", app_id);
RouteCoverageResponse noRouteCoverageResponse = new RouteCoverageResponse();
noRouteCoverageResponse.setSuccess(Boolean.FALSE);
logger.debug("No Agent session found in latest session metadata response for application ID: {}", app_id);
return noRouteCoverageResponse; // Return empty response if no session metadata found
}
RouteCoverageBySessionIDAndMetadataRequestExtended requestExtended = new RouteCoverageBySessionIDAndMetadataRequestExtended();
requestExtended.setSessionId(latest.getAgentSession().getAgentSessionId());
logger.debug("Fetching route coverage data for application ID: {}", app_id);
RouteCoverageResponse response = sdkExtension.getRouteCoverage(orgID, app_id, requestExtended);
// Call SDK to get route coverage
logger.debug("Fetching route coverage data for application ID: {}", appId);
RouteCoverageResponse response = sdkExtension.getRouteCoverage(orgID, appId, requestExtended);
logger.debug("Found {} routes for application", response.getRoutes().size());

// Fetch route details for each route
logger.debug("Retrieving route details for each route");
for(Route route : response.getRoutes()) {
for (Route route : response.getRoutes()) {
logger.trace("Fetching details for route: {}", route.getSignature());
RouteDetailsResponse routeDetailsResponse = sdkExtension.getRouteDetails(orgID, app_id, route.getRouteHash());
RouteDetailsResponse routeDetailsResponse = sdkExtension.getRouteDetails(orgID, appId, route.getRouteHash());
route.setRouteDetailsResponse(routeDetailsResponse);
}

logger.info("Successfully retrieved route coverage for application ID: {}", app_id);
logger.info("Successfully retrieved route coverage for application ID: {} ({} routes)", appId, response.getRoutes().size());
return response;
}






}
Loading