Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BoM reporting / blackduck-common #409

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -9,6 +9,7 @@ bin/
*.iml
.idea/
out/
.vscode

# build tool
.gradle/
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Expand Up @@ -7,7 +7,7 @@ project.ext.moduleName = 'com.synopsys.integration.blackduck-common'
project.ext.javaUseAutoModuleName = 'true'
project.ext.junitShowStandardStreams = 'true'

version = '66.2.8-SNAPSHOT'
version = '66.2.9-SNAPSHOT'

description = 'A library for using various capabilities of Black Duck, notably the REST API and signature scanning.'

Expand All @@ -23,7 +23,7 @@ repositories {
}

dependencies {
api 'com.synopsys.integration:blackduck-common-api:2023.4.2.2'
api 'com.synopsys.integration:blackduck-common-api:2023.4.2.3-SNAPSHOT'
api 'com.synopsys.integration:phone-home-client:5.1.10'
api 'com.synopsys.integration:integration-bdio:26.0.9'
api 'com.blackducksoftware.bdio:bdio2:3.2.5'
Expand Down
Expand Up @@ -166,6 +166,10 @@ public Response execute(BlackDuckResponseRequest request) throws IntegrationExce
blackDuckHttpClient.throwExceptionForError(response);
return response;
}

public Response executeAndRetrieveResponse(BlackDuckResponseRequest request) throws IntegrationException {
return blackDuckHttpClient.execute(request);
}

// ------------------------------------------------
// posting and getting location header
Expand Down
Expand Up @@ -51,6 +51,7 @@
import com.synopsys.integration.blackduck.service.dataservice.NotificationService;
import com.synopsys.integration.blackduck.service.dataservice.PolicyRuleService;
import com.synopsys.integration.blackduck.service.dataservice.ProjectBomService;
import com.synopsys.integration.blackduck.service.dataservice.ReportBomService;
import com.synopsys.integration.blackduck.service.dataservice.ProjectGetService;
import com.synopsys.integration.blackduck.service.dataservice.ProjectMappingService;
import com.synopsys.integration.blackduck.service.dataservice.ProjectService;
Expand Down Expand Up @@ -211,6 +212,10 @@ public ProjectBomService createProjectBomService() {
return new ProjectBomService(blackDuckApiClient, apiDiscovery, logger, createComponentService());
}

public ReportBomService createReportBomService() {
return new ReportBomService(this.getBlackDuckApiClient(), this.getApiDiscovery(), this.getLogger());
}

public ProjectUsersService createProjectUsersService() {
UserGroupService userGroupService = createUserGroupService();
return new ProjectUsersService(blackDuckApiClient, apiDiscovery, logger, userGroupService);
Expand Down
@@ -0,0 +1,228 @@
/*
* blackduck-common
*
* Copyright (c) 2023 Synopsys, Inc.
* Copyright (c) 2023 Jens Nachtigall
*
* Use subject to the terms and conditions of the Synopsys End User Software License and Maintenance Agreement. All rights reserved worldwide.
*/
package com.synopsys.integration.blackduck.service.dataservice;

import java.util.List;

import com.synopsys.integration.blackduck.api.generated.discovery.ApiDiscovery;
import com.synopsys.integration.blackduck.api.generated.view.ProjectVersionView;
import com.synopsys.integration.blackduck.api.manual.view.ReportBomView;
import com.synopsys.integration.blackduck.api.manual.view.ReportBomContentView;
import com.synopsys.integration.blackduck.api.manual.component.ReportBomRequest;
import com.synopsys.integration.blackduck.service.BlackDuckApiClient;
import com.synopsys.integration.blackduck.service.DataService;
import com.synopsys.integration.blackduck.service.model.ProjectVersionWrapper;
import com.synopsys.integration.exception.IntegrationException;
import com.synopsys.integration.exception.IntegrationTimeoutException;
import com.synopsys.integration.log.IntLogger;
import com.synopsys.integration.rest.HttpUrl;
import com.synopsys.integration.wait.ResilientJob;
import com.synopsys.integration.wait.ResilientJobConfig;
import com.synopsys.integration.wait.ResilientJobExecutor;
import com.synopsys.integration.wait.tracker.WaitIntervalTracker;
import com.synopsys.integration.wait.tracker.WaitIntervalTrackerFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Blackduck API Data Service implementation to both, request and download Bill of Materials
* reports from Black Duck Hub when it eventually becomes available.
*/
public class ReportBomService extends DataService {

private Logger log = LoggerFactory.getLogger(ReportBomService.class);
private static final int BD_WAIT_AND_RETRY_INTERVAL = 5;

// Internal class to validate user input arugments.
private static class BomRequestValidator {

// as per REST API Docs
private static List<String> acceptableFormat = List.of("JSON", "RDF", "TAGVALUE", "YAML");
private static List<String> acceptableType = List.of("SPDX_22", "CYCLONEDX_13", "CYCLONEDX_14");

/**
* Validate a user specified format string using acceptableFormat
* @param format The format string to validate
* @return The uppercase format string if valid.
* @throws IllegalArgumentException if tis is not the case
*/
public static String validateFormat(final String format) throws IllegalArgumentException{
if (acceptableFormat.contains(format.toUpperCase())) {
return format.toUpperCase();
}
throw new IllegalArgumentException("Bom Format " + format + "is not among the valid ones: " + String.join(",", acceptableFormat));
}

/**
* Validate a user specified type string using acceptableType
* @param format The type string to validate
* @return The uppercase format string if valid.
* @throws IllegalArgumentException if tis is not the case
*/
public static String validateType(final String type) throws IllegalArgumentException{
if (acceptableType.contains(type.toUpperCase())) {
return type.toUpperCase();
}
throw new IllegalArgumentException("Bom Format " + type + "is not among the valid ones: " + String.join(",", acceptableType));
}
}

/**
* Internal download task for use with the ResilientJobExecutor of the Blackduck API,
* supporting interrution, timeout and retry count.
*
* Attempts to download a Bill of Material by url/uuid when it becomes avaialble
* and maps to a ReportBomView, respectively.
*/
private static class BomDownloadJob implements ResilientJob<ReportBomView> {
private BlackDuckApiClient blackDuckApiClient;
private String jobName;
private boolean complete;
private HttpUrl uri;
private ReportBomView bomReport;

/**
* Constructor
* @param blackDuckApiClient An initialized BD API client (which has halready handled OAuth)
* @param jobName An arbitrary job name (only used for external logging)
* @param uri The reports URI as returned from the report creation request
*/
public BomDownloadJob(BlackDuckApiClient blackDuckApiClient, String jobName, HttpUrl uri) {
this.blackDuckApiClient = blackDuckApiClient;
this.jobName = jobName;
this.uri = uri;
this.complete = false;
}

@Override
public void attemptJob() throws IntegrationException {
try {
// Wait while HTTP 412 Precondition failed is returned.
// for some reason, there will always be a JSON array in the response.
bomReport = blackDuckApiClient.getResponse(uri.appendRelativeUrl("contents"), ReportBomView.class);
complete = true;
} catch (IntegrationException e) {
complete = false;
}
}

@Override
public boolean wasJobCompleted() {
return complete;
}

@Override
public ReportBomView onTimeout() throws IntegrationTimeoutException {
throw new IntegrationTimeoutException("Not able to upload BDIO due to timeout.");
}

@Override
public ReportBomView onCompletion() {
return bomReport;
}

@Override
public String getName() {
return this.jobName;
}

}

/**
* Constuctor of the BOM Data Service
* @param blackDuckApiClient An initialized BD API client (which has halready handled OAuth)
* @param apiDiscovery For the superclass of a Blackduck DataService
* @param logger For unified logging
*/
public ReportBomService(BlackDuckApiClient blackDuckApiClient, ApiDiscovery apiDiscovery, IntLogger logger) {
super(blackDuckApiClient, apiDiscovery, logger);
}

/**
* Sets up a valid Bom report request to the BDH RESET API (e.g. the POST Payload)
* @param type The BOM type, e.g. Cyclone or SPDX, @see BomRequestValidator.acceptableType
* @param format The BOM format, e.g. JSON, tag:value, @see BomRequestValidator.acceptableFormat
* @return The request obect
* @throws IllegalArgumentException
*/
public ReportBomRequest createRequest(String type, String format) throws IllegalArgumentException{
ReportBomRequest request = new ReportBomRequest();
request.setReportType("SBOM"); // SBOM - static, optional?
request.setReportFormat(BomRequestValidator.validateFormat(format).toUpperCase()); //JSON
request.setSbomType(BomRequestValidator.validateType(type).toUpperCase()); // SPDX_22
return request;
}

/**
* Request a Bom report creation on the BDH
* @param projectVersion Project version response (wraped as View) carrying project and version uuid, respectively.
* @param reportRequest A populated / configured request
* @return An URL to the scheduled report including the uuid of the report.
* @throws IntegrationException
*/
public HttpUrl createReport(ProjectVersionView projectVersion, ReportBomRequest reportRequest) throws IntegrationException {
// This is merely queing the report; it will be available upon completion
HttpUrl versionUrl = projectVersion.getHref();
log.info("Project Version URL for " + projectVersion.getVersionName() + ": " + versionUrl.toString());

// The request returns an empty response with HTTP 201 Created and an attribute "Link" in the reponse header.
// Coincidentially, this is exactly what is returned by post().
HttpUrl reportUrl = blackDuckApiClient.post(
versionUrl.appendRelativeUrl("sbom-reports"), reportRequest);

log.info("Report available from: " + reportUrl.toString());

return reportUrl;
}

/**
* Request a Bom report creation on the BDH
* @param wrapper A wrapped View response including project and version uuid.
* @param reportRequest A populated / configured request
* @return An URL to the scheduled report including the uuid of the report.
* @throws IntegrationException
*/
public HttpUrl createReport(ProjectVersionWrapper wrapper, ReportBomRequest reportRequest) throws IntegrationException {
// This is merely queing the report creation
return createReport(wrapper.getProjectVersionView(), reportRequest);
}

/**
* Await a Bom creation and download the report based on the reportUrl (@see createReport)
* @param reportUrl The URI identifying the report with it's uuid
* @param timeout Timeout in seconds
* @return
* @throws IntegrationException Something failed along the way (see stacktrace)
* @throws InterruptedException Interrupted by user
*/
public ReportBomView downloadReports(HttpUrl reportUrl, long timeout) throws IntegrationException, InterruptedException {

WaitIntervalTracker waitIntervalTracker = WaitIntervalTrackerFactory.createConstant(timeout, BD_WAIT_AND_RETRY_INTERVAL);
ResilientJobConfig jobConfig = new ResilientJobConfig(logger, System.currentTimeMillis(), waitIntervalTracker);
BomDownloadJob bomDownloadJob = new BomDownloadJob(blackDuckApiClient, "Awaiting Bom report completion", reportUrl);
ResilientJobExecutor jobExecutor = new ResilientJobExecutor(jobConfig);

return jobExecutor.executeJob(bomDownloadJob);
}

/**
* Await a Bom creation and download the report based on the reportUrl (@see createReport)
* @param reportUrl The URI identifying the report with it's uuid
* @param timeout Timeout in seconds
* @param log For unified logging
* @return
* @throws IntegrationException Something failed along the way (see stacktrace)
* @throws InterruptedException Interrupted by user
*/
public ReportBomView downloadReports(HttpUrl reportUrl, long timeout, Logger log) throws IntegrationException, InterruptedException {
this.log = log;
return downloadReports(reportUrl, timeout);
}
}