From 3c683ba3840e0044714915a162507302b3f7360d Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 10:06:12 -0400 Subject: [PATCH 01/16] Add contrast-graph OpenAPI spec for model generation --- sdk/src/main/openapi/contrast-graph.yaml | 480 +++++++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 sdk/src/main/openapi/contrast-graph.yaml diff --git a/sdk/src/main/openapi/contrast-graph.yaml b/sdk/src/main/openapi/contrast-graph.yaml new file mode 100644 index 00000000..3de3b915 --- /dev/null +++ b/sdk/src/main/openapi/contrast-graph.yaml @@ -0,0 +1,480 @@ +openapi: "3.0.3" +info: + title: Contrast Graph API + description: > + SDK-focused spec for v2 Contrast Graph endpoints served by adr-explorer-aggregator. + TypedNode oneOf discriminator is intentionally flattened to GraphNode. + All enum fields use type:string — validation happens at the MCP tool layer. + version: "2.0.0" +paths: + /v2/organizations/{organizationId}/contrast-graph: + post: + operationId: searchGraph + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ContrastGraphRequest" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ContrastGraphResponse" + /v2/organizations/{organizationId}/contrast-graph/incidents/{incidentId}: + get: + operationId: getIncidentGraph + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + - name: incidentId + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ContrastGraphResponse" + /v2/organizations/{organizationId}/contrast-graph/facets/{filterName}: + post: + operationId: getFacets + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + - name: filterName + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/RequestFilters" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/FacetsResponse" + /v2/organizations/{organizationId}/contrast-graph/applications/{applicationId}/libraries: + post: + operationId: getApplicationLibraries + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + - name: applicationId + in: path + required: true + schema: + type: string + - name: agentReportingInstanceId + in: query + required: true + schema: + type: string + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/ApplicationLibrariesRequest" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ApplicationLibrariesResponse" + /v2/organizations/{organizationId}/contrast-graph/applications/{applicationId}/libraries/{libraryHash}: + get: + operationId: getApplicationLibraryDetails + parameters: + - name: organizationId + in: path + required: true + schema: + type: string + - name: applicationId + in: path + required: true + schema: + type: string + - name: libraryHash + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/LibraryDetailsResponse" +components: + schemas: + ContrastGraphRequest: + type: object + properties: + filters: + $ref: "#/components/schemas/RequestFilters" + RequestFilters: + type: object + properties: + nodeTypes: + type: array + items: + type: string + environments: + type: array + items: + type: string + languages: + type: array + items: + type: string + openIssueSeverities: + type: array + items: + type: string + openIncidentSeverities: + type: array + items: + type: string + applications: + type: array + items: + type: string + repositories: + type: array + items: + type: string + naturalSearch: + type: string + includeStaticOnly: + type: boolean + hasAiUsage: + type: boolean + ContrastGraphResponse: + type: object + properties: + nodes: + type: array + items: + $ref: "#/components/schemas/GraphNode" + edges: + type: array + items: + $ref: "#/components/schemas/GraphEdge" + graphAssets: + type: array + items: + $ref: "#/components/schemas/GraphAsset" + GraphNode: + type: object + required: + - id + - nodeType + properties: + id: + type: string + nodeType: + type: string + name: + type: string + attributes: + type: object + additionalProperties: + type: string + url: + type: string + hash: + type: string + language: + type: string + applicationId: + type: string + lastSeenTime: + type: string + format: date-time + issuesSeverityCount: + $ref: "#/components/schemas/SeverityCount" + incidentsSeverityCount: + $ref: "#/components/schemas/SeverityCount" + postureScore: + $ref: "#/components/schemas/PostureScore" + criticality: + type: string + assetId: + type: string + attackValue: + type: string + environment: + type: string + isSource: + type: boolean + actionType: + type: string + actionSinkResult: + type: string + serverName: + type: string + agentVersion: + type: string + GraphEdge: + type: object + properties: + sourceId: + type: string + targetId: + type: string + edgeType: + type: string + GraphAsset: + type: object + required: + - assetType + - nodeId + - assetName + - assetId + properties: + assetType: + type: string + nodeId: + type: string + assetName: + type: string + assetId: + type: string + criticality: + type: string + SeverityCount: + type: object + properties: + critical: + type: integer + format: int32 + high: + type: integer + format: int32 + medium: + type: integer + format: int32 + low: + type: integer + format: int32 + note: + type: integer + format: int32 + PostureScore: + type: object + properties: + score: + type: number + format: double + severity: + type: string + FacetsResponse: + type: object + properties: + nodeTypes: + type: array + items: + $ref: "#/components/schemas/Facet" + environments: + type: array + items: + $ref: "#/components/schemas/Facet" + languages: + type: array + items: + $ref: "#/components/schemas/Facet" + openIssueSeverities: + type: array + items: + $ref: "#/components/schemas/Facet" + openIncidentSeverities: + type: array + items: + $ref: "#/components/schemas/Facet" + applications: + type: array + items: + $ref: "#/components/schemas/Facet" + Facet: + type: object + required: + - value + properties: + label: + type: string + value: + type: string + count: + type: integer + format: int32 + ApplicationLibrariesRequest: + type: object + properties: + libraryHashes: + type: array + items: + type: string + ApplicationLibrariesResponse: + type: object + properties: + dependencies: + type: array + items: + $ref: "#/components/schemas/ExtendedDependency" + cves: + type: array + items: + $ref: "#/components/schemas/LibraryCve" + Dependency: + type: object + required: + - name + - version + - hash + properties: + name: + type: string + version: + type: string + hash: + type: string + ExtendedDependency: + type: object + required: + - name + - version + - hash + properties: + name: + type: string + version: + type: string + hash: + type: string + dependencies: + type: array + items: + $ref: "#/components/schemas/ExtendedDependency" + matchedInSearch: + type: boolean + LibraryCve: + type: object + required: + - cveId + - cisa + - description + properties: + cveId: + type: string + dependencyPath: + type: array + items: + $ref: "#/components/schemas/Dependency" + cvssScore: + type: number + format: double + nullable: true + cvssVector: + type: string + nullable: true + epssScore: + type: number + format: double + nullable: true + epssPercentile: + type: number + format: double + nullable: true + nvdModified: + type: string + format: date-time + nullable: true + nvdPublished: + type: string + format: date-time + nullable: true + cisa: + type: boolean + description: + type: string + matchedInSearch: + type: boolean + LibraryDetailsResponse: + type: object + required: + - name + - language + - version + - fileName + - hash + properties: + name: + type: string + group: + type: string + language: + type: string + version: + type: string + fileName: + type: string + hash: + type: string + releaseDate: + type: string + format: date-time + nullable: true + licenses: + type: array + items: + type: string + cves: + type: array + items: + $ref: "#/components/schemas/LibraryCve" + issues: + type: array + items: + $ref: "#/components/schemas/GraphIssue" + closestStableVersion: + $ref: "#/components/schemas/LibraryDetailsResponse" + latestStableVersion: + $ref: "#/components/schemas/LibraryDetailsResponse" + GraphIssue: + type: object + required: + - issueId + - title + properties: + issueId: + type: string + title: + type: string From 25172869792f42acf85347c7800be0c13fd13c31 Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 11:55:07 -0400 Subject: [PATCH 02/16] Configure openapi-generator-maven-plugin for contrast-graph model generation - Add openapi-generator-maven-plugin 7.9.0 to sdk/pom.xml generating 16 model classes into com.contrastsecurity.sdk.graph from contrast-graph.yaml - Add jsr305 and javax.annotation-api as provided deps for generated @Nullable and @Generated annotations - Upgrade Lombok to 1.18.30 for JDK 21 compatibility - Replace 8 with / so provided jars are visible to the compiler - Add minimal JSON.java stub providing JSON.getGson() for generated model classes Co-Authored-By: Claude Sonnet 4.6 (1M context) --- sdk/pom.xml | 47 ++++++++++++++++++- .../java/com/contrastsecurity/sdk/JSON.java | 36 ++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 sdk/src/main/java/com/contrastsecurity/sdk/JSON.java diff --git a/sdk/pom.xml b/sdk/pom.xml index 11004540..c654400f 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -51,8 +51,9 @@ http://localhost:19080/Contrast/api 5.7.1 1.8.2 - 1.18.18 + 1.18.30 2022 + 7.9.0 @@ -109,6 +110,18 @@ ${versions.auto-value} provided + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + javax.annotation + javax.annotation-api + 1.3.2 + provided + @@ -128,7 +141,8 @@ maven-compiler-plugin - 8 + 8 + 8 org.projectlombok @@ -219,6 +233,35 @@ + + org.openapitools + openapi-generator-maven-plugin + ${versions.openapi-generator} + + + generate-contrast-graph-models + + generate + + + ${project.basedir}/src/main/openapi/contrast-graph.yaml + java + okhttp-gson + false + false + false + false + com.contrastsecurity.sdk.graph + + false + true + true + false + + + + + diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java b/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java new file mode 100644 index 00000000..1564e3e5 --- /dev/null +++ b/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Contrast Security, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.contrastsecurity.sdk; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Minimal JSON utility used by generated graph model classes. + * + *

Provides a shared {@link Gson} instance configured for the Contrast Graph API. + */ +public class JSON { + + private static final Gson GSON = new GsonBuilder().create(); + + private JSON() {} + + public static Gson getGson() { + return GSON; + } +} From beb74ccd480315023c0d0d4f9ff8b1b7a49201b4 Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 11:58:47 -0400 Subject: [PATCH 03/16] Use dateLibrary=string to avoid OffsetDateTime GSON adapter requirement --- sdk/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/pom.xml b/sdk/pom.xml index c654400f..c1bbe865 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -257,6 +257,7 @@ true true false + string From 64a1976f7c27b367cbb9a34dbfa96445bdfae837 Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 12:28:18 -0400 Subject: [PATCH 04/16] Apply license header to JSON.java --- .../java/com/contrastsecurity/sdk/JSON.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java b/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java index 1564e3e5..94734c24 100644 --- a/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java @@ -16,6 +16,26 @@ package com.contrastsecurity.sdk; +/*- + * #%L + * Contrast Java SDK + * %% + * Copyright (C) 2022 - 2026 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import com.google.gson.Gson; import com.google.gson.GsonBuilder; From df73e9467fb6281cc68b897b3d1ec57b406b9e27 Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 12:46:59 -0400 Subject: [PATCH 05/16] Fix code quality issues: restore --release 8, optional annotation deps, GsonFactory delegation Co-Authored-By: Claude Sonnet 4.6 (1M context) --- sdk/pom.xml | 7 +++---- .../java/com/contrastsecurity/sdk/JSON.java | 17 ++++++++--------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/sdk/pom.xml b/sdk/pom.xml index c1bbe865..0c8004af 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -114,13 +114,13 @@ com.google.code.findbugs jsr305 3.0.2 - provided + true javax.annotation javax.annotation-api 1.3.2 - provided + true @@ -141,8 +141,7 @@ maven-compiler-plugin - 8 - 8 + 8 org.projectlombok diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java b/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java index 94734c24..a1e457a5 100644 --- a/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java @@ -25,9 +25,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -36,21 +36,20 @@ * #L% */ +import com.contrastsecurity.sdk.internal.GsonFactory; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; /** - * Minimal JSON utility used by generated graph model classes. + * Gson utility required by generated graph model classes (openapi-generator okhttp-gson output). * - *

Provides a shared {@link Gson} instance configured for the Contrast Graph API. + *

Delegates to {@link GsonFactory} so generated models share the same Gson configuration as the + * rest of the SDK. */ -public class JSON { - - private static final Gson GSON = new GsonBuilder().create(); +public final class JSON { private JSON() {} public static Gson getGson() { - return GSON; + return GsonFactory.create(); } } From 93950c2e213bffc65a3cb0449f2c70ffedd47189 Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 12:47:43 -0400 Subject: [PATCH 06/16] Add ContrastGraphApi interface --- .../sdk/graph/ContrastGraphApi.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApi.java diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApi.java b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApi.java new file mode 100644 index 00000000..2d483db5 --- /dev/null +++ b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApi.java @@ -0,0 +1,48 @@ +/*- + * #%L + * Contrast Java SDK + * %% + * Copyright (C) 2022 - 2026 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +package com.contrastsecurity.sdk.graph; + +import java.io.IOException; + +public interface ContrastGraphApi { + + ContrastGraphResponse searchGraph(String organizationId, ContrastGraphRequest request) + throws IOException; + + ContrastGraphResponse getIncidentGraph(String organizationId, String incidentId) + throws IOException; + + FacetsResponse getFacets(String organizationId, String filterName, RequestFilters filters) + throws IOException; + + ApplicationLibrariesResponse getApplicationLibraries( + String organizationId, + String applicationId, + String agentReportingInstanceId, + ApplicationLibrariesRequest request) + throws IOException; + + LibraryDetailsResponse getApplicationLibraryDetails( + String organizationId, + String applicationId, + String libraryHash) + throws IOException; +} From 5f05baf71c73002aa5dae2ab346cc6c4ef2e99f1 Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 12:48:52 -0400 Subject: [PATCH 07/16] Add ContrastGraphApiImpl --- .../sdk/graph/ContrastGraphApiImpl.java | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java new file mode 100644 index 00000000..3ea8f98f --- /dev/null +++ b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java @@ -0,0 +1,136 @@ +/*- + * #%L + * Contrast Java SDK + * %% + * Copyright (C) 2022 - 2026 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.contrastsecurity.sdk.graph; + +import com.contrastsecurity.exceptions.ServerResponseException; +import com.contrastsecurity.http.HttpMethod; +import com.contrastsecurity.http.MediaType; +import com.contrastsecurity.sdk.ContrastSDK; +import com.contrastsecurity.sdk.internal.URIBuilder; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; + +final class ContrastGraphApiImpl implements ContrastGraphApi { + + private final ContrastSDK contrast; + private final Gson gson; + + ContrastGraphApiImpl(final ContrastSDK contrast, final Gson gson) { + this.contrast = contrast; + this.gson = gson; + } + + @Override + public ContrastGraphResponse searchGraph( + final String organizationId, final ContrastGraphRequest request) throws IOException { + final String uri = new URIBuilder() + .appendPathSegments("v2", "organizations", organizationId, "contrast-graph") + .toURIString(); + try (InputStream is = contrast.makeRequestWithBody( + HttpMethod.POST, uri, gson.toJson(request), MediaType.JSON); + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return gson.fromJson(reader, ContrastGraphResponse.class); + } catch (JsonParseException e) { + throw new ServerResponseException("Failed to parse Contrast Graph API response", e); + } + } + + @Override + public ContrastGraphResponse getIncidentGraph( + final String organizationId, final String incidentId) throws IOException { + final String uri = new URIBuilder() + .appendPathSegments( + "v2", "organizations", organizationId, + "contrast-graph", "incidents", incidentId) + .toURIString(); + try (InputStream is = contrast.makeRequest(HttpMethod.GET, uri); + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return gson.fromJson(reader, ContrastGraphResponse.class); + } catch (JsonParseException e) { + throw new ServerResponseException("Failed to parse Contrast Graph API response", e); + } + } + + @Override + public FacetsResponse getFacets( + final String organizationId, + final String filterName, + final RequestFilters filters) throws IOException { + final String uri = new URIBuilder() + .appendPathSegments( + "v2", "organizations", organizationId, + "contrast-graph", "facets", filterName) + .toURIString(); + try (InputStream is = contrast.makeRequestWithBody( + HttpMethod.POST, uri, gson.toJson(filters), MediaType.JSON); + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return gson.fromJson(reader, FacetsResponse.class); + } catch (JsonParseException e) { + throw new ServerResponseException("Failed to parse Contrast Graph API response", e); + } + } + + @Override + public ApplicationLibrariesResponse getApplicationLibraries( + final String organizationId, + final String applicationId, + final String agentReportingInstanceId, + final ApplicationLibrariesRequest request) throws IOException { + final String uri = new URIBuilder() + .appendPathSegments( + "v2", "organizations", organizationId, + "contrast-graph", "applications", applicationId, "libraries") + .appendQueryParam("agentReportingInstanceId", agentReportingInstanceId) + .toURIString(); + final String body = request != null ? gson.toJson(request) : null; + final MediaType mediaType = request != null ? MediaType.JSON : null; + try (InputStream is = contrast.makeRequestWithBody(HttpMethod.POST, uri, body, mediaType); + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return gson.fromJson(reader, ApplicationLibrariesResponse.class); + } catch (JsonParseException e) { + throw new ServerResponseException("Failed to parse Contrast Graph API response", e); + } + } + + @Override + public LibraryDetailsResponse getApplicationLibraryDetails( + final String organizationId, + final String applicationId, + final String libraryHash) throws IOException { + final String uri = new URIBuilder() + .appendPathSegments( + "v2", "organizations", organizationId, + "contrast-graph", "applications", applicationId, + "libraries", libraryHash) + .toURIString(); + try (InputStream is = contrast.makeRequest(HttpMethod.GET, uri); + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return gson.fromJson(reader, LibraryDetailsResponse.class); + } catch (JsonParseException e) { + throw new ServerResponseException("Failed to parse Contrast Graph API response", e); + } + } +} From 7312665e481a98c2f7e7b68838f6c6cef242be27 Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 12:51:11 -0400 Subject: [PATCH 08/16] Expose ContrastGraphApi via ContrastSDK.graphApi() factory Make ContrastGraphApiImpl public (class and constructor) so it can be instantiated from the ContrastSDK.graphApi() factory method. This allows users to access the Graph API through the main SDK entry point. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../com/contrastsecurity/sdk/ContrastSDK.java | 6 +++ .../sdk/graph/ContrastGraphApiImpl.java | 4 +- .../graph/ContrastGraphApiFactoryTest.java | 39 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java b/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java index 6f516270..17c346bf 100755 --- a/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java @@ -81,6 +81,8 @@ import com.contrastsecurity.models.VulnerabilityTrend; import com.contrastsecurity.models.dtm.ApplicationCreateRequest; import com.contrastsecurity.models.dtm.AttestationCreateRequest; +import com.contrastsecurity.sdk.graph.ContrastGraphApi; +import com.contrastsecurity.sdk.graph.ContrastGraphApiImpl; import com.contrastsecurity.sdk.internal.GsonFactory; import com.contrastsecurity.sdk.scan.ScanManager; import com.contrastsecurity.sdk.scan.ScanManagerImpl; @@ -221,6 +223,10 @@ public ScanManager scan(final String organizationId) { return new ScanManagerImpl(this, gson, organizationId); } + public ContrastGraphApi graphApi() { + return new ContrastGraphApiImpl(this, this.gson); + } + /** * Gets the global properties from TeamServer. * diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java index 3ea8f98f..f6f4a88c 100644 --- a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java @@ -33,12 +33,12 @@ import java.io.Reader; import java.nio.charset.StandardCharsets; -final class ContrastGraphApiImpl implements ContrastGraphApi { +public final class ContrastGraphApiImpl implements ContrastGraphApi { private final ContrastSDK contrast; private final Gson gson; - ContrastGraphApiImpl(final ContrastSDK contrast, final Gson gson) { + public ContrastGraphApiImpl(final ContrastSDK contrast, final Gson gson) { this.contrast = contrast; this.gson = gson; } diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java new file mode 100644 index 00000000..fef2979a --- /dev/null +++ b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java @@ -0,0 +1,39 @@ +package com.contrastsecurity.sdk.graph; + +/*- + * #%L + * Contrast Java SDK + * %% + * Copyright (C) 2022 - 2026 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.contrastsecurity.sdk.ContrastSDK; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ContrastGraphApiFactoryTest { + + @Test + void graphApi_returns_non_null_instance() { + ContrastSDK sdk = + new ContrastSDK.Builder("user", "serviceKey", "apiKey") + .withApiUrl("http://localhost:8080/Contrast/api") + .build(); + ContrastGraphApi api = sdk.graphApi(); + assertThat(api).isNotNull(); + } +} From e5a88c31463558a17dd632d07980a196a716ae6f Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 12:54:21 -0400 Subject: [PATCH 09/16] Make ContrastGraphApiImpl package-private per spec Move implementation instantiation to a package-private factory class to enforce encapsulation. Implementation classes should not be directly instantiated outside their package. Changes: - Make ContrastGraphApiImpl class package-private (remove public) - Make ContrastGraphApiImpl constructor package-private (remove public) - Create ContrastGraphApiFactory as the public factory for instantiation - Update ContrastSDK.graphApi() to use the factory method Tests: ContrastGraphApiFactoryTest passes (1 test, 0 failures) --- .../com/contrastsecurity/sdk/ContrastSDK.java | 4 +-- .../sdk/graph/ContrastGraphApiFactory.java | 34 +++++++++++++++++++ .../sdk/graph/ContrastGraphApiImpl.java | 4 +-- 3 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactory.java diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java b/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java index 17c346bf..f8f5c4a2 100755 --- a/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java @@ -82,7 +82,7 @@ import com.contrastsecurity.models.dtm.ApplicationCreateRequest; import com.contrastsecurity.models.dtm.AttestationCreateRequest; import com.contrastsecurity.sdk.graph.ContrastGraphApi; -import com.contrastsecurity.sdk.graph.ContrastGraphApiImpl; +import com.contrastsecurity.sdk.graph.ContrastGraphApiFactory; import com.contrastsecurity.sdk.internal.GsonFactory; import com.contrastsecurity.sdk.scan.ScanManager; import com.contrastsecurity.sdk.scan.ScanManagerImpl; @@ -224,7 +224,7 @@ public ScanManager scan(final String organizationId) { } public ContrastGraphApi graphApi() { - return new ContrastGraphApiImpl(this, this.gson); + return ContrastGraphApiFactory.create(this, this.gson); } /** diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactory.java b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactory.java new file mode 100644 index 00000000..e1505180 --- /dev/null +++ b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactory.java @@ -0,0 +1,34 @@ +package com.contrastsecurity.sdk.graph; + +/*- + * #%L + * Contrast Java SDK + * %% + * Copyright (C) 2022 - 2026 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.contrastsecurity.sdk.ContrastSDK; +import com.google.gson.Gson; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ContrastGraphApiFactory { + + public static ContrastGraphApi create(final ContrastSDK contrast, final Gson gson) { + return new ContrastGraphApiImpl(contrast, gson); + } +} diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java index f6f4a88c..3ea8f98f 100644 --- a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java @@ -33,12 +33,12 @@ import java.io.Reader; import java.nio.charset.StandardCharsets; -public final class ContrastGraphApiImpl implements ContrastGraphApi { +final class ContrastGraphApiImpl implements ContrastGraphApi { private final ContrastSDK contrast; private final Gson gson; - public ContrastGraphApiImpl(final ContrastSDK contrast, final Gson gson) { + ContrastGraphApiImpl(final ContrastSDK contrast, final Gson gson) { this.contrast = contrast; this.gson = gson; } From 57afe1136fc1edd7207925b10b3018ec15f2bffa Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 12:55:14 -0400 Subject: [PATCH 10/16] Simplify graphApi(): remove factory class, instantiate impl directly --- .../com/contrastsecurity/sdk/ContrastSDK.java | 4 +-- .../sdk/graph/ContrastGraphApiFactory.java | 34 ------------------- .../sdk/graph/ContrastGraphApiImpl.java | 4 +-- 3 files changed, 4 insertions(+), 38 deletions(-) delete mode 100644 sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactory.java diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java b/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java index f8f5c4a2..17c346bf 100755 --- a/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java @@ -82,7 +82,7 @@ import com.contrastsecurity.models.dtm.ApplicationCreateRequest; import com.contrastsecurity.models.dtm.AttestationCreateRequest; import com.contrastsecurity.sdk.graph.ContrastGraphApi; -import com.contrastsecurity.sdk.graph.ContrastGraphApiFactory; +import com.contrastsecurity.sdk.graph.ContrastGraphApiImpl; import com.contrastsecurity.sdk.internal.GsonFactory; import com.contrastsecurity.sdk.scan.ScanManager; import com.contrastsecurity.sdk.scan.ScanManagerImpl; @@ -224,7 +224,7 @@ public ScanManager scan(final String organizationId) { } public ContrastGraphApi graphApi() { - return ContrastGraphApiFactory.create(this, this.gson); + return new ContrastGraphApiImpl(this, this.gson); } /** diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactory.java b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactory.java deleted file mode 100644 index e1505180..00000000 --- a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.contrastsecurity.sdk.graph; - -/*- - * #%L - * Contrast Java SDK - * %% - * Copyright (C) 2022 - 2026 Contrast Security, Inc. - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import com.contrastsecurity.sdk.ContrastSDK; -import com.google.gson.Gson; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class ContrastGraphApiFactory { - - public static ContrastGraphApi create(final ContrastSDK contrast, final Gson gson) { - return new ContrastGraphApiImpl(contrast, gson); - } -} diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java index 3ea8f98f..f6f4a88c 100644 --- a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java @@ -33,12 +33,12 @@ import java.io.Reader; import java.nio.charset.StandardCharsets; -final class ContrastGraphApiImpl implements ContrastGraphApi { +public final class ContrastGraphApiImpl implements ContrastGraphApi { private final ContrastSDK contrast; private final Gson gson; - ContrastGraphApiImpl(final ContrastSDK contrast, final Gson gson) { + public ContrastGraphApiImpl(final ContrastSDK contrast, final Gson gson) { this.contrast = contrast; this.gson = gson; } From b591cda56aba59ccfedf87372fb443509ffc1dda Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 15:55:02 -0400 Subject: [PATCH 11/16] Add Pact contract tests for ContrastGraphApiImpl Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../graph/ContrastGraphApiImplPactTest.java | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java new file mode 100644 index 00000000..2622457d --- /dev/null +++ b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java @@ -0,0 +1,246 @@ +/*- + * #%L + * Contrast Java SDK + * %% + * Copyright (C) 2022 - 2026 Contrast Security, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.contrastsecurity.sdk.graph; + +import au.com.dius.pact.consumer.MockServer; +import au.com.dius.pact.consumer.dsl.PactDslWithProvider; +import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; +import au.com.dius.pact.consumer.junit5.PactTestFor; +import au.com.dius.pact.core.model.RequestResponsePact; +import au.com.dius.pact.core.model.annotations.Pact; +import com.contrastsecurity.sdk.ContrastSDK; +import com.google.gson.Gson; +import org.json.JSONObject; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.io.IOException; +import java.util.Collections; + +import static au.com.dius.pact.consumer.dsl.LambdaDsl.newJsonBody; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(PactConsumerTestExt.class) +@PactTestFor(providerName = "adr-explorer-aggregator") +final class ContrastGraphApiImplPactTest { + + private static final String ORG_ID = "119844af-42ff-4293-b06b-81d426e9a4a9"; + private static final String APP_ID = "123e4567-e89b-12d3-a456-426614174000"; + private static final String INCIDENT_ID = "INC-2025-1"; + private static final String LIBRARY_HASH = + "3a42c503953909637f78dd8c99b3b85ddde362415585afc11901bdefe8349102"; + private static final String AGENT_INSTANCE_ID = "abc1234"; + + private ContrastGraphApiImpl client(final MockServer server) { + ContrastSDK sdk = new ContrastSDK.Builder("user", "serviceKey", "apiKey") + .withApiUrl(server.getUrl() + "/Contrast/api") + .build(); + return new ContrastGraphApiImpl(sdk, new Gson()); + } + + @Nested + final class SearchGraph { + + @Pact(consumer = "contrast-sdk") + RequestResponsePact pact(final PactDslWithProvider builder) { + return builder + .given("graph data exists for organization") + .uponReceiving("search graph with filters") + .method("POST") + .path("/Contrast/api/v2/organizations/" + ORG_ID + "/contrast-graph") + .willRespondWith() + .status(200) + .body(newJsonBody(body -> { + body.array("nodes", nodes -> nodes.object(node -> { + node.stringValue("id", "node-1"); + node.stringValue("nodeType", "APPLICATION"); + node.stringValue("name", "test-app"); + })); + body.array("edges", edges -> { + }); + body.array("graphAssets", assets -> assets.object(asset -> { + asset.stringValue("assetType", "APPLICATION"); + asset.stringValue("nodeId", "node-1"); + asset.stringValue("assetName", "test-app"); + asset.stringValue("assetId", APP_ID); + })); + }).build()) + .toPact(); + } + + @Test + void searchGraph(final MockServer server) throws IOException { + ContrastGraphRequest request = new ContrastGraphRequest(); + request.setFilters(new RequestFilters()); + + ContrastGraphResponse response = client(server).searchGraph(ORG_ID, request); + + assertThat(response.getNodes()).isNotEmpty(); + assertThat(response.getNodes().get(0).getId()).isEqualTo("node-1"); + assertThat(response.getNodes().get(0).getNodeType()).isEqualTo("APPLICATION"); + assertThat(response.getGraphAssets()).isNotEmpty(); + assertThat(response.getGraphAssets().get(0).getAssetId()).isEqualTo(APP_ID); + } + } + + @Nested + final class GetIncidentGraph { + + @Pact(consumer = "contrast-sdk") + RequestResponsePact pact(final PactDslWithProvider builder) { + return builder + .given("incident exists", + Collections.singletonMap("incidentId", INCIDENT_ID)) + .uponReceiving("get incident graph") + .method("GET") + .path("/Contrast/api/v2/organizations/" + ORG_ID + + "/contrast-graph/incidents/" + INCIDENT_ID) + .willRespondWith() + .status(200) + .body(newJsonBody(body -> { + body.array("nodes", nodes -> nodes.object(node -> { + node.stringValue("id", "incident-node-1"); + node.stringValue("nodeType", "ACTION"); + })); + body.array("edges", edges -> { + }); + }).build()) + .toPact(); + } + + @Test + void getIncidentGraph(final MockServer server) throws IOException { + ContrastGraphResponse response = client(server).getIncidentGraph(ORG_ID, INCIDENT_ID); + + assertThat(response.getNodes()).isNotEmpty(); + assertThat(response.getNodes().get(0).getId()).isEqualTo("incident-node-1"); + assertThat(response.getNodes().get(0).getNodeType()).isEqualTo("ACTION"); + } + } + + @Nested + final class GetFacets { + + @Pact(consumer = "contrast-sdk") + RequestResponsePact pact(final PactDslWithProvider builder) { + return builder + .given("graph data exists for organization") + .uponReceiving("get nodeTypes facets") + .method("POST") + .path("/Contrast/api/v2/organizations/" + ORG_ID + + "/contrast-graph/facets/nodeTypes") + .willRespondWith() + .status(200) + .body(newJsonBody(body -> + body.array("nodeTypes", facets -> facets.object(facet -> { + facet.stringValue("value", "APPLICATION"); + facet.numberValue("count", 5); + })) + ).build()) + .toPact(); + } + + @Test + void getFacets(final MockServer server) throws IOException { + FacetsResponse response = + client(server).getFacets(ORG_ID, "nodeTypes", new RequestFilters()); + + assertThat(response.getNodeTypes()).isNotEmpty(); + assertThat(response.getNodeTypes().get(0).getValue()).isEqualTo("APPLICATION"); + assertThat(response.getNodeTypes().get(0).getCount()).isEqualTo(5); + } + } + + @Nested + final class GetApplicationLibraries { + + @Pact(consumer = "contrast-sdk") + RequestResponsePact pact(final PactDslWithProvider builder) { + return builder + .given("application libraries exist") + .uponReceiving("get application libraries without filter") + .method("POST") + .path("/Contrast/api/v2/organizations/" + ORG_ID + + "/contrast-graph/applications/" + APP_ID + "/libraries") + .query("agentReportingInstanceId=" + AGENT_INSTANCE_ID) + .willRespondWith() + .status(200) + .body(newJsonBody(body -> { + body.array("dependencies", deps -> deps.object(dep -> { + dep.stringValue("name", "express"); + dep.stringValue("version", "4.18.2"); + dep.stringValue("hash", LIBRARY_HASH); + })); + body.array("cves", cves -> { + }); + }).build()) + .toPact(); + } + + @Test + void getApplicationLibraries(final MockServer server) throws IOException { + ApplicationLibrariesResponse response = + client(server).getApplicationLibraries( + ORG_ID, APP_ID, AGENT_INSTANCE_ID, null); + + assertThat(response.getDependencies()).isNotEmpty(); + assertThat(response.getDependencies().get(0).getName()).isEqualTo("express"); + assertThat(response.getDependencies().get(0).getVersion()).isEqualTo("4.18.2"); + assertThat(response.getDependencies().get(0).getHash()).isEqualTo(LIBRARY_HASH); + } + } + + @Nested + final class GetApplicationLibraryDetails { + + @Pact(consumer = "contrast-sdk") + RequestResponsePact pact(final PactDslWithProvider builder) { + return builder + .given("library exists") + .uponReceiving("get library details by hash") + .method("GET") + .path("/Contrast/api/v2/organizations/" + ORG_ID + + "/contrast-graph/applications/" + APP_ID + + "/libraries/" + LIBRARY_HASH) + .willRespondWith() + .status(200) + .body(newJsonBody(body -> { + body.stringValue("name", "express"); + body.stringValue("language", "node"); + body.stringValue("version", "4.18.2"); + body.stringValue("fileName", "express-4.18.2.tgz"); + body.stringValue("hash", LIBRARY_HASH); + }).build()) + .toPact(); + } + + @Test + void getApplicationLibraryDetails(final MockServer server) throws IOException { + LibraryDetailsResponse response = + client(server).getApplicationLibraryDetails(ORG_ID, APP_ID, LIBRARY_HASH); + + assertThat(response.getName()).isEqualTo("express"); + assertThat(response.getVersion()).isEqualTo("4.18.2"); + assertThat(response.getHash()).isEqualTo(LIBRARY_HASH); + assertThat(response.getLanguage()).isEqualTo("node"); + } + } +} From 439920cceab0eb30ceb4f4238065fba15510617e Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 16:10:11 -0400 Subject: [PATCH 12/16] Remove unused JSONObject import, strengthen factory test assertion --- .../contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java | 2 +- .../sdk/graph/ContrastGraphApiImplPactTest.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java index fef2979a..f3e3228a 100644 --- a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java +++ b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java @@ -34,6 +34,6 @@ void graphApi_returns_non_null_instance() { .withApiUrl("http://localhost:8080/Contrast/api") .build(); ContrastGraphApi api = sdk.graphApi(); - assertThat(api).isNotNull(); + assertThat(api).isNotNull().isInstanceOf(ContrastGraphApiImpl.class); } } diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java index 2622457d..78dd33c8 100644 --- a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java +++ b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java @@ -27,7 +27,6 @@ import au.com.dius.pact.core.model.annotations.Pact; import com.contrastsecurity.sdk.ContrastSDK; import com.google.gson.Gson; -import org.json.JSONObject; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; From 87996a9ee7960b3a444cbc56fb9270b070447a8d Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Mon, 8 Jun 2026 16:47:50 -0400 Subject: [PATCH 13/16] Apply Spotless formatting to hand-written graph API files --- .../java/com/contrastsecurity/sdk/JSON.java | 8 +- .../sdk/graph/ContrastGraphApi.java | 39 +- .../sdk/graph/ContrastGraphApiImpl.java | 185 ++++---- .../graph/ContrastGraphApiFactoryTest.java | 4 +- .../graph/ContrastGraphApiImplPactTest.java | 420 ++++++++++-------- 5 files changed, 352 insertions(+), 304 deletions(-) diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java b/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java index a1e457a5..02a91c8f 100644 --- a/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java @@ -47,9 +47,9 @@ */ public final class JSON { - private JSON() {} + private JSON() {} - public static Gson getGson() { - return GsonFactory.create(); - } + public static Gson getGson() { + return GsonFactory.create(); + } } diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApi.java b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApi.java index 2d483db5..c08b7666 100644 --- a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApi.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApi.java @@ -24,25 +24,22 @@ public interface ContrastGraphApi { - ContrastGraphResponse searchGraph(String organizationId, ContrastGraphRequest request) - throws IOException; - - ContrastGraphResponse getIncidentGraph(String organizationId, String incidentId) - throws IOException; - - FacetsResponse getFacets(String organizationId, String filterName, RequestFilters filters) - throws IOException; - - ApplicationLibrariesResponse getApplicationLibraries( - String organizationId, - String applicationId, - String agentReportingInstanceId, - ApplicationLibrariesRequest request) - throws IOException; - - LibraryDetailsResponse getApplicationLibraryDetails( - String organizationId, - String applicationId, - String libraryHash) - throws IOException; + ContrastGraphResponse searchGraph(String organizationId, ContrastGraphRequest request) + throws IOException; + + ContrastGraphResponse getIncidentGraph(String organizationId, String incidentId) + throws IOException; + + FacetsResponse getFacets(String organizationId, String filterName, RequestFilters filters) + throws IOException; + + ApplicationLibrariesResponse getApplicationLibraries( + String organizationId, + String applicationId, + String agentReportingInstanceId, + ApplicationLibrariesRequest request) + throws IOException; + + LibraryDetailsResponse getApplicationLibraryDetails( + String organizationId, String applicationId, String libraryHash) throws IOException; } diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java index f6f4a88c..7cdf8cc2 100644 --- a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java @@ -26,7 +26,6 @@ import com.contrastsecurity.sdk.internal.URIBuilder; import com.google.gson.Gson; import com.google.gson.JsonParseException; - import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -35,102 +34,116 @@ public final class ContrastGraphApiImpl implements ContrastGraphApi { - private final ContrastSDK contrast; - private final Gson gson; + private final ContrastSDK contrast; + private final Gson gson; - public ContrastGraphApiImpl(final ContrastSDK contrast, final Gson gson) { - this.contrast = contrast; - this.gson = gson; - } + public ContrastGraphApiImpl(final ContrastSDK contrast, final Gson gson) { + this.contrast = contrast; + this.gson = gson; + } - @Override - public ContrastGraphResponse searchGraph( - final String organizationId, final ContrastGraphRequest request) throws IOException { - final String uri = new URIBuilder() - .appendPathSegments("v2", "organizations", organizationId, "contrast-graph") - .toURIString(); - try (InputStream is = contrast.makeRequestWithBody( + @Override + public ContrastGraphResponse searchGraph( + final String organizationId, final ContrastGraphRequest request) throws IOException { + final String uri = + new URIBuilder() + .appendPathSegments("v2", "organizations", organizationId, "contrast-graph") + .toURIString(); + try (InputStream is = + contrast.makeRequestWithBody( HttpMethod.POST, uri, gson.toJson(request), MediaType.JSON); - Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { - return gson.fromJson(reader, ContrastGraphResponse.class); - } catch (JsonParseException e) { - throw new ServerResponseException("Failed to parse Contrast Graph API response", e); - } + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return gson.fromJson(reader, ContrastGraphResponse.class); + } catch (JsonParseException e) { + throw new ServerResponseException("Failed to parse Contrast Graph API response", e); } + } - @Override - public ContrastGraphResponse getIncidentGraph( - final String organizationId, final String incidentId) throws IOException { - final String uri = new URIBuilder() - .appendPathSegments( - "v2", "organizations", organizationId, - "contrast-graph", "incidents", incidentId) - .toURIString(); - try (InputStream is = contrast.makeRequest(HttpMethod.GET, uri); - Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { - return gson.fromJson(reader, ContrastGraphResponse.class); - } catch (JsonParseException e) { - throw new ServerResponseException("Failed to parse Contrast Graph API response", e); - } + @Override + public ContrastGraphResponse getIncidentGraph( + final String organizationId, final String incidentId) throws IOException { + final String uri = + new URIBuilder() + .appendPathSegments( + "v2", "organizations", organizationId, "contrast-graph", "incidents", incidentId) + .toURIString(); + try (InputStream is = contrast.makeRequest(HttpMethod.GET, uri); + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return gson.fromJson(reader, ContrastGraphResponse.class); + } catch (JsonParseException e) { + throw new ServerResponseException("Failed to parse Contrast Graph API response", e); } + } - @Override - public FacetsResponse getFacets( - final String organizationId, - final String filterName, - final RequestFilters filters) throws IOException { - final String uri = new URIBuilder() - .appendPathSegments( - "v2", "organizations", organizationId, - "contrast-graph", "facets", filterName) - .toURIString(); - try (InputStream is = contrast.makeRequestWithBody( + @Override + public FacetsResponse getFacets( + final String organizationId, final String filterName, final RequestFilters filters) + throws IOException { + final String uri = + new URIBuilder() + .appendPathSegments( + "v2", "organizations", organizationId, "contrast-graph", "facets", filterName) + .toURIString(); + try (InputStream is = + contrast.makeRequestWithBody( HttpMethod.POST, uri, gson.toJson(filters), MediaType.JSON); - Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { - return gson.fromJson(reader, FacetsResponse.class); - } catch (JsonParseException e) { - throw new ServerResponseException("Failed to parse Contrast Graph API response", e); - } + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return gson.fromJson(reader, FacetsResponse.class); + } catch (JsonParseException e) { + throw new ServerResponseException("Failed to parse Contrast Graph API response", e); } + } - @Override - public ApplicationLibrariesResponse getApplicationLibraries( - final String organizationId, - final String applicationId, - final String agentReportingInstanceId, - final ApplicationLibrariesRequest request) throws IOException { - final String uri = new URIBuilder() - .appendPathSegments( - "v2", "organizations", organizationId, - "contrast-graph", "applications", applicationId, "libraries") - .appendQueryParam("agentReportingInstanceId", agentReportingInstanceId) - .toURIString(); - final String body = request != null ? gson.toJson(request) : null; - final MediaType mediaType = request != null ? MediaType.JSON : null; - try (InputStream is = contrast.makeRequestWithBody(HttpMethod.POST, uri, body, mediaType); - Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { - return gson.fromJson(reader, ApplicationLibrariesResponse.class); - } catch (JsonParseException e) { - throw new ServerResponseException("Failed to parse Contrast Graph API response", e); - } + @Override + public ApplicationLibrariesResponse getApplicationLibraries( + final String organizationId, + final String applicationId, + final String agentReportingInstanceId, + final ApplicationLibrariesRequest request) + throws IOException { + final String uri = + new URIBuilder() + .appendPathSegments( + "v2", + "organizations", + organizationId, + "contrast-graph", + "applications", + applicationId, + "libraries") + .appendQueryParam("agentReportingInstanceId", agentReportingInstanceId) + .toURIString(); + final String body = request != null ? gson.toJson(request) : null; + final MediaType mediaType = request != null ? MediaType.JSON : null; + try (InputStream is = contrast.makeRequestWithBody(HttpMethod.POST, uri, body, mediaType); + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return gson.fromJson(reader, ApplicationLibrariesResponse.class); + } catch (JsonParseException e) { + throw new ServerResponseException("Failed to parse Contrast Graph API response", e); } + } - @Override - public LibraryDetailsResponse getApplicationLibraryDetails( - final String organizationId, - final String applicationId, - final String libraryHash) throws IOException { - final String uri = new URIBuilder() - .appendPathSegments( - "v2", "organizations", organizationId, - "contrast-graph", "applications", applicationId, - "libraries", libraryHash) - .toURIString(); - try (InputStream is = contrast.makeRequest(HttpMethod.GET, uri); - Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { - return gson.fromJson(reader, LibraryDetailsResponse.class); - } catch (JsonParseException e) { - throw new ServerResponseException("Failed to parse Contrast Graph API response", e); - } + @Override + public LibraryDetailsResponse getApplicationLibraryDetails( + final String organizationId, final String applicationId, final String libraryHash) + throws IOException { + final String uri = + new URIBuilder() + .appendPathSegments( + "v2", + "organizations", + organizationId, + "contrast-graph", + "applications", + applicationId, + "libraries", + libraryHash) + .toURIString(); + try (InputStream is = contrast.makeRequest(HttpMethod.GET, uri); + Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return gson.fromJson(reader, LibraryDetailsResponse.class); + } catch (JsonParseException e) { + throw new ServerResponseException("Failed to parse Contrast Graph API response", e); } + } } diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java index f3e3228a..785663c3 100644 --- a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java +++ b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java @@ -20,11 +20,11 @@ * #L% */ +import static org.assertj.core.api.Assertions.assertThat; + import com.contrastsecurity.sdk.ContrastSDK; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; - class ContrastGraphApiFactoryTest { @Test diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java index 78dd33c8..03d2a359 100644 --- a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java +++ b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java @@ -19,6 +19,9 @@ */ package com.contrastsecurity.sdk.graph; +import static au.com.dius.pact.consumer.dsl.LambdaDsl.newJsonBody; +import static org.assertj.core.api.Assertions.assertThat; + import au.com.dius.pact.consumer.MockServer; import au.com.dius.pact.consumer.dsl.PactDslWithProvider; import au.com.dius.pact.consumer.junit5.PactConsumerTestExt; @@ -27,219 +30,254 @@ import au.com.dius.pact.core.model.annotations.Pact; import com.contrastsecurity.sdk.ContrastSDK; import com.google.gson.Gson; +import java.io.IOException; +import java.util.Collections; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import java.io.IOException; -import java.util.Collections; - -import static au.com.dius.pact.consumer.dsl.LambdaDsl.newJsonBody; -import static org.assertj.core.api.Assertions.assertThat; - @ExtendWith(PactConsumerTestExt.class) @PactTestFor(providerName = "adr-explorer-aggregator") final class ContrastGraphApiImplPactTest { - private static final String ORG_ID = "119844af-42ff-4293-b06b-81d426e9a4a9"; - private static final String APP_ID = "123e4567-e89b-12d3-a456-426614174000"; - private static final String INCIDENT_ID = "INC-2025-1"; - private static final String LIBRARY_HASH = - "3a42c503953909637f78dd8c99b3b85ddde362415585afc11901bdefe8349102"; - private static final String AGENT_INSTANCE_ID = "abc1234"; - - private ContrastGraphApiImpl client(final MockServer server) { - ContrastSDK sdk = new ContrastSDK.Builder("user", "serviceKey", "apiKey") - .withApiUrl(server.getUrl() + "/Contrast/api") - .build(); - return new ContrastGraphApiImpl(sdk, new Gson()); + private static final String ORG_ID = "119844af-42ff-4293-b06b-81d426e9a4a9"; + private static final String APP_ID = "123e4567-e89b-12d3-a456-426614174000"; + private static final String INCIDENT_ID = "INC-2025-1"; + private static final String LIBRARY_HASH = + "3a42c503953909637f78dd8c99b3b85ddde362415585afc11901bdefe8349102"; + private static final String AGENT_INSTANCE_ID = "abc1234"; + + private ContrastGraphApiImpl client(final MockServer server) { + ContrastSDK sdk = + new ContrastSDK.Builder("user", "serviceKey", "apiKey") + .withApiUrl(server.getUrl() + "/Contrast/api") + .build(); + return new ContrastGraphApiImpl(sdk, new Gson()); + } + + @Nested + final class SearchGraph { + + @Pact(consumer = "contrast-sdk") + RequestResponsePact pact(final PactDslWithProvider builder) { + return builder + .given("graph data exists for organization") + .uponReceiving("search graph with filters") + .method("POST") + .path("/Contrast/api/v2/organizations/" + ORG_ID + "/contrast-graph") + .willRespondWith() + .status(200) + .body( + newJsonBody( + body -> { + body.array( + "nodes", + nodes -> + nodes.object( + node -> { + node.stringValue("id", "node-1"); + node.stringValue("nodeType", "APPLICATION"); + node.stringValue("name", "test-app"); + })); + body.array("edges", edges -> {}); + body.array( + "graphAssets", + assets -> + assets.object( + asset -> { + asset.stringValue("assetType", "APPLICATION"); + asset.stringValue("nodeId", "node-1"); + asset.stringValue("assetName", "test-app"); + asset.stringValue("assetId", APP_ID); + })); + }) + .build()) + .toPact(); + } + + @Test + void searchGraph(final MockServer server) throws IOException { + ContrastGraphRequest request = new ContrastGraphRequest(); + request.setFilters(new RequestFilters()); + + ContrastGraphResponse response = client(server).searchGraph(ORG_ID, request); + + assertThat(response.getNodes()).isNotEmpty(); + assertThat(response.getNodes().get(0).getId()).isEqualTo("node-1"); + assertThat(response.getNodes().get(0).getNodeType()).isEqualTo("APPLICATION"); + assertThat(response.getGraphAssets()).isNotEmpty(); + assertThat(response.getGraphAssets().get(0).getAssetId()).isEqualTo(APP_ID); + } + } + + @Nested + final class GetIncidentGraph { + + @Pact(consumer = "contrast-sdk") + RequestResponsePact pact(final PactDslWithProvider builder) { + return builder + .given("incident exists", Collections.singletonMap("incidentId", INCIDENT_ID)) + .uponReceiving("get incident graph") + .method("GET") + .path( + "/Contrast/api/v2/organizations/" + + ORG_ID + + "/contrast-graph/incidents/" + + INCIDENT_ID) + .willRespondWith() + .status(200) + .body( + newJsonBody( + body -> { + body.array( + "nodes", + nodes -> + nodes.object( + node -> { + node.stringValue("id", "incident-node-1"); + node.stringValue("nodeType", "ACTION"); + })); + body.array("edges", edges -> {}); + }) + .build()) + .toPact(); + } + + @Test + void getIncidentGraph(final MockServer server) throws IOException { + ContrastGraphResponse response = client(server).getIncidentGraph(ORG_ID, INCIDENT_ID); + + assertThat(response.getNodes()).isNotEmpty(); + assertThat(response.getNodes().get(0).getId()).isEqualTo("incident-node-1"); + assertThat(response.getNodes().get(0).getNodeType()).isEqualTo("ACTION"); } + } - @Nested - final class SearchGraph { - - @Pact(consumer = "contrast-sdk") - RequestResponsePact pact(final PactDslWithProvider builder) { - return builder - .given("graph data exists for organization") - .uponReceiving("search graph with filters") - .method("POST") - .path("/Contrast/api/v2/organizations/" + ORG_ID + "/contrast-graph") - .willRespondWith() - .status(200) - .body(newJsonBody(body -> { - body.array("nodes", nodes -> nodes.object(node -> { - node.stringValue("id", "node-1"); - node.stringValue("nodeType", "APPLICATION"); - node.stringValue("name", "test-app"); - })); - body.array("edges", edges -> { - }); - body.array("graphAssets", assets -> assets.object(asset -> { - asset.stringValue("assetType", "APPLICATION"); - asset.stringValue("nodeId", "node-1"); - asset.stringValue("assetName", "test-app"); - asset.stringValue("assetId", APP_ID); - })); - }).build()) - .toPact(); - } - - @Test - void searchGraph(final MockServer server) throws IOException { - ContrastGraphRequest request = new ContrastGraphRequest(); - request.setFilters(new RequestFilters()); - - ContrastGraphResponse response = client(server).searchGraph(ORG_ID, request); - - assertThat(response.getNodes()).isNotEmpty(); - assertThat(response.getNodes().get(0).getId()).isEqualTo("node-1"); - assertThat(response.getNodes().get(0).getNodeType()).isEqualTo("APPLICATION"); - assertThat(response.getGraphAssets()).isNotEmpty(); - assertThat(response.getGraphAssets().get(0).getAssetId()).isEqualTo(APP_ID); - } + @Nested + final class GetFacets { + + @Pact(consumer = "contrast-sdk") + RequestResponsePact pact(final PactDslWithProvider builder) { + return builder + .given("graph data exists for organization") + .uponReceiving("get nodeTypes facets") + .method("POST") + .path("/Contrast/api/v2/organizations/" + ORG_ID + "/contrast-graph/facets/nodeTypes") + .willRespondWith() + .status(200) + .body( + newJsonBody( + body -> + body.array( + "nodeTypes", + facets -> + facets.object( + facet -> { + facet.stringValue("value", "APPLICATION"); + facet.numberValue("count", 5); + }))) + .build()) + .toPact(); } - @Nested - final class GetIncidentGraph { - - @Pact(consumer = "contrast-sdk") - RequestResponsePact pact(final PactDslWithProvider builder) { - return builder - .given("incident exists", - Collections.singletonMap("incidentId", INCIDENT_ID)) - .uponReceiving("get incident graph") - .method("GET") - .path("/Contrast/api/v2/organizations/" + ORG_ID - + "/contrast-graph/incidents/" + INCIDENT_ID) - .willRespondWith() - .status(200) - .body(newJsonBody(body -> { - body.array("nodes", nodes -> nodes.object(node -> { - node.stringValue("id", "incident-node-1"); - node.stringValue("nodeType", "ACTION"); - })); - body.array("edges", edges -> { - }); - }).build()) - .toPact(); - } - - @Test - void getIncidentGraph(final MockServer server) throws IOException { - ContrastGraphResponse response = client(server).getIncidentGraph(ORG_ID, INCIDENT_ID); - - assertThat(response.getNodes()).isNotEmpty(); - assertThat(response.getNodes().get(0).getId()).isEqualTo("incident-node-1"); - assertThat(response.getNodes().get(0).getNodeType()).isEqualTo("ACTION"); - } + @Test + void getFacets(final MockServer server) throws IOException { + FacetsResponse response = client(server).getFacets(ORG_ID, "nodeTypes", new RequestFilters()); + + assertThat(response.getNodeTypes()).isNotEmpty(); + assertThat(response.getNodeTypes().get(0).getValue()).isEqualTo("APPLICATION"); + assertThat(response.getNodeTypes().get(0).getCount()).isEqualTo(5); } + } - @Nested - final class GetFacets { - - @Pact(consumer = "contrast-sdk") - RequestResponsePact pact(final PactDslWithProvider builder) { - return builder - .given("graph data exists for organization") - .uponReceiving("get nodeTypes facets") - .method("POST") - .path("/Contrast/api/v2/organizations/" + ORG_ID - + "/contrast-graph/facets/nodeTypes") - .willRespondWith() - .status(200) - .body(newJsonBody(body -> - body.array("nodeTypes", facets -> facets.object(facet -> { - facet.stringValue("value", "APPLICATION"); - facet.numberValue("count", 5); - })) - ).build()) - .toPact(); - } - - @Test - void getFacets(final MockServer server) throws IOException { - FacetsResponse response = - client(server).getFacets(ORG_ID, "nodeTypes", new RequestFilters()); - - assertThat(response.getNodeTypes()).isNotEmpty(); - assertThat(response.getNodeTypes().get(0).getValue()).isEqualTo("APPLICATION"); - assertThat(response.getNodeTypes().get(0).getCount()).isEqualTo(5); - } + @Nested + final class GetApplicationLibraries { + + @Pact(consumer = "contrast-sdk") + RequestResponsePact pact(final PactDslWithProvider builder) { + return builder + .given("application libraries exist") + .uponReceiving("get application libraries without filter") + .method("POST") + .path( + "/Contrast/api/v2/organizations/" + + ORG_ID + + "/contrast-graph/applications/" + + APP_ID + + "/libraries") + .query("agentReportingInstanceId=" + AGENT_INSTANCE_ID) + .willRespondWith() + .status(200) + .body( + newJsonBody( + body -> { + body.array( + "dependencies", + deps -> + deps.object( + dep -> { + dep.stringValue("name", "express"); + dep.stringValue("version", "4.18.2"); + dep.stringValue("hash", LIBRARY_HASH); + })); + body.array("cves", cves -> {}); + }) + .build()) + .toPact(); } - @Nested - final class GetApplicationLibraries { - - @Pact(consumer = "contrast-sdk") - RequestResponsePact pact(final PactDslWithProvider builder) { - return builder - .given("application libraries exist") - .uponReceiving("get application libraries without filter") - .method("POST") - .path("/Contrast/api/v2/organizations/" + ORG_ID - + "/contrast-graph/applications/" + APP_ID + "/libraries") - .query("agentReportingInstanceId=" + AGENT_INSTANCE_ID) - .willRespondWith() - .status(200) - .body(newJsonBody(body -> { - body.array("dependencies", deps -> deps.object(dep -> { - dep.stringValue("name", "express"); - dep.stringValue("version", "4.18.2"); - dep.stringValue("hash", LIBRARY_HASH); - })); - body.array("cves", cves -> { - }); - }).build()) - .toPact(); - } - - @Test - void getApplicationLibraries(final MockServer server) throws IOException { - ApplicationLibrariesResponse response = - client(server).getApplicationLibraries( - ORG_ID, APP_ID, AGENT_INSTANCE_ID, null); - - assertThat(response.getDependencies()).isNotEmpty(); - assertThat(response.getDependencies().get(0).getName()).isEqualTo("express"); - assertThat(response.getDependencies().get(0).getVersion()).isEqualTo("4.18.2"); - assertThat(response.getDependencies().get(0).getHash()).isEqualTo(LIBRARY_HASH); - } + @Test + void getApplicationLibraries(final MockServer server) throws IOException { + ApplicationLibrariesResponse response = + client(server).getApplicationLibraries(ORG_ID, APP_ID, AGENT_INSTANCE_ID, null); + + assertThat(response.getDependencies()).isNotEmpty(); + assertThat(response.getDependencies().get(0).getName()).isEqualTo("express"); + assertThat(response.getDependencies().get(0).getVersion()).isEqualTo("4.18.2"); + assertThat(response.getDependencies().get(0).getHash()).isEqualTo(LIBRARY_HASH); } + } - @Nested - final class GetApplicationLibraryDetails { - - @Pact(consumer = "contrast-sdk") - RequestResponsePact pact(final PactDslWithProvider builder) { - return builder - .given("library exists") - .uponReceiving("get library details by hash") - .method("GET") - .path("/Contrast/api/v2/organizations/" + ORG_ID - + "/contrast-graph/applications/" + APP_ID - + "/libraries/" + LIBRARY_HASH) - .willRespondWith() - .status(200) - .body(newJsonBody(body -> { + @Nested + final class GetApplicationLibraryDetails { + + @Pact(consumer = "contrast-sdk") + RequestResponsePact pact(final PactDslWithProvider builder) { + return builder + .given("library exists") + .uponReceiving("get library details by hash") + .method("GET") + .path( + "/Contrast/api/v2/organizations/" + + ORG_ID + + "/contrast-graph/applications/" + + APP_ID + + "/libraries/" + + LIBRARY_HASH) + .willRespondWith() + .status(200) + .body( + newJsonBody( + body -> { body.stringValue("name", "express"); body.stringValue("language", "node"); body.stringValue("version", "4.18.2"); body.stringValue("fileName", "express-4.18.2.tgz"); body.stringValue("hash", LIBRARY_HASH); - }).build()) - .toPact(); - } - - @Test - void getApplicationLibraryDetails(final MockServer server) throws IOException { - LibraryDetailsResponse response = - client(server).getApplicationLibraryDetails(ORG_ID, APP_ID, LIBRARY_HASH); - - assertThat(response.getName()).isEqualTo("express"); - assertThat(response.getVersion()).isEqualTo("4.18.2"); - assertThat(response.getHash()).isEqualTo(LIBRARY_HASH); - assertThat(response.getLanguage()).isEqualTo("node"); - } + }) + .build()) + .toPact(); + } + + @Test + void getApplicationLibraryDetails(final MockServer server) throws IOException { + LibraryDetailsResponse response = + client(server).getApplicationLibraryDetails(ORG_ID, APP_ID, LIBRARY_HASH); + + assertThat(response.getName()).isEqualTo("express"); + assertThat(response.getVersion()).isEqualTo("4.18.2"); + assertThat(response.getHash()).isEqualTo(LIBRARY_HASH); + assertThat(response.getLanguage()).isEqualTo("node"); } + } } From f548be963a545f54766214c0a302e08a51f982c8 Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Wed, 10 Jun 2026 14:52:42 -0400 Subject: [PATCH 14/16] Increase code reuse, for new graph APIs reuse existing code. --- .../com/contrastsecurity/sdk/ContrastSDK.java | 34 ++++ .../sdk/graph/ContrastGraphApiImpl.java | 149 +++++++++--------- .../utils/ContrastSDKUtils.java | 13 ++ .../contrastsecurity/sdk/ContrastSDKTest.java | 146 +++++++++++++++++ .../graph/ContrastGraphApiImplPactTest.java | 13 +- .../utils/ContrastSDKUtilsTest.java | 22 +++ 6 files changed, 300 insertions(+), 77 deletions(-) diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java b/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java index 17c346bf..bd1e714c 100755 --- a/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/ContrastSDK.java @@ -1593,6 +1593,40 @@ public InputStream makeRequest(HttpMethod method, String path) return makeRequestWithResponse(method, path).is; } + public InputStream makeRequestToUrl(HttpMethod method, String url) + throws IOException, UnauthorizedException { + HttpURLConnection connection = makeConnection(url, method.toString()); + int rc = connection.getResponseCode(); + if (rc >= HttpURLConnection.HTTP_BAD_REQUEST) { + throw HttpResponseException.fromConnection( + connection, "Received unexpected status code from Contrast"); + } + return connection.getInputStream(); + } + + public InputStream makeRequestWithBodyToUrl( + HttpMethod method, String url, String body, MediaType mediaType) + throws IOException, UnauthorizedException { + HttpURLConnection connection = makeConnection(url, method.toString()); + if (mediaType != null + && body != null + && (method.equals(HttpMethod.PUT) + || method.equals(HttpMethod.POST) + || method.equals(HttpMethod.DELETE))) { + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", mediaType.getType()); + try (OutputStream os = connection.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + } + int rc = connection.getResponseCode(); + if (rc >= HttpURLConnection.HTTP_BAD_REQUEST) { + throw HttpResponseException.fromConnection( + connection, "Received unexpected status code from Contrast"); + } + return connection.getInputStream(); + } + public MakeRequestResponse makeRequestWithResponse(HttpMethod method, String path) throws IOException, UnauthorizedException { String url = restApiURL + path; diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java index 7cdf8cc2..1b85be48 100644 --- a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java @@ -24,6 +24,7 @@ import com.contrastsecurity.http.MediaType; import com.contrastsecurity.sdk.ContrastSDK; import com.contrastsecurity.sdk.internal.URIBuilder; +import com.contrastsecurity.utils.ContrastSDKUtils; import com.google.gson.Gson; import com.google.gson.JsonParseException; import java.io.IOException; @@ -36,62 +37,58 @@ public final class ContrastGraphApiImpl implements ContrastGraphApi { private final ContrastSDK contrast; private final Gson gson; + private final String graphApiBase; public ContrastGraphApiImpl(final ContrastSDK contrast, final Gson gson) { this.contrast = contrast; this.gson = gson; + this.graphApiBase = ContrastSDKUtils.getServerUrl(contrast.getRestApiURL()) + "/api"; } @Override public ContrastGraphResponse searchGraph( final String organizationId, final ContrastGraphRequest request) throws IOException { - final String uri = - new URIBuilder() - .appendPathSegments("v2", "organizations", organizationId, "contrast-graph") - .toURIString(); - try (InputStream is = - contrast.makeRequestWithBody( - HttpMethod.POST, uri, gson.toJson(request), MediaType.JSON); - Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { - return gson.fromJson(reader, ContrastGraphResponse.class); - } catch (JsonParseException e) { - throw new ServerResponseException("Failed to parse Contrast Graph API response", e); - } + final String url = + graphApiBase + + new URIBuilder() + .appendPathSegments("v2", "organizations", organizationId, "contrast-graph") + .toURIString(); + return post(url, gson.toJson(request), ContrastGraphResponse.class); } @Override public ContrastGraphResponse getIncidentGraph( final String organizationId, final String incidentId) throws IOException { - final String uri = - new URIBuilder() - .appendPathSegments( - "v2", "organizations", organizationId, "contrast-graph", "incidents", incidentId) - .toURIString(); - try (InputStream is = contrast.makeRequest(HttpMethod.GET, uri); - Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { - return gson.fromJson(reader, ContrastGraphResponse.class); - } catch (JsonParseException e) { - throw new ServerResponseException("Failed to parse Contrast Graph API response", e); - } + final String url = + graphApiBase + + new URIBuilder() + .appendPathSegments( + "v2", + "organizations", + organizationId, + "contrast-graph", + "incidents", + incidentId) + .toURIString(); + return get(url, ContrastGraphResponse.class); } @Override public FacetsResponse getFacets( final String organizationId, final String filterName, final RequestFilters filters) throws IOException { - final String uri = - new URIBuilder() - .appendPathSegments( - "v2", "organizations", organizationId, "contrast-graph", "facets", filterName) - .toURIString(); - try (InputStream is = - contrast.makeRequestWithBody( - HttpMethod.POST, uri, gson.toJson(filters), MediaType.JSON); - Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { - return gson.fromJson(reader, FacetsResponse.class); - } catch (JsonParseException e) { - throw new ServerResponseException("Failed to parse Contrast Graph API response", e); - } + final String url = + graphApiBase + + new URIBuilder() + .appendPathSegments( + "v2", + "organizations", + organizationId, + "contrast-graph", + "facets", + filterName) + .toURIString(); + return post(url, gson.toJson(filters), FacetsResponse.class); } @Override @@ -101,47 +98,57 @@ public ApplicationLibrariesResponse getApplicationLibraries( final String agentReportingInstanceId, final ApplicationLibrariesRequest request) throws IOException { - final String uri = - new URIBuilder() - .appendPathSegments( - "v2", - "organizations", - organizationId, - "contrast-graph", - "applications", - applicationId, - "libraries") - .appendQueryParam("agentReportingInstanceId", agentReportingInstanceId) - .toURIString(); - final String body = request != null ? gson.toJson(request) : null; - final MediaType mediaType = request != null ? MediaType.JSON : null; - try (InputStream is = contrast.makeRequestWithBody(HttpMethod.POST, uri, body, mediaType); - Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { - return gson.fromJson(reader, ApplicationLibrariesResponse.class); - } catch (JsonParseException e) { - throw new ServerResponseException("Failed to parse Contrast Graph API response", e); - } + final String url = + graphApiBase + + new URIBuilder() + .appendPathSegments( + "v2", + "organizations", + organizationId, + "contrast-graph", + "applications", + applicationId, + "libraries") + .appendQueryParam("agentReportingInstanceId", agentReportingInstanceId) + .toURIString(); + return post(url, request != null ? gson.toJson(request) : null, ApplicationLibrariesResponse.class); } @Override public LibraryDetailsResponse getApplicationLibraryDetails( final String organizationId, final String applicationId, final String libraryHash) throws IOException { - final String uri = - new URIBuilder() - .appendPathSegments( - "v2", - "organizations", - organizationId, - "contrast-graph", - "applications", - applicationId, - "libraries", - libraryHash) - .toURIString(); - try (InputStream is = contrast.makeRequest(HttpMethod.GET, uri); - Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { - return gson.fromJson(reader, LibraryDetailsResponse.class); + final String url = + graphApiBase + + new URIBuilder() + .appendPathSegments( + "v2", + "organizations", + organizationId, + "contrast-graph", + "applications", + applicationId, + "libraries", + libraryHash) + .toURIString(); + return get(url, LibraryDetailsResponse.class); + } + + private T get(final String url, final Class type) throws IOException { + try (InputStream is = contrast.makeRequestToUrl(HttpMethod.GET, url)) { + return parse(is, type); + } + } + + private T post(final String url, final String body, final Class type) throws IOException { + try (InputStream is = contrast.makeRequestWithBodyToUrl(HttpMethod.POST, url, body, MediaType.JSON)) { + return parse(is, type); + } + } + + private T parse(final InputStream is, final Class type) throws IOException { + try (Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return gson.fromJson(reader, type); } catch (JsonParseException e) { throw new ServerResponseException("Failed to parse Contrast Graph API response", e); } diff --git a/sdk/src/main/java/com/contrastsecurity/utils/ContrastSDKUtils.java b/sdk/src/main/java/com/contrastsecurity/utils/ContrastSDKUtils.java index 061a187c..a2bd6c80 100644 --- a/sdk/src/main/java/com/contrastsecurity/utils/ContrastSDKUtils.java +++ b/sdk/src/main/java/com/contrastsecurity/utils/ContrastSDKUtils.java @@ -65,6 +65,19 @@ public static String ensureApi(String url) { return url; } + public static String getServerUrl(String url) { + if (url != null) { + if (url.endsWith("/Contrast/api")) { + return url.substring(0, url.length() - "/Contrast/api".length()); + } else if (url.endsWith("/Contrast/")) { + return url.substring(0, url.length() - "/Contrast/".length()); + } else if (url.endsWith("/Contrast")) { + return url.substring(0, url.length() - "/Contrast".length()); + } + } + return url; + } + public static String buildExpand(String... values) { if (values == null || values.length == 0) { return ""; diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java index e8c27c5d..606dacb8 100644 --- a/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java +++ b/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java @@ -21,12 +21,23 @@ */ import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.contrastsecurity.exceptions.HttpResponseException; import com.contrastsecurity.http.HttpMethod; +import com.contrastsecurity.http.MediaType; +import com.sun.net.httpserver.HttpServer; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.HttpURLConnection; +import java.net.InetSocketAddress; import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; final class ContrastSDKTest { @@ -116,4 +127,139 @@ void sets_user_agent_header() throws IOException { .matches( "INTELLIJ_INTEGRATION/1.0.0 contrast-sdk-java/\\d\\.\\d(\\.\\d)?(-SNAPSHOT)? Java/\\d+.*"); } + + @Nested + final class MakeRequestToUrl { + + private HttpServer server; + private String baseUrl; + + @BeforeEach + void before() throws IOException { + server = HttpServer.create(); + server.setExecutor(Executors.newSingleThreadExecutor()); + server.bind(new InetSocketAddress("localhost", 0), 0); + server.start(); + baseUrl = "http://localhost:" + server.getAddress().getPort(); + } + + @AfterEach + void after() { + server.stop(0); + } + + @Test + void returns_response_body_on_success() throws IOException { + server.createContext( + "/data", + exchange -> { + final byte[] body = "response-body".getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, body.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(body); + } + }); + + try (InputStream is = contrastSDK.makeRequestToUrl(HttpMethod.GET, baseUrl + "/data")) { + assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo("response-body"); + } + } + + @Test + void throws_on_error_response() { + server.createContext( + "/data", + exchange -> { + exchange.sendResponseHeaders(400, -1); + exchange.close(); + }); + + assertThatThrownBy( + () -> contrastSDK.makeRequestToUrl(HttpMethod.GET, baseUrl + "/data")) + .isInstanceOf(HttpResponseException.class); + } + } + + @Nested + final class MakeRequestWithBodyToUrl { + + private HttpServer server; + private String baseUrl; + + @BeforeEach + void before() throws IOException { + server = HttpServer.create(); + server.setExecutor(Executors.newSingleThreadExecutor()); + server.bind(new InetSocketAddress("localhost", 0), 0); + server.start(); + baseUrl = "http://localhost:" + server.getAddress().getPort(); + } + + @AfterEach + void after() { + server.stop(0); + } + + @Test + void sends_body_and_returns_response() throws IOException { + final String[] receivedBody = {null}; + server.createContext( + "/submit", + exchange -> { + receivedBody[0] = + new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + final byte[] response = "ok".getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, response.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response); + } + }); + + try (InputStream is = + contrastSDK.makeRequestWithBodyToUrl( + HttpMethod.POST, baseUrl + "/submit", "{\"key\":\"value\"}", MediaType.JSON)) { + assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo("ok"); + } + assertThat(receivedBody[0]).isEqualTo("{\"key\":\"value\"}"); + } + + @Test + void omits_body_when_null() throws IOException { + final String[] receivedBody = {null}; + server.createContext( + "/submit", + exchange -> { + receivedBody[0] = + new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + final byte[] response = "ok".getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, response.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response); + } + }); + + try (InputStream is = + contrastSDK.makeRequestWithBodyToUrl( + HttpMethod.POST, baseUrl + "/submit", null, MediaType.JSON)) { + assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo("ok"); + } + assertThat(receivedBody[0]).isEmpty(); + } + + @Test + void throws_on_error_response() { + server.createContext( + "/submit", + exchange -> { + exchange.sendResponseHeaders(400, -1); + exchange.close(); + }); + + assertThatThrownBy( + () -> + contrastSDK.makeRequestWithBodyToUrl( + HttpMethod.POST, baseUrl + "/submit", "{}", MediaType.JSON)) + .isInstanceOf(HttpResponseException.class); + } + } } diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java index 03d2a359..4c3c4b97 100644 --- a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java +++ b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java @@ -29,6 +29,7 @@ import au.com.dius.pact.core.model.RequestResponsePact; import au.com.dius.pact.core.model.annotations.Pact; import com.contrastsecurity.sdk.ContrastSDK; +import com.contrastsecurity.sdk.JSON; import com.google.gson.Gson; import java.io.IOException; import java.util.Collections; @@ -52,7 +53,7 @@ private ContrastGraphApiImpl client(final MockServer server) { new ContrastSDK.Builder("user", "serviceKey", "apiKey") .withApiUrl(server.getUrl() + "/Contrast/api") .build(); - return new ContrastGraphApiImpl(sdk, new Gson()); + return new ContrastGraphApiImpl(sdk, JSON.getGson()); } @Nested @@ -64,7 +65,7 @@ RequestResponsePact pact(final PactDslWithProvider builder) { .given("graph data exists for organization") .uponReceiving("search graph with filters") .method("POST") - .path("/Contrast/api/v2/organizations/" + ORG_ID + "/contrast-graph") + .path("/api/v2/organizations/" + ORG_ID + "/contrast-graph") .willRespondWith() .status(200) .body( @@ -120,7 +121,7 @@ RequestResponsePact pact(final PactDslWithProvider builder) { .uponReceiving("get incident graph") .method("GET") .path( - "/Contrast/api/v2/organizations/" + "/api/v2/organizations/" + ORG_ID + "/contrast-graph/incidents/" + INCIDENT_ID) @@ -162,7 +163,7 @@ RequestResponsePact pact(final PactDslWithProvider builder) { .given("graph data exists for organization") .uponReceiving("get nodeTypes facets") .method("POST") - .path("/Contrast/api/v2/organizations/" + ORG_ID + "/contrast-graph/facets/nodeTypes") + .path("/api/v2/organizations/" + ORG_ID + "/contrast-graph/facets/nodeTypes") .willRespondWith() .status(200) .body( @@ -200,7 +201,7 @@ RequestResponsePact pact(final PactDslWithProvider builder) { .uponReceiving("get application libraries without filter") .method("POST") .path( - "/Contrast/api/v2/organizations/" + "/api/v2/organizations/" + ORG_ID + "/contrast-graph/applications/" + APP_ID @@ -248,7 +249,7 @@ RequestResponsePact pact(final PactDslWithProvider builder) { .uponReceiving("get library details by hash") .method("GET") .path( - "/Contrast/api/v2/organizations/" + "/api/v2/organizations/" + ORG_ID + "/contrast-graph/applications/" + APP_ID diff --git a/sdk/src/test/java/com/contrastsecurity/utils/ContrastSDKUtilsTest.java b/sdk/src/test/java/com/contrastsecurity/utils/ContrastSDKUtilsTest.java index 7bb93622..91c5364c 100644 --- a/sdk/src/test/java/com/contrastsecurity/utils/ContrastSDKUtilsTest.java +++ b/sdk/src/test/java/com/contrastsecurity/utils/ContrastSDKUtilsTest.java @@ -62,4 +62,26 @@ public void ensure_api_handles_blank_string() { final String ensureBlank = ""; assertThat(blankUrl).isEqualTo(ensureBlank); } + + @Test + public void get_server_url_strips_contrast_path_variants() { + final String expected = "http://localhost:19080"; + assertThat(ContrastSDKUtils.getServerUrl("http://localhost:19080/Contrast/api")) + .isEqualTo(expected); + assertThat(ContrastSDKUtils.getServerUrl("http://localhost:19080/Contrast/")) + .isEqualTo(expected); + assertThat(ContrastSDKUtils.getServerUrl("http://localhost:19080/Contrast")) + .isEqualTo(expected); + } + + @Test + public void get_server_url_leaves_plain_host_unchanged() { + final String plainHost = "http://localhost:19080"; + assertThat(ContrastSDKUtils.getServerUrl(plainHost)).isEqualTo(plainHost); + } + + @Test + public void get_server_url_ignores_null() { + assertThat(ContrastSDKUtils.getServerUrl(null)).isNull(); + } } From b8c14a534eb843e3a8f7390b2a0cedcf1c210e5e Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Wed, 10 Jun 2026 16:45:12 -0400 Subject: [PATCH 15/16] Fix formatting. --- .../java/com/contrastsecurity/sdk/JSON.java | 20 ++----------------- .../sdk/graph/ContrastGraphApiImpl.java | 13 +++++------- .../contrastsecurity/sdk/ContrastSDKTest.java | 6 +++--- .../graph/ContrastGraphApiFactoryTest.java | 4 ++-- .../graph/ContrastGraphApiImplPactTest.java | 8 ++------ 5 files changed, 14 insertions(+), 37 deletions(-) diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java b/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java index 02a91c8f..6cf8c4f2 100644 --- a/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/JSON.java @@ -1,21 +1,3 @@ -/* - * Copyright 2022 Contrast Security, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.contrastsecurity.sdk; - /*- * #%L * Contrast Java SDK @@ -36,6 +18,8 @@ * #L% */ +package com.contrastsecurity.sdk; + import com.contrastsecurity.sdk.internal.GsonFactory; import com.google.gson.Gson; diff --git a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java index 1b85be48..cc960239 100644 --- a/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java +++ b/sdk/src/main/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImpl.java @@ -81,12 +81,7 @@ public FacetsResponse getFacets( graphApiBase + new URIBuilder() .appendPathSegments( - "v2", - "organizations", - organizationId, - "contrast-graph", - "facets", - filterName) + "v2", "organizations", organizationId, "contrast-graph", "facets", filterName) .toURIString(); return post(url, gson.toJson(filters), FacetsResponse.class); } @@ -111,7 +106,8 @@ public ApplicationLibrariesResponse getApplicationLibraries( "libraries") .appendQueryParam("agentReportingInstanceId", agentReportingInstanceId) .toURIString(); - return post(url, request != null ? gson.toJson(request) : null, ApplicationLibrariesResponse.class); + return post( + url, request != null ? gson.toJson(request) : null, ApplicationLibrariesResponse.class); } @Override @@ -141,7 +137,8 @@ private T get(final String url, final Class type) throws IOException { } private T post(final String url, final String body, final Class type) throws IOException { - try (InputStream is = contrast.makeRequestWithBodyToUrl(HttpMethod.POST, url, body, MediaType.JSON)) { + try (InputStream is = + contrast.makeRequestWithBodyToUrl(HttpMethod.POST, url, body, MediaType.JSON)) { return parse(is, type); } } diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java index 606dacb8..40d56a7a 100644 --- a/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java +++ b/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java @@ -161,7 +161,8 @@ void returns_response_body_on_success() throws IOException { }); try (InputStream is = contrastSDK.makeRequestToUrl(HttpMethod.GET, baseUrl + "/data")) { - assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo("response-body"); + assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8)) + .isEqualTo("response-body"); } } @@ -174,8 +175,7 @@ void throws_on_error_response() { exchange.close(); }); - assertThatThrownBy( - () -> contrastSDK.makeRequestToUrl(HttpMethod.GET, baseUrl + "/data")) + assertThatThrownBy(() -> contrastSDK.makeRequestToUrl(HttpMethod.GET, baseUrl + "/data")) .isInstanceOf(HttpResponseException.class); } } diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java index 785663c3..234da616 100644 --- a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java +++ b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiFactoryTest.java @@ -1,5 +1,3 @@ -package com.contrastsecurity.sdk.graph; - /*- * #%L * Contrast Java SDK @@ -20,6 +18,8 @@ * #L% */ +package com.contrastsecurity.sdk.graph; + import static org.assertj.core.api.Assertions.assertThat; import com.contrastsecurity.sdk.ContrastSDK; diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java index 4c3c4b97..e40734a0 100644 --- a/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java +++ b/sdk/src/test/java/com/contrastsecurity/sdk/graph/ContrastGraphApiImplPactTest.java @@ -17,6 +17,7 @@ * limitations under the License. * #L% */ + package com.contrastsecurity.sdk.graph; import static au.com.dius.pact.consumer.dsl.LambdaDsl.newJsonBody; @@ -30,7 +31,6 @@ import au.com.dius.pact.core.model.annotations.Pact; import com.contrastsecurity.sdk.ContrastSDK; import com.contrastsecurity.sdk.JSON; -import com.google.gson.Gson; import java.io.IOException; import java.util.Collections; import org.junit.jupiter.api.Nested; @@ -120,11 +120,7 @@ RequestResponsePact pact(final PactDslWithProvider builder) { .given("incident exists", Collections.singletonMap("incidentId", INCIDENT_ID)) .uponReceiving("get incident graph") .method("GET") - .path( - "/api/v2/organizations/" - + ORG_ID - + "/contrast-graph/incidents/" - + INCIDENT_ID) + .path("/api/v2/organizations/" + ORG_ID + "/contrast-graph/incidents/" + INCIDENT_ID) .willRespondWith() .status(200) .body( From 02ff570caad2203ba616d018c39bb993578af802 Mon Sep 17 00:00:00 2001 From: "John D. Ament" Date: Wed, 10 Jun 2026 18:19:12 -0400 Subject: [PATCH 16/16] Downgrade to Java 8 compatibility. --- .../contrastsecurity/sdk/ContrastSDKTest.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java b/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java index 40d56a7a..45c58d8c 100644 --- a/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java +++ b/sdk/src/test/java/com/contrastsecurity/sdk/ContrastSDKTest.java @@ -27,6 +27,7 @@ import com.contrastsecurity.http.HttpMethod; import com.contrastsecurity.http.MediaType; import com.sun.net.httpserver.HttpServer; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -128,6 +129,16 @@ void sets_user_agent_header() throws IOException { "INTELLIJ_INTEGRATION/1.0.0 contrast-sdk-java/\\d\\.\\d(\\.\\d)?(-SNAPSHOT)? Java/\\d+.*"); } + private static String readString(final InputStream is) throws IOException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final byte[] buf = new byte[1024]; + int n; + while ((n = is.read(buf)) != -1) { + baos.write(buf, 0, n); + } + return baos.toString(StandardCharsets.UTF_8.name()); + } + @Nested final class MakeRequestToUrl { @@ -161,8 +172,7 @@ void returns_response_body_on_success() throws IOException { }); try (InputStream is = contrastSDK.makeRequestToUrl(HttpMethod.GET, baseUrl + "/data")) { - assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8)) - .isEqualTo("response-body"); + assertThat(readString(is)).isEqualTo("response-body"); } } @@ -206,8 +216,7 @@ void sends_body_and_returns_response() throws IOException { server.createContext( "/submit", exchange -> { - receivedBody[0] = - new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + receivedBody[0] = readString(exchange.getRequestBody()); final byte[] response = "ok".getBytes(StandardCharsets.UTF_8); exchange.sendResponseHeaders(200, response.length); try (OutputStream os = exchange.getResponseBody()) { @@ -218,7 +227,7 @@ void sends_body_and_returns_response() throws IOException { try (InputStream is = contrastSDK.makeRequestWithBodyToUrl( HttpMethod.POST, baseUrl + "/submit", "{\"key\":\"value\"}", MediaType.JSON)) { - assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo("ok"); + assertThat(readString(is)).isEqualTo("ok"); } assertThat(receivedBody[0]).isEqualTo("{\"key\":\"value\"}"); } @@ -229,8 +238,7 @@ void omits_body_when_null() throws IOException { server.createContext( "/submit", exchange -> { - receivedBody[0] = - new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + receivedBody[0] = readString(exchange.getRequestBody()); final byte[] response = "ok".getBytes(StandardCharsets.UTF_8); exchange.sendResponseHeaders(200, response.length); try (OutputStream os = exchange.getResponseBody()) { @@ -241,7 +249,7 @@ void omits_body_when_null() throws IOException { try (InputStream is = contrastSDK.makeRequestWithBodyToUrl( HttpMethod.POST, baseUrl + "/submit", null, MediaType.JSON)) { - assertThat(new String(is.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo("ok"); + assertThat(readString(is)).isEqualTo("ok"); } assertThat(receivedBody[0]).isEmpty(); }