Skip to content

Commit 008fcc0

Browse files
added route coverage tool. Also increased the data returned for the Application to include Last Seen and Language Type
1 parent b215a37 commit 008fcc0

File tree

11 files changed

+812
-10
lines changed

11 files changed

+812
-10
lines changed

src/main/java/com/contrast/labs/ai/mcp/contrast/AssessService.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ public List<ApplicationData> getApplications(String app_name) throws IOException
234234
List<ApplicationData> filteredApps = new ArrayList<>();
235235
for(Application app : applications) {
236236
if(app.getName().toLowerCase().contains(app_name.toLowerCase())) {
237-
filteredApps.add(new ApplicationData(app.getName(), app.getStatus(), app.getId()));
237+
filteredApps.add(new ApplicationData(app.getName()m app.getStatus(), app.getId(), app.getLastSeen(), app.getLanguage()));
238238
logger.debug("Found matching application - ID: {}, Name: {}, Status: {}",
239239
app.getId(), app.getName(), app.getStatus());
240240
}
@@ -250,7 +250,7 @@ public List<ApplicationData> getApplications(String app_name) throws IOException
250250

251251

252252
@Tool(name = "list_all_applications", description = "Takes no argument and list all the applications")
253-
public List<ApplicationData> getActiveApplications() throws IOException {
253+
public List<ApplicationData> getAllApplications() throws IOException {
254254
logger.info("Listing all applications");
255255
ContrastSDK contrastSDK = SDKHelper.getSDK(hostName, apiKey, serviceKey, userName);
256256
try {
@@ -259,7 +259,8 @@ public List<ApplicationData> getActiveApplications() throws IOException {
259259

260260
List<ApplicationData> returnedApps = new ArrayList<>();
261261
for(Application app : applications) {
262-
returnedApps.add(new ApplicationData(app.getName(), app.getStatus(), app.getId()));
262+
returnedApps.add(new ApplicationData(app.getName(), app.getStatus(), app.getId(),
263+
app.getLastSeen(), app.getLanguage()));
263264
}
264265

265266
logger.info("Found {} applications", returnedApps.size());

src/main/java/com/contrast/labs/ai/mcp/contrast/McpContrastApplication.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ public static void main(String[] args) {
3333
}
3434

3535
@Bean
36-
public List<ToolCallback> tools(AssessService assessService, SastService sastService,SCAService scaService,ADRService adrService) {
37-
return of(ToolCallbacks.from(assessService,sastService,scaService,adrService));
36+
public List<ToolCallback> tools(AssessService assessService, SastService sastService,SCAService scaService,ADRService adrService,RouteCoverageService routeCoverageService) {
37+
return of(ToolCallbacks.from(assessService,sastService,scaService,adrService,routeCoverageService));
3838
}
3939

4040
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.contrast.labs.ai.mcp.contrast;
2+
3+
import com.contrast.labs.ai.mcp.contrast.sdkexstension.SDKExtension;
4+
import com.contrast.labs.ai.mcp.contrast.sdkexstension.SDKHelper;
5+
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.routecoverage.Route;
6+
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.routecoverage.RouteCoverageResponse;
7+
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.routecoverage.RouteDetailsResponse;
8+
import com.contrastsecurity.models.Application;
9+
import com.contrastsecurity.sdk.ContrastSDK;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
import org.springframework.ai.tool.annotation.Tool;
13+
import org.springframework.beans.factory.annotation.Value;
14+
import org.springframework.stereotype.Service;
15+
16+
import java.io.IOException;
17+
import java.util.Optional;
18+
19+
@Service
20+
public class RouteCoverageService {
21+
22+
private static final Logger logger = LoggerFactory.getLogger(RouteCoverageService.class);
23+
24+
25+
@Value("${contrast.host-name:${CONTRAST_HOST_NAME:}}")
26+
private String hostName;
27+
28+
@Value("${contrast.api-key:${CONTRAST_API_KEY:}}")
29+
private String apiKey;
30+
31+
@Value("${contrast.service-key:${CONTRAST_SERVICE_KEY:}}")
32+
private String serviceKey;
33+
34+
@Value("${contrast.username:${CONTRAST_USERNAME:}}")
35+
private String userName;
36+
37+
@Value("${contrast.org-id:${CONTRAST_ORG_ID:}}")
38+
private String orgID;
39+
40+
@Tool(name = "get_application_route_coverage", description = "takes a application name and return the route coverage data for that application. " +
41+
"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.")
42+
public RouteCoverageResponse getRouteCoverage(String app_name) throws IOException {
43+
logger.info("Retrieving route coverage for application by name: {}", app_name);
44+
ContrastSDK contrastSDK = SDKHelper.getSDK(hostName, apiKey, serviceKey, userName);
45+
SDKExtension sdkExtension = new SDKExtension(contrastSDK);
46+
Optional<String> appID = Optional.empty();
47+
logger.debug("Searching for application ID matching name: {}", app_name);
48+
49+
for(Application app : SDKHelper.getApplicationsWithCache(orgID, contrastSDK)) {
50+
if(app.getName().toLowerCase().contains(app_name.toLowerCase())) {
51+
appID = Optional.of(app.getId());
52+
logger.debug("Found matching application with ID: {}", appID.get());
53+
break;
54+
}
55+
}
56+
57+
if (!appID.isPresent()) {
58+
logger.error("Application not found: {}", app_name);
59+
throw new IOException("Application not found: " + app_name);
60+
}
61+
62+
logger.debug("Fetching route coverage data for application ID: {}", appID.get());
63+
RouteCoverageResponse response = sdkExtension.getRouteCoverage(orgID, appID.get(), null);
64+
logger.debug("Found {} routes for application", response.getRoutes().size());
65+
66+
logger.debug("Retrieving route details for each route");
67+
for(Route route : response.getRoutes()) {
68+
logger.trace("Fetching details for route: {}", route.getSignature());
69+
RouteDetailsResponse routeDetailsResponse = sdkExtension.getRouteDetails(orgID, appID.get(), route.getRouteHash());
70+
route.setRouteDetailsResponse(routeDetailsResponse);
71+
}
72+
73+
logger.info("Successfully retrieved route coverage for application: {}", app_name);
74+
return response;
75+
}
76+
77+
@Tool(name = "get_application_route_coverage_by_app_id", description = "takes a application id and return the route coverage data for that application. " +
78+
"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.")
79+
public RouteCoverageResponse getRouteCoverageByAppID(String app_id) throws IOException {
80+
logger.info("Retrieving route coverage for application by ID: {}", app_id);
81+
ContrastSDK contrastSDK = SDKHelper.getSDK(hostName, apiKey, serviceKey, userName);
82+
SDKExtension sdkExtension = new SDKExtension(contrastSDK);
83+
84+
logger.debug("Fetching route coverage data for application ID: {}", app_id);
85+
RouteCoverageResponse response = sdkExtension.getRouteCoverage(orgID, app_id, null);
86+
logger.debug("Found {} routes for application", response.getRoutes().size());
87+
88+
logger.debug("Retrieving route details for each route");
89+
for(Route route : response.getRoutes()) {
90+
logger.trace("Fetching details for route: {}", route.getSignature());
91+
RouteDetailsResponse routeDetailsResponse = sdkExtension.getRouteDetails(orgID, app_id, route.getRouteHash());
92+
route.setRouteDetailsResponse(routeDetailsResponse);
93+
}
94+
95+
logger.info("Successfully retrieved route coverage for application ID: {}", app_id);
96+
return response;
97+
}
98+
99+
100+
101+
102+
103+
104+
}

src/main/java/com/contrast/labs/ai/mcp/contrast/data/ApplicationData.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@
1515
*/
1616
package com.contrast.labs.ai.mcp.contrast.data;
1717

18-
public record ApplicationData(String name, String status, String appID) {
18+
public record ApplicationData(String name, String status, String appID, long lastSeen, String language) {
1919
}

src/main/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKExtension.java

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.LibrariesExtended;
2020
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.ProtectData;
2121
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.adr.AttackEvent;
22+
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.routecoverage.RouteCoverageResponse;
2223
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.sca.LibraryObservation;
2324
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.sca.LibraryObservationsResponse;
25+
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.routecoverage.RouteDetailsResponse;
2426
import com.contrastsecurity.exceptions.UnauthorizedException;
25-
import com.contrastsecurity.http.FilterForm;
26-
import com.contrastsecurity.http.HttpMethod;
27-
import com.contrastsecurity.http.LibraryFilterForm;
28-
import com.contrastsecurity.http.UrlBuilder;
27+
import com.contrastsecurity.http.*;
28+
import com.contrastsecurity.models.RouteCoverageBySessionIDAndMetadataRequest;
2929
import com.contrastsecurity.sdk.ContrastSDK;
3030
import com.contrastsecurity.sdk.internal.GsonFactory;
3131
import com.google.gson.Gson;
@@ -166,4 +166,71 @@ private String getLibraryObservationsUrl(String organizationId, String applicati
166166
organizationId, applicationId, libraryId, offset, limit);
167167
}
168168

169+
/**
170+
* Retrieves the detailed observations for a specific route.
171+
*
172+
* @param organizationId The organization ID
173+
* @param applicationId The application ID
174+
* @param routeHash The unique hash identifying the route
175+
* @return RouteDetailsResponse containing observations for the route
176+
* @throws IOException If an I/O error occurs
177+
* @throws UnauthorizedException If the request is not authorized
178+
*/
179+
public RouteDetailsResponse getRouteDetails(String organizationId, String applicationId, String routeHash)
180+
throws IOException, UnauthorizedException {
181+
String url = getRouteDetailsUrl(organizationId, applicationId, routeHash);
182+
183+
try (InputStream is = contrastSDK.makeRequest(HttpMethod.GET, url);
184+
Reader reader = new InputStreamReader(is)) {
185+
return gson.fromJson(reader, RouteDetailsResponse.class);
186+
}
187+
}
188+
189+
/**
190+
* Retrieves route coverage information for an application.
191+
*
192+
* @param organizationId The organization ID
193+
* @param appId The application ID
194+
* @param metadata Optional metadata request for filtering (can be null)
195+
* @return RouteCoverageResponse containing route coverage information
196+
* @throws IOException If an I/O error occurs
197+
* @throws UnauthorizedException If the request is not authorized
198+
*/
199+
public RouteCoverageResponse getRouteCoverage(String organizationId, String appId,
200+
RouteCoverageBySessionIDAndMetadataRequest metadata)
201+
throws IOException, UnauthorizedException {
202+
203+
InputStream is = null;
204+
205+
try {
206+
if (metadata == null) {
207+
is = contrastSDK.makeRequest(
208+
HttpMethod.GET,
209+
urlBuilder.getRouteCoverageUrl(organizationId, appId));
210+
} else {
211+
is = contrastSDK.makeRequestWithBody(
212+
HttpMethod.POST,
213+
urlBuilder.getRouteCoverageWithMetadataUrl(organizationId, appId),
214+
gson.toJson(metadata),
215+
MediaType.JSON);
216+
}
217+
218+
try (Reader reader = new InputStreamReader(is)) {
219+
return gson.fromJson(reader, RouteCoverageResponse.class);
220+
}
221+
} finally {
222+
if (is != null) {
223+
is.close();
224+
}
225+
}
226+
}
227+
228+
/**
229+
* Builds URL for retrieving route details observations
230+
*/
231+
private String getRouteDetailsUrl(String organizationId, String applicationId, String routeHash) {
232+
return String.format(
233+
"/ng/%s/applications/%s/route/%s/observations?expand=skip_links",
234+
organizationId, applicationId, routeHash);
235+
}
169236
}

0 commit comments

Comments
 (0)