-
Notifications
You must be signed in to change notification settings - Fork 5
AIML-224: Consolidate RouteCoverageService tools to fix Claude API 64-char limit #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
67b67fb
Consolidate RouteCoverageService tools to fix Claude API 64-char limit
ChrisEdwards 04620f8
Add bead status management guidance to CLAUDE.md
ChrisEdwards ee20944
Fix empty string handling in route coverage filters (MCP-OU8)
ChrisEdwards d9149ea
Add validation test for empty string sessionMetadataValue (MCP-3EG)
ChrisEdwards e0cde08
Merge branch 'main' into AIML-224-consolidate-route-coverage
ChrisEdwards File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||
|
|
@@ -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; | ||||||
|
|
||||||
|
|
@@ -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() && | ||||||
| (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()) { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| // 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; | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| } | ||||||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
StringUtilslibrary 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:
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.