In today's fast-paced software development landscape, managing open-source dependencies is no longer a convenience but a critical necessity. While tools like Sonatype's Nexus Repository Manager help store and proxy components, Sonatype Nexus IQ Server takes it a step further by proactively identifying security vulnerabilities, license violations, and architectural policies in your applications. This article provides a practical guide for Java developers to programmatically integrate with Nexus IQ Server, enabling automated policy checks within CI/CD pipelines and custom tooling.
Understanding the Core Concepts
Before diving into the code, it's essential to understand two key concepts:
- Application ID (appId): A unique identifier for your application within IQ Server (e.g.,
myAwesomeApp). - Organization: The Nexus IQ organization under which the application is registered.
- Policy Evaluation: The process where IQ Server scans your application's bill of materials (BOM) against its defined policies and returns a detailed report with an overall threat level (e.g.,
None,Low,Medium,High).
The integration workflow typically involves:
- Triggering a Scan: Submitting your application's dependencies to IQ Server.
- Polling for Results: Checking the status of the evaluation until it is complete.
- Interpreting the Report: Parsing the results to fail a build or trigger alerts based on policy violations.
Java Integration in Practice
We can interact with the Nexus IQ Server REST API using any HTTP client. Here, we'll use the popular OkHttp library for its simplicity and power.
Step 1: Add Dependencies
First, include the necessary dependencies in your pom.xml (Maven) or build.gradle (Gradle).
Maven:
<dependencies> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.12.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.16.1</version> </dependency> </dependencies>
Step 2: The Core Integration Class
The following NexusIqClient class encapsulates the logic for authenticating, submitting a scan, and retrieving the policy evaluation report.
import okhttp3.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.util.Base64;
public class NexusIqClient {
private final OkHttpClient httpClient;
private final ObjectMapper mapper;
private final String baseUrl;
private final String username;
private final String password;
private final String authHeader;
public NexusIqClient(String baseUrl, String username, String password) {
this.httpClient = new OkHttpClient();
this.mapper = new ObjectMapper();
this.baseUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
this.username = username;
this.password = password;
// Create Basic Auth header
String credentials = username + ":" + password;
this.authHeader = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes());
}
/**
* Submits a CycloneDX or ScanCode JSON BOM file for policy evaluation.
*
* @param applicationId The target Nexus IQ Application ID.
* @param bomFile The BOM file to scan.
* @return The URL for the policy evaluation report.
* @throws IOException If the API call fails.
*/
public String submitScan(String applicationId, File bomFile) throws IOException {
// 1. Request the internal ID for the application
String internalAppId = getInternalApplicationId(applicationId);
// 2. Build the request to submit the BOM
RequestBody fileBody = RequestBody.create(MediaType.parse("application/json"), bomFile);
RequestBody multipartBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", bomFile.getName(), fileBody)
.build();
String submitUrl = baseUrl + "api/v2/scan/applications/" + internalAppId + "/sources/ci";
Request request = new Request.Builder()
.url(submitUrl)
.header("Authorization", authHeader)
.post(multipartBody)
.build();
// 3. Execute the request and get the status URL
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response + ", Body: " + response.body().string());
}
String responseBody = response.body().string();
JsonNode rootNode = mapper.readTree(responseBody);
return rootNode.path("statusUrl").asText();
}
}
/**
* Polls the status URL until the evaluation is complete and returns the final report.
*
* @param statusUrl The status URL from the submitScan response.
* @return The Policy Evaluation Report object.
* @ throws IOException If API calls fail.
* @throws InterruptedException If polling is interrupted.
*/
public PolicyEvaluation getPolicyEvaluation(String statusUrl) throws IOException, InterruptedException {
String reportUrl = null;
JsonNode statusNode;
// Poll until the evaluation is finished
do {
Thread.sleep(2000); // Wait 2 seconds between polls
Request request = new Request.Builder()
.url(baseUrl + statusUrl) // statusUrl is a relative path
.header("Authorization", authHeader)
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Failed to get status: " + response);
}
String responseBody = response.body().string();
statusNode = mapper.readTree(responseBody);
}
reportUrl = statusNode.path("reportHtmlUrl").asText(null);
} while (reportUrl == null); // 'reportHtmlUrl' is populated when done
// 4. Get the final policy evaluation result
String policyAction = statusNode.path("policyAction").asText();
int affectedComponentCount = statusNode.path("affectedComponentCount").asInt();
return new PolicyEvaluation(policyAction, affectedComponentCount, reportUrl);
}
/**
* Helper method to get the internal ID of an application from its public ID.
*/
private String getInternalApplicationId(String publicAppId) throws IOException {
HttpUrl url = HttpUrl.parse(baseUrl + "api/v2/applications").newBuilder()
.addQueryParameter("publicId", publicAppId)
.build();
Request request = new Request.Builder()
.url(url)
.header("Authorization", authHeader)
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Failed to find application: " + response);
}
String responseBody = response.body().string();
JsonNode apps = mapper.readTree(responseBody);
if (apps.isArray() && apps.size() > 0) {
return apps.get(0).path("id").asText();
} else {
throw new IOException("Application with publicId '" + publicAppId + "' not found.");
}
}
}
// Simple Data Class to hold the result
public static class PolicyEvaluation {
public final String policyAction;
public final int affectedComponentCount;
public final String reportUrl;
public PolicyEvaluation(String policyAction, int affectedComponentCount, String reportUrl) {
this.policyAction = policyAction;
this.affectedComponentCount = affectedComponentCount;
this.reportUrl = reportUrl;
}
}
}
Step 3: Using the Client in Your Application
Here is how you would use the client in a CI/CD script or a custom analysis tool.
public class IqScanExample {
public static void main(String[] args) {
String iqUrl = "http://your-iq-server:8070/";
String username = "your_username";
String password = "your_password";
String appId = "myJavaApp";
File bomFile = new File("./target/bom.json"); // e.g., from CycloneDX Maven plugin
NexusIqClient client = new NexusIqClient(iqUrl, username, password);
try {
// 1. Submit the BOM for scanning
System.out.println("Submitting BOM for analysis...");
String statusUrl = client.submitScan(appId, bomFile);
// 2. Poll for the result
System.out.println("Waiting for policy evaluation...");
NexusIqClient.PolicyEvaluation evaluation = client.getPolicyEvaluation(statusUrl);
// 3. Act on the result
System.out.println("Policy Action: " + evaluation.policyAction);
System.out.println("Affected Components: " + evaluation.affectedComponentCount);
System.out.println("Full Report: " + evaluation.reportUrl);
// Fail the build if policy is violated
if ("Failure".equalsIgnoreCase(evaluation.policyAction)) {
System.err.println("!!! POLICY VIOLATION DETECTED !!!");
System.exit(1); // Fail the build
} else {
System.out.println("Policy check passed.");
}
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
Generating the BOM File
For this integration to work, you need a Bill of Materials (BOM) file. You can generate one easily using tools like the CycloneDX Maven Plugin:
Add to your pom.xml:
<build> <plugins> <plugin> <groupId>org.cyclonedx</groupId> <artifactId>cyclonedx-maven-plugin</artifactId> <version>2.7.11</version> <executions> <execution> <phase>package</phase> <goals> <goal>makeAggregateBom</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
Run mvn clean package, and you will find a bom.json file in your target directory, which you can then submit using the client above.
Conclusion
Integrating Nexus IQ Server directly into your Java applications and build pipelines empowers you to shift security left, catching issues early in the development lifecycle. By leveraging its robust REST API with a simple HTTP client, you can automate policy enforcement, ensuring that only compliant and secure software progresses to production. This programmatic approach is the cornerstone of a modern, secure, and efficient DevSecOps practice.