diff --git a/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKExtension.java b/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKExtension.java index 0b21f8e..2d223fa 100644 --- a/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKExtension.java +++ b/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKExtension.java @@ -33,11 +33,16 @@ import com.contrastsecurity.sdk.ContrastSDK; import com.contrastsecurity.sdk.internal.GsonFactory; import com.google.gson.Gson; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -46,6 +51,7 @@ public class SDKExtension { private final ContrastSDK contrastSDK; private final UrlBuilder urlBuilder; private final Gson gson; + private static final Logger logger = LoggerFactory.getLogger(SDKExtension.class); public SDKExtension(ContrastSDK contrastSDK) { this.contrastSDK = contrastSDK; @@ -241,12 +247,36 @@ private String getRouteDetailsUrl(String organizationId, String applicationId, S public ApplicationsResponse getApplications(String organizationId) throws UnauthorizedException, IOException { String url = urlBuilder.getApplicationsUrl(organizationId)+"&expand=metadata,technologies,skip_links"; - try (InputStream is = - contrastSDK.makeRequest(HttpMethod.GET, url); - Reader reader = new InputStreamReader(is)) { + try (InputStream is = contrastSDK.makeRequest(HttpMethod.GET, url)) { + // Read the entire input stream into a string for logging + String responseContent = convertStreamToString(is); + + // Log the response content + logger.debug("Applications API response: {}", responseContent); + + // Convert the string back to a reader for GSON + Reader reader = new StringReader(responseContent); return this.gson.fromJson(reader, ApplicationsResponse.class); } } + + /** + * Converts an InputStream to String without closing the stream. + */ + private String convertStreamToString(InputStream is) throws IOException { + if (is == null) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + } + return sb.toString(); + } public Traces getTraces(String organizationId, String appId, TraceFilterBody filters) throws IOException, UnauthorizedException { diff --git a/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKHelper.java b/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKHelper.java index fefe32b..0f43c66 100644 --- a/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKHelper.java +++ b/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKHelper.java @@ -43,7 +43,7 @@ public class SDKHelper { private static final String MCP_SERVER_NAME = "contrast-mcp"; - private static final String MCP_VERSION = "0.0.9"; + private static final String MCP_VERSION = "0.0.10"; private static final Logger logger = LoggerFactory.getLogger(SDKHelper.class); @@ -150,18 +150,41 @@ public static List getLibraryObservationsWithCache( return getLibraryObservationsWithCache(libraryId, appId, orgId, 25, extendedSDK); } + /** + * Constructs a URL with protocol and server. + * If the hostname already contains a protocol (e.g., "https://host.com"), + * it returns the hostname as is. Otherwise, it prepends the protocol from properties. + * + * @param hostName The hostname, which may or may not include a protocol + * @return A URL with protocol and hostname + */ + public static String getProtocolAndServer(String hostName) { + if (hostName == null) { + return null; + } + + if (hostName.startsWith("http://") || hostName.startsWith("https://")) { + return hostName; + } + + String protocol = SDKHelper.environment.getProperty("contrast.api.protocol", "https"); + return protocol + "://" + hostName; + } + // The withUserAgentProduct will generate a user agent header that looks like // User-Agent: contrast-mcp/1.0 contrast-sdk-java/3.4.2 Java/19.0.2+7 public static ContrastSDK getSDK(String hostName, String apiKey, String serviceKey, String userName, String httpProxyHost, String httpProxyPort) { logger.info("Initializing ContrastSDK with username: {}, host: {}", userName, hostName); - + String baseUrl = getProtocolAndServer(hostName); + String apiUrl = baseUrl + "/Contrast/api"; + logger.info("API URL will be : {}", apiUrl); ContrastSDK.Builder builder = new ContrastSDK.Builder(userName, serviceKey, apiKey) - .withApiUrl(SDKHelper.environment.getProperty("contrast.api.protocol", "https") + "://" + hostName + "/Contrast/api") + .withApiUrl(apiUrl) .withUserAgentProduct(UserAgentProduct.of(MCP_SERVER_NAME, MCP_VERSION)); if (httpProxyHost != null && !httpProxyHost.isEmpty()) { int port = httpProxyPort != null && !httpProxyPort.isEmpty() ? Integer.parseInt(httpProxyPort) : 80; - logger.debug("Configuring HTTP proxy: {}:{}", httpProxyHost, port); + logger.info("Configuring HTTP proxy: {}:{}", httpProxyHost, port); java.net.Proxy proxy = new java.net.Proxy( java.net.Proxy.Type.HTTP, @@ -278,4 +301,4 @@ public static long clearAllCaches() { return totalCleared; } -} \ No newline at end of file +} diff --git a/src/test/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKHelperTest.java b/src/test/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKHelperTest.java new file mode 100644 index 0000000..70a7912 --- /dev/null +++ b/src/test/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/SDKHelperTest.java @@ -0,0 +1,83 @@ +package com.contrast.labs.ai.mcp.contrast.sdkexstension; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.core.env.Environment; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +class SDKHelperTest { + + @Mock + private Environment environment; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + + // Inject mocked Environment into SDKHelper's static field using reflection + Field envField = SDKHelper.class.getDeclaredField("environment"); + envField.setAccessible(true); + envField.set(null, environment); + + // Set up default property + when(environment.getProperty("contrast.api.protocol", "https")).thenReturn("https"); + } + + @Test + void testGetProtocolAndServer_WithNull() { + assertNull(SDKHelper.getProtocolAndServer(null)); + } + + @Test + void testGetProtocolAndServer_WithHttpProtocol() { + String result = SDKHelper.getProtocolAndServer("http://example.com"); + assertEquals("http://example.com", result); + } + + @Test + void testGetProtocolAndServer_WithHttpsProtocol() { + String result = SDKHelper.getProtocolAndServer("https://example.com"); + assertEquals("https://example.com", result); + } + + @Test + void testGetProtocolAndServer_WithoutProtocol() { + String result = SDKHelper.getProtocolAndServer("example.com"); + assertEquals("https://example.com", result); + } + + @Test + void testGetProtocolAndServer_WithCustomProtocol() { + when(environment.getProperty("contrast.api.protocol", "https")).thenReturn("http"); + + String result = SDKHelper.getProtocolAndServer("example.com"); + assertEquals("http://example.com", result); + } + + @Test + void testGetSDK_WithHttpsUrl() { + String hostWithProtocol = "https://custom.example.com"; + + // Use reflection to access the private method that builds the API URL + try { + Method method = SDKHelper.class.getDeclaredMethod("getSDK", String.class, String.class, String.class, String.class, String.class, String.class); + method.setAccessible(true); + + Object sdk = method.invoke(null, hostWithProtocol, "apiKey", "serviceKey", "username", null, null); + + assertNotNull(sdk); + // The actual URL validation would require accessing ContrastSDK's internal state, + // which is beyond the scope of a unit test. + // In a real application, you would use integration tests to verify this behavior. + } catch (Exception e) { + fail("Exception occurred: " + e.getMessage()); + } + } +}